Clarify native Runner completion tool guidance

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-02 14:23:42 -05:00
1 parent 5837aa4442
commit 26c0e552a0
17 files changed
+131 -17

No files matched your search

@@ -131,6 +131,30 @@ and existing-test update.
`paperclip_finish`/`paperclip_block` guidance must not leak into legacy
completion paths, which use the operational skill and API.
Dotta approved the six proposed 2.3 changes and clarified that native tool
guidance must stay separate from legacy runners on 2026-10-02. Legacy adapters
use their existing Paperclip skill/API and adapter completion paths; they must
not inherit instructions to call unavailable native finish/block tools.
- [ ] **2.3.1 Share native finish/block tool documentation.** First slice:
one canonical description per tool for Codex and native MCP bridges; refresh
retained provider threads through the native tool-contract fingerprint.
Executable coverage: SH-4-tools checks actual wire/catalog delivery across
all five native provider kinds; SH-4-resume checks v13 checkpoint rotation
without changing the Paperclip task or prior history. Live qualification is
pending; use native profile comparisons, with legacy results kept separate.
- [ ] **2.3.2 Move document/file delivery procedures into native tool docs.**
Include the live server override and preserve prepared versus delivered receipts.
- [ ] **2.3.3 Move hiring/dependency recipes into native discovery/tool docs.**
Preserve existing blocker IDs and workspace-release behavior.
- [ ] **2.3.4 Reduce the fixed native prompt.** Retain the pre-choice connection
cue and the one-accepted-result boundary.
- [ ] **2.3.5 Remove repeated native task/backend procedures.** Preserve runtime
facts and specialized mode, answer, and recovery constraints.
- [ ] **2.3.6 Qualify the combined native reduction.** Record prompt/tool
revisions, exact source fingerprints, fresh/resumed sessions, and matched
GitHub behavior results before claiming improvement.
### Follow-up 2.1 implementation and verification
The common task and conversation defaults now share the same identity sentence
@@ -3,8 +3,10 @@ import { describe, expect, it } from "vitest";
import {
PRP_BLOCK_RESULT_OUTPUT_SCHEMA,
PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA,
PRP_BLOCK_TOOL_DESCRIPTION,
PRP_COMPLETION_RESULT_OUTPUT_SCHEMA,
PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA,
PRP_COMPLETION_TOOL_DESCRIPTION,
} from "./completion-result.js";
import { codexSemanticToolSpecs } from "../drivers/codex/codex-driver-values.js";
@@ -83,6 +85,8 @@ describe("provider-neutral completion result schema", () => {
);
expect(finish.inputSchema).toEqual(PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA);
expect(block.inputSchema).toEqual(PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA);
expect(finish.description).toBe(PRP_COMPLETION_TOOL_DESCRIPTION);
expect(block.description).toBe(PRP_BLOCK_TOOL_DESCRIPTION);
});
it("allows only a response-wake continuation when completion explicitly yields", () => {
@@ -1,6 +1,11 @@
/** Provider-neutral semantic completion tools and their strict model-facing schemas. */
export const PRP_COMPLETION_TOOL_NAME = "paperclip_finish" as const;
export const PRP_BLOCK_TOOL_NAME = "paperclip_block" as const;
/** Native Runner guidance; legacy adapters use their own skill/API completion paths. */
export const PRP_COMPLETION_TOOL_DESCRIPTION =
"Report completed work (done), work requiring review (needs_review), or an explicit wait for the next response (yielded with response_wake). Use the current completion contract and supporting evidence. If rejected, correct the report and retry. After acceptance, read the returned outcome; do not claim completion while gated. Explain any required approval with its supplied link and action, then write the final response and end the turn without further tool calls.";
export const PRP_BLOCK_TOOL_DESCRIPTION =
"Report work that cannot continue because of a concrete blocker. Identify the blocker, its owner, and the action needed to unblock it; use the current completion contract and supporting evidence. If rejected, correct the report and retry. After acceptance, read the returned outcome, explain the blocker and any required action in the final response, and end the turn without further tool calls.";
export const PRP_SEMANTIC_TOOL_NAMES = [
PRP_COMPLETION_TOOL_NAME,
PRP_BLOCK_TOOL_NAME,
@@ -1,3 +1,9 @@
import {
PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA,
PRP_BLOCK_TOOL_DESCRIPTION,
PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA,
PRP_COMPLETION_TOOL_DESCRIPTION,
} from "../../contracts/completion-result.js";
import {
CODEX_BLOCK_RESULT_OUTPUT_SCHEMA,
CODEX_INVALID_REQUEST,
@@ -540,6 +546,10 @@ describe("Codex app-server Codex driver", () => {
expect(started.developerInstructions).toBe(conversationMode === "direct" ? undefined : baseInstructions);
expect(resumed.developerInstructions).toBe(conversationMode === "direct" ? "" : baseInstructions);
expect(resumed.dynamicTools).toEqual(started.dynamicTools);
expect(started.dynamicTools).toEqual(conversationMode === "direct" ? [] : [
{ name: "paperclip_finish", description: PRP_COMPLETION_TOOL_DESCRIPTION, inputSchema: PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA },
{ name: "paperclip_block", description: PRP_BLOCK_TOOL_DESCRIPTION, inputSchema: PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA },
]);
expect(resumed.config).toEqual(started.config);
await recovered.session?.close({ reason: "verified additive recovery" });
});
@@ -1,5 +1,9 @@
import type { PersistedHarnessProviderIdentity } from "../../contracts/harness-driver.js";
import type { NativeUserMessage } from "../../contracts/types.js";
import {
PRP_BLOCK_TOOL_DESCRIPTION,
PRP_COMPLETION_TOOL_DESCRIPTION,
} from "../../contracts/completion-result.js";
import {
CODEX_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA,
CODEX_BLOCK_TOOL_NAME,
@@ -232,8 +236,7 @@ export function differingJsonPaths(
function finishToolSpec(): Record<string, unknown> {
return {
name: CODEX_COMPLETION_TOOL_NAME,
description:
"Return the one semantic completion result for this task, including an explicit response_wake yield when waiting for the next response.",
description: PRP_COMPLETION_TOOL_DESCRIPTION,
inputSchema: CODEX_RESULT_PROVIDER_INPUT_SCHEMA,
};
}
@@ -241,8 +244,7 @@ function finishToolSpec(): Record<string, unknown> {
function blockToolSpec(): Record<string, unknown> {
return {
name: CODEX_BLOCK_TOOL_NAME,
description:
"Return the one semantic result when the task cannot continue.",
description: PRP_BLOCK_TOOL_DESCRIPTION,
inputSchema: CODEX_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA,
};
}
@@ -1,4 +1,10 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA,
PRP_BLOCK_TOOL_DESCRIPTION,
PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA,
PRP_COMPLETION_TOOL_DESCRIPTION,
} from "../../contracts/completion-result.js";
import { canonicalOpenCodeMcpToolName, startOpenCodeMcpBridge, type OpenCodeMcpBridge } from "./mcp-bridge.js";
@@ -38,8 +44,8 @@ describe("OpenCode MCP bridge", () => {
expect(await listed.json()).toMatchObject({
result: { tools: [
{ name: "documents.read" },
{ name: "paperclip_finish" },
{ name: "paperclip_block" },
{ name: "paperclip_finish", description: PRP_COMPLETION_TOOL_DESCRIPTION, inputSchema: PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA },
{ name: "paperclip_block", description: PRP_BLOCK_TOOL_DESCRIPTION, inputSchema: PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA },
] },
});
});
@@ -4,8 +4,10 @@ import { Ajv2020, type ValidateFunction } from "ajv/dist/2020.js";
import {
PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA,
PRP_BLOCK_TOOL_DESCRIPTION,
PRP_BLOCK_TOOL_NAME,
PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA,
PRP_COMPLETION_TOOL_DESCRIPTION,
PRP_COMPLETION_TOOL_NAME,
} from "../../contracts/completion-result.js";
@@ -117,8 +119,8 @@ function normalizeTools(
seen.add(name);
}
if (includeTerminalTools) for (const terminal of [
{ name: PRP_COMPLETION_TOOL_NAME, description: "Return the semantic completion result.", inputSchema: PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA },
{ name: PRP_BLOCK_TOOL_NAME, description: "Return the semantic blocked result.", inputSchema: PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA },
{ name: PRP_COMPLETION_TOOL_NAME, description: PRP_COMPLETION_TOOL_DESCRIPTION, inputSchema: PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA },
{ name: PRP_BLOCK_TOOL_NAME, description: PRP_BLOCK_TOOL_DESCRIPTION, inputSchema: PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA },
]) {
if (!seen.has(terminal.name)) tools.push(terminal);
}
@@ -1,6 +1,12 @@
import { connect } from "node:net";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA,
PRP_BLOCK_TOOL_DESCRIPTION,
PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA,
PRP_COMPLETION_TOOL_DESCRIPTION,
} from "../contracts/completion-result.js";
import {
canonicalRunnerToolName,
@@ -48,8 +54,8 @@ describe("runner semantic MCP bridge", () => {
result: {
tools: [
{ name: "documents.read" },
{ name: "paperclip_finish" },
{ name: "paperclip_block" },
{ name: "paperclip_finish", description: PRP_COMPLETION_TOOL_DESCRIPTION, inputSchema: PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA },
{ name: "paperclip_block", description: PRP_BLOCK_TOOL_DESCRIPTION, inputSchema: PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA },
],
},
});
@@ -10,8 +10,10 @@ import { Ajv2020, type ValidateFunction } from "ajv/dist/2020.js";
import {
PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA,
PRP_BLOCK_TOOL_DESCRIPTION,
PRP_BLOCK_TOOL_NAME,
PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA,
PRP_COMPLETION_TOOL_DESCRIPTION,
PRP_COMPLETION_TOOL_NAME,
} from "../contracts/completion-result.js";
@@ -70,12 +72,12 @@ const MAX_RESULT_CHUNK_BYTES = 64 * 1024;
const RESERVED_TOOLS: readonly RunnerToolDefinition[] = [
{
name: PRP_COMPLETION_TOOL_NAME,
description: "Return the semantic completion result.",
description: PRP_COMPLETION_TOOL_DESCRIPTION,
inputSchema: PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA,
},
{
name: PRP_BLOCK_TOOL_NAME,
description: "Return the semantic blocked result.",
description: PRP_BLOCK_TOOL_DESCRIPTION,
inputSchema: PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA,
},
];
@@ -54,6 +54,10 @@ import {
CODEX_SKILLLESS_BASE_INSTRUCTIONS,
createCodexTaskEnvelope,
} from "../contracts/codex.js";
import {
PRP_BLOCK_TOOL_DESCRIPTION,
PRP_COMPLETION_TOOL_DESCRIPTION,
} from "../contracts/completion-result.js";
import {
CodexAppServerDriver,
codexSemanticToolSpecs,
@@ -1336,14 +1340,19 @@ it("includes ACPX terminal tools in the authenticated bridge catalog", () => {
});
});
it("preserves answer and internal wait descriptions in the serialized native tool catalog", () => {
it.each(["codex", "opencode", "claude_managed", "aws_agentcore", "acpx"] as const)("preserves answer and internal wait descriptions in the serialized native %s tool catalog", (provider) => {
const catalog = JSON.parse(
JSON.stringify(authorizedToolSetForProvider("codex", codexSemanticToolSpecs())),
JSON.stringify(authorizedToolSetForProvider(provider, codexSemanticToolSpecs())),
);
const finish = catalog.operations.find(
(operation: { operationId: string }) =>
operation.operationId === "paperclip_finish",
);
const block = catalog.operations.find(
(operation: { operationId: string }) => operation.operationId === "paperclip_block",
);
expect(finish.description).toBe(PRP_COMPLETION_TOOL_DESCRIPTION);
expect(block.description).toBe(PRP_BLOCK_TOOL_DESCRIPTION);
expect(finish.inputSchema.properties.summary.description).toContain(
"complete user-facing answer",
);
@@ -1840,6 +1840,12 @@ describe("rebindNativeSessionCheckpoint", () => {
retainedFingerprint:
"sha256:5b7b302db36f7ed6686f9ea1ba70bbf79ebd7fabf86953b548d966a2bc38b648",
},
{
contract: "native completion tool guidance",
// Deployed v13 local catalog before canonical finish/block descriptions.
retainedFingerprint:
"sha256:68a51d34e091c55ee5d0d2b563153454dd727d72db16e6a27c358d342ae489c9",
},
{
contract: "task-bound human-input description",
// Deployed v9 still advertises the generic mock-task question description.
@@ -23,7 +23,7 @@ export function nativeToolContractFingerprintForTarget(
return `sha256:${createHash("sha256")
.update(
JSON.stringify({
schema: "paperclip.native-tool-contract.v13",
schema: "paperclip.native-tool-contract.v14",
executionTargetKind,
advertisementPolicy: {
// Direct provider threads retain declarations from thread/start.
+7
View File
@@ -14,6 +14,13 @@ agent-creation route materialize the shipped default.
| SH-1: native Codex preserves vendor base instructions | Serialized start/resume requests from the TypeScript driver, recovery paths, runnerd transport, Runner Lab/live sessions, and Rust provider use additive developer instructions. | Real new native Codex hires execute the three journeys below. Task success alone cannot prove vendor base preservation. |
| SH-2: identity-only default hire manual | Existing public agent-creation and onboarding-asset tests cover default, custom, and CEO exceptions. | The public bundle is exactly one `AGENTS.md` containing the eight-word shipped identity, checked before provider execution and again during cleanup. |
| SH-3: reduced shared legacy task/chat defaults and resume delta | Shared prompt tests and ACPX, Codex, OpenCode, Pi, Hermes, and Cursor Cloud adapter regressions retain runtime context and exclude removed generic procedures. | Actual legacy `adapter.invoke` prompts retain fresh identity/connection guidance, omit the removed procedures, and have complete receipts for every observed run. |
| SH-4: shared native finish/block tool guidance | Actual authenticated MCP catalogs, Codex start/resume frames, serialized daemon catalogs for all five native provider kinds, and rotation of a retained v13 thread to the new tool contract. | The three native profiles run the existing skill, continuation, and chat journeys. Configured coverage is not a live qualification result. |
SH-4 changes native Runner tools only. Legacy adapters do not receive
`paperclip_finish` or `paperclip_block` from this change. Their skill/API
completion paths remain separate. The shared legacy prompt tests in SH-3
continue to check their exact minimal defaults. The fixed native prompt and
task constraints are not reduced in this first tool-documentation slice.
`pnpm test:e2e:runner:stock-harness` runs these credential-free prerequisites,
including the Rust test. It writes JSON reports, the source SHA, a working-source
+19
View File
@@ -38,6 +38,20 @@ export const stockHarnessGates = [
{ id: "SH-3-hermes", name: "Hermes shared-prompt delivery", cwd: "packages/adapters/hermes",
files: ["src/server/prompt-rendering.test.ts"],
required: ["renders standard assignment wake with task authority", "renders scoped planning wake authority"] },
{ id: "SH-4-tools", name: "Native completion tool delivery", cwd: "packages/paperclip-runner", files: [
"src/contracts/completion-result.test.ts", "src/drivers/runner-tool-bridge.test.ts",
"src/drivers/opencode/mcp-bridge.test.ts", "src/drivers/codex/codex-app-server-driver.lifecycle.test.ts",
"src/live/runnerd-codex-transport.test.ts",
], testPattern: "propagates answer and wait descriptions|binds to loopback|preserves stock Codex instructions on|preserves answer and internal wait descriptions",
required: ["propagates answer and wait descriptions", "runner semantic MCP bridge binds to loopback",
"OpenCode MCP bridge binds to loopback", "preserves stock Codex instructions on task recovery",
"preserves stock Codex instructions on prepared recovery", "preserves stock Codex instructions on direct recovery",
...["codex", "opencode", "claude_managed", "aws_agentcore", "acpx"].map(provider =>
`preserves answer and internal wait descriptions in the serialized native ${provider} tool catalog`)] },
{ id: "SH-4-resume", name: "Refresh retained native completion guidance", cwd: ".", files: [
"server/src/services/native-runtime/native-session-resume.test.ts",
], testPattern: "refreshes retained",
required: ["native completion tool guidance"] },
{ id: "SH-eval", name: "Independent oracle and qualification admission", cwd: ".",
config: "tests/runner-e2e/vitest.config.ts",
files: ["tests/runner-e2e/stock-harness.test.ts", "tests/runner-e2e/stock-harness-checks.test.mjs",
@@ -73,6 +87,11 @@ export function sourceFingerprint() {
"packages/adapter-utils/src/server-utils.ts", "packages/shared/src/connection-intent-guidance.ts",
"server/src/onboarding-assets/default/AGENTS.md", "server/src/routes/agents.ts", "scripts/ensure-plugin-build-deps.mjs",
"packages/paperclip-runner/src/drivers/codex/codex-app-server-driver-impl.ts",
"packages/paperclip-runner/src/contracts/completion-result.ts",
"packages/paperclip-runner/src/drivers/codex/codex-driver-values.ts",
"packages/paperclip-runner/src/drivers/runner-tool-bridge.ts",
"packages/paperclip-runner/src/drivers/opencode/mcp-bridge.ts",
"server/src/services/native-runtime/native-session-resume.ts",
"packages/paperclip-runner/src/live/runnerd-codex-transport.ts",
"packages/paperclip-runner/src/live/live-session.ts",
"packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs",
@@ -7,7 +7,7 @@ const report = () => ({ testResults: [{ name: "/repo/boundary.test.ts", status:
numTotalTests: 1, numPassedTests: 1, numFailedTests: 0, numPendingTests: 0 });
describe("stock harness prerequisite coverage", () => {
it("maps every implemented change to an executable gate", () => {
expect(stockHarnessGates.map(gate => gate.id)).toEqual(["SH-1", "SH-2", "SH-3", "SH-3-hermes", "SH-eval"]);
expect(stockHarnessGates.map(gate => gate.id)).toEqual(["SH-1", "SH-2", "SH-3", "SH-3-hermes", "SH-4-tools", "SH-4-resume", "SH-eval"]);
expect(stockHarnessGates.every(gate => gate.files.length > 0 && gate.required.length > 0)).toBe(true);
});
it("accepts an executed passing boundary", () => expect(gradeGate(gate, report(), 0).passed).toBe(true));
@@ -5,7 +5,14 @@ import { stockHarnessSourceDigest } from "./stock-harness.js";
vi.mock("node:fs", async importOriginal => ({ ...await importOriginal<typeof import("node:fs")>(), readFileSync: vi.fn() }));
describe("stock harness instruction revision", () => {
it.each(["server/src/onboarding-assets/default/AGENTS.md", "packages/adapter-utils/src/server-utils.ts", "packages/shared/src/connection-intent-guidance.ts"])(
it.each([
"server/src/onboarding-assets/default/AGENTS.md", "packages/adapter-utils/src/server-utils.ts",
"packages/shared/src/connection-intent-guidance.ts", "packages/paperclip-runner/src/contracts/completion-result.ts",
"packages/paperclip-runner/src/drivers/codex/codex-driver-values.ts",
"packages/paperclip-runner/src/drivers/runner-tool-bridge.ts",
"packages/paperclip-runner/src/drivers/opencode/mcp-bridge.ts",
"server/src/services/native-runtime/native-session-resume.ts",
])(
"changes when the evaluated %s changes", source => {
vi.mocked(readFileSync).mockImplementation(() => Buffer.from("unchanged"));
const original = stockHarnessSourceDigest();
+5
View File
@@ -114,6 +114,11 @@ export function stockHarnessSourceDigest() {
"../../server/src/onboarding-assets/default/AGENTS.md",
"../../packages/adapter-utils/src/server-utils.ts",
"../../packages/shared/src/connection-intent-guidance.ts",
"../../packages/paperclip-runner/src/contracts/completion-result.ts",
"../../packages/paperclip-runner/src/drivers/codex/codex-driver-values.ts",
"../../packages/paperclip-runner/src/drivers/runner-tool-bridge.ts",
"../../packages/paperclip-runner/src/drivers/opencode/mcp-bridge.ts",
"../../server/src/services/native-runtime/native-session-resume.ts",
]) hash.update(source).update(readFileSync(new URL(source, import.meta.url)));
return hash.digest("hex");
}