Skip to content

Commit 3355a40

Browse files
committed
Release v2.0.48 to lock planning durability contracts
This release records the planning-session hardening now verified in the working tree: post-save artifact rendering failures surface as structured partial success, and strict planning payload validation is proven through actual OpenCode tool execution paths. Constraint: Preserve persistence-first planning semantics without widening Flow's public command/tool surface or dependency boundary. Rejected: Broaden the release into new workflow surfaces or dependency updates | the verified change is a scoped planning contract and release note update. Rejected: Treat schema-only coverage as sufficient | execution-path tests now guard the adapter wrapper boundary that OpenCode actually calls. Confidence: high Scope-risk: moderate Directive: Keep future planning schema strictness scoped to documented adapter-facing boundaries and keep artifact rendering failures distinguishable from unsaved state failures. Tested: bun test tests/config/tool-schemas.test.ts; bun run typecheck; bunx biome check tests/config/tool-schemas.test.ts; bun run check; bun run smoke:release; bun run check:fresh-surfaces && bun run check:pack-invariants. Not-tested: Live OpenCode UI runtime interaction; live GitHub-hosted release workflow run for tag v2.0.48 before push.
1 parent af4453e commit 3355a40

9 files changed

Lines changed: 310 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,29 @@
22

33
## [Unreleased]
44

5+
## [2.0.48] - 2026-05-14
6+
7+
Lock planning validation and post-save artifact recovery
8+
9+
Flow 2.0.48 closes the release-review gaps around planning-session durability and OpenCode planning payload validation. `flow_plan_start` now preserves the saved source-of-truth session when artifact rendering fails after persistence, returning a structured `partial_success` response with artifact-sync failure metadata instead of throwing after the mutation was already durable.
10+
11+
Planning payload strictness is now enforced at the adapter-facing runtime parse boundaries for `flow_plan_apply` and `flow_plan_context_record`. The outer `flow_plan_apply` payload, nested `plan`, nested optional `planning`, and `flow_plan_context_record` planning context reject unknown keys, while simple status/history tools remain intentionally tolerant. New execution-path tests call the actual OpenCode tool `execute()` wrappers to prove validation failures short-circuit through structured JSON errors, not only direct schema `safeParse()` checks.
12+
13+
The release deliberately does not add slash commands, runtime tools, prompt modes, state paths, package exports, installer behavior, dependencies, persisted schema migrations, or live OpenCode UI automation. It preserves the existing `@opencode-ai/plugin` and `zod` compatibility boundary.
14+
15+
Constraint: Preserve persistence-first planning semantics while making stale artifact rendering an explicit repair signal after saved state
16+
Constraint: Tighten only the adapter-facing planning payload boundaries; simple read/status tool payloads remain tolerant by contract
17+
Constraint: Keep `@opencode-ai/plugin` at `1.14.48` and `zod` at `4.1.8`; this release changes no dependency compatibility boundary
18+
Rejected: Treat post-save artifact rendering failure as a total `flow_plan_start` failure | callers need to know the session was saved and only derived artifacts need repair
19+
Rejected: Make every tool schema strict | the strictness contract is scoped to planning payload boundaries and preserves tolerant simple tool behavior
20+
Rejected: Add a new workspace result-kind hierarchy in this release | the current structured `partial_success` response covers callers without widening the runtime action API
21+
Confidence: high
22+
Scope-risk: moderate
23+
Reversibility: clean
24+
Directive: Keep planning payload strictness scoped to documented adapter-facing boundaries, and keep post-persistence artifact failures distinguishable from unsaved mutation failures
25+
Tested: `bun test tests/config/tool-schemas.test.ts` (18 pass, 0 fail, 515 expect() calls); `bun run typecheck`; `bunx biome check tests/config/tool-schemas.test.ts`; `bun run check` (release gate passed: dependency contract OK with project/plugin/root `zod=4.1.8`, architecture seams OK, fresh surfaces OK, pack invariants OK for version `2.0.48`, bundle sanity OK, full suite 663 pass/0 fail, lint passed, bench smoke and bench gate passed); `bun run smoke:release` (passed for package `2.0.48`, wrote release-smoke evidence under `prompt-exports/release-smoke/`, real OpenCode CLI not invoked); RepoPrompt Oracle architect review of the execution-path strictness follow-up returned APPROVE
26+
Not-tested: Live OpenCode UI runtime interaction; live GitHub-hosted release workflow run for tag `v2.0.48` before push
27+
528
## [2.0.47] - 2026-05-14
629

