opendoor-plugin 0.2.0 → 0.5.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/README.md CHANGED
@@ -1,300 +1,279 @@
1
1
  # opendoor — a Doorstop requirements-management plugin for PI WEB
2
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`).
3
+ `opendoor` is a PI WEB plugin that surfaces
4
+ [Doorstop](https://github.com/doorstop-dev/doorstop) requirements stored in the
5
+ workspace in a **Requirements** workspace tab with
6
+ the document tree, per-item state (reviewed/unreviewed changes, suspect links),
7
+ validation findings and `doorstop` actions, plus agent prompts
8
+ that insert requirement context into the session.
9
+ The paired backend also recovers an item file's reviewed version from git for
10
+ the **Changes since review** diff, and Review can opt in to a review commit
11
+ (`commitAfterReview`).
16
12
 
17
13
  Full feature spec: [`docs/feature-doorstop-plugin.md`](docs/feature-doorstop-plugin.md)
18
14
  (§5 architecture, §6 requirements state model, §7–§8 UI/actions).
19
15
 
20
16
  ## Status
21
17
 
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.
18
+ Requires **pi-web 1.202611.1 or newer** (browser and server plugin API v5).
19
+
20
+ One package (`opendoor-plugin`) ships the PI WEB plugin (both entries below)
21
+ and the Pi extension (`/opendoor-review`).
22
+
23
+ - **Browser entry** (`src/browser/pi-web-plugin.ts`, apiVersion 5, name
24
+ `Opendoor`) delegates to `createOpendoorBrowserContributions`
25
+ (`src/browser/doorstop-contributions.ts`): the **panel** `workspace.doorstop`
26
+ (title **Requirements**, order 5 — below the Files panel's order 10 so doorstop
27
+ item links reach its `fileOpenQuery` first, at the cost of the Requirements tab
28
+ preceding Files in the tab order; controller registry in
29
+ `src/browser/doorstop-panel.ts`, sections in **Using the panel**),
30
+ the **actions** `view.doorstop` (mod+7) and `workspace.refresh-doorstop`
31
+ (mod+shift+d), the **label** `doorstop-status` (requirement-health counts,
32
+ feature spec §7.4), and the **session-row label** `doorstop.review` (reopens the
33
+ Requirements tab from a row carrying public `opendoor` metadata, archived rows
34
+ included — feature spec, **Session metadata and session-list row labels**).
35
+ - **Server entry** (`src/server-plugin.ts`, apiVersion 5, default export
36
+ `PiWebServerPlugin`) contributes a workspace provider for projects whose root
37
+ has a `.doorstop.yml` (cheap `fs.access` probe; everything else stays with the
38
+ bundled Git provider) and serves six operations: `doorstop.run` (one CLI
39
+ invocation; a review carrying the opt-in `commit: true` flag is followed by a
40
+ pathspec-limited `doorstop: review <uid>` commit), the read-only
41
+ `doorstop.item-baseline` (the reviewed version, from git history), and
42
+ `doorstop.git-status` / `-stage` / `-unstage` / `-commit` (see **Git
43
+ actions**). `src/doorstop-backend.ts` validates each request, builds argv
44
+ server-side (never a browser shell string) and runs it via `execFile()`; see
45
+ its header for timeouts and per-workspace serialization. The entry declares no
46
+ `requires` and no `start()`: the retired `doorstop.ask-agent` dispatch could
47
+ not live there (provider-cycle rule) — see `src/server-plugin.ts`.
48
+ - **Pi extension** (`src/pi-extension/opendoor-review.ts`, bundled to
49
+ `dist/pi-extension/index.js`, declared by `pi.extensions`) registers
50
+ `/opendoor-review <UID> [instruction…]`: it publishes the public `opendoor`
51
+ metadata (`{ schema: 1, op: "review", uids }`) and asks PI WEB's per-session
52
+ dispatch bridge to start an independent conversation in the same project (the
53
+ `spawn_session` rules, model/thinking inherited), reporting every outcome in
54
+ the initiating conversation — the pathway **Start review session** drives.
55
+
56
+ Dispatch (`src/browser/doorstop-panel-element.ts`) uses
57
+ `context.peer.request("doorstop.run", …)` (the review-commit flag rides along
58
+ when enabled; **Changes since review** uses `doorstop.item-baseline`) with the
59
+ output in the auto-expanded status bar, or falls back to `terminal.runCommand()`
60
+ with the same shell command when unpaired. Shared types and operation contracts
61
+ live in `src/doorstop-contract.ts` and `src/doorstop-backend-contract.ts`.
78
62
 
79
63
  ## Workspace settings (`.pi-web/opendoor.json`)
80
64
 
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:
65
+ Optional per-workspace configuration, read on every plugin load (missing file →
66
+ defaults; malformed or invalid values → warning diagnostics + defaults). pi-web's
67
+ `plugins.<id>.settings` is host-side and reaches only server entries, so this
68
+ file plays the role `.pi-web/tasks.json` plays for the bundled workspace-tasks
69
+ plugin; server-entry knobs are in **Host settings** below:
89
70
 
90
71
  ```json
