opendoor-plugin0.4.0
Opendoor — a pi-web browser plugin that surfaces Doorstop requirements stored in the workspace: the document tree, per-item state (reviewed/unreviewed changes, suspect links), validation findings, and one-click doorstop CLI actions.
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
opendoor — a Doorstop requirements-management plugin for PI WEB
opendoor is a paired PI WEB plugin that surfaces
Doorstop requirements stored in
the workspace next to the code they trace to: a Requirements workspace tab
with the document tree, per-item state (reviewed/unreviewed changes, suspect
links), validation findings, and one-click doorstop CLI actions (validate,
publish, add/link/unlink, clear suspect links, review), plus agent prompts
that insert requirement context into the session. When paired, the plugin's
server entry runs the CLI headlessly and the captured output is displayed in
the panel itself (a status bar at the bottom); when unpaired, every action
falls back to the workspace terminal. The paired backend also recovers the
reviewed version of an item file from git history, so the item detail pane
can show a Changes since review diff — and Review can optionally record
a pathspec-limited review commit (commitAfterReview).
Full feature spec: docs/feature-doorstop-plugin.md
(§5 architecture, §6 requirements state model, §7–§8 UI/actions).
Status
Requires pi-web 1.202609.1 or newer (browser plugin API v4 / server
plugin API v3). Tested through pi-web 1.202610.8, which remains
additive for this plugin — its browser v4/server v3 entries still load,
with once-per-plugin deprecation diagnostics expected (see Rollout below
for the upgrade order and the 1.202610.x sessiond-restart note). The Phase 3 pairing
on top of that migration (public session metadata + session-list row
labels, plan §2.1) stays on the same v4/v3 contracts. The package is one npm
package (opendoor-plugin) that ships a PI WEB plugin AND a Pi extension:
the plugin's browser/server halves below, plus the review-dispatch extension
that starts labeled review conversations (/opendoor-review, see Review
dispatch (Pi extension) below). The plugin loads on older hosts unchanged;
only the extension's dispatch bridge, the public metadata, and the row
labels need a 1.202610.x session daemon (see Rollout). The plugin is
fully wired and green:
Browser entry (
src/browser/pi-web-plugin.ts, apiVersion 4, nameOpendoor) delegates tocreateOpendoorBrowserContributionsinsrc/browser/doorstop-contributions.ts, which contributes:- panel
workspace.doorstop(title Requirements, order 5 — deliberately below the bundled Files panel's order 10, which answers every chat file link unconditionally in the host's first-accepting-panel dispatch; this is what makes thefileOpenQueryclaim — chat links to doorstop item files open the item's detail view in this panel instead of Files — reachable, at the cost of the Requirements tab preceding Files) — the Lit body element<pi-web-opendoor-panel-body>, driven by the per-workspace controller registry (src/browser/doorstop-panel.ts): five vertical sections —project-actions(title, Items/Findings toggle, Refresh / Run validation / Publish HTML),item-list(filters row + item rows with state chips),item-details(links in/out, references, extended attributes, collapsible Changes since review diff),item-action-palette(Review / Clear suspect links / Edit / Link / Unlink / Ask-agent menu, sized to the host prompt footer), andstatus-bar(status badge, duration, Dismiss button, and the auto-expanding stdout/stderr of the latest CLI run, plus the review-commit outcome line when the review requested a commit); - actions
view.doorstop(mod+7) andworkspace.refresh-doorstop(mod+shift+d); - label
doorstop-status— the informational requirement-health counts (async-cache idiom, feature spec §7.4); - session-row label
doorstop.review— a compact indicator on session rows carrying publicopendoormetadata; clicking it reopens the Requirements tab on the row's machine/workspace (works for unselected and archived rows, no live Pi connection needed). See Session metadata and session-list row labels below.
- panel
Server entry (
src/server-plugin.ts, apiVersion 3, default exportPiWebServerPlugin) contributes a workspace provider that claims only projects whose root contains a.doorstop.yml(cheapfs.accessprobe — everything else stays with the bundled Git provider) and serves a six-operation backend:doorstop.runruns one Doorstop CLI invocation — a review request carrying the opt-incommit: trueflag is followed by a pathspec-limited git commit of the item file (doorstop: review <uid>) — the read-onlydoorstop.item-baselinerecovers the reviewed version of an item file from git history, and the four git actionsdoorstop.git-status,doorstop.git-stage,doorstop.git-unstage, anddoorstop.git-commitpower the panel's git strip and stage/unstage/commit controls (see Git actions below).src/doorstop-backend.tsvalidates each request, builds argv server-side (the browser never sends shell strings), and runs through the host'sexecFile()helper withcwd= the workspace path, timeouts clamped to 8.5 s (the review+git pipeline shares one deadline budget under the host's 10 s callback bound), and per-workspace run serialization. The server entry permanently declares norequiresand nostart(): the capability-drivendoorstop.ask-agentdispatch it once carried was unreachable by construction on a workspace-provider-contributing entry (PI WEB rejects the provider-dependency capability cycle) and has been retired. The review dispatch now runs in the Pi extension below.Pi extension (
src/pi-extension/opendoor-review.ts, bundled todist/pi-extension/index.jsand declared bypi.extensions) registers/opendoor-review <UID> [instruction…]. It publishes the shipped publicopendoorsession metadata ({ schema: 1, op: "review", uids }) and asks PI WEB's own per-session dispatch bridge to start an independent conversation in the same registered project — the same spawn semantics and delegation restrictions asspawn_session, with model/thinking inherited from the initiating conversation. Every outcome (started / refused / no bridge) is reported as a message in the initiating conversation. This is the pathway the panel's Start review session entry drives (see Review dispatch (Pi extension) below).
Dispatch (per action, in src/browser/doorstop-panel-element.ts): when the workspace
is opendoor-owned with an active backend, the action sends a structured
request via context.peer.request("doorstop.run", …) (the review-commit
flag rides along when the setting is enabled; the "Changes since review"
section fetches via context.peer.request("doorstop.item-baseline", …))
and the captured output appears in the panel's status bar (auto-expanded);
the panel auto-rescans on completion. When the backend is absent (unpaired
install, or a workspace the provider does not own), the action falls back to
terminal.runCommand() with the same shell command, so output stays visible
and auditable in the terminal.
The shared types + function-signature contracts live in
src/doorstop-contract.ts; the two backend operation contracts
(doorstop.run and doorstop.item-baseline) shared by both halves, plus
the four git-action contracts (doorstop.git-status, doorstop.git-stage,
doorstop.git-unstage, doorstop.git-commit), live in
src/doorstop-backend-contract.ts. Later modules import these and must not
redefine them.
Workspace settings (.pi-web/opendoor.json)
Optional per-workspace configuration, read by the plugin on every load
(missing file → defaults; malformed or invalid values → warning diagnostics
in the panel + defaults). Workspace-scoped settings cannot use pi-web's
plugins.<id>.settings config (that is host-side and only reaches server
entries), so this file plays the same role .pi-web/tasks.json plays for the
bundled workspace-tasks plugin. The server entry's host-scoped knobs
(CLI binary path, exec timeout) are configured through
plugins.opendoor.settings instead — see Host settings below:
{
"version": 1,
"publish": { "target": "./public" },
"discovery": { "excludedDirectories": ["published", "vendor"] },
"showAdditionalAttribute": ["text", "component"],
"commitAfterReview": false
}
publish.target— path passed todoorstop publish all. Must be a safe workspace-relative path (no.., no absolute, no drive prefix, no leading-). It is shell-quoted at the terminal-command boundary when it contains characters outside the inert\w./-alphabet; on the backend path the target is passed as an argv element, never through a shell.discovery.excludedDirectories— additional directory names the.doorstop.ymlwalk never enters (merged with the built-in.git/node_modulesskips, max 16 names).showAdditionalAttribute— array of attribute NAMES rendered in every item-list row askey: value(values jsonish-quoted, joined with·, in the configured order). Entries may be any extended attribute key; an attribute an item does not have is skipped for that row, and no unconfigured attribute ever appears. The modeledtextis special-cased: instead of atext: "…"pair in the attribute column, the item body renders raw and unquoted on its own indented second line below the row, clamped to two lines with an ellipsis (the full body lives in the detail pane); it is skipped when the item body is empty. Default[]leaves today's rows unchanged. Invalid entries are dropped with a warning each, duplicates are deduplicated, the list is capped at 16 names, and unknown keys are accepted (they simply match nothing).commitAfterReview— opt-in boolean (defaultfalse): when enabled on a paired workspace, the Review action'sdoorstop review <uid>is followed by a pathspec-limited git commit of the item file with the conforming messagedoorstop: review <uid>(gitaddthencommit -m … -- <path>— other staged/unstaged work is left untouched). Committing user content is strictly opt-in, and the status bar narrates every commit outcome (commit: <sha>/clean(already committed) /skipped(not a git repository | item file not found | review failed | deadline) /failed—<stderr>). Under the default, Review behaves exactly as before.
Host settings (plugins.opendoor.settings)
Optional, server-side only (requires the paired install and a sessiond restart to take effect). Parsed leniently; wrong types fall back to defaults:
{ "doorstopPath": "/opt/venv/bin/doorstop", "gitPath": "git", "timeoutMs": 8000 }
doorstopPath— the Doorstop CLI binary. Defaults to"doorstop"resolved on the sessiond host PATH (the daemon environment, not your login shell — set this when the CLI lives in a venv or is not on the service PATH).gitPath— the git binary used by the review→commit pipeline, thedoorstop.item-baselinefetch, and the four git actions (status strip, Stage all, per-item Unstage, Git commit). Defaults to"git"resolved on the sessiond host PATH — set it when git is not on the service PATH (the same escape hatch asdoorstopPath).timeoutMs— per-run exec timeout in ms. May SHORTEN the 8 500 ms default but never exceed it (the host bounds every provider callback at 10 s, so a longer doorstop run would be killed by pi-web, not by the plugin). It also bounds every git exec of the review→commit pipeline, the baseline fetch, and the git actions, which share one deadline budget per callback.
Using the panel
The right-hand panel body is divided into five vertical sections, top to bottom:
project-actions— the project heading (the workspace's root path, basename shown, full path on hover), the Items / Findings toggle, the project-scoped buttons Refresh, Run validation, and Publish HTML, and — on paired installs — the Git status strip, the Stage all button, and the Git commit control (message input + button); see Git actions below.item-list— a filters row (document chips, state filter, search) above the item rows; each row shows its level, UID, excerpt, any attributes configured throughshowAdditionalAttribute, and its state chips.item-details— the selected item's full detail pane (unchanged).item-action-palette— the item actions (Review, Clear suspect links, Link + target UID input, Ask agent menu), moved out of the detail pane so the row spans the panel at the same height as the center-panel chat prompt footer. A muted "Select an item…" placeholder keeps the palette at constant height while nothing is selected; the palette is hidden in the Findings view.status-bar— the run status row (badge, duration, Dismiss), sized like the center-panel status bar, plus the captured output.
- Items / Findings toggle in the project-actions row. Findings lists all
plugin-computed findings and diagnostics (error > warning > info); a
finding's UID navigates back to the item. Findings are informational — run
the Run validation button for the authoritative
doorstopoutput (paired: rendered in the panel's status bar; unpaired: in the workspace terminal). - Status bar: after a paired action completes, the bottom bar shows the
run's status (ok / failed / killed / error) and duration, and the panel
re-scans automatically. The captured stdout/stderr auto-expands whenever
a run produces a message to show (any output, an error message, or a
review-commit line) — no interaction needed; a run with nothing to display
stays collapsed to its status row. Dismiss clears the run and collapses
the bar; the next run with output re-expands it. Runs killed at the exec
timeout preserve and show partial output; prefer the workspace terminal for
long publishes. When a review ran with
commitAfterReviewenabled, the bar adds acommit: …narration line (commit: <short-sha>/clean (already committed)/skipped (not a git repository | item file not found | review failed | deadline)/failed — <stderr excerpt>); a failed or skipped commit never changes the run's ok/failed badge. - State chips per item (feature spec §6): inactive, non-normative, reviewed / unreviewed changes, suspect links, no child links, no links, unknown link, missing reference.
- Changes since review (item detail pane, paired installs only): for an
item with unreviewed changes and a stored
reviewedfingerprint, a collapsed section shows what changed since the last review. Expanding it lazily fetches the reviewed version of the item file from git history via thedoorstop.item-baselinebackend operation, stamp-matches it against the stored fingerprint, and renders a text line diff plus chips forref/references/link-UID/extended-attribute changes. When no baseline can be recovered it says so honestly ("no git history", "could not locate the reviewed version — history may have been rewritten"). Thedoorstop: review <uid>commit message is the historical anchor that the deferred browser-side git parsing of feature spec §12 could not give — the recovery runs server-side, where git lives. - Item actions (item-action-palette): Review (
doorstop review UID— withcommitAfterReviewenabled and paired, followed by the pathspec-limited review commit), Clear suspect links (doorstop clear UID [parents…]), Edit (doorstop edit UID), Link/Unlink via inline UID input (validated against the Doorstop UID grammar; shell metacharacters rejected), and — on paired installs — Stage / Unstage for the selected item's own file (doorstop.git-stage/doorstop.git-unstagewith just that item's path; see Git actions below). Each is independently disabled: Stage when the file has no unstaged (Y) changes, Unstage when it has nothing staged (X), and both while a run is in flight. - Git actions (paired installs only; every git control is hidden
without the paired backend — terminal-fallback users commit manually as
today):
- Git status strip — a compact readout above the item list:
⎇ <branch> · <n> staged · <n> dirty · ↑<ahead> ↓<behind>(zero/absent parts omitted;no gitoutside a repository; a retry link on fetch errors). The strip refreshes on panel load, after every run (a stage, unstage, or commit invalidates the cache), and whenever you click it. Outside a git repository the strip showsno gitand the stage/unstage/ commit controls are no-ops that reportskipped. - Stage all — stages every Doorstop-managed file (the root
.doorstop.yml, each document's config file, each item file) via thedoorstop.git-stagebackend operation. Staging is idempotent: a second click over an already-staged set reportsclean — nothing to stageinstead of a git error (staged deletions in particular are skipped rather than fatal). Disabled while a run is in progress or when the loaded index has no documents. Note: staging is only as fresh as the loaded index — files created on disk after the last Refresh are missed until you refresh and stage again. - Unstage (per item) — while an item's file has staged (index)
changes, its list row shows a minus in place of the usual plus, and
the item palette enables an Unstage button, both served by the
doorstop.git-unstageoperation (git reset -q -- :(literal)<path>server-side). The row's pointer shortcut prefers the minus when a file is both staged and further modified (MM), so "I staged by mistake" always surfaces a minus; the palette shows Stage and Unstage as two independently-disabled buttons in that case. Unstaging restores the index entry but never touches the working tree: a staged addition returns to??untracked, a staged deletion returns to an unstaged deletion. It is idempotent — a second Unstage over an already-clean path reportsclean — nothing to unstage. Unmerged paths (UU,AA,DD,DU,AU,UD,UA) are excluded — agit reseton an unmerged path would silently resolve the conflict in the index. On unpaired installs no git control is rendered at all; while a run is in flight no minus appears and the palette buttons are disabled (the per-row affordance is also suppressed when the status file list is truncated, so stale path data is never acted on). - Git commit — a single-line message input plus a Commit button,
served by the
doorstop.git-commitoperation. The commit records the staged index (git commit -m <message>with no pathspec and no add) — exactly like the git CLI. Consequences, all intended: unstaged WIP is never swept into a commit; files you staged yourself (via the git CLI or any other tool) are committed too; the strip'sstagedcount tells you what a commit will record before you click. Empty or whitespace-only messages are rejected client-side (git with an empty-mwould open an editor and hang the exec). Committing with nothing staged reportsclean — nothing staged. Outcomes are narrated in the status bar (staged <n> paths/committed <sha>/clean — nothing to stage/clean — nothing staged/skipped/failed — <stderr excerpt>); a failing pre-commit hook surfaces its stderr in the expanded status bar body.
- Git status strip — a compact readout above the item list:
- Ask the agent menu: inserts ready-made prompts (explain, fix suspect
links, draft child requirement, review readiness) into the session prompt
editor using the item's paths and state (feature spec §7.3). Start
review session composes
/opendoor-review <UID>plus the item context into that same editor and needs no paired backend: sending it lets the opendoor Pi extension start an independent review conversation whose list row carries afirstUid +nlabel that reopens the Requirements tab on the row's workspace — including for unselected and archived rows and with no live Pi connection — landing on the item detail pane. After the insert the panel's status bar shows a quietokreview dispatch run row. Without the extension installed the inserted text is still a self-sufficient review instruction, so the review happens in the current conversation; a refused dispatch is reported in the chat by the extension. See Session metadata and session-list row labels in the feature spec. - Fingerprints and the requirement state are recomputed in the browser from
the YAML files; everything mutating goes through the visible
doorstopCLI — paired, headlessly via the server entry (output captured in the panel), or in a workspace terminal when unpaired — plus, opt-in, the narrated pathspec-limited review commit (commitAfterReview) — so all effects stay auditable in git.
Commands
npm install # install dependencies
npm test # vitest (unit tests under src/**/*.test.ts)
npm run test:coverage # v8 coverage report
npm run typecheck # tsc --noEmit (strict)
npm run build # esbuild: dist/browser/pi-web-plugin.js + dist/server-plugin.js
# + dist/pi-extension/index.js, then the dist-root
# package.json plugin manifest (see the build script header)
npm run dev # watch mode build
Installing the plugin locally (id opendoor)
The package carries two independently enabled halves: the PI WEB plugin
(browser + server entries) and the Pi extension (/opendoor-review).
The repository-root package.json is the npm-package-root manifest, so both
halves come from ONE install through Settings → Pi packages (npm name,
git/URL, or a local package source). The ln -s dist route below remains
available for plugin-only development.
PI WEB plugin (browser + server entries)
Install the package once — Settings → Pi packages with source
opendoor-plugin (or this repository's path / git URL) — then enable
Opendoor under Settings → PI WEB plugins. For a plugin-only local
build the symlink route still works:
npm run build
ln -s "$(pwd)/dist" ~/.pi-web/plugins/opendoor
Then restart the session daemon (server entries load at sessiond startup only; the browser bundle hot-reloads on page reload) and reload the PI WEB page:
systemctl --user restart pi-web-sessiond.service
curl -s http://127.0.0.1:8504/pi-web-plugins/manifest.json
The manifest should list an opendoor plugin entry with a
backendRevision (signing the paired server module). If backendRevision
is missing, the server module failed to load — check the sessiond log; the
plugin still works fully via the terminal fallback.
Review dispatch (Pi extension)
Installing the package is what loads the extension; enabling the PI WEB
plugin is a separate decision. After installing or updating the package on a
machine, run /reload in each idle conversation so the /opendoor-review
command is registered there (an ordinary Pi-resource change — no provider
restart involved). The extension needs PI WEB 1.202610.x for the
session-dispatch bridge; on an older host it reports "requires PI WEB's
session dispatch bridge" in the conversation and starts nothing. The panel's
Start review session entry needs no paired backend — it only composes the
command into the prompt editor.
Rollout (pi-web 1.202609.1)
pi-web 1.202609.1 is a hard break for plugins — there is no
compatibility shim in either direction: a v4/v3 host rejects v2/v1 plugin
modules, and a v2/v1 host rejects the migrated v4/v3 modules. Upgrade the
opendoor package and pi-web together, then restart
pi-web-sessiond.service and reload the PI WEB page. During the transition
the opendoor panel is entirely absent (both plugin entries rejected), not
degraded. Federated gateways/targets must be upgraded in lockstep.
pi-web 1.202610.3 (and later 1.202610.x)
Unlike 1.202609.1, release 1.202610.3 is additive for opendoor: the
untouched v4/v3 plugin keeps loading on a v1.202610.3 host, with a
once-per-plugin deprecation diagnostic (no removal date). No source changes
were needed — the only mindful adaptation was a no-op navigate stub in the
test fixtures (src/test-fixtures.ts, src/browser/pi-web-plugin.test.ts),
because 1.202610.x made navigate a required member of
PluginRuntimeContext / WorkspacePanelContext.
Host upgrade order for 1.202610.x: refresh the pi-web checkout, then
manually restart pi-web-sessiond.service — a UI/API autoreload alone
is not sufficient (the release touched the session-serving paths
heavily). The restart interrupts active sessions and terminals; reload the
PI WEB page afterwards and confirm the manifest shows the opendoor entry
with a backendRevision.
Review dispatch and session-row labels (1.202610.x floor)
The Start review session dispatch and the session-list row labels require
a 1.202610.x session daemon: that release introduced the public session
metadata, the session-list label contribution, and the per-session dispatch
bridge the extension emits on. The opendoor server entry declares no
capabilities and loads across the whole supported range. opendoor-plugin
0.4.0 is the first release carrying the extension. Restart
pi-web-sessiond.service once to load the server-side change, then run
/reload in each idle conversation to load the extension's
/opendoor-review command; the browser side only needs a page reload.