Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .agents/skills/verify-open-pstack/features/worker-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The repository owner keeps Git authority while subordinate workers operate withi
- `worker-handoff`: a final response can request a parent operation without completing the assignment or authorizing that operation.
- `worker-continuation`: an explicit saved allowance permits only a bounded, inspected continuation of the same descriptor.
- `worker-operation`: duplicate or interrupted parent operations reconcile against recorded expected state before replay.
- `worker-local-checks`: command-capable writers check, repair, and recheck locally; file-only writers request parent checks.

## How to get to it (user POV)

Expand Down Expand Up @@ -48,6 +49,24 @@ From each installed parent, exercise these outcomes and retain the parent tool t

Record unsupported or blocked provider paths individually. A successful CLI test does not validate either installed parent. A provider report rejected by identity or spending guards is an unsuccessful run even when a side effect looks correct.

For local checks, create a separate fixture for each installed-parent lane. The helper performs Git operations and runs only in the parent.

```bash
capture local-check-fixture python3 "$VERIFY_REPO/tests/worker-contract/local-check.py" create --root "$VERIFY_SCRATCH/local-check"
```

Pass `prompt.txt` and the fixture's `writer` checkout through the installed workflow. The task requires a failing baseline, a change only to `normalize.py`, and a passing check. Use the saved descriptor and spending policy. Exercise native Codex and Claude assignments, external Claude's sandboxed Bash, and Devin's sandboxed exec where those routes are configured. Repeat with fresh paths for each lane.

Retain native host tool events or bounded provider tool observations from transparent process-boundary instrumentation. Record the actual check call and its matching failure or success result. Instrumentation must preserve argv, stdin, stdout, and exit status. Devin's runner deletes its private ATIF export after the attempt, so a verification recorder must select the check observations before cleanup. Never retain system context, reasoning, credentials, or unrelated tool output. Runner receipts and worker-written check logs alone do not prove that the worker ran a check.

```bash
capture local-check-observed python3 "$VERIFY_REPO/tests/worker-contract/local-check.py" inspect --root "$VERIFY_SCRATCH/local-check"
```

Require a passing parent check, an edited implementation, unchanged tracked tests and worker HEAD, and an unchanged seed. Inspect the actual diff and extra files. These state checks detect changes; they do not prove confinement. Record the installed version and tree identity, parent action, worker tool observations, and independent parent result separately.

Use fresh fixtures for negative paths. Put project sandbox exclusions in a Claude writer checkout and confirm empty setting sources keep an outside-worktree canary write denied. Observe one actual denial and no retry. Exercise sandbox startup failure and an old or malformed CLI version without model execution. Check a required outside-directory cache or network operation, and retain the concrete blocker instead of weakening isolation. A file-only lane must deliver a valid `run-checks` request without claiming command execution; validate it with the parent's fixed task, checkpoint, file, check identifier, and HEAD before running acceptance. Native and external strict preparation must remain unsupported. Label fake CLI and synthetic policy cases as boundary tests, never installed-parent proof.

## Gotchas

- File-only workers cannot promise shell checks. The parent may run authorized checks and Git operations itself.
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"name": "pstack",
"source": "./plugins/pstack",
"description": "if you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence.",
"version": "1.10.1",
"version": "1.10.2",
"author": {
"name": "Lauren Tan (original)"
},
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ This file records what each version of the Open Pstack package changed. Versions

Entries describe package versions. Published release checkpoints have tag links; installation follows `main` unless pinned. Validation belongs to the linked pull requests. Older reports remain available through immutable links.

## 1.10.2 enables worker local checks