91
72
  {
92
73
  "version": 1,
93
74
  "publish": { "target": "./public" },
94
75
  "discovery": { "excludedDirectories": ["published", "vendor"] },
76
+ "showAdditionalAttribute": ["text", "component"],
95
77
  "commitAfterReview": false
96
78
  }
97
79
  ```
98
80
 
99
- - `publish.target` — path passed to `doorstop publish all`. Must be a safe
81
+ - `publish.target` — path passed to `doorstop publish all`; must be a safe
100
82
  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.
83
+ `-`). Shell-quoted at the terminal-command boundary when it contains characters
84
+ outside the inert `\w./-` alphabet; on the backend path it is an argv element,
85
+ never a shell string.
104
86
  - `discovery.excludedDirectories` — additional directory names the
105
87
  `.doorstop.yml` walk never enters (merged with the built-in
106
88
  `.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.
89
+ - `showAdditionalAttribute` — attribute NAMES rendered in every item-list row as
90
+ `key: value` (values jsonish-quoted, joined with ` · `, in configured order);
91
+ any extended attribute key works, an attribute an item lacks is skipped, and no
92
+ unconfigured attribute ever appears. The modeled `text` is special-cased: the
93
+ body renders raw and unquoted on an indented second line below the row, clamped
94
+ to two lines with an ellipsis (the full body lives in the detail pane), skipped
95
+ when empty. Default `[]` leaves today's rows unchanged. Invalid entries are
96
+ dropped with a warning each, duplicates deduplicated, capped at 16 names;
97
+ unknown keys simply match nothing.
98
+ - `commitAfterReview` — opt-in boolean (default `false`): on a paired workspace
99
+ the **Review** action's `doorstop review <uid>` is followed by a
100
+ pathspec-limited commit of the item file with the conforming message
101
+ `doorstop: review <uid>` (git `add` then `commit -m … -- <path>`; other
102
+ staged/unstaged work is untouched). Committing user content is strictly opt-in;
103
+ the status bar narrates every outcome (`commit: <sha>` / `clean` (already
104
+ committed) / `skipped` (not a git repository | item file not found | review
105
+ failed | deadline) / `failed` — `<stderr>`); Review is otherwise unchanged.
117
106
 
118
107
  ## Host settings (`plugins.opendoor.settings`)
119
108
 
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:
109
+ Optional, server-side only (needs the paired install and a sessiond restart);
110
+ wrong types fall back to defaults:
122
111
 
123
112
  ```json
124
113
  { "doorstopPath": "/opt/venv/bin/doorstop", "gitPath": "git", "timeoutMs": 8000 }
125
114
  ```
126
115
 
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.
116
+ - `doorstopPath` — the Doorstop CLI binary, default `"doorstop"`, resolved on the
117
+ **sessiond host PATH** (the daemon environment, not your login shell — set it
118
+ when the CLI lives in a venv or is off the service PATH).
119
+ - `gitPath` — the git binary for the review→commit pipeline, the
120
+ `doorstop.item-baseline` fetch and the four git actions. Default `"git"` on the
121
+ **sessiond host PATH** — the same escape hatch as `doorstopPath`.
122
+ - `timeoutMs` — per-run exec timeout in ms. May SHORTEN the 8 500 ms default but
123
+ never exceed it (the host bounds every provider callback at 10 s, so a longer run
124
+ would be killed by pi-web, not by the plugin). It also bounds every git exec of
125
+ the review→commit pipeline, the `doorstop.item-baseline` fetch and the git
126
+ actions: one deadline budget per callback.
140
127
 
141
128
  ## Using the panel
