@zerwiz/ymir 0.1.16 → 0.1.18

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 (171) hide show
  1. package/.agents/config/app-repos.yaml +38 -0
  2. package/.claude/settings.json +31 -0
  3. package/.codex/hooks.json +43 -0
  4. package/.cursor/hooks.json +33 -0
  5. package/.pi/extensions/README.md +39 -0
  6. package/.pi/extensions/gna-pi-watch.ts +15 -0
  7. package/.pi/extensions/lib/ro-assistant-layout.ts +96 -0
  8. package/.pi/extensions/lib/ro-operational-user-layout.ts +145 -0
  9. package/.pi/extensions/lib/ro-visibility.ts +99 -0
  10. package/.pi/extensions/lib/ro-working-ship.ts +243 -0
  11. package/.pi/extensions/lib/rodd-operational-input.ts +62 -0
  12. package/.pi/extensions/lib/skuld-branch-dispatch.ts +252 -0
  13. package/.pi/extensions/lib/skuld-branch-model-picker.ts +77 -0
  14. package/.pi/extensions/lib/vordr-sessionstart-supervisor.mjs +48 -0
  15. package/.pi/extensions/ro.ts +11 -0
  16. package/.pi/extensions/skuld-branch-supervision.ts +11 -0
  17. package/.pi/extensions/syn-turnend-guard.ts +11 -0
  18. package/.pi/mcp.json +16 -0
  19. package/.pi/mcp.json.example +11 -0
  20. package/.pi/npm/.gitignore +2 -0
  21. package/.pi/settings.json +8 -0
  22. package/.pi/shared/extensions/gna-pi-watch.ts +750 -0
  23. package/.pi/shared/extensions/herdr-agent-state.ts +257 -0
  24. package/.pi/shared/extensions/open-editor.ts +509 -0
  25. package/.pi/shared/extensions/ro.ts +504 -0
  26. package/.pi/shared/extensions/skuld-branch-supervision.ts +1535 -0
  27. package/.pi/shared/extensions/syn-turnend-guard.ts +683 -0
  28. package/.pi/shared/extensions/todo.ts +133 -0
  29. package/.pi/shared/extensions/ymir-subagents.ts +287 -0
  30. package/.pi/shared/extensions/ymir-well.ts +130 -0
  31. package/CHANGELOG.md +23 -0
  32. package/assets/Ymir.md +4654 -0
  33. package/assets/healer.html +717 -0
  34. package/assets/icon-family/family-sheet.png +0 -0
  35. package/assets/icon-family/hlidskjalf.svg +1 -0
  36. package/assets/icon-family/sessrumnir.svg +1 -0
  37. package/assets/icon-family/smidja.svg +1 -0
  38. package/assets/icon-family/ymir.svg +1 -0
  39. package/assets/mimirswell.html +880 -0
  40. package/assets/mock/README.md +16 -0
  41. package/assets/mock/feeds.ts +175 -0
  42. package/assets/mock/mock.ts +408 -0
  43. package/assets/reference/docs/agent-control.md +122 -0
  44. package/assets/reference/docs/apodex/Appdex_as_Orchestrator_Planner_Reviewer_Assessment.md +176 -0
  45. package/assets/reference/docs/apodex/Final Report(1).md +198 -0
  46. package/assets/reference/docs/apodex/Final Report(2).md +54 -0
  47. package/assets/reference/docs/apodex/Final Report(3).md +40 -0
  48. package/assets/reference/docs/apodex/Final Report(4).md +234 -0
  49. package/assets/reference/docs/apodex/Final Report(5).md +13 -0
  50. package/assets/reference/docs/apodex/Final Report.md +82 -0
  51. package/assets/reference/docs/architecture.md +392 -0
  52. package/assets/reference/docs/arm-pretool-check.md +247 -0
  53. package/assets/reference/docs/calm-mode-feasibility.md +541 -0
  54. package/assets/reference/docs/calm.md +59 -0
  55. package/assets/reference/docs/captain-hold-lifecycle.md +96 -0
  56. package/assets/reference/docs/cd-guard.md +165 -0
  57. package/assets/reference/docs/cmux-backend.md +133 -0
  58. package/assets/reference/docs/codex-app-backend.md +57 -0
  59. package/assets/reference/docs/configuration.md +924 -0
  60. package/assets/reference/docs/documentation-audiences.json +468 -0
  61. package/assets/reference/docs/documentation-audiences.md +28 -0
  62. package/assets/reference/docs/examples/crew-dispatch.json +26 -0
  63. package/assets/reference/docs/examples/process-event-extension/file-signal.mjs +96 -0
  64. package/assets/reference/docs/examples/process-event-extension/firstmate-extension.json +15 -0
  65. package/assets/reference/docs/examples/watched-tools.json +24 -0
  66. package/assets/reference/docs/examples/wedge-alarm +22 -0
  67. package/assets/reference/docs/extension-bindings.md +237 -0
  68. package/assets/reference/docs/fm-test-isolation-proof.json +37 -0
  69. package/assets/reference/docs/fm-test-isolation-proof.md +149 -0
  70. package/assets/reference/docs/fm-test-portable-shards.md +118 -0
  71. package/assets/reference/docs/gitlab-merge-watch.md +275 -0
  72. package/assets/reference/docs/herdr-backend.md +346 -0
  73. package/assets/reference/docs/orca-backend.md +84 -0
  74. package/assets/reference/docs/pi-supervision-branch-poster.svg +125 -0
  75. package/assets/reference/docs/pi-supervision-branch.md +96 -0
  76. package/assets/reference/docs/plans/agentic-engineering-workflow.md +395 -0
  77. package/assets/reference/docs/plans/agentic-factory-implementation-plan.md +615 -0
  78. package/assets/reference/docs/plans/command-factory-enhancement-plan.md +420 -0
  79. package/assets/reference/docs/plans/phase-1-layer3-program-design.md +134 -0
  80. package/assets/reference/docs/plans/phase-2-vertical-slices.md +100 -0
  81. package/assets/reference/docs/plans/phase-3-measurable-goals.md +106 -0
  82. package/assets/reference/docs/plans/phase-4-context-budget.md +106 -0
  83. package/assets/reference/docs/plans/phase-5-pre-mortem-confidence.md +100 -0
  84. package/assets/reference/docs/plans/phase-6-incident-to-agent.md +109 -0
  85. package/assets/reference/docs/remote-secondmates.md +268 -0
  86. package/assets/reference/docs/scripts.md +146 -0
  87. package/assets/reference/docs/sessionstart-nudge.md +115 -0
  88. package/assets/reference/docs/subagent-guard.md +383 -0
  89. package/assets/reference/docs/supervision-protocols/claude.md +27 -0
  90. package/assets/reference/docs/supervision-protocols/codex.md +16 -0
  91. package/assets/reference/docs/supervision-protocols/cursor.md +31 -0
  92. package/assets/reference/docs/supervision-protocols/grok.md +39 -0
  93. package/assets/reference/docs/supervision-protocols/opencode.md +17 -0
  94. package/assets/reference/docs/supervision-protocols/pi.md +33 -0
  95. package/assets/reference/docs/supervision-protocols/unknown.md +13 -0
  96. package/assets/reference/docs/tmux-backend.md +115 -0
  97. package/assets/reference/docs/trace-context.md +117 -0
  98. package/assets/reference/docs/turnend-guard.md +170 -0
  99. package/assets/reference/docs/verification/dispatch-auth.md +204 -0
  100. package/assets/reference/docs/verification/muse.md +221 -0
  101. package/assets/reference/docs/verification/process-event-sources.md +208 -0
  102. package/assets/reference/docs/verification/public-followup.md +119 -0
  103. package/assets/reference/docs/verification/runtime-backends.md +1022 -0
  104. package/assets/reference/docs/verification/stow-memory.md +53 -0
  105. package/assets/reference/docs/verification/supervision.md +534 -0
  106. package/assets/reference/docs/verification/trace-context.md +31 -0
  107. package/assets/reference/docs/voice-relay.md +295 -0
  108. package/assets/reference/docs/watcher-continuity.md +119 -0
  109. package/assets/reference/docs/wedge-alarm.md +39 -0
  110. package/assets/reference/docs/zellij-backend.md +114 -0
  111. package/assets/reference/index.html +1172 -0
  112. package/assets/reference/skills/afk/SKILL.md +241 -0
  113. package/assets/reference/skills/ahoy/SKILL.md +56 -0
  114. package/assets/reference/skills/ask-user-authority/SKILL.md +56 -0
  115. package/assets/reference/skills/bearings/SKILL.md +161 -0
  116. package/assets/reference/skills/bearings/assets/board-template.html +731 -0
  117. package/assets/reference/skills/bootstrap-diagnostics/SKILL.md +68 -0
  118. package/assets/reference/skills/captain-hold-lifecycle/SKILL.md +54 -0
  119. package/assets/reference/skills/decision-hold-lifecycle/SKILL.md +15 -0
  120. package/assets/reference/skills/diagnostic-reasoning/SKILL.md +53 -0
  121. package/assets/reference/skills/firstmate-codexapp/SKILL.md +110 -0
  122. package/assets/reference/skills/firstmate-codexapp/agents/openai.yaml +4 -0
  123. package/assets/reference/skills/firstmate-coding-guidelines/SKILL.md +127 -0
  124. package/assets/reference/skills/firstmate-orca/SKILL.md +92 -0
  125. package/assets/reference/skills/fmx-respond/SKILL.md +283 -0
  126. package/assets/reference/skills/harness-adapters/SKILL.md +95 -0
  127. package/assets/reference/skills/harness-adapters/references/common/control-and-recovery.md +37 -0
  128. package/assets/reference/skills/harness-adapters/references/common/dispatch.md +32 -0
  129. package/assets/reference/skills/harness-adapters/references/common/model-and-effort.md +42 -0
  130. package/assets/reference/skills/harness-adapters/references/common/primary-hooks.md +40 -0
  131. package/assets/reference/skills/harness-adapters/references/harness/claude.md +55 -0
  132. package/assets/reference/skills/harness-adapters/references/harness/codex.md +43 -0
  133. package/assets/reference/skills/harness-adapters/references/harness/cursor.md +75 -0
  134. package/assets/reference/skills/harness-adapters/references/harness/grok.md +69 -0
  135. package/assets/reference/skills/harness-adapters/references/harness/kimi.md +51 -0
  136. package/assets/reference/skills/harness-adapters/references/harness/muse.md +70 -0
  137. package/assets/reference/skills/harness-adapters/references/harness/opencode.md +42 -0
  138. package/assets/reference/skills/harness-adapters/references/harness/pi.md +56 -0
  139. package/assets/reference/skills/process-event-sources/SKILL.md +137 -0
  140. package/assets/reference/skills/project-management/SKILL.md +92 -0
  141. package/assets/reference/skills/quota-array-dispatch/SKILL.md +131 -0
  142. package/assets/reference/skills/secondmate-provisioning/SKILL.md +254 -0
  143. package/assets/reference/skills/stow/SKILL.md +309 -0
  144. package/assets/reference/skills/stuck-crewmate-recovery/SKILL.md +54 -0
  145. package/assets/reference/skills/updatefirstmate/SKILL.md +64 -0
  146. package/assets/skills/README.md +5 -0
  147. package/assets/yggdrasil.html +649 -0
  148. package/assets/ymir-banner-01.png +0 -0
  149. package/assets/ymir-banner-02.png +0 -0
  150. package/assets/ymir-banner-03.png +0 -0
  151. package/assets/ymir-banner-04.png +0 -0
  152. package/assets/ymir-banner-05.png +0 -0
  153. package/assets/ymir-banner-06.png +0 -0
  154. package/assets/ymir-emblem-darksteel.svg +63 -0
  155. package/assets/ymir-emblem-forged.svg +55 -0
  156. package/assets/ymir-emblem-slate.svg +55 -0
  157. package/assets/ymir-mark-algiz-anvil.svg +9 -0
  158. package/assets/ymir-stave.svg +53 -0
  159. package/assets/ymir.html +563 -0
  160. package/bin/ymir-install.sh +6 -1
  161. package/data/learnings.md +33 -0
  162. package/data/local-models.md +34 -0
  163. package/data/operator.md +15 -0
  164. package/data/projects.md +28 -0
  165. package/data/realm.md +1 -0
  166. package/deploy/Containerfile +27 -0
  167. package/deploy/README.md +104 -0
  168. package/deploy/compose/compose.yaml +45 -0
  169. package/deploy/env.example +30 -0
  170. package/deploy/quadlet/ymir.container +57 -0
  171. package/package.json +10 -8
