@crouton-kit/crouter 0.3.14 → 0.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-root.d.ts +8 -0
- package/dist/build-root.js +30 -0
- package/dist/builtin-personas/design/base.md +3 -7
- package/dist/builtin-personas/design/orchestrator.md +4 -3
- package/dist/builtin-personas/developer/base.md +3 -7
- package/dist/builtin-personas/developer/orchestrator.md +5 -4
- package/dist/builtin-personas/explore/base.md +3 -7
- package/dist/builtin-personas/explore/orchestrator.md +1 -5
- package/dist/builtin-personas/general/base.md +2 -4
- package/dist/builtin-personas/general/orchestrator.md +2 -4
- package/dist/builtin-personas/lifecycle/resident.md +2 -0
- package/dist/builtin-personas/lifecycle/terminal.md +6 -0
- package/dist/builtin-personas/orchestration-kernel.md +42 -3
- package/dist/builtin-personas/plan/base.md +3 -5
- package/dist/builtin-personas/plan/orchestrator.md +5 -4
- package/dist/builtin-personas/plan/reviewers/architecture-fit/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/code-smells/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/security/base.md +9 -0
- package/dist/builtin-personas/review/base.md +3 -5
- package/dist/builtin-personas/review/orchestrator.md +2 -6
- package/dist/builtin-personas/runtime-base.md +3 -19
- package/dist/builtin-personas/spec/base.md +3 -5
- package/dist/builtin-personas/spec/orchestrator.md +4 -3
- package/dist/builtin-personas/spine/has-manager.md +10 -0
- package/dist/builtin-personas/spine/no-manager.md +2 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +96 -0
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +1 -1
- package/dist/builtin-skills/skills/spec/SKILL.md +2 -2
- package/dist/cli.js +6 -29
- package/dist/commands/attention.js +76 -7
- package/dist/commands/canvas-prune.d.ts +2 -0
- package/dist/commands/canvas-prune.js +66 -0
- package/dist/commands/canvas.js +5 -8
- package/dist/commands/chord.d.ts +2 -0
- package/dist/commands/chord.js +143 -0
- package/dist/commands/daemon.js +8 -5
- package/dist/commands/dashboard.js +2 -0
- package/dist/commands/human/prompts.js +28 -27
- package/dist/commands/human/queue.js +30 -14
- package/dist/commands/human/shared.d.ts +26 -21
- package/dist/commands/human/shared.js +45 -67
- package/dist/commands/human.js +4 -14
- package/dist/commands/node.d.ts +11 -0
- package/dist/commands/node.js +221 -99
- package/dist/commands/pkg/market-inspect.js +6 -4
- package/dist/commands/pkg/market-manage.js +10 -6
- package/dist/commands/pkg/market.js +2 -4
- package/dist/commands/pkg/plugin-inspect.js +6 -4
- package/dist/commands/pkg/plugin-manage.js +12 -7
- package/dist/commands/pkg/plugin.js +2 -4
- package/dist/commands/pkg.js +0 -4
- package/dist/commands/push.js +178 -15
- package/dist/commands/revive.js +5 -3
- package/dist/commands/skill/author.js +6 -4
- package/dist/commands/skill/find.js +8 -5
- package/dist/commands/skill/read.js +2 -0
- package/dist/commands/skill/state.js +6 -4
- package/dist/commands/skill.js +0 -6
- package/dist/commands/sys/config.js +21 -7
- package/dist/commands/sys/doctor.js +2 -0
- package/dist/commands/sys/update.js +4 -0
- package/dist/commands/sys.js +0 -6
- package/dist/commands/tmux-spread.d.ts +2 -0
- package/dist/commands/tmux-spread.js +129 -0
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
- package/dist/core/__tests__/child-followup.test.d.ts +1 -0
- package/dist/core/__tests__/child-followup.test.js +83 -0
- package/dist/core/__tests__/close.test.d.ts +1 -0
- package/dist/core/__tests__/close.test.js +148 -0
- package/dist/core/__tests__/context-intro.test.d.ts +1 -0
- package/dist/core/__tests__/context-intro.test.js +196 -0
- package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-boot.test.js +93 -0
- package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-liveness.test.js +223 -0
- package/dist/core/__tests__/focuses.test.d.ts +1 -0
- package/dist/core/__tests__/focuses.test.js +196 -0
- package/dist/core/__tests__/fork.test.d.ts +1 -0
- package/dist/core/__tests__/fork.test.js +91 -0
- package/dist/core/__tests__/home-session.test.d.ts +1 -0
- package/dist/core/__tests__/home-session.test.js +153 -0
- package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
- package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
- package/dist/core/__tests__/keystone.test.d.ts +1 -0
- package/dist/core/__tests__/keystone.test.js +185 -0
- package/dist/core/__tests__/kickoff.test.d.ts +1 -0
- package/dist/core/__tests__/kickoff.test.js +89 -0
- package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/lifecycle.test.js +178 -0
- package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
- package/dist/core/__tests__/listing-completeness.test.js +31 -0
- package/dist/core/__tests__/memory.test.d.ts +1 -0
- package/dist/core/__tests__/memory.test.js +152 -0
- package/dist/core/__tests__/migration.test.d.ts +1 -0
- package/dist/core/__tests__/migration.test.js +238 -0
- package/dist/core/__tests__/pane-column.test.d.ts +1 -0
- package/dist/core/__tests__/pane-column.test.js +153 -0
- package/dist/core/__tests__/passive-subscription.test.js +24 -1
- package/dist/core/__tests__/persona-compose.test.d.ts +1 -0
- package/dist/core/__tests__/persona-compose.test.js +53 -0
- package/dist/core/__tests__/persona-subkind.test.d.ts +1 -0
- package/dist/core/__tests__/persona-subkind.test.js +62 -0
- package/dist/core/__tests__/persona.test.d.ts +1 -0
- package/dist/core/__tests__/persona.test.js +107 -0
- package/dist/core/__tests__/placement-focus.test.d.ts +1 -0
- package/dist/core/__tests__/placement-focus.test.js +266 -0
- package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
- package/dist/core/__tests__/placement-reconcile.test.js +212 -0
- package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
- package/dist/core/__tests__/placement-revive.test.js +238 -0
- package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/placement-teardown.test.js +178 -0
- package/dist/core/__tests__/prune.test.d.ts +1 -0
- package/dist/core/__tests__/prune.test.js +116 -0
- package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
- package/dist/core/__tests__/push-final-guard.test.js +71 -0
- package/dist/core/__tests__/relaunch.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch.test.js +334 -0
- package/dist/core/__tests__/reset.test.js +26 -7
- package/dist/core/__tests__/revive.test.d.ts +1 -0
- package/dist/core/__tests__/revive.test.js +217 -0
- package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
- package/dist/core/__tests__/spawn-root.test.js +73 -0
- package/dist/core/__tests__/steer-note.test.d.ts +1 -0
- package/dist/core/__tests__/steer-note.test.js +39 -0
- package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
- package/dist/core/__tests__/stop-guard.test.js +82 -0
- package/dist/core/__tests__/subcommand-tier.test.js +35 -33
- package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
- package/dist/core/__tests__/tmux-surface.test.js +105 -0
- package/dist/core/__tests__/unknown-path.test.js +8 -2
- package/dist/core/canvas/attention.d.ts +10 -0
- package/dist/core/canvas/attention.js +40 -0
- package/dist/core/canvas/canvas.d.ts +66 -7
- package/dist/core/canvas/canvas.js +209 -21
- package/dist/core/canvas/db.d.ts +8 -0
- package/dist/core/canvas/db.js +205 -4
- package/dist/core/canvas/focuses.d.ts +22 -0
- package/dist/core/canvas/focuses.js +81 -0
- package/dist/core/canvas/index.d.ts +3 -0
- package/dist/core/canvas/index.js +3 -0
- package/dist/core/canvas/labels.d.ts +27 -0
- package/dist/core/canvas/labels.js +36 -0
- package/dist/core/canvas/render.js +25 -10
- package/dist/core/canvas/telemetry.d.ts +14 -0
- package/dist/core/canvas/telemetry.js +35 -0
- package/dist/core/canvas/types.d.ts +115 -12
- package/dist/core/command.d.ts +25 -1
- package/dist/core/command.js +23 -15
- package/dist/core/config.js +36 -2
- package/dist/core/feed/feed.js +3 -3
- package/dist/core/feed/inbox.d.ts +3 -1
- package/dist/core/feed/inbox.js +45 -5
- package/dist/core/feed/passive.js +24 -11
- package/dist/core/help.d.ts +26 -13
- package/dist/core/help.js +44 -37
- package/dist/core/personas/index.d.ts +1 -1
- package/dist/core/personas/index.js +1 -1
- package/dist/core/personas/loader.d.ts +40 -1
- package/dist/core/personas/loader.js +63 -1
- package/dist/core/personas/resolve.d.ts +13 -6
- package/dist/core/personas/resolve.js +46 -34
- package/dist/core/runtime/bearings.d.ts +20 -0
- package/dist/core/runtime/bearings.js +92 -0
- package/dist/core/runtime/close.d.ts +14 -0
- package/dist/core/runtime/close.js +151 -0
- package/dist/core/runtime/demote.js +24 -12
- package/dist/core/runtime/front-door.js +1 -1
- package/dist/core/runtime/kickoff.d.ts +23 -6
- package/dist/core/runtime/kickoff.js +92 -36
- package/dist/core/runtime/launch.d.ts +26 -12
- package/dist/core/runtime/launch.js +78 -19
- package/dist/core/runtime/lifecycle.d.ts +13 -0
- package/dist/core/runtime/lifecycle.js +86 -0
- package/dist/core/runtime/memory.d.ts +43 -0
- package/dist/core/runtime/memory.js +165 -0
- package/dist/core/runtime/naming.d.ts +22 -0
- package/dist/core/runtime/naming.js +166 -0
- package/dist/core/runtime/nodes.d.ts +39 -1
- package/dist/core/runtime/nodes.js +69 -10
- package/dist/core/runtime/persona.d.ts +25 -0
- package/dist/core/runtime/persona.js +139 -0
- package/dist/core/runtime/placement.d.ts +299 -0
- package/dist/core/runtime/placement.js +688 -0
- package/dist/core/runtime/promote.d.ts +14 -7
- package/dist/core/runtime/promote.js +57 -67
- package/dist/core/runtime/reset.d.ts +47 -4
- package/dist/core/runtime/reset.js +223 -52
- package/dist/core/runtime/revive.d.ts +26 -2
- package/dist/core/runtime/revive.js +166 -39
- package/dist/core/runtime/spawn.d.ts +20 -5
- package/dist/core/runtime/spawn.js +163 -43
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +18 -8
- package/dist/core/runtime/tmux-chrome.d.ts +1 -0
- package/dist/core/runtime/tmux-chrome.js +4 -0
- package/dist/core/runtime/tmux.d.ts +113 -20
- package/dist/core/runtime/tmux.js +221 -39
- package/dist/core/spawn.js +15 -0
- package/dist/daemon/crtrd.d.ts +12 -1
- package/dist/daemon/crtrd.js +152 -34
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +266 -0
- package/dist/pi-extensions/canvas-commands.js +16 -13
- package/dist/pi-extensions/canvas-context-intro.d.ts +70 -0
- package/dist/pi-extensions/canvas-context-intro.js +164 -0
- package/dist/pi-extensions/canvas-goal-capture.d.ts +3 -0
- package/dist/pi-extensions/canvas-goal-capture.js +15 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +11 -0
- package/dist/pi-extensions/canvas-nav.d.ts +12 -4
- package/dist/pi-extensions/canvas-nav.js +594 -262
- package/dist/pi-extensions/canvas-resume.d.ts +22 -0
- package/dist/pi-extensions/canvas-resume.js +173 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
- package/dist/pi-extensions/canvas-stophook.js +340 -228
- package/dist/types.d.ts +28 -0
- package/dist/types.js +16 -0
- package/package.json +2 -2
- package/dist/core/runtime/presence.d.ts +0 -38
- package/dist/core/runtime/presence.js +0 -154
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Session naming — turn a node's first prompt into a short, human-readable
|
|
2
|
+
// handle for the editor label.
|
|
3
|
+
//
|
|
4
|
+
// A node's editor label is `<kind> (<mode>) <name> <cycle>` (see editorLabel in
|
|
5
|
+
// launch.ts). The `<name>` is a 3-5 word kebab-case "description" derived from
|
|
6
|
+
// the first prompt by asking pi headlessly (`pi -p`), persisted on the node's
|
|
7
|
+
// meta so it survives revives and shows in every cycle.
|
|
8
|
+
//
|
|
9
|
+
// Two entry points:
|
|
10
|
+
// • generateSessionName — synchronous (spawnSync). For the CLI spawn paths
|
|
11
|
+
// (spawnChild / bootRoot) that run outside any pi event loop, where a brief
|
|
12
|
+
// block before launching the worker is fine and lets the FIRST pi session
|
|
13
|
+
// already carry the name.
|
|
14
|
+
// • generateAndPersistName — async (execFile, non-blocking). For the bare-root
|
|
15
|
+
// case where the prompt only arrives as the first interactive message inside
|
|
16
|
+
// a live pi process; it must never block the event loop. Persists the name
|
|
17
|
+
// to meta so the label picks it up on the next cycle.
|
|
18
|
+
//
|
|
19
|
+
// Both are best-effort: a failed/slow/garbled pi call falls back to a local slug
|
|
20
|
+
// of the prompt, so a node always gets a sane name.
|
|
21
|
+
import { spawnSync, execFile } from 'node:child_process';
|
|
22
|
+
import { getNode, updateNode } from '../canvas/index.js';
|
|
23
|
+
/** Cap on prompt text fed to the namer — a name needs only the gist. */
|
|
24
|
+
const PROMPT_CAP = 2000;
|
|
25
|
+
/** Wall-clock budget for the headless pi call before we fall back to a slug. */
|
|
26
|
+
const NAME_TIMEOUT_MS = 20_000;
|
|
27
|
+
const NAME_SYSTEM_PROMPT = 'You name coding-agent work sessions. This name is a label used to identify the ' +
|
|
28
|
+
'session at a glance among many other concurrent programming sessions, so it must ' +
|
|
29
|
+
'describe what the task is about. Reply with ONLY a concise 3-5 word name in ' +
|
|
30
|
+
'kebab-case: lowercase words joined by single hyphens (e.g. `refactor-auth-token-flow`, ' +
|
|
31
|
+
'`add-csv-export-endpoint`). No punctuation, quotes, prose, or trailing text. ' +
|
|
32
|
+
'Output JUST the name, nothing else.';
|
|
33
|
+
/** Put the raw task text FIRST in a delimited block, then the instruction, so the
|
|
34
|
+
* model reads the content before being told what to do and never mistakes the
|
|
35
|
+
* prompt's own text for the instruction. The prompt is capped first, so the
|
|
36
|
+
* closing tag is always present. */
|
|
37
|
+
function nameUserPrompt(prompt) {
|
|
38
|
+
return `<prompt>\n${prompt.slice(0, PROMPT_CAP)}\n</prompt>\n\nName this session based on the task above. The name should describe what the task is about, so it can be identified among many other programming sessions. Output JUST the name, nothing else.`;
|
|
39
|
+
}
|
|
40
|
+
/** A short stop-word set so the local-slug fallback skips filler words. */
|
|
41
|
+
const STOPWORDS = new Set([
|
|
42
|
+
'the', 'a', 'an', 'and', 'or', 'but', 'to', 'of', 'in', 'on', 'for', 'with',
|
|
43
|
+
'is', 'are', 'be', 'this', 'that', 'it', 'as', 'at', 'by', 'from', 'into',
|
|
44
|
+
'please', 'can', 'you', 'i', 'we', 'my', 'our', 'me', 'so', 'then',
|
|
45
|
+
]);
|
|
46
|
+
/** Coerce arbitrary text into a 3-5 word kebab-case name, or '' if nothing
|
|
47
|
+
* usable survives. Lowercases, keeps [a-z0-9], collapses everything else to a
|
|
48
|
+
* single hyphen, and clamps to the first 5 words. */
|
|
49
|
+
export function sanitizeSessionName(raw) {
|
|
50
|
+
const firstLine = (raw ?? '').split('\n').map((l) => l.trim()).find((l) => l !== '') ?? '';
|
|
51
|
+
const words = firstLine
|
|
52
|
+
.toLowerCase()
|
|
53
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
54
|
+
.split('-')
|
|
55
|
+
.filter((w) => w !== '');
|
|
56
|
+
return words.slice(0, 5).join('-');
|
|
57
|
+
}
|
|
58
|
+
/** Local fallback: derive a name straight from the prompt (no pi call). Drops
|
|
59
|
+
* stop-words, takes the first few content words. */
|
|
60
|
+
export function slugFromPrompt(prompt) {
|
|
61
|
+
const words = (prompt ?? '')
|
|
62
|
+
.toLowerCase()
|
|
63
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
64
|
+
.split(' ')
|
|
65
|
+
.filter((w) => w !== '' && !STOPWORDS.has(w));
|
|
66
|
+
const picked = (words.length > 0 ? words : (prompt ?? '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').split(' ').filter(Boolean))
|
|
67
|
+
.slice(0, 3);
|
|
68
|
+
return sanitizeSessionName(picked.join('-')) || 'session';
|
|
69
|
+
}
|
|
70
|
+
/** Default namer model — Anthropic's small/fast model. Naming is a one-line
|
|
71
|
+
* classification, so we pin Haiku (cheap, quick) instead of inheriting the
|
|
72
|
+
* node's heavyweight default. Override with CRTR_NAME_MODEL. */
|
|
73
|
+
const DEFAULT_NAME_MODEL = 'anthropic/claude-haiku-4-5';
|
|
74
|
+
/** The pi argv for a headless name request. Stripped down (no tools, session,
|
|
75
|
+
* context files, extensions, skills, templates, themes) so it's fast and
|
|
76
|
+
* side-effect free. Pinned to Haiku with thinking off — naming is a trivial
|
|
77
|
+
* classification that never needs a reasoning budget. Override the model with
|
|
78
|
+
* CRTR_NAME_MODEL. */
|
|
79
|
+
function nameArgs(prompt) {
|
|
80
|
+
const override = process.env['CRTR_NAME_MODEL'];
|
|
81
|
+
const model = override !== undefined && override.trim() !== '' ? override.trim() : DEFAULT_NAME_MODEL;
|
|
82
|
+
const argv = [
|
|
83
|
+
'-p',
|
|
84
|
+
'--no-session',
|
|
85
|
+
'--no-context-files',
|
|
86
|
+
'--no-extensions',
|
|
87
|
+
'--no-skills',
|
|
88
|
+
'--no-prompt-templates',
|
|
89
|
+
'--no-themes',
|
|
90
|
+
'--no-tools',
|
|
91
|
+
'--mode', 'text',
|
|
92
|
+
// Naming is a trivial one-line classification — no thinking budget, ever.
|
|
93
|
+
'--thinking', 'off',
|
|
94
|
+
'--model', model,
|
|
95
|
+
];
|
|
96
|
+
argv.push('--system-prompt', NAME_SYSTEM_PROMPT);
|
|
97
|
+
argv.push(nameUserPrompt(prompt));
|
|
98
|
+
return argv;
|
|
99
|
+
}
|
|
100
|
+
/** Synchronously ask pi for a 2-4 word kebab name for `prompt`. Blocks up to
|
|
101
|
+
* NAME_TIMEOUT_MS; on any failure (non-zero exit, timeout, empty/garbled
|
|
102
|
+
* output) falls back to a local slug. Returns '' only for an empty prompt. */
|
|
103
|
+
export function generateSessionName(prompt) {
|
|
104
|
+
const body = (prompt ?? '').trim();
|
|
105
|
+
if (body === '')
|
|
106
|
+
return '';
|
|
107
|
+
try {
|
|
108
|
+
const r = spawnSync('pi', nameArgs(body), {
|
|
109
|
+
encoding: 'utf8',
|
|
110
|
+
timeout: NAME_TIMEOUT_MS,
|
|
111
|
+
// Don't inherit a TUI; capture stdout only.
|
|
112
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
113
|
+
});
|
|
114
|
+
if (r.status === 0 && typeof r.stdout === 'string') {
|
|
115
|
+
const name = sanitizeSessionName(r.stdout);
|
|
116
|
+
if (name !== '')
|
|
117
|
+
return name;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// fall through to slug
|
|
122
|
+
}
|
|
123
|
+
return slugFromPrompt(body);
|
|
124
|
+
}
|
|
125
|
+
/** Asynchronously generate a name for `prompt` and persist it to the node's
|
|
126
|
+
* meta as `description` — only if the node has none yet (so a later message
|
|
127
|
+
* never clobbers it). Non-blocking: safe to call from inside a live pi event
|
|
128
|
+
* loop. Best-effort; swallows all errors.
|
|
129
|
+
*
|
|
130
|
+
* `onNamed` (optional) fires with the freshly-persisted meta the moment the
|
|
131
|
+
* name lands — the bare-root path passes a callback that calls
|
|
132
|
+
* pi.setSessionName(editorLabel(meta)) so the LIVE editor label updates in the
|
|
133
|
+
* same session, instead of waiting for the next revive/cycle. */
|
|
134
|
+
export function generateAndPersistName(nodeId, prompt, onNamed) {
|
|
135
|
+
const body = (prompt ?? '').trim();
|
|
136
|
+
if (body === '')
|
|
137
|
+
return;
|
|
138
|
+
const persist = (name) => {
|
|
139
|
+
try {
|
|
140
|
+
const meta = getNode(nodeId);
|
|
141
|
+
if (meta === null)
|
|
142
|
+
return;
|
|
143
|
+
if ((meta.description ?? '').trim() !== '')
|
|
144
|
+
return; // already named
|
|
145
|
+
const clean = sanitizeSessionName(name);
|
|
146
|
+
const updated = updateNode(nodeId, { description: clean !== '' ? clean : slugFromPrompt(body) });
|
|
147
|
+
onNamed?.(updated);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// best-effort
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
execFile('pi', nameArgs(body), { encoding: 'utf8', timeout: NAME_TIMEOUT_MS }, (err, stdout) => {
|
|
155
|
+
if (err || typeof stdout !== 'string') {
|
|
156
|
+
persist(slugFromPrompt(body));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const name = sanitizeSessionName(stdout);
|
|
160
|
+
persist(name !== '' ? name : slugFromPrompt(body));
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
persist(slugFromPrompt(body));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -1,9 +1,41 @@
|
|
|
1
1
|
import { type NodeMeta, type Mode, type Lifecycle, type LaunchSpec } from '../canvas/index.js';
|
|
2
2
|
/** Generate a node id in the same shape as job ids (time-sortable + random). */
|
|
3
3
|
export declare function newNodeId(): string;
|
|
4
|
+
/** The single, shared tmux session that ALL canvas node windows live in.
|
|
5
|
+
* Overridable with CRTR_NODE_SESSION (default `crtr`). Every root and every
|
|
6
|
+
* child opens a window here rather than cluttering the user's own working
|
|
7
|
+
* session — switch to it to browse the whole live graph, ignore it otherwise.
|
|
8
|
+
* Pure policy (env only, no tmux call), so it lives in the node layer, not the
|
|
9
|
+
* driver; the tmux driver imports it from here for installMenuBinding's use. */
|
|
10
|
+
export declare function nodeSession(): string;
|
|
11
|
+
/** Resolve the tmux session a freshly-born node's window/pane opens into — and
|
|
12
|
+
* thus its durable REVIVE-HOME (`home_session`). Pure so the birth decision is
|
|
13
|
+
* unit-testable without a live tmux:
|
|
14
|
+
* - managed background child (`adoptCaller=false`) → the shared backstage:
|
|
15
|
+
* the inherited `CRTR_ROOT_SESSION`, else `nodeSession()` (`crtr`).
|
|
16
|
+
* - independent `--root` / inline front door (`adoptCaller=true`) → the
|
|
17
|
+
* caller's CURRENT session when inside tmux (`here`), else the backstage.
|
|
18
|
+
* This is exactly the session each birth site already places the node into;
|
|
19
|
+
* centralizing it keeps `home_session` and the actual placement in lockstep. */
|
|
20
|
+
export declare function resolveBirthSession(opts: {
|
|
21
|
+
/** True for an independent root or the inline front door (both adopt the
|
|
22
|
+
* caller's session); false for a managed background child. */
|
|
23
|
+
adoptCaller: boolean;
|
|
24
|
+
/** The caller's current tmux location, or null when not inside tmux. */
|
|
25
|
+
here: {
|
|
26
|
+
session: string;
|
|
27
|
+
} | null;
|
|
28
|
+
/** The inherited CRTR_ROOT_SESSION (the backstage the subtree flows into). */
|
|
29
|
+
rootSession?: string | null;
|
|
30
|
+
}): string;
|
|
31
|
+
/** A node's durable REVIVE-HOME, with the legacy back-compat default. Nodes born
|
|
32
|
+
* before `home_session` existed have no such field in meta — they fall back to
|
|
33
|
+
* their last live LOCATION (`tmux_session`), then to the shared backstage
|
|
34
|
+
* (`nodeSession()`). The defaulted read for the placement layer; a present
|
|
35
|
+
* `home_session` is always returned verbatim. */
|
|
36
|
+
export declare function homeSessionOf(nodeId: string): string;
|
|
4
37
|
export interface NodeContext {
|
|
5
38
|
nodeId: string | null;
|
|
6
|
-
parentNodeId: string | null;
|
|
7
39
|
kind: string | null;
|
|
8
40
|
mode: Mode | null;
|
|
9
41
|
}
|
|
@@ -21,8 +53,14 @@ export interface SpawnNodeOpts {
|
|
|
21
53
|
lifecycle?: Lifecycle;
|
|
22
54
|
cwd: string;
|
|
23
55
|
name?: string;
|
|
56
|
+
/** Editor-label handle (2-4 word kebab-case) for the node's first prompt. */
|
|
57
|
+
description?: string;
|
|
24
58
|
/** Parent node id. Omit for a user-opened root. */
|
|
25
59
|
parent?: string | null;
|
|
60
|
+
/** Who spawned me (the `spawned_by` provenance edge), when it differs from
|
|
61
|
+
* `parent` — e.g. an independent root (parent=null) still records its
|
|
62
|
+
* spawner. Defaults to `parent`. */
|
|
63
|
+
spawnedBy?: string | null;
|
|
26
64
|
/** New subscriptions this node opens default to passive when true. */
|
|
27
65
|
passiveDefault?: boolean;
|
|
28
66
|
/** Resolved pi launch recipe (from resolve(kind,mode)). */
|
|
@@ -18,6 +18,47 @@ import { createNode, getNode, subscribe, recordSpawn, } from '../canvas/index.js
|
|
|
18
18
|
export function newNodeId() {
|
|
19
19
|
return `${Date.now().toString(36)}-${randomBytes(4).toString('hex')}`;
|
|
20
20
|
}
|
|
21
|
+
/** The single, shared tmux session that ALL canvas node windows live in.
|
|
22
|
+
* Overridable with CRTR_NODE_SESSION (default `crtr`). Every root and every
|
|
23
|
+
* child opens a window here rather than cluttering the user's own working
|
|
24
|
+
* session — switch to it to browse the whole live graph, ignore it otherwise.
|
|
25
|
+
* Pure policy (env only, no tmux call), so it lives in the node layer, not the
|
|
26
|
+
* driver; the tmux driver imports it from here for installMenuBinding's use. */
|
|
27
|
+
export function nodeSession() {
|
|
28
|
+
const v = process.env['CRTR_NODE_SESSION'];
|
|
29
|
+
return v !== undefined && v !== '' ? v : 'crtr';
|
|
30
|
+
}
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// REVIVE-HOME (home_session) — the durable session a node is (re)opened into
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/** Resolve the tmux session a freshly-born node's window/pane opens into — and
|
|
35
|
+
* thus its durable REVIVE-HOME (`home_session`). Pure so the birth decision is
|
|
36
|
+
* unit-testable without a live tmux:
|
|
37
|
+
* - managed background child (`adoptCaller=false`) → the shared backstage:
|
|
38
|
+
* the inherited `CRTR_ROOT_SESSION`, else `nodeSession()` (`crtr`).
|
|
39
|
+
* - independent `--root` / inline front door (`adoptCaller=true`) → the
|
|
40
|
+
* caller's CURRENT session when inside tmux (`here`), else the backstage.
|
|
41
|
+
* This is exactly the session each birth site already places the node into;
|
|
42
|
+
* centralizing it keeps `home_session` and the actual placement in lockstep. */
|
|
43
|
+
export function resolveBirthSession(opts) {
|
|
44
|
+
const backstage = opts.rootSession !== undefined && opts.rootSession !== null && opts.rootSession !== ''
|
|
45
|
+
? opts.rootSession
|
|
46
|
+
: nodeSession();
|
|
47
|
+
if (opts.adoptCaller && opts.here !== null)
|
|
48
|
+
return opts.here.session;
|
|
49
|
+
return backstage;
|
|
50
|
+
}
|
|
51
|
+
/** A node's durable REVIVE-HOME, with the legacy back-compat default. Nodes born
|
|
52
|
+
* before `home_session` existed have no such field in meta — they fall back to
|
|
53
|
+
* their last live LOCATION (`tmux_session`), then to the shared backstage
|
|
54
|
+
* (`nodeSession()`). The defaulted read for the placement layer; a present
|
|
55
|
+
* `home_session` is always returned verbatim. */
|
|
56
|
+
export function homeSessionOf(nodeId) {
|
|
57
|
+
const meta = getNode(nodeId);
|
|
58
|
+
if (meta === null)
|
|
59
|
+
return nodeSession();
|
|
60
|
+
return meta.home_session ?? meta.tmux_session ?? nodeSession();
|
|
61
|
+
}
|
|
21
62
|
/** Read the current node's identity from the environment. A spawned pi process
|
|
22
63
|
* runs with CRTR_NODE_ID set; its own `crtr` invocations spawn children under
|
|
23
64
|
* it by reading CRTR_NODE_ID as the parent. */
|
|
@@ -25,7 +66,6 @@ export function currentNodeContext() {
|
|
|
25
66
|
const env = process.env;
|
|
26
67
|
return {
|
|
27
68
|
nodeId: env['CRTR_NODE_ID'] ?? null,
|
|
28
|
-
parentNodeId: env['CRTR_NODE_ID'] ?? null, // a child's parent is the live node
|
|
29
69
|
kind: env['CRTR_KIND'] ?? null,
|
|
30
70
|
mode: env['CRTR_MODE'] ?? null,
|
|
31
71
|
};
|
|
@@ -63,33 +103,52 @@ export function nodeEnv(meta) {
|
|
|
63
103
|
export function spawnNode(opts) {
|
|
64
104
|
const parent = opts.parent ?? null;
|
|
65
105
|
const isRoot = parent === null;
|
|
106
|
+
// Provenance is independent of the spine: a root has no parent but still
|
|
107
|
+
// records who spawned it. A child's spawner is its parent unless overridden.
|
|
108
|
+
const spawnedBy = opts.spawnedBy ?? parent;
|
|
109
|
+
const mode = opts.mode ?? 'base';
|
|
110
|
+
// A user-opened root is resident (a conversation you live in); a spawned node
|
|
111
|
+
// is terminal until it must persist (promotion handles that later).
|
|
112
|
+
const lifecycle = opts.lifecycle ?? (isRoot ? 'resident' : 'terminal');
|
|
66
113
|
const meta = {
|
|
67
114
|
node_id: opts.nodeId ?? newNodeId(),
|
|
68
115
|
name: opts.name ?? opts.kind,
|
|
116
|
+
description: opts.description,
|
|
117
|
+
cycles: 0,
|
|
69
118
|
created: new Date().toISOString(),
|
|
70
119
|
cwd: opts.cwd,
|
|
71
120
|
kind: opts.kind,
|
|
72
|
-
mode
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
121
|
+
mode,
|
|
122
|
+
lifecycle,
|
|
123
|
+
// Born already acked to its initial persona: a fresh node has been "given
|
|
124
|
+
// guidance" for the state it starts in (its bearings carry it), so the
|
|
125
|
+
// persona injector sees no drift on its first turn boundary.
|
|
126
|
+
persona_ack: { mode, lifecycle },
|
|
76
127
|
status: 'active',
|
|
77
128
|
parent,
|
|
129
|
+
spawned_by: spawnedBy,
|
|
78
130
|
passive_default: opts.passiveDefault ?? false,
|
|
79
131
|
intent: null,
|
|
80
132
|
pi_session_id: null,
|
|
133
|
+
pi_session_file: null,
|
|
81
134
|
launch: opts.launch,
|
|
82
135
|
};
|
|
136
|
+
// Validate BEFORE minting: a bad parent must leave no half-born orphan row or
|
|
137
|
+
// dirs behind, so the parent's existence is checked before createNode
|
|
138
|
+
// scaffolds anything on disk or in the db.
|
|
139
|
+
if (parent !== null && getNode(parent) === null) {
|
|
140
|
+
throw new Error(`cannot spawn under unknown parent node: ${parent}`);
|
|
141
|
+
}
|
|
83
142
|
createNode(meta);
|
|
84
143
|
if (parent !== null) {
|
|
85
|
-
if (getNode(parent) === null) {
|
|
86
|
-
throw new Error(`cannot spawn under unknown parent node: ${parent}`);
|
|
87
|
-
}
|
|
88
144
|
// The load-bearing seed: parent subscribes (active) to child so it learns
|
|
89
145
|
// when the work finishes. This mirrors spawn structure into the spine.
|
|
146
|
+
// A root (parent=null) gets NO subscription — nobody is woken by it.
|
|
90
147
|
subscribe(parent, meta.node_id, true);
|
|
91
|
-
|
|
92
|
-
|
|
148
|
+
}
|
|
149
|
+
// Audit-only provenance edge — recorded for a root too (from its spawner).
|
|
150
|
+
if (spawnedBy !== null && spawnedBy !== undefined && getNode(spawnedBy) !== null) {
|
|
151
|
+
recordSpawn(meta.node_id, spawnedBy);
|
|
93
152
|
}
|
|
94
153
|
return meta;
|
|
95
154
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Mode, type Lifecycle } from '../canvas/index.js';
|
|
2
|
+
/** The two-axis persona state the injector keys on. */
|
|
3
|
+
export interface Persona {
|
|
4
|
+
mode: Mode;
|
|
5
|
+
lifecycle: Lifecycle;
|
|
6
|
+
}
|
|
7
|
+
export interface PersonaDriftResult {
|
|
8
|
+
from: Persona;
|
|
9
|
+
to: Persona;
|
|
10
|
+
/** The built transition guidance to inject for `to`. */
|
|
11
|
+
guidance: string;
|
|
12
|
+
}
|
|
13
|
+
/** Build the injected transition prompt for a `from → to` persona change.
|
|
14
|
+
* Concatenates the relevant section per changed axis (both when both changed).
|
|
15
|
+
* Pure read of the node's roadmap/memory for the base→orchestrator case. */
|
|
16
|
+
export declare function transitionGuidance(nodeId: string, from: Persona, to: Persona): string;
|
|
17
|
+
/** Compare a node's live {mode,lifecycle} against its `persona_ack` (the last
|
|
18
|
+
* state it was given guidance for). Returns the transition + built guidance
|
|
19
|
+
* when they differ, else null. Does NOT mutate — the caller delivers the
|
|
20
|
+
* guidance, then `commitPersonaAck`s the new state. An unset `persona_ack`
|
|
21
|
+
* (legacy node) defaults to the current persona, so it reads as no drift and
|
|
22
|
+
* never fabricates spurious guidance. */
|
|
23
|
+
export declare function personaDrift(nodeId: string): PersonaDriftResult | null;
|
|
24
|
+
/** Commit the persona state the node has now been given guidance for. */
|
|
25
|
+
export declare function commitPersonaAck(nodeId: string, to: Persona): void;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// persona.ts — the CENTRALIZED persona-transition injector.
|
|
2
|
+
//
|
|
3
|
+
// A node has two orthogonal, independently switchable axes:
|
|
4
|
+
// • mode — base (hands-on, finishes in one window) ↔ orchestrator
|
|
5
|
+
// (delegates, holds a roadmap, survives refresh cycles + yields)
|
|
6
|
+
// • lifecycle — terminal (owes a final up the spine, reaps when done) ↔
|
|
7
|
+
// resident (interactable, stays dormant, never forced to submit)
|
|
8
|
+
//
|
|
9
|
+
// Whenever EITHER axis changes from the value the node was last GIVEN guidance
|
|
10
|
+
// for, the node must be prompt-injected with guidance for its new state —
|
|
11
|
+
// automatically, here, not by each state-changing command. Commands just call
|
|
12
|
+
// `updateNode({ mode|lifecycle })`; this module is the single source of the
|
|
13
|
+
// transition prose, delivered from exactly two sites:
|
|
14
|
+
// • the stophook turn_end hook (self-changes this turn + external changes
|
|
15
|
+
// while the node is active), and
|
|
16
|
+
// • the revive kickoff (external changes made while the node was dormant).
|
|
17
|
+
//
|
|
18
|
+
// The `persona_ack` meta field records the last {mode,lifecycle} the node was
|
|
19
|
+
// given guidance for (born equal to its initial persona at spawn, so a fresh
|
|
20
|
+
// worker never gets spurious guidance). `personaDrift` compares live meta to it;
|
|
21
|
+
// the caller delivers the guidance, then commits the ack.
|
|
22
|
+
import { getNode, updateNode } from '../canvas/index.js';
|
|
23
|
+
import { loadKernel, loadPersona, loadLifecycleFragment } from '../personas/index.js';
|
|
24
|
+
import { resolveSkill } from '../resolver.js';
|
|
25
|
+
import { readText } from '../fs-utils.js';
|
|
26
|
+
import { parseFrontmatter } from '../frontmatter.js';
|
|
27
|
+
import { readRoadmap, roadmapPath } from './roadmap.js';
|
|
28
|
+
import { orchestratorContextNote } from './bearings.js';
|
|
29
|
+
import { memoryPath, memoryDir, userMemoryPath, userMemoryDir, projectMemoryPath, projectMemoryDir, } from './memory.js';
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// base→orchestrator guidance (the roadmap-shaping dump) — MOVED here from
|
|
32
|
+
// promote.ts so the injector is the one place that builds it.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/** Load a skill's body text by name, or null if it can't be resolved. Used to
|
|
35
|
+
* inline a kind's roadmap-shaping skill into the orchestration guidance. */
|
|
36
|
+
function loadSkillBody(name) {
|
|
37
|
+
try {
|
|
38
|
+
const skill = resolveSkill(name, {});
|
|
39
|
+
return parseFrontmatter(readText(skill.path)).body.trim();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** The base→orchestrator guidance dump, specialized to the node's kind: the
|
|
46
|
+
* shared kernel + that kind's roadmap-shaping skill + the roadmap scaffold the
|
|
47
|
+
* node must author + the orchestrator context-dir framing + the three memory
|
|
48
|
+
* stores. The node is now a delegator whose scarce resource is its own context
|
|
49
|
+
* window. (Lifecycle is left to its own section — promotion no longer forces
|
|
50
|
+
* resident, so this never asserts residency.) */
|
|
51
|
+
function orchestrationGuidance(nodeId, kind, cwd) {
|
|
52
|
+
const kernel = loadKernel();
|
|
53
|
+
const orch = loadPersona(kind, 'orchestrator');
|
|
54
|
+
const roadmapSkill = typeof orch?.frontmatter?.['roadmapSkill'] === 'string'
|
|
55
|
+
? orch.frontmatter['roadmapSkill']
|
|
56
|
+
: undefined;
|
|
57
|
+
const skillBody = roadmapSkill ? loadSkillBody(roadmapSkill) : null;
|
|
58
|
+
const roadmap = readRoadmap(nodeId) ?? '(no roadmap yet)';
|
|
59
|
+
const rmPath = roadmapPath(nodeId);
|
|
60
|
+
const parts = [
|
|
61
|
+
`You are now a ${kind.toUpperCase()} ORCHESTRATOR. Your scarce resource is your own context window.`,
|
|
62
|
+
'Your job is to manage context and delegate — not to do the goal yourself.',
|
|
63
|
+
'',
|
|
64
|
+
kernel,
|
|
65
|
+
];
|
|
66
|
+
if (skillBody) {
|
|
67
|
+
parts.push('', `--- How to shape a ${kind} roadmap (skill: ${roadmapSkill}) ---`, '', skillBody);
|
|
68
|
+
}
|
|
69
|
+
parts.push('', `Your roadmap scaffold (\`${rmPath}\`) — author it now: state the goal, exit criteria, and the phase skeleton, using the approach above. Current contents:`, '', roadmap, '',
|
|
70
|
+
// The orchestrator framing for the context dir — the missing guidance a
|
|
71
|
+
// promoted node never got at spawn (it spawned as a base worker). Same note
|
|
72
|
+
// a born-orchestrator gets in its <crtr-context> bearings block.
|
|
73
|
+
orchestratorContextNote(nodeId), '', 'Your long-term memory now exists across three seeded stores (write to them directly), each a different scope per "Your long-term memory" above:', ` • user-global \`${userMemoryDir()}\` (index \`${userMemoryPath()}\`) — who the human is, how they like to work; loaded into every orchestrator everywhere.`, ` • project \`${projectMemoryDir(cwd)}\` (index \`${projectMemoryPath(cwd)}\`) — facts bound to this repo; loaded into every orchestrator working here.`, ` • node-local \`${memoryDir(nodeId)}\` (index \`${memoryPath(nodeId)}\`) — facts specific to this goal; they die with this node.`, 'A memory\'s `type` decides which store it lands in (see "Your long-term memory"). These same paths ride into every future wake in your `<crtr-context>` block.', '', 'Then delegate each phase with `crtr node new --kind <kind>`. When your context fills, run `crtr node yield` to refresh against this roadmap.');
|
|
74
|
+
return parts.join('\n');
|
|
75
|
+
}
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// The other three transitions — short, prescriptive, audience = the node's
|
|
78
|
+
// agent (decision-first; one well-placed "don't").
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
/** orchestrator → base (demote): hands-on again, finish in-window. */
|
|
81
|
+
function baseModeGuidance() {
|
|
82
|
+
return ('You are HANDS-ON again — base mode. Do the work yourself in THIS window and finish it here; ' +
|
|
83
|
+
'stop delegating by default. You no longer drive a roadmap, so `crtr node yield` is not your exit. ' +
|
|
84
|
+
'Spawn a child only for a cleanly separable unit, never as your first move.');
|
|
85
|
+
}
|
|
86
|
+
// The lifecycle transition prose is the SAME contract that's baked into the
|
|
87
|
+
// static system prompt at birth — so both load from the one source
|
|
88
|
+
// (`personas/lifecycle/{terminal,resident}.md`) and can never drift. The flip
|
|
89
|
+
// only re-delivers that fragment as the node's new-state steer.
|
|
90
|
+
/** terminal → resident: interactable, never forced to submit. */
|
|
91
|
+
function residentLifecycleGuidance() {
|
|
92
|
+
return loadLifecycleFragment('resident');
|
|
93
|
+
}
|
|
94
|
+
/** resident → terminal: owes a final, reaps when done. */
|
|
95
|
+
function terminalLifecycleGuidance() {
|
|
96
|
+
return loadLifecycleFragment('terminal');
|
|
97
|
+
}
|
|
98
|
+
/** Build the injected transition prompt for a `from → to` persona change.
|
|
99
|
+
* Concatenates the relevant section per changed axis (both when both changed).
|
|
100
|
+
* Pure read of the node's roadmap/memory for the base→orchestrator case. */
|
|
101
|
+
export function transitionGuidance(nodeId, from, to) {
|
|
102
|
+
const sections = [];
|
|
103
|
+
if (from.mode !== to.mode) {
|
|
104
|
+
if (to.mode === 'orchestrator') {
|
|
105
|
+
const node = getNode(nodeId);
|
|
106
|
+
sections.push(orchestrationGuidance(nodeId, node?.kind ?? 'general', node?.cwd ?? process.cwd()));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
sections.push(baseModeGuidance());
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (from.lifecycle !== to.lifecycle) {
|
|
113
|
+
sections.push(to.lifecycle === 'resident' ? residentLifecycleGuidance() : terminalLifecycleGuidance());
|
|
114
|
+
}
|
|
115
|
+
return sections.join('\n\n');
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Detector + ack commit.
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
/** Compare a node's live {mode,lifecycle} against its `persona_ack` (the last
|
|
121
|
+
* state it was given guidance for). Returns the transition + built guidance
|
|
122
|
+
* when they differ, else null. Does NOT mutate — the caller delivers the
|
|
123
|
+
* guidance, then `commitPersonaAck`s the new state. An unset `persona_ack`
|
|
124
|
+
* (legacy node) defaults to the current persona, so it reads as no drift and
|
|
125
|
+
* never fabricates spurious guidance. */
|
|
126
|
+
export function personaDrift(nodeId) {
|
|
127
|
+
const meta = getNode(nodeId);
|
|
128
|
+
if (meta === null)
|
|
129
|
+
return null;
|
|
130
|
+
const to = { mode: meta.mode, lifecycle: meta.lifecycle };
|
|
131
|
+
const from = meta.persona_ack ?? { mode: meta.mode, lifecycle: meta.lifecycle };
|
|
132
|
+
if (from.mode === to.mode && from.lifecycle === to.lifecycle)
|
|
133
|
+
return null;
|
|
134
|
+
return { from, to, guidance: transitionGuidance(nodeId, from, to) };
|
|
135
|
+
}
|
|
136
|
+
/** Commit the persona state the node has now been given guidance for. */
|
|
137
|
+
export function commitPersonaAck(nodeId, to) {
|
|
138
|
+
updateNode(nodeId, { persona_ack: to });
|
|
139
|
+
}
|