@crouton-kit/crouter 0.3.18 → 0.3.19

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 (257) hide show
  1. package/dist/build-root.js +6 -0
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
  5. package/dist/builtin-memory/crouter-development/personas.md +117 -0
  6. package/dist/builtin-memory/crouter-development/plugins.md +163 -0
  7. package/dist/builtin-memory/design.md +60 -0
  8. package/dist/builtin-memory/development.md +116 -0
  9. package/dist/builtin-memory/planning.md +66 -0
  10. package/dist/builtin-memory/spec.md +99 -0
  11. package/dist/builtin-personas/design/orchestrator.md +1 -1
  12. package/dist/builtin-personas/developer/orchestrator.md +1 -1
  13. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  14. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  15. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  16. package/dist/builtin-personas/plan/orchestrator.md +1 -1
  17. package/dist/builtin-personas/runtime-base.md +6 -3
  18. package/dist/builtin-personas/spec/orchestrator.md +1 -1
  19. package/dist/builtin-personas/waiting.md +8 -0
  20. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  21. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  22. package/dist/builtin-views/_lib/states.mjs +161 -0
  23. package/dist/builtin-views/canvas/client.mjs +303 -0
  24. package/dist/builtin-views/canvas/view.mjs +576 -0
  25. package/dist/builtin-views/git-pr/client.mjs +440 -0
  26. package/dist/builtin-views/git-pr/view.mjs +675 -0
  27. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  28. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  29. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  30. package/dist/builtin-views/inbox/view.mjs +889 -0
  31. package/dist/builtin-views/linkedin/client.mjs +610 -0
  32. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  33. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  34. package/dist/clients/attach/attach-cmd.js +354 -0
  35. package/dist/clients/attach/chat-view.d.ts +77 -0
  36. package/dist/clients/attach/chat-view.js +450 -0
  37. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  38. package/dist/clients/attach/clipboard-image.js +113 -0
  39. package/dist/clients/attach/config-load.d.ts +31 -0
  40. package/dist/clients/attach/config-load.js +113 -0
  41. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  42. package/dist/clients/attach/extension-dialogs.js +101 -0
  43. package/dist/clients/attach/input-controller.d.ts +54 -0
  44. package/dist/clients/attach/input-controller.js +204 -0
  45. package/dist/clients/attach/slash-commands.d.ts +36 -0
  46. package/dist/clients/attach/slash-commands.js +200 -0
  47. package/dist/clients/attach/view-socket.d.ts +48 -0
  48. package/dist/clients/attach/view-socket.js +126 -0
  49. package/dist/commands/attention.js +3 -3
  50. package/dist/commands/canvas-prune.js +1 -1
  51. package/dist/commands/daemon.js +3 -2
  52. package/dist/commands/human/prompts.js +1 -1
  53. package/dist/commands/human/queue.js +43 -8
  54. package/dist/commands/human/shared.d.ts +2 -0
  55. package/dist/commands/memory/find.d.ts +1 -0
  56. package/dist/commands/memory/find.js +180 -0
  57. package/dist/commands/memory/lint.d.ts +1 -0
  58. package/dist/commands/memory/lint.js +140 -0
  59. package/dist/commands/memory/list.d.ts +1 -0
  60. package/dist/commands/memory/list.js +79 -0
  61. package/dist/commands/memory/read.js +103 -0
  62. package/dist/commands/memory/shared.d.ts +30 -0
  63. package/dist/commands/memory/shared.js +122 -0
  64. package/dist/commands/memory/write.d.ts +1 -0
  65. package/dist/commands/memory/write.js +85 -0
  66. package/dist/commands/memory.d.ts +2 -0
  67. package/dist/commands/memory.js +27 -0
  68. package/dist/commands/node.d.ts +3 -2
  69. package/dist/commands/node.js +607 -61
  70. package/dist/commands/pkg/market-manage.js +1 -1
  71. package/dist/commands/push.js +6 -6
  72. package/dist/commands/revive.js +1 -1
  73. package/dist/commands/skill/author.js +1 -1
  74. package/dist/commands/skill/shared.d.ts +1 -8
  75. package/dist/commands/skill/shared.js +2 -55
  76. package/dist/commands/skill.js +9 -14
  77. package/dist/commands/sys/doctor.js +1 -1
  78. package/dist/commands/sys/update.js +1 -1
  79. package/dist/commands/view-cycle.d.ts +2 -0
  80. package/dist/commands/view-cycle.js +125 -0
  81. package/dist/commands/view-list.d.ts +2 -0
  82. package/dist/commands/view-list.js +66 -0
  83. package/dist/commands/view-new.d.ts +2 -0
  84. package/dist/commands/view-new.js +70 -0
  85. package/dist/commands/view-pick.d.ts +2 -0
  86. package/dist/commands/view-pick.js +119 -0
  87. package/dist/commands/view-run.d.ts +2 -0
  88. package/dist/commands/view-run.js +191 -0
  89. package/dist/commands/view.d.ts +2 -0
  90. package/dist/commands/view.js +29 -0
  91. package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
  92. package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
  93. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  94. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  95. package/dist/core/__tests__/cascade-close.test.js +12 -2
  96. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  97. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  98. package/dist/core/__tests__/context-intro.test.js +76 -62
  99. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  100. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  101. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  102. package/dist/core/__tests__/detach-focus.test.js +206 -0
  103. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  104. package/dist/core/__tests__/draw-style.test.js +258 -0
  105. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  106. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  107. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  108. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  109. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  110. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  111. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  112. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  113. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  114. package/dist/core/__tests__/helpers/harness.js +111 -1
  115. package/dist/core/__tests__/home-session.test.js +41 -8
  116. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  117. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  118. package/dist/core/__tests__/kickoff.test.js +37 -3
  119. package/dist/core/__tests__/live-mutation.test.js +50 -33
  120. package/dist/core/__tests__/memory.test.js +23 -115
  121. package/dist/core/__tests__/placement-focus.test.js +5 -0
  122. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  123. package/dist/core/__tests__/relaunch.test.js +4 -3
  124. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  125. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  126. package/dist/core/__tests__/spawn-root.test.js +10 -0
  127. package/dist/core/__tests__/spike-harness.test.js +1 -0
  128. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  129. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  130. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  131. package/dist/core/__tests__/wake-origin.test.js +110 -0
  132. package/dist/core/bootstrap.js +1 -1
  133. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  134. package/dist/core/canvas/browse/app.js +24 -2
  135. package/dist/core/canvas/browse/model.d.ts +38 -2
  136. package/dist/core/canvas/browse/model.js +134 -10
  137. package/dist/core/canvas/browse/render.d.ts +6 -12
  138. package/dist/core/canvas/browse/render.js +72 -104
  139. package/dist/core/canvas/canvas.js +15 -22
  140. package/dist/core/canvas/db.js +46 -0
  141. package/dist/core/canvas/index.d.ts +1 -0
  142. package/dist/core/canvas/index.js +1 -0
  143. package/dist/core/canvas/pid.d.ts +4 -0
  144. package/dist/core/canvas/pid.js +23 -0
  145. package/dist/core/canvas/render.d.ts +11 -2
  146. package/dist/core/canvas/render.js +69 -0
  147. package/dist/core/canvas/types.d.ts +85 -3
  148. package/dist/core/canvas/wakeups.d.ts +76 -0
  149. package/dist/core/canvas/wakeups.js +185 -0
  150. package/dist/core/config.js +4 -1
  151. package/dist/core/frontmatter.js +37 -124
  152. package/dist/core/memory-resolver.d.ts +49 -0
  153. package/dist/core/memory-resolver.js +141 -0
  154. package/dist/core/personas/loader.d.ts +7 -0
  155. package/dist/core/personas/loader.js +36 -5
  156. package/dist/core/personas/resolve.js +13 -3
  157. package/dist/core/predicate.d.ts +63 -0
  158. package/dist/core/predicate.js +189 -0
  159. package/dist/core/resolver.js +26 -5
  160. package/dist/core/runtime/bearings.d.ts +53 -12
  161. package/dist/core/runtime/bearings.js +132 -59
  162. package/dist/core/runtime/broker-cli.d.ts +1 -0
  163. package/dist/core/runtime/broker-cli.js +46 -0
  164. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  165. package/dist/core/runtime/broker-protocol.js +153 -0
  166. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  167. package/dist/core/runtime/broker-sdk.js +72 -0
  168. package/dist/core/runtime/broker.d.ts +55 -0
  169. package/dist/core/runtime/broker.js +1128 -0
  170. package/dist/core/runtime/close.js +35 -6
  171. package/dist/core/runtime/host.d.ts +53 -0
  172. package/dist/core/runtime/host.js +186 -0
  173. package/dist/core/runtime/kickoff.d.ts +2 -1
  174. package/dist/core/runtime/kickoff.js +91 -5
  175. package/dist/core/runtime/launch.d.ts +45 -2
  176. package/dist/core/runtime/launch.js +65 -2
  177. package/dist/core/runtime/lifecycle.js +23 -6
  178. package/dist/core/runtime/memory.d.ts +2 -42
  179. package/dist/core/runtime/memory.js +11 -162
  180. package/dist/core/runtime/nodes.d.ts +33 -0
  181. package/dist/core/runtime/nodes.js +59 -1
  182. package/dist/core/runtime/persona.js +21 -11
  183. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  184. package/dist/core/runtime/pi-vendored.js +49 -0
  185. package/dist/core/runtime/placement.d.ts +32 -14
  186. package/dist/core/runtime/placement.js +206 -52
  187. package/dist/core/runtime/promote.d.ts +0 -6
  188. package/dist/core/runtime/promote.js +1 -12
  189. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  190. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  191. package/dist/core/runtime/reset.js +8 -6
  192. package/dist/core/runtime/revive.d.ts +2 -0
  193. package/dist/core/runtime/revive.js +34 -34
  194. package/dist/core/runtime/spawn.d.ts +19 -0
  195. package/dist/core/runtime/spawn.js +75 -22
  196. package/dist/core/runtime/stop-guard.d.ts +1 -1
  197. package/dist/core/runtime/stop-guard.js +6 -1
  198. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  199. package/dist/core/runtime/tmux-chrome.js +1 -1
  200. package/dist/core/runtime/tmux.d.ts +28 -0
  201. package/dist/core/runtime/tmux.js +80 -6
  202. package/dist/core/scope.d.ts +11 -0
  203. package/dist/core/scope.js +39 -0
  204. package/dist/core/spawn.d.ts +5 -0
  205. package/dist/core/spawn.js +20 -2
  206. package/dist/core/substrate/gate.d.ts +13 -0
  207. package/dist/core/substrate/gate.js +21 -0
  208. package/dist/core/substrate/index.d.ts +7 -0
  209. package/dist/core/substrate/index.js +18 -0
  210. package/dist/core/substrate/on-read.d.ts +14 -0
  211. package/dist/core/substrate/on-read.js +292 -0
  212. package/dist/core/substrate/render.d.ts +25 -0
  213. package/dist/core/substrate/render.js +256 -0
  214. package/dist/core/substrate/schema.d.ts +76 -0
  215. package/dist/core/substrate/schema.js +124 -0
  216. package/dist/core/substrate/session-cache.d.ts +30 -0
  217. package/dist/core/substrate/session-cache.js +77 -0
  218. package/dist/core/substrate/subject.d.ts +41 -0
  219. package/dist/core/substrate/subject.js +54 -0
  220. package/dist/core/tui/contract.d.ts +83 -0
  221. package/dist/core/tui/contract.js +8 -0
  222. package/dist/core/tui/draw.d.ts +96 -0
  223. package/dist/core/tui/draw.js +339 -0
  224. package/dist/core/tui/host.d.ts +29 -0
  225. package/dist/core/tui/host.js +379 -0
  226. package/dist/core/tui/loader.d.ts +16 -0
  227. package/dist/core/tui/loader.js +94 -0
  228. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  229. package/dist/core/wake.d.ts +86 -0
  230. package/dist/core/wake.js +308 -0
  231. package/dist/daemon/crtrd.d.ts +29 -4
  232. package/dist/daemon/crtrd.js +662 -46
  233. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  234. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  235. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  236. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  237. package/dist/pi-extensions/canvas-commands.js +10 -0
  238. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  239. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  240. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  241. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  242. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  243. package/dist/pi-extensions/canvas-nav.js +87 -23
  244. package/dist/pi-extensions/canvas-stophook.js +17 -8
  245. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  246. package/dist/pi-extensions/canvas-view.js +75 -0
  247. package/dist/prompts/skill.js +19 -26
  248. package/dist/prompts/view.d.ts +7 -0
  249. package/dist/prompts/view.js +101 -0
  250. package/dist/types.d.ts +4 -0
  251. package/dist/types.js +1 -0
  252. package/package.json +9 -4
  253. package/dist/commands/skill/find.d.ts +0 -4
  254. package/dist/commands/skill/find.js +0 -257
  255. package/dist/commands/skill/read.js +0 -91
  256. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  257. /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
