From bcfcbffff6b97eebbbe4a0b204805812bf3f92c8 Mon Sep 17 00:00:00 2001 From: David Ahmann <46606159+davidahmann@users.noreply.github.com> Date: Thu, 24 Sep 2026 12:48:22 -0400 Subject: [PATCH] fix: make scoped reviewer output schema provider-compatible Signed-off-by: David Ahmann <46606159+davidahmann@users.noreply.github.com> --- README.md | 2 +- docs/releases/v0.10.1.md | 12 ++++++++ package-lock.json | 4 +-- package.json | 2 +- src/runtime/codex.ts | 12 +++++++- src/version.ts | 2 +- test/runtime-codex.test.ts | 62 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 docs/releases/v0.10.1.md diff --git a/README.md b/README.md index 4516388..f014ce8 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ GitHub workflow. Pin a released version in the repository that will use Mill: ```sh -npm i -D -E --ignore-scripts @davidahmann/mill@0.10.0 +npm i -D -E --ignore-scripts @davidahmann/mill@0.10.1 npx --no-install millctl --version ``` diff --git a/docs/releases/v0.10.1.md b/docs/releases/v0.10.1.md new file mode 100644 index 0000000..5f202b4 --- /dev/null +++ b/docs/releases/v0.10.1.md @@ -0,0 +1,12 @@ +# Mill 0.10.1 + +Mill's local reviewer now gives Codex a valid strict JSON schema when a +repository selects review checklists. Version 0.10.0 could verify a candidate +but fail before review because its nested `checklists` field was optional and +its path rule used a regular-expression feature the provider rejects. + +The provider schema now requires `checklists` only when the frozen review scope +contains them and leaves path validation to Mill's existing local parser and +exact-scope comparison. Scopes without checklists keep their prior shape. +Regression checks cover both forms. This source record does not claim the +release is published or extend Mill's supported stack. diff --git a/package-lock.json b/package-lock.json index ae808b3..a0e6e36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@davidahmann/mill", - "version": "0.10.0", + "version": "0.10.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@davidahmann/mill", - "version": "0.10.0", + "version": "0.10.1", "bundleDependencies": [ "typescript" ], diff --git a/package.json b/package.json index c212cdc..fd695b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@davidahmann/mill", - "version": "0.10.0", + "version": "0.10.1", "description": "Local-first software factory for new and existing codebases. Turns approved product intent into tested, reviewed PRs with repo-native evidence and explicit human approval for delivery and merge.", "license": "Apache-2.0", "author": "David Ahmann", diff --git a/src/runtime/codex.ts b/src/runtime/codex.ts index 41edcb5..118a6a3 100644 --- a/src/runtime/codex.ts +++ b/src/runtime/codex.ts @@ -588,12 +588,22 @@ export async function runCodexReview(input: ReviewerWorkerInput): Promise<{ try { // Strict provider output requires every declared property to be required. // Public state parsing retains optional scope for legacy persisted reviews. + const providerScopeSchema = + input.reviewScope?.checklists === undefined + ? reviewScopeSchema.omit({ checklists: true }) + : reviewScopeSchema.extend({ + checklists: reviewScopeSchema.shape.checklists + .unwrap() + .element.extend({ path: z.string().min(1) }) + .array() + .max(8), + }); const providerSchema = input.reviewScope === undefined ? reviewResultSchema.omit({ scope: true, gate: true }) : reviewResultSchema .omit({ gate: true }) - .extend({ scope: reviewScopeSchema }); + .extend({ scope: providerScopeSchema }); await writeFile( schemaPath, JSON.stringify(z.toJSONSchema(providerSchema)), diff --git a/src/version.ts b/src/version.ts index 6760fbf..554d75f 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,3 +1,3 @@ export const MILL_PACKAGE = "@davidahmann/mill"; -export const MILL_VERSION = "0.10.0"; +export const MILL_VERSION = "0.10.1"; export const RESULT_SCHEMA_VERSION = "1"; diff --git a/test/runtime-codex.test.ts b/test/runtime-codex.test.ts index 88a8d3a..ff45352 100644 --- a/test/runtime-codex.test.ts +++ b/test/runtime-codex.test.ts @@ -513,6 +513,68 @@ describe("Codex adapter boundaries", () => { } }); + it("sends a strict nested review scope schema with and without selected checklists", async () => { + const fixture = await runtimeFixture(); + const tools = await temporaryDirectory("mill-codex-scope-schema-"); + const inputs = await loadRuntimeInputs(fixture.root, fixture.taskPath); + const candidate = "a".repeat(40); + const frozen = await buildContextManifest( + fixture.root, + candidate, + inputs.task, + inputs.config, + inputs.taskDigest, + ); + try { + for (const selected of [false, true]) { + const scope = { + baseCommit: "b".repeat(40), + candidateCommit: candidate, + candidateTree: "c".repeat(40), + changedPaths: ["src/example.ts"], + ...(selected + ? { + checklists: [ + { + id: "runtime", + path: "quality/runtime.md", + digest: textDigest("checklist"), + }, + ], + } + : {}), + digest: textDigest("scope"), + }; + const output = JSON.stringify({ + schemaVersion: "1", + candidateCommit: candidate, + scope, + summary: "clean", + findings: [], + }); + process.env.MILL_CODEX_PATH = await executableScript( + tools.path, + `const fs=require("node:fs");const i=process.argv.indexOf("--output-schema");const schema=JSON.parse(fs.readFileSync(process.argv[i+1],"utf8"));const nested=schema.properties.scope;const keys=Object.keys(nested.properties).sort();const required=[...nested.required].sort();if(JSON.stringify(keys)!==JSON.stringify(required)||Object.hasOwn(nested.properties,"checklists")!==${selected})process.exit(13);if(${selected}&&nested.properties.checklists.items.properties.path.pattern)process.exit(14);console.log(JSON.stringify({type:"item.completed",item:{type:"agent_message",text:${JSON.stringify(output)}}}));console.log(JSON.stringify({type:"turn.completed"}));`, + ); + await expect( + runCodexReview({ + root: fixture.root, + task: inputs.task, + manifest: frozen.manifest, + candidateCommit: candidate, + reviewScope: scope, + deadlineMs: Date.now() + 5_000, + maxOutputBytes: 1024 * 1024, + }), + ).resolves.toMatchObject({ + review: { candidateCommit: candidate, scope, findings: [] }, + }); + } + } finally { + await Promise.all([fixture.cleanup(), tools.cleanup()]); + } + }); + it("rejects unsafe or oversized explicit final-message outputs", async () => { const fixture = await runtimeFixture(); const tools = await temporaryDirectory("mill-codex-review-file-");