730
Make release smoke safe for reusable evidence directories

docs/architecture/strictness-contract.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ Scope: `src/adapters/opencode/tool-surface/schemas.ts`, `src/adapters/opencode/t
1515
A change is strictness-preserving only if **all** clauses hold:
1616

1717
1. **Required fields and unions do not widen at the bridge.**
18-
- `flow_plan_apply` remains `{ plan: PlanArgsSchema.strict(), planning?: PlanningContextArgsSchema.strict() }` at the adapter boundary.
18+
- Adapter-facing planning payloads reject unknown keys at these object boundaries: outer `flow_plan_apply`, `flow_plan_apply.plan` (`PlanArgsSchema.strict()`), optional `flow_plan_apply.planning` (`PlanningContextArgsSchema.strict()`), and outer `flow_plan_context_record`.
19+
- This planning strictness is scoped; it is not a global all-tools policy, and intentionally tolerant simple tool schemas remain covered by their own tests.
1920
- Worker completion continues to be parsed by `WorkerResultArgsSchema`, exported from `src/runtime/schema.ts` and owned internally by `src/runtime/schema-worker-result.ts`, before transition calls.
2021

2122
2. **Runtime parse/validation path is never bypassed.**

docs/releases/v2.0.48.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# v2.0.48
2+
3+
Lock planning validation and post-save artifact recovery
4+
5+
Flow 2.0.48 closes the release-review gaps around planning-session durability and OpenCode planning payload validation. `flow_plan_start` now preserves the saved source-of-truth session when artifact rendering fails after persistence, returning a structured `partial_success` response with artifact-sync failure metadata instead of throwing after the mutation was already durable.
6+
7+
Planning payload strictness is now enforced at the adapter-facing runtime parse boundaries for `flow_plan_apply` and `flow_plan_context_record`. The outer `flow_plan_apply` payload, nested `plan`, nested optional `planning`, and `flow_plan_context_record` planning context reject unknown keys, while simple status/history tools remain intentionally tolerant. New execution-path tests call the actual OpenCode tool `execute()` wrappers to prove validation failures short-circuit through structured JSON errors, not only direct schema `safeParse()` checks.
8+
9+
The release deliberately does not add slash commands, runtime tools, prompt modes, state paths, package exports, installer behavior, dependencies, persisted schema migrations, or live OpenCode UI automation. It preserves the existing `@opencode-ai/plugin` and `zod` compatibility boundary.
10+
11+
Constraint: Preserve persistence-first planning semantics while making stale artifact rendering an explicit repair signal after saved state
12+
Constraint: Tighten only the adapter-facing planning payload boundaries; simple read/status tool payloads remain tolerant by contract
13+
Constraint: Keep `@opencode-ai/plugin` at `1.14.48` and `zod` at `4.1.8`; this release changes no dependency compatibility boundary
14+
Rejected: Treat post-save artifact rendering failure as a total `flow_plan_start` failure | callers need to know the session was saved and only derived artifacts need repair
15+
Rejected: Make every tool schema strict | the strictness contract is scoped to planning payload boundaries and preserves tolerant simple tool behavior
16+
Rejected: Add a new workspace result-kind hierarchy in this release | the current structured `partial_success` response covers callers without widening the runtime action API
17+
Confidence: high
18+
Scope-risk: moderate
19+
Reversibility: clean
20+
Directive: Keep planning payload strictness scoped to documented adapter-facing boundaries, and keep post-persistence artifact failures distinguishable from unsaved mutation failures
21+
Tested: `bun test tests/config/tool-schemas.test.ts` (18 pass, 0 fail, 515 expect() calls); `bun run typecheck`; `bunx biome check tests/config/tool-schemas.test.ts`; `bun run check` (release gate passed: dependency contract OK with project/plugin/root `zod=4.1.8`, architecture seams OK, fresh surfaces OK, pack invariants OK for version `2.0.48`, bundle sanity OK, full suite 663 pass/0 fail, lint passed, bench smoke and bench gate passed); `bun run smoke:release` (passed for package `2.0.48`, wrote release-smoke evidence under `prompt-exports/release-smoke/`, real OpenCode CLI not invoked); RepoPrompt Oracle architect review of the execution-path strictness follow-up returned APPROVE
22+
Not-tested: Live OpenCode UI runtime interaction; live GitHub-hosted release workflow run for tag `v2.0.48` before push

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencode-plugin-flow",
3-
"version": "2.0.47",
3+
"version": "2.0.48",
44
"description": "Stateful planning and execution workflow plugin for OpenCode",
55
"type": "module",
66
"main": "dist/index.js",

src/adapters/opencode/tool-surface/schemas.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ export const FlowPlanStartArgsShape = {
8383
goal: z.string().trim().min(1).optional(),
8484
repoProfile: z.array(z.string().min(1)).optional(),
8585
};
86-
export const FlowPlanContextRecordArgsSchema = z.object(
87-
FlowPlanContextRecordArgsShape,
88-
);
89-
export const FlowPlanApplyArgsSchema = z.object(FlowPlanApplyArgsShape);
86+
export const FlowPlanContextRecordArgsSchema = RuntimePlanningContextArgsSchema;
87+
export const FlowPlanApplyArgsSchema = z
88+
.object(FlowPlanApplyArgsShape)
89+
.strict();
9090
export const FlowPlanApproveArgsShape = {
9191
featureIds: z.array(featureIdSchema).optional(),
9292
};

src/runtime/application/session-workspace-actions.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
type SessionWorkspaceResult,
1313
type SessionWorkspaceRuntimePort,
1414
} from "./session-engine";
15+
import type { SessionArtifactSyncFailure } from "./session-mutation-finalization";
1516
import { mergePlanningContext } from "./session-planning-context";
1617
import { detectStackAndStandardsProfile } from "./stack-standards-profile";
1718
import {
@@ -29,8 +30,17 @@ type PlannedSessionResult =
2930
| {
3031
status: "ok";
3132
session: Session;
33+
artifactSync?: SessionArtifactSyncFailure;
3234
};
3335

36+
function artifactSyncFailure(error: unknown): SessionArtifactSyncFailure {
37+
return {
38+
status: "failed",
39+
error:
40+
error instanceof Error && error.message ? error.message : String(error),
41+
};
42+
}
43+
3444
function buildPlannedSession(
3545
existing: Session | null,
3646
goal: string,
@@ -135,8 +145,16 @@ export const SESSION_WORKSPACE_ACTION_HANDLERS: SessionWorkspaceActionHandlerMap
135145
...detectedProfiles,
136146
}),
137147
);
138-
await runtime.syncSessionArtifacts(worktree, session);
139-
return { status: "ok", session };
148+
try {
149+
await runtime.syncSessionArtifacts(worktree, session);
150+
return { status: "ok", session };
151+
} catch (error) {
152+
return {
153+
status: "ok",
154+
session,
155+
artifactSync: artifactSyncFailure(error),
156+
};
157+
}
140158
},
141159
onSuccess: (value) =>
142160
value.status === "missing_goal"
@@ -146,8 +164,14 @@ export const SESSION_WORKSPACE_ACTION_HANDLERS: SessionWorkspaceActionHandlerMap
146164
nextCommand: value.nextCommand,
147165
}
148166
: {
149-
status: "ok",
167+
status: value.artifactSync ? "partial_success" : "ok",
150168
summary: `Planning session ready for goal: ${value.session.goal}`,
169+
...(value.artifactSync
170+
? {
171+
persistedMutation: true,
172+
artifactSync: value.artifactSync,
173+
}
174+
: {}),
151175
session: summarizeSession(value.session).session,
152176
},
153177
};

src/runtime/schema-plan.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,12 @@ export const PlanningContextSchema = z.object({
119119
export const PlanArgsSchema = PlanSchema.omit({
120120
goalMode: true,
121121
decompositionPolicy: true,
122-
}).extend({
123-
goalMode: GoalModeSchema.optional(),
124-
decompositionPolicy: DecompositionPolicySchema.optional(),
125-
});
122+
})
123+
.extend({
124+
goalMode: GoalModeSchema.optional(),
125+
decompositionPolicy: DecompositionPolicySchema.optional(),
126+
})
127+
.strict();
126128

127-
export const PlanningContextArgsSchema = PlanningContextSchema.partial();
129+
export const PlanningContextArgsSchema =
130+
PlanningContextSchema.partial().strict();

tests/config/tool-schemas.test.ts

Lines changed: 151 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Owns OpenCode tool arg-shape, zod/plugin alignment, and raw-schema
22
// contract coverage previously grouped in tests/config.test.ts.
3-
import { describe, expect, test } from "bun:test";
3+
import { afterEach, describe, expect, test } from "bun:test";
44
import { existsSync } from "node:fs";
55
import type { ToolContext as OpenCodeToolContext } from "@opencode-ai/plugin/tool";
66
import { z } from "zod";
@@ -22,6 +22,12 @@ import {
2222
WorkerResultArgsSchema,
2323
WorkerResultSchema,
2424
} from "../../src/runtime/schema";
25+
import {
26+
createTempDirRegistry,
27+
createTestTools,
28+
samplePlan,
29+
toolContext,
30+
} from "../runtime-test-helpers";
2531
import { asJson, getToolSchemas, projectPath, readJson } from "./helpers";
2632

2733
type IsRequired<T, K extends keyof T> =
@@ -36,6 +42,12 @@ type OpenCodeToolContextRequiredAssertions = [
3642
Assert<IsRequired<OpenCodeToolContext, "ask">>,
3743
];
3844

45+
const { makeTempDir, cleanupTempDirs } = createTempDirRegistry();
46+
47+
afterEach(() => {
48+
cleanupTempDirs();
49+
});
50+
3951
describe("tool schema config contracts", () => {
4052
test("installed OpenCode ToolContext keeps required execution fields", () => {
4153
const requiredAssertions: OpenCodeToolContextRequiredAssertions = [
@@ -225,10 +237,12 @@ describe("tool schema config contracts", () => {
225237

226238
test("runtime-owned OpenCode payload schemas document adapter/runtime parity intent", () => {
227239
const { schemas } = getToolSchemas();
228-
const RuntimeFlowPlanApplyArgsSchema = z.object({
229-
plan: PlanArgsSchema,
230-
planning: PlanningContextArgsSchema.optional(),
231-
});
240+
const RuntimeFlowPlanApplyArgsSchema = z
241+
.object({
242+
plan: PlanArgsSchema,
243+
planning: PlanningContextArgsSchema.optional(),
244+
})
245+
.strict();
232246
const runtimeOwnedTools = [
233247
"flow_plan_context_record",
234248
"flow_plan_apply",
@@ -269,7 +283,6 @@ describe("tool schema config contracts", () => {
269283
{ ...validPlanningContext, packageManager: "cargo" },
270284
false,
271285
);
272-
273286
const validPlanApply = {
274287
plan: {
275288
summary: "Implement a workflow.",
@@ -304,7 +317,6 @@ describe("tool schema config contracts", () => {
304317
},
305318
false,
306319
);
307-
308320
const validWorkerResult = {
309321
contractVersion: "1",
310322
status: "ok",
@@ -397,6 +409,138 @@ describe("tool schema config contracts", () => {
397409
);
398410
});
399411

412+
test("planning runtime parse schemas reject unknown adapter-facing keys narrowly", () => {
413+
const adapterSchema = (
414+
toolName: "flow_plan_apply" | "flow_plan_context_record",
415+
) =>
416+
FLOW_TOOL_PAYLOAD_SCHEMA_REGISTRY[toolName].argsSchema as {
417+
safeParse: (payload: unknown) => { success: boolean };
418+
};
419+
const validPlanningContext = {
420+
repoProfile: ["TypeScript"],
421+
packageManager: "bun",
422+
research: ["Use local schema contracts as source of truth."],
423+
};
424+
const validPlan = {
425+
summary: "Implement a workflow.",
426+
overview: "Create one feature.",
427+
features: [
428+
{
429+
id: "setup-runtime",
430+
title: "Create runtime helpers",
431+
summary: "Add runtime helpers.",
432+
fileTargets: ["src/runtime/session.ts"],
433+
verification: ["bun test"],
434+
},
435+
],
436+
};
437+
const validPlanApply = {
438+
plan: validPlan,
439+
planning: validPlanningContext,
440+
};
441+
442+
expect(
443+
PlanArgsSchema.safeParse({ ...validPlan, unexpected: true }).success,
444+
).toBe(false);
445+
expect(
446+
PlanningContextArgsSchema.safeParse({
447+
...validPlanningContext,
448+
unexpected: true,
449+
}).success,
450+
).toBe(false);
451+
expect(
452+
adapterSchema("flow_plan_apply").safeParse({
453+
...validPlanApply,
454+
unexpected: true,
455+
}).success,
456+
).toBe(false);
457+
expect(
458+
adapterSchema("flow_plan_apply").safeParse({
459+
...validPlanApply,
460+
plan: { ...validPlan, unexpected: true },
461+
}).success,
462+
).toBe(false);
463+
expect(
464+
adapterSchema("flow_plan_apply").safeParse({
465+
...validPlanApply,
466+
planning: { ...validPlanningContext, unexpected: true },
467+
}).success,
468+
).toBe(false);
469+
expect(
470+
adapterSchema("flow_plan_context_record").safeParse({
471+
...validPlanningContext,
472+
unexpected: true,
473+
}).success,
474+
).toBe(false);
475+
476+
const { schemas } = getToolSchemas();
477+
expect(schemas.flow_status.safeParse({ extra: true }).success).toBe(true);
478+
expect(schemas.flow_history.safeParse({ extra: true }).success).toBe(true);
479+
});
480+
481+
test("planning tools reject unknown keys through execute validation path", async () => {
482+
const tools = createTestTools();
483+
const worktree = makeTempDir();
484+
const validPlanningContext = {
485+
repoProfile: ["TypeScript"],
486+
packageManager: "bun",
487+
research: ["Use local schema contracts as source of truth."],
488+
};
489+
const validPlanApply = {
490+
plan: samplePlan(),
491+
planning: validPlanningContext,
492+
};
493+
const cases = [
494+
{
495+
name: "context top-level",
496+
toolName: "flow_plan_context_record",
497+
payload: { ...validPlanningContext, unexpectedPlanningKey: true },
498+
expectedFragments: ["unexpectedPlanningKey"],
499+
},
500+
{
501+
name: "apply outer",
502+
toolName: "flow_plan_apply",
503+
payload: { ...validPlanApply, unexpectedOuterKey: true },
504+
expectedFragments: ["unexpectedOuterKey"],
505+
},
506+
{
507+
name: "apply nested plan",
508+
toolName: "flow_plan_apply",
509+
payload: {
510+
...validPlanApply,
511+
plan: { ...validPlanApply.plan, unexpectedPlanKey: true },
512+
},
513+
expectedFragments: ["plan", "unexpectedPlanKey"],
514+
},
515+
{
516+
name: "apply nested planning",
517+
toolName: "flow_plan_apply",
518+
payload: {
519+
...validPlanApply,
520+
planning: { ...validPlanningContext, unexpectedPlanningKey: true },
521+
},
522+
expectedFragments: ["planning", "unexpectedPlanningKey"],
523+
},
524+
] as const;
525+
526+
for (const testCase of cases) {
527+
const response = await tools[testCase.toolName].execute(
528+
testCase.payload,
529+
toolContext(worktree),
530+
);
531+
const parsed = JSON.parse(response);
532+
const responseText = JSON.stringify(parsed);
533+
534+
expect(parsed.status, testCase.name).toBe("error");
535+
expect(String(parsed.summary), testCase.name).toContain(
536+
"Tool argument validation failed",
537+
);
538+
for (const fragment of testCase.expectedFragments) {
539+
expect(responseText, testCase.name).toContain(fragment);
540+
}
541+
}
542+
});
543+
400544
test("final review reviewContextPack raw schemas match runtime structured schema", () => {
401545
const { schemas } = getToolSchemas();
402546
const structuredReviewContextPack = {

0 commit comments

Comments
 (0)