Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .agents/plans/24-model-picker-patch/README.md
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"]
}
```

Expand All @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions docs/release-qualification.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
45 changes: 45 additions & 0 deletions evals/qualification/patches/10.0.4.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
32 changes: 21 additions & 11 deletions scripts/patch-release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,24 @@ const StartupFile = <Path extends string>(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 =
Expand Down Expand Up @@ -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<string, unknown>
Expand Down
1 change: 1 addition & 0 deletions src/tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export default define({
toast: ctx.ui.toast,
});
ctx.keymap.layer(() => ({
mode: "global",
commands: [
{
id: "flow.models.select",
Expand Down
39 changes: 39 additions & 0 deletions tests/model-picker.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<unknown>) => {
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",
Expand Down
68 changes: 59 additions & 9 deletions tests/patch-release.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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,
Expand All @@ -96,7 +97,7 @@ async function fixture(startup = false) {
}
: metadata;
await writeFile(join(root, "package.json"), JSON.stringify(baselinePackage));
const startupFiles: NonNullable<PatchRelease["startup"]>["files"] = [
const homeFiles = [
{
path: "src/infrastructure/fs/workspace-paths.ts",
baselineSha256: hash("before\n"),
Expand All @@ -107,7 +108,22 @@ async function fixture(startup = false) {
baselineSha256: hash("before\n"),
sha256: hash("after\n"),
},
];
] as const;
const approval: NonNullable<PatchRelease["startup"]> =
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 });
Expand Down Expand Up @@ -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,
}
: {}),
};
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -345,7 +392,10 @@ test("startup scope cannot extend to other versions, guides, dependencies or smo
baselineSha256: hash("a"),
sha256: hash("b"),
},
] as NonNullable<PatchRelease["startup"]>["files"],
] as Extract<
NonNullable<PatchRelease["startup"]>,
{ kind: "home-directory-startup-10.0.3" }
>["files"],
};
const before = {
...metadata,
Expand Down
2 changes: 1 addition & 1 deletion tests/qualification-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading