Skip to content
Open
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
8 changes: 7 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ jobs:
packages/agent-gwt/package.json \
packages/clanker-cleanroom/package.json \
packages/clanker-cleanroom-cursor/package.json \
packages/clanker-cleanroom-claude/package.json
packages/clanker-cleanroom-claude/package.json \
packages/clanker-cleanroom-plugin-agentfield/package.json
do
jq --arg v "${{ steps.version.outputs.prerelease }}" '.version = $v' "${pkg}" > "${pkg}.tmp"
mv "${pkg}.tmp" "${pkg}"
Expand All @@ -91,6 +92,7 @@ jobs:
clanker-cleanroom \
@clanker-cleanroom/cursor \
@clanker-cleanroom/claude \
@clanker-cleanroom/plugin-agentfield \
agent-gwt
do
pnpm --filter "${filter}" publish \
Expand Down Expand Up @@ -156,6 +158,7 @@ jobs:
clanker-cleanroom \
@clanker-cleanroom/cursor \
@clanker-cleanroom/claude \
@clanker-cleanroom/plugin-agentfield \
agent-gwt
do
cd "${workdir}"
Expand All @@ -171,6 +174,7 @@ jobs:
| if .dependencies["clanker-cleanroom"] then .dependencies["clanker-cleanroom"] = $v else . end
| if .dependencies["@clanker-cleanroom/cursor"] then .dependencies["@clanker-cleanroom/cursor"] = $v else . end
| if .dependencies["@clanker-cleanroom/claude"] then .dependencies["@clanker-cleanroom/claude"] = $v else . end
| if .dependencies["@clanker-cleanroom/plugin-agentfield"] then .dependencies["@clanker-cleanroom/plugin-agentfield"] = $v else . end
' staged/package/package.json > staged/package/package.json.tmp
mv staged/package/package.json.tmp staged/package/package.json
err="$(mktemp)"
Expand Down Expand Up @@ -212,6 +216,7 @@ jobs:
packages/clanker-cleanroom/package.json
packages/clanker-cleanroom-cursor/package.json
packages/clanker-cleanroom-claude/package.json
packages/clanker-cleanroom-plugin-agentfield/package.json
)
for pkg in "${packages[@]}"; do
if [[ ! -f "${pkg}" ]]; then
Expand All @@ -236,3 +241,4 @@ jobs:
npm dist-tag rm clanker-cleanroom "${PR_TAG}" || true
npm dist-tag rm @clanker-cleanroom/cursor "${PR_TAG}" || true
npm dist-tag rm @clanker-cleanroom/claude "${PR_TAG}" || true
npm dist-tag rm @clanker-cleanroom/plugin-agentfield "${PR_TAG}" || true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ coverage/
.DS_Store
.wireit/
.codegraph/
.work-items/
clanker-cleanroom.images.json
23 changes: 13 additions & 10 deletions PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,26 @@

