@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
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// MEMORY.md + memory/ — an orchestrator's persistent file-based memory.
|
|
2
|
+
//
|
|
3
|
+
// One layout, three scopes. Each store is a `memory/` directory of one-fact
|
|
4
|
+
// files (each with typed frontmatter and [[wikilinks]]) indexed by a single
|
|
5
|
+
// MEMORY.md that holds one pointer line per memory and NEVER any content — the
|
|
6
|
+
// architecture in examples/memory-instructions.md. The pointer lines are the
|
|
7
|
+
// load-bearing read: a node's <crtr-context> bearings block extracts every
|
|
8
|
+
// applicable store's pointer lines each brand-new chat (see canvas-context-intro
|
|
9
|
+
// + bearings), so the indexes must stay lean; the detail files load on demand
|
|
10
|
+
// mid-session.
|
|
11
|
+
//
|
|
12
|
+
// The three scopes differ only in WHERE they live and HOW LONG they outlast a
|
|
13
|
+
// node — the `type` taxonomy in each memory's frontmatter drives which store a
|
|
14
|
+
// fact lands in (the mapping lives in the orchestration kernel's "Your long-term
|
|
15
|
+
// memory"). ALL THREE live under the canvas home (crtrHome), all machine-local:
|
|
16
|
+
//
|
|
17
|
+
// user-global <crtrHome>/memory/ — who the human is, how
|
|
18
|
+
// they like to work; loaded into EVERY orchestrator everywhere.
|
|
19
|
+
// project <crtrHome>/projects/<key>/memory/ — facts bound to one
|
|
20
|
+
// repo; loaded into orchestrators whose cwd resolves to that project. <key>
|
|
21
|
+
// is the git-repo-root (walked up from the cwd), else the cwd, mangled.
|
|
22
|
+
// node-local <crtrHome>/nodes/<id>/context/memory/ — facts specific to this
|
|
23
|
+
// node's goal; dies with the node.
|
|
24
|
+
//
|
|
25
|
+
// An ORCHESTRATOR-only artifact — the resident, multi-cycle nodes that survive
|
|
26
|
+
// refreshes and accumulate durable lessons/preferences; terminal workers are
|
|
27
|
+
// one-shot and get none. All three stores are seeded the moment a node becomes
|
|
28
|
+
// an orchestrator (promotion, or born one — where the roadmap is seeded too),
|
|
29
|
+
// guarded so a re-seed never clobbers an evolved memory.
|
|
30
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
31
|
+
import { join, dirname, resolve } from 'node:path';
|
|
32
|
+
import { contextDir, crtrHome } from '../canvas/index.js';
|
|
33
|
+
import { mangleCwd } from '../artifact.js';
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Index template + generic store ops (shared by all three scopes).
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
/** Build the seed contents of a fresh MEMORY.md index. Deliberately tiny: the
|
|
38
|
+
* bearings block only ever extracts the pointer lines, so this prose never
|
|
39
|
+
* rides into context — it's only for a human/agent opening the file directly,
|
|
40
|
+
* and the how-to lives once in the orchestrator kernel ("Your long-term
|
|
41
|
+
* memory"), not here. `holds` is a short scope hint so the empty index still
|
|
42
|
+
* orients a fresh write. */
|
|
43
|
+
function indexTemplate(holds) {
|
|
44
|
+
return ('# memory index — one pointer line per memory (`- [Title](slug.md) — hook`); ' +
|
|
45
|
+
`how-to in "Your long-term memory". Holds ${holds}.\n\n(no memories yet)\n`);
|
|
46
|
+
}
|
|
47
|
+
/** The node-local index template. Named export kept for callers/tests that
|
|
48
|
+
* assert the seeded node store verbatim. */
|
|
49
|
+
export const MEMORY_TEMPLATE = indexTemplate('your saved memories');
|
|
50
|
+
/** The user-global index template — framed around the human, not a goal. */
|
|
51
|
+
export const USER_MEMORY_TEMPLATE = indexTemplate('your saved memories about the human — who they are and how they like to work');
|
|
52
|
+
/** The project index template — framed around the repo. */
|
|
53
|
+
export const PROJECT_MEMORY_TEMPLATE = indexTemplate('your saved memories about this project');
|
|
54
|
+
/** The MEMORY.md index path inside a memory `dir`. */
|
|
55
|
+
function indexPathOf(dir) {
|
|
56
|
+
return join(dir, 'MEMORY.md');
|
|
57
|
+
}
|
|
58
|
+
/** Seed `dir` + its MEMORY.md index with `template` IFF the index is absent.
|
|
59
|
+
* Idempotent and guarded so it never clobbers an evolved memory; creating the
|
|
60
|
+
* dir up front lets the node write detail files into it directly (no mkdir).
|
|
61
|
+
* Returns true when it seeded, false when an index already existed. */
|
|
62
|
+
function seedStore(dir, template) {
|
|
63
|
+
const idx = indexPathOf(dir);
|
|
64
|
+
if (existsSync(idx))
|
|
65
|
+
return false;
|
|
66
|
+
mkdirSync(dir, { recursive: true });
|
|
67
|
+
writeFileSync(idx, template);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
/** Read a store's MEMORY.md index, or null when it doesn't exist. */
|
|
71
|
+
function readStore(dir) {
|
|
72
|
+
const idx = indexPathOf(dir);
|
|
73
|
+
return existsSync(idx) ? readFileSync(idx, 'utf8') : null;
|
|
74
|
+
}
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// node-local store — <crtrHome>/nodes/<id>/context/memory/ (facts for this goal)
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
/** The node-local memory directory in a node's context dir — holds MEMORY.md
|
|
79
|
+
* (the index) and the one-fact detail files it points at. */
|
|
80
|
+
export function memoryDir(nodeId) {
|
|
81
|
+
return join(contextDir(nodeId), 'memory');
|
|
82
|
+
}
|
|
83
|
+
/** The node-local MEMORY.md index path (inside the memory dir). */
|
|
84
|
+
export function memoryPath(nodeId) {
|
|
85
|
+
return indexPathOf(memoryDir(nodeId));
|
|
86
|
+
}
|
|
87
|
+
/** Whether the node has a node-local memory store. This is ALSO the
|
|
88
|
+
* orchestrator gate: only orchestrators are ever seeded one, so a node with no
|
|
89
|
+
* node-local store is a terminal worker (no memory framing at all). */
|
|
90
|
+
export function hasMemory(nodeId) {
|
|
91
|
+
return existsSync(memoryPath(nodeId));
|
|
92
|
+
}
|
|
93
|
+
/** Read the node-local MEMORY.md index, or null when it doesn't exist. */
|
|
94
|
+
export function readMemory(nodeId) {
|
|
95
|
+
return readStore(memoryDir(nodeId));
|
|
96
|
+
}
|
|
97
|
+
/** Seed the node-local memory dir + index IF the node has none yet. */
|
|
98
|
+
export function seedMemory(nodeId) {
|
|
99
|
+
return seedStore(memoryDir(nodeId), MEMORY_TEMPLATE);
|
|
100
|
+
}
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// user-global store — <crtrHome>/memory/ (who the human is, how they work)
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
/** The user-global memory directory — one per machine, key-less, loaded into
|
|
105
|
+
* every orchestrator everywhere. */
|
|
106
|
+
export function userMemoryDir() {
|
|
107
|
+
return join(crtrHome(), 'memory');
|
|
108
|
+
}
|
|
109
|
+
/** The user-global MEMORY.md index path. */
|
|
110
|
+
export function userMemoryPath() {
|
|
111
|
+
return indexPathOf(userMemoryDir());
|
|
112
|
+
}
|
|
113
|
+
export function hasUserMemory() {
|
|
114
|
+
return existsSync(userMemoryPath());
|
|
115
|
+
}
|
|
116
|
+
/** Read the user-global MEMORY.md index, or null when it doesn't exist. */
|
|
117
|
+
export function readUserMemory() {
|
|
118
|
+
return readStore(userMemoryDir());
|
|
119
|
+
}
|
|
120
|
+
/** Seed the user-global memory dir + index IF absent. */
|
|
121
|
+
export function seedUserMemory() {
|
|
122
|
+
return seedStore(userMemoryDir(), USER_MEMORY_TEMPLATE);
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// project store — <crtrHome>/projects/<key>/memory/ (facts bound to one repo)
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
/** The git repo root containing `cwd` — walk up for a `.git` entry — or null
|
|
128
|
+
* when `cwd` is not inside a repo. `.git` may be a dir (normal) or a file
|
|
129
|
+
* (worktree/submodule); existsSync catches both. */
|
|
130
|
+
function gitRoot(cwd) {
|
|
131
|
+
let dir = resolve(cwd);
|
|
132
|
+
for (;;) {
|
|
133
|
+
if (existsSync(join(dir, '.git')))
|
|
134
|
+
return dir;
|
|
135
|
+
const parent = dirname(dir);
|
|
136
|
+
if (parent === dir)
|
|
137
|
+
return null; // hit the filesystem root
|
|
138
|
+
dir = parent;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** The project key for `cwd`: its git-repo-root when inside a repo, else the
|
|
142
|
+
* cwd itself, mangled into a flat directory name (reuses artifact mangleCwd).
|
|
143
|
+
* This keys the per-project memory store under <crtrHome>/projects/. */
|
|
144
|
+
export function projectKey(cwd) {
|
|
145
|
+
return mangleCwd(gitRoot(cwd) ?? cwd);
|
|
146
|
+
}
|
|
147
|
+
/** The project memory directory for `cwd`. */
|
|
148
|
+
export function projectMemoryDir(cwd) {
|
|
149
|
+
return join(crtrHome(), 'projects', projectKey(cwd), 'memory');
|
|
150
|
+
}
|
|
151
|
+
/** The project MEMORY.md index path for `cwd`. */
|
|
152
|
+
export function projectMemoryPath(cwd) {
|
|
153
|
+
return indexPathOf(projectMemoryDir(cwd));
|
|
154
|
+
}
|
|
155
|
+
export function hasProjectMemory(cwd) {
|
|
156
|
+
return existsSync(projectMemoryPath(cwd));
|
|
157
|
+
}
|
|
158
|
+
/** Read the project MEMORY.md index for `cwd`, or null when it doesn't exist. */
|
|
159
|
+
export function readProjectMemory(cwd) {
|
|
160
|
+
return readStore(projectMemoryDir(cwd));
|
|
161
|
+
}
|
|
162
|
+
/** Seed the project memory dir + index for `cwd` IF absent. */
|
|
163
|
+
export function seedProjectMemory(cwd) {
|
|
164
|
+
return seedStore(projectMemoryDir(cwd), PROJECT_MEMORY_TEMPLATE);
|
|
165
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { NodeMeta } from '../canvas/index.js';
|
|
2
|
+
/** Coerce arbitrary text into a 3-5 word kebab-case name, or '' if nothing
|
|
3
|
+
* usable survives. Lowercases, keeps [a-z0-9], collapses everything else to a
|
|
4
|
+
* single hyphen, and clamps to the first 5 words. */
|
|
5
|
+
export declare function sanitizeSessionName(raw: string): string;
|
|
6
|
+
/** Local fallback: derive a name straight from the prompt (no pi call). Drops
|
|
7
|
+
* stop-words, takes the first few content words. */
|
|
8
|
+
export declare function slugFromPrompt(prompt: string): string;
|
|
9
|
+
/** Synchronously ask pi for a 2-4 word kebab name for `prompt`. Blocks up to
|
|
10
|
+
* NAME_TIMEOUT_MS; on any failure (non-zero exit, timeout, empty/garbled
|
|
11
|
+
* output) falls back to a local slug. Returns '' only for an empty prompt. */
|
|
12
|
+
export declare function generateSessionName(prompt: string): string;
|
|
13
|
+
/** Asynchronously generate a name for `prompt` and persist it to the node's
|
|
14
|
+
* meta as `description` — only if the node has none yet (so a later message
|
|
15
|
+
* never clobbers it). Non-blocking: safe to call from inside a live pi event
|
|
16
|
+
* loop. Best-effort; swallows all errors.
|
|
17
|
+
*
|
|
18
|
+
* `onNamed` (optional) fires with the freshly-persisted meta the moment the
|
|
19
|
+
* name lands — the bare-root path passes a callback that calls
|
|
20
|
+
* pi.setSessionName(editorLabel(meta)) so the LIVE editor label updates in the
|
|
21
|
+
* same session, instead of waiting for the next revive/cycle. */
|
|
22
|
+
export declare function generateAndPersistName(nodeId: string, prompt: string, onNamed?: (meta: NodeMeta) => void): void;
|
|
@@ -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,34 @@
|
|
|
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
|
+
/** Resolve the tmux session a freshly-born node's window/pane opens into — and
|
|
5
|
+
* thus its durable REVIVE-HOME (`home_session`). Pure so the birth decision is
|
|
6
|
+
* unit-testable without a live tmux:
|
|
7
|
+
* - managed background child (`adoptCaller=false`) → the shared backstage:
|
|
8
|
+
* the inherited `CRTR_ROOT_SESSION`, else `nodeSession()` (`crtr`).
|
|
9
|
+
* - independent `--root` / inline front door (`adoptCaller=true`) → the
|
|
10
|
+
* caller's CURRENT session when inside tmux (`here`), else the backstage.
|
|
11
|
+
* This is exactly the session each birth site already places the node into;
|
|
12
|
+
* centralizing it keeps `home_session` and the actual placement in lockstep. */
|
|
13
|
+
export declare function resolveBirthSession(opts: {
|
|
14
|
+
/** True for an independent root or the inline front door (both adopt the
|
|
15
|
+
* caller's session); false for a managed background child. */
|
|
16
|
+
adoptCaller: boolean;
|
|
17
|
+
/** The caller's current tmux location, or null when not inside tmux. */
|
|
18
|
+
here: {
|
|
19
|
+
session: string;
|
|
20
|
+
} | null;
|
|
21
|
+
/** The inherited CRTR_ROOT_SESSION (the backstage the subtree flows into). */
|
|
22
|
+
rootSession?: string | null;
|
|
23
|
+
}): string;
|
|
24
|
+
/** A node's durable REVIVE-HOME, with the legacy back-compat default. Nodes born
|
|
25
|
+
* before `home_session` existed have no such field in meta — they fall back to
|
|
26
|
+
* their last live LOCATION (`tmux_session`), then to the shared backstage
|
|
27
|
+
* (`nodeSession()`). The defaulted read for the placement layer; a present
|
|
28
|
+
* `home_session` is always returned verbatim. */
|
|
29
|
+
export declare function homeSessionOf(nodeId: string): string;
|
|
4
30
|
export interface NodeContext {
|
|
5
31
|
nodeId: string | null;
|
|
6
|
-
parentNodeId: string | null;
|
|
7
32
|
kind: string | null;
|
|
8
33
|
mode: Mode | null;
|
|
9
34
|
}
|
|
@@ -21,8 +46,14 @@ export interface SpawnNodeOpts {
|
|
|
21
46
|
lifecycle?: Lifecycle;
|
|
22
47
|
cwd: string;
|
|
23
48
|
name?: string;
|
|
49
|
+
/** Editor-label handle (2-4 word kebab-case) for the node's first prompt. */
|
|
50
|
+
description?: string;
|
|
24
51
|
/** Parent node id. Omit for a user-opened root. */
|
|
25
52
|
parent?: string | null;
|
|
53
|
+
/** Who spawned me (the `spawned_by` provenance edge), when it differs from
|
|
54
|
+
* `parent` — e.g. an independent root (parent=null) still records its
|
|
55
|
+
* spawner. Defaults to `parent`. */
|
|
56
|
+
spawnedBy?: string | null;
|
|
26
57
|
/** New subscriptions this node opens default to passive when true. */
|
|
27
58
|
passiveDefault?: boolean;
|
|
28
59
|
/** Resolved pi launch recipe (from resolve(kind,mode)). */
|
|
@@ -14,10 +14,42 @@
|
|
|
14
14
|
// is also recorded.
|
|
15
15
|
import { randomBytes } from 'node:crypto';
|
|
16
16
|
import { createNode, getNode, subscribe, recordSpawn, } from '../canvas/index.js';
|
|
17
|
+
import { nodeSession } from './tmux.js';
|
|
17
18
|
/** Generate a node id in the same shape as job ids (time-sortable + random). */
|
|
18
19
|
export function newNodeId() {
|
|
19
20
|
return `${Date.now().toString(36)}-${randomBytes(4).toString('hex')}`;
|
|
20
21
|
}
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// REVIVE-HOME (home_session) — the durable session a node is (re)opened into
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
/** Resolve the tmux session a freshly-born node's window/pane opens into — and
|
|
26
|
+
* thus its durable REVIVE-HOME (`home_session`). Pure so the birth decision is
|
|
27
|
+
* unit-testable without a live tmux:
|
|
28
|
+
* - managed background child (`adoptCaller=false`) → the shared backstage:
|
|
29
|
+
* the inherited `CRTR_ROOT_SESSION`, else `nodeSession()` (`crtr`).
|
|
30
|
+
* - independent `--root` / inline front door (`adoptCaller=true`) → the
|
|
31
|
+
* caller's CURRENT session when inside tmux (`here`), else the backstage.
|
|
32
|
+
* This is exactly the session each birth site already places the node into;
|
|
33
|
+
* centralizing it keeps `home_session` and the actual placement in lockstep. */
|
|
34
|
+
export function resolveBirthSession(opts) {
|
|
35
|
+
const backstage = opts.rootSession !== undefined && opts.rootSession !== null && opts.rootSession !== ''
|
|
36
|
+
? opts.rootSession
|
|
37
|
+
: nodeSession();
|
|
38
|
+
if (opts.adoptCaller && opts.here !== null)
|
|
39
|
+
return opts.here.session;
|
|
40
|
+
return backstage;
|
|
41
|
+
}
|
|
42
|
+
/** A node's durable REVIVE-HOME, with the legacy back-compat default. Nodes born
|
|
43
|
+
* before `home_session` existed have no such field in meta — they fall back to
|
|
44
|
+
* their last live LOCATION (`tmux_session`), then to the shared backstage
|
|
45
|
+
* (`nodeSession()`). The defaulted read for the placement layer; a present
|
|
46
|
+
* `home_session` is always returned verbatim. */
|
|
47
|
+
export function homeSessionOf(nodeId) {
|
|
48
|
+
const meta = getNode(nodeId);
|
|
49
|
+
if (meta === null)
|
|
50
|
+
return nodeSession();
|
|
51
|
+
return meta.home_session ?? meta.tmux_session ?? nodeSession();
|
|
52
|
+
}
|
|
21
53
|
/** Read the current node's identity from the environment. A spawned pi process
|
|
22
54
|
* runs with CRTR_NODE_ID set; its own `crtr` invocations spawn children under
|
|
23
55
|
* it by reading CRTR_NODE_ID as the parent. */
|
|
@@ -25,7 +57,6 @@ export function currentNodeContext() {
|
|
|
25
57
|
const env = process.env;
|
|
26
58
|
return {
|
|
27
59
|
nodeId: env['CRTR_NODE_ID'] ?? null,
|
|
28
|
-
parentNodeId: env['CRTR_NODE_ID'] ?? null, // a child's parent is the live node
|
|
29
60
|
kind: env['CRTR_KIND'] ?? null,
|
|
30
61
|
mode: env['CRTR_MODE'] ?? null,
|
|
31
62
|
};
|
|
@@ -63,33 +94,52 @@ export function nodeEnv(meta) {
|
|
|
63
94
|
export function spawnNode(opts) {
|
|
64
95
|
const parent = opts.parent ?? null;
|
|
65
96
|
const isRoot = parent === null;
|
|
97
|
+
// Provenance is independent of the spine: a root has no parent but still
|
|
98
|
+
// records who spawned it. A child's spawner is its parent unless overridden.
|
|
99
|
+
const spawnedBy = opts.spawnedBy ?? parent;
|
|
100
|
+
const mode = opts.mode ?? 'base';
|
|
101
|
+
// A user-opened root is resident (a conversation you live in); a spawned node
|
|
102
|
+
// is terminal until it must persist (promotion handles that later).
|
|
103
|
+
const lifecycle = opts.lifecycle ?? (isRoot ? 'resident' : 'terminal');
|
|
66
104
|
const meta = {
|
|
67
105
|
node_id: opts.nodeId ?? newNodeId(),
|
|
68
106
|
name: opts.name ?? opts.kind,
|
|
107
|
+
description: opts.description,
|
|
108
|
+
cycles: 0,
|
|
69
109
|
created: new Date().toISOString(),
|
|
70
110
|
cwd: opts.cwd,
|
|
71
111
|
kind: opts.kind,
|
|
72
|
-
mode
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
112
|
+
mode,
|
|
113
|
+
lifecycle,
|
|
114
|
+
// Born already acked to its initial persona: a fresh node has been "given
|
|
115
|
+
// guidance" for the state it starts in (its bearings carry it), so the
|
|
116
|
+
// persona injector sees no drift on its first turn boundary.
|
|
117
|
+
persona_ack: { mode, lifecycle },
|
|
76
118
|
status: 'active',
|
|
77
119
|
parent,
|
|
120
|
+
spawned_by: spawnedBy,
|
|
78
121
|
passive_default: opts.passiveDefault ?? false,
|
|
79
122
|
intent: null,
|
|
80
123
|
pi_session_id: null,
|
|
124
|
+
pi_session_file: null,
|
|
81
125
|
launch: opts.launch,
|
|
82
126
|
};
|
|
127
|
+
// Validate BEFORE minting: a bad parent must leave no half-born orphan row or
|
|
128
|
+
// dirs behind, so the parent's existence is checked before createNode
|
|
129
|
+
// scaffolds anything on disk or in the db.
|
|
130
|
+
if (parent !== null && getNode(parent) === null) {
|
|
131
|
+
throw new Error(`cannot spawn under unknown parent node: ${parent}`);
|
|
132
|
+
}
|
|
83
133
|
createNode(meta);
|
|
84
134
|
if (parent !== null) {
|
|
85
|
-
if (getNode(parent) === null) {
|
|
86
|
-
throw new Error(`cannot spawn under unknown parent node: ${parent}`);
|
|
87
|
-
}
|
|
88
135
|
// The load-bearing seed: parent subscribes (active) to child so it learns
|
|
89
136
|
// when the work finishes. This mirrors spawn structure into the spine.
|
|
137
|
+
// A root (parent=null) gets NO subscription — nobody is woken by it.
|
|
90
138
|
subscribe(parent, meta.node_id, true);
|
|
91
|
-
|
|
92
|
-
|
|
139
|
+
}
|
|
140
|
+
// Audit-only provenance edge — recorded for a root too (from its spawner).
|
|
141
|
+
if (spawnedBy !== null && spawnedBy !== undefined && getNode(spawnedBy) !== null) {
|
|
142
|
+
recordSpawn(meta.node_id, spawnedBy);
|
|
93
143
|
}
|
|
94
144
|
return meta;
|
|
95
145
|
}
|
|
@@ -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;
|