@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,103 @@
|
|
|
1
|
+
// canvas-commands.ts — pi extension registering canvas slash-commands on nodes.
|
|
2
|
+
//
|
|
3
|
+
// /promote [kind] — promote THIS node to an orchestrator. Runs
|
|
4
|
+
// `crtr node promote --json` for CRTR_NODE_ID (optionally specializing its
|
|
5
|
+
// kind), then triggers a turn. The orchestration guidance is injected
|
|
6
|
+
// CENTRALLY by the persona injector (canvas-stophook turn_end) at the turn
|
|
7
|
+
// boundary — the same path the node gets by running the command itself by
|
|
8
|
+
// hand — surfaced as a one-keystroke affordance.
|
|
9
|
+
//
|
|
10
|
+
// The Alt+C tmux action menu's "promote to orchestrator" item (key `o`) simply
|
|
11
|
+
// send-keys `/promote` into the active pane, so the menu and the slash command
|
|
12
|
+
// share this one implementation.
|
|
13
|
+
//
|
|
14
|
+
// INERT when CRTR_NODE_ID is absent (a plain pi session, not a canvas node).
|
|
15
|
+
//
|
|
16
|
+
// Plain TS-with-types — no imports from @earendil-works/* so this compiles
|
|
17
|
+
// inside crouter's own tsc build without a dep on the pi packages (mirrors
|
|
18
|
+
// canvas-nav.ts). The only crouter import is availableKinds, used to offer
|
|
19
|
+
// `/promote <kind>` completions.
|
|
20
|
+
import { execFile } from 'node:child_process';
|
|
21
|
+
import { promisify } from 'node:util';
|
|
22
|
+
import { availableKinds } from '../core/personas/index.js';
|
|
23
|
+
const pexec = promisify(execFile);
|
|
24
|
+
// Kinds for `/promote <kind>` completions — computed once (persona dirs rarely
|
|
25
|
+
// change within a session), best-effort so a loader hiccup never breaks input.
|
|
26
|
+
let cachedKinds = null;
|
|
27
|
+
function kinds() {
|
|
28
|
+
if (cachedKinds === null) {
|
|
29
|
+
try {
|
|
30
|
+
cachedKinds = availableKinds();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
cachedKinds = [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return cachedKinds;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Register the canvas slash-commands on `pi`.
|
|
40
|
+
*
|
|
41
|
+
* Returns immediately when CRTR_NODE_ID is absent — the extension is fully
|
|
42
|
+
* inert in a non-canvas pi session.
|
|
43
|
+
*/
|
|
44
|
+
export function registerCanvasCommands(pi) {
|
|
45
|
+
const nodeId = process.env['CRTR_NODE_ID'];
|
|
46
|
+
if (nodeId === undefined || nodeId.trim() === '')
|
|
47
|
+
return; // not a canvas node
|
|
48
|
+
pi.registerCommand('promote', {
|
|
49
|
+
description: 'Promote this node to an orchestrator — /promote, or /promote <kind> to specialize',
|
|
50
|
+
getArgumentCompletions: (prefix) => {
|
|
51
|
+
const items = kinds()
|
|
52
|
+
.filter((k) => k.startsWith(prefix))
|
|
53
|
+
.map((k) => ({ value: k, label: k }));
|
|
54
|
+
return items.length > 0 ? items : null;
|
|
55
|
+
},
|
|
56
|
+
handler: async (args, ctx) => {
|
|
57
|
+
const kind = args.trim().toLowerCase();
|
|
58
|
+
ctx.ui.setStatus('crtr-promote', kind ? `promoting → ${kind}…` : 'promoting…');
|
|
59
|
+
const argv = ['node', 'promote', '--json'];
|
|
60
|
+
if (kind !== '')
|
|
61
|
+
argv.push('--kind', kind);
|
|
62
|
+
// Run promote out-of-process. On a non-zero exit, crtr still prints the
|
|
63
|
+
// structured error to stdout, so prefer its `message` over the raw throw.
|
|
64
|
+
let result = null;
|
|
65
|
+
let errMsg = null;
|
|
66
|
+
try {
|
|
67
|
+
const { stdout } = await pexec('crtr', argv, { timeout: 15_000, maxBuffer: 4 * 1024 * 1024 });
|
|
68
|
+
result = JSON.parse(stdout);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const e = err;
|
|
72
|
+
const stdout = typeof e.stdout === 'string' ? e.stdout : '';
|
|
73
|
+
try {
|
|
74
|
+
const payload = JSON.parse(stdout);
|
|
75
|
+
errMsg = typeof payload.message === 'string' ? payload.message : null;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* stdout wasn't JSON */
|
|
79
|
+
}
|
|
80
|
+
if (errMsg === null)
|
|
81
|
+
errMsg = typeof e.message === 'string' ? e.message : String(err);
|
|
82
|
+
}
|
|
83
|
+
ctx.ui.setStatus('crtr-promote', '');
|
|
84
|
+
if (result === null) {
|
|
85
|
+
ctx.ui.notify(`promote failed: ${errMsg ?? 'unknown error'}`, 'error');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const rmPath = (result.roadmap_path ?? '').trim();
|
|
89
|
+
ctx.ui.notify(`Promoted to ${result.kind ?? 'orchestrator'} orchestrator — authoring roadmap${rmPath !== '' ? ` (${rmPath})` : ''}.`, 'info');
|
|
90
|
+
// The orchestration guidance is no longer returned by the command — the
|
|
91
|
+
// persona injector (canvas-stophook turn_end) is the single source and
|
|
92
|
+
// steers it in at the next turn boundary. Trigger a turn so the node wakes
|
|
93
|
+
// and the injector fires, exactly as when the node runs `crtr node
|
|
94
|
+
// promote` itself by hand.
|
|
95
|
+
pi.sendMessage({
|
|
96
|
+
customType: 'crtr-promote',
|
|
97
|
+
content: 'You have just been promoted to an orchestrator. Your new-role guidance is arriving — read it, author your roadmap, and start delegating.',
|
|
98
|
+
display: false,
|
|
99
|
+
}, { triggerTurn: true });
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export default registerCanvasCommands;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** The `customType` stamped on the injected session message. Used both to write
|
|
2
|
+
* the entry and to detect it on resume (the idempotency guard). */
|
|
3
|
+
export declare const CONTEXT_INTRO_CUSTOM_TYPE = "crtr-context";
|
|
4
|
+
interface SessionEntryLike {
|
|
5
|
+
type: string;
|
|
6
|
+
customType?: string;
|
|
7
|
+
}
|
|
8
|
+
interface SessionStartCtxLike {
|
|
9
|
+
sessionManager: {
|
|
10
|
+
getEntries: () => SessionEntryLike[];
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
interface CustomMessageLike {
|
|
14
|
+
customType: string;
|
|
15
|
+
content: string;
|
|
16
|
+
display?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** The message handed to a message renderer. `content` is normally the string we
|
|
19
|
+
* sent, but pi types it as string-or-blocks, so we handle both. */
|
|
20
|
+
interface RenderedMessageLike {
|
|
21
|
+
customType: string;
|
|
22
|
+
content: string | Array<{
|
|
23
|
+
type: string;
|
|
24
|
+
text?: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
/** Minimal structural match for pi-tui's `Component` (render + invalidate). A
|
|
28
|
+
* plain object of this shape is a valid child for pi's Container. */
|
|
29
|
+
interface ComponentLike {
|
|
30
|
+
render: (width: number) => string[];
|
|
31
|
+
invalidate: () => void;
|
|
32
|
+
}
|
|
33
|
+
/** Subset of pi's `Theme` we touch — `fg(color, text)` wraps text in ANSI. Used
|
|
34
|
+
* defensively (falls back to plain text if absent). */
|
|
35
|
+
interface ThemeLike {
|
|
36
|
+
fg?: (color: string, text: string) => string;
|
|
37
|
+
}
|
|
38
|
+
interface PiLike {
|
|
39
|
+
on: (event: 'session_start', handler: (event: unknown, ctx: SessionStartCtxLike) => void | Promise<void>) => void;
|
|
40
|
+
sendMessage: (message: CustomMessageLike, options?: {
|
|
41
|
+
deliverAs?: string;
|
|
42
|
+
triggerTurn?: boolean;
|
|
43
|
+
}) => void;
|
|
44
|
+
registerMessageRenderer: (customType: string, renderer: (message: RenderedMessageLike, options: {
|
|
45
|
+
expanded?: boolean;
|
|
46
|
+
}, theme: ThemeLike) => ComponentLike | undefined) => void;
|
|
47
|
+
}
|
|
48
|
+
/** Build the <crtr-context> bearings block for `nodeId`. Thin wrapper over the
|
|
49
|
+
* shared builder in core/runtime/bearings.ts (the single source of truth, also
|
|
50
|
+
* used by the promotion guidance dump). Exported for testing. */
|
|
51
|
+
export declare function buildContextIntro(nodeId: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Renderer for `crtr-context` messages. Collapsed (default) shows a one-line
|
|
54
|
+
* stub; expanded (Ctrl+O) shows the label + full body. Returns a plain object
|
|
55
|
+
* matching pi's structural `Component` interface — no pi-tui import. Exported for
|
|
56
|
+
* testing.
|
|
57
|
+
*/
|
|
58
|
+
export declare function renderContextMessage(message: RenderedMessageLike, options: {
|
|
59
|
+
expanded?: boolean;
|
|
60
|
+
}, theme: ThemeLike): ComponentLike;
|
|
61
|
+
/**
|
|
62
|
+
* Register the context-intro preamble on `pi`.
|
|
63
|
+
*
|
|
64
|
+
* Returns immediately (inert) when CRTR_NODE_ID is absent. On `session_start`
|
|
65
|
+
* it injects the <crtr-context> block as the first message of a brand-new chat
|
|
66
|
+
* — but only when the session does not already carry it, so a `--session <id>`
|
|
67
|
+
* relaunch (which restores the conversation) never duplicates the block.
|
|
68
|
+
*/
|
|
69
|
+
export declare function registerCanvasContextIntro(pi: PiLike): void;
|
|
70
|
+
export default registerCanvasContextIntro;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// canvas-context-intro.ts — pi extension for pi-native canvas agent nodes.
|
|
2
|
+
//
|
|
3
|
+
// Loaded into every canvas node's pi process via the node's launch.extensions
|
|
4
|
+
// list. INERT when CRTR_NODE_ID is absent (plain pi session or legacy job agent).
|
|
5
|
+
//
|
|
6
|
+
// The bearings preamble. On `session_start` — which fires BEFORE the node's
|
|
7
|
+
// first user message enters the session — this injects ONE <crtr-context>
|
|
8
|
+
// message via `pi.sendMessage` (no delivery options, so at the idle start it is
|
|
9
|
+
// pushed straight onto the message list and persisted). Because the session is
|
|
10
|
+
// still empty at that point, the bearings land as the FIRST entry, ahead of the
|
|
11
|
+
// node's first prompt — the orienting frame, not a trailing afterthought.
|
|
12
|
+
// (before_agent_start / deliverAs:"nextTurn" both append AFTER the user
|
|
13
|
+
// message — see agent-session's submit path — which is why we use
|
|
14
|
+
// session_start instead.)
|
|
15
|
+
//
|
|
16
|
+
// The block carries: the path to the node's own context dir and the framing for
|
|
17
|
+
// what belongs there (a shared document store for the other nodes). Resident
|
|
18
|
+
// orchestrators additionally get the across-refresh-cycles framing + a <memory>
|
|
19
|
+
// block merging the indexes of their three scoped memory stores (user-global,
|
|
20
|
+
// project, node-local), each labeled with its absolute dir + index path. The
|
|
21
|
+
// prose lives in core/runtime/bearings.ts (shared with the promotion guidance
|
|
22
|
+
// dump), which gates the memory block on the node having a node-local store — so
|
|
23
|
+
// a terminal worker gets no memory framing at all.
|
|
24
|
+
//
|
|
25
|
+
// IDEMPOTENT across resumes: a `--session` relaunch restores the conversation,
|
|
26
|
+
// so the block is already in history; the session_start handler sees it via
|
|
27
|
+
// `sessionManager.getEntries()` and skips, so it never accumulates.
|
|
28
|
+
//
|
|
29
|
+
// COLLAPSED BY DEFAULT: a `registerMessageRenderer` keyed to our customType
|
|
30
|
+
// renders the block as a single one-line stub; the full body only appears when
|
|
31
|
+
// the user expands tool output (Ctrl+O / `app.tools.expand`). pi drives this via
|
|
32
|
+
// `CustomMessageComponent.setExpanded(toolOutputExpanded)`, so the same toggle
|
|
33
|
+
// that expands tool results expands the bearings. The renderer returns a plain
|
|
34
|
+
// object satisfying pi's structural `Component` interface ({ render, invalidate })
|
|
35
|
+
// — no pi-tui class needed. The LLM always sees the full `content` regardless of
|
|
36
|
+
// how it renders; the renderer is display-only.
|
|
37
|
+
//
|
|
38
|
+
// Plain TS-with-types — no imports from @earendil-works/* so this compiles inside
|
|
39
|
+
// crouter's own tsc build without a dep on the pi packages.
|
|
40
|
+
import { buildContextBearings } from '../core/runtime/bearings.js';
|
|
41
|
+
/** The `customType` stamped on the injected session message. Used both to write
|
|
42
|
+
* the entry and to detect it on resume (the idempotency guard). */
|
|
43
|
+
export const CONTEXT_INTRO_CUSTOM_TYPE = 'crtr-context';
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Block builder
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
/** Build the <crtr-context> bearings block for `nodeId`. Thin wrapper over the
|
|
48
|
+
* shared builder in core/runtime/bearings.ts (the single source of truth, also
|
|
49
|
+
* used by the promotion guidance dump). Exported for testing. */
|
|
50
|
+
export function buildContextIntro(nodeId) {
|
|
51
|
+
return buildContextBearings(nodeId);
|
|
52
|
+
}
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Collapsed-by-default rendering
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
/** Pull the plain text out of a custom message's content (string or blocks). */
|
|
57
|
+
function messageText(message) {
|
|
58
|
+
if (typeof message.content === 'string')
|
|
59
|
+
return message.content;
|
|
60
|
+
return message.content
|
|
61
|
+
.filter((c) => c.type === 'text' && typeof c.text === 'string')
|
|
62
|
+
.map((c) => c.text)
|
|
63
|
+
.join('\n');
|
|
64
|
+
}
|
|
65
|
+
/** Hard-wrap a single logical line to `width` columns (content carries no ANSI).
|
|
66
|
+
* Code-point aware so wide-string slicing never splits a surrogate pair; the
|
|
67
|
+
* bearings prose is plain text, so code-point count == visible columns. */
|
|
68
|
+
function wrapLine(line, width) {
|
|
69
|
+
if (width <= 0)
|
|
70
|
+
return [''];
|
|
71
|
+
const chars = Array.from(line);
|
|
72
|
+
if (chars.length <= width)
|
|
73
|
+
return [line];
|
|
74
|
+
const out = [];
|
|
75
|
+
for (let i = 0; i < chars.length; i += width)
|
|
76
|
+
out.push(chars.slice(i, i + width).join(''));
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
/** Truncate plain text to at most `width` columns, appending an ellipsis when it
|
|
80
|
+
* would overflow. Content here is ANSI-free plain text (label + prose), so a
|
|
81
|
+
* code-point count stands in for visible width. The renderer MUST keep every
|
|
82
|
+
* emitted line within the terminal width or pi's TUI aborts the whole render. */
|
|
83
|
+
function truncateToWidth(text, width) {
|
|
84
|
+
if (width <= 0)
|
|
85
|
+
return '';
|
|
86
|
+
const chars = Array.from(text);
|
|
87
|
+
if (chars.length <= width)
|
|
88
|
+
return text;
|
|
89
|
+
if (width === 1)
|
|
90
|
+
return '…';
|
|
91
|
+
return chars.slice(0, width - 1).join('') + '…';
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Renderer for `crtr-context` messages. Collapsed (default) shows a one-line
|
|
95
|
+
* stub; expanded (Ctrl+O) shows the label + full body. Returns a plain object
|
|
96
|
+
* matching pi's structural `Component` interface — no pi-tui import. Exported for
|
|
97
|
+
* testing.
|
|
98
|
+
*/
|
|
99
|
+
export function renderContextMessage(message, options, theme) {
|
|
100
|
+
const expanded = options?.expanded === true;
|
|
101
|
+
const paint = (color, text) => typeof theme?.fg === 'function' ? theme.fg(color, text) : text;
|
|
102
|
+
return {
|
|
103
|
+
render(width) {
|
|
104
|
+
const w = typeof width === 'number' && width > 0 ? width : 80;
|
|
105
|
+
if (!expanded) {
|
|
106
|
+
// Truncate BEFORE painting so the ANSI wrapper never inflates the
|
|
107
|
+
// measured width; an over-wide line aborts pi's entire TUI render.
|
|
108
|
+
const stub = `[${CONTEXT_INTRO_CUSTOM_TYPE}] orienting bearings — ctrl+o to expand`;
|
|
109
|
+
return [paint('dim', truncateToWidth(stub, w))];
|
|
110
|
+
}
|
|
111
|
+
const lines = [paint('customMessageLabel', truncateToWidth(`[${CONTEXT_INTRO_CUSTOM_TYPE}]`, w)), ''];
|
|
112
|
+
for (const raw of messageText(message).split('\n')) {
|
|
113
|
+
for (const wrapped of wrapLine(raw, w))
|
|
114
|
+
lines.push(paint('customMessageText', wrapped));
|
|
115
|
+
}
|
|
116
|
+
return lines;
|
|
117
|
+
},
|
|
118
|
+
invalidate() {
|
|
119
|
+
/* stateless — nothing to clear */
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Extension
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
/**
|
|
127
|
+
* Register the context-intro preamble on `pi`.
|
|
128
|
+
*
|
|
129
|
+
* Returns immediately (inert) when CRTR_NODE_ID is absent. On `session_start`
|
|
130
|
+
* it injects the <crtr-context> block as the first message of a brand-new chat
|
|
131
|
+
* — but only when the session does not already carry it, so a `--session <id>`
|
|
132
|
+
* relaunch (which restores the conversation) never duplicates the block.
|
|
133
|
+
*/
|
|
134
|
+
export function registerCanvasContextIntro(pi) {
|
|
135
|
+
// Collapse the block to a one-liner until the user expands tool output (Ctrl+O).
|
|
136
|
+
// Harmless to register outside TUI mode (it's only consulted while rendering).
|
|
137
|
+
pi.registerMessageRenderer(CONTEXT_INTRO_CUSTOM_TYPE, renderContextMessage);
|
|
138
|
+
pi.on('session_start', (_event, ctx) => {
|
|
139
|
+
try {
|
|
140
|
+
const nodeId = process.env['CRTR_NODE_ID'];
|
|
141
|
+
if (nodeId === undefined || nodeId.trim() === '')
|
|
142
|
+
return; // not a canvas node
|
|
143
|
+
// Idempotent: a restored/reloaded session already carries the block.
|
|
144
|
+
const present = ctx.sessionManager
|
|
145
|
+
.getEntries()
|
|
146
|
+
.some((e) => e.type === 'custom_message' && e.customType === CONTEXT_INTRO_CUSTOM_TYPE);
|
|
147
|
+
if (present)
|
|
148
|
+
return;
|
|
149
|
+
// No delivery options: at the idle start of a session this is pushed onto
|
|
150
|
+
// the (still empty) message list and persisted immediately, so it precedes
|
|
151
|
+
// the node's first prompt.
|
|
152
|
+
pi.sendMessage({
|
|
153
|
+
customType: CONTEXT_INTRO_CUSTOM_TYPE,
|
|
154
|
+
content: buildContextIntro(nodeId),
|
|
155
|
+
display: true,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Best-effort: a failure here must never break session startup.
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
export default registerCanvasContextIntro;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface InputEventLike {
|
|
2
|
+
type: 'input';
|
|
3
|
+
text: string;
|
|
4
|
+
images?: unknown[];
|
|
5
|
+
source: 'interactive' | 'rpc' | 'extension';
|
|
6
|
+
}
|
|
7
|
+
interface PiLike {
|
|
8
|
+
on: (event: 'input', handler: (event: InputEventLike, ctx: any) => void) => void;
|
|
9
|
+
/** Update the live session display name (pi's editor label). Present in
|
|
10
|
+
* interactive mode; optional so the extension stays inert where it's not. */
|
|
11
|
+
setSessionName?: (name: string) => void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Register the goal-capture handler on `pi`.
|
|
15
|
+
*
|
|
16
|
+
* Returns immediately (inert) when CRTR_NODE_ID is absent. The `input` handler
|
|
17
|
+
* is the whole extension: on the first interactive message of a goal-less node,
|
|
18
|
+
* persist it as the goal.
|
|
19
|
+
*/
|
|
20
|
+
export declare function registerCanvasGoalCapture(pi: PiLike): void;
|
|
21
|
+
export default registerCanvasGoalCapture;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// canvas-goal-capture.ts — pi extension for pi-native canvas agent nodes.
|
|
2
|
+
//
|
|
3
|
+
// Loaded into every canvas node's pi process via the node's launch.extensions
|
|
4
|
+
// list. INERT when CRTR_NODE_ID is absent (plain pi session or legacy job agent).
|
|
5
|
+
//
|
|
6
|
+
// A node spawned with a prompt has its goal persisted at birth (writeGoal in
|
|
7
|
+
// spawn.ts). A bare root (`crtr` with no prompt) starts goal-less — its mandate
|
|
8
|
+
// only arrives when the human types their first message. This extension closes
|
|
9
|
+
// that gap: on the FIRST interactive user message, if the node has no goal yet,
|
|
10
|
+
// it persists that message as context/initial-prompt.md. Subsequent messages
|
|
11
|
+
// never clobber it (captureGoalIfAbsent is guarded), and a fresh-revive kickoff
|
|
12
|
+
// prompt is skipped via its sentinel so it can never be mistaken for a mandate.
|
|
13
|
+
//
|
|
14
|
+
// Pure observation — it writes the goal file as a side effect and always lets
|
|
15
|
+
// the message through unchanged (returns nothing ⇒ continue). Registered before
|
|
16
|
+
// canvas-passive-context so it reads the raw user text, not a backlog-prepended
|
|
17
|
+
// transform.
|
|
18
|
+
//
|
|
19
|
+
// Plain TS-with-types — no imports from @earendil-works/* so this compiles inside
|
|
20
|
+
// crouter's own tsc build without a dep on the pi packages.
|
|
21
|
+
import { captureGoalIfAbsent, REVIVE_KICKOFF_SENTINEL } from '../core/runtime/kickoff.js';
|
|
22
|
+
import { generateAndPersistName } from '../core/runtime/naming.js';
|
|
23
|
+
import { editorLabel } from '../core/canvas/index.js';
|
|
24
|
+
/**
|
|
25
|
+
* Register the goal-capture handler on `pi`.
|
|
26
|
+
*
|
|
27
|
+
* Returns immediately (inert) when CRTR_NODE_ID is absent. The `input` handler
|
|
28
|
+
* is the whole extension: on the first interactive message of a goal-less node,
|
|
29
|
+
* persist it as the goal.
|
|
30
|
+
*/
|
|
31
|
+
export function registerCanvasGoalCapture(pi) {
|
|
32
|
+
pi.on('input', (event) => {
|
|
33
|
+
try {
|
|
34
|
+
const nodeId = process.env['CRTR_NODE_ID'];
|
|
35
|
+
if (nodeId === undefined || nodeId.trim() === '')
|
|
36
|
+
return; // not a canvas node
|
|
37
|
+
// Only a genuine human-typed prompt seeds the mandate — never an RPC or an
|
|
38
|
+
// extension-injected message (inbox wakes, steering nudges, kickoffs).
|
|
39
|
+
if (event.source !== 'interactive')
|
|
40
|
+
return;
|
|
41
|
+
const text = (event.text ?? '').trim();
|
|
42
|
+
if (text === '')
|
|
43
|
+
return;
|
|
44
|
+
// A fresh-revive kickoff is delivered as the launch prompt; never let it
|
|
45
|
+
// masquerade as the user's first mandate.
|
|
46
|
+
if (text.startsWith(REVIVE_KICKOFF_SENTINEL))
|
|
47
|
+
return;
|
|
48
|
+
// First mandate for a bare root: persist it as the goal, and ask pi
|
|
49
|
+
// (async, non-blocking) to name the session from it. The name lands on
|
|
50
|
+
// meta.description; the onNamed callback pushes the new editor label into
|
|
51
|
+
// THIS live session via setSessionName, so it updates immediately instead
|
|
52
|
+
// of only on the next cycle.
|
|
53
|
+
if (captureGoalIfAbsent(nodeId, text)) {
|
|
54
|
+
generateAndPersistName(nodeId, text, (meta) => {
|
|
55
|
+
try {
|
|
56
|
+
pi.setSessionName?.(editorLabel(meta));
|
|
57
|
+
}
|
|
58
|
+
catch { /* best-effort */ }
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Best-effort: a capture failure must never drop or alter the message.
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export default registerCanvasGoalCapture;
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
// Plain TS-with-types — no imports from @earendil-works/* so this compiles inside
|
|
33
33
|
// crouter's own tsc build without a dep on the pi packages.
|
|
34
34
|
import { readInboxSince, readCursor, writeCursor, coalesce, } from '../core/feed/inbox.js';
|
|
35
|
+
import { getNode } from '../core/canvas/index.js';
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
36
37
|
// Module-level timer — prevents stacking on /reload (the double-notify bug).
|
|
37
38
|
//
|
|
@@ -179,6 +180,16 @@ export function registerCanvasInboxWatcher(pi) {
|
|
|
179
180
|
seeded = true;
|
|
180
181
|
}
|
|
181
182
|
const newEntries = readInboxSince(nodeId, cursor);
|
|
183
|
+
// Refresh-yield in flight: the node ran `crtr node yield` and is about to be
|
|
184
|
+
// torn down and revived fresh. Hold everything — don't consume the cursor
|
|
185
|
+
// (advancing it past these entries would drop them on tear-down) and don't
|
|
186
|
+
// deliver (steering a child's `final` into the yielding turn hijacks the
|
|
187
|
+
// clean stop the refresh path depends on, which is how a yield got derailed
|
|
188
|
+
// mid-flight). The fresh pi re-reads the feed on boot. getNode only when
|
|
189
|
+
// there's actual work pending, so idle ticks stay cheap.
|
|
190
|
+
if ((newEntries.length > 0 || buffer.length > 0) && getNode(nodeId)?.intent === 'refresh') {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
182
193
|
if (newEntries.length > 0) {
|
|
183
194
|
// Advance and persist the cursor BEFORE buffering, so a crash after this
|
|
184
195
|
// point loses at most one coalesced message rather than re-injecting
|
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
type PiEvents = 'session_start' | 'turn_end' | 'session_shutdown';
|
|
2
2
|
interface ExtensionWidgetOptions {
|
|
3
|
-
/** Where the widget is rendered. "aboveEditor" | "belowEditor" */
|
|
4
3
|
placement?: 'aboveEditor' | 'belowEditor';
|
|
5
4
|
}
|
|
6
5
|
interface UIContext {
|
|
7
6
|
setWidget(key: string, content: string[] | undefined, options?: ExtensionWidgetOptions): void;
|
|
8
7
|
/** Raw key tap that fires BEFORE the editor. Return {consume:true} to swallow
|
|
9
|
-
* the key
|
|
8
|
+
* the key. Returns an unsub. */
|
|
10
9
|
onTerminalInput?(handler: (data: string) => {
|
|
11
10
|
consume?: boolean;
|
|
12
11
|
data?: string;
|
|
13
12
|
} | undefined): () => void;
|
|
14
|
-
/** Current editor buffer text — used to only hijack keys on an empty editor. */
|
|
15
13
|
getEditorText?(): string;
|
|
16
|
-
/** Transient toast, used to report a failed focus. */
|
|
17
14
|
notify?(message: string, type?: 'info' | 'warning' | 'error'): void;
|
|
18
15
|
}
|
|
19
16
|
interface ExtensionCtx {
|
|
20
17
|
ui: UIContext;
|
|
21
18
|
}
|
|
19
|
+
interface CommandCtx {
|
|
20
|
+
ui: UIContext;
|
|
21
|
+
}
|
|
22
22
|
interface PiLike {
|
|
23
23
|
on(event: PiEvents, handler: (event: any, ctx: ExtensionCtx) => void | Promise<void>): void;
|
|
24
|
+
registerCommand?(name: string, options: {
|
|
25
|
+
description?: string;
|
|
26
|
+
handler: (args: string, ctx: CommandCtx) => void | Promise<void>;
|
|
27
|
+
}): void;
|
|
28
|
+
registerShortcut?(shortcut: string, options: {
|
|
29
|
+
description?: string;
|
|
30
|
+
handler: (ctx: CommandCtx) => void | Promise<void>;
|
|
31
|
+
}): void;
|
|
24
32
|
}
|
|
25
33
|
/**
|
|
26
34
|
* Register the canvas nav chrome on `pi`.
|