@crouton-kit/crouter 0.3.14 → 0.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-root.d.ts +8 -0
- package/dist/build-root.js +30 -0
- package/dist/builtin-personas/design/base.md +3 -7
- package/dist/builtin-personas/design/orchestrator.md +4 -3
- package/dist/builtin-personas/developer/base.md +3 -7
- package/dist/builtin-personas/developer/orchestrator.md +5 -4
- package/dist/builtin-personas/explore/base.md +3 -7
- package/dist/builtin-personas/explore/orchestrator.md +1 -5
- package/dist/builtin-personas/general/base.md +2 -4
- package/dist/builtin-personas/general/orchestrator.md +2 -4
- package/dist/builtin-personas/lifecycle/resident.md +2 -0
- package/dist/builtin-personas/lifecycle/terminal.md +6 -0
- package/dist/builtin-personas/orchestration-kernel.md +42 -3
- package/dist/builtin-personas/plan/base.md +3 -5
- package/dist/builtin-personas/plan/orchestrator.md +5 -4
- package/dist/builtin-personas/plan/reviewers/architecture-fit/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/code-smells/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/security/base.md +9 -0
- package/dist/builtin-personas/review/base.md +3 -5
- package/dist/builtin-personas/review/orchestrator.md +2 -6
- package/dist/builtin-personas/runtime-base.md +3 -19
- package/dist/builtin-personas/spec/base.md +3 -5
- package/dist/builtin-personas/spec/orchestrator.md +4 -3
- package/dist/builtin-personas/spine/has-manager.md +10 -0
- package/dist/builtin-personas/spine/no-manager.md +2 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +96 -0
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +1 -1
- package/dist/builtin-skills/skills/spec/SKILL.md +2 -2
- package/dist/cli.js +6 -29
- package/dist/commands/attention.js +76 -7
- package/dist/commands/canvas-prune.d.ts +2 -0
- package/dist/commands/canvas-prune.js +66 -0
- package/dist/commands/canvas.js +5 -8
- package/dist/commands/chord.d.ts +2 -0
- package/dist/commands/chord.js +143 -0
- package/dist/commands/daemon.js +8 -5
- package/dist/commands/dashboard.js +2 -0
- package/dist/commands/human/prompts.js +28 -27
- package/dist/commands/human/queue.js +30 -14
- package/dist/commands/human/shared.d.ts +26 -21
- package/dist/commands/human/shared.js +44 -66
- package/dist/commands/human.js +4 -14
- package/dist/commands/node.d.ts +11 -0
- package/dist/commands/node.js +221 -98
- package/dist/commands/pkg/market-inspect.js +6 -4
- package/dist/commands/pkg/market-manage.js +10 -6
- package/dist/commands/pkg/market.js +2 -4
- package/dist/commands/pkg/plugin-inspect.js +6 -4
- package/dist/commands/pkg/plugin-manage.js +12 -7
- package/dist/commands/pkg/plugin.js +2 -4
- package/dist/commands/pkg.js +0 -4
- package/dist/commands/push.js +178 -15
- package/dist/commands/revive.js +5 -3
- package/dist/commands/skill/author.js +6 -4
- package/dist/commands/skill/find.js +8 -5
- package/dist/commands/skill/read.js +2 -0
- package/dist/commands/skill/state.js +6 -4
- package/dist/commands/skill.js +0 -6
- package/dist/commands/sys/config.js +21 -7
- package/dist/commands/sys/doctor.js +2 -0
- package/dist/commands/sys/update.js +4 -0
- package/dist/commands/sys.js +0 -6
- package/dist/commands/tmux-spread.d.ts +2 -0
- package/dist/commands/tmux-spread.js +130 -0
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
- package/dist/core/__tests__/child-followup.test.d.ts +1 -0
- package/dist/core/__tests__/child-followup.test.js +83 -0
- package/dist/core/__tests__/close.test.d.ts +1 -0
- package/dist/core/__tests__/close.test.js +148 -0
- package/dist/core/__tests__/context-intro.test.d.ts +1 -0
- package/dist/core/__tests__/context-intro.test.js +196 -0
- package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-boot.test.js +93 -0
- package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-liveness.test.js +223 -0
- package/dist/core/__tests__/focuses.test.d.ts +1 -0
- package/dist/core/__tests__/focuses.test.js +259 -0
- package/dist/core/__tests__/fork.test.d.ts +1 -0
- package/dist/core/__tests__/fork.test.js +91 -0
- package/dist/core/__tests__/home-session.test.d.ts +1 -0
- package/dist/core/__tests__/home-session.test.js +153 -0
- package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
- package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
- package/dist/core/__tests__/keystone.test.d.ts +1 -0
- package/dist/core/__tests__/keystone.test.js +185 -0
- package/dist/core/__tests__/kickoff.test.d.ts +1 -0
- package/dist/core/__tests__/kickoff.test.js +89 -0
- package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/lifecycle.test.js +178 -0
- package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
- package/dist/core/__tests__/listing-completeness.test.js +31 -0
- package/dist/core/__tests__/memory.test.d.ts +1 -0
- package/dist/core/__tests__/memory.test.js +152 -0
- package/dist/core/__tests__/migration.test.d.ts +1 -0
- package/dist/core/__tests__/migration.test.js +238 -0
- package/dist/core/__tests__/pane-column.test.d.ts +1 -0
- package/dist/core/__tests__/pane-column.test.js +153 -0
- package/dist/core/__tests__/passive-subscription.test.js +24 -1
- package/dist/core/__tests__/persona-compose.test.d.ts +1 -0
- package/dist/core/__tests__/persona-compose.test.js +53 -0
- package/dist/core/__tests__/persona-subkind.test.d.ts +1 -0
- package/dist/core/__tests__/persona-subkind.test.js +62 -0
- package/dist/core/__tests__/persona.test.d.ts +1 -0
- package/dist/core/__tests__/persona.test.js +107 -0
- package/dist/core/__tests__/placement-focus.test.d.ts +1 -0
- package/dist/core/__tests__/placement-focus.test.js +244 -0
- package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
- package/dist/core/__tests__/placement-reconcile.test.js +212 -0
- package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
- package/dist/core/__tests__/placement-revive.test.js +238 -0
- package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/placement-teardown.test.js +183 -0
- package/dist/core/__tests__/prune.test.d.ts +1 -0
- package/dist/core/__tests__/prune.test.js +116 -0
- package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
- package/dist/core/__tests__/push-final-guard.test.js +71 -0
- package/dist/core/__tests__/relaunch.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch.test.js +328 -0
- package/dist/core/__tests__/reset.test.js +26 -7
- package/dist/core/__tests__/revive.test.d.ts +1 -0
- package/dist/core/__tests__/revive.test.js +217 -0
- package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
- package/dist/core/__tests__/spawn-root.test.js +73 -0
- package/dist/core/__tests__/steer-note.test.d.ts +1 -0
- package/dist/core/__tests__/steer-note.test.js +39 -0
- package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
- package/dist/core/__tests__/stop-guard.test.js +82 -0
- package/dist/core/__tests__/subcommand-tier.test.js +35 -33
- package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
- package/dist/core/__tests__/tmux-surface.test.js +106 -0
- package/dist/core/__tests__/unknown-path.test.js +8 -2
- package/dist/core/canvas/attention.d.ts +10 -0
- package/dist/core/canvas/attention.js +40 -0
- package/dist/core/canvas/canvas.d.ts +66 -7
- package/dist/core/canvas/canvas.js +209 -21
- package/dist/core/canvas/db.d.ts +8 -0
- package/dist/core/canvas/db.js +206 -4
- package/dist/core/canvas/focuses.d.ts +22 -0
- package/dist/core/canvas/focuses.js +80 -0
- package/dist/core/canvas/index.d.ts +3 -0
- package/dist/core/canvas/index.js +3 -0
- package/dist/core/canvas/labels.d.ts +27 -0
- package/dist/core/canvas/labels.js +36 -0
- package/dist/core/canvas/render.js +25 -10
- package/dist/core/canvas/telemetry.d.ts +14 -0
- package/dist/core/canvas/telemetry.js +35 -0
- package/dist/core/canvas/types.d.ts +115 -12
- package/dist/core/command.d.ts +25 -1
- package/dist/core/command.js +23 -15
- package/dist/core/config.js +36 -2
- package/dist/core/feed/feed.js +3 -3
- package/dist/core/feed/inbox.d.ts +3 -1
- package/dist/core/feed/inbox.js +45 -5
- package/dist/core/feed/passive.js +24 -11
- package/dist/core/help.d.ts +26 -13
- package/dist/core/help.js +44 -37
- package/dist/core/personas/index.d.ts +1 -1
- package/dist/core/personas/index.js +1 -1
- package/dist/core/personas/loader.d.ts +40 -1
- package/dist/core/personas/loader.js +63 -1
- package/dist/core/personas/resolve.d.ts +13 -6
- package/dist/core/personas/resolve.js +46 -34
- package/dist/core/runtime/bearings.d.ts +20 -0
- package/dist/core/runtime/bearings.js +92 -0
- package/dist/core/runtime/close.d.ts +14 -0
- package/dist/core/runtime/close.js +151 -0
- package/dist/core/runtime/demote.js +27 -10
- package/dist/core/runtime/front-door.js +1 -1
- package/dist/core/runtime/kickoff.d.ts +23 -6
- package/dist/core/runtime/kickoff.js +92 -36
- package/dist/core/runtime/launch.d.ts +24 -12
- package/dist/core/runtime/launch.js +75 -19
- package/dist/core/runtime/lifecycle.d.ts +13 -0
- package/dist/core/runtime/lifecycle.js +86 -0
- package/dist/core/runtime/memory.d.ts +43 -0
- package/dist/core/runtime/memory.js +165 -0
- package/dist/core/runtime/naming.d.ts +22 -0
- package/dist/core/runtime/naming.js +166 -0
- package/dist/core/runtime/nodes.d.ts +32 -1
- package/dist/core/runtime/nodes.js +60 -10
- package/dist/core/runtime/persona.d.ts +25 -0
- package/dist/core/runtime/persona.js +139 -0
- package/dist/core/runtime/placement.d.ts +287 -0
- package/dist/core/runtime/placement.js +663 -0
- package/dist/core/runtime/presence.d.ts +7 -15
- package/dist/core/runtime/presence.js +90 -66
- package/dist/core/runtime/promote.d.ts +14 -7
- package/dist/core/runtime/promote.js +57 -67
- package/dist/core/runtime/reset.d.ts +47 -4
- package/dist/core/runtime/reset.js +223 -52
- package/dist/core/runtime/revive.d.ts +26 -2
- package/dist/core/runtime/revive.js +166 -39
- package/dist/core/runtime/spawn.d.ts +20 -5
- package/dist/core/runtime/spawn.js +163 -43
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +18 -8
- package/dist/core/runtime/tmux.d.ts +100 -14
- package/dist/core/runtime/tmux.js +201 -28
- package/dist/core/spawn.js +15 -0
- package/dist/daemon/crtrd.d.ts +12 -1
- package/dist/daemon/crtrd.js +152 -34
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +266 -0
- package/dist/pi-extensions/canvas-commands.js +16 -13
- package/dist/pi-extensions/canvas-context-intro.d.ts +70 -0
- package/dist/pi-extensions/canvas-context-intro.js +164 -0
- package/dist/pi-extensions/canvas-goal-capture.d.ts +3 -0
- package/dist/pi-extensions/canvas-goal-capture.js +15 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +11 -0
- package/dist/pi-extensions/canvas-nav.d.ts +12 -4
- package/dist/pi-extensions/canvas-nav.js +586 -262
- package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
- package/dist/pi-extensions/canvas-stophook.js +344 -228
- package/dist/types.d.ts +28 -0
- package/dist/types.js +16 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -13,14 +13,15 @@
|
|
|
13
13
|
// message) — falling back to a short note when it never reported.
|
|
14
14
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
|
-
import { getNode, updateNode } from '../canvas/index.js';
|
|
16
|
+
import { getNode, setPresence, updateNode, setFocusOccupant, fullName } from '../canvas/index.js';
|
|
17
17
|
import { reportsDir } from '../canvas/paths.js';
|
|
18
18
|
import { pushFinal } from '../feed/feed.js';
|
|
19
19
|
import { spawnNode } from './nodes.js';
|
|
20
20
|
import { buildLaunchSpec, buildPiArgv } from './launch.js';
|
|
21
|
-
import {
|
|
21
|
+
import { piCommand, paneLocation, nodeSession } from './tmux.js';
|
|
22
22
|
import { FRONT_DOOR_ENV } from './front-door.js';
|
|
23
23
|
import { getFocus, setFocus } from './presence.js';
|
|
24
|
+
import { focusOf, recycleFocusPane } from './placement.js';
|
|
24
25
|
import { ensureDaemon } from '../../daemon/manage.js';
|
|
25
26
|
/** The agent's most recent surfaced message: the newest reports/*.md body with
|
|
26
27
|
* its YAML frontmatter stripped. Empty string when the node never reported. */
|
|
@@ -70,20 +71,21 @@ export async function demoteNode(nodeId, callerPane) {
|
|
|
70
71
|
finalized = true;
|
|
71
72
|
}
|
|
72
73
|
catch { /* recycle the pane even if the report failed */ }
|
|
73
|
-
//
|
|
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);
|
|
74
78
|
try {
|
|
75
|
-
|
|
79
|
+
setPresence(nodeId, { pane: null, window: null, tmux_session: null });
|
|
76
80
|
}
|
|
77
81
|
catch { /* best-effort */ }
|
|
78
|
-
if (getFocus() === nodeId)
|
|
79
|
-
setFocus('');
|
|
80
82
|
// 2 + 3. Recycle — boot a fresh resident root in the SAME pane.
|
|
81
83
|
try {
|
|
82
84
|
ensureDaemon();
|
|
83
85
|
}
|
|
84
86
|
catch { /* daemon is best-effort */ }
|
|
85
87
|
const loc = paneLocation(pane);
|
|
86
|
-
const { launch } = buildLaunchSpec('general', 'base');
|
|
88
|
+
const { launch } = buildLaunchSpec('general', 'base', { lifecycle: 'resident', hasManager: false });
|
|
87
89
|
const root = spawnNode({
|
|
88
90
|
kind: 'general',
|
|
89
91
|
mode: 'base',
|
|
@@ -93,11 +95,26 @@ export async function demoteNode(nodeId, callerPane) {
|
|
|
93
95
|
parent: null,
|
|
94
96
|
launch,
|
|
95
97
|
});
|
|
96
|
-
|
|
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('');
|
|
98
113
|
const fresh = getNode(root.node_id);
|
|
99
114
|
const inv = buildPiArgv(fresh);
|
|
100
115
|
const env = { ...inv.env, CRTR_ROOT_SESSION: nodeSession(), [FRONT_DOOR_ENV]: '1' };
|
|
101
|
-
const ok =
|
|
116
|
+
const ok = recycleFocusPane(root.node_id, pane, {
|
|
117
|
+
command: piCommand(inv.argv), env, cwd: meta.cwd, name: fullName(fresh), resuming: false,
|
|
118
|
+
});
|
|
102
119
|
return { demoted: ok, finalized, newRoot: root.node_id, delivered };
|
|
103
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
|
}
|
|
@@ -24,9 +24,26 @@ export declare function writeYieldMessage(nodeId: string, text: string): void;
|
|
|
24
24
|
export declare function consumeYieldMessage(nodeId: string): string | null;
|
|
25
25
|
/** List the node's context/ dir (filenames, sorted). Empty when absent. */
|
|
26
26
|
export declare function listContextDir(nodeId: string): string[];
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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;
|
|
@@ -6,18 +6,22 @@
|
|
|
6
6
|
// conversation already holds the context).
|
|
7
7
|
//
|
|
8
8
|
// Layout (the framing a revived node sees):
|
|
9
|
-
// <
|
|
10
|
-
// <roadmap file=…>…</roadmap> its evolving plan
|
|
9
|
+
// <roadmap file=…>…</roadmap> its evolving plan — the source of truth
|
|
11
10
|
// <context-dir path=…>…</context-dir> what artifacts exist on disk
|
|
12
11
|
// <feed>Awaiting N nodes … digest</feed> who it waits on + unread reports
|
|
13
12
|
// <yield-message>…</yield-message> the note its prior self left on yield
|
|
14
13
|
//
|
|
15
|
-
// The
|
|
14
|
+
// The roadmap (NOT the original spawn prompt) carries the goal on a refresh: its
|
|
15
|
+
// frozen core holds goal + exit criteria, its body the live plan. context/
|
|
16
|
+
// initial-prompt.md is NEVER injected into a node's prompts — it lives on disk
|
|
17
|
+
// purely as a log of the original mandate; by the time a node is running it is
|
|
18
|
+
// usually stale, and the roadmap is the doc the node keeps current. The
|
|
16
19
|
// yield-message is one-shot (consumed on the next revive).
|
|
17
20
|
import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, readdirSync, } from 'node:fs';
|
|
18
21
|
import { join } from 'node:path';
|
|
19
|
-
import { contextDir, getNode, subscriptionsOf, } from '../canvas/index.js';
|
|
22
|
+
import { contextDir, getNode, subscriptionsOf, subscribersOf, } from '../canvas/index.js';
|
|
20
23
|
import { readRoadmap, roadmapPath } from './roadmap.js';
|
|
24
|
+
import { personaDrift, commitPersonaAck } from './persona.js';
|
|
21
25
|
import { readInboxSince, readCursor, writeCursor, coalesce, } from '../feed/inbox.js';
|
|
22
26
|
// ---------------------------------------------------------------------------
|
|
23
27
|
// Companion context files: the goal (the spawning mandate) and the one-shot
|
|
@@ -91,62 +95,114 @@ export function listContextDir(nodeId) {
|
|
|
91
95
|
return [];
|
|
92
96
|
return readdirSync(dir).sort();
|
|
93
97
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
/** Drain the one-shot revive bearings for `meta`: consume the yield note, advance
|
|
99
|
+
* the feed cursor past the unread reports, and capture+commit any external
|
|
100
|
+
* persona drift. The CONSUMING step of a fresh revive — the revive paths call it
|
|
101
|
+
* ONCE, then pass the result to buildReviveKickoff (which is then pure; building
|
|
102
|
+
* twice eats nothing). Calling drainBearings a second time would drain an
|
|
103
|
+
* already-empty note/feed, so ONLY the revive paths call it. */
|
|
104
|
+
export function drainBearings(meta) {
|
|
105
|
+
const nodeId = meta.node_id;
|
|
106
|
+
// Consume the one-shot yield note (deleted on read) BEFORE the kickoff lists
|
|
107
|
+
// the context dir, so it never shows up there.
|
|
108
|
+
const yieldMsg = consumeYieldMessage(nodeId);
|
|
109
|
+
// Drain the feed: read unread since the cursor and advance it past them, so a
|
|
110
|
+
// later `crtr feed read` shows only what arrives afterward.
|
|
111
|
+
const cursor = readCursor(nodeId);
|
|
112
|
+
const entries = readInboxSince(nodeId, cursor);
|
|
113
|
+
let unreadDigest = null;
|
|
114
|
+
if (entries.length > 0) {
|
|
115
|
+
writeCursor(nodeId, entries[entries.length - 1].ts);
|
|
116
|
+
unreadDigest = coalesce(entries);
|
|
117
|
+
}
|
|
118
|
+
// Capture + commit any external persona drift (the second of the two delivery
|
|
119
|
+
// sites). Committing the ack here is the mutation; the guidance is surfaced by
|
|
120
|
+
// the pure builder from this captured value.
|
|
121
|
+
const drift = personaDrift(nodeId);
|
|
122
|
+
let driftGuidance = null;
|
|
123
|
+
if (drift !== null) {
|
|
124
|
+
driftGuidance = drift.guidance;
|
|
125
|
+
commitPersonaAck(nodeId, drift.to);
|
|
126
|
+
}
|
|
127
|
+
return { yieldMsg, unreadDigest, driftGuidance };
|
|
128
|
+
}
|
|
129
|
+
/** Render the <feed> block PURELY: the live "awaiting" roster (a read) plus the
|
|
130
|
+
* already-drained unread digest (from drainBearings). No cursor write here. */
|
|
131
|
+
function feedBlock(nodeId, unreadDigest) {
|
|
100
132
|
// Awaiting = active subscriptions whose publisher is still live (active|idle).
|
|
101
133
|
const awaiting = subscriptionsOf(nodeId)
|
|
102
134
|
.filter((s) => s.active)
|
|
103
135
|
.map((s) => getNode(s.node_id))
|
|
104
136
|
.filter((m) => m !== null && (m.status === 'active' || m.status === 'idle'));
|
|
105
137
|
const lines = [];
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
138
|
+
if (awaiting.length > 0) {
|
|
139
|
+
const n = awaiting.length;
|
|
140
|
+
const subj = n === 1 ? 'it is' : 'they are';
|
|
141
|
+
const pron = n === 1 ? 'it' : 'they';
|
|
142
|
+
const verb = n === 1 ? 'pushes' : 'push';
|
|
143
|
+
// State aliveness + the automatic wake at the source. Bare status ("— active")
|
|
144
|
+
// left earlier revives unsure whether the worker was really live, so they
|
|
145
|
+
// burned a turn on `feed read`/`feed peek` to confirm. Asserting it here
|
|
146
|
+
// removes the reason to check.
|
|
147
|
+
lines.push(`Awaiting ${n} node${n === 1 ? '' : 's'} — ${subj} alive and running right now, and will wake you the moment ${pron} ${verb}. The wake is automatic; nothing to check, poll, or verify.`);
|
|
148
|
+
for (const m of awaiting)
|
|
149
|
+
lines.push(` - ${m.name} (${m.node_id}) — ${m.status}`);
|
|
150
|
+
lines.push('', unreadDigest ??
|
|
151
|
+
'(no unread reports yet — expected while they run: a worker leaves no pointer until it pushes, so an empty feed means still working, not stalled)');
|
|
114
152
|
}
|
|
115
153
|
else {
|
|
116
|
-
lines.push('
|
|
154
|
+
lines.push('Awaiting 0 nodes.');
|
|
155
|
+
lines.push('', unreadDigest ?? '(no unread reports)');
|
|
117
156
|
}
|
|
118
157
|
return `<feed>\n${lines.join('\n')}\n</feed>`;
|
|
119
158
|
}
|
|
120
159
|
// ---------------------------------------------------------------------------
|
|
121
160
|
// buildReviveKickoff — assemble the full fresh-revive first message.
|
|
122
161
|
// ---------------------------------------------------------------------------
|
|
123
|
-
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* turn.
|
|
127
|
-
*
|
|
128
|
-
export function buildReviveKickoff(meta) {
|
|
162
|
+
/** Assemble the auto-injected first message for a FRESH revive of `meta` from its
|
|
163
|
+
* already-drained `bearings` (see drainBearings) plus pure on-disk reads of the
|
|
164
|
+
* node's goal, roadmap, and context dir, framed so the revived node can rebuild
|
|
165
|
+
* its bearings in one turn. PURE: no state mutation, so calling it twice yields
|
|
166
|
+
* the same string and consumes nothing — drainBearings owns the one-shot reads. */
|
|
167
|
+
export function buildReviveKickoff(meta, bearings) {
|
|
129
168
|
const nodeId = meta.node_id;
|
|
130
|
-
// Consume the one-shot yield note first so it never shows in the dir listing.
|
|
131
|
-
const yieldMsg = consumeYieldMessage(nodeId);
|
|
132
169
|
const parts = [
|
|
133
170
|
`${REVIVE_KICKOFF_SENTINEL} — your previous in-memory ` +
|
|
134
171
|
'context is gone, by design. Everything below was just read from disk; it is your ' +
|
|
135
172
|
'full bearings. Rebuild from it and continue toward your goal.',
|
|
136
173
|
];
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
174
|
+
// The roadmap is the source of truth on a fresh revive: its frozen core holds
|
|
175
|
+
// the goal/exit criteria, its body the live plan the node kept current. The
|
|
176
|
+
// original spawn prompt (context/initial-prompt.md) is deliberately NOT injected
|
|
177
|
+
// — it lives on disk only as a log, and by now it is usually stale.
|
|
141
178
|
const roadmap = readRoadmap(nodeId);
|
|
142
179
|
parts.push(`<roadmap file="${roadmapPath(nodeId)}">\n${roadmap !== null && roadmap.trim() !== '' ? roadmap.trim() : '(no roadmap on disk yet)'}\n</roadmap>`);
|
|
143
180
|
const files = listContextDir(nodeId);
|
|
144
181
|
parts.push(`<context-dir path="${contextDir(nodeId)}">\n${files.length > 0 ? files.join('\n') : '(empty)'}\n</context-dir>`);
|
|
145
|
-
parts.push(feedBlock(nodeId));
|
|
146
|
-
parts.push(yieldMsg !== null
|
|
147
|
-
? `<yield-message>\n${yieldMsg.trim()}\n</yield-message>`
|
|
182
|
+
parts.push(feedBlock(nodeId, bearings.unreadDigest));
|
|
183
|
+
parts.push(bearings.yieldMsg !== null
|
|
184
|
+
? `<yield-message>\n${bearings.yieldMsg.trim()}\n</yield-message>`
|
|
148
185
|
: '<yield-message/>');
|
|
149
|
-
|
|
150
|
-
|
|
186
|
+
// A node that reports UP the spine (has subscribers awaiting its result)
|
|
187
|
+
// finishes with `push final`. A human-attended node (no subscribers — a root
|
|
188
|
+
// conversation working directly with the user) has no result to submit and
|
|
189
|
+
// must not be told to finish: it stays resident and keeps working with the
|
|
190
|
+
// user.
|
|
191
|
+
const reportsUp = subscribersOf(nodeId).length > 0;
|
|
192
|
+
parts.push(reportsUp
|
|
193
|
+
? 'If there is work to do, perform it. Otherwise stop — `crtr push final "<result>"` ' +
|
|
194
|
+
'if the goal is met, or end your turn to stay dormant awaiting your workers.'
|
|
195
|
+
: 'If there is work to do, perform it. Otherwise end your turn — you are working ' +
|
|
196
|
+
'directly with the user, so stay available and continue the conversation when they ' +
|
|
197
|
+
'write back.');
|
|
198
|
+
// Persona-transition catch-up. If the node's mode/lifecycle was changed
|
|
199
|
+
// EXTERNALLY while it was dormant (e.g. a human ran `crtr node lifecycle` /
|
|
200
|
+
// `node promote --node` on it), it never saw the turn_end injector. drainBearings
|
|
201
|
+
// captured the guidance for its new persona and committed the ack (the second
|
|
202
|
+
// and only other delivery site); we just surface it. A clean fresh revive has
|
|
203
|
+
// no drift, so this is empty unless a real external change happened.
|
|
204
|
+
if (bearings.driftGuidance !== null) {
|
|
205
|
+
parts.push(`<persona-transition>\nYour role was changed while you were away. ${bearings.driftGuidance}\n</persona-transition>`);
|
|
206
|
+
}
|
|
151
207
|
return parts.join('\n\n');
|
|
152
208
|
}
|
|
@@ -1,23 +1,33 @@
|
|
|
1
|
-
import type { NodeMeta, LaunchSpec, Mode } from '../canvas/index.js';
|
|
1
|
+
import type { NodeMeta, LaunchSpec, Mode, Lifecycle } from '../canvas/index.js';
|
|
2
2
|
export declare const CANVAS_STOPHOOK_PATH: string;
|
|
3
3
|
export declare const CANVAS_INBOX_WATCHER_PATH: string;
|
|
4
4
|
export declare const CANVAS_NAV_PATH: string;
|
|
5
5
|
export declare const CANVAS_GOAL_CAPTURE_PATH: string;
|
|
6
6
|
export declare const CANVAS_PASSIVE_CONTEXT_PATH: string;
|
|
7
|
+
export declare const CANVAS_CONTEXT_INTRO_PATH: string;
|
|
7
8
|
export declare const CANVAS_COMMANDS_PATH: string;
|
|
8
9
|
/** The canvas extensions every node loads, in order: stophook (routing +
|
|
9
10
|
* telemetry + session-id capture), inbox-watcher (wake), nav (in-editor
|
|
10
11
|
* graph chrome), goal-capture (persist the first user message as the goal),
|
|
11
12
|
* passive-context (drain passive backlog as pre-text on the next message),
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* context-intro (inject the <crtr-context> bearings block as its own session
|
|
14
|
+
* message, once per brand-new chat), commands (the /promote slash-command).
|
|
15
|
+
* All self-gate on CRTR_NODE_ID. goal-capture precedes passive-context so it
|
|
16
|
+
* reads the raw user text. */
|
|
14
17
|
export declare const CANVAS_EXTENSIONS: string[];
|
|
15
18
|
/** Bare model aliases resolve to the anthropic provider under pi (avoids the
|
|
16
19
|
* bedrock default). Anything with a `/` or an unknown name passes through. */
|
|
17
20
|
export declare function normalizeModel(model: string): string;
|
|
18
|
-
/** Compose a node's full pi launch recipe from its persona. The
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
+
/** Compose a node's full pi launch recipe from its persona. The system prompt
|
|
22
|
+
* is composed from FOUR inputs: kind×mode (the persona body) plus lifecycle
|
|
23
|
+
* (terminal/resident — the finish contract) and spine position (hasManager —
|
|
24
|
+
* whether the push-up family is taught at all). Callers pass the authoritative
|
|
25
|
+
* lifecycle + hasManager (`parent !== null`) so a polymorph/flip rebuilds the
|
|
26
|
+
* prompt faithfully. The two canvas extensions are always first; persona-
|
|
27
|
+
* declared extensions follow. */
|
|
28
|
+
export declare function buildLaunchSpec(kind: string, mode: Mode, opts: {
|
|
29
|
+
lifecycle: Lifecycle;
|
|
30
|
+
hasManager: boolean;
|
|
21
31
|
extraEnv?: Record<string, string>;
|
|
22
32
|
}): {
|
|
23
33
|
launch: LaunchSpec;
|
|
@@ -30,16 +40,18 @@ export interface PiInvocation {
|
|
|
30
40
|
/** env to merge into the process. */
|
|
31
41
|
env: Record<string, string>;
|
|
32
42
|
}
|
|
33
|
-
/** The pi session display name — the editor label in the top-left. Shows the
|
|
34
|
-
* node's name plus its current mode so base vs orchestrator reads at a glance
|
|
35
|
-
* (e.g. `developer (orchestrator)`). Recomputed from `meta.mode` on every
|
|
36
|
-
* revive, so a base→orchestrator polymorph updates the label. */
|
|
37
|
-
export declare function editorLabel(meta: NodeMeta): string;
|
|
38
43
|
/** Construct the pi invocation for a node.
|
|
39
44
|
* - fresh start: pass `prompt` (the node's first user message), no resume.
|
|
40
|
-
* -
|
|
45
|
+
* - fork start: pass `forkFrom` (absolute .jsonl path or partial uuid) to `--fork`
|
|
46
|
+
* — pi COPIES that conversation into a NEW session for this node, then `prompt`
|
|
47
|
+
* is delivered as the next message. One-shot at birth: the node thereafter
|
|
48
|
+
* captures its OWN pi_session_file and revives by `--session` like any other.
|
|
49
|
+
* - revive idle/done: pass `resumeSessionPath` (absolute .jsonl path, preferred)
|
|
50
|
+
* or `resumeSessionId` (bare uuid fallback) to `--session` (keeps conversation).
|
|
41
51
|
* - refresh-yield: fresh again (no resume) — the node re-reads its roadmap. */
|
|
42
52
|
export declare function buildPiArgv(meta: NodeMeta, opts?: {
|
|
43
53
|
prompt?: string;
|
|
44
54
|
resumeSessionId?: string;
|
|
55
|
+
resumeSessionPath?: string;
|
|
56
|
+
forkFrom?: string;
|
|
45
57
|
}): PiInvocation;
|