opendoor-plugin
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.
Package details
Install opendoor-plugin from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:opendoor-plugin- Package
opendoor-plugin- Version
0.5.1- Published
- Nov 9, 2026
- Downloads
- 240/mo · 41/wk
- Author
- kairibu
- License
- MIT
- Types
- extension
- Size
- 428 KB
- Dependencies
- 0 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./dist/pi-extension/index.js"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
opendoor — a Doorstop requirements-management plugin for PI WEB
opendoor is a PI WEB plugin that surfaces
Doorstop requirements stored in the
workspace in a Requirements workspace tab with
the document tree, per-item state (reviewed/unreviewed changes, suspect links),
validation findings and doorstop actions, plus agent prompts
that insert requirement context into the session.
The paired backend also recovers an item file's reviewed version from git for
the Changes since review diff, and Review can opt in to a review commit
(commitAfterReview).
Full feature spec: docs/feature-doorstop-plugin.md
(§5 architecture, §6 requirements state model, §7–§8 UI/actions).
Installation
Install from npm as a Pi package (requires pi-web 1.202611.1 or newer):
pi install npm:opendoor-plugin
or via the UI: Settings → Pi packages → npm:opendoor-plugin.
After installing, restart pi-web-sessiond (server plugins load only on
sessiond restart) and reload the browser tab — the Requirements tab then
appears for any workspace whose root has a .doorstop.yml.
Status
Requires pi-web 1.202611.1 or newer (browser and server plugin API v5).
One package (opendoor-plugin) ships the PI WEB plugin (both entries below)
and the Pi extension (/opendoor-review).
- Browser entry (
src/browser/pi-web-plugin.ts, apiVersion 5, nameOpendoor) delegates tocreateOpendoorBrowserContributions(src/browser/doorstop-contributions.ts): the panelworkspace.doorstop(title Requirements, order 5 — below the Files panel's order 10 so doorstop item links reach itsfileOpenQueryfirst, at the cost of the Requirements tab preceding Files in the tab order; controller registry insrc/browser/doorstop-panel.ts, sections in Using the panel), the actionsview.doorstop(mod+7) andworkspace.refresh-doorstop(mod+shift+d), the labeldoorstop-status(requirement-health counts, feature spec §7.4), and the session-row labeldoorstop.review(reopens the Requirements tab from a row carrying publicopendoormetadata, archived rows included — feature spec, Session metadata and session-list row labels; labelled rows also carry small lifecycle buttons — a pin toggle on every one, archive while the review is no longer busy, restore on archived rows — over the host's optional session-lifecycle surface, so a pre-1.202610.9 host renders the reopen link alone). - Server entry (
src/server-plugin.ts, apiVersion 5, default exportPiWebServerPlugin) contributes a workspace provider for projects whose root has a.doorstop.yml(cheapfs.accessprobe; everything else stays with the bundled Git provider) and serves six operations:doorstop.run(one CLI invocation; a review carrying the opt-incommit: trueflag is followed by a pathspec-limiteddoorstop: review <uid>commit), the read-onlydoorstop.item-baseline(the reviewed version, from git history), anddoorstop.git-status/-stage/-unstage/-commit(see Git actions).src/doorstop-backend.tsvalidates each request, builds argv server-side (never a browser shell string) and runs it viaexecFile(); see its header for timeouts and per-workspace serialization. The entry declares norequiresand nostart(): the retireddoorstop.ask-agentdispatch could not live there (provider-cycle rule) — seesrc/server-plugin.ts. - Pi extension (
src/pi-extension/opendoor-review.ts, bundled todist/pi-extension/index.js, declared bypi.extensions) registers/opendoor-review <UID> [instruction…]: it publishes the publicopendoormetadata ({ schema: 1, op: "review", uids }) and asks PI WEB's per-session dispatch bridge to start an independent conversation in the same project (thespawn_sessionrules, model/thinking inherited), reporting every outcome in the initiating conversation — the pathway Start review session drives.
Dispatch (src/browser/doorstop-panel-element.ts) uses
context.peer.request("doorstop.run", …) (the review-commit flag rides along
when enabled; Changes since review uses doorstop.item-baseline) with the
output in the auto-expanded status bar, or falls back to terminal.runCommand()
with the same shell command when unpaired. Shared types and operation contracts
live in src/doorstop-contract.ts and src/doorstop-backend-contract.ts.
Workspace settings (.pi-web/opendoor.json)
Optional per-workspace configuration, read on every plugin load (missing file →
defaults; malformed or invalid values → warning diagnostics + defaults). pi-web's
plugins.<id>.settings is host-side and reaches only server entries, so this
file plays the role .pi-web/tasks.json plays for the bundled workspace-tasks
plugin; server-entry knobs are in 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-). Shell-quoted at the terminal-command boundary when it contains characters outside the inert\w./-alphabet; on the backend path it is an argv element, never a shell string.discovery.excludedDirectories— additional directory names the.doorstop.ymlwalk never enters (merged with the built-in.git/node_modulesskips, max 16 names).showAdditionalAttribute— attribute NAMES rendered in every item-list row askey: value(values jsonish-quoted, joined with·, in configured order); any extended attribute key works, an attribute an item lacks is skipped, and no unconfigured attribute ever appears. The modeledtextis special-cased: the body renders raw and unquoted on an indented second line below the row, clamped to two lines with an ellipsis (the full body lives in the detail pane), skipped when empty. Default[]leaves today's rows unchanged. Invalid entries are dropped with a warning each, duplicates deduplicated, capped at 16 names; unknown keys simply match nothing.commitAfterReview— opt-in boolean (defaultfalse): on a paired workspace the Review action'sdoorstop review <uid>is followed by a pathspec-limited commit of the item file with the conforming messagedoorstop: review <uid>(gitaddthencommit -m … -- <path>; other staged/unstaged work is untouched). Committing user content is strictly opt-in; the status bar narrates every outcome (commit: <sha>/clean(already committed) /skipped(not a git repository | item file not found | review failed | deadline) /failed—<stderr>); Review is otherwise unchanged.
Host settings (plugins.opendoor.settings)
Optional, server-side only (needs the paired install and a sessiond restart); wrong types fall back to defaults:
{ "doorstopPath": "/opt/venv/bin/doorstop", "gitPath": "git", "timeoutMs": 8000 }
doorstopPath— the Doorstop CLI binary, default"doorstop", resolved on the sessiond host PATH (the daemon environment, not your login shell — set it when the CLI lives in a venv or is off the service PATH).gitPath— the git binary for the review→commit pipeline, thedoorstop.item-baselinefetch and the four git actions. Default"git"on the sessiond host 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 run would be killed by pi-web, not by the plugin). It also bounds every git exec of the review→commit pipeline, thedoorstop.item-baselinefetch and the git actions: one deadline budget per callback.
Using the panel
Five vertical sections, top to bottom: project-actions (project heading — root
path, basename shown, full path on hover — the Items/Findings toggle, Refresh, Run
validation, Publish HTML, and on paired installs the Git strip, Stage all and
Commit), item-list (filters row above rows showing level, UID, excerpt,
showAdditionalAttribute values and state chips), item-details (Markdown item
text, links in/out, references, extended attributes, the Changes diff),
item-action-palette (Review, Clear suspect links, Link, Ask agent — spanning the
panel at chat-prompt-footer height, with the muted "Select an item…" placeholder,
hidden in the Findings view) and status-bar (see below).
- Items / Findings toggle: Findings lists plugin-computed findings and
diagnostics (error > warning > info), and a finding's UID navigates back to the
item. They are informational — Run validation gives the authoritative
doorstopoutput (paired: status bar; unpaired: terminal). - Status bar (sized like the center-panel one): after a paired action it shows
status (ok / failed / killed / error) and duration, and the panel re-scans
automatically. Captured stdout/stderr auto-expands whenever there is something
to show (output, an error, a review-commit line); a run with nothing to display
stays collapsed. Dismiss clears the run and collapses the bar; the next run
with output re-expands it. Killed runs keep partial output (prefer the terminal
for long publishes). With
commitAfterReviewon, the bar adds thecommit: …line from Workspace settings; 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): an item with
unreviewed changes and a stored
reviewedfingerprint gets a collapsed section; expanding it fetches the reviewed item file from git history viadoorstop.item-baseline, stamp-matches it against the fingerprint and renders a line diff plus chips forref/references/link-UID/extended-attribute changes. With no recoverable baseline it says so ("no git history", "could not locate the reviewed version — history may have been rewritten"). Thedoorstop: review <uid>message is the historical anchor feature spec §12's deferred browser-side git parsing could not give; recovery runs server-side. - Item actions (item-action-palette): Review (
doorstop review UID, plus the review commit whencommitAfterReviewis on), Clear suspect links (doorstop clear UID [parents…]), Edit (doorstop edit UID), Link/Unlink via inline UID input (Doorstop UID grammar enforced; shell metacharacters rejected), and on paired installs Stage / Unstage for the item's own file (doorstop.git-stage/doorstop.git-unstage; see Git actions). Each disables independently: Stage without unstaged (Y) changes, Unstage with nothing staged (X), both mid-run. - Git actions (paired only; otherwise every git control is hidden and you
commit manually):
- Git status strip — 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). Refreshes on panel load, after every run (a stage, unstage or commit invalidates the cache) and on click; outside a repository the other git controls reportskipped. - Stage all — stages every Doorstop-managed file (root
.doorstop.yml, each document config, each item file) viadoorstop.git-stage. Idempotent: a second click over an already-staged set reportsclean — nothing to stageinstead of a git error (staged deletions are skipped, not fatal). Disabled during a run or with no documents in the loaded index, and only as fresh as that index — files created after the last Refresh are missed until you refresh and stage again. - Unstage (per item) — a staged item file swaps its row's plus for a
minus and enables Unstage in the palette, both via
doorstop.git-unstage(git reset -q -- :(literal)<path>server-side). A file both staged and further modified (MM) still shows the minus; the palette then offers Stage and Unstage as two independently-disabled buttons. Unstaging restores the index entry but never touches the working tree (a staged addition returns to??, a staged deletion to an unstaged deletion); it is idempotent (clean — nothing to unstage) and excludes unmerged paths (UU,AA,DD,DU,AU,UD,UA), wheregit resetwould silently resolve the conflict. During a run 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 message input plus Commit, served by
doorstop.git-commit. It records the staged index (git commit -m <message>, no pathspec, no add), exactly like the git CLI: unstaged WIP is never swept in, files you staged yourself are included, and the strip'sstagedcount shows what a commit will record. Empty or whitespace-only messages are rejected client-side (git would open an editor and hang the exec); 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>), including a failing pre-commit hook's stderr.
- Git status strip — 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 from
the item's paths and state (feature spec §7.3). Start review session
composes
/opendoor-review <UID>plus the item context there and needs no paired backend: the extension starts an independent review conversation whose row carries afirstUid +nlabel reopening the Requirements tab on that workspace (unselected and archived rows included, no live Pi connection needed), landing on the item detail pane. After the insert the status bar shows a quietokreview dispatch run row. Without the extension 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 requirement state are recomputed in the browser from the YAML
files; everything mutating goes through the visible
doorstopCLI — paired, headlessly via the server entry, or in a workspace terminal when unpaired — plus, opt-in, the narrated 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, dist/server, dist/pi-extension
# + the dist-root manifest (see the build script header)
npm run dev # watch 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 source); the ln -s dist route below remains 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. Plugin-only local build:
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 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 entry with a backendRevision (signing
the paired server module); if it is missing, the server module failed to load
(check the sessiond log) and the plugin works fully via the terminal fallback.
Review dispatch (Pi extension)
Installing the package loads the extension; enabling the PI WEB plugin is a
separate decision. After installing or updating the package, run /reload in
each idle conversation so /opendoor-review is registered there (an ordinary
Pi-resource change — no provider restart). The extension needs PI WEB's
session-dispatch bridge (1.202610.x); on an older host it reports "requires PI
WEB's session dispatch bridge" and starts nothing.