@crouton-kit/crouter 0.3.13 → 0.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-root.d.ts +8 -0
- package/dist/build-root.js +30 -0
- package/dist/builtin-personas/design/base.md +3 -7
- package/dist/builtin-personas/design/orchestrator.md +4 -3
- package/dist/builtin-personas/developer/base.md +3 -7
- package/dist/builtin-personas/developer/orchestrator.md +5 -4
- package/dist/builtin-personas/explore/base.md +3 -7
- package/dist/builtin-personas/explore/orchestrator.md +1 -5
- package/dist/builtin-personas/general/base.md +2 -4
- package/dist/builtin-personas/general/orchestrator.md +2 -4
- package/dist/builtin-personas/lifecycle/resident.md +2 -0
- package/dist/builtin-personas/lifecycle/terminal.md +6 -0
- package/dist/builtin-personas/orchestration-kernel.md +42 -3
- package/dist/builtin-personas/plan/base.md +3 -5
- package/dist/builtin-personas/plan/orchestrator.md +5 -4
- package/dist/builtin-personas/plan/reviewers/architecture-fit/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/code-smells/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/security/base.md +9 -0
- package/dist/builtin-personas/review/base.md +3 -5
- package/dist/builtin-personas/review/orchestrator.md +2 -6
- package/dist/builtin-personas/runtime-base.md +3 -19
- package/dist/builtin-personas/spec/base.md +3 -5
- package/dist/builtin-personas/spec/orchestrator.md +4 -3
- package/dist/builtin-personas/spine/has-manager.md +10 -0
- package/dist/builtin-personas/spine/no-manager.md +2 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +96 -0
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +1 -1
- package/dist/builtin-skills/skills/spec/SKILL.md +2 -2
- package/dist/cli.js +6 -29
- package/dist/commands/__tests__/human.test.js +73 -2
- package/dist/commands/attention.js +76 -7
- package/dist/commands/canvas-prune.d.ts +2 -0
- package/dist/commands/canvas-prune.js +66 -0
- package/dist/commands/canvas.js +5 -8
- package/dist/commands/chord.d.ts +2 -0
- package/dist/commands/chord.js +143 -0
- package/dist/commands/daemon.js +8 -5
- package/dist/commands/dashboard.js +2 -0
- package/dist/commands/human/prompts.js +28 -27
- package/dist/commands/human/queue.d.ts +1 -0
- package/dist/commands/human/queue.js +105 -2
- package/dist/commands/human/shared.d.ts +28 -18
- package/dist/commands/human/shared.js +53 -60
- package/dist/commands/human.js +6 -14
- package/dist/commands/node.d.ts +11 -0
- package/dist/commands/node.js +381 -87
- package/dist/commands/pkg/market-inspect.js +6 -4
- package/dist/commands/pkg/market-manage.js +10 -6
- package/dist/commands/pkg/market.js +2 -4
- package/dist/commands/pkg/plugin-inspect.js +6 -4
- package/dist/commands/pkg/plugin-manage.js +12 -7
- package/dist/commands/pkg/plugin.js +2 -4
- package/dist/commands/pkg.js +0 -4
- package/dist/commands/push.js +178 -15
- package/dist/commands/revive.js +5 -3
- package/dist/commands/skill/author.js +6 -4
- package/dist/commands/skill/find.js +8 -5
- package/dist/commands/skill/read.js +2 -0
- package/dist/commands/skill/state.js +6 -4
- package/dist/commands/skill.js +0 -6
- package/dist/commands/sys/config.js +21 -7
- package/dist/commands/sys/doctor.js +2 -0
- package/dist/commands/sys/update.js +4 -0
- package/dist/commands/sys.js +0 -6
- package/dist/commands/tmux-spread.d.ts +2 -0
- package/dist/commands/tmux-spread.js +130 -0
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
- package/dist/core/__tests__/child-followup.test.d.ts +1 -0
- package/dist/core/__tests__/child-followup.test.js +83 -0
- package/dist/core/__tests__/close.test.d.ts +1 -0
- package/dist/core/__tests__/close.test.js +148 -0
- package/dist/core/__tests__/context-intro.test.d.ts +1 -0
- package/dist/core/__tests__/context-intro.test.js +196 -0
- package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-boot.test.js +93 -0
- package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-liveness.test.js +223 -0
- package/dist/core/__tests__/focuses.test.d.ts +1 -0
- package/dist/core/__tests__/focuses.test.js +259 -0
- package/dist/core/__tests__/fork.test.d.ts +1 -0
- package/dist/core/__tests__/fork.test.js +91 -0
- package/dist/core/__tests__/home-session.test.d.ts +1 -0
- package/dist/core/__tests__/home-session.test.js +153 -0
- package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
- package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
- package/dist/core/__tests__/keystone.test.d.ts +1 -0
- package/dist/core/__tests__/keystone.test.js +185 -0
- package/dist/core/__tests__/kickoff.test.d.ts +1 -0
- package/dist/core/__tests__/kickoff.test.js +89 -0
- package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/lifecycle.test.js +178 -0
- package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
- package/dist/core/__tests__/listing-completeness.test.js +31 -0
- package/dist/core/__tests__/memory.test.d.ts +1 -0
- package/dist/core/__tests__/memory.test.js +152 -0
- package/dist/core/__tests__/migration.test.d.ts +1 -0
- package/dist/core/__tests__/migration.test.js +238 -0
- package/dist/core/__tests__/pane-column.test.d.ts +1 -0
- package/dist/core/__tests__/pane-column.test.js +153 -0
- package/dist/core/__tests__/passive-subscription.test.d.ts +1 -0
- package/dist/core/__tests__/passive-subscription.test.js +164 -0
- package/dist/core/__tests__/persona-compose.test.d.ts +1 -0
- package/dist/core/__tests__/persona-compose.test.js +53 -0
- package/dist/core/__tests__/persona-subkind.test.d.ts +1 -0
- package/dist/core/__tests__/persona-subkind.test.js +62 -0
- package/dist/core/__tests__/persona.test.d.ts +1 -0
- package/dist/core/__tests__/persona.test.js +107 -0
- package/dist/core/__tests__/placement-focus.test.d.ts +1 -0
- package/dist/core/__tests__/placement-focus.test.js +244 -0
- package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
- package/dist/core/__tests__/placement-reconcile.test.js +212 -0
- package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
- package/dist/core/__tests__/placement-revive.test.js +238 -0
- package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/placement-teardown.test.js +183 -0
- package/dist/core/__tests__/prune.test.d.ts +1 -0
- package/dist/core/__tests__/prune.test.js +116 -0
- package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
- package/dist/core/__tests__/push-final-guard.test.js +71 -0
- package/dist/core/__tests__/relaunch.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch.test.js +328 -0
- package/dist/core/__tests__/reset.test.js +26 -7
- package/dist/core/__tests__/revive.test.d.ts +1 -0
- package/dist/core/__tests__/revive.test.js +217 -0
- package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
- package/dist/core/__tests__/spawn-root.test.js +73 -0
- package/dist/core/__tests__/steer-note.test.d.ts +1 -0
- package/dist/core/__tests__/steer-note.test.js +39 -0
- package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
- package/dist/core/__tests__/stop-guard.test.js +82 -0
- package/dist/core/__tests__/subcommand-tier.test.d.ts +1 -0
- package/dist/core/__tests__/subcommand-tier.test.js +99 -0
- package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
- package/dist/core/__tests__/tmux-surface.test.js +106 -0
- package/dist/core/__tests__/unknown-path.test.js +8 -2
- package/dist/core/canvas/attention.d.ts +10 -0
- package/dist/core/canvas/attention.js +40 -0
- package/dist/core/canvas/canvas.d.ts +66 -7
- package/dist/core/canvas/canvas.js +209 -21
- package/dist/core/canvas/db.d.ts +8 -0
- package/dist/core/canvas/db.js +206 -4
- package/dist/core/canvas/focuses.d.ts +22 -0
- package/dist/core/canvas/focuses.js +80 -0
- package/dist/core/canvas/index.d.ts +3 -0
- package/dist/core/canvas/index.js +3 -0
- package/dist/core/canvas/labels.d.ts +27 -0
- package/dist/core/canvas/labels.js +36 -0
- package/dist/core/canvas/paths.d.ts +4 -0
- package/dist/core/canvas/paths.js +6 -0
- package/dist/core/canvas/render.js +25 -10
- package/dist/core/canvas/telemetry.d.ts +14 -0
- package/dist/core/canvas/telemetry.js +35 -0
- package/dist/core/canvas/types.d.ts +115 -12
- package/dist/core/command.d.ts +25 -1
- package/dist/core/command.js +48 -7
- package/dist/core/config.js +36 -2
- package/dist/core/feed/feed.js +14 -12
- package/dist/core/feed/inbox.d.ts +3 -1
- package/dist/core/feed/inbox.js +45 -5
- package/dist/core/feed/passive.d.ts +17 -0
- package/dist/core/feed/passive.js +92 -0
- package/dist/core/help.d.ts +59 -13
- package/dist/core/help.js +73 -28
- package/dist/core/personas/index.d.ts +1 -1
- package/dist/core/personas/index.js +1 -1
- package/dist/core/personas/loader.d.ts +40 -1
- package/dist/core/personas/loader.js +63 -1
- package/dist/core/personas/resolve.d.ts +13 -6
- package/dist/core/personas/resolve.js +46 -34
- package/dist/core/runtime/bearings.d.ts +20 -0
- package/dist/core/runtime/bearings.js +92 -0
- package/dist/core/runtime/close.d.ts +14 -0
- package/dist/core/runtime/close.js +151 -0
- package/dist/core/runtime/demote.d.ts +14 -0
- package/dist/core/runtime/demote.js +120 -0
- package/dist/core/runtime/front-door.js +1 -1
- package/dist/core/runtime/kickoff.d.ts +32 -6
- package/dist/core/runtime/kickoff.js +111 -37
- package/dist/core/runtime/launch.d.ts +29 -6
- package/dist/core/runtime/launch.js +85 -13
- package/dist/core/runtime/lifecycle.d.ts +13 -0
- package/dist/core/runtime/lifecycle.js +86 -0
- package/dist/core/runtime/memory.d.ts +43 -0
- package/dist/core/runtime/memory.js +165 -0
- package/dist/core/runtime/naming.d.ts +22 -0
- package/dist/core/runtime/naming.js +166 -0
- package/dist/core/runtime/nodes.d.ts +32 -1
- package/dist/core/runtime/nodes.js +60 -10
- package/dist/core/runtime/persona.d.ts +25 -0
- package/dist/core/runtime/persona.js +139 -0
- package/dist/core/runtime/placement.d.ts +287 -0
- package/dist/core/runtime/placement.js +663 -0
- package/dist/core/runtime/presence.d.ts +7 -32
- package/dist/core/runtime/presence.js +90 -110
- package/dist/core/runtime/promote.d.ts +18 -7
- package/dist/core/runtime/promote.js +70 -65
- package/dist/core/runtime/reset.d.ts +47 -4
- package/dist/core/runtime/reset.js +223 -52
- package/dist/core/runtime/revive.d.ts +26 -2
- package/dist/core/runtime/revive.js +166 -39
- package/dist/core/runtime/roadmap.d.ts +5 -4
- package/dist/core/runtime/roadmap.js +9 -16
- package/dist/core/runtime/spawn.d.ts +20 -5
- package/dist/core/runtime/spawn.js +169 -44
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +18 -8
- package/dist/core/runtime/tmux.d.ts +106 -21
- package/dist/core/runtime/tmux.js +249 -45
- package/dist/core/spawn.js +15 -0
- package/dist/daemon/crtrd.d.ts +12 -1
- package/dist/daemon/crtrd.js +152 -34
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +266 -0
- package/dist/pi-extensions/canvas-commands.d.ts +34 -0
- package/dist/pi-extensions/canvas-commands.js +103 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +70 -0
- package/dist/pi-extensions/canvas-context-intro.js +164 -0
- package/dist/pi-extensions/canvas-goal-capture.d.ts +21 -0
- package/dist/pi-extensions/canvas-goal-capture.js +67 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +11 -0
- package/dist/pi-extensions/canvas-nav.d.ts +12 -4
- package/dist/pi-extensions/canvas-nav.js +586 -262
- package/dist/pi-extensions/canvas-passive-context.d.ts +32 -0
- package/dist/pi-extensions/canvas-passive-context.js +114 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
- package/dist/pi-extensions/canvas-stophook.js +344 -228
- package/dist/types.d.ts +28 -0
- package/dist/types.js +16 -0
- package/package.json +1 -1
|
@@ -14,11 +14,10 @@
|
|
|
14
14
|
* If even the base is missing, fall back to general defaults + kernel.
|
|
15
15
|
*
|
|
16
16
|
* Frontmatter from whichever file is the primary source (orchestrator.md >
|
|
17
|
-
* base.md) supplies model/
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* orchestrator → 'resident'
|
|
17
|
+
* base.md) supplies model/skills/extensions/tools. Lifecycle and spine position
|
|
18
|
+
* are INPUTS (the caller decides them — root/child, terminal/resident), not
|
|
19
|
+
* derived here; they select the lifecycle/spine protocol fragments spliced
|
|
20
|
+
* ahead of the persona body.
|
|
22
21
|
*/
|
|
23
22
|
export interface ResolvedPersona {
|
|
24
23
|
systemPrompt: string;
|
|
@@ -33,4 +32,12 @@ export interface ResolvedPersona {
|
|
|
33
32
|
*
|
|
34
33
|
* Never throws for missing files — missing personas produce sensible defaults.
|
|
35
34
|
*/
|
|
36
|
-
export
|
|
35
|
+
export interface ResolveOpts {
|
|
36
|
+
/** The node's lifecycle axis — selects the "how you end" fragment. */
|
|
37
|
+
lifecycle: 'terminal' | 'resident';
|
|
38
|
+
/** Whether the node reports up to a manager (parent !== null) — selects the
|
|
39
|
+
* spine fragment (`has-manager` teaches the push family; `no-manager` omits
|
|
40
|
+
* it entirely). */
|
|
41
|
+
hasManager: boolean;
|
|
42
|
+
}
|
|
43
|
+
export declare function resolve(kind: string, mode: 'base' | 'orchestrator', opts: ResolveOpts): ResolvedPersona;
|
|
@@ -14,13 +14,12 @@
|
|
|
14
14
|
* If even the base is missing, fall back to general defaults + kernel.
|
|
15
15
|
*
|
|
16
16
|
* Frontmatter from whichever file is the primary source (orchestrator.md >
|
|
17
|
-
* base.md) supplies model/
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* orchestrator → 'resident'
|
|
17
|
+
* base.md) supplies model/skills/extensions/tools. Lifecycle and spine position
|
|
18
|
+
* are INPUTS (the caller decides them — root/child, terminal/resident), not
|
|
19
|
+
* derived here; they select the lifecycle/spine protocol fragments spliced
|
|
20
|
+
* ahead of the persona body.
|
|
22
21
|
*/
|
|
23
|
-
import { loadPersona, loadKernel, loadRuntimeBase } from './loader.js';
|
|
22
|
+
import { loadPersona, loadKernel, loadRuntimeBase, loadSpineFragment, loadLifecycleFragment, subKindsFor } from './loader.js';
|
|
24
23
|
// ---------------------------------------------------------------------------
|
|
25
24
|
// Helpers
|
|
26
25
|
// ---------------------------------------------------------------------------
|
|
@@ -32,48 +31,62 @@ function toStringArray(v) {
|
|
|
32
31
|
function toOptionalString(v) {
|
|
33
32
|
return typeof v === 'string' ? v : undefined;
|
|
34
33
|
}
|
|
35
|
-
function toLifecycle(v, defaultValue) {
|
|
36
|
-
if (v === 'terminal' || v === 'resident')
|
|
37
|
-
return v;
|
|
38
|
-
return defaultValue;
|
|
39
|
-
}
|
|
40
34
|
/** The bare-minimum system prompt used when no persona file is found at all. */
|
|
41
35
|
function fallbackBasePrompt(kind) {
|
|
42
36
|
return `You are a ${kind} agent. Complete the task you have been given.`;
|
|
43
37
|
}
|
|
44
|
-
/**
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
/** Compose the runtime protocol that precedes every persona body: the
|
|
39
|
+
* lifecycle-neutral base (identity/delegate/ask/promote), then the spine
|
|
40
|
+
* fragment (report-up vs. silent, keyed on whether the node has a manager),
|
|
41
|
+
* then the lifecycle fragment (finish-with-`push final` vs. dormant/wake). The
|
|
42
|
+
* kind×mode persona body follows after a rule. Empty fragments drop out. */
|
|
43
|
+
/** Render the "sub-kinds you may spawn" menu for a kind that owns a roster.
|
|
44
|
+
* Returns '' when the kind owns none. Data-driven: one line per sub-kind, its
|
|
45
|
+
* spawn string + its `summary`. Adding a roster file makes it appear here. */
|
|
46
|
+
function renderSubKindMenu(kind) {
|
|
47
|
+
const subs = subKindsFor(kind);
|
|
48
|
+
if (subs.length === 0)
|
|
49
|
+
return '';
|
|
50
|
+
const lines = subs.map((s) => `- \`${s.kind}\` — ${s.summary}`);
|
|
51
|
+
return [
|
|
52
|
+
'## Reviewer sub-kinds you may spawn',
|
|
53
|
+
'',
|
|
54
|
+
`These specialist reviewers exist only in the ${kind} kind's world — no other kind sees them. Spawn one with \`crtr node new --kind <sub-kind> "<scope>"\`, giving it only its scope, never your suspicions: a reviewer handed a hint anchors on it instead of finding problems independently.`,
|
|
55
|
+
'',
|
|
56
|
+
...lines,
|
|
57
|
+
].join('\n');
|
|
49
58
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
function composeProtocol(personaPrompt, kind, lifecycle, hasManager) {
|
|
60
|
+
const menu = renderSubKindMenu(kind);
|
|
61
|
+
const body = menu ? `${personaPrompt}\n\n${menu}` : personaPrompt;
|
|
62
|
+
const protocol = [
|
|
63
|
+
loadRuntimeBase(),
|
|
64
|
+
loadSpineFragment(hasManager),
|
|
65
|
+
loadLifecycleFragment(lifecycle),
|
|
66
|
+
]
|
|
67
|
+
.filter((s) => s.length > 0)
|
|
68
|
+
.join('\n\n');
|
|
69
|
+
return protocol ? `${protocol}\n\n---\n\n${body}` : body;
|
|
70
|
+
}
|
|
71
|
+
export function resolve(kind, mode, opts) {
|
|
59
72
|
if (mode === 'base') {
|
|
60
73
|
const persona = loadPersona(kind, 'base');
|
|
61
74
|
if (!persona) {
|
|
62
75
|
// No persona file for this kind — use minimal defaults.
|
|
63
76
|
return {
|
|
64
|
-
systemPrompt:
|
|
77
|
+
systemPrompt: composeProtocol(fallbackBasePrompt(kind), kind, opts.lifecycle, opts.hasManager),
|
|
65
78
|
extensions: [],
|
|
66
79
|
skills: [],
|
|
67
|
-
lifecycle:
|
|
80
|
+
lifecycle: opts.lifecycle,
|
|
68
81
|
};
|
|
69
82
|
}
|
|
70
83
|
const fm = persona.frontmatter ?? {};
|
|
71
84
|
return {
|
|
72
|
-
systemPrompt:
|
|
85
|
+
systemPrompt: composeProtocol(persona.body || fallbackBasePrompt(kind), kind, opts.lifecycle, opts.hasManager),
|
|
73
86
|
extensions: toStringArray(fm['extensions']),
|
|
74
87
|
skills: toStringArray(fm['skills']),
|
|
75
88
|
model: toOptionalString(fm['model']),
|
|
76
|
-
lifecycle:
|
|
89
|
+
lifecycle: opts.lifecycle,
|
|
77
90
|
tools: fm['tools'] !== undefined ? toStringArray(fm['tools']) : undefined,
|
|
78
91
|
};
|
|
79
92
|
}
|
|
@@ -83,11 +96,11 @@ export function resolve(kind, mode) {
|
|
|
83
96
|
// Orchestrator file exists; @include was already inlined by the loader.
|
|
84
97
|
const fm = orchestratorPersona.frontmatter ?? {};
|
|
85
98
|
return {
|
|
86
|
-
systemPrompt:
|
|
99
|
+
systemPrompt: composeProtocol(orchestratorPersona.body || fallbackBasePrompt(kind), kind, opts.lifecycle, opts.hasManager),
|
|
87
100
|
extensions: toStringArray(fm['extensions']),
|
|
88
101
|
skills: toStringArray(fm['skills']),
|
|
89
102
|
model: toOptionalString(fm['model']),
|
|
90
|
-
lifecycle:
|
|
103
|
+
lifecycle: opts.lifecycle,
|
|
91
104
|
tools: fm['tools'] !== undefined ? toStringArray(fm['tools']) : undefined,
|
|
92
105
|
};
|
|
93
106
|
}
|
|
@@ -99,12 +112,11 @@ export function resolve(kind, mode) {
|
|
|
99
112
|
// Append the kernel to the base body (with separator if kernel is non-empty).
|
|
100
113
|
const systemPrompt = kernel ? `${baseBody}\n\n${kernel}` : baseBody;
|
|
101
114
|
return {
|
|
102
|
-
systemPrompt:
|
|
115
|
+
systemPrompt: composeProtocol(systemPrompt, kind, opts.lifecycle, opts.hasManager),
|
|
103
116
|
extensions: toStringArray(fm['extensions']),
|
|
104
117
|
skills: toStringArray(fm['skills']),
|
|
105
118
|
model: toOptionalString(fm['model']),
|
|
106
|
-
|
|
107
|
-
lifecycle: 'resident',
|
|
119
|
+
lifecycle: opts.lifecycle,
|
|
108
120
|
tools: fm['tools'] !== undefined ? toStringArray(fm['tools']) : undefined,
|
|
109
121
|
};
|
|
110
122
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Base framing — present for every node. No path baked in: the caller carries
|
|
2
|
+
* the dir in the <crtr-context dir="…"> attribute. */
|
|
3
|
+
export declare const BASE_CONTEXT_NOTE: string;
|
|
4
|
+
/** Orchestrator-only framing: a resident orchestrator survives refresh cycles,
|
|
5
|
+
* so its context dir is also where a future cycle of itself resumes the work.
|
|
6
|
+
* Used inside the bearings block AND in the promotion guidance dump, so a
|
|
7
|
+
* promoted node gets the same note a born-orchestrator gets. */
|
|
8
|
+
export declare function orchestratorContextNote(nodeId: string): string;
|
|
9
|
+
/** The <memory> block (orchestrators only): the scoped stores merged, each a
|
|
10
|
+
* `label · dir` header over its live index pointer lines. A memory's `type`
|
|
11
|
+
* decides which store it lands in — the mapping + the how-to live once in the
|
|
12
|
+
* orchestration kernel ("Your long-term memory"); here we carry only the live
|
|
13
|
+
* data + a one-line pointer back to it. user-global rides in when the node has
|
|
14
|
+
* a user store, project when it has a project store, node-local always (the
|
|
15
|
+
* orchestrator gate). */
|
|
16
|
+
export declare function buildMemoryBlock(nodeId: string, cwd: string): string;
|
|
17
|
+
/** The full <crtr-context> bearings block: base framing always, plus the
|
|
18
|
+
* orchestrator addendum + the merged three-store <memory> block when the node
|
|
19
|
+
* has a node-local memory store (the orchestrator gate). */
|
|
20
|
+
export declare function buildContextBearings(nodeId: string): string;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// bearings.ts — the <crtr-context> framing prose, shared by the two paths that
|
|
2
|
+
// deliver it so they can never drift:
|
|
3
|
+
//
|
|
4
|
+
// • the context-intro pi-extension injects buildContextBearings() as the
|
|
5
|
+
// node's first session message in every brand-new chat;
|
|
6
|
+
// • promote.ts folds orchestratorContextNote() into the promotion guidance
|
|
7
|
+
// dump, so a node that becomes an orchestrator MID-LIFE gets the
|
|
8
|
+
// orchestrator framing it never received at spawn — it spawned as a base
|
|
9
|
+
// worker, and the bearings already in its history carry only the base note.
|
|
10
|
+
//
|
|
11
|
+
// Base framing (every node): the context dir is durable, shared scratch — the
|
|
12
|
+
// one place other nodes on the canvas can read from, so it is for documents
|
|
13
|
+
// worth a shared reference, NOT a task tracker, and NOT a "future memory-wiped
|
|
14
|
+
// you" stash (a terminal worker has no future cycle — that framing only makes
|
|
15
|
+
// sense once a node is a resident orchestrator).
|
|
16
|
+
//
|
|
17
|
+
// Orchestrator addendum (resident orchestrators — i.e. nodes that have a
|
|
18
|
+
// node-local memory store): the dir ALSO survives refresh cycles, so it is where
|
|
19
|
+
// a future cycle of the orchestrator resumes; durable cross-goal lessons live in
|
|
20
|
+
// the three scoped memory stores, whose index pointer lines are inlined into
|
|
21
|
+
// <memory> (the how-to lives once in the kernel, not here).
|
|
22
|
+
import { contextDir, getNode } from '../canvas/index.js';
|
|
23
|
+
import { hasMemory, memoryDir, readMemory, hasUserMemory, userMemoryDir, readUserMemory, hasProjectMemory, projectMemoryDir, readProjectMemory, } from './memory.js';
|
|
24
|
+
/** Base framing — present for every node. No path baked in: the caller carries
|
|
25
|
+
* the dir in the <crtr-context dir="…"> attribute. */
|
|
26
|
+
export const BASE_CONTEXT_NOTE = 'This is your context directory — durable scratch space on disk, and the one place the other ' +
|
|
27
|
+
'nodes on the canvas can read from. Put documents here that you want to share by reference ' +
|
|
28
|
+
'instead of re-explaining them in a prompt: specs, designs, findings, notes worth pointing a ' +
|
|
29
|
+
'sibling, child, or parent at. It is a shared document store, not a task tracker.';
|
|
30
|
+
/** Orchestrator-only framing: a resident orchestrator survives refresh cycles,
|
|
31
|
+
* so its context dir is also where a future cycle of itself resumes the work.
|
|
32
|
+
* Used inside the bearings block AND in the promotion guidance dump, so a
|
|
33
|
+
* promoted node gets the same note a born-orchestrator gets. */
|
|
34
|
+
export function orchestratorContextNote(nodeId) {
|
|
35
|
+
return (`Because you persist across refresh cycles, your context directory (${contextDir(nodeId)}) is ` +
|
|
36
|
+
`also where a future cycle of you resumes the work — keep the working notes and decisions a ` +
|
|
37
|
+
`refreshed you would need there, alongside the docs you share with the nodes you spawn.`);
|
|
38
|
+
}
|
|
39
|
+
/** One labeled store stanza inside <memory>: a compact `label · dir` header (the
|
|
40
|
+
* scope name + where to WRITE this kind of memory), then the LIVE pointer lines
|
|
41
|
+
* extracted fresh from the store's index — only lines matching `- [...` — with
|
|
42
|
+
* the index's how-to boilerplate dropped (it lives once in the kernel) and
|
|
43
|
+
* detail files loaded on demand. Falls back to `(empty)` when the index carries
|
|
44
|
+
* no pointers, which also covers the not-seeded / template-only case. */
|
|
45
|
+
function memoryStanza(label, dir, index) {
|
|
46
|
+
const pointers = (index ?? '')
|
|
47
|
+
.split('\n')
|
|
48
|
+
.filter((line) => /^\s*-\s*\[/.test(line))
|
|
49
|
+
.map((line) => line.trim());
|
|
50
|
+
const body = pointers.length > 0 ? pointers.join('\n') : '(empty)';
|
|
51
|
+
return `${label} · ${dir}\n${body}`;
|
|
52
|
+
}
|
|
53
|
+
/** The <memory> block (orchestrators only): the scoped stores merged, each a
|
|
54
|
+
* `label · dir` header over its live index pointer lines. A memory's `type`
|
|
55
|
+
* decides which store it lands in — the mapping + the how-to live once in the
|
|
56
|
+
* orchestration kernel ("Your long-term memory"); here we carry only the live
|
|
57
|
+
* data + a one-line pointer back to it. user-global rides in when the node has
|
|
58
|
+
* a user store, project when it has a project store, node-local always (the
|
|
59
|
+
* orchestrator gate). */
|
|
60
|
+
export function buildMemoryBlock(nodeId, cwd) {
|
|
61
|
+
const stanzas = [];
|
|
62
|
+
if (hasUserMemory()) {
|
|
63
|
+
stanzas.push(memoryStanza('user-global', userMemoryDir(), readUserMemory()));
|
|
64
|
+
}
|
|
65
|
+
if (hasProjectMemory(cwd)) {
|
|
66
|
+
stanzas.push(memoryStanza('project', projectMemoryDir(cwd), readProjectMemory(cwd)));
|
|
67
|
+
}
|
|
68
|
+
stanzas.push(memoryStanza('node-local', memoryDir(nodeId), readMemory(nodeId)));
|
|
69
|
+
const n = stanzas.length;
|
|
70
|
+
return ('<memory>\n' +
|
|
71
|
+
`Long-term memory, ${n} scope${n === 1 ? '' : 's'}. Each line ` +
|
|
72
|
+
'`- [Title](slug.md) — hook`; load a detail file by slug from the scope dir on demand. ' +
|
|
73
|
+
'Write a new fact to the scope matching its `type` (see "Your long-term memory").\n\n' +
|
|
74
|
+
stanzas.join('\n\n') +
|
|
75
|
+
'\n</memory>');
|
|
76
|
+
}
|
|
77
|
+
/** The full <crtr-context> bearings block: base framing always, plus the
|
|
78
|
+
* orchestrator addendum + the merged three-store <memory> block when the node
|
|
79
|
+
* has a node-local memory store (the orchestrator gate). */
|
|
80
|
+
export function buildContextBearings(nodeId) {
|
|
81
|
+
const dir = contextDir(nodeId);
|
|
82
|
+
if (!hasMemory(nodeId)) {
|
|
83
|
+
// A terminal worker (no memory store): base framing only, no memory block.
|
|
84
|
+
return `<crtr-context dir="${dir}">\n${BASE_CONTEXT_NOTE}\n</crtr-context>`;
|
|
85
|
+
}
|
|
86
|
+
// An orchestrator: across-cycles framing + the merged three-store memory. The
|
|
87
|
+
// project store is keyed off the node's cwd (its working dir on disk).
|
|
88
|
+
const cwd = getNode(nodeId)?.cwd ?? process.cwd();
|
|
89
|
+
return (`<crtr-context dir="${dir}">\n` +
|
|
90
|
+
`${BASE_CONTEXT_NOTE}\n${orchestratorContextNote(nodeId)}\n${buildMemoryBlock(nodeId, cwd)}\n` +
|
|
91
|
+
'</crtr-context>');
|
|
92
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface CloseNodeResult {
|
|
2
|
+
/** The focused node that was closed — the cascade root. */
|
|
3
|
+
root: string;
|
|
4
|
+
/** Every node torn down (root + cascaded descendants), in kill order
|
|
5
|
+
* (leaves first, root last). */
|
|
6
|
+
closed: string[];
|
|
7
|
+
/** Descendants left alive because a manager outside the subtree still
|
|
8
|
+
* subscribes to them. */
|
|
9
|
+
spared: string[];
|
|
10
|
+
}
|
|
11
|
+
/** Close `rootId` and its exclusive subtree. Best-effort throughout: a tmux/db
|
|
12
|
+
* failure on one node never aborts the cascade. Throws only on an unknown root
|
|
13
|
+
* so the command can surface a clean not-found error. */
|
|
14
|
+
export declare function closeNode(rootId: string): CloseNodeResult;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// close.ts — the "close this node + its subtree" action behind `crtr node close`.
|
|
2
|
+
//
|
|
3
|
+
// Closing a node tears down the focused node and every descendant it
|
|
4
|
+
// EXCLUSIVELY owns, walking DOWN the subscribes_to spine (subscriptionsOf = a
|
|
5
|
+
// node's reports/children). Nothing is deleted: pi_session_id, the canvas
|
|
6
|
+
// edges, and all on-disk state persist, so any closed node can later be revived
|
|
7
|
+
// (`crtr canvas revive` / focus → `pi --session <id>`). A close is a pause, not a reap.
|
|
8
|
+
//
|
|
9
|
+
// Per node, in this order — the order matters twice:
|
|
10
|
+
//
|
|
11
|
+
// 1. Mark `canceled` + clear intent. Done BEFORE the window dies: the daemon
|
|
12
|
+
// only ever revives an active|idle node, so flipping to canceled first
|
|
13
|
+
// closes the race where the supervisor sees a window-gone live node and
|
|
14
|
+
// either revives it or marks it dead (overwriting our canceled).
|
|
15
|
+
// 2. Kill its tmux PANE (the window closes once its last pane goes) — which
|
|
16
|
+
// kills pi and, with it, the inbox watcher. Pane-granular so that nodes
|
|
17
|
+
// the user co-located as panes in ONE window (via swap-pane focus) are not
|
|
18
|
+
// all taken down when one of them is closed.
|
|
19
|
+
// 3. Append the cancellation notice to its inbox AFTER the watcher is gone.
|
|
20
|
+
// The watcher advances its cursor when it READS an entry, so appending
|
|
21
|
+
// while it is still live would let it consume + skip the notice (cursor
|
|
22
|
+
// moves past it, never delivered). Killed first, the cursor stays put;
|
|
23
|
+
// on the node's next resume a fresh watcher seeds from that frozen cursor,
|
|
24
|
+
// finds the notice, and injects it — the agent learns its children died.
|
|
25
|
+
//
|
|
26
|
+
// The cascade is GUARDED: a descendant is closed only when EVERY node that
|
|
27
|
+
// subscribes to it (its managers, subscribersOf — active OR passive) is itself
|
|
28
|
+
// inside the closing set. A node still subscribed to by a manager outside the
|
|
29
|
+
// subtree is left running — "only kill the children if they are only subscribed
|
|
30
|
+
// to by the agent being closed", generalized to any depth via a fixpoint.
|
|
31
|
+
import { getNode, subscriptionsOf, subscribersOf, } from '../canvas/index.js';
|
|
32
|
+
import { transition } from './lifecycle.js';
|
|
33
|
+
import { tearDownNode } from './placement.js';
|
|
34
|
+
import { appendInbox } from '../feed/inbox.js';
|
|
35
|
+
/** The set of nodes to close: the root plus every descendant reachable down the
|
|
36
|
+
* subscriptions spine, all of whose managers are themselves in the set. Grown
|
|
37
|
+
* to a fixpoint — a node added this pass can qualify its own children next
|
|
38
|
+
* pass. Cycle-safe via the membership skip. */
|
|
39
|
+
function closingSet(root) {
|
|
40
|
+
const closing = new Set([root]);
|
|
41
|
+
let changed = true;
|
|
42
|
+
while (changed) {
|
|
43
|
+
changed = false;
|
|
44
|
+
for (const parent of [...closing]) {
|
|
45
|
+
for (const sub of subscriptionsOf(parent)) {
|
|
46
|
+
const child = sub.node_id;
|
|
47
|
+
if (closing.has(child))
|
|
48
|
+
continue;
|
|
49
|
+
// Close the child only if NOBODY outside the closing set subscribes to
|
|
50
|
+
// it. (subscriptionsOf always yields child→parent, so `parent` is one
|
|
51
|
+
// of child's managers and is in `closing` — the check is never vacuous.)
|
|
52
|
+
if (subscribersOf(child).every((m) => closing.has(m.node_id))) {
|
|
53
|
+
closing.add(child);
|
|
54
|
+
changed = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return closing;
|
|
60
|
+
}
|
|
61
|
+
/** BFS the closing set from root, then reverse: leaves die first, the focused
|
|
62
|
+
* root dies last ("cascades up"). The root being killed last also keeps the
|
|
63
|
+
* user's foreground window — the one they invoked the close from — open until
|
|
64
|
+
* every background descendant is gone. */
|
|
65
|
+
function killOrder(root, closing) {
|
|
66
|
+
const order = [];
|
|
67
|
+
const seen = new Set([root]);
|
|
68
|
+
const queue = [root];
|
|
69
|
+
while (queue.length > 0) {
|
|
70
|
+
const id = queue.shift();
|
|
71
|
+
order.push(id);
|
|
72
|
+
for (const sub of subscriptionsOf(id)) {
|
|
73
|
+
if (closing.has(sub.node_id) && !seen.has(sub.node_id)) {
|
|
74
|
+
seen.add(sub.node_id);
|
|
75
|
+
queue.push(sub.node_id);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Any closing node a cycle kept BFS from reaching still gets torn down.
|
|
80
|
+
for (const id of closing)
|
|
81
|
+
if (!seen.has(id))
|
|
82
|
+
order.push(id);
|
|
83
|
+
return order.reverse();
|
|
84
|
+
}
|
|
85
|
+
/** The inbox notice a closed node reads on its next resume. */
|
|
86
|
+
function cancellationLabel(isRoot, deadChildren) {
|
|
87
|
+
const who = isRoot
|
|
88
|
+
? 'You were CLOSED by the user from the canvas'
|
|
89
|
+
: 'You were CANCELED — an ancestor of yours was closed from the canvas';
|
|
90
|
+
if (deadChildren.length === 0) {
|
|
91
|
+
return `${who}. Your pi session is preserved; this resume reopened it.`;
|
|
92
|
+
}
|
|
93
|
+
const names = deadChildren.slice(0, 4).map((c) => {
|
|
94
|
+
const n = getNode(c);
|
|
95
|
+
return n !== null ? `${n.name} (${c})` : c;
|
|
96
|
+
});
|
|
97
|
+
const more = deadChildren.length > names.length ? ` +${deadChildren.length - names.length} more` : '';
|
|
98
|
+
return (`${who}. ${deadChildren.length} child node(s) you subscribe to were canceled with you and are no ` +
|
|
99
|
+
`longer running: ${names.join(', ')}${more}. Resuming will NOT restore them — re-spawn if you ` +
|
|
100
|
+
`still need that work.`);
|
|
101
|
+
}
|
|
102
|
+
/** Close `rootId` and its exclusive subtree. Best-effort throughout: a tmux/db
|
|
103
|
+
* failure on one node never aborts the cascade. Throws only on an unknown root
|
|
104
|
+
* so the command can surface a clean not-found error. */
|
|
105
|
+
export function closeNode(rootId) {
|
|
106
|
+
if (getNode(rootId) === null)
|
|
107
|
+
throw new Error(`closeNode: unknown node ${rootId}`);
|
|
108
|
+
const closing = closingSet(rootId);
|
|
109
|
+
const order = killOrder(rootId, closing);
|
|
110
|
+
// Descendants reachable from the subtree but kept alive (shared managers).
|
|
111
|
+
const spared = [];
|
|
112
|
+
for (const id of closing) {
|
|
113
|
+
for (const sub of subscriptionsOf(id)) {
|
|
114
|
+
if (!closing.has(sub.node_id) && !spared.includes(sub.node_id)) {
|
|
115
|
+
spared.push(sub.node_id);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const closed = [];
|
|
120
|
+
for (const id of order) {
|
|
121
|
+
try {
|
|
122
|
+
const m = getNode(id);
|
|
123
|
+
if (m === null)
|
|
124
|
+
continue;
|
|
125
|
+
// This node's reports that are dying with it (for the resume notice).
|
|
126
|
+
const deadChildren = subscriptionsOf(id)
|
|
127
|
+
.map((s) => s.node_id)
|
|
128
|
+
.filter((c) => closing.has(c));
|
|
129
|
+
// 1) Canceled + intent cleared BEFORE the window dies (daemon race).
|
|
130
|
+
transition(id, 'cancel');
|
|
131
|
+
// 2) Tear the node off its placement (pane-keyed): close any focus row it
|
|
132
|
+
// occupies, kill its PANE (the window closes once its last pane goes, so
|
|
133
|
+
// sibling nodes the user co-located in one window survive), null its
|
|
134
|
+
// LOCATION, and clear focus.ptr if it was the current focus.
|
|
135
|
+
tearDownNode(id);
|
|
136
|
+
// 3) Leave the resume notice AFTER the watcher is gone, so it survives.
|
|
137
|
+
appendInbox(id, {
|
|
138
|
+
from: null,
|
|
139
|
+
tier: 'normal',
|
|
140
|
+
kind: 'message',
|
|
141
|
+
label: cancellationLabel(id === rootId, deadChildren),
|
|
142
|
+
data: { reason: 'user-close', cascade_root: rootId, canceled_children: deadChildren },
|
|
143
|
+
});
|
|
144
|
+
closed.push(id);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
/* one bad node never aborts the cascade */
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { root: rootId, closed, spared };
|
|
151
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface DemoteResult {
|
|
2
|
+
/** True when the pane was recycled (a fresh root respawned in it). */
|
|
3
|
+
demoted: boolean;
|
|
4
|
+
/** True when a `final` report was pushed for the demoted node. */
|
|
5
|
+
finalized: boolean;
|
|
6
|
+
/** The fresh root node booted into the pane, or null on failure. */
|
|
7
|
+
newRoot: string | null;
|
|
8
|
+
/** Subscriber node ids that received the final report. */
|
|
9
|
+
delivered: string[];
|
|
10
|
+
}
|
|
11
|
+
/** Finish `nodeId` and recycle its pane into a fresh root. `callerPane` is the
|
|
12
|
+
* tmux pane the agent occupies (the Alt+C menu passes it as `#{pane_id}`).
|
|
13
|
+
* Best-effort; `demoted:false` when there is no pane to act on. */
|
|
14
|
+
export declare function demoteNode(nodeId: string, callerPane?: string): Promise<DemoteResult>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// demote.ts — the "graduate this agent" action behind `crtr node demote`.
|
|
2
|
+
//
|
|
3
|
+
// Demote finishes the agent occupying a tmux pane and recycles that pane for
|
|
4
|
+
// fresh work, in three steps:
|
|
5
|
+
//
|
|
6
|
+
// 1. Finalize — push the agent's last surfaced message as a `final` report so
|
|
7
|
+
// every subscriber/manager waiting on it is unblocked, and mark it done.
|
|
8
|
+
// 2. Close — kill the agent's pi (respawn-pane -k tears it down in place).
|
|
9
|
+
// 3. Recycle — boot a fresh resident root in that same pane (a new `crtr`).
|
|
10
|
+
//
|
|
11
|
+
// The agent's real conversation lives inside pi (not on disk), so the final
|
|
12
|
+
// body is its newest report (which, on a natural stop, IS its last assistant
|
|
13
|
+
// message) — falling back to a short note when it never reported.
|
|
14
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { getNode, setPresence, updateNode, setFocusOccupant, fullName } from '../canvas/index.js';
|
|
17
|
+
import { reportsDir } from '../canvas/paths.js';
|
|
18
|
+
import { pushFinal } from '../feed/feed.js';
|
|
19
|
+
import { spawnNode } from './nodes.js';
|
|
20
|
+
import { buildLaunchSpec, buildPiArgv } from './launch.js';
|
|
21
|
+
import { piCommand, paneLocation, nodeSession } from './tmux.js';
|
|
22
|
+
import { FRONT_DOOR_ENV } from './front-door.js';
|
|
23
|
+
import { getFocus, setFocus } from './presence.js';
|
|
24
|
+
import { focusOf, recycleFocusPane } from './placement.js';
|
|
25
|
+
import { ensureDaemon } from '../../daemon/manage.js';
|
|
26
|
+
/** The agent's most recent surfaced message: the newest reports/*.md body with
|
|
27
|
+
* its YAML frontmatter stripped. Empty string when the node never reported. */
|
|
28
|
+
function lastReportBody(nodeId) {
|
|
29
|
+
try {
|
|
30
|
+
const dir = reportsDir(nodeId);
|
|
31
|
+
const files = readdirSync(dir).filter((f) => f.endsWith('.md'));
|
|
32
|
+
if (files.length === 0)
|
|
33
|
+
return '';
|
|
34
|
+
let newest = '';
|
|
35
|
+
let newestMs = -1;
|
|
36
|
+
for (const f of files) {
|
|
37
|
+
const ms = statSync(join(dir, f)).mtimeMs;
|
|
38
|
+
if (ms > newestMs) {
|
|
39
|
+
newestMs = ms;
|
|
40
|
+
newest = f;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const raw = readFileSync(join(dir, newest), 'utf8');
|
|
44
|
+
// Strip leading YAML frontmatter: ---\n …\n---\n<body>
|
|
45
|
+
const m = /^---\n[\s\S]*?\n---\n/.exec(raw);
|
|
46
|
+
return (m !== null ? raw.slice(m[0].length) : raw).trim();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Finish `nodeId` and recycle its pane into a fresh root. `callerPane` is the
|
|
53
|
+
* tmux pane the agent occupies (the Alt+C menu passes it as `#{pane_id}`).
|
|
54
|
+
* Best-effort; `demoted:false` when there is no pane to act on. */
|
|
55
|
+
export async function demoteNode(nodeId, callerPane) {
|
|
56
|
+
const meta = getNode(nodeId);
|
|
57
|
+
if (meta === null)
|
|
58
|
+
return { demoted: false, finalized: false, newRoot: null, delivered: [] };
|
|
59
|
+
const pane = callerPane ?? process.env['TMUX_PANE'];
|
|
60
|
+
if (pane === undefined || pane === '') {
|
|
61
|
+
return { demoted: false, finalized: false, newRoot: null, delivered: [] };
|
|
62
|
+
}
|
|
63
|
+
// 1. Finalize — fan the agent's last message out as a `final`, mark it done.
|
|
64
|
+
const body = lastReportBody(nodeId) ||
|
|
65
|
+
`Closed via demote — no final summary was authored by ${meta.name}.`;
|
|
66
|
+
let delivered = [];
|
|
67
|
+
let finalized = false;
|
|
68
|
+
try {
|
|
69
|
+
const res = await pushFinal(nodeId, body);
|
|
70
|
+
delivered = res.deliveredTo;
|
|
71
|
+
finalized = true;
|
|
72
|
+
}
|
|
73
|
+
catch { /* recycle the pane even if the report failed */ }
|
|
74
|
+
// Capture M's focus viewport (if any) BEFORE nulling — the fresh root inherits
|
|
75
|
+
// it (the SAME focus row + pane). The demoted node no longer holds a pane: it is
|
|
76
|
+
// being reclaimed.
|
|
77
|
+
const f = focusOf(nodeId);
|
|
78
|
+
try {
|
|
79
|
+
setPresence(nodeId, { pane: null, window: null, tmux_session: null });
|
|
80
|
+
}
|
|
81
|
+
catch { /* best-effort */ }
|
|
82
|
+
// 2 + 3. Recycle — boot a fresh resident root in the SAME pane.
|
|
83
|
+
try {
|
|
84
|
+
ensureDaemon();
|
|
85
|
+
}
|
|
86
|
+
catch { /* daemon is best-effort */ }
|
|
87
|
+
const loc = paneLocation(pane);
|
|
88
|
+
const { launch } = buildLaunchSpec('general', 'base', { lifecycle: 'resident', hasManager: false });
|
|
89
|
+
const root = spawnNode({
|
|
90
|
+
kind: 'general',
|
|
91
|
+
mode: 'base',
|
|
92
|
+
lifecycle: 'resident',
|
|
93
|
+
cwd: meta.cwd,
|
|
94
|
+
name: 'general',
|
|
95
|
+
parent: null,
|
|
96
|
+
launch,
|
|
97
|
+
});
|
|
98
|
+
// REVIVE-HOME: a demote-recycled root's durable revive target is the session
|
|
99
|
+
// of the pane it was recycled into (the one place home_session is rewritten
|
|
100
|
+
// after birth). Falls back to the backstage when the pane can't be located.
|
|
101
|
+
updateNode(root.node_id, { home_session: loc?.session ?? nodeSession() });
|
|
102
|
+
// Hand the viewport to the fresh root: reuse M's focus row over the SAME pane
|
|
103
|
+
// (respawn-pane -k below keeps the %id), so the user keeps watching this slot.
|
|
104
|
+
if (f !== null) {
|
|
105
|
+
try {
|
|
106
|
+
setFocusOccupant(f.focus_id, root.node_id);
|
|
107
|
+
setFocus(root.node_id);
|
|
108
|
+
}
|
|
109
|
+
catch { /* best-effort */ }
|
|
110
|
+
}
|
|
111
|
+
else if (getFocus() === nodeId)
|
|
112
|
+
setFocus('');
|
|
113
|
+
const fresh = getNode(root.node_id);
|
|
114
|
+
const inv = buildPiArgv(fresh);
|
|
115
|
+
const env = { ...inv.env, CRTR_ROOT_SESSION: nodeSession(), [FRONT_DOOR_ENV]: '1' };
|
|
116
|
+
const ok = recycleFocusPane(root.node_id, pane, {
|
|
117
|
+
command: piCommand(inv.argv), env, cwd: meta.cwd, name: fullName(fresh), resuming: false,
|
|
118
|
+
});
|
|
119
|
+
return { demoted: ok, finalized, newRoot: root.node_id, delivered };
|
|
120
|
+
}
|
|
@@ -92,6 +92,6 @@ export function maybeBootRoot(root, argv) {
|
|
|
92
92
|
// Unambiguous front-door launch → boot a resident root inline (exec pi in
|
|
93
93
|
// this terminal). Does not return.
|
|
94
94
|
const args = parseRootArgs(tokens);
|
|
95
|
-
bootRoot({ ...args
|
|
95
|
+
bootRoot({ ...args });
|
|
96
96
|
return true;
|
|
97
97
|
}
|
|
@@ -6,6 +6,15 @@ export declare function readGoal(nodeId: string): string | null;
|
|
|
6
6
|
/** Persist the spawning prompt as the node's goal. No-op for an empty prompt
|
|
7
7
|
* (e.g. a bare root). Idempotent enough — call once at spawn/boot. */
|
|
8
8
|
export declare function writeGoal(nodeId: string, text: string): void;
|
|
9
|
+
/** Write the goal ONLY if the node has none yet. This is how a bare root (no
|
|
10
|
+
* spawn prompt) acquires its mandate: the first real user message becomes the
|
|
11
|
+
* goal. Returns true when it wrote one, false when a goal already existed or
|
|
12
|
+
* the text was empty. Guarded so a later message never clobbers the mandate. */
|
|
13
|
+
export declare function captureGoalIfAbsent(nodeId: string, text: string): boolean;
|
|
14
|
+
/** Sentinel opening the fresh-revive kickoff message (see buildReviveKickoff).
|
|
15
|
+
* The goal-capture extension skips any input starting with this so a kickoff
|
|
16
|
+
* prompt is never mistaken for a user's first mandate. */
|
|
17
|
+
export declare const REVIVE_KICKOFF_SENTINEL = "You have been revived fresh after a context refresh";
|
|
9
18
|
/** The yield-message file — a short note `crtr node yield` records for the next
|
|
10
19
|
* revive ("on wake, do X"). Consumed (deleted) when the revive reads it. */
|
|
11
20
|
export declare function yieldMessagePath(nodeId: string): string;
|
|
@@ -15,9 +24,26 @@ export declare function writeYieldMessage(nodeId: string, text: string): void;
|
|
|
15
24
|
export declare function consumeYieldMessage(nodeId: string): string | null;
|
|
16
25
|
/** List the node's context/ dir (filenames, sorted). Empty when absent. */
|
|
17
26
|
export declare function listContextDir(nodeId: string): string[];
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
export interface ReviveBearings {
|
|
28
|
+
/** The one-shot yield note left by the prior self (already consumed/deleted). */
|
|
29
|
+
yieldMsg: string | null;
|
|
30
|
+
/** Coalesced digest of unread reports, or null when the feed was empty. The
|
|
31
|
+
* cursor has already been advanced past these. */
|
|
32
|
+
unreadDigest: string | null;
|
|
33
|
+
/** Persona-transition guidance to surface when the node's role was changed
|
|
34
|
+
* while it was away (its ack has already been committed), else null. */
|
|
35
|
+
driftGuidance: string | null;
|
|
36
|
+
}
|
|
37
|
+
/** Drain the one-shot revive bearings for `meta`: consume the yield note, advance
|
|
38
|
+
* the feed cursor past the unread reports, and capture+commit any external
|
|
39
|
+
* persona drift. The CONSUMING step of a fresh revive — the revive paths call it
|
|
40
|
+
* ONCE, then pass the result to buildReviveKickoff (which is then pure; building
|
|
41
|
+
* twice eats nothing). Calling drainBearings a second time would drain an
|
|
42
|
+
* already-empty note/feed, so ONLY the revive paths call it. */
|
|
43
|
+
export declare function drainBearings(meta: NodeMeta): ReviveBearings;
|
|
44
|
+
/** Assemble the auto-injected first message for a FRESH revive of `meta` from its
|
|
45
|
+
* already-drained `bearings` (see drainBearings) plus pure on-disk reads of the
|
|
46
|
+
* node's goal, roadmap, and context dir, framed so the revived node can rebuild
|
|
47
|
+
* its bearings in one turn. PURE: no state mutation, so calling it twice yields
|
|
48
|
+
* the same string and consumes nothing — drainBearings owns the one-shot reads. */
|
|
49
|
+
export declare function buildReviveKickoff(meta: NodeMeta, bearings: ReviveBearings): string;
|