omp packages

@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.

mikemacleod Published 10 days ago MIT ~236 downloads/mo

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.

Did it work for you?

Result

One report per person. A new report replaces your old one.

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. The TWILL_REGISTRY environment variable overrides it (env wins).
  • agentLocation — the consumer location matched against route match blocks.
  • claimTimeoutMs / pollIntervalMs — the claim loop's default timeout and poll cadence.
  • writeGuard — confine the write/edit tools to the attached workspace root (default true). Set false to 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. Read descriptor.route (e.g. {"type": "local", "path": "~/repos/payments-repo"}) and descriptor.status. status.ready: true means twill_workspace_attach can attach it now. ready: false means the workspace is COLD — it is not attachable. The tool prepends a NOTE: directing you to twill_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 / edit whose path escapes 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_detach releases 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 (or twill_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