@@ -0,0 +1,258 @@
1
+ // Bug-regression test for the crtr TUI Draw style encoding.
2
+ //
3
+ // Observed bug (canvas + linkedin views, 2026-06-07): view code set Style.fg to a
4
+ // color NAME ('green'/'cyan'/'red') instead of an SGR parameter ('32'/'36'/'31').
5
+ // With color ON, styleSpan emitted `\x1b[greenm` — an invalid CSI the terminal
6
+ // eats as a control op, printing the tail ('reenm') as literal garbage. The
7
+ // monitor's status glyphs/flags rendered as garbage on any color terminal (the
8
+ // non-TTY/NO_COLOR path was unaffected because fg is gated behind `color`).
9
+ //
10
+ // Root-cause fix: styleSpan now only emits fg/bg when the value is a valid SGR
11
+ // parameter string (digits + semicolons); a bad value degrades to no-color
12
+ // instead of emitting a broken escape. These tests lock that guarantee.
13
+ //
14
+ // Run: node --import tsx/esm --test src/core/__tests__/draw-style.test.ts
15
+ import { test, describe } from 'node:test';
16
+ import assert from 'node:assert/strict';
17
+ import { styleSpan, isSgrParams, createDraw } from '../tui/draw.js';
18
+ import { drawChrome } from '../tui/host.js';
19
+ const ESC = '\x1b[';
20
+ const ON = { color: true, color256: true };
21
+ const OFF = { color: false, color256: false };
22
+ const SIZE = { cols: 80, rows: 24 };
23
+ /** Strip the harmless framing CSIs the serializer emits (cursor home / clear),
24
+ * so any REMAINING `\x1b[` not followed by an SGR param digit/semicolon is a
25
+ * broken SGR escape — the exact bug class (`\x1b[greenm`). */
26
+ function hasInvalidCsi(frame) {
27
+ const stripped = frame.replace(/\x1b\[[HJK]/g, '').replace(/[\r\n]/g, '');
28
+ return /\x1b\[[^0-9;]/.test(stripped);
29
+ }
30
+ /** Any 8-bit color SGR (fg 30-37/90-97, or 256-color bg) present in the frame. */
31
+ function hasColorSgr(frame) {
32
+ return /\x1b\[(3[0-9]|9[0-7])m/.test(frame) || /\x1b\[48;5;/.test(frame);
33
+ }
34
+ function baseChrome(over = {}) {
35
+ return { status: null, banner: null, busy: false, loaded: true, lastRefresh: Date.now(), tick: 0, subtitle: null, mode: null, ...over };
36
+ }
37
+ const MANIFEST = {
38
+ id: 't', title: 'Test View', subtitle: '3 unread', description: 'd',
39
+ keymap: [
40
+ { keys: 'j/k', label: 'move' }, { keys: 'enter', label: 'open' },
41
+ { keys: 'r', label: 'reply' }, { keys: 'g', label: 'refresh' }, { keys: 'q', label: 'quit' },
42
+ ],
43
+ };
44
+ describe('styleSpan: SGR encoding (bug-regression)', () => {
45
+ test('a valid numeric fg emits a well-formed CSI', () => {
46
+ const out = styleSpan('●', { fg: '32', bold: true }, true, '');
47
+ assert.ok(out.includes(`${ESC}32m`), 'should contain \\x1b[32m');
48
+ assert.ok(out.includes(`${ESC}1m`), 'bold still emitted');
49
+ });
50
+ test('a color NAME fg degrades to no-color — never emits a broken escape', () => {
51
+ const out = styleSpan('●', { fg: 'green' }, true, '');
52
+ // The bug: \x1b[greenm. Assert no CSI is followed by a non-digit/semicolon.
53
+ assert.ok(!/\x1b\[[^0-9;]/.test(out), 'no invalid CSI introducer');
54
+ assert.ok(!out.includes('green'), 'the bad token never leaks into output');
55
+ });
56
+ test('a color NAME bg degrades to no-color', () => {
57
+ const out = styleSpan('x', { bg: 'red' }, true, '');
58
+ assert.ok(!/\x1b\[[^0-9;]/.test(out), 'no invalid CSI introducer');
59
+ assert.ok(!out.includes('red'), 'the bad token never leaks into output');
60
+ });
61
+ test('a valid 256-color bg index emits 48;5;N', () => {
62
+ const out = styleSpan('x', { bg: '236' }, true, '');
63
+ assert.ok(out.includes(`${ESC}48;5;236m`), 'should contain \\x1b[48;5;236m');
64
+ });
65
+ test('compound SGR params (e.g. "1;36") are valid', () => {
66
+ assert.equal(isSgrParams('1;36'), true);
67
+ assert.equal(isSgrParams('236'), true);
68
+ assert.equal(isSgrParams('green'), false);
69
+ assert.equal(isSgrParams(''), false);
70
+ });
71
+ test('structural styles (bold/dim/reverse) are unaffected by color gating', () => {
72
+ const out = styleSpan('x', { fg: 'green', dim: true }, true, '');
73
+ assert.ok(out.includes(`${ESC}2m`), 'dim still emitted even when fg is rejected');
74
+ });
75
+ });
76
+ // ── New Draw primitives — same bug class (no invalid CSI; bad value degrades) ──
77
+ describe('spansRight: SGR encoding (bug-regression)', () => {
78
+ test('valid numeric fg → well-formed CSI, no broken escape', () => {
79
+ const { draw, frame } = createDraw(SIZE, ON);
80
+ draw.spansRight(0, 80, [{ text: 'ready', style: { fg: '32', bold: true } }]);
81
+ const f = frame();
82
+ assert.ok(f.includes(`${ESC}32m`), 'emits \\x1b[32m');
83
+ assert.ok(!hasInvalidCsi(f), 'no invalid CSI');
84
+ });
85
+ test('a color NAME fg degrades — no broken escape, name not leaked', () => {
86
+ const { draw, frame } = createDraw(SIZE, ON);
87
+ draw.spansRight(0, 80, [{ text: 'x', style: { fg: 'green' } }]);
88
+ const f = frame();
89
+ assert.ok(!hasInvalidCsi(f), 'no invalid CSI introducer');
90
+ assert.ok(!f.includes('green'), 'the bad token never leaks');
91
+ });
92
+ test('over-maxWidth groups left-clip with a leading … (no overflow, no garbage)', () => {
93
+ const { draw, frame } = createDraw(SIZE, ON);
94
+ draw.spansRight(0, 80, [{ text: 'abcdefghij', style: { dim: true } }], 4);
95
+ const f = frame();
96
+ assert.ok(f.includes('…'), 'leading ellipsis present');
97
+ assert.ok(!hasInvalidCsi(f), 'no invalid CSI');
98
+ });
99
+ });
100
+ describe('vline: SGR encoding + mono fallback (bug-regression)', () => {
101
+ test('renders a dim rule with no invalid CSI (color on)', () => {
102
+ const { draw, frame } = createDraw(SIZE, ON);
103
+ draw.vline(40, 0, 10);
104
+ const f = frame();
105
+ assert.ok(f.includes('│'), 'default │ glyph present');
106
+ assert.ok(f.includes(`${ESC}2m`), 'dim emitted');
107
+ assert.ok(!hasInvalidCsi(f), 'no invalid CSI');
108
+ });
109
+ test('ASCII fallback ch and mono caps both stay well-formed', () => {
110
+ const { draw, frame } = createDraw(SIZE, OFF);
111
+ draw.vline(40, 0, 10, '|');
112
+ const f = frame();
113
+ assert.ok(f.includes('|'), 'ASCII fallback | present');
114
+ assert.ok(!hasColorSgr(f), 'no color SGR when caps.color is off');
115
+ assert.ok(!hasInvalidCsi(f), 'no invalid CSI');
116
+ });
117
+ });
118
+ describe('ListItemRow.right: SGR encoding (bug-regression)', () => {
119
+ test('right group + cursor-row base merge → no invalid CSI', () => {
120
+ const { draw, frame } = createDraw(SIZE, ON);
121
+ /** @type {ListItemRow[]} */
122
+ const items = [
123
+ { spans: [{ text: 'Ada Lovelace', style: { bold: true } }], right: [{ text: '2h', style: { fg: '90' } }] },
124
+ { spans: [{ text: 'Grace Hopper' }], right: [{ text: '4h', style: { fg: '90' } }] },
125
+ ];
126
+ const res = draw.list({ row: 2, col: 0, width: 80, height: 5 }, items, 0, 0);
127
+ const f = frame();
128
+ assert.equal(res.scroll, 0);
129
+ assert.ok(f.includes('2h') && f.includes('4h'), 'right-flushed timestamps drawn');
130
+ assert.ok(f.includes(`${ESC}48;5;236m`), 'cursor-row 236-bg highlight present');
131
+ assert.ok(!hasInvalidCsi(f), 'no invalid CSI');
132
+ });
133
+ test('a color NAME fg in the right group degrades, not leaks', () => {
134
+ const { draw, frame } = createDraw(SIZE, ON);
135
+ const items = [{ spans: [{ text: 'x' }], right: [{ text: 'y', style: { fg: 'cyan' } }] }];
136
+ draw.list({ row: 0, col: 0, width: 80, height: 2 }, items, 0, 0);
137
+ const f = frame();
138
+ assert.ok(!hasInvalidCsi(f), 'no invalid CSI');
139
+ assert.ok(!f.includes('cyan'), 'the bad token never leaks');
140
+ });
141
+ });
142
+ // ── Chrome — every new element survives the bug class + has a mono fallback ──
143
+ describe('drawChrome: SGR encoding + mono fallback (bug-regression)', () => {
144
+ const states = [
145
+ ['working', baseChrome({ busy: true, status: 'Loading…' })],
146
+ ['blocked', baseChrome({ banner: { msg: 'Send rejected', level: 'error' } })],
147
+ ['attention', baseChrome({ banner: { msg: 'Log in, then press r', level: 'action' } })],
148
+ ['ready', baseChrome({ status: '5 conversations' })],
149
+ ['idle', baseChrome({ loaded: false, lastRefresh: 0 })],
150
+ ['info-banner', baseChrome({ banner: { msg: 'Throttling — waiting', level: 'info' } })],
151
+ ];
152
+ for (const [name, chrome] of states) {
153
+ test(`state "${name}" never emits a broken escape (color on)`, () => {
154
+ const { draw, frame } = createDraw(SIZE, ON);
155
+ drawChrome(draw, SIZE, MANIFEST, chrome);
156
+ assert.ok(!hasInvalidCsi(frame()), 'no invalid CSI');
157
+ });
158
+ }
159
+ test('mono fallback: no color SGR when caps.color is off, glyphs/words still render', () => {
160
+ const { draw, frame } = createDraw(SIZE, OFF);
161
+ drawChrome(draw, SIZE, MANIFEST, baseChrome({ busy: true, status: 'Loading…' }));
162
+ const f = frame();
163
+ assert.ok(!hasColorSgr(f), 'no color SGR emitted in mono');
164
+ assert.ok(f.includes('▎'), 'state rail glyph always drawn');
165
+ assert.ok(f.includes('working'), 'state WORD carries meaning in mono');
166
+ assert.ok(f.includes(`${ESC}1m`), 'structural bold still emitted');
167
+ assert.ok(!hasInvalidCsi(f), 'no invalid CSI');
168
+ });
169
+ test('every banner level renders its glyph with no broken escape', () => {
170
+ for (const [level, glyph] of [['info', 'ℹ'], ['action', '▸'], ['error', '✗']]) {
171
+ const { draw, frame } = createDraw(SIZE, ON);
172
+ drawChrome(draw, SIZE, MANIFEST, baseChrome({ banner: { msg: 'm', level } }));
173
+ const f = frame();
174
+ assert.ok(f.includes(glyph), `${level} banner glyph ${glyph} present`);
175
+ assert.ok(!hasInvalidCsi(f), `${level} banner: no invalid CSI`);
176
+ }
177
+ });
178
+ test('keymap overflow degrades to keys-only on a narrow screen, still well-formed', () => {
179
+ const narrow = { cols: 28, rows: 24 };
180
+ const { draw, frame } = createDraw(narrow, ON);
181
+ drawChrome(draw, narrow, MANIFEST, baseChrome({ status: 'a very long status line here' }));
182
+ const f = frame();
183
+ assert.ok(f.includes('q'), 'q quit hint survives overflow');
184
+ assert.ok(!hasInvalidCsi(f), 'no invalid CSI under overflow');
185
+ });
186
+ });
187
+ // ── Dynamic subtitle (host.setSubtitle) — same bug class + mono fallback ──
188
+ describe('drawChrome subtitle: SGR encoding + mono fallback (bug-regression)', () => {
189
+ test('a dynamic subtitle overrides manifest.subtitle, dim, no broken escape', () => {
190
+ const { draw, frame } = createDraw(SIZE, ON);
191
+ drawChrome(draw, SIZE, MANIFEST, baseChrome({ subtitle: '7 unread' }));
192
+ const f = frame();
193
+ assert.ok(f.includes('7 unread'), 'dynamic subtitle rendered');
194
+ assert.ok(!f.includes('3 unread'), 'dynamic value overrides the static manifest subtitle');
195
+ assert.ok(f.includes(`${ESC}2m`), 'subtitle is dim (structural)');
196
+ assert.ok(!hasInvalidCsi(f), 'no invalid CSI');
197
+ });
198
+ test('null subtitle falls back to the manifest default', () => {
199
+ const { draw, frame } = createDraw(SIZE, ON);
200
+ drawChrome(draw, SIZE, MANIFEST, baseChrome({ subtitle: null }));
201
+ const f = frame();
202
+ assert.ok(f.includes('3 unread'), 'manifest.subtitle shown when dynamic is null');
203
+ assert.ok(!hasInvalidCsi(f), 'no invalid CSI');
204
+ });
205
+ test('mono fallback: subtitle text renders dim with no color SGR', () => {
206
+ const { draw, frame } = createDraw(SIZE, OFF);
207
+ drawChrome(draw, SIZE, MANIFEST, baseChrome({ subtitle: '7 unread' }));
208
+ const f = frame();
209
+ assert.ok(f.includes('7 unread'), 'subtitle text still renders in mono');
210
+ assert.ok(!hasColorSgr(f), 'no color SGR emitted for the subtitle in mono');
211
+ assert.ok(!hasInvalidCsi(f), 'no invalid CSI');
212
+ });
213
+ });
214
+ // ── Mode chip (host.setMode) — same bug class, mono fallback, precedence ──
215
+ describe('drawChrome mode chip: SGR encoding + mono fallback (bug-regression)', () => {
216
+ test('compose mode → ✎ compose in yellow (33), no broken escape', () => {
217
+ const { draw, frame } = createDraw(SIZE, ON);
218
+ drawChrome(draw, SIZE, MANIFEST, baseChrome({ mode: 'compose' }));
219
+ const f = frame();
220
+ assert.ok(f.includes('✎') && f.includes('compose'), 'mode glyph + word present');
221
+ assert.ok(f.includes(`${ESC}33m`), 'compose-accent yellow 33 emitted');
222
+ assert.ok(!hasInvalidCsi(f), 'no invalid CSI');
223
+ });
224
+ test('react mode → ☺ react reuses the compose-accent yellow (33)', () => {
225
+ const { draw, frame } = createDraw(SIZE, ON);
226
+ drawChrome(draw, SIZE, MANIFEST, baseChrome({ mode: 'react' }));
227
+ const f = frame();
228
+ assert.ok(f.includes('☺') && f.includes('react'), 'react glyph + word present');
229
+ assert.ok(f.includes(`${ESC}33m`), 'react reuses yellow 33');
230
+ assert.ok(!hasInvalidCsi(f), 'no invalid CSI');
231
+ });
232
+ test('mono fallback: mode chip is glyph + bold word, no color SGR', () => {
233
+ const { draw, frame } = createDraw(SIZE, OFF);
234
+ drawChrome(draw, SIZE, MANIFEST, baseChrome({ mode: 'compose' }));
235
+ const f = frame();
236
+ assert.ok(f.includes('✎'), 'glyph carries the mode in mono');
237
+ assert.ok(f.includes('compose'), 'mode word carries meaning in mono');
238
+ assert.ok(f.includes(`${ESC}1m`), 'bold still emitted (mono carrier)');
239
+ assert.ok(!hasColorSgr(f), 'no color SGR emitted in mono');
240
+ assert.ok(!hasInvalidCsi(f), 'no invalid CSI');
241
+ });
242
+ test('precedence: an explicit mode WINS the chip over the derived busy/working state', () => {
243
+ const { draw, frame } = createDraw(SIZE, ON);
244
+ // busy would derive `working`; the explicit mode must override it.
245
+ drawChrome(draw, SIZE, MANIFEST, baseChrome({ busy: true, mode: 'compose' }));
246
+ const f = frame();
247
+ assert.ok(f.includes('compose'), 'mode word shown');
248
+ assert.ok(!f.includes('working'), 'derived working chip suppressed by the mode override');
249
+ assert.ok(!hasInvalidCsi(f), 'no invalid CSI');
250
+ });
251
+ test('null mode returns to the derived chip (ready when loaded)', () => {
252
+ const { draw, frame } = createDraw(SIZE, ON);
253
+ drawChrome(draw, SIZE, MANIFEST, baseChrome({ mode: null }));
254
+ const f = frame();
255
+ assert.ok(f.includes('ready'), 'derived ready chip shown when no mode set');
256
+ assert.ok(!hasInvalidCsi(f), 'no invalid CSI');
257
+ });
258
+ });
@@ -0,0 +1,2 @@
1
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+ export default function (pi: ExtensionAPI): void;
@@ -0,0 +1,18 @@
1
+ export default function (pi) {
2
+ pi.registerProvider('c3prov', {
3
+ baseUrl: 'https://example.invalid',
4
+ apiKey: 'c3-test-key',
5
+ api: 'anthropic-messages',
6
+ models: [
7
+ {
8
+ id: 'c3model',
9
+ name: 'C3 Custom Model',
10
+ reasoning: false,
11
+ input: ['text'],
12
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
13
+ contextWindow: 1000,
14
+ maxTokens: 100,
15
+ },
16
+ ],
17
+ });
18
+ }
@@ -0,0 +1,138 @@
1
+ /** Test seam (M-1 regression): a FRESH-start kickoff prompt carrying this token
2
+ * makes the fake engine THROW inside bindExtensions BEFORE it fires
3
+ * session_start — the exact failure mode that records no pid and no session, so
4
+ * the broker exits(1) and the daemon must surface a boot failure rather than
5
+ * strand the node. Carried on the prompt (not a global env) so it is per-spawn. */
6
+ export declare const FAIL_BEFORE_SESSION_START = "__FAIL_BEFORE_SESSION_START__";
7
+ export declare class SessionManager {
8
+ readonly sessionId: string;
9
+ readonly sessionFile: string;
10
+ readonly resumed: boolean;
11
+ private constructor();
12
+ /** Fresh start: a brand-new session whose .jsonl lives under the node dir
13
+ * (created empty if missing, like fake-pi-host). */
14
+ static create(_cwd: string): SessionManager;
15
+ /** Resume: round-trip the .jsonl PATH the revive passed via `--session`. */
16
+ static open(path: string): SessionManager;
17
+ getSessionId(): string;
18
+ getSessionFile(): string;
19
+ }
20
+ type Handler = (ev: unknown, ctx: unknown) => void | Promise<void>;
21
+ interface BindExtensionsOpts {
22
+ uiContext: BrokerUiContext;
23
+ mode: string;
24
+ shutdownHandler?: () => void;
25
+ }
26
+ /** The slice of the broker's ExtensionUIContext we exercise (the only blocking
27
+ * dialog scenario 7 drives). */
28
+ interface BrokerUiContext {
29
+ confirm?: (title: string, message: string, opts?: {
30
+ signal?: AbortSignal;
31
+ timeout?: number;
32
+ }) => Promise<boolean>;
33
+ }
34
+ /** The slice of a loaded SDK extension we drive: its path + the handler map the
35
+ * SDK's createExtensionAPI populated as the extension called pi.on(event, …)
36
+ * during the loader's reload(). */
37
+ interface LoadedExtension {
38
+ path: string;
39
+ handlers: Map<string, Handler[]>;
40
+ }
41
+ /** The shared extension runtime the SDK's loader built (createExtensionRuntime).
42
+ * Its action methods start as `notInitialized` throwers; the REAL session's
43
+ * bindCore swaps them for working impls bound to the live session — we replicate
44
+ * the minimal slice (sendUserMessage/sendMessage/setSessionName) so the already-
45
+ * registered handlers (e.g. the inbox-watcher) deliver instead of throwing. */
46
+ interface ExtRuntime {
47
+ assertActive: () => void;
48
+ sendUserMessage: (content: string, options?: {
49
+ deliverAs?: string;
50
+ }) => void;
51
+ sendMessage: (message: unknown, options?: {
52
+ deliverAs?: string;
53
+ }) => void;
54
+ setSessionName: (name: string) => void;
55
+ [k: string]: unknown;
56
+ }
57
+ /** The minimal slice of the REAL DefaultResourceLoader the broker hands to
58
+ * createAgentSession: getExtensions() returns the already-loaded (jiti) canvas
59
+ * extensions + their shared runtime. */
60
+ interface ResourceLoader {
61
+ getExtensions(): {
62
+ extensions: LoadedExtension[];
63
+ errors?: unknown[];
64
+ runtime: ExtRuntime;
65
+ };
66
+ }
67
+ declare class FakeSession {
68
+ private readonly sm;
69
+ private readonly loader;
70
+ private readonly dir;
71
+ private extensions;
72
+ private readonly injected;
73
+ private uiContext;
74
+ private shutdownHandler;
75
+ private streaming;
76
+ private disposed;
77
+ private eventSeq;
78
+ private timer;
79
+ private readonly listeners;
80
+ private readonly messageLog;
81
+ constructor(sm: SessionManager, loader: ResourceLoader | undefined);
82
+ get messages(): unknown[];
83
+ get sessionId(): string;
84
+ get sessionFile(): string | undefined;
85
+ get model(): {
86
+ id: string;
87
+ } | undefined;
88
+ get isStreaming(): boolean;
89
+ getSessionStats(): Record<string, unknown>;
90
+ setSessionName(_name: string): void;
91
+ prompt(text: string, _options?: unknown): Promise<void>;
92
+ steer(_text: string): Promise<void>;
93
+ followUp(_text: string): Promise<void>;
94
+ abort(): Promise<void>;
95
+ subscribe(listener: (event: unknown) => void): () => void;
96
+ /** Fan one typed AgentSessionEvent out to every broker subscriber. A throwing
97
+ * listener is recorded, never propagated (mirrors the broker's m7 try/catch). */
98
+ private emit;
99
+ /** Emit a realistic streaming assistant turn on the subscribe channel:
100
+ * agent_start → turn_start → message_start → message_update×N →
101
+ * tool_execution_start/update/end → message_end → turn_end → agent_end. Drives
102
+ * G1 (controller prompt relay), G3 (produce-while-detached, accrues messageLog),
103
+ * and G8 (a fast event stream — `updates`/`padBytes` size the flood that sheds a
104
+ * stalled viewer at the broker HWM). A per-update setImmediate yield lets a
105
+ * fast viewer drain between frames so only the stalled one trips the HWM. */
106
+ private emitTurn;
107
+ dispose(): void;
108
+ bindExtensions(opts: BindExtensionsOpts): Promise<void>;
109
+ private recordInjected;
110
+ private buildCtx;
111
+ private recordEvent;
112
+ private fire;
113
+ private step;
114
+ private dispatch;
115
+ private runDialog;
116
+ }
117
+ interface FakeServices {
118
+ resourceLoader: ResourceLoader;
119
+ modelRegistry: {
120
+ find: (provider: string, id: string) => undefined;
121
+ };
122
+ }
123
+ export declare function createAgentSessionServices(options: {
124
+ cwd: string;
125
+ agentDir?: string;
126
+ resourceLoaderOptions?: {
127
+ additionalExtensionPaths?: string[];
128
+ appendSystemPrompt?: string[];
129
+ };
130
+ }): Promise<FakeServices>;
131
+ export declare function createAgentSessionFromServices(options: {
132
+ services: FakeServices;
133
+ sessionManager: SessionManager;
134
+ }): Promise<{
135
+ session: FakeSession;
136
+ }>;
137
+ export declare const VERSION = "0.78.1";
138
+ export {};