CI publishes via [npm trusted publishers](https://docs.npmjs.com/trusted-publishers) (OIDC) from [`.github/workflows/publish.yml`](.github/workflows/publish.yml). There is no `NPM_TOKEN` secret.

This repo is a pnpm workspace with four publishable packages:
This repo is a pnpm workspace with five publishable packages:

| Package | Path |
| --------------------------- | -------------------------------------------------------------------------- |
| `clanker-cleanroom` | [`packages/clanker-cleanroom`](packages/clanker-cleanroom) |
| `@clanker-cleanroom/cursor` | [`packages/clanker-cleanroom-cursor`](packages/clanker-cleanroom-cursor) |
| `@clanker-cleanroom/claude` | [`packages/clanker-cleanroom-claude`](packages/clanker-cleanroom-claude) |
| `agent-gwt` | [`packages/agent-gwt`](packages/agent-gwt) |
| Package | Path |
| -------------------------------------- | ------------------------------------------------------------------------------------------ |
| `clanker-cleanroom` | [`packages/clanker-cleanroom`](packages/clanker-cleanroom) |
| `@clanker-cleanroom/cursor` | [`packages/clanker-cleanroom-cursor`](packages/clanker-cleanroom-cursor) |
| `@clanker-cleanroom/claude` | [`packages/clanker-cleanroom-claude`](packages/clanker-cleanroom-claude) |
| `@clanker-cleanroom/plugin-agentfield` | [`packages/clanker-cleanroom-plugin-agentfield`](packages/clanker-cleanroom-plugin-agentfield) |
| `agent-gwt` | [`packages/agent-gwt`](packages/agent-gwt) |

**Versions stay in sync** across five `package.json` files:
**Versions stay in sync** across six `package.json` files:

1. Root [`package.json`](package.json) (canonical for CI bumps)
2. [`packages/agent-gwt/package.json`](packages/agent-gwt/package.json)
3. [`packages/clanker-cleanroom/package.json`](packages/clanker-cleanroom/package.json)
4. [`packages/clanker-cleanroom-cursor/package.json`](packages/clanker-cleanroom-cursor/package.json)
5. [`packages/clanker-cleanroom-claude/package.json`](packages/clanker-cleanroom-claude/package.json)
6. [`packages/clanker-cleanroom-plugin-agentfield/package.json`](packages/clanker-cleanroom-plugin-agentfield/package.json)

CI publishes in dependency order: `clanker-cleanroom` → agent packages → `agent-gwt`.
CI publishes in dependency order: `clanker-cleanroom` → agent packages → `plugin-agentfield` → `agent-gwt`.

## One-time setup

Expand All @@ -33,12 +35,13 @@ pnpm run lint && pnpm run test && pnpm run build
pnpm --filter clanker-cleanroom publish --access public --ignore-scripts
pnpm --filter @clanker-cleanroom/cursor publish --access public --ignore-scripts
pnpm --filter @clanker-cleanroom/claude publish --access public --ignore-scripts
pnpm --filter @clanker-cleanroom/plugin-agentfield publish --access public --ignore-scripts
pnpm --filter agent-gwt publish --access public --ignore-scripts
```

### 2. Configure the trusted publisher (each package)

On [npmjs.com](https://www.npmjs.com) → package → Settings → Trusted Publisher, for **each** of the four packages:
On [npmjs.com](https://www.npmjs.com) → package → Settings → Trusted Publisher, for **each** of the five packages:

| Field | Value |
| -------------------- | --------------------------------------------------- |
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

pnpm monorepo:

| Package | Role |
| ------------------------------------------------- | ----------------------------------------------------- |
| [`clanker-cleanroom`](packages/clanker-cleanroom) | Build/run agent Docker images with workspace bindings |
| [`agent-gwt`](packages/agent-gwt) | GWT step functions for repeatable agent tests |
| Package | Role |
| --------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| [`clanker-cleanroom`](packages/clanker-cleanroom) | Build/run agent Docker images with workspace bindings |
| [`@clanker-cleanroom/cursor`](packages/clanker-cleanroom-cursor) | Cursor binding + Docker image |
| [`@clanker-cleanroom/claude`](packages/clanker-cleanroom-claude) | Claude Code binding + Docker image |
| [`@clanker-cleanroom/plugin-agentfield`](packages/clanker-cleanroom-plugin-agentfield) | `agent.clanker()` bridge into `@agentfield/sdk` with usage auditing |
| [`agent-gwt`](packages/agent-gwt) | GWT step functions for repeatable agent tests |

```bash
pnpm install
Expand All @@ -14,4 +17,4 @@ pnpm run test
pnpm run lint
```

See [packages/clanker-cleanroom/README.md](packages/clanker-cleanroom/README.md) and [packages/agent-gwt/README.md](packages/agent-gwt/README.md) for usage.
See [packages/clanker-cleanroom/README.md](packages/clanker-cleanroom/README.md), [packages/clanker-cleanroom-plugin-agentfield/README.md](packages/clanker-cleanroom-plugin-agentfield/README.md), and [packages/agent-gwt/README.md](packages/agent-gwt/README.md) for usage.
23 changes: 19 additions & 4 deletions packages/clanker-cleanroom-claude/src/binding.command.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ describe("claudeBinding.command", () => {
includes_resume_flag,
},
});

test("omits the raw prompt from argv so the CLI reads it from stdin", {
when: {
building_claude_command,
},
then: {
command_omits_raw_prompt,
},
});
});

function oauth_token_credentials(this: Context) {
Expand All @@ -121,6 +130,10 @@ function building_docker_args_with_session(this: Context) {
this.args = dockerArgs(this.credentials, { sessionId: "sess-claude-1" });
}

function building_claude_command(this: Context) {
this.args = claudeBinding.command({ prompt: "Create a README" });
}

function dockerArgs(
credentials: ClaudeCredentials,
options: { model?: string; sessionId?: string } = {},
Expand Down Expand Up @@ -214,20 +227,22 @@ function invokes_claude_headless_with_json_output(this: Context) {
expect(this.args).toContain("stream-json");
expect(this.args).toContain("--verbose");
expect(this.args).toContain("--dangerously-skip-permissions");
expect(this.args.at(-2)).toBe("--");
expect(this.args.at(-1)).toBe("Create a README");
expect(this.args.includes("Create a README")).toBe(false);
}

function command_omits_raw_prompt(this: Context) {
expect(this.args.includes("Create a README")).toBe(false);
expect(this.args.at(-1)).not.toBe("--");
}

function includes_model_flag(this: Context) {
const modelIndex = this.args.indexOf("--model");
expect(modelIndex).toBeGreaterThan(-1);
expect(this.args[modelIndex + 1]).toBe("sonnet");
expect(this.args.indexOf("--")).toBeGreaterThan(modelIndex);
}

function includes_resume_flag(this: Context) {
const resumeIndex = this.args.indexOf("--resume");
expect(resumeIndex).toBeGreaterThan(-1);
expect(this.args[resumeIndex + 1]).toBe("sess-claude-1");
expect(this.args.indexOf("--")).toBeGreaterThan(resumeIndex);
}
3 changes: 1 addition & 2 deletions packages/clanker-cleanroom-claude/src/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const claudeBinding: AgentBinding = {
trajectoryKind: "claude",
adaptEvents: adaptClaudeEvents,
sessionDataPath: CLAUDE_CONTAINER_SESSION_PATH,
command: ({ prompt, model, sessionId }) => {
command: ({ model, sessionId }) => {
const claudeArgs = [
"claude",
"-p",
Expand All @@ -32,7 +32,6 @@ export const claudeBinding: AgentBinding = {
if (model !== undefined && model !== "") {
claudeArgs.push("--model", model);
}
claudeArgs.push("--", prompt);
return claudeArgs;
},
prepare: async () => {
Expand Down
22 changes: 19 additions & 3 deletions packages/clanker-cleanroom-cursor/src/binding.command.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ describe("cursorBinding.command", () => {
includes_resume_flag,
},
});

test("omits the raw prompt from argv so the CLI reads it from stdin", {
when: {
building_cursor_command,
},
then: {
command_omits_raw_prompt,
},
});
});

function building_docker_args(this: Context) {
Expand All @@ -60,6 +69,10 @@ function building_docker_args_with_session(this: Context) {
this.args = dockerArgs({ sessionId: "sess-cursor-1" });
}

function building_cursor_command(this: Context) {
this.args = cursorBinding.command({ prompt: "Create a README" });
}

function dockerArgs(options: { model?: string; sessionId?: string } = {}): string[] {
return buildDockerRunArgs({
image: "clanker-cleanroom/cursor",
Expand Down Expand Up @@ -115,19 +128,22 @@ function invokes_agent_with_json_output(this: Context) {
expect(this.args).toContain("--output-format");
expect(this.args).toContain("stream-json");
expect(this.args).toContain("--force");
expect(this.args.at(-1)).toBe("Create a README");
expect(this.args.includes("Create a README")).toBe(false);
}

function command_omits_raw_prompt(this: Context) {
expect(this.args.includes("Create a README")).toBe(false);
expect(this.args.at(-1)).not.toBe("--");
}

function includes_model_flag(this: Context) {
const modelIndex = this.args.indexOf("--model");
expect(modelIndex).toBeGreaterThan(-1);
expect(this.args[modelIndex + 1]).toBe("composer-2");
expect(this.args.indexOf("--")).toBeGreaterThan(modelIndex);
}

function includes_resume_flag(this: Context) {
const resumeIndex = this.args.indexOf("--resume");
expect(resumeIndex).toBeGreaterThan(-1);
expect(this.args[resumeIndex + 1]).toBe("sess-cursor-1");
expect(this.args.indexOf("--")).toBeGreaterThan(resumeIndex);
}
3 changes: 1 addition & 2 deletions packages/clanker-cleanroom-cursor/src/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@ export const cursorBinding: AgentBinding = {
trajectoryKind: "cursor",
adaptEvents: adaptCursorEvents,
sessionDataPath: CONTAINER_SESSION_PATH,
command: ({ prompt, model, sessionId }) => {
command: ({ model, sessionId }) => {
const agentArgs = ["agent", "-p", "--force", "--output-format", "stream-json"];
if (sessionId !== undefined && sessionId !== "") {
agentArgs.push("--resume", sessionId);
}
if (model !== undefined && model !== "") {
agentArgs.push("--model", model);
}
agentArgs.push("--", prompt);
return agentArgs;
},
prepare: async () => {
Expand Down
58 changes: 58 additions & 0 deletions packages/clanker-cleanroom-plugin-agentfield/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# @clanker-cleanroom/plugin-agentfield

Monkeypatches `@agentfield/sdk` `Agent` with `agent.clanker(prompt, options)` so cleanroom coding agents run inside AgentField reasoners and record harness-style usage into the control-plane audit rollup.

## Install

```bash
pnpm add @clanker-cleanroom/plugin-agentfield clanker-cleanroom @agentfield/sdk
# plus the agent(s) you use:
pnpm add @clanker-cleanroom/cursor # and/or @clanker-cleanroom/claude
```

## Usage

```ts
import { Agent } from "@agentfield/sdk";
import "@clanker-cleanroom/cursor/register";
import "@clanker-cleanroom/plugin-agentfield/register";

const agent = new Agent({
nodeId: "fixer",
aiConfig: { provider: "openai", model: "gpt-4o" },
});

agent.reasoner("fix", async () => {
const result = await agent.clanker("Fix the failing test and leave a short summary", {
agent: "cursor",
workspace: "/tmp/ws",
model: "auto",
});

return { summary: result.text, costUsd: result.costUsd };
});
```

Or call `installClanker()` / the free `clanker(prompt, options, deps?)` helper from the package root instead of the `/register` side-effect.

Optional third argument `deps` is forwarded the same way on both `agent.clanker` and free `clanker` — pass `{ createAgent }` to reuse a staged cleanroom `Agent` (for `/agent/input` + `/agent/output`).

## Options

| Field | Type | Required | Role |
| --- | --- | --- | --- |
| `agent` | `string` | yes | Stock name (`cursor`, `claude`) or registry tag (`cursor:node`) |
| `workspace` | `string` | yes | Host workspace mounted at `/workspace` |
| `model` | `string` | no | Forwarded to the cleanroom agent |
| `image` | `string` | no | Docker image override |
| `remaps` | `Remaps` | no | Nested DooD path remaps |
| `signal` | `AbortSignal` | no | Cancel the in-flight run |
| `schema` | Zod / `{ parse }` | no | Structured output via `.agentfield_output.json` in the workspace |

Returns an AgentField `HarnessResult`. Token/cost usage is recorded on the current execution's `CostTracker` with `source: "harness"` and `harness: "clanker_<agent>"` when an execution context is active.

## Prerequisites

1. Docker (same as `clanker-cleanroom`)
2. Built/registered cleanroom images and bindings for the agents you call
3. Host credentials for those agents
Loading
Loading