CI caught a startup race the PTY smoke stages naturally: a TUI /exit typed
while watchPersonalPatches is still opening its watcher disposes the tree,
and the HMR effect registration rejects with INACTIVE_EFFECT, crashing an
app that exited exactly as asked. Return a no-op disposer for that exact
code; every other registration failure still propagates (covered both ways).
ds-review-bot round 1 on the DSH-home integration:
- generated wrappers now inject the services their manifest needs (skills/
tools beside loader), and loadPreparedRepository rejects a wrapper fiber
that settles anything but ACTIVE — a composition missing a required
service fails the repository transaction instead of committing an ACTIVE
row over a silently PENDING child (critical finding)
- the github: source ref segment excludes '#', so 'a#b' refs fail at the
config parser with the promised syntax instead of inside pnpm
- watchPersonalPatches re-reads the include's non-patch options per refresh
instead of a registration-time snapshot
- the TUI smoke's cache-seeded wrapper is produced by the real
prepareDshPlugin (cache LAYOUT stays a deliberate external pin)
- new Loader integration test drives a live repositories update through
entry.update: generation swap, old skills removed, failed candidate
rolled back to the previous generation
ds-review-bot round 1 on the repository-plugin runtime:
- a manifest-declared skill root absent or non-directory in the installed
package now fails the plugin load (skill-local treats a missing root as
legitimately empty, which silently mounted a skill-less plugin)
- includeDefaultRoots: false no longer inherits $DSH_BUNDLED_SKILL_DIR, so
isolated repository providers see only their explicit roots
- prepared wrapper baseUrl schema requires the file: scheme, failing hostile
URLs at the declared validation boundary
- preparedPath reuses format.ts's isOutside; SERVER_NAME_PATTERN is exported
and pinned equal to dsh-mcp-client's, with the restatement justified (the
prepare bin keeps a zod-only module graph); the unexplained `as never`
cast now carries its schemastery rationale
- the import-free wrapper assertion also rejects dynamic import(
- the headless fixture wrapper is regenerated by the real prepareDshPlugin
and a drift test pins fixture == generator output
- prepareDshPlugin JSDoc states the non-atomic publish repair contract
ds-review-bot round 1: boot() now throws `host preparation failed` when
prepare() rejects before any config-tree entry mounts (the plugin-tree label
overstated), and the new hygiene gate verify-vendored-links pins the
linkWorkspacePackages fix — every vendored package name in pnpm-lock.yaml
must resolve to a workspace link with no registry copy alongside.
- inline the collect() identity wrapper now that both streams use the
seam's diagnostic-tail shape
- resolve the packaged rg path lazily at the first call (memoized):
@vscode/ripgrep resolves its platform package at module evaluation, so a
static import turned a missing/corrupt platform package into a Loader
composition failure instead of the documented per-call SEARCH_FAILED
- classify synchronous spawn-creation throws (a NUL in argv, an abort
racing the pre-check, a rejected resolution) into SEARCH_FAILED /
SEARCH_ABORTED instead of leaking raw errors
- correct the stderrMaxBytes contract: the stderr excerpt is embedded in
SEARCH_* error messages, not hidden from the model
- export virtualManifest and pin its three acceptance paths (prefix hit,
pnpm-11 truncated-name content-scan fallback, both miss) with fixture
unit tests
Tests: rg-path.spec.ts (resolution failure + memoized rejection),
tools.spec.ts spawn-creation classification, notices spec virtualManifest.
Merges master (default-workspace-write-ui, installer-adopt-checkout,
remove-scoped-bash, goal-clear-single-flight, frontend-plugin-loader,
install-interface-choice, …).
Conflict resolutions:
- apps/cli/tests/shipped-composition.e2e.ts: keep the fixed glob/grep
roster assertion; master's workspace-write composition now confines
tool-bash, so the escalation pair is pinned present (my earlier
absence pin is superseded) together with master's permission facts.
- even-out-shipped-tool-rosters note: both sides edited it; the merged
text keeps both sets of changes and the pair is re-recorded.
- delete the singleQuote shell-quoting helper and its bash-spawning tests
(no in-repo consumers; no shell layer exists anymore)
- drop spill from both collect streams: the tool never reads a raw spill
path, and a lossy stdout read is a pure SEARCH_RAW_OUTPUT_OVERFLOW error
- prepend --no-config so a host RIPGREP_CONFIG_PATH cannot inject a --pre
preprocessor into the unconfined spawn
- promote graceMs and stderrMaxBytes to validated Config fields (defaults
SEARCH_GRACE_MS / SEARCH_STDERR_MAX_BYTES) instead of inheriting
bash-local's config
- correct the grep tool's JSDoc seam reference (bash -> subprocess)
- drop the dead exit-127/command-not-found classification branch
- jscpd: the executor/tool mirror dsh-bash-local/dsh-tool-bash by design
(Agent Note), so the mirrored regions carry explicit ignore markers with
reasons instead of being flagged as duplication.
- pwsh-local: a self-terminated process reports SIGTERM or SIGKILL on
POSIX (PowerShell's Stop-Process choice), not only SIGTERM.
- gen-tool-catalog.spec: the shipped-tool completeness list gains 'pwsh'.
Windows-native execution foundation: PwshLocalExecutor implements the bash
executor seam over ctx.subprocess (pwsh -NoLogo -NoProfile -NonInteractive
-Command, one argv element, no quoting layer; resolvePwshPath probes
PowerShell 7 / PATH / Windows PowerShell 5.1 as a pure function), and
tool-pwsh is the minimal PowerShell-dialect model-facing tool over ctx.bash
(foreground only, managed DSH_* env, timeout/signal/exit markers, terminal
and generic presenters). Both packages carry full suites (real pwsh,
self-skipping without it) at per-file 100% coverage; vitest's Windows
exclusion narrows from packages/bash/* to the bash-requiring packages so the
pwsh suites run natively on Windows too. The CLI gains the workspace deps
and tsconfig projects without mounting either plugin; the Windows-default
roadmap is recorded as a proposed Agent Note.
Scope decision: previews for a browser that is not on the Host machine are
not supported. With that settled, host.openPath answers the supported case
completely — a file:// document in a real browser has full page capabilities
and no reach into /api — and the HTTP serving this branch had built answered
only the unsupported one.
Removed: the /f route and its listener, the workspace-file URL shape,
ApiProxy.workspaceRootOf, ConnectionHandle.fileUrl, and the port published
into the index page.
Kept, and finished:
- the produced-files row a turn ends with, derived from mutation locations;
- the path link now reads as a link at rest, not only on hover — the reported
"I can't open what it made" was this, sitting on a working capability;
- the Host opener prefers the default BROWSER for .html/.htm/.xhtml/.svg, so
a developer who binds .html to an editor still gets a rendered page
(macOS via the LaunchServices https handler, Linux via $BROWSER, every
failure falling back to the default application).
The retired designs and their measurements stay in the Agent Note, including
why same-origin serving was unsafe and why the sandbox that fixed it broke
the pages invisibly.
glob/grep now run the @vscode/ripgrep binary via ctx.subprocess with a plain
argv vector: no system rg install, no shell layer, unconditional registration.
The load-time command -v rg probe and the bash-seam coupling are removed;
timeouts ride the cooperative exec.signal plus the seam's terminate
escalation. The fs-glob-sampling ACP snapshot executes the real packaged
binary against an mtime-pinned fixture. Adds the packaged-ripgrep-search
Agent Note, updates the roster-note facts and both shipped-composition e2es,
and regenerates the doc catalogs and third-party notices (surfacing
pre-existing manifest drift plus the new @vscode/ripgrep row; the notices
generator also learns pnpm 11's truncated virtual-store names).
A sandbox header bought isolation by taking the document's origin away, and
measuring that cost decided against it: the reported artifact throws
SecurityError on load, and because an uncaught exception aborts the rest of
its <script>, every listener declared after that line — theme toggle, mobile
menu, model tabs — never binds. Two of the four artifacts in the reporting
user's workspace were dead pages under it, and they still looked right.
A second listener on the API's host, answering /f and nothing else, is the
same boundary without the amputation: cross-origin to /api (refused by the
Origin fence and by CORS), same-origin with itself (localStorage, cookies and
fetch all work). Its port is published into the index page; the browser half
reads it to address previews, and its absence — the keyless fixture lane — is
what makes a file row fall back to the Host opener instead of a dead tab.
fileUrl moves from IWorkspaces to ConnectionHandle: the transport owns both
the listener that serves the bytes and the port that addresses it.
Isolation is restored on the premise the review corrected: a workspace file
need not be agent-authored — a read row makes every file in a cloned
repository openable — and a same-origin active document was measured driving
/api/settings.describe to a 200 with full data. Script-capable documents go
back into an opaque origin; the preview's lost localStorage is the known cost,
and a separate serving origin is the way to retire it.
- confine(): a workspace rooted at a filesystem root has a realpath already
ending in the separator, and the doubled prefix 403'd every child.
- turnDeliverables(): reset on the turn boundary, not only at a closing
assistant, so an interrupted turn cannot spill into the next turn's row;
and recognize a mutation by render intent (diff card, or generic with
kind 'edit') so str_replace_editor's insert counts.
- 405 answers name the methods it allows.
- The e2e now cold-seeds a recorded WRITE turn, so the assembled application
covers the Produced row, its chip's served URL, and the isolation header.
- Agent Note matched to what shipped (the row is in this PR, not deferred);
ui-conversation README documents the new destination and the row; the
fixture lane's dead-tab quirk and the cold-path listing cost are recorded.
A preview lost localStorage and cookies under CSP sandbox — measurably, the
reported artifact throws SecurityError on load and its theme toggle goes
dead. The capability the sandbox denied is one the file's author, an agent
already holding this user's shell, never needed the browser for, so the
header sat behind a trust boundary it had already crossed.
Isolating a preview becomes a real question when workspace content stops
being the viewer's own; the answer then is a separate origin, not a header.
The paths come from the mutation tools' follow-along locations, not from the
closing prose, so a turn's output is listed whether or not the model named
it. Each chip opens through the same openFile the tool rows use.
Reads contribute nothing (looking at a file does not produce it), a failed
mutation contributes nothing, a file touched twice is one entry, and the row
shows six with an explicit remainder rather than burying the answer.
Serve one file at a time out of a Session's workspace under /f on the web
transport, and point the conversation's existing file-open affordance at it.
Clicking a write/edit/read row's path now opens that file in a browser tab —
including from a LAN client, where the Host's system opener is fenced to
loopback and answered nothing.
- /f/<sessionId>/<segments> in client-connection, behind the same
browser-trust fence as /api; realpath confinement, streamed reads,
GET/HEAD only, nosniff + no-store.
- Script-capable documents carry CSP sandbox: model-authored markup must not
be same-origin with /api, where events.mux is a readable GET stream.
- ApiProxy.workspaceRootOf answers where a Session's files live without
resuming an agent; the client program cannot reach the core services.
- The /f URL shape lives in dsh-host-apiproxy/api so both ends share one
encoding (client bundles may not value-import another plugin).