@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
package/dist/core/feed/feed.js
CHANGED
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
// file-system friendliness and lexicographic sort alignment.
|
|
10
10
|
import { writeFileSync, renameSync, mkdirSync, existsSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
|
-
import { reportsDir, subscribersOf,
|
|
12
|
+
import { reportsDir, subscribersOf, } from '../canvas/index.js';
|
|
13
|
+
import { transition } from '../runtime/lifecycle.js';
|
|
13
14
|
import { appendInbox } from './inbox.js';
|
|
15
|
+
import { appendPassive } from './passive.js';
|
|
14
16
|
// ---------------------------------------------------------------------------
|
|
15
17
|
// Internal helpers
|
|
16
18
|
// ---------------------------------------------------------------------------
|
|
@@ -74,25 +76,25 @@ export async function push(nodeId, opts) {
|
|
|
74
76
|
const ts = compactTs(now);
|
|
75
77
|
// (a) Write the report.
|
|
76
78
|
const reportPath = writeReport(nodeId, kind, ts, body);
|
|
77
|
-
// (b) Fan out
|
|
78
|
-
//
|
|
79
|
+
// (b) Fan out a pointer to every subscriber. Active subscribers get it on
|
|
80
|
+
// inbox.jsonl (the inbox-watcher polls that → a wake). Passive subscribers
|
|
81
|
+
// get it on passive.jsonl instead — the watcher never polls that, so they
|
|
82
|
+
// are NOT woken; the pointer accumulates until the node is next messaged,
|
|
83
|
+
// when canvas-passive-context drains it as XML pre-text.
|
|
79
84
|
const subscribers = subscribersOf(nodeId);
|
|
80
85
|
const deliveredTo = [];
|
|
81
86
|
const label = firstLine(body);
|
|
82
87
|
for (const sub of subscribers) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
label,
|
|
89
|
-
});
|
|
88
|
+
const entry = { from, tier: tierFor(kind), kind, ref: reportPath, label };
|
|
89
|
+
if (sub.active)
|
|
90
|
+
appendInbox(sub.node_id, entry);
|
|
91
|
+
else
|
|
92
|
+
appendPassive(sub.node_id, entry);
|
|
90
93
|
deliveredTo.push(sub.node_id);
|
|
91
94
|
}
|
|
92
95
|
// (c) Finalise node when kind === 'final'.
|
|
93
96
|
if (kind === 'final') {
|
|
94
|
-
|
|
95
|
-
updateNode(nodeId, { intent: 'done' });
|
|
97
|
+
transition(nodeId, 'finalize');
|
|
96
98
|
}
|
|
97
99
|
return { reportPath, deliveredTo };
|
|
98
100
|
}
|
|
@@ -41,7 +41,9 @@ export declare function writeCursor(nodeId: string, iso: string): void;
|
|
|
41
41
|
*
|
|
42
42
|
* Format (per sender group):
|
|
43
43
|
* From <sender> — <N> update(s):
|
|
44
|
-
* [<kind>] <label> (ref: <path>)
|
|
44
|
+
* [<kind>] <label> (ref: <path>) ← push: pointer, dereference the ref
|
|
45
|
+
* [<kind>] ← ref-less msg: full body inlined
|
|
46
|
+
* <body line>
|
|
45
47
|
* …
|
|
46
48
|
*
|
|
47
49
|
* A header line announces the total count and instructs the receiver to
|
package/dist/core/feed/inbox.js
CHANGED
|
@@ -89,12 +89,55 @@ export function writeCursor(nodeId, iso) {
|
|
|
89
89
|
// ---------------------------------------------------------------------------
|
|
90
90
|
// Coalesce
|
|
91
91
|
// ---------------------------------------------------------------------------
|
|
92
|
+
/** Bounds for inlining a ref-less entry's body in the digest. */
|
|
93
|
+
const BODY_MAX_LINES = 12;
|
|
94
|
+
const BODY_MAX_CHARS = 1000;
|
|
95
|
+
/** Clip a body to a bounded preview, reporting whether anything was dropped. */
|
|
96
|
+
function clipBody(body) {
|
|
97
|
+
let text = body;
|
|
98
|
+
let clipped = false;
|
|
99
|
+
const lines = text.split('\n');
|
|
100
|
+
if (lines.length > BODY_MAX_LINES) {
|
|
101
|
+
text = lines.slice(0, BODY_MAX_LINES).join('\n');
|
|
102
|
+
clipped = true;
|
|
103
|
+
}
|
|
104
|
+
if (text.length > BODY_MAX_CHARS) {
|
|
105
|
+
text = text.slice(0, BODY_MAX_CHARS);
|
|
106
|
+
clipped = true;
|
|
107
|
+
}
|
|
108
|
+
return { text: text.trimEnd(), clipped };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Render one entry's digest line(s).
|
|
112
|
+
*
|
|
113
|
+
* A push pointer (has a `ref`) stays a pointer — the body lives in the report
|
|
114
|
+
* file, dereferenced on demand by reading that path. A ref-less entry (a direct
|
|
115
|
+
* `node msg` or a system alert) has NO report to dereference; its full body
|
|
116
|
+
* lives only in `data.body`, and `label` is just the first line truncated. So
|
|
117
|
+
* for those we inline the body (bounded) — rendering only the truncated label
|
|
118
|
+
* would strand the rest with nowhere to recover it.
|
|
119
|
+
*/
|
|
120
|
+
function renderEntry(e) {
|
|
121
|
+
if (e.ref !== undefined) {
|
|
122
|
+
return ` [${e.kind}] ${e.label} (ref: ${e.ref})`;
|
|
123
|
+
}
|
|
124
|
+
const body = typeof e.data?.['body'] === 'string' ? e.data['body'].trim() : '';
|
|
125
|
+
if (body === '' || body === e.label) {
|
|
126
|
+
return ` [${e.kind}] ${e.label}`;
|
|
127
|
+
}
|
|
128
|
+
const { text, clipped } = clipBody(body);
|
|
129
|
+
const indented = text.split('\n').map((l) => ` ${l}`).join('\n');
|
|
130
|
+
const more = clipped ? '\n … (body clipped)' : '';
|
|
131
|
+
return ` [${e.kind}]\n${indented}${more}`;
|
|
132
|
+
}
|
|
92
133
|
/**
|
|
93
134
|
* Render many unread inbox pointers into one compact digest string.
|
|
94
135
|
*
|
|
95
136
|
* Format (per sender group):
|
|
96
137
|
* From <sender> — <N> update(s):
|
|
97
|
-
* [<kind>] <label> (ref: <path>)
|
|
138
|
+
* [<kind>] <label> (ref: <path>) ← push: pointer, dereference the ref
|
|
139
|
+
* [<kind>] ← ref-less msg: full body inlined
|
|
140
|
+
* <body line>
|
|
98
141
|
* …
|
|
99
142
|
*
|
|
100
143
|
* A header line announces the total count and instructs the receiver to
|
|
@@ -114,10 +157,7 @@ export function coalesce(entries) {
|
|
|
114
157
|
}
|
|
115
158
|
const sections = [];
|
|
116
159
|
for (const [sender, items] of groups) {
|
|
117
|
-
const lines = items.map(
|
|
118
|
-
const refPart = e.ref !== undefined ? ` (ref: ${e.ref})` : '';
|
|
119
|
-
return ` [${e.kind}] ${e.label}${refPart}`;
|
|
120
|
-
});
|
|
160
|
+
const lines = items.map(renderEntry);
|
|
121
161
|
sections.push(`From ${sender} — ${items.length} update${items.length === 1 ? '' : 's'}:\n${lines.join('\n')}`);
|
|
122
162
|
}
|
|
123
163
|
return header + sections.join('\n\n');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { InboxEntry } from './inbox.js';
|
|
2
|
+
/**
|
|
3
|
+
* Atomically append one entry to `nodes/<nodeId>/passive.jsonl`.
|
|
4
|
+
* Fills `ts` (current ISO time). Returns the completed entry.
|
|
5
|
+
*/
|
|
6
|
+
export declare function appendPassive(nodeId: string, entry: Omit<InboxEntry, 'ts'>): InboxEntry;
|
|
7
|
+
/** Return every accumulated passive entry (oldest first) without clearing. */
|
|
8
|
+
export declare function readPassive(nodeId: string): InboxEntry[];
|
|
9
|
+
/**
|
|
10
|
+
* Read AND clear the accumulator in one shot — the drain-on-message primitive.
|
|
11
|
+
*
|
|
12
|
+
* We rename the file aside before reading so a concurrent `appendPassive` (a
|
|
13
|
+
* publisher pushing at the same instant) starts a fresh file and is never lost
|
|
14
|
+
* to the truncate: at worst it lands in the next drain. The renamed snapshot is
|
|
15
|
+
* removed after a successful read. Returns the drained entries (oldest first).
|
|
16
|
+
*/
|
|
17
|
+
export declare function drainPassive(nodeId: string): InboxEntry[];
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Per-node passive-subscription accumulator for the pi-native canvas runtime.
|
|
2
|
+
//
|
|
3
|
+
// A PASSIVE subscription (the `active=false` flavor of a subscribes_to edge)
|
|
4
|
+
// must never WAKE its subscriber. So when `push` fans out, a passive
|
|
5
|
+
// subscriber's pointer is written here — to nodes/<id>/passive.jsonl — instead
|
|
6
|
+
// of inbox.jsonl. The inbox-watcher polls only inbox.jsonl, so nothing here
|
|
7
|
+
// triggers a turn.
|
|
8
|
+
//
|
|
9
|
+
// The accumulator is drained the moment the node is next MESSAGED: the
|
|
10
|
+
// canvas-passive-context extension reads + clears this file on pi's `input`
|
|
11
|
+
// event and injects every entry as timestamped XML pre-text before the message
|
|
12
|
+
// reaches the LLM. Until then entries simply pile up, oldest first.
|
|
13
|
+
//
|
|
14
|
+
// Same entry shape as the inbox (InboxEntry) so the two stores stay symmetric
|
|
15
|
+
// and a passive edge can be flipped active without reshaping data.
|
|
16
|
+
import { appendFileSync, existsSync, readFileSync, renameSync, rmSync, mkdirSync, } from 'node:fs';
|
|
17
|
+
import { dirname } from 'node:path';
|
|
18
|
+
import { passivePath } from '../canvas/index.js';
|
|
19
|
+
/**
|
|
20
|
+
* Atomically append one entry to `nodes/<nodeId>/passive.jsonl`.
|
|
21
|
+
* Fills `ts` (current ISO time). Returns the completed entry.
|
|
22
|
+
*/
|
|
23
|
+
export function appendPassive(nodeId, entry) {
|
|
24
|
+
const full = { ts: new Date().toISOString(), ...entry };
|
|
25
|
+
const line = JSON.stringify(full) + '\n';
|
|
26
|
+
const dir = dirname(passivePath(nodeId));
|
|
27
|
+
if (!existsSync(dir))
|
|
28
|
+
mkdirSync(dir, { recursive: true });
|
|
29
|
+
appendFileSync(passivePath(nodeId), line, { encoding: 'utf8', flag: 'a' });
|
|
30
|
+
return full;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parse jsonl text into entries, tolerating corruption: each line is parsed on
|
|
34
|
+
* its own and a single malformed line is SKIPPED, never the whole feed. The
|
|
35
|
+
* drain path renames + deletes its snapshot, so a non-tolerant parse here would
|
|
36
|
+
* silently discard every accumulated entry the moment one bad line appears.
|
|
37
|
+
*/
|
|
38
|
+
function parseEntries(text) {
|
|
39
|
+
const entries = [];
|
|
40
|
+
for (const line of text.split('\n')) {
|
|
41
|
+
if (line.trim() === '')
|
|
42
|
+
continue;
|
|
43
|
+
try {
|
|
44
|
+
entries.push(JSON.parse(line));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Skip only the corrupt line — the rest of the feed survives.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return entries;
|
|
51
|
+
}
|
|
52
|
+
/** Return every accumulated passive entry (oldest first) without clearing. */
|
|
53
|
+
export function readPassive(nodeId) {
|
|
54
|
+
const p = passivePath(nodeId);
|
|
55
|
+
if (!existsSync(p))
|
|
56
|
+
return [];
|
|
57
|
+
return parseEntries(readFileSync(p, 'utf8'));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Read AND clear the accumulator in one shot — the drain-on-message primitive.
|
|
61
|
+
*
|
|
62
|
+
* We rename the file aside before reading so a concurrent `appendPassive` (a
|
|
63
|
+
* publisher pushing at the same instant) starts a fresh file and is never lost
|
|
64
|
+
* to the truncate: at worst it lands in the next drain. The renamed snapshot is
|
|
65
|
+
* removed after a successful read. Returns the drained entries (oldest first).
|
|
66
|
+
*/
|
|
67
|
+
export function drainPassive(nodeId) {
|
|
68
|
+
const p = passivePath(nodeId);
|
|
69
|
+
if (!existsSync(p))
|
|
70
|
+
return [];
|
|
71
|
+
const snapshot = `${p}.draining`;
|
|
72
|
+
try {
|
|
73
|
+
renameSync(p, snapshot);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Lost the race (file vanished) — nothing to drain.
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
// Parse with the tolerant per-line parser BEFORE removing the snapshot, so a
|
|
80
|
+
// single corrupt line can never discard the whole accumulated feed.
|
|
81
|
+
let entries = [];
|
|
82
|
+
try {
|
|
83
|
+
entries = parseEntries(readFileSync(snapshot, 'utf8'));
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
try {
|
|
87
|
+
rmSync(snapshot, { force: true });
|
|
88
|
+
}
|
|
89
|
+
catch { /* best-effort cleanup */ }
|
|
90
|
+
}
|
|
91
|
+
return entries;
|
|
92
|
+
}
|
package/dist/core/help.d.ts
CHANGED
|
@@ -47,6 +47,36 @@ export interface ContextFileParam {
|
|
|
47
47
|
shape?: string;
|
|
48
48
|
}
|
|
49
49
|
export type InputParam = PositionalParam | FlagParam | StdinParam | ContextFileParam;
|
|
50
|
+
/** How prominently a subcommand surfaces in ancestor (parent / root) -h
|
|
51
|
+
* listings. Set per child in the parent branch's `help.children`. Default
|
|
52
|
+
* 'normal'.
|
|
53
|
+
* - hidden — never listed anywhere, not even in this branch's own -h.
|
|
54
|
+
* You must already know it exists to invoke it.
|
|
55
|
+
* - normal — listed in this branch's own -h only (the default).
|
|
56
|
+
* - common — ALSO promoted into the parent's -h, as a bare qualified name.
|
|
57
|
+
* - important — ALSO promoted into the parent's -h, name + shortform desc. */
|
|
58
|
+
export type SubTier = 'hidden' | 'normal' | 'common' | 'important';
|
|
59
|
+
/** A child's assembled parent-level listing entry — computed by defineBranch
|
|
60
|
+
* from each child def's own self-description (`description`/`whenToUse`/`tier`).
|
|
61
|
+
* renderBranch consumes this; it is never authored by hand and there is no
|
|
62
|
+
* parent-side copy of a child's description (principle 16: each node owns its
|
|
63
|
+
* representation one level up). */
|
|
64
|
+
export interface ListingChild {
|
|
65
|
+
name: string;
|
|
66
|
+
/** Short description for this child's <subcommand> row. */
|
|
67
|
+
description: string;
|
|
68
|
+
/** Selection rubric — plainly states when to reach for this command. Expansive
|
|
69
|
+
* with a variety of examples for judgment-heavy commands; concise for
|
|
70
|
+
* genuinely single-purpose ones. Rendered verbatim (no prefix). */
|
|
71
|
+
whenToUse: string;
|
|
72
|
+
/** Visibility tier in ancestor listings (see SubTier). 'hidden' children are
|
|
73
|
+
* dropped from every listing. */
|
|
74
|
+
tier: SubTier;
|
|
75
|
+
/** How many non-hidden subcommands this child itself owns — drives the
|
|
76
|
+
* `subcommands="N"` attribute when a branch child is listed without
|
|
77
|
+
* expansion. Absent for leaves and childless branches. */
|
|
78
|
+
subCount?: number;
|
|
79
|
+
}
|
|
50
80
|
/** A subtree's self-description at the parent (root) level. Each subtree owns
|
|
51
81
|
* the content that represents it one level up: its vocabulary line, its
|
|
52
82
|
* selection rubric, and any bounded block it contributes to the parent's -h.
|
|
@@ -75,32 +105,48 @@ export interface RootHelp {
|
|
|
75
105
|
* root, carrying the subtree's concept, selection rubric, and any nested
|
|
76
106
|
* runtime-state block. Assembled from subtrees' RootEntry by defineRoot;
|
|
77
107
|
* root hardcodes none of it. */
|
|
78
|
-
commands:
|
|
79
|
-
name: string;
|
|
80
|
-
concept: string;
|
|
81
|
-
desc: string;
|
|
82
|
-
useWhen: string;
|
|
83
|
-
dynamicState?: () => string | null;
|
|
84
|
-
}[];
|
|
108
|
+
commands: RootCommand[];
|
|
85
109
|
globals: {
|
|
86
110
|
name: string;
|
|
87
111
|
desc: string;
|
|
88
112
|
}[];
|
|
89
113
|
}
|
|
114
|
+
/** A single command block at root. Most fields come from the subtree's
|
|
115
|
+
* RootEntry; `subcommands`/`otherSubcommandCount` are computed by defineRoot
|
|
116
|
+
* from the subtree's children tiers. */
|
|
117
|
+
export interface RootCommand {
|
|
118
|
+
name: string;
|
|
119
|
+
concept: string;
|
|
120
|
+
desc: string;
|
|
121
|
+
useWhen: string;
|
|
122
|
+
dynamicState?: () => string | null;
|
|
123
|
+
/** Promoted subcommands surfaced inline under this command at root, in
|
|
124
|
+
* declaration order. `desc` is present only for 'important' tier; 'common'
|
|
125
|
+
* tier carries the bare qualified path. */
|
|
126
|
+
subcommands?: {
|
|
127
|
+
path: string;
|
|
128
|
+
desc?: string;
|
|
129
|
+
}[];
|
|
130
|
+
/** How many of this command's other (non-hidden, not-promoted) direct
|
|
131
|
+
* subcommands are not shown. Drives the "[+N (other) subcommands]" line. */
|
|
132
|
+
otherSubcommandCount?: number;
|
|
133
|
+
}
|
|
90
134
|
export interface BranchHelp {
|
|
91
135
|
name: string;
|
|
136
|
+
/** The command's own description — rendered as the `description` attribute of
|
|
137
|
+
* its <command> card at its own -h. */
|
|
92
138
|
summary: string;
|
|
93
|
-
/** Local
|
|
139
|
+
/** Local model prose orienting the agent to what the subtree contains and how
|
|
140
|
+
* the children differ as a group — never a per-child restatement (each
|
|
141
|
+
* child's purpose lives in its own listing row). */
|
|
94
142
|
model?: string;
|
|
95
143
|
/** Bounded runtime aggregate as a complete self-named state element (build
|
|
96
144
|
* it with stateBlock), e.g. `<skills count="42">…</skills>`. Renderer
|
|
97
145
|
* soft-fails to omission if this returns null or throws. */
|
|
98
146
|
dynamicState?: () => string | null;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
useWhen: string;
|
|
103
|
-
}[];
|
|
147
|
+
/** Parent-level listing assembled by defineBranch from the actual child defs.
|
|
148
|
+
* renderBranch reads this; never author it by hand. */
|
|
149
|
+
listing?: ListingChild[];
|
|
104
150
|
}
|
|
105
151
|
export interface LeafHelp {
|
|
106
152
|
name: string;
|
package/dist/core/help.js
CHANGED
|
@@ -50,10 +50,38 @@ const IO_CONTRACT = 'I/O contract: flags and positional args on input; stdout is
|
|
|
50
50
|
'Exit 0 on success, non-zero on failure. Schemas appear at leaf -h.';
|
|
51
51
|
// Behavioral instruction (not a schema) — engrained in the appended system
|
|
52
52
|
// prompt so the model treats unfamiliar capabilities as a cue to discover the
|
|
53
|
-
// contract, never to guess
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
'
|
|
53
|
+
// contract, never to guess, AND reads a command's contract before invoking it.
|
|
54
|
+
// Lives in the root guide, outside any leaf -h.
|
|
55
|
+
const CAPABILITY_DISCOVERY = 'Before running a crtr command whose exact contract (args, flags, effects) ' +
|
|
56
|
+
"you haven't verified this session, run `-h` on it and read the schema first " +
|
|
57
|
+
'— a reliable read beats a guess that wastes a turn or triggers an unintended ' +
|
|
58
|
+
"effect. Same when the user names a capability you don't fully recognize: " +
|
|
59
|
+
'`-h` it before acting.';
|
|
60
|
+
/** Lines for a command's subcommand affordance at root: any promoted
|
|
61
|
+
* (common/important) subcommands, then a remainder line naming how many other
|
|
62
|
+
* subcommands exist behind `crtr <name> -h`. Returns [] when the command has
|
|
63
|
+
* no listable subcommands at all. */
|
|
64
|
+
function rootSubcommandLines(c) {
|
|
65
|
+
const promoted = c.subcommands ?? [];
|
|
66
|
+
const other = c.otherSubcommandCount ?? 0;
|
|
67
|
+
if (promoted.length === 0 && other === 0)
|
|
68
|
+
return [];
|
|
69
|
+
const out = [];
|
|
70
|
+
if (promoted.length > 0) {
|
|
71
|
+
const labelW = maxLen(promoted.map((s) => s.path));
|
|
72
|
+
for (const s of promoted) {
|
|
73
|
+
// important → padded name + shortform desc; common → bare name.
|
|
74
|
+
out.push(s.desc !== undefined && s.desc !== ''
|
|
75
|
+
? ` ${pad(s.path, labelW)} ${s.desc}`
|
|
76
|
+
: ` ${s.path}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (other > 0) {
|
|
80
|
+
const word = promoted.length > 0 ? 'other subcommand' : 'subcommand';
|
|
81
|
+
out.push(` [+${other} ${word}${other === 1 ? '' : 's'} — \`crtr ${c.name} -h\`]`);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
57
85
|
export function renderRoot(h) {
|
|
58
86
|
const lines = [];
|
|
59
87
|
lines.push(`${h.tagline}`);
|
|
@@ -71,6 +99,11 @@ export function renderRoot(h) {
|
|
|
71
99
|
lines.push(`<command name="${c.name}">`);
|
|
72
100
|
lines.push(c.concept);
|
|
73
101
|
lines.push(`use when ${c.useWhen}`);
|
|
102
|
+
// The command's subcommand surface: promoted (common/important) children
|
|
103
|
+
// inline, plus a "[+N other subcommands]" pointer to its own -h. Sits
|
|
104
|
+
// between the selection rubric and any live state block.
|
|
105
|
+
for (const l of rootSubcommandLines(c))
|
|
106
|
+
lines.push(l);
|
|
74
107
|
// dynamicState returns a complete self-named element (e.g.
|
|
75
108
|
// <skills count="42">…</skills>) — emit it as-is, nested in the command.
|
|
76
109
|
const state = evalDynamic(c.dynamicState);
|
|
@@ -79,13 +112,18 @@ export function renderRoot(h) {
|
|
|
79
112
|
lines.push('</command>');
|
|
80
113
|
lines.push('');
|
|
81
114
|
}
|
|
82
|
-
// Globals block (footer)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
// Globals block (footer) — rendered only when globals exist, so an empty
|
|
116
|
+
// list never leaves a bare "Globals" header. -h itself is not a global: the
|
|
117
|
+
// capability-discovery rule below teaches -h usage with its reasoning, so no
|
|
118
|
+
// per-command CTA or standalone "-h: print help" stub is needed.
|
|
119
|
+
if (h.globals.length > 0) {
|
|
120
|
+
lines.push('Globals');
|
|
121
|
+
const gNameW = maxLen(h.globals.map((g) => g.name));
|
|
122
|
+
for (const g of h.globals) {
|
|
123
|
+
lines.push(` ${pad(g.name, gNameW)} ${g.desc}`);
|
|
124
|
+
}
|
|
125
|
+
lines.push('');
|
|
87
126
|
}
|
|
88
|
-
lines.push('');
|
|
89
127
|
lines.push(IO_CONTRACT);
|
|
90
128
|
lines.push('');
|
|
91
129
|
lines.push(CAPABILITY_DISCOVERY);
|
|
@@ -94,30 +132,37 @@ export function renderRoot(h) {
|
|
|
94
132
|
// ---------------------------------------------------------------------------
|
|
95
133
|
// renderBranch
|
|
96
134
|
// ---------------------------------------------------------------------------
|
|
135
|
+
/** Escape a value for a rendered XML attribute. Output is light XML around
|
|
136
|
+
* markdown read as prose by a model, not parsed — so we only guard the
|
|
137
|
+
* double-quote that would visually break the attribute, swapping it for a
|
|
138
|
+
* single quote rather than emitting noisy entities. */
|
|
139
|
+
function attr(s) {
|
|
140
|
+
return s.replace(/"/g, "'");
|
|
141
|
+
}
|
|
97
142
|
export function renderBranch(h) {
|
|
98
143
|
const lines = [];
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
144
|
+
// The branch renders as one <command> card: its own description in the
|
|
145
|
+
// opening attribute, then orientation prose / live state, then one
|
|
146
|
+
// self-closing <subcommand> per child. Each child's description + whenToUse
|
|
147
|
+
// are assembled by defineBranch from the child's own self-description, so the
|
|
148
|
+
// parent never restates what a child is — the child owns its representation.
|
|
149
|
+
lines.push(`<command name="${h.name}" description="${attr(h.summary)}">`);
|
|
104
150
|
const branchState = evalDynamic(h.dynamicState);
|
|
105
|
-
if (branchState !== null)
|
|
106
|
-
// dynamicState returns a complete self-named element — emit as-is.
|
|
107
|
-
lines.push('');
|
|
151
|
+
if (branchState !== null)
|
|
108
152
|
lines.push(branchState);
|
|
109
|
-
|
|
110
|
-
if (h.model !== undefined) {
|
|
111
|
-
lines.push('');
|
|
153
|
+
if (h.model !== undefined)
|
|
112
154
|
lines.push(h.model);
|
|
155
|
+
for (const c of h.listing ?? []) {
|
|
156
|
+
if (c.tier === 'hidden')
|
|
157
|
+
continue;
|
|
158
|
+
const subs = c.subCount !== undefined && c.subCount > 0 ? ` subcommands="${c.subCount}"` : '';
|
|
159
|
+
// whenToUse plainly states when to reach for this child, rendered verbatim —
|
|
160
|
+
// expansive with examples for judgment-heavy commands, concise for
|
|
161
|
+
// single-purpose ones. It does not restate "read my -h"; the
|
|
162
|
+
// capability-discovery rule in the root footer already teaches that.
|
|
163
|
+
lines.push(`<subcommand name="${c.name}" description="${attr(c.description)}" whenToUse="${attr(c.whenToUse)}"${subs}/>`);
|
|
113
164
|
}
|
|
114
|
-
lines.push('');
|
|
115
|
-
lines.push('Branches');
|
|
116
|
-
const nameW = maxLen(h.children.map((c) => c.name));
|
|
117
|
-
const descW = maxLen(h.children.map((c) => c.desc));
|
|
118
|
-
for (const c of h.children) {
|
|
119
|
-
lines.push(` ${pad(c.name, nameW)} ${pad(c.desc, descW)} | use when ${c.useWhen}`);
|
|
120
|
-
}
|
|
165
|
+
lines.push('</command>');
|
|
121
166
|
return lines.join('\n');
|
|
122
167
|
}
|
|
123
168
|
// ---------------------------------------------------------------------------
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - resolve (high-level composer)
|
|
7
7
|
* - ResolvedPersona (return type of resolve)
|
|
8
8
|
*/
|
|
9
|
-
export { loadPersona, loadKernel, availableKinds } from './loader.js';
|
|
9
|
+
export { loadPersona, loadKernel, availableKinds, loadLifecycleFragment, loadSpineFragment } from './loader.js';
|
|
10
10
|
export type { LoadedPersona } from './loader.js';
|
|
11
11
|
export { resolve } from './resolve.js';
|
|
12
12
|
export type { ResolvedPersona } from './resolve.js';
|
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
* - resolve (high-level composer)
|
|
7
7
|
* - ResolvedPersona (return type of resolve)
|
|
8
8
|
*/
|
|
9
|
-
export { loadPersona, loadKernel, availableKinds } from './loader.js';
|
|
9
|
+
export { loadPersona, loadKernel, availableKinds, loadLifecycleFragment, loadSpineFragment } from './loader.js';
|
|
10
10
|
export { resolve } from './resolve.js';
|
|
@@ -33,12 +33,51 @@ export declare function loadPersona(kind: string, mode: 'base' | 'orchestrator')
|
|
|
33
33
|
export declare function loadKernel(): string;
|
|
34
34
|
/**
|
|
35
35
|
* Load the base runtime prompt — the node operating protocol prepended to
|
|
36
|
-
* EVERY persona (
|
|
36
|
+
* EVERY persona (delegate/ask/promote). Returns '' if not found. The
|
|
37
|
+
* lifecycle/spine-specific sections (finish vs. dormant, report-up vs. silent)
|
|
38
|
+
* live in their own fragments, loaded below.
|
|
37
39
|
*/
|
|
38
40
|
export declare function loadRuntimeBase(): string;
|
|
41
|
+
/**
|
|
42
|
+
* Load the lifecycle fragment — the "how you end" contract, keyed on the node's
|
|
43
|
+
* lifecycle axis: `terminal` (drive to done + `push final`) or `resident`
|
|
44
|
+
* (dormant/wake, never forced to submit). Single source for both the baked-in
|
|
45
|
+
* system prompt (resolve) and the transition guidance (runtime/persona.ts).
|
|
46
|
+
* Returns '' if the fragment file cannot be found.
|
|
47
|
+
*/
|
|
48
|
+
export declare function loadLifecycleFragment(lifecycle: 'terminal' | 'resident'): string;
|
|
49
|
+
/**
|
|
50
|
+
* Load the spine fragment — the "who you report to" contract, keyed on whether
|
|
51
|
+
* the node has a manager (anyone it reports up to). `has-manager` teaches the
|
|
52
|
+
* `push update`/`push urgent`/escalate verbs; `no-manager` (a top-of-spine root)
|
|
53
|
+
* omits the push family entirely — it answers to the human directly.
|
|
54
|
+
* Returns '' if the fragment file cannot be found.
|
|
55
|
+
*/
|
|
56
|
+
export declare function loadSpineFragment(hasManager: boolean): string;
|
|
39
57
|
/**
|
|
40
58
|
* Enumerate the kinds with at least one persona file (base.md or
|
|
41
59
|
* orchestrator.md) across all scope roots (project/user/builtin). Used to
|
|
42
60
|
* validate a requested `--kind` and to list the valid choices.
|
|
43
61
|
*/
|
|
44
62
|
export declare function availableKinds(): string[];
|
|
63
|
+
export interface SubKind {
|
|
64
|
+
/** Full kind string to spawn, e.g. 'plan/reviewers/security'. */
|
|
65
|
+
kind: string;
|
|
66
|
+
/** Leaf name, e.g. 'security'. */
|
|
67
|
+
name: string;
|
|
68
|
+
/** One-line "what it reviews", from the sub-kind base.md `summary` frontmatter (or ''). */
|
|
69
|
+
summary: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Enumerate the reviewer sub-kinds owned by `parentKind` — the specialist
|
|
73
|
+
* personas at `<root>/<parentKind>/reviewers/<name>/base.md`, scanned across all
|
|
74
|
+
* scope roots (project > user > builtin; highest precedence wins per name).
|
|
75
|
+
*
|
|
76
|
+
* Sub-kinds are intentionally NOT global kinds: `availableKinds()` scans only the
|
|
77
|
+
* immediate children of each persona root, so `<parentKind>/reviewers/*` never
|
|
78
|
+
* leaks into the global list. A sub-kind is reachable only by its full kind
|
|
79
|
+
* string and is surfaced only in its parent kind's composed prompt (resolve.ts).
|
|
80
|
+
* Kind-parametric: any kind owns a roster simply by adding
|
|
81
|
+
* `<kind>/reviewers/<name>/base.md` — no code change.
|
|
82
|
+
*/
|
|
83
|
+
export declare function subKindsFor(parentKind: string): SubKind[];
|
|
@@ -124,7 +124,9 @@ export function loadKernel() {
|
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
126
|
* Load the base runtime prompt — the node operating protocol prepended to
|
|
127
|
-
* EVERY persona (
|
|
127
|
+
* EVERY persona (delegate/ask/promote). Returns '' if not found. The
|
|
128
|
+
* lifecycle/spine-specific sections (finish vs. dormant, report-up vs. silent)
|
|
129
|
+
* live in their own fragments, loaded below.
|
|
128
130
|
*/
|
|
129
131
|
export function loadRuntimeBase() {
|
|
130
132
|
const filePath = resolveFile('runtime-base.md');
|
|
@@ -134,6 +136,34 @@ export function loadRuntimeBase() {
|
|
|
134
136
|
const { body } = parseFrontmatterGeneric(src);
|
|
135
137
|
return body.trim();
|
|
136
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Load the lifecycle fragment — the "how you end" contract, keyed on the node's
|
|
141
|
+
* lifecycle axis: `terminal` (drive to done + `push final`) or `resident`
|
|
142
|
+
* (dormant/wake, never forced to submit). Single source for both the baked-in
|
|
143
|
+
* system prompt (resolve) and the transition guidance (runtime/persona.ts).
|
|
144
|
+
* Returns '' if the fragment file cannot be found.
|
|
145
|
+
*/
|
|
146
|
+
export function loadLifecycleFragment(lifecycle) {
|
|
147
|
+
const filePath = resolveFile(`lifecycle/${lifecycle}.md`);
|
|
148
|
+
if (!filePath)
|
|
149
|
+
return '';
|
|
150
|
+
const { body } = parseFrontmatterGeneric(readFileSync(filePath, 'utf8'));
|
|
151
|
+
return body.trim();
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Load the spine fragment — the "who you report to" contract, keyed on whether
|
|
155
|
+
* the node has a manager (anyone it reports up to). `has-manager` teaches the
|
|
156
|
+
* `push update`/`push urgent`/escalate verbs; `no-manager` (a top-of-spine root)
|
|
157
|
+
* omits the push family entirely — it answers to the human directly.
|
|
158
|
+
* Returns '' if the fragment file cannot be found.
|
|
159
|
+
*/
|
|
160
|
+
export function loadSpineFragment(hasManager) {
|
|
161
|
+
const filePath = resolveFile(`spine/${hasManager ? 'has-manager' : 'no-manager'}.md`);
|
|
162
|
+
if (!filePath)
|
|
163
|
+
return '';
|
|
164
|
+
const { body } = parseFrontmatterGeneric(readFileSync(filePath, 'utf8'));
|
|
165
|
+
return body.trim();
|
|
166
|
+
}
|
|
137
167
|
/**
|
|
138
168
|
* Enumerate the kinds with at least one persona file (base.md or
|
|
139
169
|
* orchestrator.md) across all scope roots (project/user/builtin). Used to
|
|
@@ -155,3 +185,35 @@ export function availableKinds() {
|
|
|
155
185
|
}
|
|
156
186
|
return [...kinds].sort();
|
|
157
187
|
}
|
|
188
|
+
const REVIEWERS_SUBDIR = 'reviewers';
|
|
189
|
+
/**
|
|
190
|
+
* Enumerate the reviewer sub-kinds owned by `parentKind` — the specialist
|
|
191
|
+
* personas at `<root>/<parentKind>/reviewers/<name>/base.md`, scanned across all
|
|
192
|
+
* scope roots (project > user > builtin; highest precedence wins per name).
|
|
193
|
+
*
|
|
194
|
+
* Sub-kinds are intentionally NOT global kinds: `availableKinds()` scans only the
|
|
195
|
+
* immediate children of each persona root, so `<parentKind>/reviewers/*` never
|
|
196
|
+
* leaks into the global list. A sub-kind is reachable only by its full kind
|
|
197
|
+
* string and is surfaced only in its parent kind's composed prompt (resolve.ts).
|
|
198
|
+
* Kind-parametric: any kind owns a roster simply by adding
|
|
199
|
+
* `<kind>/reviewers/<name>/base.md` — no code change.
|
|
200
|
+
*/
|
|
201
|
+
export function subKindsFor(parentKind) {
|
|
202
|
+
const byName = new Map();
|
|
203
|
+
for (const root of personaSearchRoots()) {
|
|
204
|
+
const dir = join(root, parentKind, REVIEWERS_SUBDIR);
|
|
205
|
+
if (!existsSync(dir))
|
|
206
|
+
continue;
|
|
207
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
208
|
+
if (!entry.isDirectory() || byName.has(entry.name))
|
|
209
|
+
continue; // higher root already won
|
|
210
|
+
const baseFile = join(dir, entry.name, 'base.md');
|
|
211
|
+
if (!existsSync(baseFile))
|
|
212
|
+
continue;
|
|
213
|
+
const { data } = parseFrontmatterGeneric(readFileSync(baseFile, 'utf8'));
|
|
214
|
+
const summary = data && typeof data['summary'] === 'string' ? data['summary'] : '';
|
|
215
|
+
byName.set(entry.name, { kind: `${parentKind}/${REVIEWERS_SUBDIR}/${entry.name}`, name: entry.name, summary });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
219
|
+
}
|