@hachej/boring-workspace 0.1.17 → 0.1.20

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 (37) hide show
  1. package/README.md +36 -34
  2. package/dist/{FileTree-Dvaud3jU.js → FileTree-DHVB9rpk.js} +15 -15
  3. package/dist/{MarkdownEditor-sLkqTXDj.js → MarkdownEditor-L1KDH0bM.js} +1 -1
  4. package/dist/{WorkspaceLoadingState-zLzh1tGc.js → WorkspaceLoadingState-DYDxUYnx.js} +114 -110
  5. package/dist/WorkspaceProvider-CDPaAO5u.js +5971 -0
  6. package/dist/app-front.d.ts +94 -107
  7. package/dist/app-front.js +243 -233
  8. package/dist/app-server.d.ts +130 -15
  9. package/dist/app-server.js +1569 -304
  10. package/dist/{bootstrapServer-BreQ9QBc.d.ts → createInMemoryBridge-BDxDzihm.d.ts} +11 -26
  11. package/dist/manifest-CyNNdfYz.d.ts +58 -0
  12. package/dist/plugin.d.ts +199 -0
  13. package/dist/plugin.js +300 -0
  14. package/dist/server.d.ts +239 -4
  15. package/dist/server.js +901 -78
  16. package/dist/shared.d.ts +4 -112
  17. package/dist/surface-COYagY2m.d.ts +111 -0
  18. package/dist/testing.d.ts +19 -1
  19. package/dist/testing.js +2 -2
  20. package/dist/{agent-tool-DEtfQPVB.d.ts → ui-bridge-Gfh1MMgl.d.ts} +30 -30
  21. package/dist/workspace.css +36 -0
  22. package/dist/workspace.d.ts +165 -120
  23. package/dist/workspace.js +330 -377
  24. package/docs/INTERFACES.md +9 -9
  25. package/docs/PLUGIN_STRUCTURE.md +39 -145
  26. package/docs/PLUGIN_SYSTEM.md +355 -0
  27. package/docs/README.md +6 -1
  28. package/docs/plans/README.md +1 -0
  29. package/docs/plans/archive/HOT_RELOADABLE_AGENT_PLUGINS_PLAN.md +218 -0
  30. package/docs/plans/archive/RELOAD_PLUGGABILITY_PLAN.md +174 -0
  31. package/docs/plans/archive/UNIFIED_PLUGIN_SYSTEM_PLAN.md +769 -0
  32. package/package.json +11 -5
  33. package/dist/CommandPalette-CJHuTJlD.js +0 -5716
  34. package/docs/bridge.md +0 -135
  35. package/docs/panels.md +0 -102
  36. package/docs/plugins.md +0 -158
  37. /package/docs/plans/{MACRO_PLUGIN_GENERIC_HELPERS_AUDIT.md → archive/MACRO_PLUGIN_GENERIC_HELPERS_AUDIT.md} +0 -0