Cursor baseline: [0.15.5](https://github.com/cursor/plugins/tree/12d587dfb20741cafc376c42c696c5f6e2a64487/pstack). [Issue #103](https://github.com/arjitj2/open-pstack/issues/103). [PR #104](https://github.com/arjitj2/open-pstack/pull/104).

Writers receive tool-specific guidance and check their changes before returning. External Claude writers use sandboxed Bash for local checks, with no unsandboxed retry. File-only providers retain parent check handoffs. Parent acceptance and Git ownership remain separate from worker reports.

## 1.10.1 fixes the documented receipt-normalization command

Cursor baseline: [0.15.5](https://github.com/cursor/plugins/tree/12d587dfb20741cafc376c42c696c5f6e2a64487/pstack). [Issue #99](https://github.com/arjitj2/open-pstack/issues/99). [PR #100](https://github.com/arjitj2/open-pstack/pull/100). Tag [v1.10.1](https://github.com/arjitj2/open-pstack/releases/tag/v1.10.1).
Expand Down
2 changes: 1 addition & 1 deletion UPSTREAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ This page records the current Cursor baseline and the maintainer procedure for r
| Path | `pstack/` |
| Commit | `12d587dfb20741cafc376c42c696c5f6e2a64487` |
| Upstream version | `0.15.5` |
| open-pstack version | `1.10.1` |
| open-pstack version | `1.10.2` |

The table records the packaged version on `main` and the Cursor content it incorporates, minus the exclusions below. Detecting or reviewing a newer Cursor commit does not advance this baseline. Cursor's version identifies the imported content. The open-pstack version identifies the cross-harness package, and its numbers are independent of Cursor and Eric's port.

Expand Down
2 changes: 1 addition & 1 deletion plugins/pstack/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "pstack",
"displayName": "pstack",
"version": "1.10.1",
"version": "1.10.2",
"description": "if you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Ported from cursor/plugins/pstack for Claude Code and Codex.",
"author": {
"name": "Lauren Tan"
Expand Down
2 changes: 1 addition & 1 deletion plugins/pstack/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pstack",
"version": "1.10.1",
"version": "1.10.2",
"description": "if you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Codex port of the Claude Code plugin; skills are shared, tool names resolve via skills/poteto-mode/references/codex-tools.md.",
"author": {
"name": "Lauren Tan"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ Pass arguments as an argv array or quote every path. Never interpolate prompt te

`legacy` is the default for existing assignments. Its ownership rule is prompt guidance. `strict` requires live proof that file edits remain possible while repository Git metadata, alternate gitdirs, metadata pointers and hardlinks, shell descendants, and authenticated remote mutation stay blocked. No current native or external route has that complete proof. `prepare --contract strict` returns `unsupported`; the runner writes a schema 2 `unsupported-capability` preflight receipt with `processStarted:false` and exit 79. This is a terminal capability stop, not a route failure that can advance a saved fallback. CLI help flags, tool allowlists, sandbox labels, and an inherited full-access parent do not establish the boundary.

Claude has no separate authentication preflight on any platform. Do not run `claude auth status` during setup or worker dispatch: its short-lived process can consume a refresh token and exit before saving the replacement ([upstream issue 95822](https://github.com/anthropics/claude-code/issues/95822)). The actual task handles authentication. Receipts record `preflight.status: not-run` and an unverified billing route. Known API environment checks remain, but Claude's account billing type is not asserted. See [the exception and conditions for revisiting it](https://github.com/arjitj2/open-pstack/issues/38).
Claude has no separate authentication preflight on any platform. Do not run `claude auth status` during setup or worker dispatch: its short-lived process can consume a refresh token and exit before saving the replacement ([upstream issue 95822](https://github.com/anthropics/claude-code/issues/95822)). The actual task handles authentication. Read-only receipts record `preflight.status: not-run`. Writers first run `claude --version` and require a stable version of at least `2.1.285`, the first version validated with the writer sandbox profile. Old or malformed versions produce `unavailable-cli` before model execution. A passing version probe does not establish authentication or billing. Known API environment checks remain, but Claude's account billing type is not asserted. See [the exception and conditions for revisiting it](https://github.com/arjitj2/open-pstack/issues/38).

Grok authentication preflight has one bounded retry. If the first `grok models` result would be classified as unauthenticated, the runner waits five seconds and tries the same preflight once more. A second failure is terminal. The delay and second attempt share the runner's absolute deadline and cancellation latch, and the receipt keeps evidence from both attempts. Model execution is never retried.

Expand Down Expand Up @@ -203,7 +203,9 @@ Surface a lane update to the user only when its rendered `changeKey` differs fro

The runner and its preflight have no implicit timeout. Do not invent a duration from role, mode, or a convenient round number; real implementation lanes can run for 90 minutes or much longer. Pass `--timeout` only when the user, an external service deadline, or a measured task contract supplies a real bound. That value starts at wrapper entry, before module loading and argument parsing, and remains one absolute deadline across setup, preflight, model execution, and output capture. It is never a fresh allowance per child, and long waits are armed in runtime-safe chunks without shortening the supplied deadline. Otherwise supervise liveness through the retained background task/session handle and cancel manually only on evidence that the run is dead. Cancel through that retained handle so the runner receives SIGINT or SIGTERM, sends it to an active child when one remains, stops waiting on inherited output pipes, removes the empty output reservation, and writes a `cancelled` receipt. Preserve that receipt; a retry is a new attempt with new unique output and receipt paths. Unchanged running state is not a dropout, and Claude's ten-minute foreground ceiling is never a reason to terminate a healthy lane.

Read-only mode maps to Claude plan mode with project-only settings and an explicit tool list, Codex's read-only sandbox, and Grok plan mode plus its `read-only` sandbox and read-oriented tool list. Grok's built-in read-only profile deliberately keeps its own state and system temporary directories writable, so point a read-only Grok lane at the actual checkout rather than a worktree under `/tmp`, `/var/tmp`, or the host's temporary directory. `isolated-write` maps to Claude `acceptEdits` with project-only settings, Codex `workspace-write`, and Grok `acceptEdits` plus its `workspace` sandbox and write-capable tool list. Give every writer only a dedicated worktree or output directory. Never route a writer into the primary checkout.
Read-only mode maps to Claude plan mode with an explicit tool list and project-only settings, or empty setting sources under `apiSpend: deny`. It maps to Codex's read-only sandbox and Grok plan mode plus its `read-only` sandbox and read-oriented tool list. Grok's built-in read-only profile deliberately keeps its own state and system temporary directories writable, so point a read-only Grok lane at the actual checkout rather than a worktree under `/tmp`, `/var/tmp`, or the host's temporary directory. `isolated-write` maps to Claude `acceptEdits`, Codex `workspace-write`, and Grok `acceptEdits` plus its `workspace` sandbox and write-capable tool list. Give every writer only a dedicated worktree or output directory. Never route a writer into the primary checkout.

External Claude writers receive session `--settings` that enable sandboxed Bash and `autoAllowBashIfSandboxed`, require `failIfUnavailable`, disable `allowUnsandboxedCommands`, leave `excludedCommands` empty, keep filesystem isolation enabled, and allow no network domains. Writer setting sources are empty for every spending policy, so project sandbox exclusions cannot widen command access. Managed policies still apply. Read-only lanes receive no sandbox settings because automatic Bash approval could permit writes. A missing sandbox dependency or unsupported platform fails loudly; the runner never retries without isolation or grants unrestricted Bash. Checks that need outside-directory caches or network access remain blocked and require parent inspection. This shell profile does not establish strict Git confinement. See [Claude's sandbox contract](https://code.claude.com/docs/en/sandboxing).

Cursor uses the explicit `cursor-agent` executable, not `agent` (which can name another CLI). For stored OAuth credentials, preflight requires `cursor-agent status --format json` to return `isAuthenticated: true`. With a nonblank `CURSOR_API_KEY`, preflight checks `cursor-agent --version` only and records that authentication is deferred to the real model invocation; Cursor status does not report API-key authentication. The key stays in the environment. Authentication failures from execution fail the lane. Setup always performs a real model probe because preflight alone does not prove model access. The runner sends the prompt through stdin, pins `--model`, requests a successful JSON terminal result, and uses `--workspace` with `--sandbox enabled`. Read-only adds `--mode ask` and denies `Write(**)` and `Shell(*)`; it cannot run shell-based tests. Writers use the sandbox in their dedicated workspace without `--force` or `--yolo`.

Expand Down Expand Up @@ -236,6 +238,8 @@ Start native and external lanes in the same fan-out phase, then wait for all of

The parent owns the assigned repository's index, refs, configuration, Git metadata, and remote writes. Every native or external worker receives this rule in its prepared prompt. Workers may edit ordinary assigned files and run allowed checks; tests may create disposable fixture repositories only when the task allows them. Read-only tool settings alone do not remove every route's shell or network authority, and the legacy prompt is not an enforcement claim.

Writer assignments require suitable local checks, repair of failures caused by their edits, and another affected check run. The parent gives provider-neutral tasks; each external invocation supplies guidance for its configured tools. Devin writers use sandboxed `exec` for edits and checks. OpenCode and Antigravity remain file-only and request required parent checks through `run-checks`. Native workers use permitted inherited host tools without an enforcement promise. A failing assertion permits repair; a denied tool ends the worker turn without retry, workaround, or escalation. Final responses name actual commands, observed results, and checks left unverified. Worker reports and worker-written logs are advisory. Provider tool observations establish worker execution; separate parent acceptance checks establish the reviewed candidate's behavior. A `complete` receipt proves final delivery, not independently verified checks.

A worker that needs a parent-only operation may end its ordinary final response with exactly one `pstack-handoff` fenced JSON object containing `task`, `checkpoint`, `operation`, `files`, `checks`, and `summary`. `operation` is `commit-checkpoint` or `run-checks`. A delivered handoff is **not** task completion. Schema 2 receipts use status `needs-parent-operation`; an ordinary final response uses `complete`. Truncated, repeated, or malformed reserved markers fail closed. Older schema 1 readers reject schema 2 rather than interpreting the handoff as completed work. A verified permission refusal is classified from provider-owned evidence; worker prose alone cannot prove it, and an earlier nonessential refusal cannot replay a delivered completion.

The parent can inspect a response with `pstack-worker-contract interpret --response <final-file>`. Before acting on a handoff, run:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,21 @@ describe("Antigravity external lanes", () => {
expect(existsSync(join(opts.cwd,".agents"))).toBe(false);
});

it("sends the rendered contract with file-only guidance and workspace addressing", async () => {
const opts = options({mode:"isolated-write"});
const result = await runLane(opts);
expect(result.receipt.status).toBe("complete");
const seen = observed();
const turn = JSON.parse(seen.prompt);
expect(turn.event).toBe("user");
const content = turn.message.content as string;
expect(content).toContain(`Edit only the assigned dedicated worktree at ${opts.cwd}.`);
expect(content).toContain("## Worker contract");
expect(content).toContain("command execution is unavailable, so request required checks through the run-checks handoff");
expect(content).not.toContain("the parent runs tests");
expect(content).toContain("Read the assigned value and answer.");
});

it("blocks ambient API credentials before any CLI process starts", async () => {
process.env.GEMINI_API_KEY = "secret-not-for-receipts";
const result = await runLane(options({apiSpend:"deny"}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function createAntigravityLaneFiles(options: RunnerOptions): AntigravityC
export function antigravityStdin(prompt: string, cwd: string, mode: AccessMode): string {
const instruction = mode === "read-only"
? `Inspect the assigned workspace at ${cwd}. Do not write files.`
: `Edit only the assigned dedicated worktree at ${cwd}. File tools only; the parent runs tests.`;
: `Edit only the assigned dedicated worktree at ${cwd}.`;
return `${JSON.stringify({ event: "user", message: { content: `${instruction}\n\n${prompt}` } })}\n`;
}

Expand Down
Loading
Loading