diff --git a/.agents/plans/24-model-picker-patch/README.md b/.agents/plans/24-model-picker-patch/README.md new file mode 100644 index 00000000..4e2cffbd --- /dev/null +++ b/.agents/plans/24-model-picker-patch/README.md @@ -0,0 +1,25 @@ +# Flow 10.0.4 model-picker patch + +The maintainer approved full-send merge and a new release through release CI on +2026-10-30, without new model qualification. Flow's terminal keymap layer +implicitly used base mode. It loaded successfully but was hidden in prompt and +palette modes. Set the existing layer to global; model choices and runtime gates +stay unchanged. + +Use the exact-version baseline-qualified patch route against measured 10.0.2. +The record seals the inherited home-startup files and the picker file, comparing +all three directly to measured source fdd11b7d. Dependencies, build settings, +guidance, permissions, schema and lifecycle behavior are unchanged. Historical +80/81 matrix and 6/6 canary results remain historical, with no candidate model +measurement. Other versions and runtime paths require full qualification. + +Proof: reproduce missing palette entry with published 10.0.3; confirm both +palette/slash discovery and the planning/reviewer dialog after the fix in an +isolated terminal without a model prompt. Add a regression that fails without +global mode. Run pinned toolchain checks, packed native smoke, replay and strict +release initialization; obtain independent Sol review before pushing. CI and the +existing guarded Release workflow own publication of the exact sealed tarball. + +Dependabot #213 and #214 remain separate because this offline path requires +unchanged dependency and lockfile bytes. The unrelated intermittent MCP +execution-attribution failure is outside this UI fix. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bb7b705..7e3858c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ One short entry per release, written for users deciding whether to upgrade. +## [10.0.4] + +- Restore `/flow-models` and **Flow: Model settings** in the terminal prompt and + command palette. The picker now uses a global input mode; planning and reviewer + preferences, coding-model selection, runtime gates and Session v5 schema are + unchanged. +- Use an exact-version offline patch approval against the retained 10.0.2 + qualification. No new live model qualification or model canary was run; + historical results do not measure this UI fix. + +Remove an older exact pin before installing this release, then restart OpenCode: +`opencode2 plugin remove opencode-plugin-flow@10.0.3` followed by +`opencode2 plugin add opencode-plugin-flow@10.0.4`. + ## [10.0.3] - Start OpenCode safely from home after a global installation. Flow stays diff --git a/README.md b/README.md index db4bcad3..cb1f485a 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Install OpenCode v2 and the exact Flow release: ```sh npm install -g @opencode/cli@2.1.4 opencode2 --version -opencode2 plugin add opencode-plugin-flow@10.0.3 +opencode2 plugin add opencode-plugin-flow@10.0.4 ``` `opencode2 --version` must report `opencode v2.1.4` or a later tested 2.x version. @@ -63,7 +63,7 @@ For project scope, add this entry to the project's `opencode.json`: ```json { - "plugins": ["opencode-plugin-flow@10.0.3"] + "plugins": ["opencode-plugin-flow@10.0.4"] } ``` @@ -80,7 +80,7 @@ Set plugin defaults: ```json { "plugins": [{ - "package": "opencode-plugin-flow@10.0.3", + "package": "opencode-plugin-flow@10.0.4", "options": { "reviewer": { "model": "provider/model", "steps": 80 }, "typesafeApiKeyEnv": "MY_TYPESAFE_KEY" diff --git a/docs/release-qualification.md b/docs/release-qualification.md index 1208cb9b..43a07a61 100644 --- a/docs/release-qualification.md +++ b/docs/release-qualification.md @@ -37,8 +37,8 @@ sources or changed outcomes fail. Canary retries keep manager/reviewer identity. Reviewed offline paths cover [narrow patches](../.agents/plans/06-patch-release/README.md) and [frozen features](../.agents/plans/09-planning-models/README.md), always against -a fully qualified baseline. Exact 10.0.3's reviewed home-startup patch uses -10.0.2's released-tag evidence plus Linux startup tests, without new model calls or +a fully qualified baseline. The exact 10.0.3 startup and 10.0.4 picker patches use +10.0.2's released evidence plus offline native tests, without new model calls or canary. Other runtime changes retain full qualification. Prior passes never become candidate measurements. diff --git a/evals/qualification/patches/10.0.4.json b/evals/qualification/patches/10.0.4.json new file mode 100644 index 00000000..ad7afd48 --- /dev/null +++ b/evals/qualification/patches/10.0.4.json @@ -0,0 +1,45 @@ +{ + "schemaVersion": 1, + "kind": "baseline-qualified-patch", + "version": "10.0.4", + "baseline": { + "commit": "a920f7375d2bd5262e7fc5c272f5cac55c6b3b4b", + "artifact": { + "packageVersion": "10.0.2", + "sourceCommit": "fdd11b7dea1e95a87d781bdee89ed84aea8a29e7", + "sourceTreeSha256": "sha256:5ca3dc21436044f8a4d3b0c92f160d3cb90a7af422cfed8dd252846a818eccc9", + "tarballSha256": "sha256:6d8345ea7968799778fa4dc9c818ab37449b70c16a08a7eedba48f8c24c8f5a4", + "unpackedManifestSha256": "sha256:cf68469968b148aac2ccf30d6e0d22460079597d59ff6662ec39907362842153" + }, + "canary": "evals/canary/10.0.2.json", + "bundleSha256": "sha256:89b32228d6651737152a616dea44439b1d8acd23037c1f6123f91c2e920674ed" + }, + "artifact": { + "packageVersion": "10.0.4", + "tarballSha256": "sha256:34f1f0feda43d712030660cc0d8ffd40fe3f7f05a74f35630aaaa79354646591", + "unpackedManifestSha256": "sha256:712182b40c2314e66b31de1f6a1a86077f33775ff9599db597d5182975b46a58" + }, + "guidance": [], + "approvedBy": "Maintainer full-send approval in this T3 Code thread on 2026-10-30: release the model-picker fix through release CI without new model qualification.", + "rationale": "Exact-version terminal UI input-mode fix, retaining the two sealed 10.0.3 home-startup files against fully qualified 10.0.2. No guidance, dependency, model routing or runtime-gate changes. Deterministic regression, native terminal reproduction, full offline checks and independent review replace new candidate model measurements; historical evidence remains historical.", + "startup": { + "kind": "model-picker-10.0.4", + "files": [ + { + "path": "src/infrastructure/fs/workspace-paths.ts", + "baselineSha256": "sha256:f49ac543d8253e868aafd885feaced8214b0e3704815d606477ab3ab45c1e1fa", + "sha256": "sha256:fd146e0a680bd9d879292b6e3c88e853527d3354e1db842e13934b835c1b80c2" + }, + { + "path": "src/platform/opencode/plugin-composition.ts", + "baselineSha256": "sha256:26ffd9677ca07b4ab5f8f7297ecabc10cdf7df1feae0a2b8471490b5ca3ee214", + "sha256": "sha256:9e164e3a31464e8cfee7b860d6b707ae18e6418762c5647c0a05acba65556986" + }, + { + "path": "src/tui.ts", + "baselineSha256": "sha256:c8e9ba09c18220ccdc88288eb2c667caee1580980d695be45af436f95dd944e8", + "sha256": "sha256:63b5286453552c36f5cd55071c74ef3aa9b0b7cc342a82f4ec85f990f1ea4447" + } + ] + } +} diff --git a/package.json b/package.json index de16acf0..59eac113 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opencode-plugin-flow", - "version": "10.0.3", + "version": "10.0.4", "description": "Durable planning, validation, and independent review for OpenCode v2", "type": "module", "repository": { diff --git a/scripts/patch-release.ts b/scripts/patch-release.ts index 99eed7ad..2c3d37fb 100644 --- a/scripts/patch-release.ts +++ b/scripts/patch-release.ts @@ -18,15 +18,24 @@ const StartupFile = (path: Path) => z .object({ path: z.literal(path), baselineSha256: Hash, sha256: Hash }) .strict(); -const StartupApproval = z - .object({ - kind: z.literal("home-directory-startup-10.0.3"), - files: z.tuple([ - StartupFile("src/infrastructure/fs/workspace-paths.ts"), - StartupFile("src/platform/opencode/plugin-composition.ts"), - ]), - }) - .strict(); +const StartupFiles = [ + StartupFile("src/infrastructure/fs/workspace-paths.ts"), + StartupFile("src/platform/opencode/plugin-composition.ts"), +] as const; +const StartupApproval = z.discriminatedUnion("kind", [ + z + .object({ + kind: z.literal("home-directory-startup-10.0.3"), + files: z.tuple(StartupFiles), + }) + .strict(), + z + .object({ + kind: z.literal("model-picker-10.0.4"), + files: z.tuple([...StartupFiles, StartupFile("src/tui.ts")]), + }) + .strict(), +]); const STARTUP_SMOKE = "FLOW_LIVE_SMOKE=1 bun test tests/live-opencode-smoke.test.ts tests/plugin-startup-live.test.ts"; const BASELINE_SMOKE = @@ -95,11 +104,12 @@ export function assertPatchScope(input: { StartupApproval.parse(input.startup); if ( input.baselineVersion !== "10.0.2" || - input.version !== "10.0.3" || + input.version !== + (input.startup.kind === "model-picker-10.0.4" ? "10.0.4" : "10.0.3") || input.guidance.length ) throw new Error( - "Startup exception is only 10.0.3 against fully qualified 10.0.2, without guide changes.", + "Startup/picker exceptions require their exact version against fully qualified 10.0.2, without guide changes.", ); const oldScripts = oldPackage.scripts as | Record diff --git a/src/tui.ts b/src/tui.ts index c4cd48e5..ca96a270 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -41,6 +41,7 @@ export default define({ toast: ctx.ui.toast, }); ctx.keymap.layer(() => ({ + mode: "global", commands: [ { id: "flow.models.select", diff --git a/tests/model-picker.test.ts b/tests/model-picker.test.ts index a7ac7aa2..957247a2 100644 --- a/tests/model-picker.test.ts +++ b/tests/model-picker.test.ts @@ -1,7 +1,9 @@ import { expect, test } from "bun:test"; import type { LocationRef } from "@opencode/client"; import type { + Context, DialogSelectOptions, + KeymapLayer, ToastOptions, } from "@opencode/plugin/tui/context"; import type { @@ -24,6 +26,43 @@ import { } from "../src/platform/opencode/model-picker-ui.js"; import pickerDefinition from "../src/tui.js"; +test("picker is discoverable from prompt and command palette input modes", async () => { + let layer: KeymapLayer | undefined; + let titles: string[] = []; + const cleanup = await pickerDefinition.setup({ + location: { directory: "/project" }, + client: { rpc: () => ({}) }, + ui: { + dialog: { + select: async (input: DialogSelectOptions) => { + titles = input.options.map((option) => option.title); + return undefined; + }, + clear() {}, + }, + toast: {}, + }, + keymap: { + layer: (factory: () => KeymapLayer) => { + layer = factory(); + }, + }, + } as unknown as Context); + try { + // OpenCode defaults omitted mode to base, hiding the command in prompt/palette modes. + expect(layer?.mode).toBe("global"); + const command = layer?.commands?.find( + (item) => item.id === "flow.models.select", + ); + expect(command?.palette).toBe(true); + expect(command?.slash?.name).toBe("flow-models"); + await command?.run(); + expect(titles).toEqual(["Planning specialist", "Reviewer"]); + } finally { + await cleanup?.(); + } +}); + const model: PickerCatalogModel = { id: "native-id", providerID: "test", diff --git a/tests/patch-release.test.ts b/tests/patch-release.test.ts index 20cb5c96..e1f12cb1 100644 --- a/tests/patch-release.test.ts +++ b/tests/patch-release.test.ts @@ -72,7 +72,7 @@ async function git(root: string, ...args: string[]) { if (code) throw new Error(err); return out.trim(); } -async function fixture(startup = false) { +async function fixture(startup: boolean | "picker" = false) { const parent = await mkdtemp(join(tmpdir(), "flow-patch-")); roots.push(parent); const root = join(parent, "repo"); @@ -83,7 +83,8 @@ async function fixture(startup = false) { await mkdir(join(root, "src")); await mkdir(join(root, "skills/flow-run"), { recursive: true }); const baselineVersion = startup ? "10.0.2" : "8.3.0"; - const version = startup ? "10.0.3" : "8.3.1"; + const version = + startup === "picker" ? "10.0.4" : startup ? "10.0.3" : "8.3.1"; const baselinePackage = startup ? { ...metadata, @@ -96,7 +97,7 @@ async function fixture(startup = false) { } : metadata; await writeFile(join(root, "package.json"), JSON.stringify(baselinePackage)); - const startupFiles: NonNullable["files"] = [ + const homeFiles = [ { path: "src/infrastructure/fs/workspace-paths.ts", baselineSha256: hash("before\n"), @@ -107,7 +108,22 @@ async function fixture(startup = false) { baselineSha256: hash("before\n"), sha256: hash("after\n"), }, - ]; + ] as const; + const approval: NonNullable = + startup === "picker" + ? { + kind: "model-picker-10.0.4", + files: [ + ...homeFiles, + { + path: "src/tui.ts", + baselineSha256: hash("before\n"), + sha256: hash("after\n"), + }, + ], + } + : { kind: "home-directory-startup-10.0.3", files: [...homeFiles] }; + const startupFiles = approval.files; if (startup) for (const file of startupFiles) { await mkdir(join(root, file.path, ".."), { recursive: true }); @@ -207,10 +223,7 @@ export async function assertStrictReleaseEvidence(input) { rationale: "No core guarantees changed", ...(startup ? { - startup: { - kind: "home-directory-startup-10.0.3" as const, - files: startupFiles, - }, + startup: approval, } : {}), }; @@ -288,6 +301,40 @@ test("exact startup patch verifies the retained OpenAI baseline without claiming }), ).rejects.toThrow("two-provider baseline"); }); +test("10.0.4 seals the picker and inherited startup fixes without admitting future versions", async () => { + const f = await fixture("picker"); + const result = await assertPatchReleaseEvidence(f.input, f.verify); + expect(result.notes).toContain("Candidate 10.0.4"); + expect(result.notes).toContain("not measurements of this candidate"); + const approved = f.record.startup; + if (approved?.kind !== "model-picker-10.0.4") + throw new Error("Missing picker approval"); + for (const field of ["baselineSha256", "sha256"] as const) { + const prior = approved.files[2][field]; + approved.files[2][field] = hash("changed picker"); + await f.writeRecord(); + await expect(assertPatchReleaseEvidence(f.input, f.verify)).rejects.toThrow( + "startup content changed", + ); + approved.files[2][field] = prior; + } + f.record.version = "10.0.5"; + f.record.artifact.packageVersion = "10.0.5"; + f.input.expectedArtifact.packageVersion = "10.0.5"; + await writeFile( + join(f.root, "package.json"), + (await readFile(join(f.root, "package.json"), "utf8")).replace( + "10.0.4", + "10.0.5", + ), + ); + await git(f.root, "add", "."); + await git(f.root, "commit", "-m", "future version"); + await f.writeRecord(); + await expect(assertPatchReleaseEvidence(f.input, f.verify)).rejects.toThrow( + "exact version", + ); +}); test("startup baseline uses the released tag's verifier with isolated environment and cleans its worktree", async () => { const f = await fixture(true); await writeFile( @@ -345,7 +392,10 @@ test("startup scope cannot extend to other versions, guides, dependencies or smo baselineSha256: hash("a"), sha256: hash("b"), }, - ] as NonNullable["files"], + ] as Extract< + NonNullable, + { kind: "home-directory-startup-10.0.3" } + >["files"], }; const before = { ...metadata, diff --git a/tests/qualification-cli.test.ts b/tests/qualification-cli.test.ts index 665d67f4..d60b241a 100644 --- a/tests/qualification-cli.test.ts +++ b/tests/qualification-cli.test.ts @@ -56,7 +56,7 @@ import { assertQualificationBundle } from "../scripts/release-metadata.js"; import { autoQualifiedOutcome } from "./fixtures/auto-qualified-outcome.js"; test("full qualification retains two providers; the startup patch uses separate offline evidence", () => { - expect(packageJson.version).toBe("10.0.3"); + expect(packageJson.version).toBe("10.0.4"); const scenarios = releaseScenarios(packageJson.version); const catalog = releaseCatalog(packageJson.version); expect(scenarios).toHaveLength(20);