Match Pi native question schemas to accepted method fields

This commit is contained in:
Dotta committed 2026-10-05 17:15:37 -05:00
1 parent 926104e4d6
commit a97649f283
3 files changed
+45 -5

No files matched your search

@@ -36,6 +36,15 @@ Cleanup passes. Both failed corrected local attempts remain failed; the memory
case is not qualified. No byte assertion, timeout, model or permission boundary
is relaxed. Production remains held; no merge or release is authorized.
The corrected Linux native-questions attempt delivers the first three typed
answers, then its editor call fails with `Pi question has unsupported fields`.
The original schema advertises fields that the selected method cannot accept.
The schema correction separates the four method contracts while retaining
strict handler validation. All 37 Pi extension tests pass. A credential-free
calibration proves that the installed old schema accepts an editor placeholder
that its handler rejects, while the corrected schema rejects that input before
execution. This correction also requires fresh runtime artifact qualification.
## Frozen target
- Pi: `@earendil-works/pi-coding-agent@1.0.0`.
@@ -4,6 +4,7 @@ import { mkdtemp, mkdir, realpath, rename, rm, symlink, writeFile } from "node:f
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import Ajv from "ajv";
import {
checkPiNativeTool, installPiRuntimeExtension, piMcpRequest, readPiRuntimeConfiguration,
PI_NATIVE_QUESTION_TOOL, PI_PERMISSION_TITLE_PREFIX, type PiExtensionApi, type PiRuntimeConfiguration, type PiToolDefinition,
@@ -30,6 +31,26 @@ function harness() {
}
describe("owned Pi runtime extension", () => {
it("advertises only the fields each native question method accepts", async () => {
const { config } = await workspace(); const h = harness();
await installPiRuntimeExtension(h.api, config);
const validate = new Ajv().compile(h.nativeTools[0]!.parameters);
const questions = [
{ method: "select", title: "Color", options: [{ id: "blue", label: "Blue" }] },
{ method: "confirm", title: "Preference", message: "Prefer dark mode?" },
{ method: "input", title: "Name", placeholder: "Name" },
{ method: "editor", title: "Draft", prefill: "Old draft" },
];
for (const question of questions) expect(validate(question)).toBe(true);
for (const question of [
{ ...questions[3], placeholder: "Name" },
{ ...questions[2], prefill: "Old draft" },
{ ...questions[1], options: [] },
{ method: "select", title: "Color" },
{ method: "confirm", title: "Preference" },
]) expect(validate(question)).toBe(false);
});
it("stops Pi 1 cache warming even when native economics recommend a paid refresh", async () => {
const { config } = await workspace(); const h = harness();
await installPiRuntimeExtension(h.api, config);
@@ -374,11 +374,21 @@ export async function installPiRuntimeExtension(
pi.registerTool({
name: PI_NATIVE_QUESTION_TOOL, label: "Ask a native question",
description: "Ask the human a native Pi select, confirm, input, or editor question. Select is single-choice and returns the supplied stable option ID. This cannot approve tools or a Paperclip Plan. For durable task questions or Plan approval use the assigned Paperclip semantic tools. Cancellation is not an answer; confirm false means No or dismissal.",
parameters: { type: "object", additionalProperties: false, required: ["method", "title"], properties: {
method: { type: "string", enum: ["select", "confirm", "input", "editor"] }, title: { type: "string", maxLength: PI_QUESTION_LABEL_MAX_LENGTH },
options: { type: "array", minItems: 1, maxItems: 128, items: { type: "object", additionalProperties: false, required: ["id", "label"], properties: { id: { type: "string", pattern: "^[A-Za-z0-9_-]{1,128}$" }, label: { type: "string", maxLength: PI_QUESTION_LABEL_MAX_LENGTH } } } },
message: { type: "string", maxLength: 16384 }, placeholder: { type: "string", maxLength: 16384 }, prefill: { type: "string", maxLength: 16384 },
} },
parameters: { type: "object", anyOf: [
{ type: "object", additionalProperties: false, required: ["method", "title", "options"], properties: {
method: { const: "select" }, title: { type: "string", maxLength: PI_QUESTION_LABEL_MAX_LENGTH },
options: { type: "array", minItems: 1, maxItems: 128, items: { type: "object", additionalProperties: false, required: ["id", "label"], properties: { id: { type: "string", pattern: "^[A-Za-z0-9_-]{1,128}$" }, label: { type: "string", maxLength: PI_QUESTION_LABEL_MAX_LENGTH } } } },
} },
{ type: "object", additionalProperties: false, required: ["method", "title", "message"], properties: {
method: { const: "confirm" }, title: { type: "string", maxLength: PI_QUESTION_LABEL_MAX_LENGTH }, message: { type: "string", maxLength: 16384 },
} },
{ type: "object", additionalProperties: false, required: ["method", "title"], properties: {
method: { const: "input" }, title: { type: "string", maxLength: PI_QUESTION_LABEL_MAX_LENGTH }, placeholder: { type: "string", maxLength: 16384 },
} },
{ type: "object", additionalProperties: false, required: ["method", "title"], properties: {
method: { const: "editor" }, title: { type: "string", maxLength: PI_QUESTION_LABEL_MAX_LENGTH }, prefill: { type: "string", maxLength: 16384 },
} },
] },
async execute(callId, args, signal, _onUpdate, context) {
const id = identities.bind(callId, PI_NATIVE_QUESTION_TOOL, args);
const previous = questions.get(id); if (previous) return previous;