@hachej/boring-workspace 0.1.0

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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/dist/CodeEditor-DQqOn4xz.js +266 -0
  4. package/dist/CommandPalette-aM61U-b0.js +5229 -0
  5. package/dist/FileTree-DRq_bfue.js +245 -0
  6. package/dist/MarkdownEditor-DjiHxnRv.js +349 -0
  7. package/dist/WorkspaceLoadingState-By0dZoPD.js +568 -0
  8. package/dist/agent-tool-NvxKfist.d.ts +28 -0
  9. package/dist/app-front.d.ts +485 -0
  10. package/dist/app-front.js +452 -0
  11. package/dist/app-server.d.ts +53 -0
  12. package/dist/app-server.js +769 -0
  13. package/dist/bootstrapServer-BRUqUpVW.d.ts +66 -0
  14. package/dist/boring-workspace.css +1 -0
  15. package/dist/charts.d.ts +114 -0
  16. package/dist/charts.js +143 -0
  17. package/dist/events.d.ts +178 -0
  18. package/dist/events.js +88 -0
  19. package/dist/explorer-DtLUnuah.d.ts +129 -0
  20. package/dist/panel-DnvDNQac.js +6 -0
  21. package/dist/server.d.ts +84 -0
  22. package/dist/server.js +811 -0
  23. package/dist/shared.d.ts +113 -0
  24. package/dist/shared.js +11 -0
  25. package/dist/testing-e2e.d.ts +68 -0
  26. package/dist/testing-e2e.js +45 -0
  27. package/dist/testing.d.ts +464 -0
  28. package/dist/testing.js +10984 -0
  29. package/dist/utils-B6yFEsav.js +8 -0
  30. package/dist/workspace.css +5780 -0
  31. package/dist/workspace.d.ts +2119 -0
  32. package/dist/workspace.js +1884 -0
  33. package/docs/INTERFACES.md +58 -0
  34. package/docs/PLUGIN_STRUCTURE.md +162 -0
  35. package/docs/README.md +19 -0
  36. package/docs/bridge.md +135 -0
  37. package/docs/panels.md +102 -0
  38. package/docs/plans/GENERIC_EXPLORER_PLUGIN_PLAN.md +455 -0
  39. package/docs/plans/MACRO_PLUGIN_GENERIC_HELPERS_AUDIT.md +962 -0
  40. package/docs/plans/PLUGIN_OUTPUTS_ISOLATION_PLAN.md +301 -0
  41. package/docs/plans/README.md +9 -0
  42. package/docs/plans/UI_BRIDGE_OWNERSHIP_REFACTOR.md +303 -0
  43. package/docs/plans/archive/CODE_OWNERSHIP_CLEANUP_PLAN.md +387 -0
  44. package/docs/plans/archive/COMMAND_PALETTE_REGISTRY.md +814 -0
  45. package/docs/plans/archive/DECLARATIVE_LAYOUT_MIGRATION.md +277 -0
  46. package/docs/plans/archive/PLUGIN_MODEL.md +3674 -0
  47. package/docs/plans/archive/SRC_FOLDER_REORG_PLAN.md +307 -0
  48. package/docs/plans/archive/UNIFIED_EVENT_BUS.md +647 -0
  49. package/docs/plans/archive/WORKSPACE_V2_PLAN.md +2489 -0
  50. package/docs/plugins.md +158 -0
  51. package/package.json +164 -0