@@ -0,0 +1,218 @@
1
+ # Hot-Reloadable Boring Plugins Plan
2
+
3
+ Last updated: 2026-05-12
4
+ Status: current architecture snapshot for PR #18
5
+
6
+ This file is the single active plan for the hot-reloadable plugin/agent layer.
7
+ Older split plans for agent docs, plugin-agent layering, and derivation path A
8
+ were consolidated here so implementation guidance matches the code now in the
9
+ branch.
10
+
11
+ ## Goals
12
+
13
+ - `/reload` reloads both Pi agent assets and Boring workspace plugin assets.
14
+ - Plugin metadata is package-shaped and easy to author.
15
+ - UI registration is runtime code, not JSON-driven wiring.
16
+ - Pi-specific runtime/reload behavior stays behind the Pi harness adapter.
17
+ - The agent harness remains pluggable for non-Pi runtimes.
18
+ - Bad plugin metadata is observable and recoverable: no process crash, stable
19
+ error events, and `.error` files.
20
+
21
+ ## Package model
22
+
23
+ A Boring plugin is a package/directory with `package.json` metadata and optional
24
+ runtime entrypoints:
25
+
26
+ ```txt
27
+ plugin/
28
+ package.json
29
+ front/index.tsx # browser-only BoringFrontFactory
30
+ agent/index.ts # optional Pi ExtensionFactory
31
+ agent/skills/ # optional Pi skills
32
+ server/index.ts # optional trusted workspace/server routes
33
+ shared/ # optional platform-neutral types/constants
34
+ ```
35
+
36
+ ### `package.json#boring`
37
+
38
+ `boring` is workspace/UI discovery metadata only:
39
+
40
+ ```json
41
+ {
42
+ "name": "example-plugin",
43
+ "boring": {
44
+ "label": "Example Plugin",
45
+ "front": "front/index.tsx",
46
+ "server": "server/index.ts",
47
+ "derivesFrom": "data-catalog"
48
+ }
49
+ }
50
+ ```
51
+
52
+ Allowed runtime semantics:
53
+
54
+ - `id` / derived package name selects the stable plugin id.
55
+ - `label` is display metadata.
56
+ - `front` points at the front factory entrypoint.
57
+ - `server` points at trusted Node routes/helpers, or `false` to opt out.
58
+ - `derivesFrom` is discovery metadata for templates/catalogs.
59
+
60
+ Not allowed in `boring`: panels, commands, left tabs, surface resolvers, agent
61
+ tools, skills, extensions, packages, system prompts. Those old JSON registration
62
+ arrays were intentionally removed.
63
+
64
+ ### `package.json#pi`
65
+
66
+ `pi` owns agent/Pi contributions:
67
+
68
+ ```json
69
+ {
70
+ "pi": {
71
+ "extensions": ["agent/index.ts"],
72
+ "skills": ["agent/skills"],
73
+ "packages": [{ "source": "file:.", "extensions": ["agent/index.ts"] }],
74
+ "systemPrompt": "Use this plugin's tools when working with example data."
75
+ }
76
+ }
77
+ ```
78
+
79
+ `extensions`, `skills`, `packages`, and `systemPrompt` are consumed by the Pi
80
+ adapter. Workspace code only discovers and forwards them; it does not implement
81
+ Pi loading directly.
82
+
83
+ ## Runtime registration
84
+
85
+ ### Front/UI
86
+
87
+ `BoringFrontFactory` is the single runtime UI registration source:
88
+
89
+ ```ts
90
+ import type { BoringFrontFactory } from "@hachej/boring-workspace/plugin"
91
+
92
+ const plugin: BoringFrontFactory = (api) => {
93
+ api.registerPanel({ id: "example.panel", title: "Example", component: ExamplePane })
94
+ api.registerCommand({ id: "example.open", title: "Open Example", run: () => {} })
95
+ api.registerLeftTab({ id: "example.left", title: "Example", component: ExampleLeft })
96
+ api.registerSurfaceResolver({ id: "example.surface", resolve: () => ({ component: "example.panel" }) })
97
+ }
98
+
99
+ export default plugin
100
+ ```
101
+
102
+ Front plugin code is browser code. It must not contribute executable agent tools.
103
+ Hot-loaded panels are ordinary React function components: hooks such as
104
+ `useState`, `useEffect`, and `useMemo` are supported. The host Vite dev server
105
+ must resolve `react`, `react-dom`, `react/jsx-runtime`, and
106
+ `react/jsx-dev-runtime` to the same singleton modules used by the workspace
107
+ shell; duplicate React copies are a host configuration bug, not a plugin author
108
+ contract. The legacy front-side `agentTools` / `agent-tool` output path is
109
+ removed.
110
+
111
+ ### Server/workspace
112
+
113
+ `server/index.ts` is trusted host-process code for workspace routes or support
114
+ helpers. Hot-reloadable package plugins should put new tool capabilities in
115
+ `pi.extensions` via `agent/index.ts`. Programmatic host/server plugin APIs may
116
+ still adapt legacy `extraTools` during migration, but package metadata does not
117
+ carry front-side or JSON-declared tools.
118
+
119
+ ### Agent/Pi
120
+
121
+ `agent/index.ts` exports native Pi extension factories. The Pi harness adapter
122
+ owns conversion into `DefaultResourceLoader`, dynamic package metadata refresh,
123
+ skill loading, and `piSession.reload()`.
124
+
125
+ ## Reload flow
126
+
127
+ 1. User sends `/reload` in chat.
128
+ 2. Front slash-command calls `POST /api/v1/agent/reload`.
129
+ 3. Generic agent route calls the workspace-provided `beforeReload` hook.
130
+ 4. Workspace composition refreshes package.json `pi` inputs and reloads
131
+ `BoringPluginAssetManager`.
132
+ 5. Manager emits `boring.plugin.load`, `boring.plugin.unload`, or
133
+ `boring.plugin.error` SSE events.
134
+ 6. Generic agent route calls the configured harness `reloadSession` method.
135
+ 7. Pi harness implementation reloads Pi extension/skill/package resources for
136
+ the session.
137
+ 8. Front plugin runtime hot-swaps successful `front` modules and keeps the last
138
+ good UI alive on malformed/error events.
139
+
140
+ The generic agent package exposes only the harness seam; Pi-specific knobs live
141
+ under `pi` options.
142
+
143
+ ## Validation and safety
144
+
145
+ Plugin discovery/preflight must:
146
+
147
+ - reject invalid ids and duplicate effective plugin ids;
148
+ - reject `.` paths, absolute paths, backslash paths, null-byte paths, and
149
+ traversal (`../`) paths;
150
+ - validate explicit `front`, `server`, `pi.extensions`, `pi.skills`, and nested
151
+ `pi.packages[*]` resource filters;
152
+ - perform realpath containment checks for existing paths;
153
+ - check the nearest existing ancestor for missing paths under symlinked parents;
154
+ - allow empty collection directories such as `.pi/extensions`;
155
+ - report explicit plugin dirs without `package.json` as `MISSING_PACKAGE_JSON`;
156
+ - surface invalid JSON/metadata through preflight errors rather than crashing
157
+ startup.
158
+
159
+ Error reporting:
160
+
161
+ - `BoringPluginAssetManager.load()` returns errors.
162
+ - SSE emits `boring.plugin.error`.
163
+ - `.error` files are written under the configured error root.
164
+ - If a plugin id cannot be safely derived, use a stable `preflight-<hash>` id.
165
+
166
+ ## Agent docs for plugin creation
167
+
168
+ The agent should learn the plugin API from workspace-owned Markdown docs:
169
+
170
+ - `packages/workspace/src/server/docs/plugins.md`
171
+ - `packages/workspace/src/server/docs/panels.md`
172
+ - `packages/workspace/src/server/docs/bridge.md`
173
+
174
+ `buildBoringSystemPrompt()` embeds those docs into the agent prompt when the
175
+ workspace app has strong filesystem capability. This replaces the older separate
176
+ agent-doc-embedding plan.
177
+
178
+ ## Current asset-serving caveat
179
+
180
+ Hot-loaded front entries currently use Vite-style `/@fs/<absolute-path>` module
181
+ URLs. `WorkspaceProvider` gates that path behind `frontPluginHotReload="vite"`,
182
+ which defaults on only in dev, because Vite transforms TypeScript/TSX and React
183
+ imports for the browser. Vite hosts that enable this mode must alias/dedupe the
184
+ React family imports to the host app singleton so hook-based plugin panels run
185
+ under the same dispatcher as the workspace shell.
186
+
187
+ A production Fastify-only host needs a workspace-owned authenticated module
188
+ asset endpoint/bundler before front plugin hot-loading can work without Vite.
189
+ Until that endpoint exists, document front plugin hot-reload as development /
190
+ workspace-dev-server scope.
191
+
192
+ ## Public API boundaries
193
+
194
+ - `@hachej/boring-workspace/plugin` exports front authoring helpers,
195
+ `BoringFrontFactory`, and package metadata types.
196
+ - Public barrels export `BoringPluginPackageJson`, not the server runtime
197
+ manifest shape.
198
+ - Server-only runtime manifests stay under `server/agentPlugins`.
199
+ - Shared/browser code must not import `@hachej/boring-agent` values.
200
+ - `UiBridge.postCommand` remains the single command dispatch source.
201
+
202
+ ## Done in PR #18
203
+
204
+ - Generic `/api/v1/agent/reload` route and `/reload` slash command.
205
+ - Pluggable `AgentHarnessFactory` and Pi-scoped adapter options.
206
+ - Workspace plugin scanner, asset manager, routes, and SSE front reload client.
207
+ - `package.json#pi` / `package.json#boring` split.
208
+ - `BoringFrontFactory` as the only runtime UI registration source.
209
+ - Removal of obsolete front-side `agentTools` registration path.
210
+ - Realpath-aware path validation and observable preflight errors.
211
+ - Consolidated plan docs into this current architecture plan.
212
+
213
+ ## Not in scope for PR #18
214
+
215
+ - `apps/boring-macro-v2`.
216
+ - Password-reset smoke tests.
217
+ - Production Fastify asset bundling for front plugin modules.
218
+ - Cloud/multi-tenant plugin provisioning.
@@ -0,0 +1,174 @@
1
+ # Reload pluggability — small, harness-agnostic seams
2
+
3
+ Status: proposal, builds on PR #18.
4
+ Scope: cut workspace ↔ Pi coupling in the reload path. Keep Pi as the default
5
+ harness; let a future custom harness slot in without forking the workspace.
6
+
7
+ ## Two-track reload, no callback between tracks
8
+
9
+ ```
10
+ POST /api/v1/agent/reload
11
+ ├─ Track A — workspace owns it
12
+ │ BoringPluginAssetManager.load() → SSE → front hot-swap
13
+ │ (errors → 422 with details; preserves last-good UI)
14
+ └─ Track B — harness owns it (optional)
15
+ harness.reloadSession?(sessionId)
16
+ Pi today; custom harness later; missing method = skip.
17
+ ```
18
+
19
+ The two tracks do not call into each other. The workspace stops registering Pi
20
+ extensions and stops mutating arrays that Pi later reads.
21
+
22
+ ## Single new seam: dynamic prompt provider
23
+
24
+ ```ts
25
+ // packages/agent/src/shared/harness.ts
26
+ interface AgentHarnessFactoryInput {
27
+ // ...existing
28
+ /**
29
+ * Optional source of additional system prompt content. Harness reads it
30
+ * each time it builds/rebuilds a session prompt. Returning `undefined`
31
+ * means "nothing to add right now". Workspace plugin layer supplies it;
32
+ * harness decides when to call it.
33
+ */
34
+ systemPromptDynamic?: () => string | undefined | Promise<string | undefined>
35
+ }
36
+ ```
37
+
38
+ - Workspace passes `systemPromptDynamic: () => aggregatePluginPrompts(boringAssetManager)`.
39
+ - Pi adapter, internally, registers a `before_agent_start` extension that calls
40
+ the provider and appends to `event.systemPrompt`. Pi's native
41
+ `reloadSession` already re-fires `before_agent_start`, so /reload picks up
42
+ fresh plugin prompts with no extra lifecycle plumbing.
43
+ - Other harnesses call the provider during their own session-init flow, or
44
+ ignore it.
45
+
46
+ ## Audit — other leaky abstractions in today's reload path
47
+
48
+ ### 1. Workspace mutates Pi-owned arrays in place (high)
49
+
50
+ `createWorkspaceAgentServer.ts → syncPackageJsonPiOptions()` calls
51
+ `piAdditionalSkillPaths.splice(...)`, `piPackages.splice(...)`,
52
+ `piExtensionPaths.splice(...)` to mutate the *same* arrays that were passed
53
+ into `pi: { ... }` on `createAgentApp`. This relies on Pi re-reading those
54
+ arrays inside `reloadSession`. Two bad properties:
55
+
56
+ - It's invisible coupling — neither side declares the contract.
57
+ - A non-Pi harness has no reason to read those arrays again, so the mutation
58
+ is silently meaningless.
59
+
60
+ **Fix:** replace the snapshot arrays with a single getter:
61
+
62
+ ```ts
63
+ // On PiHarnessOptions:
64
+ interface PiHarnessOptions {
65
+ // ...existing
66
+ getResources?: () => {
67
+ additionalSkillPaths?: string[]
68
+ packages?: WorkspacePiPackageSource[]
69
+ extensionPaths?: string[]
70
+ extensionFactories?: ExtensionFactory[]
71
+ }
72
+ }
73
+ ```
74
+
75
+ Pi's resource-loader rebuild reads from `getResources()` each time. Workspace
76
+ provides one getter that merges static + package.json-discovered values. No
77
+ splices. No shared array references.
78
+
79
+ This is Pi-internal and doesn't change the workspace's public surface much,
80
+ but it removes the most surprising piece of coupling in the file.
81
+
82
+ ### 2. Pi-shaped helpers live in the workspace package (medium)
83
+
84
+ `createBoringPiPackageSource(workspaceRoot)` builds `{ source, skills:
85
+ ["skills/boring-plugin-authoring"] }` — a Pi `WorkspacePiPackageSource` shape.
86
+ It lives in `createWorkspaceAgentServer.ts` and gets prepended to `piPackages`.
87
+
88
+ A non-Pi harness has no use for this shape and shouldn't have to pretend it
89
+ does. **Fix:** move the helper into the Pi-default composition path:
90
+
91
+ ```ts
92
+ // pseudo
93
+ const harnessFactory = opts.harnessFactory ?? composePiHarnessFactory({
94
+ workspaceRoot,
95
+ pi: opts.pi,
96
+ bundledSkillPackage: createBoringPiPackageSource(workspaceRoot),
97
+ })
98
+ ```
99
+
100
+ Workspace stops constructing Pi-shaped values when a non-default harness is
101
+ in use.
102
+
103
+ ### 3. Pi's bundled skill package is provisioned unconditionally (medium)
104
+
105
+ `createBoringPiPackageProvisioningContribution()` materializes
106
+ `@hachej/boring-pi` into the child workspace's `node_modules` regardless of
107
+ which harness is active. It should only run when the Pi adapter is the
108
+ selected harness.
109
+
110
+ **Fix:** make provisioning contributions a *harness adapter* responsibility
111
+ too:
112
+
113
+ ```ts
114
+ interface AgentHarnessFactory {
115
+ contributeProvisioning?(workspaceRoot: string): WorkspaceProvisioningContribution[]
116
+ }
117
+ ```
118
+
119
+ `composePiHarnessFactory` returns the Pi-bundled provisioning entry from
120
+ `contributeProvisioning`. Workspace collects them generically. Custom
121
+ harnesses contribute their own asset packages, or nothing.
122
+
123
+ ### 4. `/reload` response shape is harness-shaped (low)
124
+
125
+ `reloadRoutes` returns `{ ok, sessionId, reloaded }`. `reloaded: boolean` is
126
+ fine for Pi but doesn't carry boring-plugin reload outcomes (loaded count,
127
+ errors-but-still-usable, last-good UI kept), so the front always renders the
128
+ same "Agent plugins reloaded." message regardless of what really happened.
129
+
130
+ **Fix:** widen the response without breaking the existing field:
131
+
132
+ ```ts
133
+ {
134
+ ok: true,
135
+ sessionId,
136
+ reloaded, // keep — harness reload result
137
+ boring?: { // new — workspace track
138
+ loaded: number,
139
+ errors: { id: string; message: string }[],
140
+ }
141
+ }
142
+ ```
143
+
144
+ Front `/reload` handler uses `boring.errors` to surface compile failures
145
+ without forcing a 422 (today's `throw` model conflates "reload broken"
146
+ with "some plugins broken").
147
+
148
+ ### 5. Naming creep — `pi` in the workspace public API (defer)
149
+
150
+ `WorkspaceAgentServerOptions.pi`, `WorkspacePiPackageSource`,
151
+ `compactPiPackages` are exported by `@hachej/boring-workspace`. Hard to fix
152
+ without a breaking change for plugin authors. Leave for the day a second
153
+ harness lands; rename then under one umbrella (`harness?: { pi?: ..., ...}`)
154
+ with one release of aliases.
155
+
156
+ ## Tradeoff acknowledged once
157
+
158
+ Track A and Track B don't share a callback. If a custom harness wants
159
+ plugin `systemPrompt`s to refresh inside a running session, it implements
160
+ the `systemPromptDynamic` getter call into its own session-init flow. Pi
161
+ already does this for free via `before_agent_start`. Other harnesses opt
162
+ in or accept static prompts.
163
+
164
+ ## Execution order
165
+
166
+ | # | Change | Risk | Notes |
167
+ |---|--------|------|-------|
168
+ | 1 | Add `systemPromptDynamic` on `AgentHarnessFactoryInput`. Pi adapter consumes it via an internal `before_agent_start` extension. Delete `packages/workspace/src/server/agentPlugins/boringPiExtension.ts` and its test. Workspace passes the getter. | Low — same runtime behavior, code moves. | Step 1 keeps every existing test green. |
169
+ | 2 | Replace `syncPackageJsonPiOptions` array splices with a single `getResources()` getter on `PiHarnessOptions`. Pi reads it on every session rebuild. Workspace stops mutating shared arrays. | Low | Drops 30+ lines of mutation glue. |
170
+ | 3 | Move `createBoringPiPackageSource` and Pi-bundled provisioning into a `composePiHarnessFactory` helper. Workspace constructs them only on the Pi-default path. | Low | Custom harness path now allocates zero Pi-shaped data. |
171
+ | 4 | Widen `/api/v1/agent/reload` response with optional `boring` block. Update front `/reload` handler to surface compile errors without 422. | Low | Strictly additive on the wire. |
172
+ | 5 (later) | Rename workspace public types to drop `pi` prefix; ship `harness: { pi?: ... }` namespacing with deprecated aliases. | Medium | Wait until a second harness justifies the churn. |
173
+
174
+ Steps 1–4 are independent commits. Each can land on its own.