gentle-shell-bridge 0.0.0-stage → 0.3.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.
- package/.codegraph/.gitignore +5 -0
- package/PRD.md +144 -0
- package/README.md +745 -2
- package/docs/architecture.md +981 -0
- package/docs/contract.md +1266 -0
- package/docs/state.md +251 -0
- package/examples/bash-card.ts +1694 -0
- package/gentle-shell-bridge-0.3.0.tgz +0 -0
- package/odd/tasks/palette-sections.md +30 -0
- package/odd/tasks/plugin-keymap.md +32 -0
- package/odd/tasks/plugin-surface-contract.md +55 -0
- package/package.json +35 -4
- package/src/api.ts +492 -0
- package/src/arm.ts +83 -0
- package/src/bus.ts +558 -0
- package/src/capture.ts +886 -0
- package/src/card-style-line.ts +112 -0
- package/src/card-style-rows.ts +390 -0
- package/src/card-style-transform.ts +862 -0
- package/src/card-style-wiring.ts +472 -0
- package/src/card-styles.ts +159 -0
- package/src/config.ts +71 -0
- package/src/extension.ts +271 -0
- package/src/hook.ts +310 -0
- package/src/index.ts +71 -0
- package/src/notices.ts +137 -0
- package/src/plugin-lines.ts +865 -0
- package/src/plugin-surface.ts +218 -0
- package/src/presence.ts +259 -0
- package/src/repaint.ts +351 -0
- package/src/row-labels.ts +146 -0
- package/src/row-names.ts +273 -0
- package/src/row-origin.ts +144 -0
- package/src/rows.ts +248 -0
- package/src/section-panel.ts +378 -0
- package/src/section-pointer.ts +164 -0
- package/src/sections-rows.ts +128 -0
- package/src/sidebar-render.ts +535 -0
- package/src/sidebar.ts +919 -0
- package/src/status.ts +1161 -0
- package/src/store.ts +279 -0
- package/src/tool-design-adoption.ts +263 -0
- package/src/tool-design-rows.ts +246 -0
- package/src/tool-design-wiring.ts +428 -0
- package/src/tool-designs.ts +402 -0
- package/src/transform-pipeline.ts +351 -0
- package/src/types.ts +442 -0
- package/src/visibility.ts +154 -0
- package/test/api.test.ts +395 -0
- package/test/apply-on-move.test.ts +477 -0
- package/test/arm.test.ts +220 -0
- package/test/bridge-ready.test.ts +309 -0
- package/test/bus.test.ts +165 -0
- package/test/button-rows.test.ts +409 -0
- package/test/capture.test.ts +356 -0
- package/test/card-style-rows.test.ts +510 -0
- package/test/card-style-transform.test.ts +379 -0
- package/test/card-style-wiring.test.ts +258 -0
- package/test/card-styles-e2e.test.ts +491 -0
- package/test/card-styles.test.ts +324 -0
- package/test/card-transcript-path.test.ts +178 -0
- package/test/config.test.ts +93 -0
- package/test/e2e.test.ts +443 -0
- package/test/example-bash-identity.test.ts +372 -0
- package/test/example-bash-matrix.test.ts +856 -0
- package/test/example-designs.test.ts +734 -0
- package/test/example-neon-transform.test.ts +283 -0
- package/test/example-plain-style.test.ts +1268 -0
- package/test/extension.test.ts +160 -0
- package/test/fake-events.ts +58 -0
- package/test/fake-host.ts +211 -0
- package/test/fake-sidebar.ts +516 -0
- package/test/fake-transcript.ts +486 -0
- package/test/frame-telemetry.test.ts +417 -0
- package/test/hook.test.ts +196 -0
- package/test/manifest.test.ts +105 -0
- package/test/new-category.test.ts +979 -0
- package/test/notices.test.ts +328 -0
- package/test/plugin-integration-lines.test.ts +979 -0
- package/test/plugin-surface.test.ts +152 -0
- package/test/presence.test.ts +459 -0
- package/test/rail-surfaces.test.ts +457 -0
- package/test/rail-width.test.ts +423 -0
- package/test/regression.test.ts +483 -0
- package/test/repaint-api.test.ts +200 -0
- package/test/reserved-categories.test.ts +361 -0
- package/test/row-labels.test.ts +284 -0
- package/test/row-names.test.ts +473 -0
- package/test/row-origin.test.ts +395 -0
- package/test/section-interaction.test.ts +364 -0
- package/test/section-panel.test.ts +403 -0
- package/test/sections-rows.test.ts +291 -0
- package/test/sidebar-backend.test.ts +471 -0
- package/test/sidebar-e2e.test.ts +331 -0
- package/test/sidebar-hover.test.ts +276 -0
- package/test/sidebar-independent.test.ts +349 -0
- package/test/sidebar-render.test.ts +281 -0
- package/test/sidebar-sections.test.ts +245 -0
- package/test/status-card-styles.test.ts +201 -0
- package/test/status-section-frame.test.ts +181 -0
- package/test/status-sections.test.ts +243 -0
- package/test/status.test.ts +209 -0
- package/test/store.test.ts +192 -0
- package/test/surface-repaint-cost.test.ts +520 -0
- package/test/tool-design-chain.test.ts +483 -0
- package/test/tool-design-cost.test.ts +473 -0
- package/test/tool-design-rows.test.ts +359 -0
- package/test/tool-designs.test.ts +745 -0
- package/test/transform-pipeline.test.ts +465 -0
- package/test/transforms-bus.test.ts +233 -0
- package/test/verifier-width-regressions.test.ts +164 -0
- package/tsconfig.json +18 -0
package/docs/state.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# ADR 0001 — Stateless composition, with an optional per-plugin store
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted for the extraction; implemented in `src/store.ts` and exercised by the
|
|
4
|
+
test suite. **Amended** by decision 4: a registered sidebar section's visibility is the one
|
|
5
|
+
piece of state the bridge itself owns. **Amended** by decision 5: plugins reach the bridge
|
|
6
|
+
through a published, versioned `globalThis` slot instead of a path or an environment variable.
|
|
7
|
+
**Amended** by decision 6: the selected card style is bridge-owned state, and the conversation
|
|
8
|
+
frames are repainted opt-in, degrading to native. **Amended** by decision 7: the repaint wrapper
|
|
9
|
+
became a general, ordered transform pipeline, so a plugin's rendering choice reaches cards already
|
|
10
|
+
on screen instead of only the next one.
|
|
11
|
+
- **Context:** [contract.md](contract.md), [architecture.md](architecture.md)
|
|
12
|
+
- **Deciders:** package author and gentle-shell maintainers.
|
|
13
|
+
|
|
14
|
+
## Context
|
|
15
|
+
|
|
16
|
+
The bridge exists to compose rows from many plugins into one menu. A row's `●` is computed
|
|
17
|
+
by `current()`, which the renderer calls at render time (`lib/visual-customize-view.ts:585`),
|
|
18
|
+
and a toggle applies through `action()`, which the view runs on ←/→ or Enter
|
|
19
|
+
(`lib/visual-customize-view.ts:565`) and follows with its own repaint
|
|
20
|
+
(`lib/visual-customize-view.ts:579`). Persistence therefore has to sit behind the row, and
|
|
21
|
+
the question is *who owns it*: the bridge, each plugin, or nobody.
|
|
22
|
+
|
|
23
|
+
Two practical constraints shape the answer:
|
|
24
|
+
|
|
25
|
+
1. Row state must outlive `/reload` to be useful, and Pi gives each extension its own copy
|
|
26
|
+
of any imported module (`lib/shell-card.ts:30-32`), so in-memory module state is
|
|
27
|
+
per-extension and per-load.
|
|
28
|
+
2. Plugins already have their own config files in gentle-shell, written atomically
|
|
29
|
+
(gentle-shell uses temp-file plus `renameSync`, e.g. `lib/agent-profiles.ts:715`,
|
|
30
|
+
`lib/animation-policy.ts:47`). A bridge that owns state would need to replicate that
|
|
31
|
+
discipline for data it does not understand.
|
|
32
|
+
|
|
33
|
+
## Decision
|
|
34
|
+
|
|
35
|
+
**1. The bridge is stateless by default.** It composes rows; it does not own preferences,
|
|
36
|
+
caches no row values, and has no opinion about a plugin's config format. Diagnostics hold
|
|
37
|
+
runtime facts (registrations, capture outcome, failures), not settings.
|
|
38
|
+
|
|
39
|
+
**2. The bridge also exposes an optional `createStore(pluginId)`.** A plugin that wants
|
|
40
|
+
zero boilerplate gets:
|
|
41
|
+
|
|
42
|
+
- path: `<gentle config home>/gentle-shell-bridge/<pluginId>.json` — namespaced by plugin,
|
|
43
|
+
under the directory gentle-shell already resolves (default `~/.pi/gentle-ai`,
|
|
44
|
+
`GENTLE_PI_CONFIG_HOME` override — `lib/gentle-shell-bundled.ts:231-239`);
|
|
45
|
+
- atomic writes: write a temporary file in the same directory, then rename over the
|
|
46
|
+
target, so a crash mid-write cannot truncate the live file;
|
|
47
|
+
- a schema version field (integer `schemaVersion`, currently `1`) so a future shape change
|
|
48
|
+
can migrate instead of guessing;
|
|
49
|
+
- a document shape of `{ "schemaVersion": 1, "values": { … } }`, decided during
|
|
50
|
+
implementation: the `values` envelope keeps a plugin key from ever colliding with
|
|
51
|
+
`schemaVersion`;
|
|
52
|
+
- `pluginId` validation before any filesystem access: one non-empty path segment, no
|
|
53
|
+
separators, no `.` or `..`, and a resolved path that cannot leave
|
|
54
|
+
`<config home>/gentle-shell-bridge`;
|
|
55
|
+
- read/write accessors (`get(key, fallback)`, `set(key, value)`) scoped to that one file.
|
|
56
|
+
|
|
57
|
+
**3. A plugin may keep using its own file** — gentle-shell plugins do this today, e.g.
|
|
58
|
+
`loon-tui.json`. The bridge never reads or writes a plugin's own file. `createStore` is a
|
|
59
|
+
convenience for plugins that have no file yet, not a migration target for plugins that do.
|
|
60
|
+
|
|
61
|
+
**4. One exception, and only one: a sidebar section's visibility is bridge-owned state.**
|
|
62
|
+
`registerSection` makes the bridge create the show/hide toggle, so the value behind that toggle
|
|
63
|
+
cannot belong to the plugin: there is no plugin row, no plugin-chosen setting name and no plugin
|
|
64
|
+
store in the flow. The bridge persists it in its own namespaced file,
|
|
65
|
+
`<gentle config home>/gentle-shell-bridge/bridge-sections.json`, as
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{ "schemaVersion": 1, "values": { "sections": { "loon.music": false } } }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
one key (`sections`) mapping section id → boolean, defaults taken from the section's
|
|
72
|
+
`defaultVisible`, non-boolean values ignored rather than coerced.
|
|
73
|
+
|
|
74
|
+
Why not the host's `visual-customization.json`: gentle-shell validates that file against a closed
|
|
75
|
+
`VISUAL_SECTION_KEYS` list (`lib/visual-customization-policy.ts:82`, `keysMatch` ~:112-130) and
|
|
76
|
+
rejects unknown keys, so a third-party section key cannot live there. The user experience is the
|
|
77
|
+
native one; the file deliberately is not, and that difference is documented in
|
|
78
|
+
[contract.md](contract.md) §11 rather than hidden.
|
|
79
|
+
|
|
80
|
+
## Consequences
|
|
81
|
+
|
|
82
|
+
### The correctness rule
|
|
83
|
+
|
|
84
|
+
`current()` must read the plugin's live state at render time, and `action()` must persist
|
|
85
|
+
first and re-render second. Concretely: `action()` writes through `createStore` (or its own
|
|
86
|
+
file) and returns; the view's own repaint re-evaluates `current()`, which reads the same
|
|
87
|
+
store, so `●` lands on the value that was just written. Inverting the order — paint, then
|
|
88
|
+
persist — makes the bullet lie for one frame and, on a failed write, permanently.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
choice: { setting: "Loon → Banner source", value, current: () => read() === value },
|
|
92
|
+
action: () => store.set("bannerSource", value), // persist; repaint follows
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The bridge cannot enforce this. It can only document the rule; a plugin that caches a value
|
|
96
|
+
at registration produces a stale `●` and the diagnostics command will not detect it.
|
|
97
|
+
|
|
98
|
+
### Bridge-owned section visibility
|
|
99
|
+
|
|
100
|
+
This is the one place the bridge is not stateless, and the reason is structural: the bridge
|
|
101
|
+
generates the toggle, so only the bridge can know which value it points at. The plugin still owns
|
|
102
|
+
nothing — it never sees this file, never picks a key, and its section appears or disappears from
|
|
103
|
+
the same user gesture as a native one.
|
|
104
|
+
|
|
105
|
+
- The blast radius stays per surface: one corrupt `bridge-sections.json` disables section
|
|
106
|
+
visibility only. It reads as empty (defaults apply), is reported by `/bridge-status` as a store
|
|
107
|
+
problem, and is rewritten as a valid document by the next toggle — the same rules
|
|
108
|
+
`createStore` already follows for plugin files.
|
|
109
|
+
- A plugin that also keeps its own state is unaffected: this file is keyed by section id and the
|
|
110
|
+
bridge never reads anything else from it.
|
|
111
|
+
- The cost is honest: the bridge now owns one preference. It is a preference about the bridge's
|
|
112
|
+
own surface, which is why it belongs here and not in a plugin's namespace.
|
|
113
|
+
|
|
114
|
+
### Reads, cost and limits of bridge-owned visibility
|
|
115
|
+
|
|
116
|
+
- Visibility is read once per frame by the renderer and once per `current()` call by the rows, so a
|
|
117
|
+
toggle shows up on the sidebar's next frame (pinned by a test), at the cost of a small file read
|
|
118
|
+
per read. A cache would need to be invalidated by the toggle's own write.
|
|
119
|
+
- There is no migration path from a future `visual-customization.json` section key; if the host ever
|
|
120
|
+
opens `VISUAL_SECTION_KEYS`, this file becomes redundant and can be migrated in one place.
|
|
121
|
+
|
|
122
|
+
### Other consequences
|
|
123
|
+
|
|
124
|
+
- One plugin's corrupt or slow file cannot affect another plugin's rows or the menu.
|
|
125
|
+
- Two plugins writing concurrently never touch the same file, so there is no cross-plugin
|
|
126
|
+
write contention and no cross-plugin lock protocol to design.
|
|
127
|
+
- Duplicate work is accepted: N plugins may each implement a small atomic-write helper.
|
|
128
|
+
The alternative was N plugins contending on one file owned by the bridge.
|
|
129
|
+
- A malformed store file is reported through `/bridge-status` and reads fall back to the
|
|
130
|
+
plugin's defaults; the bridge does not repair or delete the file, and a later `set` writes
|
|
131
|
+
a valid document. Existing `values` are carried into that write **only** when the file was
|
|
132
|
+
a usable `schemaVersion: 1` document (so a partially written file keeps its other keys).
|
|
133
|
+
For an unknown `schemaVersion` the write stores only the keys the caller set and records
|
|
134
|
+
the discarded payload as a store problem: the bridge cannot know what a future schema's
|
|
135
|
+
payload means, so re-labelling it as schema 1 would be a lie about its format, and
|
|
136
|
+
silently destroying it while pretending it is valid would be worse than reporting it.
|
|
137
|
+
Foreign top-level keys are always dropped. A reported problem stays on record until a
|
|
138
|
+
write resolves it, so reading the repaired file cannot erase it from `/bridge-status`.
|
|
139
|
+
- A plugin that keeps its own file gets no migration help from the bridge — this is the
|
|
140
|
+
documented cost of the opt-in helper.
|
|
141
|
+
- The bridge acquires disk-write code it must test, and it does test it (round-trip,
|
|
142
|
+
malformed file, atomicity, path rejection on temporary directories); if `createStore`
|
|
143
|
+
proves to add more risk than boilerplate it removes, it can be dropped without changing
|
|
144
|
+
`registerRows`.
|
|
145
|
+
|
|
146
|
+
## Alternatives rejected
|
|
147
|
+
|
|
148
|
+
**A. Bridge centralises all state in one JSON file.** One file under the gentle config home
|
|
149
|
+
holding every plugin's settings. Rejected: a corrupt or truncated file breaks every plugin
|
|
150
|
+
at once, and concurrent writers from independent plugins contend on the same file — the
|
|
151
|
+
exact cross-plugin coupling the bridge exists to remove. It also forces the bridge to
|
|
152
|
+
understand, validate and migrate data from plugins it does not control.
|
|
153
|
+
|
|
154
|
+
**B. Bridge writes nothing and offers no helper.** Maximum separation: composition only.
|
|
155
|
+
Rejected: every plugin re-implements atomic writes, namespace paths and version fields, and
|
|
156
|
+
there is no shared migration story — a plugin that gets it wrong loses user settings, and
|
|
157
|
+
there is no single place to fix the pattern. The duplication is real and predictable, which
|
|
158
|
+
is why the optional helper wins over pure minimalism.
|
|
159
|
+
|
|
160
|
+
**C. Chosen: stateless composition plus optional namespaced store.** Keeps the blast radius
|
|
161
|
+
per plugin (A's failure mode), keeps a fixable default for the write path (B's gap), and
|
|
162
|
+
leaves existing plugin files untouched.
|
|
163
|
+
|
|
164
|
+
**D. Rejected: the plugin imports the bridge by path, or the user sets
|
|
165
|
+
`GENTLE_SHELL_BRIDGE_MODULE`.** The worked example used to resolve the package by trying a
|
|
166
|
+
dynamic `import()` of candidate `src/index.ts` paths. Rejected: a path is not a contract between
|
|
167
|
+
two independent projects — it names one checkout on one machine, reaches into another package's
|
|
168
|
+
files, and breaks silently the day either repository moves. An environment variable moves the
|
|
169
|
+
same path into every user's shell setup, so the feature does nothing until someone is told to set
|
|
170
|
+
it, which contradicts "one call, zero extra steps". Neither channel carries a version gate, so
|
|
171
|
+
either can hand a plugin an object it cannot use with no way to notice. Removed from the example
|
|
172
|
+
in `f0f30fe`.
|
|
173
|
+
|
|
174
|
+
**E. Chosen: publish a frozen, versioned object on `globalThis`; consume it at `session_start`.**
|
|
175
|
+
The bridge publishes `{ schema, version, VERSION, registerRows, registerSection, createStore }` at
|
|
176
|
+
`Symbol.for("gentle-shell-bridge/api/v1")` when its module loads — first-wins, never overwritten,
|
|
177
|
+
never reset. A plugin registers from its own `session_start` handler: Pi completes every extension
|
|
178
|
+
module's async initialization before firing that event (`docs/extensions.md:213`, `:393`, `:512`),
|
|
179
|
+
so the slot either holds the object or the bridge is not installed. (`registerCardStyle` joined the
|
|
180
|
+
object in 0.2.0 and `registerTransform` in 0.3.0; the gate stays on the three original members, so an
|
|
181
|
+
older object is still a usable
|
|
182
|
+
bridge for rows, sections and the store.) A consumer gates on the schema
|
|
183
|
+
string, `version === 1` and the three callable members; anything else is absent, `bridgeApi()`
|
|
184
|
+
returns `undefined`, and the bridge warns once per process. `bridgeApi()` and `isBridgeApi(value)`
|
|
185
|
+
are the same handshake for a plugin that does import the package, and the exact form is quoted in
|
|
186
|
+
[contract.md](contract.md) §9. It follows the `pi-lens` bridge convention (stable symbol, versioned,
|
|
187
|
+
first-wins frozen, new major = new key) and gentle-shell's schema-string plus shape gate.
|
|
188
|
+
|
|
189
|
+
**History: a deferred-registration slot was removed.** An earlier revision of this decision added
|
|
190
|
+
a second `Symbol.for` slot holding initialisation callbacks, run at publish time and on every
|
|
191
|
+
`session_start`. It existed only to let a plugin register at module load — the one moment where
|
|
192
|
+
"not yet" and "never" are indistinguishable — and `session_start` already fires after every module
|
|
193
|
+
has loaded, so that path was redundant. Removing it also removed the only path that could defer a
|
|
194
|
+
plugin silently: a newer plugin against an older bridge now sees "absent", which is the intended
|
|
195
|
+
answer.
|
|
196
|
+
|
|
197
|
+
The cost is real and accepted: every plugin repeats the same handful of lines — a symbol lookup
|
|
198
|
+
and a shape gate — and copies them into its own file, because the whole point is that it cannot
|
|
199
|
+
import the bridge. That boilerplate lasts until Pi ships a real cross-extension registry; on that
|
|
200
|
+
day the inline block in each plugin becomes one call and this decision is superseded, not extended.
|
|
201
|
+
|
|
202
|
+
**F. Rejected: ask gentle-shell for a card-style registry.** A third style could be supported
|
|
203
|
+
properly — `CARD_STYLE` gaining members, the style policy file accepting any registered id, the
|
|
204
|
+
frame functions living in the host — by one upstream change. Rejected: this package exists
|
|
205
|
+
precisely because gentle-shell does not take every change (the sidebar work landed here for the
|
|
206
|
+
same reason). A design that waits for an upstream merge is not a bridge: it makes the feature's
|
|
207
|
+
delivery depend on a different project's review queue, and it would fork the moment the host
|
|
208
|
+
declined. `CARD_STYLE` stays a two-member union (`lib/shell-card.ts:33-38`), any unknown stored
|
|
209
|
+
value normalizes to `float` (`:43-45`), and the host keeps its own `Card style` line exactly as it
|
|
210
|
+
built it — the bridge only extends that line and repaints what the host already drew.
|
|
211
|
+
|
|
212
|
+
**G. Chosen: repaint the frames, opt-in, and degrade to native.** The bridge takes the `float`
|
|
213
|
+
frame the host drew and re-renders it with the selected style's three functions, leaving every
|
|
214
|
+
non-card row and every non-float card byte-identical. `native` is the default and the pass-through;
|
|
215
|
+
selecting a host value (`neon`/`float`) is the way back; a style that throws is a pass-through for
|
|
216
|
+
that frame and is reported; a disposed style resolves to `missing`, which behaves as `native`. The
|
|
217
|
+
selection is bridge-owned state in the bridge's own file (`bridge-card-style.json`), not the host's
|
|
218
|
+
`card-style.json`, for the same closed-key reason section visibility uses `bridge-sections.json`.
|
|
219
|
+
Costs accepted and recorded: the repaint depends on the host's `float` grammar, and a downstream
|
|
220
|
+
post-processor can override the frame (see [architecture.md](architecture.md) §14).
|
|
221
|
+
|
|
222
|
+
**H. Chosen: the conversation wrapper runs a general, ordered transform pipeline.** Decision G
|
|
223
|
+
proved that the conversation component can be repainted, but it was written as one hard-coded
|
|
224
|
+
feature. A `pi.registerToolRenderer` resolver cannot apply a rendering choice to rows that are
|
|
225
|
+
already drawn — it resolves when a row is BUILT — so that hook cannot serve a plugin that wants to
|
|
226
|
+
restyle history. Rather than a second wrapper, the existing one became a pipeline: the card-style
|
|
227
|
+
repaint runs first, then every transform registered with `registerTransform`, in order, each
|
|
228
|
+
receiving the previous output. The bridge keeps what it already owed the frame — clip to the host's
|
|
229
|
+
width, isolate a throw to the transform that made it, return the same array when nothing changed —
|
|
230
|
+
and reports the last frame's outcome per transform in `/bridge-status`. The alternative (a renderer
|
|
231
|
+
resolver plus a transform, one for new rows and one for history) was rejected because it gives one
|
|
232
|
+
plugin two different pictures of the same card, which is the defect the surface exists to remove.
|
|
233
|
+
|
|
234
|
+
## Open questions
|
|
235
|
+
|
|
236
|
+
- Should `createStore` ship at all in the first release, or follow once a second plugin
|
|
237
|
+
needs it? — **Settled during implementation: it ships.** Deferring would have been
|
|
238
|
+
compatible with this decision; nothing in `registerRows` depends on the store.
|
|
239
|
+
- Whether migrations beyond `schemaVersion: 1` belong in the bridge or in the plugin.
|
|
240
|
+
- Whether a plugin that keeps its own file should be able to *declare* that path to
|
|
241
|
+
`/bridge-status` for diagnostics only. No read, no write — purely visibility.
|
|
242
|
+
|
|
243
|
+
## Limits
|
|
244
|
+
|
|
245
|
+
- No file locking is specified: atomic rename prevents torn writes, not lost updates from
|
|
246
|
+
two processes editing the same plugin file. A plugin that runs in parallel sessions must
|
|
247
|
+
accept last-writer-wins or guard its own writes.
|
|
248
|
+
- `createStore` is implemented and tested against temporary directories, including the
|
|
249
|
+
malformed-file path; it has not been exercised inside a running Pi session, and it has no
|
|
250
|
+
cross-process locking (see the first limit).
|
|
251
|
+
- Secrets do not belong in these files; the store provides no redaction.
|