@@ -0,0 +1,3674 @@
1
+ # Workspace plugin model
2
+
3
+ > **Historical plan.** This file records the plugin model design process. Use
4
+ > `../../INTERFACES.md` and `../../PLUGIN_STRUCTURE.md` for the current concise
5
+ > contract reference.
6
+
7
+ **Status:** v7.7 — round-7 governance: ChatPanel via dependency injection (NOT plugin); baseline protocol uses `git worktree` (NOT `git stash`); systemPrompt ordering reaffirmed (bootstrap first, createAgentApp second); excludeDefaults semantics corrected (UI off; tools stay); j9p7.30 sed→explicit Edits; bead-level dep + path fixes integrated.
8
+
9
+ > **2026-04-30 routing update:** any later references in this historical plan
10
+ > to `PanelConfig.filePatterns`, `fileFallback`, or `PanelRegistry.resolve`
11
+ > are superseded by generic `surface-resolver` outputs. Workspace core owns
12
+ > only the resolver registry and `openSurface` dispatch. Filesystem path/glob
13
+ > mapping lives in `plugins/filesystemPlugin/surfaceResolver.ts`; data catalog
14
+ > row-to-visualization mapping lives in `plugins/dataCatalogPlugin`.
15
+
16
+ Prior status (v7.6): round-5 review patches — Step 0 sequencing (move workspace into v7.5 layout BEFORE Phase 1, not after); split plugin entrypoints (index.ts client + server.ts per plugin); strict type-only imports for cross-folder Plugin refs; cleanup pack (uiBridge dedup, EmptyFilePanel relocation, A/B parallelism tightened, TL;DR scrub, tsconfig excludes, pi-tools-migration catch-up). **Meta-rule: when files move, they go DIRECTLY to final v7.6 destinations — no intermediate placements.**
17
+
18
+ > **Factory pattern:** Plugins may be exposed as factories when they need
19
+ > runtime config (e.g. macro's `makeMacroServerPlugin()`). v7.0 dropped the
20
+ > filesystemPlugin factory because the plugin no longer carries `agentTools`
21
+ > — it's UI-only (panels + catalog) and constructible at module load.
22
+ > Domain plugins with runtime deps (DB clients, etc.) still use the factory
23
+ > shape; the `Plugin` contract is unchanged.
24
+
25
+ **Owners:** workspace
26
+ **Last updated:** 2026-04-29
27
+
28
+ ## TL;DR
29
+
30
+ A `Plugin` is a tagged bag of contributions for the workspace's
31
+ existing per-type registries (panels, commands, catalogs, agentTools).
32
+ Hosts compose plugins; the workspace fans them into their respective
33
+ registries and that's it. No lifecycle hooks, no dep graph, no
34
+ ordering field, no route plumbing on the contract — those are either
35
+ unused in Phase 1 or solved by simpler primitives (factories, npm
36
+ sub-path exports, `app.register(...)`).
37
+
38
+ The model unifies five fragmented host-wiring APIs into one. The
39
+ boring-macro-v2 migration is the acceptance test — ~260 LOC of glue
40
+ + inlined UI bridge collapse to ~30 LOC of plugin definition.
41
+
42
+ ## Scope of this plan
43
+
44
+ **Phase 1 (this PR's scope):**
45
+
46
+ - The `Plugin` contract + `definePlugin` factory
47
+ - Subscribe-aware registries (Catalog new; retrofit Command + Panel)
48
+ - One default plugin: `filesystemPlugin` (UI-only — panels + catalog; no agentTools per v7.0+)
49
+ - `<WorkspaceProvider plugins={…}>` and
50
+ `createWorkspaceAgentApp({ plugins })` entry points
51
+ - File ops shared bundle in `@boring/agent`; filesystemPlugin
52
+ references the same bundle (single source of truth, standalone
53
+ agent stays a real coding agent)
54
+ - UI bridge moves from `@boring/agent` to `@boring/workspace`
55
+ - Path-aware (not basename-only) file-pattern panel resolver
56
+ - `WorkbenchLeftPane` becomes registry-driven so `excludeDefaults`
57
+ actually removes default tabs (Files / Data)
58
+ - `SurfaceShell` fallback chain replaced with `EmptyFilePanel` so
59
+ ghost tabs don't appear when registry resolution misses
60
+ - `<CommandPalette />` consumes catalogs via the registry
61
+ - Polymorphic Recent (entries tagged with their source catalog)
62
+ - `<ChatCenteredShell />` migrated off legacy `data` / `extraPanels`
63
+ props (KEEPS `chatSuggestions` prop — it's app config, not a
64
+ registry contribution; see §"Why no chatSuggestions on the
65
+ contract")
66
+ - boring-macro-v2 migrated to a single inline plugin
67
+
68
+ **Phase 2 (sketched, separate PR):** npm-installable plugins via
69
+ package.json sub-path exports (`./client`, `./server`); pi loader
70
+ extension to read the wider `Plugin` shape; `/api/v1/plugins`
71
+ discovery endpoint for system-prompt augmentation; generic
72
+ `search_catalog(id, q)` agent tool; workbench data-tab catalog
73
+ selector.
74
+
75
+ **Phase 3 (longer-term):** agent-authored plugins; hot-reload;
76
+ sandboxing; capability flags; if/when needed: `dependsOn`,
77
+ `onMount`, etc., re-introduced as the dep graph stops being
78
+ trivial.
79
+
80
+ ## Problem
81
+
82
+ Boring-macro-v2 — the realest "child app" we have — contributes
83
+ five distinct kinds of things and wires them through five different
84
+ APIs:
85
+
86
+ | Contribution | Macro's instance | Today's wiring |
87
+ |---|---|---|
88
+ | Panels | `chart-canvas`, `deck` | `<WorkspaceProvider panels={…}>` |
89
+ | Catalogs | Macro series catalog (87k FRED series) | `<ChatCenteredShell data={DataPaneConfig}>` |
90
+ | Agent tools | `execute_sql`, `macro_search`, `get_series_data`, `persist_derived_series` | `createAgentApp({ extraTools })` |
91
+ | Server routes | `registerMacroRoutes` (takes `{ clickhouse, deckRoot }`) | `app.register(registerMacroRoutes, { … })` |
92
+ | Commands | (none today) | (would be) `useCommandRegistry().registerCommand` |
93
+
94
+ Plus chat suggestions (a 6th but UX-bounded thing) and ~150 LOC of
95
+ `@boring/workspace`'s UI bridge code inlined into
96
+ `apps/boring-macro-v2/src/server/uiBridge.ts` (still present at 9.3
97
+ KB on disk — confirmed). The workspace package's server export now
98
+ builds; the inlined copy is dead weight that this plan deletes.
99
+
100
+ The pi-coding-agent already has a plugin loader
101
+ (`packages/agent/src/server/harness/pi-coding-agent/pluginLoader.ts`)
102
+ that handles **agent tools only** — flat `default: AgentTool[]` /
103
+ `tools: AgentTool[]` exports from `.pi/extensions/`,
104
+ `~/.pi/agent/extensions/`, and `node_modules/pi-plugin-*`. The
105
+ discovery infrastructure exists; the plugin shape is too narrow.
106
+
107
+ ## Goal
108
+
109
+ 1. **One Plugin contract.** Every contribution type that goes into
110
+ an aggregating registry fits into one declarative object.
111
+ 2. **Workspace orchestrates.** Bootstrap, file-pattern resolution,
112
+ `excludeDefaults` opt-out all live in `@boring/workspace`.
113
+ 3. **Composable.** File-pattern-driven panel resolution lets domain
114
+ plugins bind their panes to their domain paths;
115
+ late-wins-on-id lets hosts override anything at the abstraction
116
+ level above.
117
+ 4. **Honest boundaries.** Substrate is core (HTTP plumbing,
118
+ registries, bridge); capabilities are plugins (file ops, data
119
+ catalogs, macro). Defaults auto-mount; opt-outs are explicit and
120
+ really take effect, including UI tabs.
121
+ 5. **Forward-compatible.** Phase 1's inline path doesn't paint the
122
+ model into a corner — Phase 2's npm + pi-loader extensions slot
123
+ in additively.
124
+
125
+ ## Decisions log (locked unless explicitly revisited)
126
+
127
+ The key architectural choices, summarized for fresh-eyes reviewers.
128
+ Each links to the changelog entry that locked it.
129
+
130
+ | Decision | Rationale | Locked at |
131
+ |---|---|---|
132
+ | **Pure-data Plugin contract (no lifecycle)** | All Phase 1 plugins are React-component-based or factory-injected. `onMount`/cleanup adds API surface that no Phase 1 plugin uses. | v6 |
133
+ | **No `dependsOn` for plugin deps** | Phase 1 has 1 declared dep total (macro → filesystem). Topo sort + dep graph not worth the contract surface; array order suffices. | v6 |
134
+ | **Routes off the Plugin contract** | Routes are HTTP infrastructure, not registry contributions. Mixing blurs identity AND lies about lifecycle (Fastify routes are non-unregisterable). | v6 |
135
+ | **`chatSuggestions` stays as a `<ChatCenteredShell>` prop** | UX cap (~6 cards) forces hosts to curate. Registry aggregation adds nothing useful. | v6.3 |
136
+ | **Filesystem tools are harness substrate, not plugin contributions** | "Should agent have file tools?" (harness) vs "Should UI have file tree?" (workspace) are distinct concerns at different layers. | v7.0 |
137
+ | **Reserved tool names** (`read`/`write`/`edit`/`find`/`grep`/`ls`/`bash`/`executeIsolatedCode`) | Harness owns these names. Plugins use domain prefixes (`macro_*`). Dev-warn on collision; no hard reject (some override IS intended). | v7.1 |
138
+ | **Single-pass mount; array order > topo sort** | Defaults prepended → register first; user plugins follow; late-wins-on-id handles overrides. | v6 |
139
+ | **Path-aware micromatch resolver with `(segments × 10) + non-wildcard chars` specificity** | Domain patterns (`deck/**/*.md`) must beat generic (`**/*.md`). Concrete formula → testable. | v6 |
140
+ | **Polymorphic Recent (catalogs + commands)** | VS Code/Raycast/Linear all show recent commands. Catalog-only would be a regression. | v6.3 |
141
+ | **`disableDefaultFileTools` (harness) vs `excludeDefaults` (workspace)** | Two switches at two layers. Conflating them was over-engineering. | v7.0 |
142
+ | **Per-plugin React error boundaries** | A plugin's panel can't crash the workspace shell. Failure isolation per pluginId. | v7.2 |
143
+ | **`Plugin.systemPrompt?: string` for LLM context augmentation** | Plugins frame their own domain to the LLM. macro tells the model "FRED database, use macro_*." Reduces host-author burden. | v7.2 |
144
+ | **`<PluginInspector />` ships in Phase 1 (DEV-only)** | Plugin authors debug "why didn't my command appear?" visually instead of via React DevTools. ~50 LOC, zero production cost. | v7.2 (un-cut from v6) |
145
+
146
+ If a reviewer wants to re-litigate a locked decision, they should
147
+ (1) read the linked changelog entry first, then (2) propose a
148
+ revision against the rationale documented there. Don't relitigate
149
+ from first principles — we've been there.
150
+
151
+ ## Non-goals
152
+
153
+ - A plugin marketplace, signing, trust model, or capability sandbox.
154
+ - Hot-reload of plugins at runtime (Phase 1 — server boot loads,
155
+ client boot loads; restart to add/remove). **Server-side caveat:**
156
+ Fastify routes are boot-time-only and cannot be unregistered;
157
+ this is one of the reasons routes are NOT on the Plugin contract
158
+ in Phase 1.
159
+ - Cross-plugin dependency declarations. Plugins coordinate
160
+ implicitly through shared registries (panel id namespace, catalog
161
+ registry); they don't declare "I require plugin X." If/when a
162
+ real dep graph appears, add `dependsOn` then.
163
+ - Plugin lifecycle hooks (`onMount` / `onUnmount`). All Phase 1
164
+ plugins are pure declarative bags; if/when a plugin needs
165
+ imperative setup, add the hook then.
166
+ - Numeric mount ordering (`Plugin.order`). Array order does the work
167
+ — defaults register first, host plugins after, late-wins-on-id
168
+ for collisions.
169
+ - Replacing the agent's runtime tools (`bash`,
170
+ `execute_isolated_code`). Those are harness-level, not
171
+ workspace-level. Stay in `@boring/agent`.
172
+ - Per-contribution dependency declarations.
173
+ - Cross-environment dynamic discovery in Phase 1 (the discovery
174
+ endpoint is Phase 2).
175
+ - Inline plugin sandboxing (full host privileges; spec is a
176
+ structuring tool, not a security boundary).
177
+ - Routes as a Plugin contract field. Plugin distributors ship
178
+ routes via npm sub-path exports; hosts wire routes via
179
+ `app.register(routePlugin, opts)`. See §"Distribution".
180
+ - Chat suggestions as a Plugin contribution. UX caps at ~6 cards →
181
+ hosts curate, registry aggregation is useless. Stays as a
182
+ `<ChatCenteredShell>` prop.
183
+
184
+ ## Design
185
+
186
+ ### The Plugin contract — six fields, all data
187
+
188
+ ```ts
189
+ // @boring/workspace/shared/plugin.ts
190
+ import type { AgentTool } from "@boring/agent/shared"
191
+ import type { ExplorerAdapter, ExplorerRow } from "@boring/workspace"
192
+
193
+ export interface Plugin {
194
+ /** Stable id. Convention: package or app name. Used for
195
+ * late-wins-on-id, debug provenance. */
196
+ id: string
197
+
198
+ /** Human-readable label (defaults to id). */
199
+ label?: string
200
+
201
+ /** Optional context prepended to the agent's system prompt at boot.
202
+ * Use to tell the LLM what your plugin's domain is and when its
203
+ * agent tools apply. Concatenated across all registered plugins
204
+ * (in registration order) and joined with newlines. Plain Markdown.
205
+ * ~200-500 chars per plugin recommended; longer eats context window.
206
+ * v7.2 addition. */
207
+ systemPrompt?: string
208
+
209
+ // Aggregating registries — every field fans into ONE registry that
210
+ // genuinely benefits from cross-plugin merging.
211
+ panels?: PanelConfig[]
212
+ commands?: CommandConfig[]
213
+ catalogs?: CatalogConfig[]
214
+
215
+ // Server-only contribution.
216
+ agentTools?: AgentTool[]
217
+ }
218
+ ```
219
+
220
+ That's the entire contract. No lifecycle. No deps. No ordering. No
221
+ routes. No chat suggestions. No mount context. Just data.
222
+
223
+ ### Concrete contribution types
224
+
225
+ ```ts
226
+ // PanelConfig — already a discriminated union in
227
+ // packages/workspace/src/registry/types.ts. v6 PRESERVES the
228
+ // existing shape; the plugin model only ADDS fields:
229
+ // - 'left-tab' | 'right-tab' to placement (registry-driven tabs)
230
+ // - pluginId?: string (auto-set provenance)
231
+ // Existing fields kept verbatim: SyncPanelConfig vs LazyPanelConfig
232
+ // discriminated by `lazy: true | false`, requiresCapabilities,
233
+ // essential, chromeless, source: 'builtin' | 'app',
234
+ // definePanel<T>() factory.
235
+
236
+ interface PanelConfigBase {
237
+ id: string
238
+ title: string
239
+ icon?: ComponentType<{ className?: string }>
240
+ placement?: "left" | "center" | "right" | "bottom" | "left-tab" | "right-tab"
241
+ filePatterns?: string[] // path-aware micromatch (Step 2d)
242
+ requiresCapabilities?: string[]
243
+ essential?: boolean
244
+ source?: "builtin" | "app"
245
+ chromeless?: boolean
246
+ pluginId?: string // auto-set by registry
247
+ }
248
+ interface SyncPanelConfig<T = unknown> extends PanelConfigBase {
249
+ component: ComponentType<PaneProps<T>>
250
+ lazy?: false
251
+ }
252
+ interface LazyPanelConfig<T = unknown> extends PanelConfigBase {
253
+ component: () => Promise<{ default: ComponentType<PaneProps<T>> }>
254
+ lazy: true
255
+ }
256
+ type PanelConfig<T = unknown> = SyncPanelConfig<T> | LazyPanelConfig<T>
257
+
258
+ // CommandConfig — already exists; only adds pluginId.
259
+ type CommandConfig = {
260
+ id: string
261
+ title: string
262
+ shortcut?: string
263
+ when?: () => boolean
264
+ run: () => void
265
+ pluginId?: string
266
+ }
267
+
268
+ // CatalogConfig — new.
269
+ type CatalogConfig = {
270
+ id: string
271
+ label: string
272
+ adapter: ExplorerAdapter // existing DataExplorer type
273
+ onSelect: (row: ExplorerRow) => void
274
+ pluginId?: string // auto-set by registry
275
+ }
276
+ // NOTE: v5/v6 had a `recentKind?: string` field intended for Recent
277
+ // fallback when the source catalog is unregistered. The spec
278
+ // settled on "drop orphan entries" — so recentKind would be set
279
+ // but never read. Cut from v6.1. Future Phase 2 "filter Recent by
280
+ // kind" UX can add it back as a non-breaking optional field.
281
+
282
+ // AgentTool — already exists in @boring/agent/shared
283
+ ```
284
+
285
+ `pluginId` is set automatically when contributions are fanned into
286
+ registries; plugin code never assigns it. Late-wins-on-id collisions
287
+ log a dev-mode warning identifying both contributors.
288
+
289
+ ### PanelConfig roles — three uses, one type (v7.3)
290
+
291
+ The `PanelConfig` type unifies three conceptually-distinct
292
+ contributions disambiguated by `placement`. v7.x keeps them in one
293
+ type for impl simplicity; **Phase 2 may go to a discriminated union**
294
+ (see Phase 2 sketch). Plugin authors should treat the three roles
295
+ as semantically separate even though they share a TypeScript type.
296
+
297
+ #### 1. Sidebar tab — `placement: 'left-tab' | 'right-tab'`
298
+
299
+ Persistent tab in `WorkbenchLeftPane` (and, reserved, the
300
+ not-yet-built `WorkbenchRightPane`). Always-on; user clicks to
301
+ activate; the tab content renders inside the sidebar pane.
302
+
303
+ **Required fields:** `id`, `title`, `component`, `placement`.
304
+ **Should NOT set:** `filePatterns` (sidebar tabs aren't file-routed —
305
+ they're navigation surfaces).
306
+ **Examples:**
307
+ - `filesystemPlugin`'s `files` tab (FileTreePanel)
308
+ - macroPlugin's `macro-series` tab (DataExplorer with macroAdapter)
309
+
310
+ #### 2. Workbench pane — `placement: 'center'`
311
+
312
+ Ephemeral content opened via `surface.openFile(path)` (file-pattern
313
+ resolver picks the panel) or `surface.openPanel({component, params})`
314
+ (host-author picks explicitly). Lives as a tab in the dockview
315
+ center area. Closes when user closes the tab.
316
+
317
+ **Required fields:** `id`, `title`, `component`, `placement: 'center'`.
318
+ **Often set:** `filePatterns: string[]` to drive auto-routing on
319
+ `openFile()`. Without filePatterns, the panel can ONLY be opened
320
+ explicitly via `openPanel({component: '<id>'})`.
321
+ **Examples:**
322
+ - `filesystemPlugin`'s `code-editor` (`filePatterns: ["**/*.ts", ...]`)
323
+ - `filesystemPlugin`'s `markdown-editor` (`filePatterns: ["**/*.md"]`)
324
+ - macroPlugin's `chart-canvas` (no filePatterns — opened explicitly
325
+ by macroSeriesPanel's `onActivate`)
326
+ - macroPlugin's `deck` (`filePatterns: ["deck/**/*.md"]` — beats the
327
+ generic markdown editor for deck files via specificity scoring)
328
+
329
+ #### 3. Bottom dock — `placement: 'bottom'`
330
+
331
+ Fixed-position panel below the workbench center area. Persistent
332
+ across tab changes. Suitable for terminals, consoles, log viewers.
333
+
334
+ **Required fields:** `id`, `title`, `component`, `placement: 'bottom'`.
335
+ **Should NOT set:** `filePatterns`.
336
+ **Examples (none ship in Phase 1):** a future `terminalPlugin`
337
+ might contribute a bottom dock.
338
+
339
+ #### Reserved / future placements
340
+
341
+ - `'right-tab'` — symmetric to `'left-tab'` but no Phase 1 component
342
+ consumes it (no `WorkbenchRightPane` exists). Keep the union member
343
+ for plugin authors who anticipate the right pane shipping.
344
+ - `'left'` / `'right'` (without `-tab` suffix) — legacy placements
345
+ for non-tabbed left/right docks. Existing in current `PanelConfig`
346
+ union; Phase 1 plugins don't use them.
347
+
348
+ #### Future contribution type — `Plugin.pages?: PageConfig[]`
349
+
350
+ A "page" is a **full-viewport** view that replaces the chat-centered
351
+ shell entirely. Conceptually similar to VS Code's `viewsContainers`
352
+ (activity-bar entries). Use cases: a "Settings" page, a "Reports"
353
+ dashboard, a multi-step "Onboarding" flow.
354
+
355
+ ```ts
356
+ interface PageConfig {
357
+ id: string // 'settings', 'reports'
358
+ title: string
359
+ icon: ComponentType<{ className?: string }>
360
+ route?: string // '/settings' — optional URL routing
361
+ component: ComponentType // mounts at viewport level
362
+ }
363
+ ```
364
+
365
+ **Out of scope for Phase 1** — no boring-* host has a real page need.
366
+ Phase 2/3 if/when the first plugin author proposes it.
367
+
368
+ #### Why one type today, possibly split later
369
+
370
+ The three roles share enough structure (id, title, component,
371
+ placement, source provenance) that splitting up-front would just
372
+ add fields without enforcing meaningful invariants — `filePatterns`
373
+ is the only role-specific field, and runtime validation
374
+ (`PanelRegistry.resolve` only consults `filePatterns` for `'center'`
375
+ panels) handles it correctly even when authors set it on the wrong
376
+ placement.
377
+
378
+ The split becomes worth it when:
379
+ - A second role-specific field appears (e.g., sidebar tabs gain
380
+ `tabOrder`, bottom docks gain `defaultHeight`)
381
+ - Plugin authors hit type-confusion bugs in practice
382
+
383
+ Phase 2's discriminated-union refactor (`SidebarTabPanel | WorkbenchPane
384
+ | BottomDock`) is a 2-3 hour change once we decide to do it.
385
+ TypeScript narrowing makes the consumer-side ergonomic
386
+ (`registry.resolve()` filters by kind first; sidebar-tab consumers
387
+ filter by kind too).
388
+
389
+ ### `definePlugin(spec)` and the factory pattern
390
+
391
+ Two distribution shapes for inline plugins:
392
+
393
+ **Stateless plugins** — `definePlugin({ ... })` directly:
394
+
395
+ ```ts
396
+ import { definePlugin } from "@boring/workspace"
397
+
398
+ export const formattingPlugin = definePlugin({
399
+ id: "formatting",
400
+ label: "Formatting",
401
+ commands: [{ id: "format.json", title: "Format JSON", run: () => /*…*/ }],
402
+ })
403
+ ```
404
+
405
+ **Stateful plugins (or plugins with server deps)** — wrap in a
406
+ factory function. The factory captures runtime config and returns a
407
+ Plugin:
408
+
409
+ ```ts
410
+ export const makeMacroPlugin = (): Plugin =>
411
+ definePlugin({
412
+ id: "boring-macro",
413
+ label: "Macro",
414
+ panels: [chartCanvasPanel, deckPanel],
415
+ catalogs: [seriesCatalog],
416
+ agentTools: macroAgentTools,
417
+ })
418
+ ```
419
+
420
+ For plugins whose **server-side dependencies** (DB clients,
421
+ filesystem roots, etc.) need to be constructed at boot, the deps
422
+ DON'T enter the Plugin shape — they go to the route handlers the
423
+ host wires separately. See §"Where do routes go?" below.
424
+
425
+ ### What `definePlugin` validates
426
+
427
+ Validation runs synchronously at the `definePlugin({...})` call.
428
+ Throwing means the plugin module fails to import — the host's build
429
+ or dev server reports the error with a clear stack trace.
430
+
431
+ Checks:
432
+
433
+ 1. `id` is a non-empty string. (Convention: kebab-case package or
434
+ app name; not enforced.)
435
+ 2. Within `panels`: each `id` is unique within this plugin; each
436
+ `placement` is one of the allowed values; if `lazy: true` the
437
+ `component` is a function returning a Promise; if `lazy:
438
+ false`/absent the `component` is a `ComponentType`.
439
+ 3. Within `commands`: each `id` is unique within this plugin; `run`
440
+ is a function.
441
+ 4. Within `catalogs`: each `id` is unique within this plugin;
442
+ `adapter.search` is a function; `onSelect` is a function.
443
+ 5. Within `agentTools`: delegates to `validateAgentTool` (which
444
+ re-exports `validateTool` from `@boring/agent/shared`). Each
445
+ tool has non-empty `name`, `description`, `parameters` object,
446
+ and `execute` function.
447
+
448
+ Cross-plugin id collisions (same panel id from two different
449
+ plugins) are NOT errors — they're handled by late-wins-on-id at
450
+ registration time, with a dev-mode warning.
451
+
452
+ ```
453
+ PluginValidationError: plugin "boring-macro": catalogs[0].adapter.search
454
+ must be a function (got: undefined)
455
+ ```
456
+
457
+ ### Reserved tool names (v7.1)
458
+
459
+ The harness substrate registers a fixed set of tool names: **`bash`,
460
+ `executeIsolatedCode`, `read`, `write`, `edit`, `find`, `grep`,
461
+ `ls`** (plus any custom non-pi additions made in
462
+ `buildFilesystemAgentTools`). Plugin-contributed `agentTools` MUST
463
+ NOT use these names.
464
+
465
+ **Convention:** plugin-contributed tool names should use a
466
+ domain prefix (e.g., `macro_search`, `macro_execute_sql`,
467
+ `docs_lookup`) so they're unambiguously plugin-scoped, never
468
+ shadowing harness tools.
469
+
470
+ **Why an explicit rule:** today `definePlugin` accepts any name
471
+ without checking against harness names; the existing `mergeTools`
472
+ path can let plugin tools override harness tools by `name`
473
+ collision (`packages/agent/src/server/catalog/mergeTools.ts:30`).
474
+ That override behavior is intentional for the LEGACY pi loader's
475
+ late-wins-on-name semantics, but it's an anti-pattern for the
476
+ v7.1 plugin model where harness ownership is supposed to be
477
+ clean.
478
+
479
+ **Phase 1 enforcement:** dev-mode `console.warn` from `definePlugin`
480
+ when a plugin's `agentTools[].name` matches the substrate set.
481
+ **No hard rejection** — there are real cases (a plugin shipping a
482
+ specialized `read` for encrypted files) where override IS
483
+ intended; the warn gives an audit trail without breaking those.
484
+ The plugin's intent should be explicit in the plugin's docs/README.
485
+
486
+ ### Plugin id collision policy — plugin-level vs contribution-level
487
+
488
+ Two distinct collision types with different semantics:
489
+
490
+ | Collision | Example | Policy | Why |
491
+ |---|---|---|---|
492
+ | Two plugins share `Plugin.id` | Two npm packages both register `id: "boring-macro"` | **Throw at registration** (`PluginError { kind: 'duplicate-id' }`) | Same plugin id = same identity. Two things claiming the same identity is an authoring bug, not a composition pattern. Hosts should rename or remove one. |
493
+ | Two plugins contribute same panel/command/catalog id | macro and superCoder both contribute `id: "code-editor"` | **Late-wins, dev-warn** | Composition pattern: a host plugin overrides a default. Working as intended; warn so the override is traceable. |
494
+
495
+ The plugin-level collision throws because there's no useful
496
+ "override the whole plugin" semantic — if you want to replace a
497
+ plugin, exclude it via `excludeDefaults` and register a different
498
+ one with a different id.
499
+
500
+ ### Build/bundle invariants
501
+
502
+ A plugin module split across `plugin.client.ts` and
503
+ `plugin.server.ts` MUST avoid cross-import. Server modules import
504
+ `node:*`, Fastify types, DB clients; bundling them into the client
505
+ breaks the build (or worse, ships secrets to browsers).
506
+
507
+ Three enforcement strategies (any one suffices):
508
+
509
+ 1. **`"use server"` / `"use client"` directives** at the top of
510
+ each file (RSC-style). Bundlers honor them. This is the
511
+ recommended approach.
512
+ 2. **Per-environment package.json `exports`**: in npm-distributed
513
+ plugins (Phase 2), expose `./client` and `./server` sub-paths
514
+ that point at non-overlapping bundles.
515
+ 3. **Vite/tsup conditional imports**: `import.meta.env.SSR` guards
516
+ server-only requires.
517
+
518
+ Inline plugins (Phase 1) typically use strategy 1 plus a barrel
519
+ `plugin/index.ts` that re-exports only client-safe symbols by
520
+ default; server entry imports `plugin/server.ts` directly when it
521
+ needs the server half.
522
+
523
+ The plan does NOT add static enforcement (e.g., a custom ESLint
524
+ rule). Reviewers + CI build catches the leak. This can change if
525
+ mistakes prove common.
526
+
527
+ ### Plugin testability
528
+
529
+ Plugins are pure data. Testing them at three levels:
530
+
531
+ **Unit — assert contract shape:**
532
+
533
+ ```ts
534
+ import { describe, it, expect } from "vitest"
535
+ import { makeMacroPlugin } from "../plugin"
536
+
537
+ describe("makeMacroPlugin", () => {
538
+ it("registers expected contributions", () => {
539
+ const p = makeMacroPlugin()
540
+ expect(p.id).toBe("boring-macro")
541
+ expect(p.panels?.map((x) => x.id)).toContain("deck")
542
+ expect(p.catalogs?.map((x) => x.id)).toContain("macro-series")
543
+ expect(p.agentTools?.map((t) => t.name)).toEqual(
544
+ expect.arrayContaining(["execute_sql", "macro_search"]),
545
+ )
546
+ })
547
+ })
548
+ ```
549
+
550
+ No registries, no provider, no Fastify — just inspect the returned
551
+ object. `definePlugin` validation already ran at module load.
552
+
553
+ **Integration — render through `<WorkspaceProvider>`:**
554
+
555
+ ```ts
556
+ import { renderHook } from "@testing-library/react"
557
+ import { WorkspaceProvider, useCatalogs } from "@boring/workspace"
558
+
559
+ const wrapper = ({ children }) => (
560
+ <WorkspaceProvider plugins={[makeMacroPlugin()]}>{children}</WorkspaceProvider>
561
+ )
562
+ const { result } = renderHook(() => useCatalogs(), { wrapper })
563
+ expect(result.current.find((c) => c.id === "macro-series")).toBeDefined()
564
+ ```
565
+
566
+ Tests the bootstrap fan-in + registry subscriptions in one shot.
567
+
568
+ **Server — boot a Fastify app with the plugin:**
569
+
570
+ ```ts
571
+ import { createWorkspaceAgentApp } from "@boring/workspace/server"
572
+
573
+ const app = await createWorkspaceAgentApp({
574
+ plugins: [makeMacroPlugin()],
575
+ workspaceRoot: tmpDir,
576
+ })
577
+ const res = await app.inject({ url: "/api/v1/catalog/agent-tools" })
578
+ expect(res.json()).toEqual(expect.arrayContaining([
579
+ expect.objectContaining({ name: "execute_sql" }),
580
+ ]))
581
+ await app.close()
582
+ ```
583
+
584
+ Use Fastify's `inject()` for in-process testing; no port binding.
585
+
586
+ ### Convenience: `createDataCatalogPlugin(opts)`
587
+
588
+ Dropping `data: DataPaneConfig` from `<ChatCenteredShell>` removed
589
+ the one-liner ergonomics for hosts that just want a simple data
590
+ tab with their adapter (gemini P2). Restore them via the reusable
591
+ data catalog plugin factory exported from `@boring/workspace`:
592
+
593
+ ```ts
594
+ import { createDataCatalogPlugin } from "@boring/workspace"
595
+ import { myAdapter } from "./adapter"
596
+
597
+ export const dataPlugin = createDataCatalogPlugin({
598
+ id: "my-data",
599
+ label: "Data",
600
+ adapter: myAdapter,
601
+ catalogId: "my-data",
602
+ })
603
+ ```
604
+
605
+ Host with simple needs (single adapter, no custom panel):
606
+
607
+ ```tsx
608
+ <WorkspaceProvider plugins={[dataPlugin]}>
609
+ <ChatCenteredShell />
610
+ </WorkspaceProvider>
611
+ ```
612
+
613
+ Apps with domain-specific behavior compose around this factory:
614
+ boring-macro installs the data catalog outputs inside its own
615
+ macro plugin, then keeps chart/deck panels and macro server tools
616
+ in the app plugin.
617
+
618
+ ### Concrete filesystemPlugin source
619
+
620
+ ```ts
621
+ // packages/workspace/src/plugin/defaults/filesystemPlugin.ts (v7.1 — UI-only)
622
+ import { definePlugin, type Plugin } from "../definePlugin"
623
+ import { FileTreePanel, CodeEditorPanel, MarkdownEditorPanel } from "../../panels"
624
+ import { filesCatalog } from "./filesystemCatalog"
625
+
626
+ export const filesystemPlugin: Plugin = definePlugin({
627
+ id: "filesystem",
628
+ label: "Filesystem",
629
+
630
+ panels: [
631
+ {
632
+ id: "files",
633
+ title: "Files",
634
+ component: FileTreePanel,
635
+ placement: "left-tab",
636
+ source: "builtin",
637
+ },
638
+ {
639
+ id: "code-editor",
640
+ title: "Code",
641
+ component: CodeEditorPanel,
642
+ placement: "center",
643
+ filePatterns: [
644
+ "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx",
645
+ "**/*.py", "**/*.rs", "**/*.go", "**/*.json", "**/*.yml", "**/*.yaml",
646
+ ],
647
+ source: "builtin",
648
+ },
649
+ {
650
+ id: "markdown-editor",
651
+ title: "Markdown",
652
+ component: MarkdownEditorPanel,
653
+ placement: "center",
654
+ filePatterns: ["**/*.md", "**/*.mdx"],
655
+ source: "builtin",
656
+ },
657
+ ],
658
+
659
+ catalogs: [filesCatalog],
660
+ // No agentTools field — file tools are HARNESS substrate (live in
661
+ // @boring/agent via pi-tools-migration's bundle factories). See
662
+ // §"Tools belong with the harness, not the plugin".
663
+ })
664
+ ```
665
+
666
+ The panel ids (`code-editor`, `markdown-editor`) are the override
667
+ seams: a host plugin can register the same id with a
668
+ `SuperCoderPanel` and late-wins-on-id replaces. `source: "builtin"`
669
+ means user/app-source plugins win the file-pattern resolver
670
+ tie-breaker.
671
+
672
+ `filesCatalog` (in `filesystemCatalog.ts`) wires
673
+ `/api/v1/files/search` to the catalog adapter — the same backend
674
+ the LLM's `find` tool uses, so the cmd palette and the LLM
675
+ share one search engine.
676
+
677
+ ### Where do routes go?
678
+
679
+ Not on the Plugin contract. Routes are HTTP infrastructure; plugins
680
+ are registry contributions. Mixing them blurs identity AND lies
681
+ about lifecycle (Fastify routes can't be unregistered, but a
682
+ contract field would imply they could).
683
+
684
+ **Three categories of routes** in the running app:
685
+
686
+ | Source | Who registers | Where |
687
+ |---|---|---|
688
+ | Substrate (`/api/v1/ui/*`, `/api/v1/files`, `/tree`, `/files/search`) | `createWorkspaceAgentApp` itself, always | Inside the workspace package; hosts don't see this. |
689
+ | Agent core (`/api/v1/chat`, `/sessions`, `/models`) | `createAgentApp` itself, always | Inside the agent package; hosts don't see this. |
690
+ | Plugin-specific (e.g. `/api/v1/macro/*`) | The host, via Fastify's standard `app.register(...)` | The host's server entry. One line per plugin that has routes. |
691
+
692
+ **Macro's host server entry**:
693
+
694
+ ```ts
695
+ // apps/boring-macro-v2/src/server/index.ts
696
+ import { createWorkspaceAgentApp } from "@boring/workspace/server"
697
+ import { makeMacroPlugin } from "../plugin"
698
+ import { registerMacroRoutes } from "../server/macroRoutes"
699
+
700
+ const clickhouse = await createClickHouseClient(env)
701
+
702
+ const app = await createWorkspaceAgentApp({
703
+ plugins: [makeMacroPlugin()], // ← UI/catalog/tool contributions
704
+ })
705
+ await app.register(registerMacroRoutes, { clickhouse, deckRoot }) // ← routes (one line)
706
+ await app.listen({ port })
707
+ ```
708
+
709
+ In Phase 1, **only macro** has plugin-specific routes (filesystem's
710
+ routes are substrate; dataCatalog has none; chat-shell has none).
711
+ So this single extra line per plugin-with-routes is the entire
712
+ "routes story." No new abstraction needed.
713
+
714
+ ### Why no chatSuggestions on the contract
715
+
716
+ The other contribution types pass an honest aggregation test: *N
717
+ plugins each contribute items, all merged by a registry, all useful
718
+ to the user.* Chat suggestions fail that test:
719
+
720
+ - Empty-state UX caps at ~4–6 cards.
721
+ - N plugins × 4–6 each = 12–24, way more than the cap.
722
+ - Truncation forces the host to **curate** which suggestions appear
723
+ → if the host curates, the registry adds nothing.
724
+
725
+ So suggestions stay where they belong: a single `ChatSuggestion[]`
726
+ prop on `<ChatCenteredShell>`, host writes it directly. macro's
727
+ plugin module re-exports the suggestions array as a regular const;
728
+ the host imports + passes alongside the plugin:
729
+
730
+ ```tsx
731
+ // apps/boring-macro-v2/src/web/App.tsx
732
+ import { makeMacroPlugin, macroChatSuggestions } from "../plugin"
733
+
734
+ <WorkspaceProvider plugins={[makeMacroPlugin()]}>
735
+ <ChatCenteredShell chatSuggestions={macroChatSuggestions} />
736
+ </WorkspaceProvider>
737
+ ```
738
+
739
+ `ChatSuggestion` lives in `@boring/agent/front-shadcn` as today; no
740
+ type changes. The plan does NOT delete `chatSuggestions` from
741
+ `<ChatCenteredShell>`'s props (reverses an earlier draft).
742
+
743
+ ### Default plugins — ONE, finalized (UI-only)
744
+
745
+ | Plugin | Contributes | Why a plugin (not core) |
746
+ |---|---|---|
747
+ | **`filesystemPlugin`** | UI-only: a Files catalog (cmd palette); FileTree panel registration as `placement: 'left-tab'`; CodeEditor + MarkdownEditor panel registrations (with `filePatterns`). **No `agentTools` field** — file tools are harness substrate (see §"Tools belong with the harness, not the plugin"). | Hosts that want a chat-only UI (no file tree, no code editor opening on file click) can opt out. When excluded: file UI disappears; LLM file tools STAY (controlled separately by `disableDefaultFileTools` on `createAgentApp`). |
748
+
749
+ **v6 had a `dataCatalogPlugin` second default; v6.2 cuts it.**
750
+ Reason (codex round-3 P1): with `recentKind` cut and no other
751
+ filter, a generic "Data" tab couldn't unambiguously pick *which*
752
+ catalog to display when multiple plugins contribute catalogs (e.g.,
753
+ filesystem's Files catalog + macro's Series catalog). Rather than
754
+ re-add a `defaultForDataTab` flag or invent precedence, **plugins
755
+ that want a workbench data tab register their own `placement:
756
+ 'left-tab'` panel** that internally renders DataExplorer with their
757
+ adapter. macro's plugin gets a "Macro Series" tab; filesystem
758
+ already has the Files tab; no generic placeholder.
759
+
760
+ If a host wants a vanilla "pick any catalog" data tab, they can
761
+ register one — it's not a hard substrate concern.
762
+
763
+ **Note on filesystem ROUTES:** `/api/v1/files`, `/tree`,
764
+ `/files/search` are **substrate**, not part of `filesystemPlugin`.
765
+ `createWorkspaceAgentApp` always registers them so the workspace
766
+ UI's HTTP plumbing works; `excludeDefaults: ['filesystem']` removes
767
+ the filesystem **capability** (tools + tabs + editors) but leaves
768
+ the HTTP plumbing available for any host or other plugin.
769
+
770
+ The single default plugin auto-mounts. Hosts opt out via:
771
+
772
+ ```tsx
773
+ <WorkspaceProvider
774
+ plugins={[macroPlugin]}
775
+ excludeDefaults={["filesystem"]} // or [] (only filesystem is a default)
776
+ >
777
+ ```
778
+
779
+ `excludeDefaults` is the single switch — no `includeDefaults`
780
+ allowlist.
781
+
782
+ #### Tools belong with the harness, not the plugin (v7.0)
783
+
784
+ Earlier drafts (v5–v6.3) had `filesystemPlugin` carry `agentTools`
785
+ via a factory + a "dual registration path" that suppressed
786
+ duplication between standalone agent and workspace hosts. v7.0
787
+ drops this entire arrangement. **File ops tools are harness
788
+ substrate, not plugin contributions.** Two separate concerns,
789
+ two separate switches:
790
+
791
+ | Concern | Real opt-out switch | Layer |
792
+ |---|---|---|
793
+ | Should the agent have file tools? | `disableDefaultFileTools: true` on `createAgentApp` | Harness config |
794
+ | Should the UI have a file tree / Files tab? | `excludeDefaults: ['filesystem']` on `<WorkspaceProvider>` | Workspace config |
795
+
796
+ These should NOT be the same switch — they live at different
797
+ layers. Conflating them was over-engineering.
798
+
799
+ #### Where file tools actually live (v7.0)
800
+
801
+ Per `packages/agent/docs/plans/pi-tools-migration.md` (which ships
802
+ before this plan), `@boring/agent` exposes:
803
+
804
+ - `buildHarnessAgentTools(bundle): AgentTool[]` — `[bash, executeIsolatedCode]`
805
+ - `buildFilesystemAgentTools(bundle): AgentTool[]` — `[read, write, edit, find, grep, ls]` plus any custom non-pi additions
806
+
807
+ `createAgentApp` registers both bundles by default. Opt out of
808
+ file ops with `disableDefaultFileTools: true`. Standalone CLI agent
809
+ keeps file tools because it's a coding agent — that's the harness's
810
+ job, not a plugin's.
811
+
812
+ `createWorkspaceAgentApp` does **not** pass
813
+ `disableDefaultFileTools` — it just wraps `createAgentApp`
814
+ unchanged + runs the plugin bootstrap on top. No dual-registration.
815
+
816
+ #### Custom (non-pi) filesystem tools
817
+
818
+ `buildFilesystemAgentTools(bundle)` is NOT restricted to pi's
819
+ factories. It can return pi tools + project-specific filesystem
820
+ tools that don't exist in pi's catalog. Examples that might land
821
+ here later:
822
+
823
+ - `watch_files(glob)` — long-poll for file changes (pi has no equivalent)
824
+ - `stat(path)` — file metadata (size, mtime, perms)
825
+ - `git_status` / `git_diff` — git-aware filesystem ops
826
+ - `multi_edit(edits[])` — atomic batch edits across many files
827
+
828
+ These would be **substrate** alongside pi's defaults — same
829
+ registration path, same lifecycle. They live in
830
+ `@boring/agent/server/tools/filesystem/` (not in
831
+ `filesystemPlugin`). Author wraps them as `AgentTool`; bundle
832
+ factory composes them into the array.
833
+
834
+ The principle from pi-tools-migration's Principle 3 still applies:
835
+ add custom tools only when pi cannot be made to work. But "can't
836
+ be made to work" includes "pi doesn't ship this capability at all."
837
+
838
+ **filesystemPlugin (v7.0) — UI-only, plain const, no factory:**
839
+
840
+ ```ts
841
+ // packages/workspace/src/plugin/defaults/filesystemPlugin.ts
842
+ import { definePlugin } from "../definePlugin"
843
+ import { FileTreePanel, CodeEditorPanel, MarkdownEditorPanel } from "../../panels"
844
+ import { filesCatalog } from "./filesystemCatalog"
845
+
846
+ export const filesystemPlugin = definePlugin({
847
+ id: "filesystem",
848
+ label: "Filesystem",
849
+ panels: [
850
+ { id: "files", title: "Files", component: FileTreePanel, placement: "left-tab", source: "builtin" },
851
+ { id: "code-editor", title: "Code", component: CodeEditorPanel, placement: "center", filePatterns: [...], source: "builtin" },
852
+ { id: "markdown-editor", title: "Markdown", component: MarkdownEditorPanel, placement: "center", filePatterns: ["**/*.md", "**/*.mdx"], source: "builtin" },
853
+ ],
854
+ catalogs: [filesCatalog],
855
+ // No agentTools — file tools live with the harness in @boring/agent.
856
+ })
857
+ ```
858
+
859
+ No `(deps)` argument. No runtime bundle dependency. Plain
860
+ module-scope const that imports cleanly. WorkspaceProvider /
861
+ createWorkspaceAgentApp prepend it as a default unless
862
+ `excludeDefaults: ['filesystem']` says otherwise.
863
+
864
+ No "ONE source of truth, TWO registration paths" puzzle. Just:
865
+ **harness owns tools; plugins own UI.**
866
+
867
+ ### Core — substrate, not plugins
868
+
869
+ Always present. Not pluggable. Replacing core by accident shouldn't
870
+ be possible.
871
+
872
+ - Per-type registries (Catalog new; Command + Panel retrofitted
873
+ subscribable)
874
+ - EventBus (already shipped in `packages/workspace/src/events/`)
875
+ - UiBridge (in-memory message queue;
876
+ `@boring/workspace/src/bridge/`)
877
+ - React component primitives: `CodeEditor`, `MarkdownEditor`,
878
+ `FileTree`, `DataExplorer`, `EmptyPane`, `EmptyFilePanel` (the
879
+ components themselves are core exports; their **panel
880
+ registrations** with `filePatterns` belong to the relevant
881
+ default plugin)
882
+ - Default agent tools that expose the substrate: `get_ui_state`,
883
+ `exec_ui` (registered directly by `createWorkspaceAgentApp`)
884
+ - Default routes: `/api/v1/ui/*`, `/api/v1/files`, `/tree`,
885
+ `/files/search` (registered directly)
886
+ - Default commands: `toggleSidebar`, `toggleAgentPanel`, `closeTab`
887
+ - Chat shell + palette + workbench themselves
888
+ - ChatPanel mount point
889
+
890
+ **Substrate is core, capabilities are plugins.**
891
+
892
+ ### Plugin composability — file-pattern resolution + late-wins
893
+
894
+ **File-pattern panel resolution.** The current resolver
895
+ (`PanelRegistry.ts:91`, `SurfaceShell.tsx:98`) matches **basename
896
+ only** via a hand-rolled `*suffix`/exact matcher. It also has a
897
+ working source-priority tie-breaker (`app` beats `builtin`) which
898
+ we PRESERVE. Phase 1 step 2 upgrades the matcher itself to
899
+ **path-aware micromatch** so patterns like `deck/**/*.md` actually
900
+ work. When `openFile(path)` runs:
901
+
902
+ 1. Filter panels whose `filePatterns` include the full `path` under
903
+ path-aware micromatch (`{ matchBase: false, dot: true }`).
904
+ 2. Sort by **specificity** —
905
+ `score = (segment_count * 10) + non_wildcard_chars`. Higher wins.
906
+ 3. Tie-break A: `source: 'app'` beats `source: 'builtin'` (current
907
+ behavior, preserved).
908
+ 4. Tie-break B: registration order, late wins.
909
+ 5. Hosts can bypass pattern matching at the call site:
910
+ `surface.openPanel({ component: "<id>", … })`.
911
+
912
+ **Late-wins-on-id.** If two contributions share the same `id`, the
913
+ later registration wins. Combined with the convention that defaults
914
+ mount before host plugins, this means:
915
+
916
+ ```
917
+ 1. ADD a domain pane for a domain path
918
+ filesystem: { id: "markdown-editor", patterns: ["**/*.md"] }
919
+ macro: { id: "deck", patterns: ["deck/**/*.md"] }
920
+ notes.md → MarkdownEditor (default)
921
+ deck/labor/labor.md → DeckPane (specificity wins)
922
+
923
+ 2. REPLACE a default pane (late-wins-on-id)
924
+ filesystem: { id: "code-editor", component: CodeEditor, patterns: ["**/*.ts"] }
925
+ superCoder: { id: "code-editor", component: SuperCoder, patterns: ["**/*.ts"] }
926
+ any *.ts → SuperCoder (same id ⇒ replaces)
927
+ ```
928
+
929
+ Late-wins logs a dev-mode `console.warn` so the override is
930
+ traceable.
931
+
932
+ ### Plugin patterns: cross-plugin communication (v7.2)
933
+
934
+ The plan deliberately omits `dependsOn` (see §Non-goals) — but
935
+ plugins still need to coordinate. Three canonical patterns, each
936
+ with a worked example:
937
+
938
+ **1. Shared registry — read another plugin's catalog.** No dep
939
+ edge needed; null-check + graceful degradation.
940
+
941
+ ```ts
942
+ // Inside Plugin B's panel component
943
+ const catalogs = useCatalogs()
944
+ const filesCatalog = catalogs.find(c => c.id === "files")
945
+ if (filesCatalog) {
946
+ const result = await filesCatalog.adapter.search({
947
+ query: "foo", filters: {}, limit: 50, offset: 0, signal,
948
+ })
949
+ // …use result.items
950
+ }
951
+ // If filesystemPlugin isn't loaded → filesCatalog is undefined → skip.
952
+ ```
953
+
954
+ **2. Event bus — emit + subscribe.** Decoupled; transitions only.
955
+
956
+ ```ts
957
+ // Plugin A: in a panel component
958
+ const onClick = () =>
959
+ events.emit("plugin-a:thing-happened", { ts: Date.now(), userId })
960
+
961
+ // Plugin B: in another panel component
962
+ useEvent("plugin-a:thing-happened", (payload) => { /* react */ })
963
+ ```
964
+
965
+ New event keys go in `WorkspaceEventMap` (events declared on
966
+ demand — see §Event bus integration).
967
+
968
+ **3. Late-wins override — replace another plugin's panel.**
969
+
970
+ ```ts
971
+ // Plugin B (registered after Plugin A) overrides A's "code-editor"
972
+ definePlugin({
973
+ id: "super-coder",
974
+ panels: [{
975
+ id: "code-editor", // SAME id as filesystem's default
976
+ component: SuperCoderPanel,
977
+ placement: "center",
978
+ filePatterns: ["**/*.ts"],
979
+ source: "app",
980
+ }],
981
+ })
982
+ ```
983
+
984
+ Late-wins-on-id replaces filesystem's `CodeEditorPanel` for any
985
+ `*.ts` file. Dev-mode `console.warn` flags the override; no hard
986
+ error.
987
+
988
+ **When to use which:**
989
+
990
+ | Pattern | When |
991
+ |---|---|
992
+ | Shared registry | Plugin B needs Plugin A's data (catalog, panel, command) at runtime. |
993
+ | Event bus | Plugin B reacts to something Plugin A does. No return value, no dependency. |
994
+ | Late-wins override | Plugin B replaces a default's panel/command/catalog by id. Composition pattern. |
995
+
996
+ **Anti-patterns — don't do these:**
997
+
998
+ - Direct module import between plugins (couples them; defeats the registry).
999
+ - Module-scope `events.on(...)` (fires globally; leaks subscriptions).
1000
+ - Polling for another plugin's state (use the event bus instead).
1001
+
1002
+ ### Workspace orchestration — bootstrap
1003
+
1004
+ ```ts
1005
+ // BootstrapOptions (v7.7 — chatPanel slot added)
1006
+ import type { ComponentType } from 'react'
1007
+ import type { ChatPanelProps } from '@boring/agent' // TYPE-ONLY (Inv #7)
1008
+
1009
+ export interface BootstrapOptions {
1010
+ /** Required. The ChatPanel implementation, value-imported by the host
1011
+ * app from @boring/agent. Workspace stores the slot on context; the
1012
+ * internal `chatPanel` chrome reads + renders it with workspace
1013
+ * integrations (auto-open hooks, command-stream, suggestions).
1014
+ * v7.7 addition. */
1015
+ chatPanel: ComponentType<ChatPanelProps>
1016
+
1017
+ plugins?: Plugin[]
1018
+ excludeDefaults?: string[]
1019
+ registries: {
1020
+ panels: PanelRegistry
1021
+ commands: CommandRegistry
1022
+ catalogs: CatalogRegistry
1023
+ agentTools?: AgentToolRegistry
1024
+ }
1025
+ defaults?: Plugin[]
1026
+ }
1027
+ ```
1028
+
1029
+ ```
1030
+ bootstrap(opts):
1031
+ if !opts.chatPanel: throw — workspace will not silently fallback
1032
+ store opts.chatPanel on WorkspaceContext (read by ChatPanelHost chrome)
1033
+
1034
+ finalSet = [...defaultPlugins.filter(d => !excludeDefaults.includes(d.id)),
1035
+ ...opts.plugins]
1036
+
1037
+ for each plugin in finalSet (array order):
1038
+ fan plugin.panels → PanelRegistry (pluginId provenance)
1039
+ fan plugin.commands → CommandRegistry (pluginId provenance)
1040
+ fan plugin.catalogs → CatalogRegistry (pluginId provenance)
1041
+ (server) fan plugin.agentTools → AgentToolRegistry
1042
+
1043
+ systemPromptAppend = finalSet
1044
+ .filter(p => p.systemPrompt?.trim())
1045
+ .map(p => p.systemPrompt!.trim())
1046
+ .join('\n\n')
1047
+
1048
+ return { registered: finalSet.map(p => p.id), systemPromptAppend }
1049
+ ```
1050
+
1051
+ Single pass. No async. No lifecycle. No ordering contract beyond
1052
+ "array order." Defaults are prepended so they register first; host
1053
+ plugins register after; late-wins-on-id gives hosts a clean
1054
+ override mechanism without explicit precedence rules.
1055
+
1056
+ The retrofit applies to existing `CommandRegistry` and
1057
+ `PanelRegistry` — they get `subscribe()` semantics so late
1058
+ `registerCommand` calls reach an open palette.
1059
+
1060
+ #### Chat as core chrome — DI shape, not plugin (v7.7)
1061
+
1062
+ Chat is core: workspace lays it out, sizes it, knows where it goes.
1063
+ **Only the React component is injected.** The workspace package
1064
+ holds **zero value imports** of `@boring/agent` (Inv #7); a TYPE-ONLY
1065
+ import for `ChatPanelProps` is fine and grep-enforced.
1066
+
1067
+ Worked example:
1068
+
1069
+ ```tsx
1070
+ // CONSUMING APP — value-imports ChatPanel and passes it
1071
+ import { ChatPanel } from '@boring/agent' // value import — host's prerogative
1072
+ import { WorkspaceProvider, type Plugin } from '@boring/workspace'
1073
+ import { myPlugin } from './plugin'
1074
+
1075
+ export const App = () => (
1076
+ <WorkspaceProvider chatPanel={ChatPanel} plugins={[myPlugin]}>
1077
+ {/* layouts, etc. */}
1078
+ </WorkspaceProvider>
1079
+ )
1080
+ ```
1081
+
1082
+ ```tsx
1083
+ // WORKSPACE INTERNAL — type-only import; chrome that consumes the slot
1084
+ // packages/workspace/src/front/chrome/chat/ChatPanelHost.tsx
1085
+ import type { ChatPanelProps } from '@boring/agent' // type-only
1086
+ import { useWorkspaceContext } from '../../WorkspaceProvider'
1087
+
1088
+ export function ChatPanelHost(props: ChatPanelProps) {
1089
+ const { chatPanel: ChatPanelImpl } = useWorkspaceContext()
1090
+ // workspace integrations: auto-open agent files, suggestions wiring, etc.
1091
+ return <ChatPanelImpl {...props} />
1092
+ }
1093
+ ```
1094
+
1095
+ The chat chrome's `definition.ts` registers `ChatPanelHost` (not the
1096
+ agent's ChatPanel) into the PanelRegistry as a CORE panel. Hosts that
1097
+ need to swap chat impls (e.g., a stripped-down terminal renderer)
1098
+ just pass a different `chatPanel` prop — they don't author a plugin
1099
+ to do it.
1100
+
1101
+ Why this shape (vs plugin-ifying chat):
1102
+
1103
+ - **Inv #7 stays verifiable.** `grep -RE "from ['\"]@boring/agent['\"]" packages/workspace/src` finds zero non-type-import hits.
1104
+ - **Bootstrap stays single-purpose.** Plugins describe optional contributions; chat is non-optional core. Forcing chat into the Plugin contract would mean either (a) every host registers a "chat plugin" boilerplate-style, or (b) the workspace value-imports its own chat plugin (violating Inv #7).
1105
+ - **Layout knows about chat.** `ChatLayout`, `IdeLayout` reference `'chat'` panel id directly; that's correct because chat is chrome the layouts can rely on, not a maybe-present contribution.
1106
+
1107
+ ### Per-plugin error boundaries (v7.2)
1108
+
1109
+ A plugin's panel that throws during render must NOT crash the
1110
+ workspace shell. Every plugin contribution that renders React
1111
+ (panels; catalog adapter row renderers; `<ChatEmptyState>` cards
1112
+ sourced from chatSuggestions) is wrapped in
1113
+ `<PluginErrorBoundary pluginId={id}>`. On error:
1114
+
1115
+ 1. Boundary renders an `<ErrorChip>` showing the plugin id +
1116
+ short error message in place of the contribution.
1117
+ 2. A `PluginError { kind: 'contribution', pluginId, error }` is
1118
+ pushed onto `WorkspaceContext.errors` (consumed by
1119
+ `<PluginInspector />`).
1120
+ 3. Other plugins continue rendering unaffected.
1121
+
1122
+ Implementation sites:
1123
+
1124
+ - `<PanelHost panelId>` — wraps the resolved panel component.
1125
+ - `<CatalogResults>` — wraps each row renderer (so a bad row
1126
+ doesn't kill the palette).
1127
+ - The chat shell's empty-state card list — wraps each
1128
+ `<ChatSuggestion>` card.
1129
+
1130
+ No contract change for plugin authors. The boundary is a
1131
+ host-side wrapper; plugins just opt in implicitly by having their
1132
+ contribution mounted.
1133
+
1134
+ **Server-side parallel:** per-catalog-search try/catch already
1135
+ covered in §Error model. The CommandPalette's debounced search
1136
+ loop catches per-catalog `search()` rejections and surfaces them
1137
+ as inline error chips per catalog group; one bad adapter doesn't
1138
+ fail the entire palette query.
1139
+
1140
+ ### Search semantics — debouncing + cancellation (v7.2)
1141
+
1142
+ The CommandPalette runs catalog searches on every keystroke.
1143
+ Without coordination this would (a) race (older search resolves
1144
+ after newer; UI shows stale results) and (b) waste work (5
1145
+ catalogs × 10 keystrokes = 50 in-flight HTTP fetches).
1146
+
1147
+ `@boring/workspace/plugin` exports `useDebouncedCatalogSearch`:
1148
+
1149
+ ```ts
1150
+ function useDebouncedCatalogSearch(
1151
+ query: string,
1152
+ opts?: { debounceMs?: number },
1153
+ ): {
1154
+ results: Map<string, ExplorerRow[]> // catalogId → rows
1155
+ loading: boolean
1156
+ errors: Map<string, Error> // catalogId → error (per-catalog)
1157
+ }
1158
+ ```
1159
+
1160
+ Behavior:
1161
+ - Debounces 150ms by default (override via `debounceMs`).
1162
+ - On every new query: aborts in-flight searches via
1163
+ `AbortController.abort()`, fires fresh ones across all registered
1164
+ catalogs in parallel.
1165
+ - Per-catalog errors (one adapter throws) isolated — surface in
1166
+ `errors` map; other catalogs still return results.
1167
+
1168
+ **Adapter contract:** `ExplorerAdapter.search(args: SearchArgs)`
1169
+ already accepts `args.signal?: AbortSignal` (verified live at
1170
+ `packages/workspace/src/components/DataExplorer/types.ts:46-55`).
1171
+ Adapters that honor the signal get cancellation; adapters that
1172
+ ignore it are still safe (last-write-wins via the debounce).
1173
+
1174
+ **Plugin authors:** if your catalog hits an HTTP backend, pass
1175
+ `args.signal` to `fetch(url, { signal })`. If your catalog runs
1176
+ synchronous filtering, you can ignore the signal — debouncing
1177
+ handles the wasted-work case.
1178
+
1179
+ ### Polymorphic Recent
1180
+
1181
+ The Command Palette today has a Recent section with a known bug
1182
+ (`CommandPalette.tsx:34`-`60`, `:157`, `:230-232`): it stores items
1183
+ uniformly as path strings and renders all entries through
1184
+ `FilePathLabel`. When a command is the most-recent action it
1185
+ renders as a (broken) file path.
1186
+
1187
+ The plugin model fixes this by tagging each Recent entry with the
1188
+ catalog it came from. RecentStore entries:
1189
+
1190
+ ```ts
1191
+ type RecentEntry =
1192
+ | {
1193
+ type: "catalog"
1194
+ catalogId: string // ↔ CatalogConfig.id
1195
+ rowId: string // ↔ ExplorerRow.id within that catalog
1196
+ /** Snapshot of the row at time of selection. Guards against
1197
+ * catalog data changing under our feet (file renamed, series
1198
+ * re-tagged, …). Cheap (~200 bytes); essential because
1199
+ * adapters don't have a `getById(rowId)` method.
1200
+ * IMPORTANT: ExplorerRow participating in Recent MUST be
1201
+ * 100% JSON-serializable — see §"Recent serialization
1202
+ * invariant" (gemini P1). */
1203
+ rowSnapshot: ExplorerRow
1204
+ selectedAt: number // unix ms
1205
+ }
1206
+ | {
1207
+ type: "command"
1208
+ commandId: string // ↔ CommandConfig.id
1209
+ /** Snapshot of the command's title at time of selection,
1210
+ * in case the command is later unregistered. */
1211
+ titleSnapshot: string
1212
+ selectedAt: number
1213
+ }
1214
+ ```
1215
+
1216
+ Render flow:
1217
+
1218
+ 1. For each entry, look up the source by id (catalog by
1219
+ `catalogId`, command by `commandId`). If absent (plugin
1220
+ uninstalled, command unregistered), drop the entry — don't
1221
+ render orphans. Show `titleSnapshot` text only if the user
1222
+ needs to see what they recently used (we drop on click since
1223
+ we can't run a missing command).
1224
+ 2. For `type: "catalog"`: render via the catalog's adapter row
1225
+ renderer; on click → `catalog.onSelect(rowSnapshot)`.
1226
+ 3. For `type: "command"`: render the title with a small "command"
1227
+ chip; on click → `command.run()`.
1228
+
1229
+ **Recent covers BOTH catalog rows AND commands** (gemini P1
1230
+ correction — earlier drafts said "catalog-only," but every mature
1231
+ palette UX — VS Code, Raycast, Linear — keeps recent commands.
1232
+ Re-running frequent actions like "Toggle Theme" / "Format JSON"
1233
+ is the primary use case for many users).
1234
+
1235
+ Existing localStorage entries (`boring-ui-v2:command-palette:recent`)
1236
+ are read once on first load. Strings prefixed `cmd:` (today's
1237
+ broken command path) become `{type: "command", commandId: ...}`;
1238
+ plain path strings become `{type: "catalog", catalogId: "files",
1239
+ rowId: path, rowSnapshot: {...minimal row…}}`. Unrecognizable
1240
+ entries dropped.
1241
+
1242
+ #### Recent serialization invariant (gemini P1)
1243
+
1244
+ `rowSnapshot: ExplorerRow` is round-tripped through
1245
+ `JSON.stringify`/`JSON.parse` in localStorage. ExplorerRow values
1246
+ that contain `Date`, `Map`, `Set`, functions, React nodes, or
1247
+ class instances will be silently corrupted on save and crash on
1248
+ restore.
1249
+
1250
+ **Invariant:** any `ExplorerRow` shape that can appear in a
1251
+ catalog's selected items MUST be JSON-serializable. Adapters that
1252
+ naturally hold non-serializable values (e.g., a Date object for
1253
+ "last modified") should serialize at row construction time
1254
+ (ISO string) and re-hydrate in the renderer.
1255
+
1256
+ **No `deserializeRecent` hook in Phase 1.** If a real adapter has
1257
+ a need that the JSON-serializable invariant can't express, the
1258
+ hook can be added as a non-breaking optional field on
1259
+ `CatalogConfig`. Phase 1 doesn't need it; documenting the
1260
+ constraint is sufficient.
1261
+
1262
+ ### Registry-driven workbench tabs
1263
+
1264
+ Today `WorkbenchLeftPane` hardcodes Files / Data tabs
1265
+ (`WorkbenchLeftPane.tsx:97`, `:174`, `:181`) — which means
1266
+ `excludeDefaults: ['filesystem']` would suppress the filesystem
1267
+ agent tools and catalogs but leave a dead Files tab in the UI.
1268
+
1269
+ Phase 1 step 5c retrofits `WorkbenchLeftPane` to query
1270
+ `PanelRegistry` for `placement: 'left-tab'`, sorted by
1271
+ registration order. `filesystemPlugin` contributes the Files tab;
1272
+ `dataCatalogPlugin` contributes the Data tab.
1273
+ `excludeDefaults: ['filesystem']` truly removes the tab.
1274
+
1275
+ (`'right-tab'` is reserved in the contract but no Phase 1 component
1276
+ consumes it; `WorkbenchRightPane` doesn't exist yet.)
1277
+
1278
+ ### Closing the SurfaceShell hardcoded-fallback hole
1279
+
1280
+ `SurfaceShell.fallbackComponentForPath`
1281
+ (`SurfaceShell.tsx:81-91`) maps extensions to literal panel ids
1282
+ (`code-editor`, `markdown-editor`, `csv-viewer`) regardless of
1283
+ whether they're registered. `resolvePanelForPath`
1284
+ (`SurfaceShell.tsx:99-108`) checks `registry.has(fallback)` then
1285
+ returns the fallback id anyway as a "last-ditch."
1286
+
1287
+ Step 5c also fixes the resolver chain: registry resolve →
1288
+ registered fallback (only if `has()`) → `EmptyFilePanel` (a core
1289
+ panel) showing "No editor for `<path>` — install or enable a
1290
+ plugin that handles `<ext>`." Zero ghost tabs when defaults are
1291
+ excluded.
1292
+
1293
+ ### Event bus integration
1294
+
1295
+ The bus is **already implemented** at
1296
+ `packages/workspace/src/events/{bus,index,types,useEvent}.ts`. This
1297
+ section reflects the actual API.
1298
+
1299
+ ```ts
1300
+ // Module singleton — import directly anywhere
1301
+ import { events, useEvent } from "@boring/workspace/events"
1302
+ import type { WorkspaceEventMap } from "@boring/workspace/events"
1303
+
1304
+ events.on("file:moved", ({ from, to }) => { /* … */ }) // returns unsubscribe
1305
+ events.emit("file:moved", { ...userMeta(), from, to })
1306
+
1307
+ // React hook
1308
+ useEvent("file:moved", ({ from, to }) => { /* … */ })
1309
+ ```
1310
+
1311
+ **Events are declared on demand** — `WorkspaceEventMap` (in
1312
+ `events/types.ts`) intentionally pre-declares no future events.
1313
+ Phase 1 does NOT add plugin lifecycle events because Phase 1
1314
+ plugins have no lifecycle. If/when something emits and consumes,
1315
+ the key gets added to the map.
1316
+
1317
+ **Plugin authors** subscribe via `useEvent` inside panel components
1318
+ (natural React lifecycle, automatic cleanup) or via `events.on(...)`
1319
+ inside route handlers. They do NOT receive an injected bus through
1320
+ a plugin context — there's no plugin context to inject into.
1321
+
1322
+ **Package-exports gap:** the workspace package's `exports` map
1323
+ (`packages/workspace/package.json:9-30`) currently exposes `.`,
1324
+ `./testing`, `./ui-shadcn`, `./shared`, `./server`, `./globals.css`
1325
+ — but NOT `./events`. Step 4a adds:
1326
+
1327
+ ```json
1328
+ "./events": {
1329
+ "types": "./dist/events.d.ts",
1330
+ "import": "./dist/events.js"
1331
+ }
1332
+ ```
1333
+
1334
+ Plus the corresponding `tsup` entry. Events are also re-exported
1335
+ from the package barrel for convenience.
1336
+
1337
+ ### Phase 1 debug overlay: `<PluginInspector />` (v7.2)
1338
+
1339
+ DEV-only React component mounted by `<WorkspaceProvider>` when
1340
+ `import.meta.env.DEV` (zero production bundle impact). Toggle via
1341
+ `Cmd+Shift+P P` (or "Show Plugin Inspector" command).
1342
+
1343
+ Displays:
1344
+ - Registered plugins: id, label, source (default / inline / npm),
1345
+ contribution counts (N panels, N commands, N catalogs, N agentTools)
1346
+ - Per-plugin systemPrompt preview (first 200 chars; expandable)
1347
+ - Errors keyed to plugin id (from `WorkspaceContext.errors`)
1348
+ - Late-wins-on-id replacements: when plugin B overrode plugin A's
1349
+ contribution, both pluginIds shown
1350
+ - Reserved-name collisions (if a plugin's agentTools name collides
1351
+ with harness substrate)
1352
+
1353
+ Implementation: ~50 LOC reading from registries via the existing
1354
+ `useCatalogs`/`useCommands`/`useActivePanels` hooks plus a new
1355
+ `usePlugins()` returning the list of registered plugins.
1356
+
1357
+ Why ship in Phase 1 (re-introduced from v6 cut): plugin authors
1358
+ testing locally hit "why didn't my command appear?" repeatedly.
1359
+ Inspector turns 5-minute spelunking into a 2-second visual check.
1360
+
1361
+ ### Inline plugin layout (Phase 1)
1362
+
1363
+ ```
1364
+ apps/<some-app>/
1365
+ ├── package.json
1366
+ ├── src/
1367
+ │ ├── plugin/
1368
+ │ │ ├── index.ts ← env-aware barrel + makeXyzPlugin factory
1369
+ │ │ ├── plugin.shared.ts ← id, label, fixed config
1370
+ │ │ ├── plugin.client.ts ← panels, catalogs, commands
1371
+ │ │ └── plugin.server.ts ← agentTools (route handlers live in src/server/)
1372
+ │ ├── server/index.ts ← createWorkspaceAgentApp({ plugins }) + app.register(routes)
1373
+ │ └── web/App.tsx ← <WorkspaceProvider plugins={[…]}>
1374
+ ```
1375
+
1376
+ For multi-plugin apps: `src/plugins/<id>/...` mirrors per plugin;
1377
+ `src/plugins/index.ts` exports an array.
1378
+
1379
+ ### Entry points
1380
+
1381
+ ```ts
1382
+ // CLIENT
1383
+ import { WorkspaceProvider } from "@boring/workspace"
1384
+ import { ChatCenteredShell } from "@boring/workspace"
1385
+ import { makeMacroPlugin, macroChatSuggestions } from "../plugin"
1386
+
1387
+ <WorkspaceProvider plugins={[makeMacroPlugin()]}>
1388
+ <ChatCenteredShell chatSuggestions={macroChatSuggestions} />
1389
+ </WorkspaceProvider>
1390
+
1391
+ // SERVER
1392
+ import { createWorkspaceAgentApp } from "@boring/workspace/server"
1393
+ import { makeMacroPlugin } from "../plugin"
1394
+ import { registerMacroRoutes } from "../server/macroRoutes"
1395
+
1396
+ const clickhouse = await createClickHouseClient(env)
1397
+ const app = await createWorkspaceAgentApp({
1398
+ plugins: [makeMacroPlugin()],
1399
+ // excludeDefaults: ["dataCatalog"] // optional
1400
+ })
1401
+ await app.register(registerMacroRoutes, { clickhouse, deckRoot })
1402
+ await app.listen({ port })
1403
+ ```
1404
+
1405
+ Both entry points auto-mount default plugins (filesystem +
1406
+ dataCatalog) unless excluded.
1407
+
1408
+ ## Distribution — Phase 2 sketch
1409
+
1410
+ **Inline plugins** (Phase 1) live in the host's source tree. No
1411
+ distribution model needed — direct imports.
1412
+
1413
+ **npm-distributed plugins** (Phase 2) follow the standard
1414
+ sub-path-exports pattern. Same Plugin shape, just delivered via
1415
+ package boundaries:
1416
+
1417
+ ```json
1418
+ // pi-plugin-macro/package.json
1419
+ {
1420
+ "exports": {
1421
+ "./client": { "types": "./dist/client.d.ts", "import": "./dist/client.js" },
1422
+ "./server": { "types": "./dist/server.d.ts", "import": "./dist/server.js" }
1423
+ }
1424
+ }
1425
+ ```
1426
+
1427
+ ```ts
1428
+ // pi-plugin-macro/dist/client.js — UI half
1429
+ export const macroClientPlugin = definePlugin({
1430
+ id: "boring-macro",
1431
+ panels: [chartCanvasPanel, deckPanel],
1432
+ catalogs: [seriesCatalog],
1433
+ })
1434
+ ```
1435
+
1436
+ ```ts
1437
+ // pi-plugin-macro/dist/server.js — server half
1438
+ export const macroServerPlugin = definePlugin({
1439
+ id: "boring-macro", // same id, different bag
1440
+ agentTools: macroAgentTools,
1441
+ })
1442
+ export const registerMacroRoutes = async (app, opts) => { /* … */ }
1443
+
1444
+ // optional convenience helper for hosts that want one-liner install:
1445
+ export const installMacroServer = async (app, opts) => {
1446
+ await app.register(registerMacroRoutes, opts)
1447
+ return macroServerPlugin
1448
+ }
1449
+ ```
1450
+
1451
+ ```ts
1452
+ // host server entry
1453
+ import { createWorkspaceAgentApp } from "@boring/workspace/server"
1454
+ import { macroServerPlugin, registerMacroRoutes } from "pi-plugin-macro/server"
1455
+
1456
+ const clickhouse = await createClickHouseClient(env)
1457
+ const app = await createWorkspaceAgentApp({ plugins: [macroServerPlugin] })
1458
+ await app.register(registerMacroRoutes, { clickhouse, deckRoot })
1459
+ ```
1460
+
1461
+ Two imports + two calls per server-side plugin. Same shape mature
1462
+ ecosystems already use (Express middleware, Fastify plugins, Vite
1463
+ plugins).
1464
+
1465
+ **The same plugin id** (`"boring-macro"`) on client and server ties
1466
+ them together for provenance — but client and server processes don't
1467
+ share state, so a "single fat plugin object spanning both" was
1468
+ always an illusion.
1469
+
1470
+ ## Relationship to pi-mono ecosystem
1471
+
1472
+ The pi-coding-agent's existing plugin loader
1473
+ (`packages/agent/src/server/harness/pi-coding-agent/pluginLoader.ts`)
1474
+ gives us discovery infrastructure for free. Verified what it
1475
+ actually does (2026-04-28):
1476
+
1477
+ - 4 discovery channels: `~/.pi/agent/extensions/`,
1478
+ `<cwd>/.pi/extensions/`, `node_modules/pi-plugin-*`, and
1479
+ `.pi/extensions.json`.
1480
+ - Plugin module shape: `{ default: AgentTool[] | tools: AgentTool[] }`
1481
+ — **tools only**, flat list.
1482
+ - Conflict resolution: late-wins-on-name with a warning.
1483
+ - npm namespace convention: `pi-plugin-*`.
1484
+ - Ecosystem index at
1485
+ `~/.pi/agent/extension-index.json` lists ~50+ extensions
1486
+ (`antigravity-image-gen`, `auto-commit-on-exit`, `bookmark`,
1487
+ `claude-rules`, …) — every one is a single-file `.ts` exporting
1488
+ tools.
1489
+
1490
+ **What pi explicitly DOESN'T have:** `dependsOn`, version ranges,
1491
+ load ordering, multi-contribution types, lifecycle hooks. Pi
1492
+ plugins are flat tool exporters. Cross-plugin coordination is not a
1493
+ pi concern.
1494
+
1495
+ **What we adopt from pi:**
1496
+
1497
+ - Discovery channels verbatim (Phase 2's pi loader extension uses
1498
+ the same paths).
1499
+ - `pi-plugin-*` npm convention.
1500
+ - Late-wins conflict resolution.
1501
+ - `.pi/extensions.json` settings file format.
1502
+ - Tool-only legacy plugins keep working — Phase 2's
1503
+ `extractTools` → `extractPlugin` is additive: a module exporting
1504
+ the new `Plugin` shape gets read as a Plugin; a module exporting
1505
+ the old `tools: AgentTool[]` keeps being read as tools-only.
1506
+
1507
+ **What we add on top:**
1508
+
1509
+ - Multi-contribution shape (`Plugin` with panels/catalogs/commands/
1510
+ agentTools).
1511
+ - Workspace-side aggregation (registries, file-pattern resolution,
1512
+ excludeDefaults).
1513
+ - npm sub-path exports (`./client`, `./server`) for full-plugin
1514
+ distribution.
1515
+
1516
+ Pi gives us **distribution infrastructure**; we add **coordination
1517
+ model**.
1518
+
1519
+ ## Architecture diagram — post-Phase 1
1520
+
1521
+ ### Package dependency graph
1522
+
1523
+ ```
1524
+ ┌─────────────────────────────────────┐
1525
+ │ apps/ │
1526
+ │ ├── boring-macro-v2 │
1527
+ │ ├── full-app │
1528
+ │ └── <new host> │
1529
+ └─────────────────┬───────────────────┘
1530
+ │ imports
1531
+
1532
+ ┌────────────────────────────────────────────────────────────┐
1533
+ │ @boring/workspace │
1534
+ │ • Plugin contract (definePlugin, factories) │
1535
+ │ • Registries (Panel / Command / Catalog) │
1536
+ │ • Default plugins (filesystemPlugin, dataCatalogPlugin) │
1537
+ │ • UI bridge core (moved from @boring/agent) │
1538
+ │ • Substrate routes /api/v1/{ui,files,tree,files/search} │
1539
+ │ • Event bus + WorkspaceEventMap │
1540
+ │ • WorkbenchLeftPane / SurfaceShell / CommandPalette │
1541
+ │ • createWorkspaceAgentApp (wraps createAgentApp) │
1542
+ └─────────────────┬───────────────────────────────────────────┘
1543
+ │ imports (one direction; never the reverse)
1544
+
1545
+ ┌────────────────────────────────────────────────────────────┐
1546
+ │ @boring/agent │
1547
+ │ • pi-coding-agent harness │
1548
+ │ • AgentTool type │
1549
+ │ • validateTool ← extracted to /shared (NEW location)│
1550
+ │ • Pi loader (legacy tools-only; Phase 2 extends) │
1551
+ │ • filesystemAgentTools bundle ← shared with workspace │
1552
+ │ • bash, execute_isolated_code (harness-only) │
1553
+ │ • Chat / session / model HTTP routes │
1554
+ │ • createAgentApp (disableDefaultFileTools? new flag) │
1555
+ └────────────────────────────────────────────────────────────┘
1556
+
1557
+ Invariants:
1558
+ • @boring/agent has NO dep on @boring/workspace (acyclic).
1559
+ • @boring/workspace imports from @boring/agent (one way).
1560
+ • @boring/agent/shared is browser-safe (no node:* imports);
1561
+ this is what workspace's client bundle pulls in.
1562
+ ```
1563
+
1564
+ ### Tool registration flow (file ops as worked example)
1565
+
1566
+ ```
1567
+ filesystemAgentTools (shared bundle, in @boring/agent)
1568
+ ┌────────────────────┐
1569
+ │ read, write, edit, │
1570
+ │ find, │
1571
+ │ grep │
1572
+ └─────────┬──────────┘
1573
+
1574
+ ┌──────────────┴───────────────┐
1575
+
1576
+ SINGLE PATH (v7.1 — both standalone + workspace use it)
1577
+ ─────────────────────────────────────────────────────────
1578
+ createAgentApp({})
1579
+ └─ standardCatalog
1580
+ └─ buildHarnessAgentTools(bundle) (bash, executeIsolatedCode)
1581
+ └─ buildFilesystemAgentTools(bundle) (read/write/edit/find/grep/ls)
1582
+ + any custom non-pi additions
1583
+ (default ON; opt out with disableDefaultFileTools: true
1584
+ for sandboxed/no-fs agents)
1585
+
1586
+ createWorkspaceAgentApp wraps createAgentApp PLAINLY (no
1587
+ disableDefaultFileTools dance) + bootstraps filesystemPlugin
1588
+ (UI-only) on top.
1589
+
1590
+ standalone CLI agent workspace host
1591
+ = real coding agent = same harness tools +
1592
+ plugin model adds UI;
1593
+ excludeDefaults:
1594
+ ['filesystem'] removes
1595
+ Files left-tab + auto-
1596
+ routing — TOOLS STAY
1597
+ (use disableDefaultFileTools
1598
+ for that)
1599
+ ```
1600
+
1601
+ ### File tree — what changes in Phase 1
1602
+
1603
+ ```
1604
+ packages/agent/
1605
+ ├── src/
1606
+ │ ├── shared/
1607
+ │ │ ├── tool.ts [EXISTS]
1608
+ │ │ └── validateTool.ts [NEW — extracted from
1609
+ │ │ pluginLoader.ts; node-clean
1610
+ │ │ so workspace client can import]
1611
+ │ ├── server/
1612
+ │ │ ├── createAgentApp.ts [EXISTS — adds
1613
+ │ │ │ disableDefaultFileTools? flag]
1614
+ │ │ ├── catalog/
1615
+ │ │ │ ├── standardCatalog.ts [EXISTS — drops file ops,
1616
+ │ │ │ │ conditionally re-adds them
1617
+ │ │ │ │ from the shared bundle]
1618
+ │ │ │ └── tools/ [EXISTS — read/write/edit/
1619
+ │ │ │ │ find/grep
1620
+ │ │ │ │ individual implementations
1621
+ │ │ │ │ stay here]
1622
+ │ │ │ └── (read|write|edit|findFiles|grepFiles)Tool.ts
1623
+ │ │ ├── tools/
1624
+ │ │ │ └── filesystem/ [NEW]
1625
+ │ │ │ └── index.ts [NEW — exports
1626
+ │ │ │ filesystemAgentTools[]
1627
+ │ │ │ that re-bundles the
1628
+ │ │ │ individual tools above]
1629
+ │ │ ├── harness/pi-coding-agent/
1630
+ │ │ │ └── pluginLoader.ts [EXISTS — imports
1631
+ │ │ │ validateTool from
1632
+ │ │ │ ../../../shared/validateTool
1633
+ │ │ │ instead of defining it here]
1634
+ │ │ └── http/routes/
1635
+ │ │ ├── file.ts ──────moves to───► [packages/workspace/src/server/
1636
+ │ │ ├── tree.ts ──────moves to───► routes/files.ts]
1637
+ │ │ ├── search.ts ──────moves to───► [routes/files.ts]
1638
+ │ │ └── ui.ts ──────moves to───► [routes/ui.ts]
1639
+ │ └── ...
1640
+ └── package.json
1641
+
1642
+ packages/workspace/
1643
+ ├── src/
1644
+ │ ├── shared/
1645
+ │ │ ├── plugin.ts [NEW — Plugin contract,
1646
+ │ │ │ 6 fields, pure data]
1647
+ │ │ └── ui-bridge.ts [MOVED from @boring/agent]
1648
+ │ ├── events/ [EXISTS — bus already shipped]
1649
+ │ ├── plugin/ [NEW — the plugin system]
1650
+ │ │ ├── definePlugin.ts (factory + validation)
1651
+ │ │ ├── validators.ts (validateAgentTool re-exports
1652
+ │ │ │ @boring/agent/shared)
1653
+ │ │ ├── bootstrap.ts (the single-pass mount loop)
1654
+ │ │ ├── CatalogRegistry.ts (subscribable)
1655
+ │ │ └── defaults/
1656
+ │ │ ├── filesystemPlugin.ts (imports filesystemAgentTools
1657
+ │ │ │ from @boring/agent)
1658
+ │ │ └── dataCatalogPlugin.ts
1659
+ │ ├── registry/
1660
+ │ │ ├── PanelRegistry.ts [EXISTS — retrofitted:
1661
+ │ │ │ subscribable, path-aware
1662
+ │ │ │ micromatch resolver,
1663
+ │ │ │ specificity scoring]
1664
+ │ │ ├── CommandRegistry.ts [EXISTS — retrofitted
1665
+ │ │ │ subscribable]
1666
+ │ │ └── types.ts [EXISTS — adds
1667
+ │ │ 'left-tab'/'right-tab'
1668
+ │ │ placement, pluginId]
1669
+ │ ├── components/
1670
+ │ │ ├── CommandPalette.tsx [EXISTS — refactored to
1671
+ │ │ │ consume useCatalogs();
1672
+ │ │ │ polymorphic Recent;
1673
+ │ │ │ drops fileSearchFn/
1674
+ │ │ │ onOpenFile props]
1675
+ │ │ └── chat/
1676
+ │ │ ├── ChatCenteredShell.tsx [EXISTS — drops `data` +
1677
+ │ │ │ `extraPanels` (KEEPS
1678
+ │ │ │ `chatSuggestions` prop);
1679
+ │ │ │ migrates imperative
1680
+ │ │ │ useEffect command reg]
1681
+ │ │ ├── WorkbenchLeftPane.tsx [EXISTS — registry-driven
1682
+ │ │ │ tabs from PanelRegistry
1683
+ │ │ │ (placement: 'left-tab')]
1684
+ │ │ ├── SurfaceShell.tsx [EXISTS — fallback chain
1685
+ │ │ │ fixed, EmptyFilePanel
1686
+ │ │ │ used when registry +
1687
+ │ │ │ registered fallback both
1688
+ │ │ │ miss]
1689
+ │ │ └── EmptyFilePanel.tsx [NEW — "No editor for X"
1690
+ │ │ panel; replaces ghost-tab
1691
+ │ │ fallback]
1692
+ │ ├── bridge/
1693
+ │ │ └── createInMemoryBridge.ts [MOVED from @boring/agent]
1694
+ │ └── server/
1695
+ │ ├── createWorkspaceAgentApp.ts [EXISTS — wraps createAgentApp
1696
+ │ │ with disableDefaultFileTools:
1697
+ │ │ true; runs bootstrap();
1698
+ │ │ registers substrate routes]
1699
+ │ ├── uiTools.ts [MOVED from @boring/agent —
1700
+ │ │ get_ui_state, exec_ui]
1701
+ │ └── routes/
1702
+ │ ├── ui.ts [MOVED from @boring/agent]
1703
+ │ └── files.ts [MOVED — file/tree/search
1704
+ │ consolidated]
1705
+ └── package.json [EXISTS — adds:
1706
+ "./events": { ... }
1707
+ export]
1708
+
1709
+ apps/boring-macro-v2/
1710
+ ├── src/
1711
+ │ ├── plugin/ [NEW — Step 6]
1712
+ │ │ ├── index.ts (makeMacroPlugin factory +
1713
+ │ │ │ macroChatSuggestions const)
1714
+ │ │ ├── plugin.shared.ts
1715
+ │ │ ├── plugin.client.ts (panels, catalog)
1716
+ │ │ └── plugin.server.ts (agentTools)
1717
+ │ ├── web/App.tsx [EXISTS — shrinks to ~6 LOC]
1718
+ │ └── server/
1719
+ │ ├── index.ts [EXISTS — uses
1720
+ │ │ createWorkspaceAgentApp +
1721
+ │ │ one app.register() for
1722
+ │ │ routes; ~10 LOC]
1723
+ │ ├── macroRoutes.ts [EXISTS — registered via
1724
+ │ │ app.register() in
1725
+ │ │ server/index.ts]
1726
+ │ ├── macroTools.ts [EXISTS — referenced as
1727
+ │ │ plugin.agentTools]
1728
+ │ └── uiBridge.ts [DELETED — 150 LOC; the
1729
+ │ workspace's UI bridge core
1730
+ │ replaces it]
1731
+ ```
1732
+
1733
+ Markers: **[NEW]** **[MOVED]** **[EXISTS]** (modified) **[DELETED]**.
1734
+
1735
+ ## Reorganization (file moves)
1736
+
1737
+ | From | To | Lands as |
1738
+ |---|---|---|
1739
+ | `@boring/agent`: file ops (`find`, `grep`, `read`, `write`, `edit`, `ls`) | **Stays in `@boring/agent`** as `src/server/tools/filesystem/index.ts` (per pi-tools-migration's `buildFilesystemAgentTools(bundle)` factory; pi tools + custom non-pi additions allowed) | Always-on via `createAgentApp`'s `standardCatalog`; opt out with `disableDefaultFileTools: true`. **Not** imported by `filesystemPlugin` — that plugin is UI-only in v7.0+. |
1740
+ | `@boring/agent`: `validateTool` (in pluginLoader.ts) | `@boring/agent/shared/validateTool.ts` (extracted; node-clean) | Imported by pluginLoader; re-exported by `@boring/workspace`'s `validateAgentTool` |
1741
+ | `@boring/agent`: UI bridge tools (`get_ui_state`, `exec_ui`) | `@boring/workspace/server/uiTools.ts` | Core (registered directly by `createWorkspaceAgentApp`) |
1742
+ | `@boring/agent`: file/tree/search HTTP routes | `@boring/workspace/server/routes/files.ts` | Substrate (registered directly) |
1743
+ | `@boring/agent`: UI HTTP routes | `@boring/workspace/server/routes/ui.ts` | Substrate (registered directly) |
1744
+ | `@boring/agent`: `src/shared/ui-bridge.ts` (`UiState`, `UiCommand` types) | `@boring/workspace/src/shared/ui-bridge.ts` | Core types |
1745
+ | `@boring/agent`: `src/server/ui-bridge/createInMemoryBridge.ts` | `@boring/workspace/src/bridge/createInMemoryBridge.ts` | Core |
1746
+
1747
+ `@boring/agent` keeps:
1748
+
1749
+ - `pi-coding-agent` harness (LLM loop, sessions, models)
1750
+ - `AgentTool` type (shared contract)
1751
+ - `validateTool` (now in `/shared`, re-used by workspace)
1752
+ - Pi loader (legacy tools-only; Phase 2 extends)
1753
+ - File ops shared bundle (`filesystemAgentTools`)
1754
+ - `bash`, `execute_isolated_code` tools (harness-only)
1755
+ - Chat / session / model HTTP routes
1756
+ - `createAgentApp` (UI-less; standalone CLI; auto-includes file ops)
1757
+
1758
+ ## Exact path: now → Phase 1 done
1759
+
1760
+ Six sequenced commits, plus a v7.6-mandated **Step 0** at the front
1761
+ to put files into their FINAL v7.6 destinations before Phase 1
1762
+ builds anything else. (Both reviewers in r5 flagged the alternative
1763
+ — building Phase 1 into the OLD flat layout then ripping up later —
1764
+ as wasted work; gemini called it P0.)
1765
+
1766
+ **Meta-rule (v7.6):** when files move, they go DIRECTLY to their
1767
+ final v7.6 destinations. No "move to old path then move again
1768
+ later." Step 0 is the ONE place the existing workspace files
1769
+ restructure; subsequent phases assume the v7.6 layout exists.
1770
+
1771
+ ```
1772
+ ┌──────────────────────────────────────────────────────────────────┐
1773
+ │ STEP 0 — Workspace package reorg into v7.6 layout (NEW; gemini │
1774
+ │ P0 + codex P1) │
1775
+ │ ─────────────────────────────────────────────────────────────── │
1776
+ │ Mechanical git mv of existing workspace src/ files into the │
1777
+ │ v7.6 four-folder layout. NO behavior change. NO new files. One │
1778
+ │ PR. │
1779
+ │ │
1780
+ │ Moves (existing → v7.6 destination): │
1781
+ │ src/components/{ui, DataExplorer, CommandPalette, SessionList,│
1782
+ │ PluginErrorBoundary?} → src/front/components/ │
1783
+ │ src/registry/ → src/front/registry/ │
1784
+ │ src/dock/ → src/front/dock/ │
1785
+ │ src/events/ → src/front/events/ │
1786
+ │ src/hooks/ → src/front/hooks/ │
1787
+ │ src/layouts/ → src/front/layout/ │
1788
+ │ src/panes/{ArtifactSurfacePane.tsx, → to be moved into │
1789
+ │ EmptyPane.tsx} chrome/ in Phase A │
1790
+ │ src/panes/{code-editor, markdown-editor, file-tree}/ │
1791
+ │ → to be moved INTO │
1792
+ │ filesystemPlugin in │
1793
+ │ Step 3 (NOT to │
1794
+ │ front/panes/) │
1795
+ │ src/panes/data-catalog/ → audit + delete OR │
1796
+ │ move to front/ │
1797
+ │ components/ │
1798
+ │ src/plugin/{types, definePlugin, → src/shared/plugin/ │
1799
+ │ bootstrap}.ts (the SHARED parts) │
1800
+ │ src/plugin/{CatalogRegistry, → src/front/plugin/ │
1801
+ │ use*.ts, index.ts} │
1802
+ │ src/store/, src/testing/, src/types/ → audit; fold into │
1803
+ │ shared/ if minimal │
1804
+ │ src/server/ (already in src/server;│
1805
+ │ stays; minor file │
1806
+ │ renames if needed) │
1807
+ │ src/shared/ (already in src/shared;│
1808
+ │ gains plugin/ subdir│
1809
+ │ from above) │
1810
+ │ │
1811
+ │ tsconfig changes: │
1812
+ │ - tsconfig.front.json adds excludes for │
1813
+ │ src/plugins/**/server/**, src/plugin/server/** (codex P1) │
1814
+ │ - tsconfig.shared.json (if exists) doesn't include DOM lib │
1815
+ │ │
1816
+ │ Deliverable: src/ has front/, server/, shared/, plugins/ at top │
1817
+ │ level. ALL existing tests pass unchanged (vi.mock paths follow │
1818
+ │ moved files). No new functionality. │
1819
+ │ ETA: 1-2 days. │
1820
+ └──────────────────────────────────────────────────────────────────┘
1821
+
1822
+
1823
+ ┌──────────────────────────────────────────────────────────────────┐
1824
+ │ STEP 1 — REORG (no plugin model yet, pure refactor) │
1825
+ │ ─────────────────────────────────────────────────────────────── │
1826
+ │ 1a. UI bridge ownership refactor │
1827
+ │ Move ui-bridge types/tools/routes from @boring/agent → │
1828
+ │ @boring/workspace. boring-macro deletes its 150-LOC inline │
1829
+ │ copy. │
1830
+ │ │
1831
+ │ 1b. File ops bundle extraction │
1832
+ │ Extract find/grep/read/write/edit into │
1833
+ │ @boring/agent/server/tools/filesystem (a shared bundle). │
1834
+ │ standardCatalog imports the bundle by default; expose │
1835
+ │ `disableDefaultFileTools` on createAgentApp. Move file/ │
1836
+ │ tree/search HTTP routes to @boring/workspace/server. │
1837
+ │ standardCatalog tools (bash, execute_isolated_code) stay. │
1838
+ │ │
1839
+ │ ETA: 1–2 days. │
1840
+ └──────────────────────────────────────────────────────────────────┘
1841
+
1842
+
1843
+ ┌──────────────────────────────────────────────────────────────────┐
1844
+ │ STEP 2 — PLUGIN PRIMITIVES │
1845
+ │ ─────────────────────────────────────────────────────────────── │
1846
+ │ 2a. validateTool extraction │
1847
+ │ Extract from pluginLoader.ts (node-leaky module) into │
1848
+ │ @boring/agent/shared/validateTool.ts (no node imports). │
1849
+ │ pluginLoader imports the extracted version. │
1850
+ │ │
1851
+ │ 2b. Plugin type + definePlugin + validators │
1852
+ │ packages/workspace/src/plugin/{types,definePlugin, │
1853
+ │ validators,bootstrap}.ts. validateAgentTool re-exports │
1854
+ │ from @boring/agent/shared. Single-pass bootstrap. │
1855
+ │ │
1856
+ │ 2c. CatalogRegistry (new) + subscribe retrofit for existing │
1857
+ │ CommandRegistry + PanelRegistry. │
1858
+ │ │
1859
+ │ 2d. Path-aware file-pattern resolver upgrade │
1860
+ │ Replace basename-only matcher (PanelRegistry.ts:91 + │
1861
+ │ SurfaceShell.tsx:98) with path-aware micromatch │
1862
+ │ ({ matchBase: false, dot: true }) + specificity-scoring │
1863
+ │ (segments × 10 + non-wildcard chars). Preserve the │
1864
+ │ app-beats-builtin source tie-breaker. │
1865
+ │ │
1866
+ │ ETA: 1.5–2 days. │
1867
+ └──────────────────────────────────────────────────────────────────┘
1868
+
1869
+
1870
+ ┌──────────────────────────────────────────────────────────────────┐
1871
+ │ STEP 3 — DEFAULT PLUGINS │
1872
+ │ ─────────────────────────────────────────────────────────────── │
1873
+ │ 3a. filesystemPlugin (sole default; v7.1 UI-only) │
1874
+ │ Plain module-scope const (NO factory, NO agentTools field). │
1875
+ │ Contributes: Files catalog; FileTree (placement: │
1876
+ │ 'left-tab'); CodeEditor + MarkdownEditor (with │
1877
+ │ filePatterns). File ops tools are HARNESS substrate │
1878
+ │ registered by createAgentApp via pi-tools-migration's │
1879
+ │ buildFilesystemAgentTools(bundle) — NOT the plugin's job. │
1880
+ │ │
1881
+ │ (v6 had dataCatalogPlugin as a second default; v6.2 cuts │
1882
+ │ it — plugins that want a workbench data tab contribute │
1883
+ │ their own left-tab panel.) │
1884
+ │ │
1885
+ │ ETA: 0.5 day. │
1886
+ └──────────────────────────────────────────────────────────────────┘
1887
+
1888
+
1889
+ ┌──────────────────────────────────────────────────────────────────┐
1890
+ │ STEP 4 — ENTRY POINTS │
1891
+ │ ─────────────────────────────────────────────────────────────── │
1892
+ │ 4a. <WorkspaceProvider plugins={…}> │
1893
+ │ Adds plugins prop + excludeDefaults prop. Auto-registers │
1894
+ │ defaults; runs bootstrap(). Adds package.json "./events" │
1895
+ │ export so plugins can `import { events } from │
1896
+ │ "@boring/workspace/events"`. │
1897
+ │ │
1898
+ │ 4b. createWorkspaceAgentApp({ plugins }) │
1899
+ │ Plain wrap of createAgentApp (NO disableDefaultFileTools │
1900
+ │ passed — v7.0 simplification: harness owns tools always). │
1901
+ │ Runs bootstrap() for server-side fan-in. Registers │
1902
+ │ substrate routes (/api/v1/ui/*, /files, /tree, /files/ │
1903
+ │ search) directly. │
1904
+ │ │
1905
+ │ ETA: 1 day. │
1906
+ └──────────────────────────────────────────────────────────────────┘
1907
+
1908
+
1909
+ ┌──────────────────────────────────────────────────────────────────┐
1910
+ │ STEP 5 — CONSUMER REFACTORS │
1911
+ │ ─────────────────────────────────────────────────────────────── │
1912
+ │ 5a. <CommandPalette /> consumes useCatalogs() + polymorphic │
1913
+ │ Recent. Drop dead fileSearchFn / onOpenFile props. │
1914
+ │ Migrate existing localStorage Recent entries. │
1915
+ │ │
1916
+ │ 5b. <ChatCenteredShell /> migration (REVISED — gemini P0) │
1917
+ │ - ALL ChatCenteredShell-internal commands STAY as │
1918
+ │ imperative useEffect+registerCommand calls (toggleDrawer/ │
1919
+ │ toggleSurface/newChat AND per-session quick-switch). │
1920
+ │ Reason (gemini): toggleDrawer/toggleSurface close over │
1921
+ │ local useState — a module-scope "internal chat-shell │
1922
+ │ plugin" can't reach component instance state, and │
1923
+ │ bridging via events would just shift the same closure │
1924
+ │ problem to the event handler. Keeping these imperative │
1925
+ │ is honest: the plugin model is for module-stable │
1926
+ │ contributions; component-instance commands belong inside │
1927
+ │ the component. │
1928
+ │ - Registry's subscribe retrofit (Step 2c) ensures these │
1929
+ │ late registrations propagate to an open palette — that │
1930
+ │ was the original justification for the retrofit. │
1931
+ │ - Drop `data: DataPaneConfig` prop. Hosts that want a │
1932
+ │ workbench data tab register their own left-tab panel │
1933
+ │ or compose the reusable `createDataCatalogPlugin(opts)` │
1934
+ │ / `appendDataCatalogOutputs(...)` helpers. │
1935
+ │ - Drop `extraPanels` prop. Panels come from PanelRegistry; │
1936
+ │ new optional `allowedPanels?: string[]` for gating. │
1937
+ │ - KEEP `chatSuggestions: ChatSuggestion[]` prop. │
1938
+ │ │
1939
+ │ 5c. WorkbenchLeftPane registry-driven + SurfaceShell fallback │
1940
+ │ fix │
1941
+ │ - Read 'left-tab' panels from PanelRegistry. defaults │
1942
+ │ contribute their respective tabs. excludeDefaults: │
1943
+ │ ['filesystem'] truly removes the tab. │
1944
+ │ - Replace SurfaceShell.tsx:81-108 hardcoded fallback with: │
1945
+ │ registry resolve → registered fallback (only if has()) → │
1946
+ │ EmptyFilePanel. │
1947
+ │ - 'right-tab' placement reserved; no Phase 1 consumer. │
1948
+ │ │
1949
+ │ ETA: 1.5 days. │
1950
+ └──────────────────────────────────────────────────────────────────┘
1951
+
1952
+
1953
+ ┌──────────────────────────────────────────────────────────────────┐
1954
+ │ STEP 6 — ACCEPTANCE: BORING-MACRO MIGRATION │
1955
+ │ ─────────────────────────────────────────────────────────────── │
1956
+ │ See §"Concrete before/after". Net: ~260 LOC → ~30 LOC. │
1957
+ │ ETA: 0.5–1 day. │
1958
+ └──────────────────────────────────────────────────────────────────┘
1959
+
1960
+
1961
+ ┌──────────────────────────────────────────────────────────────────┐
1962
+ │ STEP 7 — TESTS + RELEASE NOTES │
1963
+ │ ─────────────────────────────────────────────────────────────── │
1964
+ │ Tests per §Test plan. Release notes documenting the THREE │
1965
+ │ breaking changes (CommandPaletteProps, │
1966
+ │ ChatCenteredShellProps.{data,extraPanels,withCommandPalette}, │
1967
+ │ WorkbenchLeftPane internal tab API). ETA: 1–2 days. │
1968
+ └──────────────────────────────────────────────────────────────────┘
1969
+
1970
+ TOTAL: ~6–8 days of focused work.
1971
+ ```
1972
+
1973
+ ## Concrete before/after — boring-macro migration
1974
+
1975
+ Acceptance test for the model.
1976
+
1977
+ ### BEFORE (today)
1978
+
1979
+ ```ts
1980
+ // apps/boring-macro-v2/src/web/App.tsx — ~80 LOC
1981
+ const dataPaneConfig: DataPaneConfig = { /* …seriesAdapter, filesAdapter… */ }
1982
+ const macroPanels: PanelConfig[] = [chartCanvasPanel, deckPanel]
1983
+ const macroChatSuggestions = [/* …8 suggestions… */]
1984
+
1985
+ export function App() {
1986
+ return (
1987
+ <WorkspaceProvider panels={macroPanels}>
1988
+ <ChatCenteredShell
1989
+ data={dataPaneConfig}
1990
+ chatSuggestions={macroChatSuggestions}
1991
+ extraPanels={macroPanels}
1992
+ />
1993
+ </WorkspaceProvider>
1994
+ )
1995
+ }
1996
+
1997
+ // apps/boring-macro-v2/src/server/index.ts — ~30 LOC
1998
+ const clickhouse = await createClickHouseClient(env)
1999
+ const app = await createAgentApp({
2000
+ workspaceRoot,
2001
+ extraTools: [...macroAgentTools, ...uiTools],
2002
+ })
2003
+ await app.register(uiRoutes)
2004
+ await app.register(registerMacroRoutes, { clickhouse, deckRoot })
2005
+ await app.listen({ port })
2006
+
2007
+ // apps/boring-macro-v2/src/server/uiBridge.ts — ~150 LOC
2008
+ // Full inlined copy of @boring/workspace/server's UI bridge.
2009
+ ```
2010
+
2011
+ ### AFTER (Phase 1 done)
2012
+
2013
+ **Two plugin objects, same id, split by environment** (this honors
2014
+ the build invariant — never cross-import `node:*` symbols into
2015
+ client code; codex round-3 P2 caught the v6 example violating its
2016
+ own rule):
2017
+
2018
+ ```ts
2019
+ // apps/boring-macro-v2/src/plugin/index.ts — CLIENT entry, ~18 LOC
2020
+ "use client"
2021
+ import { definePlugin, type Plugin } from "@boring/workspace"
2022
+ import type { ChatSuggestion } from "@boring/agent/front-shadcn"
2023
+ import { chartCanvasPanel, deckPanel, macroSeriesPanel } from "./panels"
2024
+ import { seriesCatalog } from "./catalogs"
2025
+
2026
+ export const macroChatSuggestions: ChatSuggestion[] = [
2027
+ { label: "Find a series", prompt: "Help me find a macro series." },
2028
+ // …
2029
+ ]
2030
+
2031
+ export const makeMacroClientPlugin = (): Plugin =>
2032
+ definePlugin({
2033
+ id: "boring-macro",
2034
+ label: "Macro",
2035
+ panels: [chartCanvasPanel, deckPanel, macroSeriesPanel],
2036
+ catalogs: [seriesCatalog],
2037
+ })
2038
+ ```
2039
+
2040
+ ```ts
2041
+ // apps/boring-macro-v2/src/plugin/server.ts — SERVER entry, ~10 LOC
2042
+ "use server"
2043
+ import { definePlugin, type Plugin } from "@boring/workspace"
2044
+ import { macroAgentTools } from "../server/macroTools"
2045
+
2046
+ export const makeMacroServerPlugin = (): Plugin =>
2047
+ definePlugin({
2048
+ id: "boring-macro", // same id as client; bootstrap dedupes
2049
+ label: "Macro",
2050
+ agentTools: macroAgentTools,
2051
+ })
2052
+ ```
2053
+
2054
+ `macroSeriesPanel` is the workbench-data-tab equivalent of the v5
2055
+ `data: DataPaneConfig`: a panel with `placement: 'left-tab'` whose
2056
+ component is `DataExplorer` configured with macro's adapter and
2057
+ `onActivate` that calls `surface.openPanel({ component:
2058
+ "chart-canvas", … })`. This replaces the v5 dataPaneConfig wiring
2059
+ without needing a default `dataCatalogPlugin`. The catalog
2060
+ (`seriesCatalog`) stays separate — it's what powers the cmd palette
2061
+ search; the panel is what shows the workbench browser.
2062
+
2063
+ ```tsx
2064
+ // apps/boring-macro-v2/src/web/App.tsx — ~7 LOC
2065
+ import { WorkspaceProvider, ChatCenteredShell } from "@boring/workspace"
2066
+ import { makeMacroClientPlugin, macroChatSuggestions } from "../plugin"
2067
+
2068
+ export const App = () => (
2069
+ <WorkspaceProvider plugins={[makeMacroClientPlugin()]}>
2070
+ <ChatCenteredShell chatSuggestions={macroChatSuggestions} />
2071
+ </WorkspaceProvider>
2072
+ )
2073
+ ```
2074
+
2075
+ ```ts
2076
+ // apps/boring-macro-v2/src/server/index.ts — ~11 LOC
2077
+ import { createWorkspaceAgentApp } from "@boring/workspace/server"
2078
+ import { makeMacroServerPlugin } from "../plugin/server"
2079
+ import { registerMacroRoutes } from "./macroRoutes"
2080
+
2081
+ const clickhouse = await createClickHouseClient(env)
2082
+ const app = await createWorkspaceAgentApp({
2083
+ workspaceRoot,
2084
+ plugins: [makeMacroServerPlugin()],
2085
+ })
2086
+ await app.register(registerMacroRoutes, { clickhouse, deckRoot })
2087
+ await app.listen({ port })
2088
+
2089
+ // DELETED: apps/boring-macro-v2/src/server/uiBridge.ts (~150 LOC)
2090
+ ```
2091
+
2092
+ ### LOC accounting
2093
+
2094
+ | File | Before | After | Δ |
2095
+ |---|--:|--:|--:|
2096
+ | `src/web/App.tsx` | 80 | 6 | -74 |
2097
+ | `src/server/index.ts` | 30 | 11 | -19 |
2098
+ | `src/server/uiBridge.ts` | 150 | 0 | -150 |
2099
+ | `src/plugin/index.ts` (client) | 0 | 18 | +18 |
2100
+ | `src/plugin/server.ts` | 0 | 10 | +10 |
2101
+ | **Total** | **260** | **46** | **-214 (-82%)** |
2102
+
2103
+ ## Known gaps — deferred to Phase 2+
2104
+
2105
+ Things v6.1 deliberately does NOT solve, listed explicitly so
2106
+ reviewers don't think they were missed.
2107
+
2108
+ | Gap | Why deferred | When to revisit |
2109
+ |---|---|---|
2110
+ | **Hot-reload / unregister cleanup** | Fastify routes can't unregister; React subscriptions clean themselves; catalog adapters with their own state would leak. No Phase 1 plugin uses this. | Phase 3 hot-reload story. |
2111
+ | **Plugin-vs-substrate tool name collision** | A plugin shipping a tool called `read` replaces substrate's. Late-wins logged. Could confuse the LLM if names diverge mid-session. | When agent-authored plugins (Phase 3) make accidental collision likely. |
2112
+ | **Catalog adapter memory** | If an adapter holds a long-running subscription (e.g., websocket to a remote search service), there's no place to clean up because there's no `onUnmount`. | When a real adapter needs it. Adding a teardown hook on `CatalogConfig` is non-breaking. |
2113
+ | **Non-React stateful adapters need lifecycle** | Gemini P1: an adapter that wants to subscribe to `events.on('file:moved', …)` to invalidate its cache has nowhere to do it. Module-scope `events.on(...)` fires globally for all hosts. Lazy on-first-call leaks (no unsubscribe). | First plugin that needs it. Re-introduce `Plugin.onMount(ctx) → cleanup` (we cut it in v6 because Phase 1 plugins are all React-component-based or factory-injected; that assumption breaks for stateful adapters). |
2114
+ | **`registerAgentRoutes` lacks `disableDefaultFileTools`** | Codex round-4 P1: `createAgentApp` accepts `disableDefaultFileTools` (verified `createAgentApp.ts:47`) but `registerAgentRoutes` (the embedded-Fastify path used by `apps/full-app/src/server/main.ts:383`) does NOT — it always includes filesystem tools (`registerAgentRoutes.ts:94`). The "harness opt-out" promise only holds for the standalone `createAgentApp` path. | Add `disableDefaultFileTools` option to `registerAgentRoutes` plumbed through to its internal tool-catalog construction. **Out of scope for Phase 1** macro acceptance (macro uses `createWorkspaceAgentApp` which uses `createAgentApp`, not `registerAgentRoutes`). Add a follow-up bead under the j9p7 epic OR Phase 2 if a `registerAgentRoutes`-using host needs the opt-out. |
2115
+ | **Build-time enforcement of client/server split** | Documented invariant; not a custom lint rule. | If accidental cross-imports become common. |
2116
+ | **Plugin versioning / compat** | `Plugin.version` field cut. No compat negotiation. | Phase 2 npm distribution. |
2117
+ | **Layout migrations** | Renaming a panel id breaks cached dockview layouts. Same problem as today; not made worse by plugin model. | When layouts become stable enough to merit migration tooling. |
2118
+ | **System-prompt augmentation from registered plugins** | LLM doesn't currently see "these plugins are loaded; here's what they do." | Phase 2 discovery endpoint + prompt injection. |
2119
+ | **Permission / capability gating** | Inline plugins run with full host privileges. | Phase 3 sandbox / capability flags. |
2120
+ | **Per-plugin telemetry** | No per-plugin error counter / call latency telemetry. | When debug volume justifies it. |
2121
+ | ~~`<PluginInspector />`~~ | **PROMOTED to Phase 1 in v7.2** — see §"Phase 1 debug overlay". | (no longer deferred) |
2122
+ | **Plugin discovery via `/api/v1/plugins`** | Phase 1 hosts know what they imported. | Phase 2 npm + agent-authored. |
2123
+
2124
+ None of these block the boring-macro acceptance test.
2125
+
2126
+ ## Phase 1.5: Consumer migration — decompose shells, declarative layouts, delete ChatCenteredShell
2127
+
2128
+ (v7.4 merger: was tracked separately as `DECLARATIVE_LAYOUT_MIGRATION.md` + epic `boring-ui-v2-zrby`. Folded in here as Phase 1.5 because it's the natural consumer-side migration of the plugin model machinery.)
2129
+
2130
+ ### Why this phase exists
2131
+
2132
+ `@boring/workspace` ships TWO parallel layout systems today:
2133
+
2134
+ 1. **Imperative shell** — `ChatCenteredShell` (29KB) + `SurfaceShell` (23KB) + `ChatTopBar` + `SessionBrowser` + `WorkbenchLeftPane` + `ChatStagePlaceholder`, all under `src/components/chat/`. Hardcodes the chat panel from `@boring/agent`, top-bar slots, session drawer, artifact dockview. Apps pass props in; they cannot restructure.
2135
+
2136
+ 2. **Declarative layouts** — `ChatLayout` + `IdeLayout` + `ResponsiveDockviewShell` in `src/layouts/`. Compose panels by id (e.g. `<ChatLayout nav="session-list" center="chat" surface="artifact-surface" />`). Plugins register panels into the registry; the layout config resolves ids → components via dockview.
2137
+
2138
+ Today every consuming app (`apps/workspace-playground`, `apps/full-app`, `apps/boring-macro-v2`) uses **only the imperative shell**. The declarative layouts have **zero app consumers** — tested and exported but never reached.
2139
+
2140
+ The plugin model machinery (Phase 1 Steps 1-5) finally makes declarative composition viable: panels register via the contract; layouts resolve ids → components. **Phase 1.5 retires the imperative shell and migrates all three apps to the declarative pattern**, closing the loop the earlier sketches opened.
2141
+
2142
+ ### Three-tier API after migration
2143
+
2144
+ All three tiers share the SAME core panel registrations (chat, session-list, workbench-left, artifact-surface). Only the shape composition differs.
2145
+
2146
+ #### Tier 1 — declarative pre-shaped layouts (~80% of apps)
2147
+
2148
+ ```tsx
2149
+ import { WorkspaceProvider, ChatLayout, TopBar } from "@boring/workspace"
2150
+ import { macroPlugin } from "./macro-plugin"
2151
+
2152
+ <WorkspaceProvider plugins={[macroPlugin]}>
2153
+ <TopBar appTitle="Macro" right={<UserMenu />} />
2154
+ <ChatLayout
2155
+ nav="session-list"
2156
+ center="chat"
2157
+ sidebar="charts"
2158
+ surface="artifact-surface"
2159
+ />
2160
+ </WorkspaceProvider>
2161
+ ```
2162
+
2163
+ Apps swap panels by changing `nav` / `center` / `sidebar` / `surface` ids. Plugins register the implementations.
2164
+
2165
+ #### Tier 2 — custom LayoutConfig with stock chrome
2166
+
2167
+ ```tsx
2168
+ import { WorkspaceProvider, ResponsiveDockviewShell, TopBar, type LayoutConfig } from "@boring/workspace"
2169
+
2170
+ const myLayout: LayoutConfig = {
2171
+ version: "2.0",
2172
+ groups: [
2173
+ { id: "rail", position: "left", panel: "session-list", locked: true, hideHeader: true },
2174
+ { id: "tree", position: "left", panel: "filetree", collapsible: true },
2175
+ { id: "center", position: "center", panel: "chat" },
2176
+ { id: "split-a", position: "right", panel: "code-editor" },
2177
+ { id: "split-b", position: "right", panel: "live-preview" },
2178
+ ],
2179
+ }
2180
+
2181
+ <WorkspaceProvider plugins={[livePreviewPlugin]}>
2182
+ <TopBar appTitle="…" />
2183
+ <ResponsiveDockviewShell layout={myLayout} />
2184
+ </WorkspaceProvider>
2185
+ ```
2186
+
2187
+ For apps that need non-stock layout shapes (split center, multiple right surfaces, custom group constraints) but still want responsive sidebar collapse + dockview integration + same `<TopBar>` chrome.
2188
+
2189
+ #### Tier 3 — full custom (rare)
2190
+
2191
+ ```tsx
2192
+ import {
2193
+ WorkspaceProvider,
2194
+ DockviewShell,
2195
+ useViewportBreakpoint,
2196
+ useResponsiveSidebarCollapse,
2197
+ useTopBarSlot,
2198
+ } from "@boring/workspace"
2199
+
2200
+ function MyShell() {
2201
+ // App fully composes chrome + dockview structure
2202
+ }
2203
+ ```
2204
+
2205
+ Raw primitives. For apps with bespoke chrome (non-rectangular layout, multiple dockview instances, embedded workspace inside a non-workspace shell).
2206
+
2207
+ ### Substrate vs plugin (architectural commitment)
2208
+
2209
+ The migration is a chance to clarify what's substrate vs. plugin:
2210
+
2211
+ | Tier | What it is | Examples |
2212
+ |---|---|---|
2213
+ | **Substrate** | Constitutive workspace panels. Without them `@boring/workspace` is an empty dockview. Apps cannot opt out — these ARE what the package is. | `chat`, `session-list`, `workbench-left`, `artifact-surface` |
2214
+ | **Default plugins** | Optional capabilities apps can disable via `excludeDefaults: ['filesystem']`. | `filesystemPlugin` (file tree + code editor + markdown editor + filesCatalog) |
2215
+ | **App plugins** | Host-specific contributions registered via `<WorkspaceProvider plugins={[...]}>`. | `macroPlugin` (charts + slides + series), future `analyticsPlugin`, etc. |
2216
+
2217
+ **There is no `chatExperiencePlugin`.** Chat is core, not a plugin. The earlier sketch of a "chat experience plugin" bundling the chat-flavored panels was wrong — it conflates substrate with extension.
2218
+
2219
+ `WorkspaceProvider` registers core panels at mount, before running the plugin bootstrap:
2220
+
2221
+ ```ts
2222
+ function WorkspaceProvider({ plugins, excludeDefaults, children }) {
2223
+ const panelRegistry = new PanelRegistry()
2224
+ panelRegistry.registerAll(coreWorkspacePanels) // ← substrate, always
2225
+ bootstrap({
2226
+ plugins,
2227
+ defaults: filteredDefaults, // filesystemPlugin unless excluded
2228
+ registries,
2229
+ })
2230
+ // …
2231
+ }
2232
+ ```
2233
+
2234
+ ### File-tree end state (v7.5 — final framing)
2235
+
2236
+ Three architectural commitments locked in v7.5:
2237
+
2238
+ 1. **Top-level split: `front/` + `server/` + `shared/` + `plugins/`.**
2239
+ Mirrors `@boring/agent`'s convention. `front/` = client-side
2240
+ workspace code; `server/` = backend; `shared/` = cross-process
2241
+ types (used by both halves). `plugins/` is a sibling concern
2242
+ holding **actual plugin instances only** (no machinery —
2243
+ machinery lives in `front/plugin/` + `server/plugin/` + `shared/plugin/`).
2244
+
2245
+ 2. **Plugins own both halves of their contributions.** A plugin in
2246
+ `plugins/<id>/` contains internal `front/` + `server/` directories
2247
+ for its own client-side panels/sidebar/catalogs and server-side
2248
+ tools/routes. Apps follow the same shape via `apps/<host>/src/plugin/{front,server}/`.
2249
+
2250
+ 3. **Singular vs plural naming convention.**
2251
+ - `plugin/` (singular) = plugin model **machinery** (definePlugin,
2252
+ bootstrap, hooks, registries) — lives in `front/plugin/`,
2253
+ `server/plugin/`, `shared/plugin/`.
2254
+ - `plugins/` (plural) = a directory of plugin **instances** —
2255
+ lives at the workspace root for defaults; apps with multiple
2256
+ plugins use `apps/<host>/src/plugins/`.
2257
+
2258
+ ```
2259
+ packages/workspace/src/
2260
+
2261
+ # ─── FRONT (workspace own client + plugin model client machinery) ──
2262
+
2263
+ front/
2264
+ ├── chrome/ shell parts (NOT plugin contributions)
2265
+ │ ├── chat/ NEW (Phase A — substrate chat panel wrapper)
2266
+ │ │ ├── ChatPanel.tsx thin wrapper around @boring/agent's ChatPanel
2267
+ │ │ │ + workspace integrations
2268
+ │ │ └── definition.ts definePanel({ id: "chat", … })
2269
+ │ ├── session-list/ NEW (Phase A — was components/chat/SessionBrowser)
2270
+ │ ├── workbench-left/ NEW (Phase A — tab-strip chrome that HOSTS
2271
+ │ │ sidebar tabs contributed by plugins)
2272
+ │ ├── artifact-surface/ NEW (Phase A — dockview wrapper that HOSTS
2273
+ │ │ workbench panes contributed by plugins)
2274
+ │ ├── chat-stage-placeholder/ NEW (Phase A)
2275
+ │ └── EmptyPane.tsx MOVED (was loose panes/EmptyPane.tsx)
2276
+
2277
+ ├── components/ cross-cutting UI primitives (NOT panels)
2278
+ │ ├── DataExplorer/ generic data subsystem; used by plugins
2279
+ │ ├── ui/ shadcn primitives
2280
+ │ ├── CommandPalette.tsx substrate palette overlay
2281
+ │ ├── SessionList.tsx data-list helper used by chrome/session-list/
2282
+ │ └── PluginErrorBoundary.tsx NEW (j9p7.22) — wraps plugin contributions
2283
+
2284
+ ├── registry/ base registries (subscribe-aware singletons)
2285
+ │ ├── PanelRegistry.ts retrofitted (j9p7.6 done)
2286
+ │ ├── CommandRegistry.ts retrofitted (j9p7.6 done)
2287
+ │ ├── RegistryProvider.tsx React context anchor
2288
+ │ ├── coreRegistrations.ts NEW (Phase B — coreWorkspacePanels[])
2289
+ │ ├── types.ts PanelConfig, CommandConfig, PaneProps
2290
+ │ └── getFileIcon.ts
2291
+
2292
+ ├── dock/ DockviewShell + LayoutConfig types
2293
+ ├── events/ bus singleton (client side)
2294
+ ├── bridge/ UI bridge client + uiCommandStream/Dispatcher
2295
+ ├── hooks/ viewport, sidebar, etc.
2296
+
2297
+ ├── layout/ composition layer (Tier 1/2/3)
2298
+ │ ├── ChatLayout.tsx
2299
+ │ ├── IdeLayout.tsx
2300
+ │ ├── ResponsiveDockviewShell.tsx
2301
+ │ ├── TopBar.tsx NEW (Phase C — was components/chat/ChatTopBar)
2302
+ │ └── index.ts
2303
+
2304
+ ├── chrome/empty-file-panel/ NEW (j9p7.12 — fallback for unmatched
2305
+ │ files; relocated from panes/ in v7.6
2306
+ │ per gemini P2 — kills the lone-resident
2307
+ │ panes/ folder at workspace root)
2308
+
2309
+ ├── plugin/ ← plugin model FRONT machinery (singular)
2310
+ │ ├── CatalogRegistry.ts (DONE — file at packages/workspace/src/plugin/
2311
+ │ │ today; moves under front/plugin/ in restructure)
2312
+ │ ├── PluginErrorBoundary.tsx NEW (j9p7.22)
2313
+ │ ├── PluginInspector.tsx NEW (j9p7.23, DEV-only)
2314
+ │ ├── usePlugins.ts NEW
2315
+ │ ├── useCatalogs.ts (DONE)
2316
+ │ ├── useCommands.ts (DONE)
2317
+ │ ├── useActivePanels.ts (DONE)
2318
+ │ └── index.ts
2319
+
2320
+ └── WorkspaceProvider.tsx
2321
+
2322
+ # ─── SERVER (workspace own backend + plugin model server machinery) ─
2323
+
2324
+ server/
2325
+ ├── createWorkspaceAgentApp.ts
2326
+ ├── http/ substrate routes (uiRoutes, fileRoutes)
2327
+ ├── ui-bridge/ createInMemoryBridge
2328
+ └── plugin/ ← plugin model SERVER machinery (singular)
2329
+ └── … (server-side registries — currently minimal;
2330
+ AgentToolRegistry interface, etc.)
2331
+
2332
+ # ─── SHARED (types + functions both halves need) ───────────────────
2333
+
2334
+ shared/
2335
+ ├── ui-bridge.ts UiState, UiCommand types
2336
+ ├── events/ WorkspaceEventMap types
2337
+ └── plugin/ ← plugin model SHARED (singular)
2338
+ ├── types.ts Plugin, CatalogConfig (DONE — needs v7.2
2339
+ │ systemPrompt addition per j9p7.4)
2340
+ ├── definePlugin.ts factory + validation (DONE; both halves use)
2341
+ ├── bootstrap.ts single-pass mount (DONE — needs v7.2
2342
+ │ systemPromptAppend output per j9p7.7)
2343
+ └── index.ts
2344
+
2345
+ # ─── PLUGINS (actual plugin instances; plural) ─────────────────────
2346
+
2347
+ plugins/ ← directory of plugin instances
2348
+ └── filesystemPlugin/ ← one folder per plugin
2349
+ ├── index.ts exports CLIENT + SERVER Plugins (same id)
2350
+ ├── front/ client-side contributions
2351
+ │ ├── panes/
2352
+ │ │ ├── CodeEditorPane.tsx MOVED (was src/panes/code-editor/)
2353
+ │ │ └── MarkdownEditorPane.tsx MOVED (was src/panes/markdown-editor/)
2354
+ │ ├── sidebar/
2355
+ │ │ └── FileTreePane.tsx MOVED (was src/panes/file-tree/)
2356
+ │ └── catalogs/
2357
+ │ └── filesCatalog.ts
2358
+ └── server/ server-side contributions
2359
+ └── … (filesystemPlugin v7 has NONE — UI-only;
2360
+ future plugins add agentTools, route handlers)
2361
+
2362
+ # ─── BARREL ────────────────────────────────────────────────────────
2363
+
2364
+ index.ts package public API (re-exports from front/,
2365
+ layout/, plugin model bits, plugins barrel)
2366
+ ```
2367
+
2368
+ **Apps follow the same shape — singular `plugin/` for the host's one plugin:**
2369
+
2370
+ ```
2371
+ apps/boring-macro-v2/src/
2372
+
2373
+ plugin/ ← THIS app's plugin (singular: one per host)
2374
+ ├── index.ts exports CLIENT + SERVER Plugins (same id)
2375
+ ├── front/ client-side contributions
2376
+ │ ├── panes/
2377
+ │ │ ├── ChartCanvasPane.tsx
2378
+ │ │ └── DeckPane.tsx
2379
+ │ ├── sidebar/MacroSeriesPane.tsx
2380
+ │ └── catalogs/seriesCatalog.ts
2381
+ └── server/ server-side contributions
2382
+ ├── tools/ agent tool implementations
2383
+ │ ├── execute_sql.ts
2384
+ │ ├── macro_search.ts
2385
+ │ ├── get_series_data.ts
2386
+ │ └── persist_derived_series.ts
2387
+ └── routes/macroRoutes.ts Fastify route plugin
2388
+
2389
+ web/App.tsx app front entry (mounts WorkspaceProvider)
2390
+ server/index.ts app backend entry (Fastify boot)
2391
+ ```
2392
+
2393
+ **If an app grows multi-plugin** (rare): rename `plugin/` → `plugins/<id>/` mirroring workspace's convention. Phase 1 macro doesn't need this.
2394
+
2395
+ **Apps follow the same pattern** — every plugin (default or app-contributed) owns its components inside its own directory:
2396
+
2397
+ ```
2398
+ apps/boring-macro-v2/src/plugin/ ← macroPlugin lives here
2399
+ ├── index.ts the Plugin definition (client half)
2400
+ ├── server.ts server-half Plugin (agentTools)
2401
+ ├── panes/ ← workbench-center contributions
2402
+ │ ├── ChartCanvasPane.tsx
2403
+ │ └── DeckPane.tsx
2404
+ ├── sidebar/ ← sidebar-tab contributions
2405
+ │ └── MacroSeriesPane.tsx
2406
+ ├── catalogs/
2407
+ │ └── seriesCatalog.ts
2408
+ └── server/ (paired with src/server/)
2409
+ └── tools/ agent tool implementations
2410
+ ├── execute_sql.ts
2411
+ ├── macro_search.ts
2412
+ └── …
2413
+ ```
2414
+
2415
+ **What disappears:**
2416
+
2417
+ - `src/components/chat/` (whole folder; Phase A + C + G)
2418
+ - Top-level `src/panes/{code-editor, markdown-editor, file-tree}/` — moved INTO
2419
+ `src/plugin/defaults/filesystemPlugin/` (panes/sidebar split per role) by j9p7.9
2420
+ - `src/panes/data-catalog/` — orphaned (dataCatalogPlugin was cut in v6.2);
2421
+ audit during j9p7.30: delete if no consumer, otherwise re-home as a primitive
2422
+ in `components/` for plugin authors who want a generic data-tab implementation
2423
+ - `src/panes/EmptyPane.tsx` (loose) — moved to `chrome/EmptyPane.tsx`
2424
+ - `src/panes/ArtifactSurfacePane.tsx` (loose) — moved to `chrome/artifact-surface/`
2425
+ alongside SurfaceShell (Phase A)
2426
+
2427
+ **What stays exported (Tier 1 / Tier 2 / Tier 3 surface):**
2428
+
2429
+ ```ts
2430
+ // Tier 1
2431
+ export { ChatLayout, IdeLayout, buildChatLayout, buildIdeLayout } from "./layouts"
2432
+ export type { ChatLayoutProps, IdeLayoutProps } from "./layouts"
2433
+ export { TopBar } from "./layouts/TopBar"
2434
+
2435
+ // Tier 2
2436
+ export { ResponsiveDockviewShell } from "./layouts/ResponsiveDockviewShell"
2437
+
2438
+ // Tier 3 (raw primitives)
2439
+ export { DockviewShell } from "./dock"
2440
+ export type { LayoutConfig, GroupConfig } from "./dock"
2441
+ export { useViewportBreakpoint, useResponsiveSidebarCollapse } from "./hooks"
2442
+ export { useTopBarSlot } from "./components/TopBarSlot"
2443
+
2444
+ // WorkspaceProvider + plugin model
2445
+ export { WorkspaceProvider } from "./WorkspaceProvider"
2446
+ export { definePlugin, definePanel, PluginError, … } from "./plugin"
2447
+ export { CatalogRegistry, useCommands, useActivePanels, useCatalogs, usePlugins, … } from "./plugin"
2448
+ ```
2449
+
2450
+ The default `filesystemPlugin` is exported via `plugin/defaults/`; hosts that want
2451
+ to disable it pass `excludeDefaults: ['filesystem']`. The plugin's INTERNAL
2452
+ components (CodeEditorPane etc.) are NOT exported — they're encapsulated.
2453
+
2454
+ ### Why this taxonomy (vs status quo)
2455
+
2456
+ **`panes/` was conflating three distinct categories.** Today
2457
+ `packages/workspace/src/panes/` mixes:
2458
+
2459
+ - Workbench center panes (`code-editor`, `markdown-editor`)
2460
+ - Sidebar tab content (`file-tree`, `data-catalog`)
2461
+ - Shell containers (`ArtifactSurfacePane.tsx`, `EmptyPane.tsx`)
2462
+
2463
+ Each of these has different runtime semantics (centered tab vs persistent sidebar vs chrome wrapper) and different ownership (filesystem-plugin vs filesystem-plugin vs substrate). Mixing them blurred the architecture and made plugin authors uncertain where their contributions belong.
2464
+
2465
+ **Plugin authors need a clear template.** The `panes/`-flat-at-workspace-root pattern told plugin authors "drop your panel here" — but that pattern is only correct for workspace substrate. Plugins should bundle their components, not scatter them. v7.5 makes the right path the obvious path: **everything a plugin contributes lives inside the plugin's directory**, mirroring what Phase 2's npm distribution will require anyway (a distributed plugin SHIPS its components).
2466
+
2467
+ **Apps and defaults follow the SAME pattern.** A `pi-plugin-foo` npm package's `dist/client/` will look identical to `apps/boring-macro-v2/src/plugin/` and `packages/workspace/src/plugin/defaults/filesystemPlugin/`. Same shape; different distribution channel.
2468
+
2469
+ **What disappears:**
2470
+
2471
+ - `src/components/chat/` (whole folder)
2472
+ - `ChatCenteredShell.tsx` + `ChatShellContext` (`context.ts`)
2473
+ - The old `presets.test.tsx` (superseded by migrated apps' e2e + new layout tests)
2474
+ - `src/index.ts` exports for `ChatCenteredShell`, `useChatShell`, `useChatSurface`, `ChatStagePlaceholder` (imperative-shell internals)
2475
+
2476
+ **What stays exported (Tier 1 / Tier 2 / Tier 3 surface):**
2477
+
2478
+ ```ts
2479
+ // Tier 1
2480
+ export { ChatLayout, IdeLayout, buildChatLayout, buildIdeLayout } from "./layouts"
2481
+ export type { ChatLayoutProps, IdeLayoutProps } from "./layouts"
2482
+ export { TopBar } from "./layouts/TopBar"
2483
+
2484
+ // Tier 2
2485
+ export { ResponsiveDockviewShell } from "./layouts/ResponsiveDockviewShell"
2486
+
2487
+ // Tier 3 (raw primitives)
2488
+ export { DockviewShell } from "./dock"
2489
+ export type { LayoutConfig, GroupConfig } from "./dock"
2490
+ export { useViewportBreakpoint, useResponsiveSidebarCollapse } from "./hooks"
2491
+ export { useTopBarSlot } from "./components/TopBarSlot"
2492
+
2493
+ // WorkspaceProvider + plugin model (unchanged)
2494
+ export { WorkspaceProvider } from "./WorkspaceProvider"
2495
+ export { definePlugin, definePanel, PluginError, … } from "./plugin"
2496
+ export { CatalogRegistry, useCommands, useActivePanels, useCatalogs, … } from "./plugin"
2497
+ ```
2498
+
2499
+ ### Phase 1.5 breakdown — 7 phase beads (A through G)
2500
+
2501
+ A and B can run in parallel. C depends on A. D, E, F can run in parallel after A+B+C. G depends on D+E+F.
2502
+
2503
+ ```
2504
+ ┌──────────────────────────────────────────────────────────────────┐
2505
+ │ Phase A — Decompose chat shells into front/chrome/ + front/ │
2506
+ │ bridge/ (v7.6 paths, ONE-GO move) │
2507
+ │ ─────────────────────────────────────────────────────────────── │
2508
+ │ - git mv components/chat/{SessionBrowser, ChatStagePlaceholder, │
2509
+ │ SurfaceShell, WorkbenchLeftPane}.tsx → front/chrome/ │
2510
+ │ {session-list, chat-stage-placeholder, artifact- │
2511
+ │ surface, workbench-left}/ │
2512
+ │ - Each chrome folder gains a definition.ts exporting a │
2513
+ │ PanelConfig (used by Phase B's coreWorkspacePanels) │
2514
+ │ - Create front/chrome/chat/ChatPanel.tsx (thin wrapper around │
2515
+ │ @boring/agent's ChatPanel + workspace integrations) + │
2516
+ │ definition.ts │
2517
+ │ - git mv components/chat/{uiCommandStream, │
2518
+ │ uiCommandDispatcher}.ts → front/bridge/ │
2519
+ │ - Update internal imports │
2520
+ │ - DON'T touch ChatCenteredShell.tsx or ChatTopBar.tsx yet — │
2521
+ │ they stay until Phase G/C respectively. │
2522
+ │ Bead: j9p7.24 │
2523
+ │ │
2524
+ │ Note: Step 0 already ran the workspace reorg into v7.6 layout, │
2525
+ │ so source paths use front/ prefix. ArtifactSurfacePane.tsx and │
2526
+ │ EmptyPane.tsx (loose under panes/ before Step 0) ALSO move to │
2527
+ │ front/chrome/{artifact-surface, EmptyPane.tsx} in this Phase. │
2528
+ └──────────────────────────────────────────────────────────────────┘
2529
+
2530
+
2531
+ ┌──────────────────────────────────────────────────────────────────┐
2532
+ │ Phase B — Wire core panel registrations in WorkspaceProvider │
2533
+ │ ─────────────────────────────────────────────────────────────── │
2534
+ │ Parallelism note (codex P2): can START in parallel with Phase A │
2535
+ │ but cannot CLOSE before Phase A's definition.ts files exist. │
2536
+ │ │
2537
+ │ - Create front/registry/coreRegistrations.ts exporting │
2538
+ │ coreWorkspacePanels: PanelConfig[] aggregating the 4 core │
2539
+ │ chrome panel defs (chat, session-list, workbench-left, │
2540
+ │ artifact-surface). Imports each pane's definition.ts. │
2541
+ │ - front/WorkspaceProvider.tsx imports and registers them at │
2542
+ │ mount, BEFORE bootstrap() runs │
2543
+ │ - Test: render WorkspaceProvider with no plugins; assert the │
2544
+ │ panel registry has the 4 core ids │
2545
+ │ Bead: j9p7.25 (depends on j9p7.24 closing for the actual │
2546
+ │ definition.ts files to import) │
2547
+ └──────────────────────────────────────────────────────────────────┘
2548
+
2549
+
2550
+ ┌──────────────────────────────────────────────────────────────────┐
2551
+ │ Phase C — Lift TopBar chrome + expose ResponsiveDockviewShell │
2552
+ │ ─────────────────────────────────────────────────────────────── │
2553
+ │ - git mv components/chat/ChatTopBar.tsx │
2554
+ │ → front/layout/TopBar.tsx │
2555
+ │ - Rename type ChatTopBarProps → TopBarProps │
2556
+ │ - Update barrel │
2557
+ │ - Export ResponsiveDockviewShell from package barrel with jsdoc │
2558
+ │ explaining Tier 2 │
2559
+ │ - Add §"Three-tier API" to packages/workspace/README.md │
2560
+ │ Bead: j9p7.26 │
2561
+ └──────────────────────────────────────────────────────────────────┘
2562
+
2563
+
2564
+ ┌──────────────────────────────────────────────────────────────────┐
2565
+ │ Phase D — Migrate workspace-playground to ChatLayout (canary) │
2566
+ │ ─────────────────────────────────────────────────────────────── │
2567
+ │ - Rewrite apps/workspace-playground/src/App.tsx to use │
2568
+ │ <WorkspaceProvider> + <TopBar> + <ChatLayout> │
2569
+ │ - Confirm e2e tests (apps/workspace-playground/e2e/*.spec.ts) │
2570
+ │ still pass │
2571
+ │ - Document any gotchas for the next two app migrations │
2572
+ │ Bead: j9p7.27 │
2573
+ └──────────────────────────────────────────────────────────────────┘
2574
+
2575
+
2576
+ ┌──────────────────────────────────────────────────────────────────┐
2577
+ │ Phase E — Migrate boring-macro-v2: ChatLayout + extract │
2578
+ │ macroPlugin (v7.6 paths, ONE-GO move) │
2579
+ │ ─────────────────────────────────────────────────────────────── │
2580
+ │ - Create apps/boring-macro-v2/src/plugin/{index.ts, server.ts, │
2581
+ │ front/, server/} per v7.6 layout. SPLIT entrypoints (codex │
2582
+ │ P1): index.ts exports macroClientPlugin (panels, sidebar, │
2583
+ │ catalogs); server.ts exports macroServerPlugin (agentTools, │
2584
+ │ routes adapter). │
2585
+ │ - front/: panes/ (chart-canvas, deck), sidebar/ (macro-series),│
2586
+ │ catalogs/ (seriesCatalog) │
2587
+ │ - server/: tools/ (execute_sql, macro_search, get_series_data, │
2588
+ │ persist_derived_series), routes/ (macroRoutes) │
2589
+ │ - Rewrite apps/boring-macro-v2/src/web/App.tsx: │
2590
+ │ <WorkspaceProvider plugins={[macroClientPlugin]}> │
2591
+ │ <ChatLayout nav="session-list" center="chat" sidebar= │
2592
+ │ "macro-series" surface="artifact-surface" /> │
2593
+ │ </WorkspaceProvider> │
2594
+ │ - Rewrite apps/boring-macro-v2/src/server/index.ts to use │
2595
+ │ createWorkspaceAgentApp({ plugins: [macroServerPlugin()] }) │
2596
+ │ + app.register(registerMacroRoutes, opts) │
2597
+ │ - macro's uiBridge.ts is ALREADY deleted in Step 1a (NOT here │
2598
+ │ — gemini P2 catch). │
2599
+ │ - Confirm boring-macro e2e tests pass │
2600
+ │ Beads: j9p7.18 (plugin module), j9p7.19 (app refactor), │
2601
+ │ j9p7.20 (e2e gate). Reframed under Phase E. │
2602
+ └──────────────────────────────────────────────────────────────────┘
2603
+
2604
+
2605
+ ┌──────────────────────────────────────────────────────────────────┐
2606
+ │ Phase F — Migrate full-app to ChatLayout (or IdeLayout) │
2607
+ │ ─────────────────────────────────────────────────────────────── │
2608
+ │ - Rewrite apps/full-app/src/front/main.tsx to use the │
2609
+ │ appropriate declarative layout │
2610
+ │ - Confirm e2e tests pass │
2611
+ │ Bead: j9p7.28 │
2612
+ └──────────────────────────────────────────────────────────────────┘
2613
+
2614
+
2615
+ ┌──────────────────────────────────────────────────────────────────┐
2616
+ │ Phase G — Delete legacy chat shell + context + finalize │
2617
+ │ (SHIPPED by j9p7.29) │
2618
+ │ ─────────────────────────────────────────────────────────────── │
2619
+ │ Once D + E + F merged: │
2620
+ │ - Deleted components/chat/ChatCenteredShell.tsx │
2621
+ │ - Deleted components/chat/context.ts │
2622
+ │ - Dropped related exports from src/index.ts │
2623
+ │ - Removed now-empty components/chat/ folder │
2624
+ │ - Updated WORKSPACE_V2_PLAN.md and active package docs │
2625
+ │ Bead: j9p7.29 │
2626
+ └──────────────────────────────────────────────────────────────────┘
2627
+ ```
2628
+
2629
+ ### Phase 1.5 risks
2630
+
2631
+ - **Pixel drift on canary migration.** Tier 1's `<ChatLayout>` may render at slightly different pixel offsets than `<ChatCenteredShell>` (different padding stack, different transition timing on the sidebar). Plan: regenerate visual snapshots when migrating workspace-playground; eyeball the diff for genuine regressions vs cosmetic shifts.
2632
+ - **Plugin id collisions.** boring-macro's existing panels might collide with workspace's core panel ids. Audit during Phase E; rename macro panels to be namespaced (`macro:charts`, `macro:slides`).
2633
+ - **boring-macro custom shell logic.** boring-macro's current App.tsx has more glue than the playground (custom session handling, custom topbar variants). Phase E may surface that some glue should live in `macroPlugin` (commands), and some should stay app-level (host wiring + auth). Will need careful split.
2634
+ - **`uiCommandStream` / `uiCommandDispatcher` re-homing.** They're called from inside `ChatCenteredShell` today. After Phase A's move + WorkspaceProvider's mount-time wiring, they should be invoked from a `useEffect` inside `WorkspaceProvider` (or a dedicated hook). Confirm the lifecycle: the stream must start before the chat panel mounts.
2635
+
2636
+ ### Phase 1.5 ship criteria
2637
+
2638
+ - All 7 phase beads (j9p7.24-29 + j9p7.18-20) closed
2639
+ - Three apps run on declarative layouts in production
2640
+ - `ChatCenteredShell` and `ChatShellContext` deleted from the tree
2641
+ - Public API exposes Tier 1 / Tier 2 / Tier 3 entries with jsdoc
2642
+ - README section in `packages/workspace/` describing the three-tier model with one snippet per tier
2643
+
2644
+ ### Phase 1.5 supersedes / replaces
2645
+
2646
+ The earlier Phase 1 Step 5b ("ChatCenteredShell drops `data` + `extraPanels` props") and Step 5c ("WorkbenchLeftPane registry-driven") are **subsumed by Phase 1.5**:
2647
+
2648
+ - Step 5b's "drop legacy props" becomes "delete the whole shell" (Phase G).
2649
+ - Step 5c's "registry-driven WorkbenchLeftPane" becomes "WorkbenchLeftPane is just one of the core panels registered by WorkspaceProvider; tab strip is gone" (Phase A + B).
2650
+
2651
+ Beads j9p7.16 (Step 5b) and j9p7.17 (Step 5c) close as **scope-shifted to Phase 1.5** — see Phase A/B beads for the new location.
2652
+
2653
+ ## Phase 2/3 (sketched, deferred)
2654
+
2655
+ **Phase 2 — distributable plugins:**
2656
+ - npm sub-path exports pattern (see §Distribution).
2657
+ - Extend pi loader: `extractTools` → `extractPlugin`. Files in
2658
+ `.pi/extensions/` and `node_modules/pi-plugin-*` can export the
2659
+ full `Plugin` shape. Legacy tools-only plugins keep working.
2660
+ - `GET /api/v1/plugins` discovery endpoint for system-prompt
2661
+ augmentation.
2662
+ - Workbench data tab gains catalog selector — picks any registered
2663
+ catalog.
2664
+ - Generic `search_catalog(id, query)` agent tool, auto-generated
2665
+ from registered catalogs.
2666
+
2667
+ **Phase 3 — agent-authored + dynamic:**
2668
+ - `create_plugin` / `update_plugin` agent tools writing to
2669
+ `.pi/extensions/.agent-authored/`.
2670
+ - Plugin hot-reload (requires Fastify-routes workaround).
2671
+ - If/when needed (the dep graph stops being trivial): re-introduce
2672
+ `dependsOn`, `onMount`, lifecycle.
2673
+ - Per-plugin sandboxing / capability flags.
2674
+
2675
+ ## Test plan
2676
+
2677
+ - **Unit**
2678
+ - `definePlugin` validation: well-formed plugin passes;
2679
+ malformed contributions throw with field-level errors.
2680
+ - Bootstrap: defaults register before host plugins;
2681
+ excludeDefaults skips them; plugin contributions appear in
2682
+ per-type registries with correct pluginId; late-wins-on-id
2683
+ replaces and warns in dev.
2684
+ - File-pattern resolution: path-aware micromatch
2685
+ (`deck/**/*.md` matches `deck/labor/labor.md`); specificity
2686
+ ordering (deck/**/*.md beats **/*.md); same-specificity →
2687
+ app-beats-builtin → late-wins; explicit `surface.openPanel`
2688
+ bypasses.
2689
+ - `disableDefaultFileTools: true` removes file ops from
2690
+ standardCatalog; default keeps them.
2691
+ - RecentEntry: catalog-tagged entries render via the right
2692
+ catalog adapter; entries pointing at uninstalled catalogs are
2693
+ dropped; localStorage migration from string entries.
2694
+ - `validateTool` from `@boring/agent/shared/validateTool` works
2695
+ in a non-Node environment (no `node:*` imports leak).
2696
+
2697
+ - **Integration**
2698
+ - `<WorkspaceProvider plugins={[testPlugin]}>` →
2699
+ catalog/command/panel all reachable via their hooks.
2700
+ - `createWorkspaceAgentApp({ plugins: [testPlugin] })` exposes
2701
+ `agentTools` in agent catalog endpoint; substrate routes
2702
+ register.
2703
+ - Cmd palette renders catalogs from registered plugins;
2704
+ error-isolated per group.
2705
+ - `excludeDefaults: ['filesystem']` — Files tab not rendered,
2706
+ file ops not in tool catalog, file routes still served
2707
+ (substrate).
2708
+ - `excludeDefaults: ['dataCatalog']` removes the Data tab.
2709
+ - SurfaceShell fallback: opening a `.foo` file with no matching
2710
+ panel renders `EmptyFilePanel` (not a ghost tab).
2711
+ - allowedPanels gating: when set, only listed panel ids appear
2712
+ in the surface.
2713
+
2714
+ - **E2E**
2715
+ - **boring-macro-v2 existing e2e suite is the Step 6 acceptance
2716
+ gate** — all 10 specs (composer-border, deck, catalog-to-chart,
2717
+ catalog, split-no-clip, layout-persistence, chat-suggestions,
2718
+ chart-tabs, topbar, agent) MUST pass post-migration. The specs
2719
+ are behavior-level; only `App.tsx` and `server/index.ts`
2720
+ reference the deleted props.
2721
+ - Open `deck/labor/labor.md` → DeckPane (not generic
2722
+ MarkdownEditor) — confirms path-aware resolver.
2723
+ - Recent: open file from palette → close + reopen palette →
2724
+ file appears in Recent rendered as file path; run a command,
2725
+ Recent stays files-only.
2726
+
2727
+ ## Acceptance
2728
+
2729
+ ### Baseline protocol (v7.7 — worktree-based)
2730
+
2731
+ Establish a pre-migration baseline of the macro e2e suite using
2732
+ **`git worktree`**, NOT `git stash`. AGENTS.md forbids destructive
2733
+ ops on user state (multi-agent awareness rule); `git stash` against
2734
+ a working tree another agent might be editing is exactly that
2735
+ hazard.
2736
+
2737
+ ```bash
2738
+ BASELINE_REF=<pre-migration-sha-or-tag>
2739
+ WORKTREE_DIR=../baseline-${BASELINE_REF:0:8}
2740
+
2741
+ git worktree add "$WORKTREE_DIR" "$BASELINE_REF"
2742
+ trap 'git worktree remove --force "$WORKTREE_DIR" 2>/dev/null' EXIT
2743
+
2744
+ pushd "$WORKTREE_DIR/apps/boring-macro-v2" >/dev/null
2745
+ pnpm install --frozen-lockfile
2746
+ pnpm exec playwright test --reporter=json > /tmp/macro-e2e-baseline.json
2747
+ popd >/dev/null
2748
+ ```
2749
+
2750
+ Properties:
2751
+
2752
+ - **Idempotent.** Re-running creates a fresh worktree at the same
2753
+ sha; no working-tree state to recover.
2754
+ - **Parallelizable.** Multiple runs use distinct worktree dirs.
2755
+ - **Failure mode is benign.** Crash mid-run leaves
2756
+ `../baseline-<sha>/`; `git worktree list` finds it; `git worktree
2757
+ remove --force` cleans it. Never overwrites user work.
2758
+
2759
+ The post-migration acceptance suite then runs against `HEAD` and
2760
+ diffs results against `/tmp/macro-e2e-baseline.json`. Specs that
2761
+ were green pre-migration must stay green; specs that were red
2762
+ pre-migration are not this gate's job.
2763
+
2764
+ ### Acceptance criteria
2765
+
2766
+ - `Plugin` contract (six fields) + `definePlugin` exported from
2767
+ `@boring/workspace`.
2768
+ - `CatalogRegistry` new; `CommandRegistry` + `PanelRegistry`
2769
+ retrofitted subscribable.
2770
+ - `<WorkspaceProvider plugins={[…]}>` and
2771
+ `createWorkspaceAgentApp({ plugins: [...] })` are the only
2772
+ registration APIs hosts use.
2773
+ - One default plugin: `filesystemPlugin` (UI-only — panels + catalog; no agentTools per v7.0+).
2774
+ Both auto-mount; both individually opt-out-able; opt-out actually
2775
+ removes UI surface (registry-driven workbench tabs +
2776
+ EmptyFilePanel fallback).
2777
+ - File-ops shared bundle in `@boring/agent` so standalone
2778
+ `createAgentApp` stays a real coding agent.
2779
+ - `validateTool` extracted to `@boring/agent/shared` so the client
2780
+ bundle stays node-clean.
2781
+ - Path-aware file-pattern resolver — `deck/**/*.md` works.
2782
+ - `<CommandPalette />` renders catalogs from plugins; old
2783
+ `fileSearchFn`/`onOpenFile` props removed; Recent is polymorphic
2784
+ (catalog-tagged entries) and the type-mix bug is fixed.
2785
+ - `<ChatCenteredShell />` registers its commands declaratively via
2786
+ an internal plugin; legacy `data` + `extraPanels` props deleted;
2787
+ `chatSuggestions` prop kept.
2788
+ - `boring-macro-v2` migrated per §"Concrete before/after": ~260
2789
+ LOC → ~36 LOC. Same user-visible behavior. macro routes
2790
+ registered with `{ clickhouse, deckRoot }` opts via host's
2791
+ `app.register(...)` — one line in server/index.ts.
2792
+ - Three breaking changes (`CommandPaletteProps`,
2793
+ `ChatCenteredShellProps.{data,extraPanels,withCommandPalette}`,
2794
+ `WorkbenchLeftPane` internal tab API) documented.
2795
+ - `package.json` exports `./events`.
2796
+ - All Phase 1 tests + macro e2e suite green.
2797
+
2798
+ ## Open questions
2799
+
2800
+ 1. **Plugin client/server file split — env guard or package.json
2801
+ exports?** Both work. Inline plugins use env guard; npm-published
2802
+ plugins (Phase 2) use package.json `exports`. Documented both.
2803
+ 2. **Discovery endpoint authentication?** Same as other agent routes
2804
+ (session cookie). Phase 2 concern.
2805
+ 3. **Hot-reload of Fastify routes (Phase 3 only).** Today
2806
+ `app.register(plugin)` is irreversible; this is one of the
2807
+ reasons routes aren't on the Plugin contract.
2808
+
2809
+ ## Reference
2810
+
2811
+ - Existing pi plugin loader:
2812
+ `packages/agent/src/server/harness/pi-coding-agent/pluginLoader.ts`
2813
+ - Existing `WorkspaceProvider`:
2814
+ `packages/workspace/src/WorkspaceProvider.tsx`
2815
+ - Existing `<CommandPalette />`:
2816
+ `packages/workspace/src/components/CommandPalette.tsx`
2817
+ (Recent bug at lines 34, 59-60, 157, 230-232)
2818
+ - Existing tool implementations (verified names: `read`, `write`,
2819
+ `edit`, `find`, `grep`):
2820
+ `packages/agent/src/server/catalog/tools/{readTool,writeTool,editTool,findFilesTool,grepFilesTool}.ts`
2821
+ - Workspace tool renderers (keyed to current names):
2822
+ `packages/agent/src/ui-shadcn/workspaceToolRenderers.tsx:30`
2823
+ - Hardcoded workbench tabs (target of step 5c):
2824
+ `packages/workspace/src/components/chat/WorkbenchLeftPane.tsx:97,174,181`
2825
+ - ChatCenteredShell legacy props (target of step 5b):
2826
+ `packages/workspace/src/components/chat/ChatCenteredShell.tsx:45,116,637`
2827
+ + `packages/workspace/src/components/chat/SurfaceShell.tsx:466`
2828
+ - File-pattern resolver to upgrade in step 2d:
2829
+ `packages/workspace/src/registry/PanelRegistry.ts:91` +
2830
+ `packages/workspace/src/components/chat/SurfaceShell.tsx:98`
2831
+ - ExplorerAdapter (catalog adapter contract):
2832
+ `packages/workspace/src/components/DataExplorer/types.ts`
2833
+ - ChatSuggestion + ChatEmptyState (kept as-is):
2834
+ `packages/agent/src/front-shadcn/ChatEmptyState.tsx`
2835
+ - Boring-macro-v2 host (the migration target — `src/web/`, not
2836
+ `src/front/`; uiBridge.ts confirmed at 9.3 KB):
2837
+ `/home/ubuntu/projects/boring-macro-v2/src/{server/index.ts,
2838
+ web/App.tsx, server/macroTools.ts, server/uiBridge.ts,
2839
+ server/macroRoutes.ts}`
2840
+ - Sibling plans:
2841
+ - `UNIFIED_EVENT_BUS.md` — bus model (already implemented)
2842
+ - `UI_BRIDGE_OWNERSHIP_REFACTOR.md` — step 1a of this plan
2843
+ - Superseded plans: `COMMAND_PALETTE_REGISTRY.md` (older); v2-v5.2
2844
+ of this file (in git history).
2845
+
2846
+ ## Changelog v7.6 → v7.7 (round-7 governance + bead-level fixes)
2847
+
2848
+ Round-6 (codex bead-level review) returned 2 P0 governance questions, 7 P1s, 3 P2s. The user resolved both P0s; this changelog documents the integrated answers and the bead-level patches.
2849
+
2850
+ ### P0 — ChatPanel resolution: dependency injection (NOT plugin)
2851
+
2852
+ The plan ambiguated whether chat enters the workspace as a plugin contribution, a wrapped value-import, or an injected slot. **Locked answer: injected slot.** `BootstrapOptions.chatPanel: ComponentType<ChatPanelProps>` is required; the consuming app value-imports `ChatPanel` from `@boring/agent` and passes it. The workspace holds a `import type { ChatPanelProps } from '@boring/agent'` only — Inv #7 stays grep-verifiable. Chat remains core chrome (workspace lays it out and sizes it); only the React component is injected.
2853
+
2854
+ Spec edits: §"Workspace orchestration — bootstrap" gains the BootstrapOptions surface + a "Chat as core chrome — DI shape, not plugin" subsection with a worked example. j9p7.24 description was rewritten to describe the DI shape (no "wrap inside workspace" wording survives) and adds an explicit invariant assertion: `grep -RE "from ['\"]@boring/agent['\"]" packages/workspace/src` excluding `import type` must return 0.
2855
+
2856
+ ### P0 — Baseline protocol: worktree-based (NOT git stash)
2857
+
2858
+ Round-5 used `git stash` to establish a pre-migration baseline. That violates AGENTS.md "no destructive ops on user state" + multi-agent awareness rule. **Locked replacement: `git worktree add ../baseline-<sha> <ref>` → run macro e2e inside the worktree → capture artifacts → `git worktree remove`.** Idempotent, parallelizable, failure mode is a leftover dir (not lost work).
2859
+
2860
+ Spec edits: §"Acceptance" prepends a "Baseline protocol (v7.7 — worktree-based)" subsection with the canonical script. j9p7.20 (round-7 notes) replaces all `git stash` mentions with the worktree procedure. j9p7.20's dep on j9p7.19 was REMOVED via `br dep remove` — baseline runs BEFORE migration, so the graph edge that implied "baseline blocks on migration" was wrong.
2861
+
2862
+ ### P1 fixes (bead-level)
2863
+
2864
+ 1. **systemPrompt ordering circularity (j9p7.11 / j9p7.31)** — both beads now state the canonical sequence: bootstrap FIRST (computes `systemPromptAppend` + stages agentTools), then `createAgentApp({ systemPromptAppend, extraTools: [...uiTools, ...stagedAgentTools] })`. agentTools are staged into a list during bootstrap, not registered against a live registry.
2865
+
2866
+ 2. **excludeDefaults semantics correction (j9p7.10)** — the round-1 test "LLM file ops not in agentTool list" was wrong. `excludeDefaults: ['filesystem']` removes filesystemPlugin's UI contributions (panels + catalog). It does NOT remove file tools — those are HARNESS substrate, never in the plugin's `agentTools` (which is undefined). Two switches, two layers: `excludeDefaults` for UI, `disableDefaultFileTools` for tools.
2867
+
2868
+ 3. **Missing dep edges** — added `j9p7.12 → j9p7.25` (EmptyFilePanel registration), `j9p7.21 → j9p7.29` (release notes after final shell deletion), `j9p7.27 → j9p7.10` + `j9p7.27 → j9p7.9` (canary needs WorkspaceProvider+filesystemPlugin), `j9p7.19 → j9p7.27` (macro refactor canary-gated on playground migration).
2869
+
2870
+ 4. **Macro path drift (j9p7.18 / j9p7.19)** — references to `apps/boring-macro-v2/src/web/App.tsx` corrected to `src/front/App.tsx`. The `web/` directory does not exist (verified `ls`); the React app lives in `front/`.
2871
+
2872
+ 5. **j9p7.30 broad sed → explicit Edits** — round-5's `find ... -exec sed -i` cascade violated AGENTS "no broad rewrite scripts." Replaced with: pre/post-grep counts as sanity check + per-file Edit operations (READ → identify the specific vi.mock string → Edit). Deletion decisions for `src/store/`, `src/types/`, `src/panes/data-catalog/`, etc. were locked to a concrete table — no agent discretion at implementation time.
2873
+
2874
+ ### P2 fixes
2875
+
2876
+ - **j9p7.9 / j9p7.22** — round-7 notes flag pre-Step-0 paths (`src/plugin/defaults/`, `src/plugin/PluginErrorBoundary.tsx`) and pin the post-Step-0 canonical destinations (`src/plugins/filesystemPlugin/`, `src/front/plugin/PluginErrorBoundary.tsx`).
2877
+ - **j9p7.32** — dist filename verified (`dist/workspace.d.ts`); no path correction needed; minor port note added (macro = 5174, full-app = check vite config).
2878
+
2879
+ ### Status correction
2880
+
2881
+ - `j9p7.13` was flagged as open in round-6 notes; verified CLOSED. No action needed.
2882
+
2883
+ ## Changelog v7.5 → v7.6 (round-5 review patches)
2884
+
2885
+ Codex r5 (4 P1 + 3 P2) + gemini r2 (1 P0 + 2 P1 + 3 P2) returned with sharp findings. **Convergence**: both flagged the sequencing tension (gemini P0 / codex P1 #4) and the per-plugin halves entrypoint shape (codex P1 #3 / gemini P1 empty-server). User decided all four design questions; this changelog captures the integration.
2886
+
2887
+ ### P0 — Sequencing fixed (gemini)
2888
+
2889
+ The plan had Phase 1.5 Phase A doing BOTH the workspace reorg AND the chat-shell decomposition. That meant Phase 1 would build into the OLD flat layout, then Phase A would rip up and re-arrange. **Fix:** new **Step 0** before Phase 1 runs the mechanical workspace-reorg into v7.6 layout (front/+server/+shared/+plugins/) one-go. Phase A then ONLY decomposes the chat shell. **Meta-rule added:** when files move, they go directly to final v7.6 destinations — no intermediate placements.
2890
+
2891
+ ### P1 — Plugin entrypoints split (codex)
2892
+
2893
+ v7.5's spec said "plugin index.ts exports CLIENT + server Plugins (same id)." Codex flagged: this contradicts the build invariant that client barrels never re-export server-only code. **Fix:** every plugin has TWO entrypoints — `index.ts` (client side) and `server.ts` (server side). Hosts import each from the appropriate environment. Same id ties them; different files ship them. macro example updated. filesystemPlugin (UI-only) has only `index.ts` (no server.ts needed; gemini P1 — "create only halves you need").
2894
+
2895
+ ### P1 — Strict type-only imports for shared/plugin
2896
+
2897
+ Codex flagged: `shared/plugin/types.ts` referencing `ExplorerAdapter` from `components/DataExplorer/types` would leak DOM lib into the shared bundle. **Fix:** all cross-folder type references in `shared/plugin/` use `import type` (TypeScript erases at compile time). Plus `tsconfig.shared.json` (if it exists) excludes DOM lib. Plus `tsconfig.front.json` adds excludes for `src/plugins/**/server/**` to prevent server code being typechecked as front (codex P1 #1).
2898
+
2899
+ ### P1 — Phase 1.5 step text path-updated
2900
+
2901
+ Codex flagged that Phase A still said `panes/<id>/`, Phase C said `layouts/TopBar.tsx`, Phase E said `src/macroPlugin.ts` — all pre-v7.5. **Fix:** Phase A → `front/chrome/<id>/`; Phase C → `front/layout/TopBar.tsx`; Phase E → `apps/.../src/plugin/{front,server}/` directly. Bead descriptions to be updated post-commit.
2902
+
2903
+ ### P2 cleanup pack (applied)
2904
+
2905
+ 1. **uiBridge dedup** — drop deletion from Phase E; keep in Step 1a only (gemini P2)
2906
+ 2. **EmptyFilePanel relocation** — `panes/EmptyFilePanel/` → `front/chrome/empty-file-panel/` to kill the lone-resident `panes/` folder (gemini P2)
2907
+ 3. **TL;DR scrub** — "two default plugins" wording in scope text → "one default plugin: filesystemPlugin" (codex P2)
2908
+ 4. **A/B parallelism tightened** — "B can start in parallel but cannot close before A's definition.ts files exist" (codex P2)
2909
+ 5. **pi-tools-migration catch-up** — note that `standardCatalog.ts` no longer exists; `createAgentApp.ts:91` already uses `buildHarnessAgentTools` + `buildFilesystemAgentTools` directly
2910
+ 6. **tsconfig excludes** — already noted under P1 type imports
2911
+
2912
+ ### Verdict from both reviewers
2913
+
2914
+ - **Gemini r2:** "implementable as-is, provided the Step 0 sequencing tweak."
2915
+ - **Codex r5:** "implementable after above cleanup, not quite as-is."
2916
+
2917
+ After v7.6 patches: both verdicts converge on **implementable as-is**. No reversals from earlier rounds. The architecture stands.
2918
+
2919
+ ### Beads needing path updates
2920
+
2921
+ - **NEW j9p7.31** — Step 0 workspace reorg (mechanical)
2922
+ - **j9p7.9** (filesystemPlugin) — paths under `plugins/filesystemPlugin/{index.ts (no server.ts since UI-only), front/}`
2923
+ - **j9p7.18** (macro plugin module) — paths under `apps/boring-macro-v2/src/plugin/{index.ts, server.ts, front/, server/}`
2924
+ - **j9p7.24** (Phase A) — paths under `front/chrome/<id>/` and `front/bridge/`
2925
+ - **j9p7.25** (Phase B) — `front/registry/coreRegistrations.ts`
2926
+ - **j9p7.26** (Phase C) — `front/layout/TopBar.tsx`
2927
+
2928
+ ## Changelog v7.4 → v7.5 (final structural framing)
2929
+
2930
+ User iterated through four refinements (2026-04-29) to lock the
2931
+ post-migration directory layout. Each round narrowed scope:
2932
+
2933
+ 1. **"`panes/` should be workbench panes only"** — panes/ was conflating
2934
+ workbench-center (code-editor, markdown-editor) with sidebar tabs
2935
+ (file-tree, data-catalog) and chrome (ArtifactSurfacePane,
2936
+ EmptyPane). Strict role split.
2937
+ 2. **"Plan structure to fit the plugin system; panes will belong to a plugin"** —
2938
+ plugin-contributed components live INSIDE the contributing
2939
+ plugin's directory, not at the workspace root. CodeEditor /
2940
+ MarkdownEditor / FileTree all migrate into `plugins/filesystemPlugin/`.
2941
+ 3. **"core/ + layout/ + plugins/ as high-level framing"** — initial
2942
+ three-folder taxonomy.
2943
+ 4. **"Should we distinguish front and back?"** — yes; matches
2944
+ `@boring/agent`'s `front/ + server/ + shared/` convention.
2945
+ 5. **"plugins/ should contain actual plugins only"** — plugin model
2946
+ machinery moves out of `plugins/` into `front/plugin/`,
2947
+ `server/plugin/`, `shared/plugin/`. Singular `plugin/` for
2948
+ machinery; plural `plugins/` for instances.
2949
+
2950
+ ### Final structure (locked)
2951
+
2952
+ ```
2953
+ src/
2954
+ ├── front/ workspace front + plugin front machinery
2955
+ │ ├── chrome/ shell parts (chat, sessions, surface, …)
2956
+ │ ├── components/ UI primitives
2957
+ │ ├── registry/ base registries
2958
+ │ ├── dock/, events/, bridge/, hooks/, layout/
2959
+ │ ├── panes/EmptyFilePanel/ workbench-center fallback (substrate)
2960
+ │ ├── plugin/ ← plugin model FRONT machinery
2961
+ │ │ ├── CatalogRegistry.ts
2962
+ │ │ ├── PluginErrorBoundary.tsx
2963
+ │ │ ├── PluginInspector.tsx
2964
+ │ │ └── hooks (usePlugins, useCatalogs, useCommands, useActivePanels)
2965
+ │ └── WorkspaceProvider.tsx
2966
+
2967
+ ├── server/ workspace server + plugin server machinery
2968
+ │ ├── createWorkspaceAgentApp.ts, http/, ui-bridge/
2969
+ │ └── plugin/ ← plugin model SERVER machinery
2970
+
2971
+ ├── shared/ types both halves need
2972
+ │ ├── ui-bridge.ts, events/
2973
+ │ └── plugin/ ← plugin model SHARED
2974
+ │ ├── types.ts Plugin, CatalogConfig
2975
+ │ ├── definePlugin.ts
2976
+ │ └── bootstrap.ts
2977
+
2978
+ └── plugins/ ← ACTUAL plugin instances (plural)
2979
+ └── filesystemPlugin/
2980
+ ├── index.ts client + server Plugins (same id)
2981
+ ├── front/{panes, sidebar, catalogs}
2982
+ └── server/ (currently empty for filesystemPlugin)
2983
+ ```
2984
+
2985
+ ### Naming convention (locked)
2986
+
2987
+ | Folder | Meaning |
2988
+ |---|---|
2989
+ | `front/` (root) | Workspace's own client-side code |
2990
+ | `server/` (root) | Workspace's own backend code |
2991
+ | `shared/` (root) | Workspace's own cross-process types |
2992
+ | `plugins/` (root, plural) | Directory of plugin instances |
2993
+ | `plugin/` (in front/, server/, shared/) | Plugin model machinery (singular) |
2994
+ | `<plugin>/front/` (per-plugin) | The plugin's client-side contributions |
2995
+ | `<plugin>/server/` (per-plugin) | The plugin's server-side contributions |
2996
+
2997
+ Apps mirror: each app contributes ONE plugin, lives in `apps/<host>/src/plugin/{front,server}/`. Multi-plugin apps would rename to `plugins/<id>/`.
2998
+
2999
+ ### Why this is the right framing
3000
+
3001
+ 1. **Matches `@boring/agent` convention** — `front/ + server/ + shared/` parity across packages.
3002
+ 2. **Bundle boundary explicit** — tsup config can target `front/index.ts` and `server/index.ts` cleanly. Phase 2 npm sub-path exports (`./client`/`./server`) line up directly.
3003
+ 3. **Plugin = self-contained unit** — a plugin owns BOTH its front and server halves in ONE directory. Distributed npm plugins ship the same shape.
3004
+ 4. **No category mixing at any level** — workspace own code is in `front/server/shared`; plugins are in `plugins/`; plugin machinery is in singular `plugin/` sub-folders. Zero ambiguity for new contributors.
3005
+ 5. **Plays well with build invariants** — codex round-2 caught a P0 where server-only modules were about to leak into client code. With the explicit split, the lint rule "front/ must not import from server/" is one path-prefix check.
3006
+
3007
+ ### Bead impact
3008
+
3009
+ Mostly path-string updates; no semantic changes:
3010
+
3011
+ - **j9p7.9** (filesystemPlugin) — paths update from
3012
+ `packages/workspace/src/plugin/defaults/filesystemPlugin.ts` to
3013
+ `packages/workspace/src/plugins/filesystemPlugin/{index.ts,front/...,server/...}`
3014
+ - **j9p7.24** (Phase A — decompose chat shells) — paths update from
3015
+ `panes/<id>/` to `front/chrome/<id>/` for all chrome moves
3016
+ - **j9p7.18** (macro plugin module) — paths update from
3017
+ `apps/boring-macro-v2/src/plugin/{index,server}.ts` to
3018
+ `apps/boring-macro-v2/src/plugin/{index.ts,front/,server/}`
3019
+
3020
+ A new bead **j9p7.30** could capture the workspace front/server reorg
3021
+ itself if it's substantive enough to warrant separate tracking. For
3022
+ now, fold into Phase A (j9p7.24) since they're both restructuring
3023
+ the same directory.
3024
+
3025
+ ## Changelog v7.3 → v7.4 (merger of declarative-layout-migration plan)
3026
+
3027
+ User decision (2026-04-29): **merge into one mega-plan + one epic**. The earlier `DECLARATIVE_LAYOUT_MIGRATION.md` (epic boring-ui-v2-zrby, empty) is now Phase 1.5 of this plan. Single acceptance gate.
3028
+
3029
+ ### What changed
3030
+
3031
+ 1. **New §"Phase 1.5: Consumer migration — decompose shells, declarative layouts, delete ChatCenteredShell"** added between §"Concrete before/after" and §"Phase 2/3."
3032
+ - Documents three-tier API (Tier 1 declarative pre-shaped layouts; Tier 2 custom LayoutConfig with stock chrome; Tier 3 raw primitives)
3033
+ - Substrate vs default-plugin vs app-plugin clarification (chat is core, not a plugin)
3034
+ - File-tree end state (panes/ as canonical home; components/chat/ disappears)
3035
+ - 7 phase beads breakdown (Phase A → G) with parallelism map
3036
+ - Risks + ship criteria
3037
+
3038
+ 2. **j9p7.16 (Step 5b) and j9p7.17 (Step 5c) marked obsolete** — scope-shifted to Phase 1.5. Closing them with reason "subsumed by Phase 1.5 Phase A/B." Replacement beads created for each Phase.
3039
+
3040
+ 3. **j9p7.18 / .19 / .20** (macro plugin module / app refactor / e2e gate) **reframed under Phase E**, not Step 6. Same work, different grouping.
3041
+
3042
+ 4. **6 new beads created** for Phase 1.5 phases that don't have direct j9p7 equivalents:
3043
+ - j9p7.24 (Phase A — decompose shells)
3044
+ - j9p7.25 (Phase B — wire core panel registrations)
3045
+ - j9p7.26 (Phase C — lift TopBar chrome)
3046
+ - j9p7.27 (Phase D — migrate workspace-playground canary)
3047
+ - j9p7.28 (Phase F — migrate full-app)
3048
+ - j9p7.29 (Phase G — delete ChatCenteredShell + finalize)
3049
+
3050
+ 5. **`DECLARATIVE_LAYOUT_MIGRATION.md` marked superseded** with a banner pointing to this plan. Content preserved in git history; the file remains as a tombstone redirect.
3051
+
3052
+ 6. **Epic boring-ui-v2-zrby closed** as merged into j9p7. Its 0 children become moot.
3053
+
3054
+ ### Why merge (vs keep two plans)
3055
+
3056
+ The two plans had ~37 + 24 = 61 cross-references between them (`ChatCenteredShell`, `macroPlugin`, `delete uiBridge`). Macro migration was duplicated: j9p7 had it as Step 6; zrby had it as Phase E. The risk of doing j9p7's "ChatCenteredShell drops props" THEN having zrby delete the shell entirely is real wasted work — the user named it as the deciding concern.
3057
+
3058
+ Merging gives:
3059
+ - One coherent narrative: "build plugin model machinery → migrate consumers to declarative composition"
3060
+ - One acceptance gate (boring-macro e2e suite passes after Phase E)
3061
+ - One epic to track end-to-end progress
3062
+ - ~270 lines of doc (Phase 1.5 section) vs ~270 lines duplicated across two files
3063
+
3064
+ ### Net spec impact
3065
+
3066
+ - PLUGIN_MODEL.md grows by ~280 lines (Phase 1.5 + this changelog entry)
3067
+ - DECLARATIVE_LAYOUT_MIGRATION.md gains a SUPERSEDED banner; content preserved in history
3068
+ - j9p7 epic gains 6 new beads, closes 2 obsolete beads
3069
+ - zrby epic closes
3070
+ - Acceptance contract updated: j9p7 closes when Phase 1.5 ship criteria met (declarative apps + ChatCenteredShell deleted + Tier 1/2/3 public API)
3071
+
3072
+ ## Changelog v7.2 → v7.3 (PanelConfig roles clarified)
3073
+
3074
+ User question (2026-04-29): "should we distinguish between pane and
3075
+ component that belong to the left sidebar... + we could imagine full
3076
+ workbench pages as well?"
3077
+
3078
+ Honest answer: yes, the three roles ARE conceptually distinct. v7.3
3079
+ chooses to clarify them in docs without splitting the type — Phase 1
3080
+ impl already works; up-front splitting adds API surface without
3081
+ catching real bugs. Discriminated-union refactor is a good Phase 2
3082
+ candidate, blocked until a second role-specific field appears.
3083
+
3084
+ ### Spec additions
3085
+
3086
+ 1. **New §"PanelConfig roles — three uses, one type"** inside
3087
+ §"Concrete contribution types". Names the three roles
3088
+ (sidebar tab / workbench pane / bottom dock); for each:
3089
+ required fields, fields-not-to-set, concrete examples from
3090
+ filesystemPlugin and macroPlugin.
3091
+
3092
+ 2. **Reserved/future placements documented:** `'right-tab'`
3093
+ (symmetric to left-tab; no Phase 1 consumer); `'left'` /
3094
+ `'right'` (legacy non-tabbed; Phase 1 plugins don't use).
3095
+
3096
+ 3. **`Plugin.pages?: PageConfig[]` sketched as future contribution
3097
+ type** for full-viewport views (Settings, Reports, Onboarding
3098
+ flow). Out of scope for Phase 1; conceptually similar to VS
3099
+ Code's `viewsContainers`. Documented but not in the contract.
3100
+
3101
+ ### Decision: defer the discriminated-union split
3102
+
3103
+ The split becomes worth it when:
3104
+ - A second role-specific field appears (e.g., sidebar tabs gain
3105
+ `tabOrder`, bottom docks gain `defaultHeight`)
3106
+ - Plugin authors hit type-confusion bugs in practice
3107
+
3108
+ Until then: one type with `placement` runtime-disambiguating is
3109
+ cheaper.
3110
+
3111
+ ### `/registry` directory clarification (also v7.3)
3112
+
3113
+ User question: "will `packages/workspace/src/registry/` disappear
3114
+ after Phase 1?"
3115
+
3116
+ Answer: no — it stays. It holds the BASE PanelRegistry +
3117
+ CommandRegistry + RegistryProvider that the plugin model
3118
+ (`/plugin/`) FANS INTO via `bootstrap()`. The split is logical:
3119
+ `/registry` is "primitives any host could use directly without
3120
+ plugins"; `/plugin` is "the unified contribution layer." Active
3121
+ consumers of `/registry` outside the plugin path:
3122
+ WorkspaceProvider, DockviewShell, ChatCenteredShell, layout
3123
+ presets, ArtifactSurfacePane tests. Tearing down would break them
3124
+ without payoff.
3125
+
3126
+ ### Net impact
3127
+
3128
+ - Spec: +130 lines (new role-clarification subsection + future
3129
+ `pages` sketch + this changelog entry).
3130
+ - Contract: unchanged — same 7 fields (id, label, systemPrompt,
3131
+ panels, commands, catalogs, agentTools).
3132
+ - Beads: unchanged. Bead descriptions can keep using
3133
+ `placement: 'left-tab'` / `'center'` etc. as today.
3134
+
3135
+ ## Changelog v7.1 → v7.2 (planning-workflow review pass)
3136
+
3137
+ After 4 codex rounds + 1 gemini round + ultrathink self-audit, ran
3138
+ the GPT-Pro-style "review and propose your best revisions" pass.
3139
+ User selected the wholeheartedly-recommended subset (5 of 8
3140
+ proposals) plus the cross-plan alignment.
3141
+
3142
+ ### Robustness additions (selected)
3143
+
3144
+ **1. Per-plugin React error boundaries.** Every plugin contribution
3145
+ that renders React (panels, catalog row renderers, chat-suggestion
3146
+ cards) is wrapped in `<PluginErrorBoundary pluginId>`. A buggy
3147
+ plugin can no longer crash the workspace shell. Dropped errors
3148
+ push into `WorkspaceContext.errors[]` for visibility via
3149
+ `<PluginInspector />`. Implementation sites: PanelHost, CatalogResults,
3150
+ chat-suggestion card list. ~50 LOC. New bead: j9p7.22.
3151
+
3152
+ **2. CatalogRegistry search debouncing + AbortSignal propagation.**
3153
+ New hook: `useDebouncedCatalogSearch(query, opts?)` debounces 150ms,
3154
+ aborts in-flight searches per keystroke via the AbortSignal that
3155
+ ExplorerAdapter.search ALREADY accepts (verified
3156
+ `packages/workspace/src/components/DataExplorer/types.ts:46-55` —
3157
+ no contract change needed; just consumer-side wiring). Per-catalog
3158
+ errors isolated via try/catch. Updates bead j9p7.13.
3159
+
3160
+ ### Compelling-feature addition (selected)
3161
+
3162
+ **3. `Plugin.systemPrompt?: string`.** Optional field; bootstrap
3163
+ concatenates across registered plugins (in registration order),
3164
+ prepends to the harness's base system prompt. Lets plugin authors
3165
+ own their domain framing for the LLM ("you have a FRED database;
3166
+ use macro_* tools for X"). Concrete LLM-quality improvement;
3167
+ reduces host-author burden. Updates beads j9p7.4 (contract +
3168
+ validation), j9p7.7 (bootstrap concat), j9p7.18 (macro plugin sets).
3169
+
3170
+ ### DX additions (selected)
3171
+
3172
+ **4. `<PluginInspector />` PROMOTED to Phase 1.** Was cut in v6
3173
+ ("console.log is enough"). Reversal justified: plugin authors hit
3174
+ "why didn't my command appear?" daily; visual check beats
3175
+ spelunking React DevTools. ~50 LOC, zero production cost
3176
+ (import.meta.env.DEV gated). New bead: j9p7.23.
3177
+
3178
+ **5. New §"Decisions log".** Single-table summary of locked
3179
+ architectural decisions with one-line rationale + pointer to the
3180
+ changelog entry. Sits near the top so fresh-eyes reviewers don't
3181
+ re-litigate from first principles. ~30 lines docs.
3182
+
3183
+ **6. New §"Plugin patterns: cross-plugin communication".** Three
3184
+ canonical patterns — shared registry / event bus / late-wins
3185
+ override — each with a worked code example + anti-patterns table.
3186
+ ~60 lines docs. Plugin authors get templates instead of
3187
+ reinventing.
3188
+
3189
+ ### NOT selected from the proposal set
3190
+
3191
+ - **`Plugin.contractVersion?: number`** (Revision 4) — defensive
3192
+ forward-compat insurance for Phase 2 distribution. User chose to
3193
+ skip; revisit when Phase 2 lands.
3194
+
3195
+ ### Cross-plan alignment
3196
+
3197
+ **7. pi-tools-migration.md aligned to v7.x.** v6.3-aligned text in
3198
+ 4 places (lines 265, 294, 323, 886) updated to reflect v7.0+'s
3199
+ harness-owns-tools framing. The two plans now read consistently.
3200
+
3201
+ ### Net impact
3202
+
3203
+ - New beads: j9p7.22 (error boundaries) + j9p7.23 (PluginInspector)
3204
+ - Updated beads: j9p7.4 (systemPrompt validation), j9p7.7 (bootstrap
3205
+ concat), j9p7.13 (debounced search), j9p7.18 (macro systemPrompt)
3206
+ - Spec additions: ~250 lines (decisions log + patterns + 3 new
3207
+ sections + v7.2 changelog)
3208
+ - Same boring-macro acceptance test; LOC accounting unchanged
3209
+ - Plugin contract grows from 6 fields to 7 (added optional
3210
+ systemPrompt)
3211
+
3212
+ ## Changelog v7.0 → v7.1 (codex round-4 cleanup)
3213
+
3214
+ Codex round-4 verdict: **v7.0 concept is implementable** (no P0). But
3215
+ the v7.0 patch missed four spots where v6.3 text remained in active
3216
+ spec sections. Plus one missing policy + one cross-package
3217
+ implementation gap.
3218
+
3219
+ ### Patches
3220
+
3221
+ 1. **§"Concrete filesystemPlugin source"** (line ~474) — was still
3222
+ `import { filesystemAgentTools }` + `agentTools:
3223
+ filesystemAgentTools`. Fixed: import dropped, `agentTools` field
3224
+ gone, comment points to the harness-owns-tools section.
3225
+ 2. **Tool registration flow ASCII diagram** (line ~1180) — was
3226
+ showing v6.x dual-registration ("STANDALONE PATH" vs "WORKSPACE
3227
+ PATH" with `disableDefaultFileTools: true`). Fixed: single path,
3228
+ v7.1 narrative, `excludeDefaults` semantics narrowed.
3229
+ 3. **Reorganization table** (line ~1336) — said file ops bundle is
3230
+ "imported by both `createAgentApp` AND `filesystemPlugin.agentTools`."
3231
+ Fixed: only `createAgentApp` imports; "Not imported by
3232
+ filesystemPlugin" called out.
3233
+ 4. **Step 3 ASCII (in exact-path block)** — said `makeFilesystemPlugin
3234
+ (deps)` contributes agentTools. Fixed: plain const, UI-only,
3235
+ pi-tools-migration owns the tools.
3236
+
3237
+ ### Additions
3238
+
3239
+ 5. **§"Reserved tool names"** (codex round-4 P2) — explicit policy:
3240
+ `bash` / `executeIsolatedCode` / `read` / `write` / `edit` /
3241
+ `find` / `grep` / `ls` are HARNESS substrate. Plugin-contributed
3242
+ `agentTools` should use a domain prefix (`macro_search`,
3243
+ `docs_lookup`). Dev-mode warn on collision; no hard reject
3244
+ (override is sometimes intended). Existing
3245
+ `mergeTools.ts:30`'s late-wins-on-name behavior is documented as
3246
+ the legacy semantic for the pi loader path.
3247
+ 6. **Known-gaps register entry** — `registerAgentRoutes` lacks
3248
+ `disableDefaultFileTools` (codex round-4 P1). Live verified:
3249
+ `createAgentApp` has the flag but `registerAgentRoutes` doesn't.
3250
+ The "harness opt-out" promise only holds for the `createAgentApp`
3251
+ path. **Out of scope for Phase 1 macro acceptance** (macro uses
3252
+ `createWorkspaceAgentApp` → `createAgentApp`, not
3253
+ `registerAgentRoutes`). Tracked as a known-gap; promote to a
3254
+ bead if a `registerAgentRoutes`-using host needs the opt-out.
3255
+
3256
+ ### Cross-plan note (NOT patched in this commit)
3257
+
3258
+ `packages/agent/docs/plans/pi-tools-migration.md` (the sibling plan
3259
+ that ships first) still has v6.3-aligned text in three places:
3260
+ - Line 265: file tools called from BOTH createAgentApp AND filesystemPlugin
3261
+ - Line 294: filesystemPlugin becomes a factory with agentTools
3262
+ - Lines 323, 886: `excludeDefaults: ['filesystem']` removes file tools
3263
+
3264
+ Live code (`createAgentApp.ts:91` + `createWorkspaceAgentApp.ts:50`)
3265
+ matches v7.x, NOT pi-tools-migration's text. The sibling plan
3266
+ needs an alignment pass — flagged for the user (separate plan
3267
+ ownership).
3268
+
3269
+ ### Codex round-4 verdict on macro acceptance
3270
+
3271
+ Macro 260→46 still holds. v7 automatic file tools don't add macro
3272
+ glue; macro domain tools still fit `Plugin.agentTools`. Current
3273
+ j9p7 beads are v7-aligned (verified post f2d73bc). Old uhwx
3274
+ (pi-tools-migration) docs/beads are stale relative to v7 — track
3275
+ separately.
3276
+
3277
+ ## Changelog v6.3 → v7.0 (separation of concerns: harness owns tools)
3278
+
3279
+ User insight (2026-04-28): "fs plugin should not expose tools —
3280
+ those tools belong to the harness." Sharp framing. Adopted.
3281
+
3282
+ ### What changes
3283
+
3284
+ The dual-registration arrangement (filesystemPlugin + standalone
3285
+ agent share the same tool factory) was conflating two concerns:
3286
+ "should the agent have file tools?" (harness config) and "should
3287
+ the UI show a file tree?" (plugin config). Two separate switches,
3288
+ two separate layers. v7.0 untangles them.
3289
+
3290
+ - **`filesystemPlugin` becomes UI-only.** No `agentTools` field.
3291
+ Just panels (FileTree left-tab, CodeEditor center, MarkdownEditor
3292
+ center) + a Files catalog (cmd palette). Plain module-scope
3293
+ const — no `(deps)` factory needed.
3294
+ - **Substrate file tools live with the harness.** Per
3295
+ pi-tools-migration: `buildFilesystemAgentTools(bundle)` returns
3296
+ `[read, write, edit, find, grep, ls]`; `buildHarnessAgentTools`
3297
+ returns `[bash, executeIsolatedCode]`. Both registered by
3298
+ `createAgentApp` directly. Always-on for the harness path.
3299
+ - **`disableDefaultFileTools: true`** (on `createAgentApp`) is the
3300
+ only switch that removes file tools entirely. Use case:
3301
+ sandboxed deployment / no-fs agent.
3302
+ - **`excludeDefaults: ['filesystem']`** removes only the UI (Files
3303
+ tab, code/markdown editor auto-routing). The LLM still has
3304
+ `read`/`write`/etc. — they're substrate, not plugin
3305
+ contributions. Honest narrower promise.
3306
+ - **`createWorkspaceAgentApp` no longer passes
3307
+ `disableDefaultFileTools: true`** to the underlying
3308
+ `createAgentApp`. Plain wrap. No coordination dance.
3309
+ - **`Plugin.agentTools`** survives — but only for **domain tools**
3310
+ like macro's `execute_sql`/`macro_search`/`get_series_data`.
3311
+ Those depend on app-specific runtime state (ClickHouse client)
3312
+ the host owns; they belong on the plugin contract. Substrate
3313
+ tools don't.
3314
+
3315
+ ### Custom (non-pi) filesystem tools — supported
3316
+
3317
+ `buildFilesystemAgentTools(bundle)` is **not** restricted to pi
3318
+ factories. It can return pi tools + project-specific filesystem
3319
+ tools that pi doesn't ship (e.g., `watch_files`, `stat`,
3320
+ `git_status`, `multi_edit`). These are substrate alongside pi's
3321
+ defaults — same registration path, same lifecycle. They live in
3322
+ `@boring/agent/server/tools/filesystem/`, NOT in
3323
+ `filesystemPlugin`. Author wraps as `AgentTool`; bundle factory
3324
+ composes them into the array.
3325
+
3326
+ This was an explicit user clarification: "we may add fs tools that
3327
+ are not the default pi ones."
3328
+
3329
+ ### What this removes from the spec
3330
+
3331
+ - The whole §"File ops: shared bundle, dual registration path"
3332
+ walkthrough — replaced by §"Tools belong with the harness, not
3333
+ the plugin" + §"Custom (non-pi) filesystem tools".
3334
+ - The `(deps)` argument on `makeFilesystemPlugin` — plugin is now
3335
+ a plain module-scope const.
3336
+ - The `disableDefaultFileTools: true` indirection from
3337
+ `createWorkspaceAgentApp` — plain wrap.
3338
+ - ~50 lines of spec total.
3339
+
3340
+ ### Acceptance test impact
3341
+
3342
+ boring-macro-v2 migration LOC accounting unchanged (-86%). What
3343
+ changes is that macro's agentTools come ONLY from
3344
+ `makeMacroServerPlugin` (which is right — they're domain tools).
3345
+ File tools come from the harness automatically. No plumbing
3346
+ difference for macro's host code.
3347
+
3348
+ ### Bead impact
3349
+
3350
+ - **B2** (file ops bundle extraction) → reframed as "extract
3351
+ pi-factory wiring per pi-tools-migration; bundle includes pi
3352
+ tools + any custom additions." No dual-registration story.
3353
+ - **B9** (filesystemPlugin) → becomes a tiny bead: plain const
3354
+ with panels + catalog, no agentTools. ~10 LOC of plugin code.
3355
+ - **B11** (createWorkspaceAgentApp) → no
3356
+ `disableDefaultFileTools: true` wiring; just wraps
3357
+ `createAgentApp` and runs bootstrap.
3358
+
3359
+ All three bead descriptions updated to match v7.0 framing.
3360
+
3361
+ ## Changelog v6.2 → v6.3 (gemini fresh-eyes review patches)
3362
+
3363
+ Gemini did a fresh-eyes review (Gemini hadn't reviewed v6.x; codex
3364
+ ran rounds 2 and 3). Surfaced 1 P0 + 4 P1 + 1 P2 — all real, none
3365
+ duplicating codex. Quality of the catch on the P0 was particularly
3366
+ good: it noticed an actual closure-over-React-state bug in v6.2's
3367
+ `ChatCenteredShell` migration that codex round-3 missed.
3368
+
3369
+ **P0 — Static "internal chat-shell plugin" can't access
3370
+ `ChatCenteredShell` local state.** v6.2 said static commands
3371
+ (`toggleSessions`/`toggleWorkbench`/`newChat`) move into a
3372
+ module-scope plugin while only per-session commands stay
3373
+ imperative. But `toggleDrawer` and `toggleSurface` are closures
3374
+ over `useState` *inside* the React component
3375
+ (`ChatCenteredShell.tsx:222-226`). A module-scope plugin can't
3376
+ reach that state, and bridging via events would just shift the
3377
+ closure problem to the event handler.
3378
+
3379
+ **Fix:** drop the "internal chat-shell plugin" idea entirely. ALL
3380
+ ChatCenteredShell-internal commands stay as imperative
3381
+ `useEffect`+`registerCommand` calls. The registry's subscribe
3382
+ retrofit (Step 2c) propagates them to an open palette. The plugin
3383
+ model is for module-stable contributions; component-instance
3384
+ commands belong inside the component. Honest.
3385
+
3386
+ **P1.1 — Removing commands from Recent is a UX regression.** v6.2
3387
+ said "Recent is catalog-only — commands don't appear in Recent."
3388
+ Every mature command palette (VS Code, Raycast, Linear) shows
3389
+ recent commands; users rely on them for quick re-runs of frequent
3390
+ actions ("Toggle Theme," "Format JSON"). **Fix:** `RecentEntry`
3391
+ becomes a discriminated union — `{type: 'catalog', ...} | {type:
3392
+ 'command', ...}`. Render both, drop orphans of either. Existing
3393
+ localStorage `cmd:foo` entries map to the command branch on
3394
+ migration; plain paths to the catalog branch.
3395
+
3396
+ **P1.2 — `rowSnapshot` localStorage round-trip can corrupt
3397
+ non-serializable values.** `JSON.stringify` silently strips
3398
+ `Date` / `Map` / functions / React nodes; restore would crash.
3399
+ **Fix:** added §"Recent serialization invariant" — `ExplorerRow`
3400
+ participating in Recent MUST be 100% JSON-serializable; adapters
3401
+ naturally holding non-serializable values (e.g., Dates) serialize
3402
+ at row construction time and re-hydrate in the renderer. No
3403
+ deserialize hook in Phase 1 (add as non-breaking optional if a
3404
+ real case appears).
3405
+
3406
+ **P1.3 — Cutting `onMount` strands non-React stateful adapters.**
3407
+ A catalog adapter that wants `events.on('file:moved')` for cache
3408
+ invalidation has nowhere to do it: module-scope subscription fires
3409
+ globally for all hosts; lazy-on-first-call leaks.
3410
+ **Fix:** documented the limitation in §"Known gaps — deferred to
3411
+ Phase 2+". `onMount` is the trigger condition for re-introduction.
3412
+ Phase 1 plugins are all React-component-based or factory-injected,
3413
+ so the assumption holds for now; honest about when it breaks.
3414
+
3415
+ **P2 — Dropping `dataSources`/`data` props forces boilerplate for
3416
+ simple hosts.** A host that just wants "a data tab with my
3417
+ adapter" had a one-liner; v6.2's "register your own left-tab
3418
+ panel" makes it ~10 lines. **Fix:** use
3419
+ `createDataCatalogPlugin` for standalone data catalog plugins, or
3420
+ `appendDataCatalogOutputs` when an app plugin needs to install
3421
+ the data catalog as part of a domain plugin. Macro uses the latter
3422
+ so chart/deck behavior stays in the macro app.
3423
+
3424
+ ### Verdict on the simplification cuts (gemini)
3425
+
3426
+ > *"Cutting `routes`, `dependsOn`, `order`, and `chatSuggestions`
3427
+ > is highly defensible and correctly shifts HTTP infrastructure
3428
+ > and declarative configuration out of the pure-data plugin
3429
+ > envelope. The only cut that went too far is `onMount`."*
3430
+
3431
+ Codex (round-3): *"order/dependsOn/optionalDeps/version/routes/
3432
+ chatSuggestions/onMount are mostly defensible for macro."*
3433
+
3434
+ Both reviewers converge on `onMount` being the riskiest cut, and
3435
+ both agree it's defensible for Phase 1 specifically (no Phase 1
3436
+ plugin needs it). v6.3 documents the limitation with an explicit
3437
+ re-introduction trigger; if/when a stateful adapter appears,
3438
+ adding `onMount` is a non-breaking optional contract field. The
3439
+ cut holds for Phase 1; the sensitivity is acknowledged.
3440
+
3441
+ ### Net impact (v6.2 → v6.3)
3442
+
3443
+ - ChatCenteredShell migration honest: ALL its commands stay
3444
+ imperative; no fictional "internal plugin" indirection.
3445
+ - `RecentEntry` discriminated union: catalogs + commands.
3446
+ - JSON-serializable invariant on `ExplorerRow` documented.
3447
+ - Known-gaps register adds the non-React-adapter lifecycle item
3448
+ with `onMount` re-introduction trigger.
3449
+ - Replaced the static data factory with the reusable data catalog plugin helpers.
3450
+ - Same boring-macro acceptance test; LOC accounting unchanged.
3451
+
3452
+ ## Changelog v6.1 → v6.2 (round-3 codex review patches)
3453
+
3454
+ Round-3 codex review against v6.1 surfaced 1 P0 + 1 P1 + 2 P2 — all
3455
+ real, all verified against the live codebase. The cuts from v6 (no
3456
+ dependsOn, no onMount, no routes, no chatSuggestions on contract)
3457
+ verdict: defensible. Patches focus on Phase 1 semantics that were
3458
+ underspecified.
3459
+
3460
+ **P0 — `filesystemAgentTools: AgentTool[]` static shape doesn't
3461
+ match runtime reality.** The current `standardCatalog.ts:84`
3462
+ constructs file tools from runtime deps (`workspace`, `sandbox`,
3463
+ `fileSearch`); each `createXxxTool(deps)` returns an `AgentTool`.
3464
+ The plan's claim that `filesystemAgentTools` is a static array
3465
+ can't be wired. **Fix:** the bundle is now a factory:
3466
+ `createFilesystemAgentTools(deps): AgentTool[]`. Both
3467
+ `createAgentApp` (default-on) and `filesystemPlugin` call it with
3468
+ their respective runtime bundles. `filesystemPlugin` itself
3469
+ becomes `makeFilesystemPlugin(deps)` (factory shape, like
3470
+ `makeMacroPlugin`) so it can be constructed with the runtime deps
3471
+ by `createWorkspaceAgentApp` rather than evaluated at module load.
3472
+
3473
+ **P1 — Workbench data tab semantics ambiguous after dropping
3474
+ `data` prop.** With `recentKind` cut and multiple registered
3475
+ catalogs (filesystem's Files + macro's Series), nothing in the
3476
+ spec said which one fills the generic Data tab. **Fix:** drop
3477
+ `dataCatalogPlugin` from defaults entirely. There is no generic
3478
+ Data tab; plugins that want a workbench data tab register their
3479
+ own `placement: 'left-tab'` panel (e.g., macro's `macroSeriesPanel`
3480
+ which internally renders DataExplorer with the macro adapter +
3481
+ `onActivate` → `surface.openPanel({ component: "chart-canvas",
3482
+ ... })`). Cleaner, no precedence rule needed.
3483
+
3484
+ **P2 #1 — Macro example violated its own client/server build
3485
+ invariant.** v6.1's `apps/boring-macro-v2/src/plugin/index.ts`
3486
+ imported `macroAgentTools` from `../server/macroTools` — server
3487
+ code in client-facing index.ts. **Fix:** macro plugin splits into
3488
+ `makeMacroClientPlugin()` (panels/catalogs) in `plugin/index.ts`
3489
+ + `makeMacroServerPlugin()` (agentTools) in `plugin/server.ts`,
3490
+ both `definePlugin({ id: "boring-macro", ... })` with the same id.
3491
+ `<WorkspaceProvider>` gets the client one; `createWorkspaceAgentApp`
3492
+ gets the server one. Same pattern as Phase 2 npm distribution
3493
+ (client/server sub-path exports).
3494
+
3495
+ **P2 #2 — ChatCenteredShell migration didn't address dynamic
3496
+ command registration.** The current code registers per-session
3497
+ quick-switch commands inside a `useEffect` that depends on
3498
+ `sessions` prop changing at runtime. A static plugin contribution
3499
+ can't represent that. **Fix:** spec now says ONLY the static
3500
+ commands (toggleSessions/toggleWorkbench/newChat) move to the
3501
+ internal chat-shell plugin; per-session commands STAY as
3502
+ imperative `useCommandRegistry().registerCommand` calls inside
3503
+ ChatCenteredShell. Coexistence works because the registry's
3504
+ subscribe retrofit (Step 2c) handles late registrations.
3505
+
3506
+ ### Verdict on the v6 simplification cuts
3507
+
3508
+ Codex round-3 explicitly: *"`order/dependsOn/optionalDeps/version/
3509
+ routes/chatSuggestions/onMount` are mostly defensible for macro.
3510
+ The simplification breaks only where Phase 1 semantics are
3511
+ underspecified (catalog selection) or where the spec's tool-bundle
3512
+ shape is incompatible with current runtime dependency injection."*
3513
+
3514
+ Both blockers are now patched in v6.2. None of the v6 cuts get
3515
+ rolled back.
3516
+
3517
+ ### Net impact (v6.1 → v6.2)
3518
+
3519
+ - One default plugin removed (`dataCatalogPlugin`).
3520
+ - Two factory shapes formalized (`createFilesystemAgentTools(deps)`
3521
+ and `makeFilesystemPlugin(deps)`).
3522
+ - Macro example honors client/server split.
3523
+ - ChatCenteredShell migration spec acknowledges static-vs-dynamic
3524
+ command lifetime.
3525
+ - LOC accounting updates: 260 → 46 (-82%); slightly less reduction
3526
+ than v6.1's claimed 36 because the macro plugin is split into
3527
+ two files now.
3528
+
3529
+ ## Changelog v6 → v6.1 (ultrathink self-audit)
3530
+
3531
+ Self-applied ultrathink review against v6 — the kind of pass an
3532
+ external reviewer would make. Seven findings, all small:
3533
+
3534
+ 1. **`recentKind` on CatalogConfig was dead code.** Set but never
3535
+ read after the spec normalized to "drop orphans." Cut from the
3536
+ contract; cut from `RecentEntry`. Phase 2 "filter Recent by
3537
+ kind" UX can re-add as a non-breaking optional field.
3538
+
3539
+ 2. **`definePlugin` validation was claimed but never enumerated.**
3540
+ New §"What `definePlugin` validates" lists the five categories
3541
+ of checks (id, panels, commands, catalogs, agentTools) so plugin
3542
+ authors can predict what fails.
3543
+
3544
+ 3. **Plugin-level id collision policy was missing.** Contribution-
3545
+ level (panel id, command id) is late-wins-with-warn; that's
3546
+ composition. But two plugins sharing `Plugin.id` is identity
3547
+ confusion, not composition — should throw. Documented in new
3548
+ §"Plugin id collision policy."
3549
+
3550
+ 4. **Build/bundle invariants were implicit.** `plugin.client.ts`
3551
+ and `plugin.server.ts` MUST NOT cross-import or the client
3552
+ bundle leaks `node:*` imports. Added §"Build/bundle invariants"
3553
+ listing three enforcement strategies (RSC directives, npm
3554
+ sub-path exports, conditional imports) — any one suffices.
3555
+
3556
+ 5. **No testing guidance.** Plugin authors had to figure out
3557
+ testing patterns themselves. Added §"Plugin testability" with
3558
+ three concrete patterns: unit (assert contract shape),
3559
+ integration (`<WorkspaceProvider plugins={[testPlugin]}>` +
3560
+ `renderHook`), server (`createWorkspaceAgentApp` + Fastify
3561
+ `inject()`).
3562
+
3563
+ 6. **No concrete `filesystemPlugin` source.** The plan was abstract
3564
+ about what the canonical default plugin looks like. Added the
3565
+ actual code so the plan is self-contained; it's the most-cited
3566
+ exemplar in the spec.
3567
+
3568
+ 7. **Known-gaps register added.** §"Known gaps — deferred to Phase
3569
+ 2+" makes 11 things explicit (hot-reload, build-time
3570
+ enforcement, plugin versioning, layout migrations, …) with a
3571
+ "when to revisit" column. Reviewers who want to flag gaps can
3572
+ check whether they're already on the deferral list before
3573
+ adding scope.
3574
+
3575
+ ### Considered but not changed
3576
+
3577
+ - **`Plugin.label` removal** — considered cutting (defaults to
3578
+ `id`). Kept because `label?: string` is one optional line and
3579
+ it'll be visible in any future inspector / discovery UI.
3580
+ - **`agentTools` field on Plugin** — considered moving server-only
3581
+ contributions into a separate `ServerPlugin` shape. Rejected:
3582
+ same plugin id on client and server is the design intent (see
3583
+ §Distribution); separate shapes would bifurcate the contract
3584
+ for no real benefit.
3585
+ - **`definePlugin` immutability via `Object.freeze`** — considered.
3586
+ Rejected as over-engineering until accidental mutation is a
3587
+ real problem.
3588
+
3589
+ ### Net impact (v6 → v6.1)
3590
+
3591
+ - One field cut (`recentKind`).
3592
+ - Five sections added (~100 lines): validation enumeration, id
3593
+ collision policy, build invariants, testability, concrete
3594
+ filesystemPlugin source, known-gaps register.
3595
+ - Same boring-macro acceptance test.
3596
+ - Same 6-field Plugin contract (now actually 5 mandatory fields +
3597
+ optional `label`).
3598
+
3599
+ ## Changelog v5.2 → v6 (simplification pass)
3600
+
3601
+ After multiple review rounds we noticed v5.2 had grown defensible
3602
+ fields the way good plans do — every reviewer adds one ornament.
3603
+ v6 audits each field against "does Phase 1 actually need this?"
3604
+ and cuts everything that doesn't earn its keep.
3605
+
3606
+ ### Cut from the contract
3607
+
3608
+ | Field | Why it's safe to cut |
3609
+ |---|---|
3610
+ | `Plugin.order: number` | Array order does the work. Defaults prepended → register first; host plugins after; late-wins-on-id for collisions. No numeric ordering footgun for plugin authors. |
3611
+ | `Plugin.dependsOn` | Phase 1 has exactly one declared dep (macro→filesystem). Add when the dep graph stops being trivial. The "fail at boot if missing" gate is replaced by either runtime degradation (dev notices in 30s) or an `if (!ctx.catalogs.find(...))` line in `onMount` — and we're cutting onMount too. |
3612
+ | `Plugin.optionalDeps` | Soft deps that warn if missing → just a `console.warn` with extra contract surface. Plugins null-check the registry. |
3613
+ | `Plugin.version` | Used by nothing in Phase 1. |
3614
+ | `Plugin.routes: RouteRegistration[]` | Routes are HTTP infrastructure, not registry contributions. Mixing them blurs identity AND lies about lifecycle (Fastify can't unregister). Hosts wire routes via standard `app.register(...)` — one line per plugin that has routes. |
3615
+ | `Plugin.chatSuggestions` | Empty-state UX caps at ~6 cards → aggregation is impossible; hosts have to curate. If hosts curate, the registry adds nothing. Stays as a `<ChatCenteredShell>` prop. |
3616
+ | `Plugin.onMount` + `Cleanup` types | Zero Phase 1 plugins use it. Event subscriptions are better handled via `useEvent` in panels or `events.on()` in routes. macro's clickhouse client is constructed by the host before the plugin is built (factory pattern). |
3617
+ | `RouteRegistration<TOpts>` type | Gone with `routes`. |
3618
+ | `PluginMountCtx` type | Gone with `onMount`. |
3619
+ | `WorkspaceSurface` interface | Gone with `PluginMountCtx`. Plugin-level imperative actions weren't needed; declarative contributions handle everything. |
3620
+ | `MaybePromise` / `Cleanup` types | Gone with `onMount`. |
3621
+ | `tabOrder?: number` | Registration order works. |
3622
+ | `paletteIcon`, `paletteLimit`, `renderRecentRow` | Reasonable defaults. Add when a plugin overrides. |
3623
+ | 5 error subclasses (`PluginValidationError`, `PluginRegistrationError`, `PluginMountError`, `PluginContributionError`, `id collision`) | One `PluginError { kind: '...' }`. |
3624
+ | `<PluginInspector />` | `console.log(registries)` works fine for Phase 1. |
3625
+ | Pre-declared lifecycle events on the bus (`plugin:registered` etc.) | "Events declared on demand" policy. No emitter or consumer in Phase 1; don't declare. |
3626
+
3627
+ ### Sections compressed
3628
+
3629
+ - **Order semantics** — entire ~40-line section gone.
3630
+ - **Boot sequence** — collapses from a numbered async two-pass
3631
+ protocol with topo sort to a 6-line single-pass loop.
3632
+ - **Plugin composability — dependencies + file-pattern resolution**
3633
+ → renamed §"File-pattern resolution + late-wins" (the
3634
+ dependency half disappears).
3635
+ - **Security stance** — collapsed to a one-line non-goal.
3636
+ - **Dev tools** — collapsed to a one-line note about dev-mode
3637
+ warnings.
3638
+ - **Factory pattern** — was a section; now a paragraph.
3639
+
3640
+ ### Sections added
3641
+
3642
+ - **Why no chatSuggestions on the contract** — explicit
3643
+ aggregation-honest test.
3644
+ - **Where do routes go?** — substrate vs agent core vs plugin-specific.
3645
+ - **Distribution — Phase 2 sketch** — npm sub-path exports pattern;
3646
+ same shape as Express/Fastify/Vite.
3647
+ - **Relationship to pi-mono ecosystem** — what we adopt verbatim,
3648
+ what we add on top.
3649
+
3650
+ ### Net impact
3651
+
3652
+ - Spec line count: ~50% smaller than v5.2.
3653
+ - Implementation surface: ~40% smaller (no lifecycle, no
3654
+ RouteRegistration, no WorkspaceSurface, simpler bootstrap).
3655
+ - Same boring-macro acceptance test.
3656
+ - Same `excludeDefaults` honesty.
3657
+ - Same path-aware resolver.
3658
+ - Same polymorphic Recent.
3659
+ - Same file-ops shared bundle.
3660
+ - Same validateTool extraction (P0 fix preserved).
3661
+ - Same events subpath export (P1 fix preserved).
3662
+ - Same SurfaceShell EmptyFilePanel fallback (P1 fix preserved).
3663
+
3664
+ ### What we add back when (not if)
3665
+
3666
+ | Field | Add when |
3667
+ |---|---|
3668
+ | `dependsOn` | Phase 2: npm plugins from different authors → real dep graph emerges. |
3669
+ | `onMount` | First plugin that needs imperative setup not solved by panel-level `useEvent` or factory closure. |
3670
+ | `Plugin.version` + semver in deps | Phase 2 npm distribution. |
3671
+ | Ordering field | If/when registration order proves insufficient (no current evidence). |
3672
+ | `Plugin.routes` | If a Phase 3 hot-reload story makes route-as-plugin-data viable. |
3673
+ | `chatSuggestions` field | If an empty-state-aggregation use case appears that the host can't solve by curating. |
3674
+ | `<PluginInspector />` | When devs ask for it. |