@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
|
@@ -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
|
|
@@ -41,6 +45,24 @@ export function writeGoal(nodeId, text) {
|
|
|
41
45
|
mkdirSync(contextDir(nodeId), { recursive: true });
|
|
42
46
|
writeFileSync(goalPath(nodeId), body + '\n', 'utf8');
|
|
43
47
|
}
|
|
48
|
+
/** Write the goal ONLY if the node has none yet. This is how a bare root (no
|
|
49
|
+
* spawn prompt) acquires its mandate: the first real user message becomes the
|
|
50
|
+
* goal. Returns true when it wrote one, false when a goal already existed or
|
|
51
|
+
* the text was empty. Guarded so a later message never clobbers the mandate. */
|
|
52
|
+
export function captureGoalIfAbsent(nodeId, text) {
|
|
53
|
+
const existing = readGoal(nodeId);
|
|
54
|
+
if (existing !== null && existing.trim() !== '')
|
|
55
|
+
return false;
|
|
56
|
+
const body = text.trim();
|
|
57
|
+
if (body === '')
|
|
58
|
+
return false;
|
|
59
|
+
writeGoal(nodeId, body);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
/** Sentinel opening the fresh-revive kickoff message (see buildReviveKickoff).
|
|
63
|
+
* The goal-capture extension skips any input starting with this so a kickoff
|
|
64
|
+
* prompt is never mistaken for a user's first mandate. */
|
|
65
|
+
export const REVIVE_KICKOFF_SENTINEL = 'You have been revived fresh after a context refresh';
|
|
44
66
|
/** The yield-message file — a short note `crtr node yield` records for the next
|
|
45
67
|
* revive ("on wake, do X"). Consumed (deleted) when the revive reads it. */
|
|
46
68
|
export function yieldMessagePath(nodeId) {
|
|
@@ -73,62 +95,114 @@ export function listContextDir(nodeId) {
|
|
|
73
95
|
return [];
|
|
74
96
|
return readdirSync(dir).sort();
|
|
75
97
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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) {
|
|
82
132
|
// Awaiting = active subscriptions whose publisher is still live (active|idle).
|
|
83
133
|
const awaiting = subscriptionsOf(nodeId)
|
|
84
134
|
.filter((s) => s.active)
|
|
85
135
|
.map((s) => getNode(s.node_id))
|
|
86
136
|
.filter((m) => m !== null && (m.status === 'active' || m.status === 'idle'));
|
|
87
137
|
const lines = [];
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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)');
|
|
96
152
|
}
|
|
97
153
|
else {
|
|
98
|
-
lines.push('
|
|
154
|
+
lines.push('Awaiting 0 nodes.');
|
|
155
|
+
lines.push('', unreadDigest ?? '(no unread reports)');
|
|
99
156
|
}
|
|
100
157
|
return `<feed>\n${lines.join('\n')}\n</feed>`;
|
|
101
158
|
}
|
|
102
159
|
// ---------------------------------------------------------------------------
|
|
103
160
|
// buildReviveKickoff — assemble the full fresh-revive first message.
|
|
104
161
|
// ---------------------------------------------------------------------------
|
|
105
|
-
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* turn.
|
|
109
|
-
*
|
|
110
|
-
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) {
|
|
111
168
|
const nodeId = meta.node_id;
|
|
112
|
-
// Consume the one-shot yield note first so it never shows in the dir listing.
|
|
113
|
-
const yieldMsg = consumeYieldMessage(nodeId);
|
|
114
169
|
const parts = [
|
|
115
|
-
|
|
170
|
+
`${REVIVE_KICKOFF_SENTINEL} — your previous in-memory ` +
|
|
116
171
|
'context is gone, by design. Everything below was just read from disk; it is your ' +
|
|
117
172
|
'full bearings. Rebuild from it and continue toward your goal.',
|
|
118
173
|
];
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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.
|
|
123
178
|
const roadmap = readRoadmap(nodeId);
|
|
124
179
|
parts.push(`<roadmap file="${roadmapPath(nodeId)}">\n${roadmap !== null && roadmap.trim() !== '' ? roadmap.trim() : '(no roadmap on disk yet)'}\n</roadmap>`);
|
|
125
180
|
const files = listContextDir(nodeId);
|
|
126
181
|
parts.push(`<context-dir path="${contextDir(nodeId)}">\n${files.length > 0 ? files.join('\n') : '(empty)'}\n</context-dir>`);
|
|
127
|
-
parts.push(feedBlock(nodeId));
|
|
128
|
-
parts.push(yieldMsg !== null
|
|
129
|
-
? `<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>`
|
|
130
185
|
: '<yield-message/>');
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
}
|
|
133
207
|
return parts.join('\n\n');
|
|
134
208
|
}
|
|
@@ -1,17 +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
|
+
export declare const CANVAS_GOAL_CAPTURE_PATH: string;
|
|
6
|
+
export declare const CANVAS_PASSIVE_CONTEXT_PATH: string;
|
|
7
|
+
export declare const CANVAS_CONTEXT_INTRO_PATH: string;
|
|
8
|
+
export declare const CANVAS_COMMANDS_PATH: string;
|
|
5
9
|
/** The canvas extensions every node loads, in order: stophook (routing +
|
|
6
10
|
* telemetry + session-id capture), inbox-watcher (wake), nav (in-editor
|
|
7
|
-
* graph chrome)
|
|
11
|
+
* graph chrome), goal-capture (persist the first user message as the goal),
|
|
12
|
+
* passive-context (drain passive backlog as pre-text on the next message),
|
|
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. */
|
|
8
17
|
export declare const CANVAS_EXTENSIONS: string[];
|
|
9
18
|
/** Bare model aliases resolve to the anthropic provider under pi (avoids the
|
|
10
19
|
* bedrock default). Anything with a `/` or an unknown name passes through. */
|
|
11
20
|
export declare function normalizeModel(model: string): string;
|
|
12
|
-
/** Compose a node's full pi launch recipe from its persona. The
|
|
13
|
-
*
|
|
14
|
-
|
|
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;
|
|
15
31
|
extraEnv?: Record<string, string>;
|
|
16
32
|
}): {
|
|
17
33
|
launch: LaunchSpec;
|
|
@@ -26,9 +42,16 @@ export interface PiInvocation {
|
|
|
26
42
|
}
|
|
27
43
|
/** Construct the pi invocation for a node.
|
|
28
44
|
* - fresh start: pass `prompt` (the node's first user message), no resume.
|
|
29
|
-
* -
|
|
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).
|
|
30
51
|
* - refresh-yield: fresh again (no resume) — the node re-reads its roadmap. */
|
|
31
52
|
export declare function buildPiArgv(meta: NodeMeta, opts?: {
|
|
32
53
|
prompt?: string;
|
|
33
54
|
resumeSessionId?: string;
|
|
55
|
+
resumeSessionPath?: string;
|
|
56
|
+
forkFrom?: string;
|
|
34
57
|
}): PiInvocation;
|
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
//
|
|
3
3
|
// pi-only. No claude branch — we are a super-opinionated system. A node's
|
|
4
4
|
// LaunchSpec (persisted in meta.json) is the canonical recipe the daemon
|
|
5
|
-
// replays to revive it faithfully: `--
|
|
6
|
-
// its conversation), or fresh (against the context dir) for a refresh-yield.
|
|
5
|
+
// replays to revive it faithfully: `--session <id>` to wake a done/idle node
|
|
6
|
+
// (keeps its conversation), or fresh (against the context dir) for a refresh-yield.
|
|
7
7
|
// The spec is rewritten on every polymorph (base→orchestrator) so a node
|
|
8
8
|
// always comes back as its *current* self.
|
|
9
|
-
import { existsSync } from 'node:fs';
|
|
9
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import { dirname, join } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { resolve as resolvePersona } from '../personas/index.js';
|
|
13
13
|
import { nodeEnv } from './nodes.js';
|
|
14
|
+
import { editorLabel } from '../canvas/index.js';
|
|
15
|
+
import { nodeDir } from '../canvas/paths.js';
|
|
14
16
|
// ---------------------------------------------------------------------------
|
|
15
17
|
// The two canvas pi-extensions every node loads. They self-gate on the live
|
|
16
18
|
// {kind,mode} env, so the worker→orchestrator polymorph flips hook behavior
|
|
@@ -27,13 +29,26 @@ function resolveExtension(name) {
|
|
|
27
29
|
export const CANVAS_STOPHOOK_PATH = resolveExtension('canvas-stophook');
|
|
28
30
|
export const CANVAS_INBOX_WATCHER_PATH = resolveExtension('canvas-inbox-watcher');
|
|
29
31
|
export const CANVAS_NAV_PATH = resolveExtension('canvas-nav');
|
|
32
|
+
export const CANVAS_GOAL_CAPTURE_PATH = resolveExtension('canvas-goal-capture');
|
|
33
|
+
export const CANVAS_PASSIVE_CONTEXT_PATH = resolveExtension('canvas-passive-context');
|
|
34
|
+
export const CANVAS_CONTEXT_INTRO_PATH = resolveExtension('canvas-context-intro');
|
|
35
|
+
export const CANVAS_COMMANDS_PATH = resolveExtension('canvas-commands');
|
|
30
36
|
/** The canvas extensions every node loads, in order: stophook (routing +
|
|
31
37
|
* telemetry + session-id capture), inbox-watcher (wake), nav (in-editor
|
|
32
|
-
* graph chrome)
|
|
38
|
+
* graph chrome), goal-capture (persist the first user message as the goal),
|
|
39
|
+
* passive-context (drain passive backlog as pre-text on the next message),
|
|
40
|
+
* context-intro (inject the <crtr-context> bearings block as its own session
|
|
41
|
+
* message, once per brand-new chat), commands (the /promote slash-command).
|
|
42
|
+
* All self-gate on CRTR_NODE_ID. goal-capture precedes passive-context so it
|
|
43
|
+
* reads the raw user text. */
|
|
33
44
|
export const CANVAS_EXTENSIONS = [
|
|
34
45
|
CANVAS_STOPHOOK_PATH,
|
|
35
46
|
CANVAS_INBOX_WATCHER_PATH,
|
|
36
47
|
CANVAS_NAV_PATH,
|
|
48
|
+
CANVAS_GOAL_CAPTURE_PATH,
|
|
49
|
+
CANVAS_PASSIVE_CONTEXT_PATH,
|
|
50
|
+
CANVAS_CONTEXT_INTRO_PATH,
|
|
51
|
+
CANVAS_COMMANDS_PATH,
|
|
37
52
|
];
|
|
38
53
|
/** Bare model aliases resolve to the anthropic provider under pi (avoids the
|
|
39
54
|
* bedrock default). Anything with a `/` or an unknown name passes through. */
|
|
@@ -46,10 +61,15 @@ export function normalizeModel(model) {
|
|
|
46
61
|
// ---------------------------------------------------------------------------
|
|
47
62
|
// Build the launch spec from {kind, mode}
|
|
48
63
|
// ---------------------------------------------------------------------------
|
|
49
|
-
/** Compose a node's full pi launch recipe from its persona. The
|
|
50
|
-
*
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
/** Compose a node's full pi launch recipe from its persona. The system prompt
|
|
65
|
+
* is composed from FOUR inputs: kind×mode (the persona body) plus lifecycle
|
|
66
|
+
* (terminal/resident — the finish contract) and spine position (hasManager —
|
|
67
|
+
* whether the push-up family is taught at all). Callers pass the authoritative
|
|
68
|
+
* lifecycle + hasManager (`parent !== null`) so a polymorph/flip rebuilds the
|
|
69
|
+
* prompt faithfully. The two canvas extensions are always first; persona-
|
|
70
|
+
* declared extensions follow. */
|
|
71
|
+
export function buildLaunchSpec(kind, mode, opts) {
|
|
72
|
+
const p = resolvePersona(kind, mode, { lifecycle: opts.lifecycle, hasManager: opts.hasManager });
|
|
53
73
|
const launch = {
|
|
54
74
|
model: p.model !== undefined ? normalizeModel(p.model) : undefined,
|
|
55
75
|
tools: p.tools,
|
|
@@ -59,9 +79,34 @@ export function buildLaunchSpec(kind, mode, opts = {}) {
|
|
|
59
79
|
};
|
|
60
80
|
return { launch, lifecycle: p.lifecycle, skills: p.skills };
|
|
61
81
|
}
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Build the pi argv to launch / revive a node
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
/** Persist a node's (possibly large) system prompt to a file in its node dir and
|
|
86
|
+
* return the absolute path, so callers can pass a short path to pi instead of
|
|
87
|
+
* the inline text. Returns null if the write fails — the caller then falls back
|
|
88
|
+
* to passing the prompt inline. Rewritten every launch so a polymorph's updated
|
|
89
|
+
* prompt always lands. */
|
|
90
|
+
function writeSystemPromptFile(nodeId, prompt) {
|
|
91
|
+
try {
|
|
92
|
+
const dir = nodeDir(nodeId);
|
|
93
|
+
mkdirSync(dir, { recursive: true });
|
|
94
|
+
const p = join(dir, 'system-prompt.md');
|
|
95
|
+
writeFileSync(p, prompt, 'utf8');
|
|
96
|
+
return p;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
62
102
|
/** Construct the pi invocation for a node.
|
|
63
103
|
* - fresh start: pass `prompt` (the node's first user message), no resume.
|
|
64
|
-
* -
|
|
104
|
+
* - fork start: pass `forkFrom` (absolute .jsonl path or partial uuid) to `--fork`
|
|
105
|
+
* — pi COPIES that conversation into a NEW session for this node, then `prompt`
|
|
106
|
+
* is delivered as the next message. One-shot at birth: the node thereafter
|
|
107
|
+
* captures its OWN pi_session_file and revives by `--session` like any other.
|
|
108
|
+
* - revive idle/done: pass `resumeSessionPath` (absolute .jsonl path, preferred)
|
|
109
|
+
* or `resumeSessionId` (bare uuid fallback) to `--session` (keeps conversation).
|
|
65
110
|
* - refresh-yield: fresh again (no resume) — the node re-reads its roadmap. */
|
|
66
111
|
export function buildPiArgv(meta, opts = {}) {
|
|
67
112
|
const spec = meta.launch;
|
|
@@ -69,15 +114,42 @@ export function buildPiArgv(meta, opts = {}) {
|
|
|
69
114
|
for (const ext of spec?.extensions ?? CANVAS_EXTENSIONS) {
|
|
70
115
|
argv.push('-e', ext);
|
|
71
116
|
}
|
|
72
|
-
argv.push('-n', meta
|
|
73
|
-
|
|
74
|
-
|
|
117
|
+
argv.push('-n', editorLabel(meta));
|
|
118
|
+
// pi's `--resume` is a bare toggle that opens the interactive picker; the
|
|
119
|
+
// flag that resumes a *specific* session is `--session <path|id>`. Prefer the
|
|
120
|
+
// absolute FILE path when present: pi resolves a bare id cwd-relative first
|
|
121
|
+
// and shows a cross-project "Fork? [y/N]" prompt when the revive cwd differs
|
|
122
|
+
// from the session's creation cwd, whereas a path (contains `/` or ends
|
|
123
|
+
// `.jsonl`) is opened directly — immune to any cwd discrepancy. The bare uuid
|
|
124
|
+
// is the fallback for older nodes booted before pi_session_file was captured.
|
|
125
|
+
// `--fork <path|id>` is the spawn-time branch: pi copies the source session
|
|
126
|
+
// into a fresh one for this node (the source is untouched), then delivers the
|
|
127
|
+
// kickoff prompt as the next message. Mutually exclusive with `--session`
|
|
128
|
+
// (resume) — fork wins when both are somehow set, but in practice a spawn
|
|
129
|
+
// never resumes and a revive never forks.
|
|
130
|
+
if (opts.forkFrom !== undefined && opts.forkFrom !== '') {
|
|
131
|
+
argv.push('--fork', opts.forkFrom);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const resumeArg = opts.resumeSessionPath ?? opts.resumeSessionId;
|
|
135
|
+
if (resumeArg !== undefined)
|
|
136
|
+
argv.push('--session', resumeArg);
|
|
137
|
+
}
|
|
75
138
|
if (spec?.model !== undefined)
|
|
76
139
|
argv.push('--model', spec.model);
|
|
77
140
|
if (spec?.tools !== undefined && spec.tools.length > 0)
|
|
78
141
|
argv.push('--tools', spec.tools.join(','));
|
|
79
142
|
if (spec?.systemPrompt !== undefined && spec.systemPrompt !== '') {
|
|
80
|
-
|
|
143
|
+
// pi's --append-system-prompt reads a FILE when the arg is an existing path,
|
|
144
|
+
// else treats the arg as literal text. Pass the prompt as a file path, not
|
|
145
|
+
// inline: an orchestrator persona is ~17KB, and passed inline it inflates the
|
|
146
|
+
// `tmux new-window 'pi …'` command past tmux's command-length limit, so the
|
|
147
|
+
// spawn dies with "command too long" and the node is marked dead before pi
|
|
148
|
+
// ever starts (base workers fit, orchestrator children don't). Writing it to
|
|
149
|
+
// the node dir keeps the command tiny. Falls back to inline if the write
|
|
150
|
+
// fails (e.g. an ephemeral meta with no node dir).
|
|
151
|
+
const promptArg = writeSystemPromptFile(meta.node_id, spec.systemPrompt) ?? spec.systemPrompt;
|
|
152
|
+
argv.push('--append-system-prompt', promptArg);
|
|
81
153
|
}
|
|
82
154
|
if (opts.prompt !== undefined && opts.prompt !== '')
|
|
83
155
|
argv.push(opts.prompt);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NodeMeta } from '../canvas/types.js';
|
|
2
|
+
/** The lifecycle events — the only vocabulary for moving a node's status/intent.
|
|
3
|
+
* Each maps (in the table below) to a target status and/or intent plus the set
|
|
4
|
+
* of from-statuses it is legal from. */
|
|
5
|
+
export type LifecycleEvent = 'finalize' | 'reap' | 'cancel' | 'crash' | 'yield' | 'release' | 'revive' | 'boot';
|
|
6
|
+
/** Enact a lifecycle event on a node: validate the from-status against the
|
|
7
|
+
* table, then write status+intent in ONE atomic statement (so they can never
|
|
8
|
+
* disagree). Returns the hydrated node view after the write.
|
|
9
|
+
*
|
|
10
|
+
* Throws on an unknown node, or on an ILLEGAL move (e.g. `finalize` on a `dead`
|
|
11
|
+
* node) — illegal states are unrepresentable. The throw is a real signal:
|
|
12
|
+
* callers that previously swallowed db-mutation errors now surface them. */
|
|
13
|
+
export declare function transition(nodeId: string, event: LifecycleEvent): NodeMeta;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// lifecycle.ts — the node status×intent state machine.
|
|
2
|
+
//
|
|
3
|
+
// ONE place defines which (status, intent) moves are legal and enacts them.
|
|
4
|
+
// Before this, ~a dozen scattered setStatus()/setIntent() pairs across
|
|
5
|
+
// reset/close/revive/feed/daemon/stophook/queue/promote re-derived the lifecycle
|
|
6
|
+
// by hand, with no shared definition of "what move is legal." Here the legal
|
|
7
|
+
// transition TABLE is the definition, and `transition(id, event)` is the single
|
|
8
|
+
// writer of status+intent: it validates the from-status, then writes both fields
|
|
9
|
+
// in ONE atomic statement (built on Phase 2's WAL'd row setters) so the two can
|
|
10
|
+
// never disagree.
|
|
11
|
+
//
|
|
12
|
+
// This mirrors persona.ts: persona.ts is the single source of transition PROSE;
|
|
13
|
+
// lifecycle.ts is the single source of which status/intent move is LEGAL. Two
|
|
14
|
+
// parallel, legible state machines instead of scattered enactment.
|
|
15
|
+
//
|
|
16
|
+
// Crash-safety invariant (was a comment repeated in reset/close/reapDescendants):
|
|
17
|
+
// "flip status to a non-supervised value + clear intent BEFORE killing the
|
|
18
|
+
// window" — the daemon only ever revives active|idle nodes, so a teardown must
|
|
19
|
+
// leave the node done/canceled first to close the revive race. That invariant is
|
|
20
|
+
// now the DEFINITION of the `reap`/`cancel` events: callers flip via transition()
|
|
21
|
+
// and only THEN kill the window.
|
|
22
|
+
//
|
|
23
|
+
// Layering note: lifecycle.ts is runtime, but it is the canvas write surface's
|
|
24
|
+
// `transition` verb (the only writer of status+intent), so it owns its atomic
|
|
25
|
+
// row UPDATE directly via openDb — the one sanctioned exception to "only
|
|
26
|
+
// canvas.ts touches the db" (see canvas/CLAUDE.md), exactly as db.ts's backfill
|
|
27
|
+
// is the sanctioned exception for a data migration.
|
|
28
|
+
import { openDb, getNode } from '../canvas/index.js';
|
|
29
|
+
const ANY = '*';
|
|
30
|
+
/** The supervised statuses — a live node the daemon watches. */
|
|
31
|
+
const LIVE = ['active', 'idle'];
|
|
32
|
+
/** The legal transition table — derived directly from the (status, intent) pairs
|
|
33
|
+
* the runtime actually wrote at its audited call sites, so behavior is preserved
|
|
34
|
+
* by construction. Each entry's comment names its writer(s). */
|
|
35
|
+
const TRANSITIONS = {
|
|
36
|
+
// feed.push(final) · queue.cancelJob · markCleanExitDone (clean quit).
|
|
37
|
+
finalize: { status: 'done', intent: 'done', from: LIVE },
|
|
38
|
+
// reapDescendants · relaunchRoot park-old. Forced teardown → done, intent cleared.
|
|
39
|
+
reap: { status: 'done', intent: null, from: ANY },
|
|
40
|
+
// closeNode cascade. Forced teardown → canceled, intent cleared.
|
|
41
|
+
cancel: { status: 'canceled', intent: null, from: ANY },
|
|
42
|
+
// daemon superviseTick: window gone with no yield/release intent. Intent KEPT
|
|
43
|
+
// (the dead log line still reports it).
|
|
44
|
+
crash: { status: 'dead', from: LIVE },
|
|
45
|
+
// requestYield · relaunchRoot new-node safety net. Status KEPT (already active).
|
|
46
|
+
yield: { intent: 'refresh', from: LIVE },
|
|
47
|
+
// stophook idle-release: free the window, stay woken by the inbox.
|
|
48
|
+
release: { status: 'idle', intent: 'idle-release', from: LIVE },
|
|
49
|
+
// reviveNode · resetRoot · stophook boot-confirm (clear a pending refresh net).
|
|
50
|
+
revive: { status: 'active', intent: null, from: ANY },
|
|
51
|
+
// reviveInPlace: re-exec a fresh pi in the SAME pane. Status (re)affirmed
|
|
52
|
+
// active; intent KEPT so a pending refresh survives as proof-of-boot until the
|
|
53
|
+
// fresh pi's session_start clears it (a premature clear is how a failed
|
|
54
|
+
// respawn became a silent death — see revive.ts).
|
|
55
|
+
boot: { status: 'active', from: LIVE },
|
|
56
|
+
};
|
|
57
|
+
/** Enact a lifecycle event on a node: validate the from-status against the
|
|
58
|
+
* table, then write status+intent in ONE atomic statement (so they can never
|
|
59
|
+
* disagree). Returns the hydrated node view after the write.
|
|
60
|
+
*
|
|
61
|
+
* Throws on an unknown node, or on an ILLEGAL move (e.g. `finalize` on a `dead`
|
|
62
|
+
* node) — illegal states are unrepresentable. The throw is a real signal:
|
|
63
|
+
* callers that previously swallowed db-mutation errors now surface them. */
|
|
64
|
+
export function transition(nodeId, event) {
|
|
65
|
+
const spec = TRANSITIONS[event];
|
|
66
|
+
const cur = getNode(nodeId);
|
|
67
|
+
if (cur === null)
|
|
68
|
+
throw new Error(`transition: unknown node ${nodeId}`);
|
|
69
|
+
if (spec.from !== ANY && !spec.from.includes(cur.status)) {
|
|
70
|
+
throw new Error(`illegal lifecycle transition: '${event}' from status='${cur.status}' (node ${nodeId})`);
|
|
71
|
+
}
|
|
72
|
+
const writeStatus = Object.prototype.hasOwnProperty.call(spec, 'status');
|
|
73
|
+
const writeIntent = Object.prototype.hasOwnProperty.call(spec, 'intent');
|
|
74
|
+
const db = openDb();
|
|
75
|
+
if (writeStatus && writeIntent) {
|
|
76
|
+
db.prepare('UPDATE nodes SET status = ?, intent = ? WHERE node_id = ?')
|
|
77
|
+
.run(spec.status, spec.intent ?? null, nodeId);
|
|
78
|
+
}
|
|
79
|
+
else if (writeStatus) {
|
|
80
|
+
db.prepare('UPDATE nodes SET status = ? WHERE node_id = ?').run(spec.status, nodeId);
|
|
81
|
+
}
|
|
82
|
+
else if (writeIntent) {
|
|
83
|
+
db.prepare('UPDATE nodes SET intent = ? WHERE node_id = ?').run(spec.intent ?? null, nodeId);
|
|
84
|
+
}
|
|
85
|
+
return getNode(nodeId);
|
|
86
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** The node-local index template. Named export kept for callers/tests that
|
|
2
|
+
* assert the seeded node store verbatim. */
|
|
3
|
+
export declare const MEMORY_TEMPLATE: string;
|
|
4
|
+
/** The user-global index template — framed around the human, not a goal. */
|
|
5
|
+
export declare const USER_MEMORY_TEMPLATE: string;
|
|
6
|
+
/** The project index template — framed around the repo. */
|
|
7
|
+
export declare const PROJECT_MEMORY_TEMPLATE: string;
|
|
8
|
+
/** The node-local memory directory in a node's context dir — holds MEMORY.md
|
|
9
|
+
* (the index) and the one-fact detail files it points at. */
|
|
10
|
+
export declare function memoryDir(nodeId: string): string;
|
|
11
|
+
/** The node-local MEMORY.md index path (inside the memory dir). */
|
|
12
|
+
export declare function memoryPath(nodeId: string): string;
|
|
13
|
+
/** Whether the node has a node-local memory store. This is ALSO the
|
|
14
|
+
* orchestrator gate: only orchestrators are ever seeded one, so a node with no
|
|
15
|
+
* node-local store is a terminal worker (no memory framing at all). */
|
|
16
|
+
export declare function hasMemory(nodeId: string): boolean;
|
|
17
|
+
/** Read the node-local MEMORY.md index, or null when it doesn't exist. */
|
|
18
|
+
export declare function readMemory(nodeId: string): string | null;
|
|
19
|
+
/** Seed the node-local memory dir + index IF the node has none yet. */
|
|
20
|
+
export declare function seedMemory(nodeId: string): boolean;
|
|
21
|
+
/** The user-global memory directory — one per machine, key-less, loaded into
|
|
22
|
+
* every orchestrator everywhere. */
|
|
23
|
+
export declare function userMemoryDir(): string;
|
|
24
|
+
/** The user-global MEMORY.md index path. */
|
|
25
|
+
export declare function userMemoryPath(): string;
|
|
26
|
+
export declare function hasUserMemory(): boolean;
|
|
27
|
+
/** Read the user-global MEMORY.md index, or null when it doesn't exist. */
|
|
28
|
+
export declare function readUserMemory(): string | null;
|
|
29
|
+
/** Seed the user-global memory dir + index IF absent. */
|
|
30
|
+
export declare function seedUserMemory(): boolean;
|
|
31
|
+
/** The project key for `cwd`: its git-repo-root when inside a repo, else the
|
|
32
|
+
* cwd itself, mangled into a flat directory name (reuses artifact mangleCwd).
|
|
33
|
+
* This keys the per-project memory store under <crtrHome>/projects/. */
|
|
34
|
+
export declare function projectKey(cwd: string): string;
|
|
35
|
+
/** The project memory directory for `cwd`. */
|
|
36
|
+
export declare function projectMemoryDir(cwd: string): string;
|
|
37
|
+
/** The project MEMORY.md index path for `cwd`. */
|
|
38
|
+
export declare function projectMemoryPath(cwd: string): string;
|
|
39
|
+
export declare function hasProjectMemory(cwd: string): boolean;
|
|
40
|
+
/** Read the project MEMORY.md index for `cwd`, or null when it doesn't exist. */
|
|
41
|
+
export declare function readProjectMemory(cwd: string): string | null;
|
|
42
|
+
/** Seed the project memory dir + index for `cwd` IF absent. */
|
|
43
|
+
export declare function seedProjectMemory(cwd: string): boolean;
|