@bitwise-operations/twill-pi-workspace0.1.0
Workspace kind adapter for the twill fabric pi substrate: resolve/attach/claim/detach tools and the path-scoped write guard.
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
twill-pi-workspace
The workspace kind adapter for the twill fabric's pi substrate — a pi extension that resolves workspaces through a twill registry, attaches them as the session's active workspace, confines the write/edit tools to the workspace root with a path-scoped guard (configurable via writeGuard), and claims cold workspaces through the claim-and-poll loop. It is a pure HTTP consumer of the registry's v1alpha1 API (the same contract the twill CLI exercises); it imports nothing from the twill repo.
It is built on the twill-pi substrate package (config, registry client, consumer identity, resolve wire types, per-kind session-state container) and registers the workspace tools under pi's twill tool namespace. twill-pi on its own provides only the /twill status command — this package is the one you want for actual workspaces.
Design contracts (in the twill repo): the fabric core spec (docs/superpowers/specs/2026-10-04-twill-fabric-core.md in the twill repo) (resolve protocol, consumer tuple, claims) and the workspace spec (docs/superpowers/specs/2026-10-04-twill-workspace.md, same repo) (cold/ready lifecycle, RWO advisory, claim flow).
Install
pi loads extensions from ~/.pi/agent/extensions/. Both packages are needed (the substrate and this adapter):
pi install <twill-pi repo or package> # substrate (registers /twill)
pi install <this repo or package> # this adapter
For local development:
git clone <twill-pi repo> ~/.pi/agent/extensions/twill-pi
git clone <this repo> ~/.pi/agent/extensions/twill-pi-workspace
cd ~/.pi/agent/extensions/twill-pi-workspace && npm install
The npm install step resolves this package's twill-pi dependency, which for now is a local file: path (the publish shape comes later) — point it at your local twill-pi checkout if it differs. No build step — pi loads index.ts at runtime via jiti. Restart pi after installing.
Config
Same file the substrate reads: ~/.pi/agent/twill-pi.json (optional; a missing file means defaults — no error):
{
"registry": "http://localhost:8080/twill/v1alpha1",
"agentLocation": "workstation",
"claimTimeoutMs": 600000,
"pollIntervalMs": 7000,
"writeGuard": true
}
registry— the registry's v1alpha1 root. TheTWILL_REGISTRYenvironment variable overrides it (env wins).agentLocation— the consumer location matched against routematchblocks.claimTimeoutMs/pollIntervalMs— the claim loop's default timeout and poll cadence.writeGuard— confine the write/edit tools to the attached workspace root (defaulttrue). Setfalseto disable the write guard entirely. See The write guard.
Malformed JSON fails loud at extension load, not mid-tool-call.
The tools
All three register under pi's twill tool namespace (one grouped heading in tool listings).
twill_workspace_resolve — look before you attach
{ "kind": "Workspace", "name": "payments-repo" }
Returns the registry's ResolveResult verbatim as JSON. Three shapes:
Attached— a route was selected. Readdescriptor.route(e.g.{"type": "local", "path": "~/repos/payments-repo"}) anddescriptor.status.status.ready: truemeanstwill_workspace_attachcan attach it now.ready: falsemeans the workspace is COLD — it is not attachable. The tool prepends aNOTE:directing you totwill_workspace_claim.NoMatch— no object by that name, or no route matches this consumer.Unreachable— matching routes exist but none is selectable right now; per-route errors are listed.
twill_workspace_attach — make a workspace the session's active workspace
{ "kind": "Workspace", "name": "payments-repo" }
Resolves fresh every call. On a ready workspace it binds the route as the session state and answers with the expanded root, e.g. root: /home/you/repos/payments-repo. From then on use root-relative paths; pi's cwd does not change. Cold workspaces are refused (call twill_workspace_claim first). Attaching a second workspace replaces the first — the result notes the swap. If other consumers hold claims on the workspace, the attach still succeeds but carries a WARNING: naming them (the workspace spec's RWO advisory — coordinate before editing).
twill_workspace_claim — wake a cold workspace
{ "name": "payments-repo", "branch": "feat-x", "worktreeName": "feat-x" }
The cold-workspace flow (workspace spec §6), all inside the tool: resolve the parent, POST a Claim bound to the resolved route, then poll the materialized workspace until status.ready: true — then attach it automatically and answer like twill_workspace_attach. With worktreeName the materialized child is <name>-<worktreeName>; without it the parent itself is materialized.
On timeout or abort the claim is left in place (visible to operators; the provider may still finish) — the result names the claim and the release command, e.g.:
To release it explicitly: twill claim release Workspace payments-repo claim-payments-repo-pi-…-1759723200
Refuses when the workspace is already ready (use twill_workspace_attach) or not provisionable.
twill_workspace_detach — release the attachment
{}
Takes no parameters. Clears the session's attached workspace, releasing the write-guard confinement immediately. Idempotent — with nothing attached it reports so (not an error). Use it to switch away from a workspace without ending the session.
The write guard
Once a workspace is attached, a tool_call handler confines the write/edit tools to the workspace root.
What it blocks (only when the attached route is local):
write/editwhosepathescapes the root — including the classic sibling-repo mistake (~/repos/other-repo/…while attached to~/repos/payments-repo) and../escapes.
Block reasons name the tool, the root, and the offending resolved path. In-root paths pass; ~ is expanded and ./.. folded before containment is judged. The check is an exact prefix test on the tool's explicit path argument — no command parsing, so no false positives.
Bash and reads are never guarded. The guard does not touch bash: a shell command's write surface is unbounded (infinite commands and parameter shapes, substitutions, subshells), so classifying read-vs-write there is unwinnable — and pretending otherwise would be a false sense of security. Read-only and custom tools pass unconditionally.
This is a guardrail, NOT a sandbox or security boundary. It catches the common "agent wrote a stray file outside the workspace" accident via the dedicated file-writing tools, and nothing more. It can be bypassed (bash, symlinks). Real isolation comes from running the work in a sandboxed/remote workspace, not from this check. Non-local routes (ssh, k8s-exec) are advisory-only: a local-process guard cannot see a remote filesystem, so everything is allowed (the attach summary still tells the model the root).
Configurable. Set "writeGuard": false in ~/.pi/agent/twill-pi.json to disable the guard entirely (the tool_call handler is then not registered at all). Default: true (write/edit confined). Inert when no workspace is attached — an unattached session is never blocked.
Session lifecycle
Attaching is session state, nothing more: no daemon, no subprocess, no working-directory change. The attached workspace lives in this package's own factory-closure state (pi loads installed packages with separate module roots, so the state is deliberately NOT a module singleton shared with the substrate).
twill_workspace_detachreleases the attachment mid-session (the guard goes inert immediately). Idempotent — detaching with nothing attached just reports that.- Session shutdown (quit, or replacement via new/resume/fork): the extension detaches — the next session starts unattached and must
twill_workspace_attach(ortwill_workspace_claim) again. A fresh session never silently inherits a workspace. - pi
/reload: the extension factory re-runs, so the fresh closure likewise starts unattached — the model must re-attach. Re-deriving the attachment from session entries (so a reload could restore it) is a planned later enhancement, explicitly out of scope for now.
Develop
npm install
npm test # vitest, run outside pi against a canned registry
npx tsc --noEmit # typecheck