Skip to content
Merged
3 changes: 2 additions & 1 deletion control-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"type": "module",
"scripts": {
"start": "node src/server.mjs",
"test": "node --test test/*.test.mjs"
"test": "node --test test/*.test.mjs",
"test:e2e:github": "node test/github-unreviewed-pr.e2e.mjs"
},
"dependencies": {
"ws": "8.21.3"
Expand Down
126 changes: 126 additions & 0 deletions control-server/test/github-unreviewed-pr.e2e.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import assert from "node:assert/strict";

const baseUrl = new URL(process.env.MULTIAGENT_E2E_URL || "http://127.0.0.1:18080");
const repository = process.env.MULTIAGENT_E2E_REPOSITORY || "multiagent";
const githubRepository = process.env.MULTIAGENT_E2E_GITHUB_REPOSITORY || "aptos-labs/aptos-core";
const timeoutMs = Number(process.env.MULTIAGENT_E2E_TIMEOUT_MS || 30 * 60_000);
const sessionId = process.env.MULTIAGENT_E2E_SESSION_ID || `github-pr-e2e-${Date.now().toString(36)}`;

const expected = await latestOpenPullRequestWithoutReviews(githubRepository);
console.log(`oracle: ${expected.html_url} (${expected.title})`);

let cookie = process.env.MULTIAGENT_E2E_COOKIE || "";
if (!cookie) {
const login = await request("/api/login", {
method: "POST",
body: {
username: required("MULTIAGENT_E2E_USERNAME"),
password: required("MULTIAGENT_E2E_PASSWORD"),
},
});
cookie = login.headers.get("set-cookie")?.split(";", 1)[0] || "";
assert.ok(cookie, "control server did not issue an authentication cookie");
}

const repositories = await request("/api/repositories", { cookie });
assert.ok(repositories.body.repositories.includes(repository), `repository is not configured: ${repository}`);

const task = [
`Read the latest open pull request in ${githubRepository} that has no submitted pull-request reviews.`,
"Use the GitHub Markdown runbook and access GitHub only through prod-mcp.",
"Return its PR number, title, author, URL, creation timestamp, and explicit evidence that its submitted review list is empty.",
"Do not clone or modify the repository.",
].join(" ");
await request("/api/sessions", {
method: "POST",
cookie,
body: { id: sessionId, repository, task },
expectedStatus: 201,
});
console.log(`session: ${sessionId}`);

const deadline = Date.now() + timeoutMs;
let lastStatus = "pending";
let lastReport = "";
while (Date.now() < deadline) {
const sessions = await request("/api/sessions", { cookie });
const session = sessions.body.sessions.find((candidate) => candidate.id === sessionId);
assert.ok(session, `session disappeared: ${sessionId}`);
if (session.status !== lastStatus) {
lastStatus = session.status;
console.log(`status: ${lastStatus}`);
}
try {
const report = await request(`/api/sessions/${sessionId}/report`, { cookie });
lastReport = String(report.body.report || "");
if (reportMatches(lastReport, expected)) {
console.log(lastReport);
console.log("GitHub unreviewed PR E2E passed");
process.exit(0);
}
} catch {}
if (["failed", "archived"].includes(session.status)) {
throw new Error(`session ended with status ${session.status}\n${lastReport}`);
}
await sleep(10_000);
}
throw new Error(`timed out waiting for ${sessionId}; last status: ${lastStatus}\n${lastReport}`);

function reportMatches(report, pullRequest) {
const number = String(pullRequest.number);
return report.includes(pullRequest.html_url)
&& (report.includes(`#${number}`) || report.includes(`PR ${number}`) || report.includes(`pull request ${number}`))
&& /(?:zero|no|empty).{0,80}(?:submitted )?reviews?/is.test(report);
}

async function latestOpenPullRequestWithoutReviews(repositoryName) {
const headers = {
accept: "application/vnd.github+json",
"user-agent": "multiagent-live-e2e",
"x-github-api-version": "2022-11-28",
};
if (process.env.GITHUB_TOKEN) headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
const pulls = await githubJson(`https://api.github.com/repos/${repositoryName}/pulls?state=open&sort=created&direction=desc&per_page=30`, headers);
for (const pull of pulls) {
const reviews = await githubJson(
`https://api.github.com/repos/${repositoryName}/pulls/${pull.number}/reviews?per_page=1`,
headers,
);
if (reviews.length === 0) return pull;
}
throw new Error(`no unreviewed open pull request found in the latest ${pulls.length} PRs`);
}

async function githubJson(url, headers) {
const response = await fetch(url, { headers });
const body = await response.json().catch(() => null);
if (!response.ok) throw new Error(`GitHub oracle failed with HTTP ${response.status}: ${JSON.stringify(body)}`);
return body;
}

async function request(path, options = {}) {
const headers = { accept: "application/json", origin: baseUrl.origin };
if (options.body) headers["content-type"] = "application/json";
if (options.cookie) headers.cookie = options.cookie;
const response = await fetch(new URL(path, baseUrl), {
method: options.method || "GET",
headers,
body: options.body ? JSON.stringify(options.body) : undefined,
});
const body = await response.json().catch(() => ({}));
const expectedStatus = options.expectedStatus || 200;
if (response.status !== expectedStatus) {
throw new Error(`${options.method || "GET"} ${path} returned ${response.status}: ${JSON.stringify(body)}`);
}
return { response, headers: response.headers, body };
}

function required(name) {
const value = process.env[name];
if (!value) throw new Error(`${name} is required`);
return value;
}

function sleep(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
29 changes: 26 additions & 3 deletions orchestrator_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
Coordinate isolated agents to satisfy the authenticated caller goal. Do not do
worker, ops, scout, or reviewer work yourself.

All framework paths in this prompt resolve under `$MULTIAGENT_FRAMEWORK_ROOT`.
Read policies, role modules, playbooks, and runbooks only from that image-owned
root; never use same-named files from the cloned application repository.

## Start

On a clean launch:
Expand Down Expand Up @@ -44,13 +48,32 @@ bindings, independent review, and phase completion.

- Spawn roles with `multiagent subagent spawn`; provider-native agents do not
establish the required Linux identity or evidence boundary.
- Source changes follow `prompts/playbooks/implementation-lifecycle.md`.
- Source changes follow
`$MULTIAGENT_FRAMEWORK_ROOT/prompts/playbooks/implementation-lifecycle.md`.
- External-only work skips the source lifecycle and uses reviewed ops requests.
- For ops, load only `prompts/playbooks/reviewed-ops-cycle.md`. Keep one ops
identity for the session and invoke `multiagent subagent reviewed-ops-cycle`
- For ops, load only
`$MULTIAGENT_FRAMEWORK_ROOT/prompts/playbooks/reviewed-ops-cycle.md`. Keep one
ops identity for the session and invoke `multiagent subagent reviewed-ops-cycle`
for every immutable request.
- Use a fresh reviewer for each immutable ops request. Finalize the ops identity
only when operational work completes or reaches a blocker.
- `reviewed-ops-cycle` waits for both review and the ops continuation. Consume
its compact result directly: never call `subagent wait` afterward and never
inspect logs, transcripts, role homes, operation directories, or receipts to
rediscover its result.
- After a reviewed ops cycle, treat any required follow-up operation as
incomplete until the same ops identity has materialized its complete bound
request at `$MULTIAGENT_LOG_DIR/agents/OPS_NAME/request.json`. A prose proposal
or `awaiting` report is not a result; restore that ops identity, then run a new
reviewed cycle with a fresh reviewer.
- Complete successful external-only work with
`multiagent orchestrator complete --external-only`; do not enter source
lifecycle phases or write surrogate result files.
- Preserve literal predicates from the authenticated goal. When the caller
requires an empty list, zero records, or no submitted items, any returned item
disqualifies that candidate regardless of its subtype or state. Do not weaken
the predicate by reclassifying records; continue the same bounded search until
the exact predicate is proven or a concrete blocker is reached.
- Load other playbooks only when their lifecycle is selected. Do not enumerate
prompt files to discover known roles.

Expand Down
39 changes: 31 additions & 8 deletions prompts/playbooks/reviewed-ops-cycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,51 @@ Do not replace this identity after review.

## Review and continue

After ops returns a published artifact descriptor:
After ops returns its bound request path and digest lines, pass that exact
ops-owned file to the supervisor-owned cycle. For the standard role contract it
is `$MULTIAGENT_LOG_DIR/agents/OPS_NAME/request.json`:

```bash
multiagent subagent reviewed-ops-cycle OPS_NAME \
--request-file "$PUBLISHED_REQUEST_PATH" \
--request-file "$MULTIAGENT_LOG_DIR/agents/OPS_NAME/request.json" \
--reviewer ops-reviewer-NN \
--timeout 900
```

Use a fresh reviewer name for each immutable request. This command:
Use a fresh reviewer name for each immutable request. The reviewer is required;
it is the independent authority boundary, not optional orchestration overhead.
This command:

1. publishes a safe legacy request when necessary;
1. validates that the bound request belongs to the named ops identity and
publishes it as a supervisor-owned immutable artifact;
2. binds the reviewer to the immutable request and exact runbook;
3. passes only a bounded artifact descriptor to the reviewer;
4. finalizes accepted review evidence before execution; and
5. continues the same ops identity in a fresh provider context with the exact
execute command.
execute command; and
6. waits for that continuation and prints one compact `ReviewedOpsCycleResult`
containing the ops conclusion or the next bound request.

Do not reconstruct these mechanics manually. Prior panes, transcripts, final
messages, and native provider resume state are intentionally excluded from the
continuation boundary.

On rejection or preflight failure, report the blocker. A changed request needs a
new publication and reviewer. A review correction may use a fresh reviewer on
the same immutable request. Never create a second ops identity.
Never pass the supervisor-owned published artifact back as `--request-file`;
that path is intentionally outside the ops identity directory. On rejection or
preflight failure, report the blocker. A changed request needs a new publication
and reviewer. A review correction may use a fresh reviewer on the same immutable
request. Never create a second ops identity.

The cycle already waits. Do not call `subagent wait` afterward, and do not read,
tail, grep, find, or list agent logs, transcripts, role homes, operation
directories, or receipts. Use only the returned `opsResult` and
`followUpRequest`. If `followUpRequest` is non-null, run a new cycle on that
exact path with a fresh reviewer. If it is null, use `opsResult` as the durable
conclusion. A prose proposal, an `awaiting` statement`, or a draft under a
private role-home path is incomplete work: restore the same ops identity to
materialize the canonical request rather than treating it as a result or
spawning a replacement.

For an external-only task with successful reviewed operations and no source
changes, finish with `multiagent orchestrator complete --external-only`. Do not
manufacture source lifecycle phases or files.
62 changes: 48 additions & 14 deletions prompts/roles/ops-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,46 @@ operations or encode provider-specific behavior in policy or source code.

## Prepare

1. Select the applicable runbook and derive the operation, target, parameters,
and phase from it and the prod-mcp target contract.
2. Write one bounded JSON draft under
`$MULTIAGENT_LOG_DIR/agents/$MULTIAGENT_SUBAGENT_NAME/` using the generic
envelope: `taskId`, `goal`, `operation`, `target`, `parameters`, and
`runbook`. Add `changeTicket` only when required. Never add `approvals` or
calculate `runbookContentSha256`.
3. Publish it with:
1. Select the applicable runbook and operation. Read the operation's live
prod-mcp contract before constructing parameters:

```bash
multiagent ops publish --draft-file "$DRAFT_FILE" \
multiagent ops describe OPERATION_ID
```

Use the returned description, JSON schema, examples, and authorization
requirements exactly. Do not infer provider fields from public APIs,
repository source, validation failures, or prior operations.
2. Generate, then edit, one bounded JSON draft under
`$MULTIAGENT_LOG_DIR/agents/$MULTIAGENT_SUBAGENT_NAME/`:

```bash
DRAFT_FILE="$MULTIAGENT_LOG_DIR/agents/$MULTIAGENT_SUBAGENT_NAME/request.json"
multiagent ops template > "$DRAFT_FILE"
```

Preserve the generated field shapes exactly. Set `taskId`, `goal`,
`operation.id`, `operation.version`, `parameters`, `runbook.id`,
`runbook.phase`, and `runbook.version` from the authenticated goal,
selected runbook, and the `ops describe` result. Do not add `target`;
runbook binding derives the canonical four-field target from the Markdown
runbook. Add `changeTicket` only when required. Never add `approvals`,
`runbookDocument`, or `runbookContentSha256`.
3. Bind the completed draft to a normalized path relative to the framework
root, then make the bound ops-owned request readable by the supervisor group
and not group-writable:

```bash
multiagent ops bind-runbook --request-file "$DRAFT_FILE" \
--runbook-document runbooks/SELECTED.md
chmod 0640 "$DRAFT_FILE"
```

Correct only the unpublished draft if validation fails. The returned descriptor
identifies the supervisor-owned immutable request; never copy or modify it.
Report that descriptor and stop for independent review.
Correct only this ops-owned request if binding fails. Report the exact
`DRAFT_FILE` path and the two digest lines returned by `bind-runbook`, then stop
for independent review. Do not call `ops publish`; the supervisor-owned
`reviewed-ops-cycle` publishes the immutable artifact after validating that the
bound request belongs to this ops identity.

## Execute after review

Expand All @@ -33,8 +56,19 @@ multiagent ops execute --request-file PATH --reviewer REVIEWER_NAME

Execute the reviewed request once. Interpret the structured outcome under the
runbook and decide whether to finish, escalate, or prepare a distinct request.
Changed bytes always require a new review. Report the action ID, receipt path,
result, or exact blocker.
The command prints a compact result and persists the full receipt at
`receiptPath`. Use the compact result directly. Do not search logs, transcripts,
role homes, or operation directories, and do not reread the receipt unless the
compact result explicitly reports missing or truncated evidence. An
`operationId` or `actionId` returned by execution is evidence, not an operation
capability ID; never pass it to `ops describe`.
Changed bytes always require a new review. For every follow-up operation, rerun
`multiagent ops describe OPERATION_ID`, overwrite and bind the canonical
`$MULTIAGENT_LOG_DIR/agents/$MULTIAGENT_SUBAGENT_NAME/request.json`, run
`chmod 0640` on it, and report its exact path and digest lines. Never use a
private role-home path and never finish with only a proposed request. Report the
action ID, receipt path, final result, or exact blocker only when no follow-up
operation remains.

Keep evidence in your trace directory. Credentials and signing authority remain
with the supervisor and prod-mcp; missing credential environment variables are
Expand Down
3 changes: 3 additions & 0 deletions prompts/roles/ops-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@ Before execution:
- If `review-bind` fails for either schema or binding, reject the request. Manual digest calculation or visual comparison is not a substitute for successful deterministic validation.
- If and only if the request matches the goal and runbook, make the first non-empty line exactly `Verdict: ACCEPTED`, then explain the decision without reproducing the binding artifact.
- Otherwise make the first non-empty line `Verdict: REJECTED` and explain the deviation.
- Keep the explanation to at most three concise bullets. Do not restate the
request, runbook, schemas, digests, or evidence that the supervisor already
supplied.

After execution, a separate reviewer invocation must inspect the persisted request and receipt under `MULTIAGENT_STATE_DIR/operations/ACTION_ID` and report any behavioral deviation or unexpected side effect.
5 changes: 3 additions & 2 deletions runbooks/github-repository-work.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Version: `1.0.0`
- Prod MCP operations: `github.read`, `github.clone`, `github.create-pr`
- Operation version: `1.0.0`
- Set `target` to `{"cluster":"external-services","environment":"production","namespace":"github","service":"installation"}`.

## Goal

Expand All @@ -16,8 +17,8 @@ pull request. GitHub credentials remain inside prod-mcp.
## Read phase

1. Set the phase to `read` and operation to `github.read`.
2. Identify the exact `owner/repository` and choose `get-repository`, `get-file`, `get-pull-request`, or `list-pull-requests`.
3. Bound file paths, refs, pull-request numbers, state, and result limits to the original goal.
2. Identify the exact `owner/repository` and choose `get-repository`, `get-file`, `get-pull-request`, `list-pull-requests`, or `list-pull-request-reviews`.
3. Bound file paths, refs, pull-request numbers, state, and result limits to the original goal. Compose multiple read requests when the goal requires correlating pull requests with their submitted reviews.
4. Persist the signed action ID and receipt.

## Materialize phase
Expand Down
Loading
Loading