opendoor-plugin 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kai Ritterbusch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,299 @@
1
+ # opendoor — a Doorstop requirements-management plugin for PI WEB
2
+
3
+ `opendoor` is a **paired** PI WEB plugin that surfaces
4
+ [Doorstop](https://github.com/doorstop-dev/doorstop) requirements stored in
5
+ the workspace next to the code they trace to: a **Requirements** workspace tab
6
+ with the document tree, per-item state (reviewed/unreviewed changes, suspect
7
+ links), validation findings, and one-click `doorstop` CLI actions (validate,
8
+ publish, add/link/unlink, clear suspect links, review), plus agent prompts
9
+ that insert requirement context into the session. When paired, the plugin's
10
+ server entry runs the CLI headlessly and the captured output is displayed in
11
+ the panel itself (a **status bar** at the bottom); when unpaired, every action
12
+ falls back to the workspace terminal. The paired backend also recovers the
13
+ reviewed version of an item file from git history, so the item detail pane
14
+ can show a **Changes since review** diff — and Review can optionally record
15
+ a pathspec-limited review commit (`commitAfterReview`).
16
+
17
+ Full feature spec: [`docs/feature-doorstop-plugin.md`](docs/feature-doorstop-plugin.md)
18
+ (§5 architecture, §6 requirements state model, §7–§8 UI/actions).
19
+
20
+ ## Status
21
+
22
+ The plugin is fully wired and green, in two halves:
23
+
24
+ - **Browser entry** (`src/pi-web-plugin.ts`, apiVersion 2, name `Opendoor`)
25
+ delegates to `createOpendoorBrowserContributions` in
26
+ `src/doorstop-contributions.ts`, which contributes:
27
+
28
+ - **panel** `workspace.doorstop` (title **Requirements**, order 60) — the Lit
29
+ body element `<pi-web-opendoor-panel-body>`, driven by the per-workspace
30
+ controller registry (`src/doorstop-panel.ts`): five vertical sections —
31
+ `project-actions` (title, Items/Findings toggle, Refresh / Run validation
32
+ / Publish HTML), `item-list` (filters row + item rows with state chips),
33
+ `item-details` (links in/out, references, extended attributes,
34
+ collapsible **Changes since review** diff), `item-action-palette`
35
+ (Review / Clear suspect links / Edit / Link / Unlink / Ask-agent menu,
36
+ sized to the host prompt footer), and `status-bar` (status badge,
37
+ duration, Dismiss button, and the auto-expanding stdout/stderr of the
38
+ latest CLI run, plus the review-commit outcome line when the review
39
+ requested a commit);
40
+ - **actions** `view.doorstop` (mod+7) and `workspace.refresh-doorstop`
41
+ (mod+shift+d);
42
+ - **label** `doorstop-status` — the informational requirement-health counts
43
+ (async-cache idiom, feature spec §7.4).
44
+
45
+ - **Server entry** (`src/server-plugin.ts`, default export `PiWebServerPlugin`)
46
+ contributes a workspace provider that claims only projects whose root
47
+ contains a `.doorstop.yml` (cheap `fs.access` probe — everything else stays
48
+ with the bundled Git provider) and serves a six-operation backend:
49
+ `doorstop.run` runs one Doorstop CLI invocation — a review request carrying
50
+ the opt-in `commit: true` flag is followed by a pathspec-limited git commit
51
+ of the item file (`doorstop: review <uid>`) — the read-only
52
+ `doorstop.item-baseline` recovers the reviewed version of an item file from
53
+ git history, and the four git actions `doorstop.git-status`,
54
+ `doorstop.git-stage`, `doorstop.git-unstage`, and `doorstop.git-commit`
55
+ power the panel's git strip and stage/unstage/commit controls (see **Git
56
+ actions** below). `src/doorstop-backend.ts` validates each request, builds argv
57
+ server-side (the browser never sends shell strings), and runs through the
58
+ host's `execFile()` helper with `cwd` = the workspace path, timeouts
59
+ clamped to 8.5 s (the review+git pipeline shares one deadline budget under
60
+ the host's 10 s callback bound), and per-workspace run serialization.
61
+
62
+ Dispatch (per action, in `src/doorstop-panel-elements.ts`): when the workspace
63
+ is opendoor-owned with an active backend, the action sends a structured
64
+ request via `context.backend.request("doorstop.run", …)` (the review-commit
65
+ flag rides along when the setting is enabled; the "Changes since review"
66
+ section fetches via `context.backend.request("doorstop.item-baseline", …)`)
67
+ and the captured output appears in the panel's status bar (auto-expanded);
68
+ the panel auto-rescans on completion. When the backend is absent (unpaired
69
+ install, or a workspace the provider does not own), the action falls back to
70
+ `terminal.runCommand()` with the same shell command, so output stays visible
71
+ and auditable in the terminal.
72
+
73
+ `doorstop.run` and `doorstop.item-baseline`) shared by both halves, plus the
74
+ four git-action contracts (`doorstop.git-status`, `doorstop.git-stage`,
75
+ `doorstop.git-unstage`, `doorstop.git-commit`), live in
76
+ `src/doorstop-backend-contract.ts`. Later modules import these and must not
77
+ redefine them.
78
+
79
+ ## Workspace settings (`.pi-web/opendoor.json`)
80
+
81
+ Optional per-workspace configuration, read by the plugin on every load
82
+ (missing file → defaults; malformed or invalid values → warning diagnostics
83
+ in the panel + defaults). Workspace-scoped settings cannot use pi-web's
84
+ `plugins.<id>.settings` config (that is host-side and only reaches server
85
+ entries), so this file plays the same role `.pi-web/tasks.json` plays for the
86
+ bundled workspace-tasks plugin. The *server* entry's host-scoped knobs
87
+ (CLI binary path, exec timeout) are configured through
88
+ `plugins.opendoor.settings` instead — see **Host settings** below:
89
+
90
+ ```json
91
+ {
92
+ "version": 1,
93
+ "publish": { "target": "./public" },
94
+ "discovery": { "excludedDirectories": ["published", "vendor"] },
95
+ "commitAfterReview": false
96
+ }
97
+ ```
98
+
99
+ - `publish.target` — path passed to `doorstop publish all`. Must be a safe
100
+ workspace-relative path (no `..`, no absolute, no drive prefix, no leading
101
+ `-`). It is shell-quoted at the terminal-command boundary when it contains
102
+ characters outside the inert `\w./-` alphabet; on the backend path the
103
+ target is passed as an argv element, never through a shell.
104
+ - `discovery.excludedDirectories` — additional directory names the
105
+ `.doorstop.yml` walk never enters (merged with the built-in
106
+ `.git`/`node_modules` skips, max 16 names).
107
+ - `commitAfterReview` — opt-in boolean (default `false`): when enabled on a
108
+ paired workspace, the **Review** action's `doorstop review <uid>` is
109
+ followed by a pathspec-limited git commit of the item file with the
110
+ conforming message `doorstop: review <uid>` (git `add` then
111
+ `commit -m … -- <path>` — other staged/unstaged work is left untouched).
112
+ Committing user content is strictly opt-in, and the status bar
113
+ narrates every commit outcome (`commit: <sha>` / `clean` (already
114
+ committed) / `skipped` (not a git repository | item file not found |
115
+ review failed | deadline) / `failed` — `<stderr>`). Under the default,
116
+ Review behaves exactly as before.
117
+
118
+ ## Host settings (`plugins.opendoor.settings`)
119
+
120
+ Optional, server-side only (requires the paired install and a sessiond
121
+ restart to take effect). Parsed leniently; wrong types fall back to defaults:
122
+
123
+ ```json
124
+ { "doorstopPath": "/opt/venv/bin/doorstop", "gitPath": "git", "timeoutMs": 8000 }
125
+ ```
126
+
127
+ - `doorstopPath` — the Doorstop CLI binary. Defaults to `"doorstop"` resolved
128
+ on the **sessiond host PATH** (the daemon environment, not your login shell
129
+ — set this when the CLI lives in a venv or is not on the service PATH).
130
+ - `gitPath` — the git binary used by the review→commit pipeline, the
131
+ `doorstop.item-baseline` fetch, and the four git actions (status strip,
132
+ Stage all, per-item Unstage, Git commit). Defaults to `"git"` resolved on the
133
+ **sessiond host PATH** — set it when git is not on the service PATH (the
134
+ same escape hatch as `doorstopPath`).
135
+ - `timeoutMs` — per-run exec timeout in ms. May SHORTEN the 8 500 ms default
136
+ but never exceed it (the host bounds every provider callback at 10 s, so a
137
+ longer doorstop run would be killed by pi-web, not by the plugin). It also
138
+ bounds every git exec of the review→commit pipeline, the baseline fetch,
139
+ and the git actions, which share one deadline budget per callback.
140
+
141
+ ## Using the panel
142
+
143
+ The right-hand panel body is divided into five vertical sections, top to
144
+ bottom:
145
+
146
+ 1. **`project-actions`** — the project heading (the workspace's root path,
147
+ basename shown, full path on hover), the **Items / Findings** toggle, the
148
+ project-scoped buttons **Refresh**, **Run validation**, and **Publish
149
+ HTML**, and — on paired installs — the **Git status** strip, the **Stage
150
+ all** button, and the **Git commit** control (message input + button); see
151
+ **Git actions** below.
152
+ 2. **`item-list`** — a filters row (document chips, state filter, search)
153
+ above the item rows.
154
+ 3. **`item-details`** — the selected item's full detail pane (unchanged).
155
+ 4. **`item-action-palette`** — the item actions (**Review**, **Clear suspect
156
+ links**, **Edit**, **Unlink**/**Link** + target UID inputs, **Ask agent**
157
+ menu), moved out of the detail pane so the row spans the panel at the same
158
+ height as the center-panel chat prompt footer. A muted "Select an item…"
159
+ placeholder keeps the palette at constant height while nothing is
160
+ selected; the palette is hidden in the Findings view.
161
+ 5. **`status-bar`** — the run status row (badge, duration, **Dismiss**),
162
+ sized like the center-panel status bar, plus the captured output.
163
+
164
+ - **Items / Findings toggle** in the project-actions row. Findings lists all
165
+ plugin-computed findings and diagnostics (error > warning > info); a
166
+ finding's UID navigates back to the item. Findings are informational — run
167
+ the **Run validation** button for the authoritative `doorstop` output
168
+ (paired: rendered in the panel's status bar; unpaired: in the workspace
169
+ terminal).
170
+ - **Status bar**: after a paired action completes, the bottom bar shows the
171
+ run's status (ok / failed / killed / error) and duration, and the panel
172
+ re-scans automatically. The captured stdout/stderr *auto-expands* whenever
173
+ a run produces a message to show (any output, an error message, or a
174
+ review-commit line) — no interaction needed; a run with nothing to display
175
+ stays collapsed to its status row. **Dismiss** clears the run and collapses
176
+ the bar; the next run with output re-expands it. Runs killed at the exec
177
+ timeout preserve and show partial output; prefer the workspace terminal for
178
+ long publishes. When a review ran with `commitAfterReview` enabled, the bar
179
+ adds a `commit: …` narration line (`commit: <short-sha>` /
180
+ `clean (already committed)` / `skipped (not a git repository | item file
181
+ not found | review failed | deadline)` / `failed — <stderr excerpt>`); a
182
+ failed or skipped commit never changes the run's ok/failed badge.
183
+ - **State chips** per item (feature spec §6): inactive, non-normative,
184
+ reviewed / unreviewed changes, suspect links, no child links, no links,
185
+ unknown link, missing reference.
186
+ - **Changes since review** (item detail pane, paired installs only): for an
187
+ item with unreviewed changes and a stored `reviewed` fingerprint, a
188
+ collapsed section shows what changed since the last review. Expanding it
189
+ lazily fetches the reviewed version of the item file from git history via
190
+ the `doorstop.item-baseline` backend operation, stamp-matches it against
191
+ the stored fingerprint, and renders a text line diff plus chips for
192
+ `ref`/`references`/link-UID/extended-attribute changes. When no baseline
193
+ can be recovered it says so honestly ("no git history", "could not locate
194
+ the reviewed version — history may have been rewritten"). The
195
+ `doorstop: review <uid>` commit message is the historical anchor that the
196
+ deferred browser-side git parsing of feature spec §12 could not give — the
197
+ recovery runs server-side, where git lives.
198
+ - **Item actions** (item-action-palette): Review (`doorstop review UID` — with
199
+ `commitAfterReview` enabled and paired, followed by the pathspec-limited
200
+ review commit), Clear suspect links (`doorstop clear UID [parents…]`),
201
+ Edit (`doorstop edit UID`), Link/Unlink via inline UID input (validated
202
+ against the Doorstop UID grammar; shell metacharacters rejected), and — on
203
+ paired installs — Stage / Unstage for the selected item's own file
204
+ (`doorstop.git-stage` / `doorstop.git-unstage` with just that item's
205
+ path; see **Git actions** below). Each is independently disabled: Stage
206
+ when the file has no unstaged (`Y`) changes, Unstage when it has nothing
207
+ staged (`X`), and both while a run is in flight.
208
+ - **Git actions** (paired installs only; every git control is hidden
209
+ without the paired backend — terminal-fallback users commit manually as
210
+ today):
211
+ - **Git status strip** — a compact readout above the item list:
212
+ `⎇ <branch> · <n> staged · <n> dirty · ↑<ahead> ↓<behind>` (zero/absent
213
+ parts omitted; `no git` outside a repository; a retry link on fetch
214
+ errors). The strip refreshes on panel load, after every run (a stage,
215
+ unstage, or commit invalidates the cache), and whenever you click it.
216
+ Outside a git repository the strip shows `no git` and the stage/unstage/
217
+ commit controls are no-ops that report `skipped`.
218
+ - **Stage all** — stages every Doorstop-managed file (the root
219
+ `.doorstop.yml`, each document's config file, each item file) via the
220
+ `doorstop.git-stage` backend operation. Staging is idempotent: a second
221
+ click over an already-staged set reports `clean — nothing to stage`
222
+ instead of a git error (staged deletions in particular are skipped
223
+ rather than fatal). Disabled while a run is in progress or when the
224
+ loaded index has no documents. Note: staging is only as fresh as the
225
+ loaded index — files created on disk after the last **Refresh** are
226
+ missed until you refresh and stage again.
227
+ - **Unstage (per item)** — while an item's file has staged (index)
228
+ changes, its list row shows a **minus** in place of the usual plus, and
229
+ the item palette enables an **Unstage** button, both served by the
230
+ `doorstop.git-unstage` operation (`git reset -q -- :(literal)<path>`
231
+ server-side). The row's pointer shortcut prefers the minus when a file
232
+ is both staged and further modified (`MM`), so "I staged by mistake"
233
+ always surfaces a minus; the palette shows Stage and Unstage as two
234
+ independently-disabled buttons in that case. Unstaging restores the
235
+ index entry but never touches the working tree: a staged addition
236
+ returns to `??` untracked, a staged deletion returns to an unstaged
237
+ deletion. It is idempotent — a second Unstage over an already-clean path
238
+ reports `clean — nothing to unstage`. Unmerged paths (`UU`, `AA`, `DD`,
239
+ `DU`, `AU`, `UD`, `UA`) are excluded — a `git reset` on an unmerged path
240
+ would silently resolve the conflict in the index. On unpaired installs no
241
+ git control is rendered at all; while a run is in flight no minus appears
242
+ and the palette buttons are disabled (the per-row affordance is also
243
+ suppressed when the status file list is truncated, so stale path data is
244
+ never acted on).
245
+ - **Git commit** — a single-line message input plus a **Commit** button,
246
+ served by the `doorstop.git-commit` operation. The commit records **the
247
+ staged index** (`git commit -m <message>` with no pathspec and no
248
+ add) — exactly like the git CLI. Consequences, all intended: unstaged
249
+ WIP is never swept into a commit; files you staged yourself (via the
250
+ git CLI or any other tool) are committed too; the strip's `staged`
251
+ count tells you what a commit will record before you click. Empty or
252
+ whitespace-only messages are rejected client-side (git with an empty
253
+ `-m` would open an editor and hang the exec). Committing with nothing
254
+ staged reports `clean — nothing staged`. Outcomes are narrated in the
255
+ status bar (`staged <n> paths` / `committed <sha>` / `clean — nothing
256
+ to stage` / `clean — nothing staged` / `skipped` / `failed — <stderr
257
+ excerpt>`); a failing pre-commit hook surfaces its stderr in the
258
+ expanded status bar body.
259
+ - **Ask the agent** menu: inserts ready-made prompts (explain, fix suspect
260
+ links, draft child requirement, review readiness) into the session prompt
261
+ editor using the item's paths and state (feature spec §7.3).
262
+ - Fingerprints and the requirement state are recomputed in the browser from
263
+ the YAML files; everything mutating goes through the visible `doorstop`
264
+ CLI — paired, headlessly via the server entry (output captured in the
265
+ panel), or in a workspace terminal when unpaired — plus, opt-in, the
266
+ narrated pathspec-limited review commit (`commitAfterReview`) — so all
267
+ effects stay auditable in git.
268
+
269
+ ## Commands
270
+
271
+ ```sh
272
+ npm install # install dependencies
273
+ npm test # vitest (unit tests under src/**/*.test.ts)
274
+ npm run typecheck # tsc --noEmit (strict)
275
+ npm run build # esbuild: dist/browser/pi-web-plugin.js + dist/server-plugin.js
276
+ # (+ verbatim package.json manifest copy into dist/)
277
+ npm run dev # watch mode build
278
+ ```
279
+
280
+ ## Installing the plugin locally (id `opendoor`)
281
+
282
+ ```sh
283
+ npm run build
284
+ ln -s "$(pwd)/dist" ~/.pi-web/plugins/opendoor
285
+ ```
286
+
287
+ Then **restart the session daemon** (server entries load at sessiond startup
288
+ only; the browser bundle hot-reloads on page reload) and reload the PI WEB
289
+ page:
290
+
291
+ ```sh
292
+ systemctl --user restart pi-web-sessiond.service
293
+ curl -s http://127.0.0.1:8504/pi-web-plugins/manifest.json
294
+ ```
295
+
296
+ The manifest should list an `opendoor` plugin entry **with a
297
+ `backendRevision`** (signing the paired server module). If `backendRevision`
298
+ is missing, the server module failed to load — check the sessiond log; the
299
+ plugin still works fully via the terminal fallback.