142
129
 
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.
130
+ Five vertical sections, top to bottom: `project-actions` (project heading — root
131
+ path, basename shown, full path on hover — the Items/Findings toggle, Refresh, Run
132
+ validation, Publish HTML, and on paired installs the Git strip, Stage all and
133
+ Commit), `item-list` (filters row above rows showing level, UID, excerpt,
134
+ `showAdditionalAttribute` values and state chips), `item-details` (Markdown item
135
+ text, links in/out, references, extended attributes, the Changes diff),
136
+ `item-action-palette` (Review, Clear suspect links, Link, Ask agent — spanning the
137
+ panel at chat-prompt-footer height, with the muted "Select an item…" placeholder,
138
+ hidden in the Findings view) and `status-bar` (see below).
139
+
140
+ - **Items / Findings toggle**: Findings lists plugin-computed findings and
141
+ diagnostics (error > warning > info), and a finding's UID navigates back to the
142
+ item. They are informational — **Run validation** gives the authoritative
143
+ `doorstop` output (paired: status bar; unpaired: terminal).
144
+ - **Status bar** (sized like the center-panel one): after a paired action it shows
145
+ status (ok / failed / killed / error) and duration, and the panel re-scans
146
+ automatically. Captured stdout/stderr *auto-expands* whenever there is something
147
+ to show (output, an error, a review-commit line); a run with nothing to display
148
+ stays collapsed. **Dismiss** clears the run and collapses the bar; the next run
149
+ with output re-expands it. Killed runs keep partial output (prefer the terminal
150
+ for long publishes). With `commitAfterReview` on, the bar adds the `commit: …`
151
+ line from **Workspace settings**; a failed or skipped commit never changes the
152
+ run's ok/failed badge.
153
+ - **State chips** per item (feature spec §6): inactive, non-normative, reviewed /
154
+ unreviewed changes, suspect links, no child links, no links, unknown link,
155
+ missing reference.
156
+ - **Changes since review** (item detail pane, paired installs only): an item with
157
+ unreviewed changes and a stored `reviewed` fingerprint gets a collapsed section;
158
+ expanding it fetches the reviewed item file from git history via
159
+ `doorstop.item-baseline`, stamp-matches it against the fingerprint and renders a
160
+ line diff plus chips for `ref`/`references`/link-UID/extended-attribute changes.
161
+ With no recoverable baseline it says so ("no git history", "could not locate the
162
+ reviewed version — history may have been rewritten"). The `doorstop: review
163
+ <uid>` message is the historical anchor feature spec §12's deferred browser-side
164
+ git parsing could not give; recovery runs server-side.
165
+ - **Item actions** (item-action-palette): Review (`doorstop review UID`, plus the
166
+ review commit when `commitAfterReview` is on), Clear suspect links (`doorstop
167
+ clear UID [parents…]`), Edit (`doorstop edit UID`), Link/Unlink via inline UID
168
+ input (Doorstop UID grammar enforced; shell metacharacters rejected), and on
169
+ paired installs Stage / Unstage for the item's own file (`doorstop.git-stage` /
170
+ `doorstop.git-unstage`; see **Git actions**). Each disables independently: Stage
171
+ without unstaged (`Y`) changes, Unstage with nothing staged (`X`), both mid-run.
172
+ - **Git actions** (paired only; otherwise every git control is hidden and you
173
+ commit manually):
174
+ - **Git status strip** — compact readout above the item list:
175
+ `⎇ <branch> · <n> staged · <n> dirty · ↑<ahead> ↓<behind>` (zero/absent parts
176
+ omitted, `no git` outside a repository, a retry link on fetch errors).
177
+ Refreshes on panel load, after every run (a stage, unstage or commit
178
+ invalidates the cache) and on click; outside a repository the other git
179
+ controls report `skipped`.
180
+ - **Stage all** — stages every Doorstop-managed file (root `.doorstop.yml`, each
181
+ document config, each item file) via `doorstop.git-stage`. Idempotent: a
182
+ second click over an already-staged set reports `clean — nothing to stage`
183
+ instead of a git error (staged deletions are skipped, not fatal). Disabled
184
+ during a run or with no documents in the loaded index, and only as fresh as
185
+ that index — files created after the last **Refresh** are missed until you
186
+ refresh and stage again.
187
+ - **Unstage (per item)** — a staged item file swaps its row's plus for a
188
+ **minus** and enables **Unstage** in the palette, both via
189
+ `doorstop.git-unstage` (`git reset -q -- :(literal)<path>` server-side). A file
190
+ both staged and further modified (`MM`) still shows the minus; the palette then
191
+ offers Stage and Unstage as two independently-disabled buttons. Unstaging
192
+ restores the index entry but never touches the working tree (a staged addition
193
+ returns to `??`, a staged deletion to an unstaged deletion); it is idempotent
194
+ (`clean — nothing to unstage`) and excludes unmerged paths (`UU`, `AA`, `DD`,
195
+ `DU`, `AU`, `UD`, `UA`), where `git reset` would silently resolve the conflict.
196
+ During a run no minus appears and the palette buttons are disabled; the per-row
197
+ affordance is also suppressed when the status file list is truncated, so stale
198
+ path data is never acted on.
199
+ - **Git commit** — a message input plus **Commit**, served by
200
+ `doorstop.git-commit`. It records **the staged index** (`git commit -m
201
+ <message>`, no pathspec, no add), exactly like the git CLI: unstaged WIP is
202
+ never swept in, files you staged yourself are included, and the strip's
203
+ `staged` count shows what a commit will record. Empty or whitespace-only
204
+ messages are rejected client-side (git would open an editor and hang the exec);
205
+ nothing staged reports `clean — nothing staged`. Outcomes are narrated in the
206
+ status bar (`staged <n> paths` / `committed <sha>` / `clean — nothing to stage`
207
+ / `clean — nothing staged` / `skipped` / `failed — <stderr excerpt>`), including
208
+ a failing pre-commit hook's stderr.
209
+ - **Ask the agent** menu: inserts ready-made prompts (explain, fix suspect links,
210
+ draft child requirement, review readiness) into the session prompt editor from
211
+ the item's paths and state (feature spec §7.3). **Start review session**
212
+ composes `/opendoor-review <UID>` plus the item context there and needs no
213
+ paired backend: the extension starts an independent review conversation whose
214
+ row carries a **`firstUid +n` label** reopening the Requirements tab on that
215
+ workspace (unselected and archived rows included, no live Pi connection needed),
216
+ landing on the item detail pane. After the insert the status bar shows a quiet
217
+ `ok` **review dispatch** run row. Without the extension the inserted text is
218
+ still a self-sufficient review instruction, so the review happens in the current
219
+ conversation; a refused dispatch is reported in the chat by the extension (see
220
+ **Session metadata and session-list row labels** in the feature spec).
221
+ - Fingerprints and requirement state are recomputed in the browser from the YAML
222
+ files; everything mutating goes through the visible `doorstop` CLI — paired,
223
+ headlessly via the server entry, or in a workspace terminal when unpaired —
224
+ plus, opt-in, the narrated review commit (`commitAfterReview`) — so all effects
225
+ stay auditable in git.
226
+
268
227
 
269
228
  ## Commands
270
229
 
271
230
  ```sh
272
- npm install # install dependencies
273
- npm test # vitest (unit tests under src/**/*.test.ts)
231
+ npm install # install dependencies
232
+ npm test # vitest (unit tests under src/**/*.test.ts)
274
233
  npm run test:coverage # v8 coverage report
275
- npm run typecheck # tsc --noEmit (strict)
276
- npm run build # esbuild: dist/browser/pi-web-plugin.js + dist/server-plugin.js
277
- # (+ verbatim package.json manifest copy into dist/)
278
- npm run dev # watch mode build
234
+ npm run typecheck # tsc --noEmit (strict)
235
+ npm run build # esbuild → dist/browser, dist/server, dist/pi-extension
236
+ # + the dist-root manifest (see the build script header)
237
+ npm run dev # watch build
279
238
  ```
280
239
 
281
240
  ## Installing the plugin locally (id `opendoor`)
282
241
 
242
+ The package carries two independently enabled halves: the **PI WEB plugin**
243
+ (browser + server entries) and the **Pi extension** (`/opendoor-review`). The
244
+ repository-root `package.json` is the npm-package-root manifest, so both halves
245
+ come from ONE install through **Settings → Pi packages** (npm name, git/URL or a
246
+ local source); the `ln -s dist` route below remains for plugin-only development.
247
+
248
+ ### PI WEB plugin (browser + server entries)
249
+
250
+ Install the package once — **Settings → Pi packages** with source
251
+ `opendoor-plugin` (or this repository's path / git URL) — then enable
252
+ **Opendoor** under **Settings → PI WEB plugins**. Plugin-only local build:
253
+
283
254
  ```sh
284
255
  npm run build
285
256
  ln -s "$(pwd)/dist" ~/.pi-web/plugins/opendoor
286
257
  ```
287
258
 
288
259
  Then **restart the session daemon** (server entries load at sessiond startup
289
- only; the browser bundle hot-reloads on page reload) and reload the PI WEB
290
- page:
260
+ only; the browser bundle hot-reloads on page reload) and reload the page:
291
261
 
292
262
  ```sh
293
263
  systemctl --user restart pi-web-sessiond.service
294
264
  curl -s http://127.0.0.1:8504/pi-web-plugins/manifest.json
295
265
  ```
296
266
 
297
- The manifest should list an `opendoor` plugin entry **with a
298
- `backendRevision`** (signing the paired server module). If `backendRevision`
299
- is missing, the server module failed to load — check the sessiond log; the
300
- plugin still works fully via the terminal fallback.
267
+ The manifest should list an `opendoor` entry **with a `backendRevision`** (signing
268
+ the paired server module); if it is missing, the server module failed to load
269
+ (check the sessiond log) and the plugin works fully via the terminal fallback.
270
+
271
+ ### Review dispatch (Pi extension)
272
+
273
+ Installing the package loads the extension; enabling the PI WEB plugin is a
274
+ separate decision. After installing or updating the package, run `/reload` in
275
+ each idle conversation so `/opendoor-review` is registered there (an ordinary
276
+ Pi-resource change — no provider restart). The extension needs PI WEB's
277
+ session-dispatch bridge (1.202610.x); on an older host it reports "requires PI
278
+ WEB's session dispatch bridge" and starts nothing.
279
+