@@ -0,0 +1,504 @@
1
+ // Brokk's home-persistent Pi transcript presentation toggle.
2
+ //
3
+ // Verified against Pi 0.81.1, 0.82.0, and 0.84.4, which expose built-in ToolDefinitions, per-slot
4
+ // renderers, renderShell: "self", session_start replacement reasons, agent_start and
5
+ // agent_settled, ExtensionUIContext.setToolsExpanded(), setWorkingVisible(), setWidget()
6
+ // with a disposable component smidja, and setHiddenThinkingLabel().
7
+ // ./lib/ro-working-ship.ts owns the animated working presentation this file
8
+ // installs. The focused tests pin those assumptions but never reject a
9
+ // newer Pi solely for its version. The collapsed-thinking and operational-user
10
+ // presentation adapters probe the exact API they patch and degrade independently with a
11
+ // diagnostic (see installCalmPresentationAdapter below) if a future Pi removes it; Pi
12
+ // still exposes no global renderer for arbitrary built-in or custom rows.
13
+ // docs/configuration.md owns the home-local Ró preference contract.
14
+ //
15
+ // Pi has one first-registration-wins ToolDefinition per tool name, with no merge or
16
+ // unregister operation. Keep Ró-off registration empty; keep Ró-on load-time
17
+ // registration synchronous because restored rows capture the registry before
18
+ // session_start; and collision-check only the later first-activation path, when
19
+ // getAllTools() is reliable. docs/ro-mode-feasibility.md owns the Pi-source evidence
20
+ // and docs/ro.md owns the user-facing behavior and non-retroactive first-toggle bound.
21
+ import { randomUUID } from "node:crypto";
22
+ import {
23
+ mkdirSync,
24
+ readFileSync,
25
+ realpathSync,
26
+ renameSync,
27
+ rmSync,
28
+ writeFileSync,
29
+ } from "node:fs";
30
+ import { dirname, resolve } from "node:path";
31
+ import { fileURLToPath } from "node:url";
32
+ import type {
33
+ ExtensionAPI,
34
+ ExtensionUIContext,
35
+ ToolDefinition,
36
+ ToolInfo,
37
+ ToolRenderResultOptions,
38
+ } from "@earendil-works/pi-coding-agent";
39
+ import {
40
+ createBashToolDefinition,
41
+ createEditToolDefinition,
42
+ createFindToolDefinition,
43
+ createGrepToolDefinition,
44
+ createLsToolDefinition,
45
+ createReadToolDefinition,
46
+ createWriteToolDefinition,
47
+ } from "@earendil-works/pi-coding-agent";
48
+ import { Box, Container, getKeybindings, type Component } from "@earendil-works/pi-tui";
49
+ import type { TSchema } from "typebox";
50
+ import { installRoAssistantLayout } from "./lib/ro-assistant-layout.ts";
51
+ import { installRoOperationalUserLayout } from "./lib/ro-operational-user-layout.ts";
52
+ import {
53
+ RO_WORKING_SHIP_WIDGET_KEY,
54
+ createCalmWorkingShipAnimation,
55
+ createCalmWorkingShipWidget,
56
+ } from "./lib/ro-working-ship.ts";
57
+ import {
58
+ roPresentationHides,
59
+ calmPresentationIsActive,
60
+ RO_PRESENTATION_EVENT,
61
+ registerBrokkSyntheticPresentation,
62
+ setCalmPresentation,
63
+ setCalmStockExportRendering,
64
+ } from "./lib/ro-visibility.ts";
65
+
66
+ type DefinitionFactory<TParams extends TSchema, TDetails, TState> = (
67
+ cwd: string,
68
+ ) => ToolDefinition<TParams, TDetails, TState>;
69
+
70
+ type RenderContext<TParams extends TSchema, TDetails, TState> = Parameters<
71
+ NonNullable<ToolDefinition<TParams, TDetails, TState>["renderCall"]>
72
+ >[2];
73
+
74
+ type RenderArgs<TParams extends TSchema, TDetails, TState> = Parameters<
75
+ NonNullable<ToolDefinition<TParams, TDetails, TState>["renderCall"]>
76
+ >[0];
77
+
78
+ type RenderTheme<TParams extends TSchema, TDetails, TState> = Parameters<
79
+ NonNullable<ToolDefinition<TParams, TDetails, TState>["renderCall"]>
80
+ >[1];
81
+
82
+ type RenderResult<TParams extends TSchema, TDetails, TState> = Parameters<
83
+ NonNullable<ToolDefinition<TParams, TDetails, TState>["renderResult"]>
84
+ >[0];
85
+
86
+ type StandardShellState = {
87
+ shell?: Box;
88
+ call?: Component;
89
+ result?: Component;
90
+ };
91
+
92
+ const extensionFile = fileURLToPath(import.meta.url);
93
+ const extensionDir = dirname(extensionFile);
94
+ const root = resolve(extensionDir, "../..");
95
+
96
+ // Resolves symlinks before comparing tool-ownership identity below: sourceInfo.path
97
+ // values come from independent path-resolution code paths (this module's own
98
+ // import.meta.url vs. Pi's extension loader), and macOS alone symlinks /tmp and /var
99
+ // to /private/..., so lexical string comparison alone spuriously reads a symlinked
100
+ // self-path as a foreign one. Falls back to the raw path for synthetic, non-file
101
+ // sourceInfo paths such as "<builtin:read>" or "<inline>", which realpathSync rejects.
102
+ const realpathOrSelf = (path: string): string => {
103
+ try {
104
+ return realpathSync(path);
105
+ } catch {
106
+ return path;
107
+ }
108
+ };
109
+ const extensionRealFile = realpathOrSelf(extensionFile);
110
+
111
+ // Each presentation adapter probes the exact Pi API it patches. If a future Pi removes
112
+ // that API, only the affected adapter degrades; the rest of Ró keeps working.
113
+ function installCalmPresentationAdapter(name: string, install: () => void): void {
114
+ try {
115
+ install();
116
+ } catch (error) {
117
+ const reason = error instanceof Error ? error.message : String(error);
118
+ console.error(`Brokk Ró: ${name} presentation adapter unavailable, skipping. ${reason}`);
119
+ }
120
+ }
121
+
122
+ export default function (pi: ExtensionAPI) {
123
+ installCalmPresentationAdapter("collapsed-thinking", installRoAssistantLayout);
124
+ installCalmPresentationAdapter("operational-user-row", installRoOperationalUserLayout);
125
+
126
+ let exportRendering = false;
127
+ let removeTerminalInputHandler: (() => void) | undefined;
128
+ // One logical agent run, tracked from agent_start through agent_settled rather than
129
+ // from turns or tool calls, so the boat never flickers between tool calls, automatic
130
+ // continuations, retries, or compaction that stay inside the same run.
131
+ let agentRunActive = false;
132
+ let workingShipShown = false;
133
+ // One animation instance per extension lifetime. Hiding the working widget freezes
134
+ // this state; the next working period resumes it. session_start resets it so a fresh
135
+ // Pi session starts at the normal initial position. Never module-global.
136
+ const workingShipAnimation = createCalmWorkingShipAnimation();
137
+
138
+ // Single owner of Ró's working-row presentation choice. The widget is only created
139
+ // or removed on a real transition, so repeated starts cannot duplicate its timer.
140
+ const applyWorkingPresentation = (
141
+ ui: ExtensionUIContext,
142
+ forceStockVisibility = false,
143
+ ): void => {
144
+ const showShip = agentRunActive && calmPresentationIsActive();
145
+ if (showShip !== workingShipShown) {
146
+ workingShipShown = showShip;
147
+ ui.setWidget(
148
+ RO_WORKING_SHIP_WIDGET_KEY,
149
+ showShip
150
+ ? (tui) => createCalmWorkingShipWidget(tui, workingShipAnimation)
151
+ : undefined,
152
+ );
153
+ ui.setWorkingVisible(!showShip);
154
+ } else if (forceStockVisibility && !showShip) {
155
+ ui.setWorkingVisible(true);
156
+ }
157
+ };
158
+
159
+ const fmHome = process.env.BROKK_HOME || process.env.BROKK_ROOT_OVERRIDE || root;
160
+ const configDirectory = process.env.BROKK_CONFIG_OVERRIDE || resolve(fmHome, "config");
161
+ const stateDirectory = process.env.BROKK_STATE_OVERRIDE || resolve(fmHome, "state");
162
+ // Ró is a PER-USER preference: it lives in the gitignored state dir so toggling
163
+ // it never dirties the tracked tree, and YMIR_RO/BROKK_RO may set the default.
164
+ // The legacy tracked `config/ro` is still read (upgrade path) but never written.
165
+ const calmPreferencePath = resolve(stateDirectory, "ro");
166
+ const legacyCalmPath = resolve(configDirectory, "ro");
167
+ const truthy = (v: string): boolean => v === "on" || v === "max";
168
+ const loadCalmPreference = (): boolean => {
169
+ for (const p of [calmPreferencePath, legacyCalmPath]) {
170
+ try {
171
+ const stored = readFileSync(p, "utf8").trim();
172
+ if (stored) return truthy(stored);
173
+ } catch {
174
+ /* try the next source */
175
+ }
176
+ }
177
+ const env = (process.env.YMIR_RO || process.env.BROKK_RO || "").trim().toLowerCase();
178
+ if (env === "on" || env === "max") return true;
179
+ if (env === "off") return false;
180
+ return false;
181
+ };
182
+ const persistCalmPreference = (active: boolean): void => {
183
+ mkdirSync(dirname(calmPreferencePath), { recursive: true });
184
+ const temporaryPath = `${calmPreferencePath}.${process.pid}.${randomUUID()}.tmp`;
185
+ try {
186
+ writeFileSync(temporaryPath, active ? "on\n" : "off\n", {
187
+ encoding: "utf8",
188
+ flag: "wx",
189
+ mode: 0o600,
190
+ });
191
+ renameSync(temporaryPath, calmPreferencePath);
192
+ } finally {
193
+ rmSync(temporaryPath, { force: true });
194
+ }
195
+ };
196
+
197
+ const publishPresentationState = (): void => {
198
+ pi.events.emit(RO_PRESENTATION_EVENT, {
199
+ active: calmPresentationIsActive(),
200
+ stockExportRendering: exportRendering,
201
+ });
202
+ };
203
+
204
+ registerBrokkSyntheticPresentation(pi);
205
+
206
+ // Every on-screen tool row Ró currently presents, keyed by the row-local state Pi
207
+ // hands its render slots, so Ró can repaint exactly those rows without touching
208
+ // Pi's transcript. Pi can re-render a row at any time - the built-in edit row
209
+ // invalidates itself once its diff is ready - so a row can be redrawn during the
210
+ // window where /export forces stock rendering and keep that stock content
211
+ // afterwards. Rows Pi's exporter renders are excluded: those use throwaway state
212
+ // and never appear on screen. Cleared per session lifetime, which rebuilds the rows.
213
+ const calmToolRowRepaints = new Map<object, () => void>();
214
+ const rememberCalmToolRow = (state: object, invalidate: unknown): void => {
215
+ if (exportRendering || typeof invalidate !== "function") return;
216
+ calmToolRowRepaints.set(state, invalidate as () => void);
217
+ };
218
+ const repaintCalmToolRows = (): void => {
219
+ for (const invalidate of calmToolRowRepaints.values()) invalidate();
220
+ };
221
+
222
+ function wrapBuiltIn<TParams extends TSchema, TDetails, TState>(
223
+ smidja: DefinitionFactory<TParams, TDetails, TState>,
224
+ ): ToolDefinition<TParams, TDetails, TState> {
225
+ const definitions = new Map<string, ToolDefinition<TParams, TDetails, TState>>();
226
+ const definitionFor = (cwd: string): ToolDefinition<TParams, TDetails, TState> => {
227
+ let definition = definitions.get(cwd);
228
+ if (!definition) {
229
+ definition = smidja(cwd);
230
+ definitions.set(cwd, definition);
231
+ }
232
+ return definition;
233
+ };
234
+
235
+ const original = definitionFor(process.cwd());
236
+ const originalRenderCall = original.renderCall;
237
+ const originalRenderResult = original.renderResult;
238
+ const originalSelfShell = original.renderShell === "self";
239
+ const standardShells = new WeakMap<object, StandardShellState>();
240
+
241
+ if (!originalRenderCall || !originalRenderResult) {
242
+ throw new Error(`Brokk ro mode requires both render slots for Pi built-in tool ${original.name}`);
243
+ }
244
+
245
+ const shellStateFor = (
246
+ context: RenderContext<TParams, TDetails, TState>,
247
+ ): StandardShellState => {
248
+ const rowState = context.state as object;
249
+ let shellState = standardShells.get(rowState);
250
+ if (!shellState) {
251
+ shellState = {};
252
+ standardShells.set(rowState, shellState);
253
+ }
254
+ return shellState;
255
+ };
256
+
257
+ const refreshStandardShell = (
258
+ state: StandardShellState,
259
+ theme: RenderTheme<TParams, TDetails, TState>,
260
+ context: RenderContext<TParams, TDetails, TState>,
261
+ ): Box => {
262
+ const background = context.isPartial
263
+ ? (text: string) => theme.bg("toolPendingBg", text)
264
+ : context.isError
265
+ ? (text: string) => theme.bg("toolErrorBg", text)
266
+ : (text: string) => theme.bg("toolSuccessBg", text);
267
+ const shell = state.shell ?? new Box(1, 1, background);
268
+ state.shell = shell;
269
+ shell.setBgFn(background);
270
+ shell.clear();
271
+ if (state.call) shell.addChild(state.call);
272
+ if (state.result) shell.addChild(state.result);
273
+ return shell;
274
+ };
275
+
276
+ return {
277
+ ...original,
278
+ renderShell: "self",
279
+
280
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
281
+ return definitionFor(ctx.cwd).execute(toolCallId, params, signal, onUpdate, ctx);
282
+ },
283
+
284
+ renderCall(
285
+ args: RenderArgs<TParams, TDetails, TState>,
286
+ theme: RenderTheme<TParams, TDetails, TState>,
287
+ context: RenderContext<TParams, TDetails, TState>,
288
+ ) {
289
+ rememberCalmToolRow(context.state as object, context.invalidate);
290
+ if (exportRendering) return originalRenderCall(args, theme, context);
291
+ if (roPresentationHides("assistant-tool-call")) return new Container();
292
+ if (originalSelfShell) return originalRenderCall(args, theme, context);
293
+
294
+ const state = shellStateFor(context);
295
+ state.call = originalRenderCall(args, theme, {
296
+ ...context,
297
+ lastComponent: state.call,
298
+ });
299
+ return refreshStandardShell(state, theme, context);
300
+ },
301
+
302
+ renderResult(
303
+ result: RenderResult<TParams, TDetails, TState>,
304
+ options: ToolRenderResultOptions,
305
+ theme: RenderTheme<TParams, TDetails, TState>,
306
+ context: RenderContext<TParams, TDetails, TState>,
307
+ ) {
308
+ rememberCalmToolRow(context.state as object, context.invalidate);
309
+ if (exportRendering) return originalRenderResult(result, options, theme, context);
310
+ if (roPresentationHides("tool-result")) return new Container();
311
+ if (originalSelfShell) return originalRenderResult(result, options, theme, context);
312
+
313
+ const state = shellStateFor(context);
314
+ state.result = originalRenderResult(result, options, theme, {
315
+ ...context,
316
+ lastComponent: state.result,
317
+ });
318
+ refreshStandardShell(state, theme, context);
319
+ return new Container();
320
+ },
321
+ };
322
+ }
323
+
324
+ // Each wrapBuiltIn() call below has its own concrete TParams/TDetails/TState; the
325
+ // array holding all seven has no single sound instantiation, so it is typed the same
326
+ // way Pi's own ToolDefinition consumers erase this (any, any, any).
327
+ const wrappedBuiltIns: ToolDefinition<any, any, any>[] = [
328
+ wrapBuiltIn(createReadToolDefinition),
329
+ wrapBuiltIn(createBashToolDefinition),
330
+ wrapBuiltIn(createEditToolDefinition),
331
+ wrapBuiltIn(createWriteToolDefinition),
332
+ wrapBuiltIn(createGrepToolDefinition),
333
+ wrapBuiltIn(createFindToolDefinition),
334
+ wrapBuiltIn(createLsToolDefinition),
335
+ ];
336
+
337
+ // True once this extension has handled built-in registration for its lifetime:
338
+ // either all seven synchronously at load, or only the uncontested subset during
339
+ // first activation.
340
+ let builtInsRegistered = false;
341
+
342
+ // Gate on Ró already being on at load time. This must stay synchronous and
343
+ // unconditional here (see file header): a foreign-claim check is not reachable at
344
+ // this point, while deferral would make restored rows capture the wrong definition.
345
+ // A Ró-off session or reload registers nothing and creates no collision exposure.
346
+ if (loadCalmPreference()) {
347
+ for (const tool of wrappedBuiltIns) pi.registerTool(tool);
348
+ builtInsRegistered = true;
349
+ }
350
+
351
+ // Which of the 7 built-ins are currently owned by a different, non-builtin
352
+ // extension. Only safe to call once every extension has finished loading (see file
353
+ // header); never call this during the smidja's own synchronous execution above.
354
+ function contestedBuiltIns(): ToolDefinition<any, any, any>[] {
355
+ let registered: ToolInfo[];
356
+ try {
357
+ registered = pi.getAllTools();
358
+ } catch (error) {
359
+ const reason = error instanceof Error ? error.message : String(error);
360
+ console.error(`Brokk Ró: built-in ownership check unavailable, claiming every built-in unconditionally. ${reason}`);
361
+ return [];
362
+ }
363
+ return wrappedBuiltIns.filter((tool) => {
364
+ const owner = registered.find((info) => info.name === tool.name)?.sourceInfo;
365
+ return owner !== undefined && owner.source !== "builtin" && realpathOrSelf(owner.path) !== extensionRealFile;
366
+ });
367
+ }
368
+
369
+ // The first time Ró turns on in a session that started off, claim every
370
+ // uncontested built-in and leave each contested tool and its owning extension
371
+ // untouched. Tell the user which built-in Ró could not take over, since Ró's
372
+ // presentation does not apply to it.
373
+ function activateBuiltInsIfNeeded(ui: ExtensionUIContext): void {
374
+ if (builtInsRegistered) return;
375
+ const contested = contestedBuiltIns();
376
+ const contestedNames = new Set(contested.map((tool) => tool.name));
377
+ for (const tool of wrappedBuiltIns) {
378
+ if (!contestedNames.has(tool.name)) pi.registerTool(tool);
379
+ }
380
+ builtInsRegistered = true;
381
+ if (contested.length === 0) return;
382
+ const names = contested.map((tool) => `"${tool.name}"`).join(", ");
383
+ const plural = contested.length > 1;
384
+ ui.notify(
385
+ `Brokk Ró: the ${names} built-in tool${plural ? "s are" : " is"} already provided by another extension, so Ró may not fully function for ${plural ? "them" : "it"} this session.`,
386
+ "warning",
387
+ );
388
+ for (const tool of contested) {
389
+ console.error(`Brokk Ró: skipped claiming built-in "${tool.name}" because another extension already owns it.`);
390
+ }
391
+ }
392
+
393
+ // Backstop for the one case activateBuiltInsIfNeeded cannot reach: Ró registered
394
+ // unconditionally at load time because it was already on, without any chance to
395
+ // check for a foreign claim first, so it can still silently lose a name to an
396
+ // earlier-loaded extension. Runs on every session_start reason because a reload
397
+ // rebuilds every extension's registrations from scratch, so last session's clean
398
+ // bill of health does not carry over.
399
+ function reportBuiltInLosses(): void {
400
+ if (!builtInsRegistered) return;
401
+ let registered: ToolInfo[];
402
+ try {
403
+ registered = pi.getAllTools();
404
+ } catch (error) {
405
+ const reason = error instanceof Error ? error.message : String(error);
406
+ console.error(`Brokk Ró: built-in ownership check unavailable. ${reason}`);
407
+ return;
408
+ }
409
+ for (const tool of wrappedBuiltIns) {
410
+ const owner = registered.find((info) => info.name === tool.name)?.sourceInfo;
411
+ if (owner && owner.source !== "builtin" && realpathOrSelf(owner.path) !== extensionRealFile) {
412
+ console.error(
413
+ `Brokk Ró: another extension (${owner.path}) also claimed the built-in "${tool.name}" tool and won; Ró's presentation for it is unavailable this session.`,
414
+ );
415
+ }
416
+ }
417
+ }
418
+
419
+ pi.on("session_start", (_event, ctx) => {
420
+ reportBuiltInLosses();
421
+ calmToolRowRepaints.clear();
422
+ exportRendering = false;
423
+ setCalmPresentation(loadCalmPreference());
424
+ setCalmStockExportRendering(false);
425
+ publishPresentationState();
426
+ agentRunActive = false;
427
+ workingShipShown = false;
428
+ // A genuine new session lifetime starts the boat at the normal initial position.
429
+ workingShipAnimation.reset();
430
+ applyWorkingPresentation(ctx.ui, true);
431
+ ctx.ui.setHiddenThinkingLabel(calmPresentationIsActive() ? "" : undefined);
432
+ ctx.ui.setStatus("brokk-ro", undefined);
433
+ removeTerminalInputHandler?.();
434
+ removeTerminalInputHandler = ctx.ui.onTerminalInput((data) => {
435
+ if (!getKeybindings().matches(data, "tui.input.submit")) return undefined;
436
+
437
+ const input = ctx.ui.getEditorText().trim();
438
+ if (
439
+ input !== "/share" &&
440
+ input !== "/export" &&
441
+ !input.startsWith("/export ")
442
+ ) {
443
+ return undefined;
444
+ }
445
+
446
+ exportRendering = true;
447
+ setCalmStockExportRendering(true);
448
+ publishPresentationState();
449
+ setTimeout(() => {
450
+ exportRendering = false;
451
+ setCalmStockExportRendering(false);
452
+ publishPresentationState();
453
+ // Repaint the rows Ró presents, never the whole transcript. Pi's export
454
+ // prints "Session exported to: <path>" immediately before this runs, and
455
+ // since Pi 0.83.0 setToolsExpanded() emits its own status line; consecutive
456
+ // status lines coalesce, so a tools-expanded round-trip here silently
457
+ // overwrote the confirmation and left the Allfather no record of where their
458
+ // export landed. Invalidating the rows individually repaints the same
459
+ // content with no status line of its own, and setStatus adds the redraw the
460
+ // rows that consult Ró live in render(), such as operational user rows,
461
+ // need without appending anything to the transcript.
462
+ repaintCalmToolRows();
463
+ ctx.ui.setStatus("brokk-ro", undefined);
464
+ }, 0);
465
+ return undefined;
466
+ });
467
+ });
468
+
469
+ pi.on("agent_start", (_event, ctx) => {
470
+ agentRunActive = true;
471
+ applyWorkingPresentation(ctx.ui);
472
+ });
473
+
474
+ // agent_settled is emitted from a finally block, so it also covers abort and failure.
475
+ pi.on("agent_settled", (_event, ctx) => {
476
+ agentRunActive = false;
477
+ applyWorkingPresentation(ctx.ui);
478
+ });
479
+
480
+ pi.on("session_shutdown", (_event, ctx) => {
481
+ agentRunActive = false;
482
+ applyWorkingPresentation(ctx.ui);
483
+ });
484
+
485
+ pi.registerCommand("ro", {
486
+ description: "Toggle Brokk's supported conversation-only transcript presentation.",
487
+ handler: async (_args, ctx) => {
488
+ const active = !calmPresentationIsActive();
489
+ persistCalmPreference(active);
490
+ setCalmPresentation(active);
491
+ if (active) activateBuiltInsIfNeeded(ctx.ui);
492
+ publishPresentationState();
493
+ applyWorkingPresentation(ctx.ui, true);
494
+ // Pi re-runs every assistant row's layout from this call even when the label is
495
+ // unchanged, which is what makes a toggle apply to rows already on screen.
496
+ ctx.ui.setHiddenThinkingLabel(active ? "" : undefined);
497
+ ctx.ui.setStatus("brokk-ro", undefined);
498
+
499
+ const expanded = ctx.ui.getToolsExpanded();
500
+ ctx.ui.setToolsExpanded(!expanded);
501
+ ctx.ui.setToolsExpanded(expanded);
502
+ },
503
+ });
504
+ }