@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.
Files changed (233) hide show
  1. package/dist/build-root.d.ts +8 -0
  2. package/dist/build-root.js +30 -0
  3. package/dist/builtin-personas/design/base.md +3 -7
  4. package/dist/builtin-personas/design/orchestrator.md +4 -3
  5. package/dist/builtin-personas/developer/base.md +3 -7
  6. package/dist/builtin-personas/developer/orchestrator.md +5 -4
  7. package/dist/builtin-personas/explore/base.md +3 -7
  8. package/dist/builtin-personas/explore/orchestrator.md +1 -5
  9. package/dist/builtin-personas/general/base.md +2 -4
  10. package/dist/builtin-personas/general/orchestrator.md +2 -4
  11. package/dist/builtin-personas/lifecycle/resident.md +2 -0
  12. package/dist/builtin-personas/lifecycle/terminal.md +6 -0
  13. package/dist/builtin-personas/orchestration-kernel.md +42 -3
  14. package/dist/builtin-personas/plan/base.md +3 -5
  15. package/dist/builtin-personas/plan/orchestrator.md +5 -4
  16. package/dist/builtin-personas/plan/reviewers/architecture-fit/base.md +9 -0
  17. package/dist/builtin-personas/plan/reviewers/code-smells/base.md +9 -0
  18. package/dist/builtin-personas/plan/reviewers/pattern-consistency/base.md +9 -0
  19. package/dist/builtin-personas/plan/reviewers/requirements-coverage/base.md +9 -0
  20. package/dist/builtin-personas/plan/reviewers/security/base.md +9 -0
  21. package/dist/builtin-personas/review/base.md +3 -5
  22. package/dist/builtin-personas/review/orchestrator.md +2 -6
  23. package/dist/builtin-personas/runtime-base.md +3 -19
  24. package/dist/builtin-personas/spec/base.md +3 -5
  25. package/dist/builtin-personas/spec/orchestrator.md +4 -3
  26. package/dist/builtin-personas/spine/has-manager.md +10 -0
  27. package/dist/builtin-personas/spine/no-manager.md +2 -0
  28. package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +96 -0
  29. package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +49 -0
  30. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +49 -0
  31. package/dist/builtin-skills/skills/planning/SKILL.md +1 -1
  32. package/dist/builtin-skills/skills/spec/SKILL.md +2 -2
  33. package/dist/cli.js +6 -29
  34. package/dist/commands/__tests__/human.test.js +73 -2
  35. package/dist/commands/attention.js +76 -7
  36. package/dist/commands/canvas-prune.d.ts +2 -0
  37. package/dist/commands/canvas-prune.js +66 -0
  38. package/dist/commands/canvas.js +5 -8
  39. package/dist/commands/chord.d.ts +2 -0
  40. package/dist/commands/chord.js +143 -0
  41. package/dist/commands/daemon.js +8 -5
  42. package/dist/commands/dashboard.js +2 -0
  43. package/dist/commands/human/prompts.js +28 -27
  44. package/dist/commands/human/queue.d.ts +1 -0
  45. package/dist/commands/human/queue.js +105 -2
  46. package/dist/commands/human/shared.d.ts +28 -18
  47. package/dist/commands/human/shared.js +53 -60
  48. package/dist/commands/human.js +6 -14
  49. package/dist/commands/node.d.ts +11 -0
  50. package/dist/commands/node.js +381 -87
  51. package/dist/commands/pkg/market-inspect.js +6 -4
  52. package/dist/commands/pkg/market-manage.js +10 -6
  53. package/dist/commands/pkg/market.js +2 -4
  54. package/dist/commands/pkg/plugin-inspect.js +6 -4
  55. package/dist/commands/pkg/plugin-manage.js +12 -7
  56. package/dist/commands/pkg/plugin.js +2 -4
  57. package/dist/commands/pkg.js +0 -4
  58. package/dist/commands/push.js +178 -15
  59. package/dist/commands/revive.js +5 -3
  60. package/dist/commands/skill/author.js +6 -4
  61. package/dist/commands/skill/find.js +8 -5
  62. package/dist/commands/skill/read.js +2 -0
  63. package/dist/commands/skill/state.js +6 -4
  64. package/dist/commands/skill.js +0 -6
  65. package/dist/commands/sys/config.js +21 -7
  66. package/dist/commands/sys/doctor.js +2 -0
  67. package/dist/commands/sys/update.js +4 -0
  68. package/dist/commands/sys.js +0 -6
  69. package/dist/commands/tmux-spread.d.ts +2 -0
  70. package/dist/commands/tmux-spread.js +130 -0
  71. package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
  72. package/dist/core/__tests__/child-followup.test.d.ts +1 -0
  73. package/dist/core/__tests__/child-followup.test.js +83 -0
  74. package/dist/core/__tests__/close.test.d.ts +1 -0
  75. package/dist/core/__tests__/close.test.js +148 -0
  76. package/dist/core/__tests__/context-intro.test.d.ts +1 -0
  77. package/dist/core/__tests__/context-intro.test.js +196 -0
  78. package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
  79. package/dist/core/__tests__/daemon-boot.test.js +93 -0
  80. package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
  81. package/dist/core/__tests__/daemon-liveness.test.js +223 -0
  82. package/dist/core/__tests__/focuses.test.d.ts +1 -0
  83. package/dist/core/__tests__/focuses.test.js +259 -0
  84. package/dist/core/__tests__/fork.test.d.ts +1 -0
  85. package/dist/core/__tests__/fork.test.js +91 -0
  86. package/dist/core/__tests__/home-session.test.d.ts +1 -0
  87. package/dist/core/__tests__/home-session.test.js +153 -0
  88. package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
  89. package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
  90. package/dist/core/__tests__/keystone.test.d.ts +1 -0
  91. package/dist/core/__tests__/keystone.test.js +185 -0
  92. package/dist/core/__tests__/kickoff.test.d.ts +1 -0
  93. package/dist/core/__tests__/kickoff.test.js +89 -0
  94. package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
  95. package/dist/core/__tests__/lifecycle.test.js +178 -0
  96. package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
  97. package/dist/core/__tests__/listing-completeness.test.js +31 -0
  98. package/dist/core/__tests__/memory.test.d.ts +1 -0
  99. package/dist/core/__tests__/memory.test.js +152 -0
  100. package/dist/core/__tests__/migration.test.d.ts +1 -0
  101. package/dist/core/__tests__/migration.test.js +238 -0
  102. package/dist/core/__tests__/pane-column.test.d.ts +1 -0
  103. package/dist/core/__tests__/pane-column.test.js +153 -0
  104. package/dist/core/__tests__/passive-subscription.test.d.ts +1 -0
  105. package/dist/core/__tests__/passive-subscription.test.js +164 -0
  106. package/dist/core/__tests__/persona-compose.test.d.ts +1 -0
  107. package/dist/core/__tests__/persona-compose.test.js +53 -0
  108. package/dist/core/__tests__/persona-subkind.test.d.ts +1 -0
  109. package/dist/core/__tests__/persona-subkind.test.js +62 -0
  110. package/dist/core/__tests__/persona.test.d.ts +1 -0
  111. package/dist/core/__tests__/persona.test.js +107 -0
  112. package/dist/core/__tests__/placement-focus.test.d.ts +1 -0
  113. package/dist/core/__tests__/placement-focus.test.js +244 -0
  114. package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
  115. package/dist/core/__tests__/placement-reconcile.test.js +212 -0
  116. package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
  117. package/dist/core/__tests__/placement-revive.test.js +238 -0
  118. package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
  119. package/dist/core/__tests__/placement-teardown.test.js +183 -0
  120. package/dist/core/__tests__/prune.test.d.ts +1 -0
  121. package/dist/core/__tests__/prune.test.js +116 -0
  122. package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
  123. package/dist/core/__tests__/push-final-guard.test.js +71 -0
  124. package/dist/core/__tests__/relaunch.test.d.ts +1 -0
  125. package/dist/core/__tests__/relaunch.test.js +328 -0
  126. package/dist/core/__tests__/reset.test.js +26 -7
  127. package/dist/core/__tests__/revive.test.d.ts +1 -0
  128. package/dist/core/__tests__/revive.test.js +217 -0
  129. package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
  130. package/dist/core/__tests__/spawn-root.test.js +73 -0
  131. package/dist/core/__tests__/steer-note.test.d.ts +1 -0
  132. package/dist/core/__tests__/steer-note.test.js +39 -0
  133. package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
  134. package/dist/core/__tests__/stop-guard.test.js +82 -0
  135. package/dist/core/__tests__/subcommand-tier.test.d.ts +1 -0
  136. package/dist/core/__tests__/subcommand-tier.test.js +99 -0
  137. package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
  138. package/dist/core/__tests__/tmux-surface.test.js +106 -0
  139. package/dist/core/__tests__/unknown-path.test.js +8 -2
  140. package/dist/core/canvas/attention.d.ts +10 -0
  141. package/dist/core/canvas/attention.js +40 -0
  142. package/dist/core/canvas/canvas.d.ts +66 -7
  143. package/dist/core/canvas/canvas.js +209 -21
  144. package/dist/core/canvas/db.d.ts +8 -0
  145. package/dist/core/canvas/db.js +206 -4
  146. package/dist/core/canvas/focuses.d.ts +22 -0
  147. package/dist/core/canvas/focuses.js +80 -0
  148. package/dist/core/canvas/index.d.ts +3 -0
  149. package/dist/core/canvas/index.js +3 -0
  150. package/dist/core/canvas/labels.d.ts +27 -0
  151. package/dist/core/canvas/labels.js +36 -0
  152. package/dist/core/canvas/paths.d.ts +4 -0
  153. package/dist/core/canvas/paths.js +6 -0
  154. package/dist/core/canvas/render.js +25 -10
  155. package/dist/core/canvas/telemetry.d.ts +14 -0
  156. package/dist/core/canvas/telemetry.js +35 -0
  157. package/dist/core/canvas/types.d.ts +115 -12
  158. package/dist/core/command.d.ts +25 -1
  159. package/dist/core/command.js +48 -7
  160. package/dist/core/config.js +36 -2
  161. package/dist/core/feed/feed.js +14 -12
  162. package/dist/core/feed/inbox.d.ts +3 -1
  163. package/dist/core/feed/inbox.js +45 -5
  164. package/dist/core/feed/passive.d.ts +17 -0
  165. package/dist/core/feed/passive.js +92 -0
  166. package/dist/core/help.d.ts +59 -13
  167. package/dist/core/help.js +73 -28
  168. package/dist/core/personas/index.d.ts +1 -1
  169. package/dist/core/personas/index.js +1 -1
  170. package/dist/core/personas/loader.d.ts +40 -1
  171. package/dist/core/personas/loader.js +63 -1
  172. package/dist/core/personas/resolve.d.ts +13 -6
  173. package/dist/core/personas/resolve.js +46 -34
  174. package/dist/core/runtime/bearings.d.ts +20 -0
  175. package/dist/core/runtime/bearings.js +92 -0
  176. package/dist/core/runtime/close.d.ts +14 -0
  177. package/dist/core/runtime/close.js +151 -0
  178. package/dist/core/runtime/demote.d.ts +14 -0
  179. package/dist/core/runtime/demote.js +120 -0
  180. package/dist/core/runtime/front-door.js +1 -1
  181. package/dist/core/runtime/kickoff.d.ts +32 -6
  182. package/dist/core/runtime/kickoff.js +111 -37
  183. package/dist/core/runtime/launch.d.ts +29 -6
  184. package/dist/core/runtime/launch.js +85 -13
  185. package/dist/core/runtime/lifecycle.d.ts +13 -0
  186. package/dist/core/runtime/lifecycle.js +86 -0
  187. package/dist/core/runtime/memory.d.ts +43 -0
  188. package/dist/core/runtime/memory.js +165 -0
  189. package/dist/core/runtime/naming.d.ts +22 -0
  190. package/dist/core/runtime/naming.js +166 -0
  191. package/dist/core/runtime/nodes.d.ts +32 -1
  192. package/dist/core/runtime/nodes.js +60 -10
  193. package/dist/core/runtime/persona.d.ts +25 -0
  194. package/dist/core/runtime/persona.js +139 -0
  195. package/dist/core/runtime/placement.d.ts +287 -0
  196. package/dist/core/runtime/placement.js +663 -0
  197. package/dist/core/runtime/presence.d.ts +7 -32
  198. package/dist/core/runtime/presence.js +90 -110
  199. package/dist/core/runtime/promote.d.ts +18 -7
  200. package/dist/core/runtime/promote.js +70 -65
  201. package/dist/core/runtime/reset.d.ts +47 -4
  202. package/dist/core/runtime/reset.js +223 -52
  203. package/dist/core/runtime/revive.d.ts +26 -2
  204. package/dist/core/runtime/revive.js +166 -39
  205. package/dist/core/runtime/roadmap.d.ts +5 -4
  206. package/dist/core/runtime/roadmap.js +9 -16
  207. package/dist/core/runtime/spawn.d.ts +20 -5
  208. package/dist/core/runtime/spawn.js +169 -44
  209. package/dist/core/runtime/stop-guard.d.ts +1 -1
  210. package/dist/core/runtime/stop-guard.js +18 -8
  211. package/dist/core/runtime/tmux.d.ts +106 -21
  212. package/dist/core/runtime/tmux.js +249 -45
  213. package/dist/core/spawn.js +15 -0
  214. package/dist/daemon/crtrd.d.ts +12 -1
  215. package/dist/daemon/crtrd.js +152 -34
  216. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
  217. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +266 -0
  218. package/dist/pi-extensions/canvas-commands.d.ts +34 -0
  219. package/dist/pi-extensions/canvas-commands.js +103 -0
  220. package/dist/pi-extensions/canvas-context-intro.d.ts +70 -0
  221. package/dist/pi-extensions/canvas-context-intro.js +164 -0
  222. package/dist/pi-extensions/canvas-goal-capture.d.ts +21 -0
  223. package/dist/pi-extensions/canvas-goal-capture.js +67 -0
  224. package/dist/pi-extensions/canvas-inbox-watcher.js +11 -0
  225. package/dist/pi-extensions/canvas-nav.d.ts +12 -4
  226. package/dist/pi-extensions/canvas-nav.js +586 -262
  227. package/dist/pi-extensions/canvas-passive-context.d.ts +32 -0
  228. package/dist/pi-extensions/canvas-passive-context.js +114 -0
  229. package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
  230. package/dist/pi-extensions/canvas-stophook.js +344 -228
  231. package/dist/types.d.ts +28 -0
  232. package/dist/types.js +16 -0
  233. 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 (so e.g. UP doesn't trigger pi's history recall). Returns unsub. */
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`.