@crouton-kit/crouter 0.3.14 → 0.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) 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/attention.js +76 -7
  35. package/dist/commands/canvas-prune.d.ts +2 -0
  36. package/dist/commands/canvas-prune.js +66 -0
  37. package/dist/commands/canvas.js +5 -8
  38. package/dist/commands/chord.d.ts +2 -0
  39. package/dist/commands/chord.js +143 -0
  40. package/dist/commands/daemon.js +8 -5
  41. package/dist/commands/dashboard.js +2 -0
  42. package/dist/commands/human/prompts.js +28 -27
  43. package/dist/commands/human/queue.js +30 -14
  44. package/dist/commands/human/shared.d.ts +26 -21
  45. package/dist/commands/human/shared.js +45 -67
  46. package/dist/commands/human.js +4 -14
  47. package/dist/commands/node.d.ts +11 -0
  48. package/dist/commands/node.js +221 -99
  49. package/dist/commands/pkg/market-inspect.js +6 -4
  50. package/dist/commands/pkg/market-manage.js +10 -6
  51. package/dist/commands/pkg/market.js +2 -4
  52. package/dist/commands/pkg/plugin-inspect.js +6 -4
  53. package/dist/commands/pkg/plugin-manage.js +12 -7
  54. package/dist/commands/pkg/plugin.js +2 -4
  55. package/dist/commands/pkg.js +0 -4
  56. package/dist/commands/push.js +178 -15
  57. package/dist/commands/revive.js +5 -3
  58. package/dist/commands/skill/author.js +6 -4
  59. package/dist/commands/skill/find.js +8 -5
  60. package/dist/commands/skill/read.js +2 -0
  61. package/dist/commands/skill/state.js +6 -4
  62. package/dist/commands/skill.js +0 -6
  63. package/dist/commands/sys/config.js +21 -7
  64. package/dist/commands/sys/doctor.js +2 -0
  65. package/dist/commands/sys/update.js +4 -0
  66. package/dist/commands/sys.js +0 -6
  67. package/dist/commands/tmux-spread.d.ts +2 -0
  68. package/dist/commands/tmux-spread.js +129 -0
  69. package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
  70. package/dist/core/__tests__/child-followup.test.d.ts +1 -0
  71. package/dist/core/__tests__/child-followup.test.js +83 -0
  72. package/dist/core/__tests__/close.test.d.ts +1 -0
  73. package/dist/core/__tests__/close.test.js +148 -0
  74. package/dist/core/__tests__/context-intro.test.d.ts +1 -0
  75. package/dist/core/__tests__/context-intro.test.js +196 -0
  76. package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
  77. package/dist/core/__tests__/daemon-boot.test.js +93 -0
  78. package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
  79. package/dist/core/__tests__/daemon-liveness.test.js +223 -0
  80. package/dist/core/__tests__/focuses.test.d.ts +1 -0
  81. package/dist/core/__tests__/focuses.test.js +196 -0
  82. package/dist/core/__tests__/fork.test.d.ts +1 -0
  83. package/dist/core/__tests__/fork.test.js +91 -0
  84. package/dist/core/__tests__/home-session.test.d.ts +1 -0
  85. package/dist/core/__tests__/home-session.test.js +153 -0
  86. package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
  87. package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
  88. package/dist/core/__tests__/keystone.test.d.ts +1 -0
  89. package/dist/core/__tests__/keystone.test.js +185 -0
  90. package/dist/core/__tests__/kickoff.test.d.ts +1 -0
  91. package/dist/core/__tests__/kickoff.test.js +89 -0
  92. package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
  93. package/dist/core/__tests__/lifecycle.test.js +178 -0
  94. package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
  95. package/dist/core/__tests__/listing-completeness.test.js +31 -0
  96. package/dist/core/__tests__/memory.test.d.ts +1 -0
  97. package/dist/core/__tests__/memory.test.js +152 -0
  98. package/dist/core/__tests__/migration.test.d.ts +1 -0
  99. package/dist/core/__tests__/migration.test.js +238 -0
  100. package/dist/core/__tests__/pane-column.test.d.ts +1 -0
  101. package/dist/core/__tests__/pane-column.test.js +153 -0
  102. package/dist/core/__tests__/passive-subscription.test.js +24 -1
  103. package/dist/core/__tests__/persona-compose.test.d.ts +1 -0
  104. package/dist/core/__tests__/persona-compose.test.js +53 -0
  105. package/dist/core/__tests__/persona-subkind.test.d.ts +1 -0
  106. package/dist/core/__tests__/persona-subkind.test.js +62 -0
  107. package/dist/core/__tests__/persona.test.d.ts +1 -0
  108. package/dist/core/__tests__/persona.test.js +107 -0
  109. package/dist/core/__tests__/placement-focus.test.d.ts +1 -0
  110. package/dist/core/__tests__/placement-focus.test.js +266 -0
  111. package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
  112. package/dist/core/__tests__/placement-reconcile.test.js +212 -0
  113. package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
  114. package/dist/core/__tests__/placement-revive.test.js +238 -0
  115. package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
  116. package/dist/core/__tests__/placement-teardown.test.js +178 -0
  117. package/dist/core/__tests__/prune.test.d.ts +1 -0
  118. package/dist/core/__tests__/prune.test.js +116 -0
  119. package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
  120. package/dist/core/__tests__/push-final-guard.test.js +71 -0
  121. package/dist/core/__tests__/relaunch.test.d.ts +1 -0
  122. package/dist/core/__tests__/relaunch.test.js +334 -0
  123. package/dist/core/__tests__/reset.test.js +26 -7
  124. package/dist/core/__tests__/revive.test.d.ts +1 -0
  125. package/dist/core/__tests__/revive.test.js +217 -0
  126. package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
  127. package/dist/core/__tests__/spawn-root.test.js +73 -0
  128. package/dist/core/__tests__/steer-note.test.d.ts +1 -0
  129. package/dist/core/__tests__/steer-note.test.js +39 -0
  130. package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
  131. package/dist/core/__tests__/stop-guard.test.js +82 -0
  132. package/dist/core/__tests__/subcommand-tier.test.js +35 -33
  133. package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
  134. package/dist/core/__tests__/tmux-surface.test.js +105 -0
  135. package/dist/core/__tests__/unknown-path.test.js +8 -2
  136. package/dist/core/canvas/attention.d.ts +10 -0
  137. package/dist/core/canvas/attention.js +40 -0
  138. package/dist/core/canvas/canvas.d.ts +66 -7
  139. package/dist/core/canvas/canvas.js +209 -21
  140. package/dist/core/canvas/db.d.ts +8 -0
  141. package/dist/core/canvas/db.js +205 -4
  142. package/dist/core/canvas/focuses.d.ts +22 -0
  143. package/dist/core/canvas/focuses.js +81 -0
  144. package/dist/core/canvas/index.d.ts +3 -0
  145. package/dist/core/canvas/index.js +3 -0
  146. package/dist/core/canvas/labels.d.ts +27 -0
  147. package/dist/core/canvas/labels.js +36 -0
  148. package/dist/core/canvas/render.js +25 -10
  149. package/dist/core/canvas/telemetry.d.ts +14 -0
  150. package/dist/core/canvas/telemetry.js +35 -0
  151. package/dist/core/canvas/types.d.ts +115 -12
  152. package/dist/core/command.d.ts +25 -1
  153. package/dist/core/command.js +23 -15
  154. package/dist/core/config.js +36 -2
  155. package/dist/core/feed/feed.js +3 -3
  156. package/dist/core/feed/inbox.d.ts +3 -1
  157. package/dist/core/feed/inbox.js +45 -5
  158. package/dist/core/feed/passive.js +24 -11
  159. package/dist/core/help.d.ts +26 -13
  160. package/dist/core/help.js +44 -37
  161. package/dist/core/personas/index.d.ts +1 -1
  162. package/dist/core/personas/index.js +1 -1
  163. package/dist/core/personas/loader.d.ts +40 -1
  164. package/dist/core/personas/loader.js +63 -1
  165. package/dist/core/personas/resolve.d.ts +13 -6
  166. package/dist/core/personas/resolve.js +46 -34
  167. package/dist/core/runtime/bearings.d.ts +20 -0
  168. package/dist/core/runtime/bearings.js +92 -0
  169. package/dist/core/runtime/close.d.ts +14 -0
  170. package/dist/core/runtime/close.js +151 -0
  171. package/dist/core/runtime/demote.js +24 -12
  172. package/dist/core/runtime/front-door.js +1 -1
  173. package/dist/core/runtime/kickoff.d.ts +23 -6
  174. package/dist/core/runtime/kickoff.js +92 -36
  175. package/dist/core/runtime/launch.d.ts +26 -12
  176. package/dist/core/runtime/launch.js +78 -19
  177. package/dist/core/runtime/lifecycle.d.ts +13 -0
  178. package/dist/core/runtime/lifecycle.js +86 -0
  179. package/dist/core/runtime/memory.d.ts +43 -0
  180. package/dist/core/runtime/memory.js +165 -0
  181. package/dist/core/runtime/naming.d.ts +22 -0
  182. package/dist/core/runtime/naming.js +166 -0
  183. package/dist/core/runtime/nodes.d.ts +39 -1
  184. package/dist/core/runtime/nodes.js +69 -10
  185. package/dist/core/runtime/persona.d.ts +25 -0
  186. package/dist/core/runtime/persona.js +139 -0
  187. package/dist/core/runtime/placement.d.ts +299 -0
  188. package/dist/core/runtime/placement.js +688 -0
  189. package/dist/core/runtime/promote.d.ts +14 -7
  190. package/dist/core/runtime/promote.js +57 -67
  191. package/dist/core/runtime/reset.d.ts +47 -4
  192. package/dist/core/runtime/reset.js +223 -52
  193. package/dist/core/runtime/revive.d.ts +26 -2
  194. package/dist/core/runtime/revive.js +166 -39
  195. package/dist/core/runtime/spawn.d.ts +20 -5
  196. package/dist/core/runtime/spawn.js +163 -43
  197. package/dist/core/runtime/stop-guard.d.ts +1 -1
  198. package/dist/core/runtime/stop-guard.js +18 -8
  199. package/dist/core/runtime/tmux-chrome.d.ts +1 -0
  200. package/dist/core/runtime/tmux-chrome.js +4 -0
  201. package/dist/core/runtime/tmux.d.ts +113 -20
  202. package/dist/core/runtime/tmux.js +221 -39
  203. package/dist/core/spawn.js +15 -0
  204. package/dist/daemon/crtrd.d.ts +12 -1
  205. package/dist/daemon/crtrd.js +152 -34
  206. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
  207. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +266 -0
  208. package/dist/pi-extensions/canvas-commands.js +16 -13
  209. package/dist/pi-extensions/canvas-context-intro.d.ts +70 -0
  210. package/dist/pi-extensions/canvas-context-intro.js +164 -0
  211. package/dist/pi-extensions/canvas-goal-capture.d.ts +3 -0
  212. package/dist/pi-extensions/canvas-goal-capture.js +15 -1
  213. package/dist/pi-extensions/canvas-inbox-watcher.js +11 -0
  214. package/dist/pi-extensions/canvas-nav.d.ts +12 -4
  215. package/dist/pi-extensions/canvas-nav.js +594 -262
  216. package/dist/pi-extensions/canvas-resume.d.ts +22 -0
  217. package/dist/pi-extensions/canvas-resume.js +173 -0
  218. package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
  219. package/dist/pi-extensions/canvas-stophook.js +340 -228
  220. package/dist/types.d.ts +28 -0
  221. package/dist/types.js +16 -0
  222. package/package.json +2 -2
  223. package/dist/core/runtime/presence.d.ts +0 -38
  224. package/dist/core/runtime/presence.js +0 -154
@@ -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;
@@ -6,6 +6,9 @@ interface InputEventLike {
6
6
  }
7
7
  interface PiLike {
8
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;
9
12
  }
10
13
  /**
11
14
  * Register the goal-capture handler on `pi`.
@@ -19,6 +19,8 @@
19
19
  // Plain TS-with-types — no imports from @earendil-works/* so this compiles inside
20
20
  // crouter's own tsc build without a dep on the pi packages.
21
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';
22
24
  /**
23
25
  * Register the goal-capture handler on `pi`.
24
26
  *
@@ -43,7 +45,19 @@ export function registerCanvasGoalCapture(pi) {
43
45
  // masquerade as the user's first mandate.
44
46
  if (text.startsWith(REVIVE_KICKOFF_SENTINEL))
45
47
  return;
46
- captureGoalIfAbsent(nodeId, text);
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
+ }
47
61
  }
48
62
  catch {
49
63
  // Best-effort: a capture failure must never drop or alter the message.
@@ -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`.