opencode-plugin-flow 3.3.0 → 3.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +7 -6
- package/dist/cli.js +5 -5
- package/dist/index.js +45 -44
- package/dist/index.js.map +6 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.3.2] - 2026-06-13
|
|
6
|
+
|
|
7
|
+
Document the real completion contract: Flow's public docs now match the runtime gates
|
|
8
|
+
|
|
9
|
+
The skills-first v3 docs still described the runtime as enforcing four hard invariants, but that wording hid part of the actual completion contract: successful worker results also need passing validation entries, the correct `validationScope`, a passing `featureReview`, and a matching passing `finalReview` on the final completion path. The semantic invariant registry already said the fuller truth; the README, maintainer contract, development guide, contributor map, and skill text now say it too.
|
|
10
|
+
|
|
11
|
+
This release separates session/state invariants from completion payload gates. State invariants remain the small durable set: no evidence-free completion, no completed close with unfinished target work, no approved-plan mutation without reset, and strict review governance requiring a recorded approved reviewer decision. Completion payload gates are documented as binary runtime checks rather than judgment rubrics: targeted vs broad validation scope, passing `featureReview`, and final-path `finalReview` depth matching `deliveryPolicy.finalReviewPolicy`.
|
|
12
|
+
|
|
13
|
+
The Flow skill, validation rubric, and recovery playbook now use the same language operators see from structured recovery errors. A new cross-area documentation guard reads the semantic invariant registry and verifies the current-facing docs and skill references mention validation evidence, validation scope, `featureReview`, final `finalReview`, strict reviewer decisions, and unfinished-feature close blocking.
|
|
14
|
+
|
|
15
|
+
No commands, tools, state paths, schemas, synced file ownership rules, runtime transitions, review policy defaults, or validation strictness changed.
|
|
16
|
+
|
|
17
|
+
Constraint: Preserve the public tool surface, session schema, runtime transitions, review policy defaults, and validation strictness while making the documented contract honest
|
|
18
|
+
Constraint: Keep documentation hand-authored; do not reintroduce generated docs or projection machinery for this anti-drift fix
|
|
19
|
+
Rejected: Relaxing the `featureReview` or final `finalReview` completion gates | that would be a behavior release, not a trust/clarity patch
|
|
20
|
+
Rejected: Refactoring runtime hotspots in the same release | large-file maintainability work would obscure this contract-only change
|
|
21
|
+
Confidence: high
|
|
22
|
+
Scope-risk: low
|
|
23
|
+
Reversibility: clean — docs, skills wording, one cross-area test, and release metadata only
|
|
24
|
+
Directive: When a runtime semantic invariant names a completion gate, current-facing docs and skills must name the same gate explicitly
|
|
25
|
+
Tested: `bun run check` (typecheck, lint, build, full test suite including the new completion-contract documentation guard, npm-tarball install smoke asserting the 3.3.2 README pin, bundle sanity); `bun run smoke:release` (packed candidate tarball install smoke and release evidence bundle)
|
|
26
|
+
Not-tested: Live OpenCode UI restart against the release candidate; this release changes documentation, skills wording, release metadata, and anti-drift tests only.
|
|
27
|
+
|
|
28
|
+
## [3.3.1] - 2026-06-13
|
|
29
|
+
|
|
30
|
+
Finish the SDK log-contract fix: tool-surface logging now uses the same safe wrapper as plugin startup
|
|
31
|
+
|
|
32
|
+
The 3.2.2 release fixed the startup log call that could crash plugin init on generated OpenCode SDK clients, but a second log path remained in `createTools`: it called `ctx.client.app.log` directly with top-level `{ level, message }` fields. That path did not currently crash because it was not detached, but it still violated the host contract (`app.log` expects `{ body: ... }`) and could record an `undefined` entry or become the next plugin-load footgun if the SDK tightens validation.
|
|
33
|
+
|
|
34
|
+
Flow now has one adapter logging helper. Both plugin startup and tool-surface creation call through `createFlowLog`, which keeps host logging best-effort, invokes the SDK method through the app object, and always posts `{ body: { service: "opencode-plugin-flow", level, message } }`. The duplicate ad-hoc logger in `tools.ts` is gone.
|
|
35
|
+
|
|
36
|
+
The SDK-shaped regression tests now assert the tool-surface log entry itself, not only the startup entry, and reject `undefined` log bodies. The distributed bundle smoke carries the same assertion so the packaged path stays covered.
|
|
37
|
+
|
|
38
|
+
No commands, tools, state paths, schemas, skills, or user-facing workflow behavior changed.
|
|
39
|
+
|
|
40
|
+
Tested: `bun run check`; `bun run smoke:release`.
|
|
41
|
+
|
|
42
|
+
Not-tested: Live OpenCode UI restart against the release candidate; covered by SDK-shaped unit/dist smoke and packed-tarball install smoke.
|
|
43
|
+
|
|
5
44
|
## [3.3.0] - 2026-06-13
|
|
6
45
|
|
|
7
46
|
Audit findings must now survive refutation: a new audit rubric for the run lane, and adversarial review of findings reports
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Add Flow to the `plugin` array in your `opencode.json` (global `~/.config/openco
|
|
|
29
29
|
|
|
30
30
|
```json
|
|
31
31
|
{
|
|
32
|
-
"plugin": ["opencode-plugin-flow@3.3.
|
|
32
|
+
"plugin": ["opencode-plugin-flow@3.3.2"]
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
35
|
|
|
@@ -138,12 +138,13 @@ Flow ships one dedicated subagent: `flow-reviewer`, a read-only reviewer used fo
|
|
|
138
138
|
|
|
139
139
|
## What the plugin enforces vs. what skills guide
|
|
140
140
|
|
|
141
|
-
The plugin code enforces only
|
|
141
|
+
The plugin code enforces only binary runtime gates and persistence safety:
|
|
142
142
|
|
|
143
|
-
1.
|
|
144
|
-
2.
|
|
145
|
-
3.
|
|
146
|
-
4.
|
|
143
|
+
1. Completion payloads must carry recorded passing validation evidence. Non-final features require `validationScope: "targeted"`; the feature that completes the session requires `validationScope: "broad"`.
|
|
144
|
+
2. Completion payloads must carry a passing `featureReview`. The final completion payload must also carry a passing `finalReview` whose `reviewDepth` matches the plan's `deliveryPolicy.finalReviewPolicy`.
|
|
145
|
+
3. A session cannot close as `completed` with unfinished target work.
|
|
146
|
+
4. An approved plan cannot be mutated without an explicit reset.
|
|
147
|
+
5. If the session's review policy is strict, a recorded approved reviewer decision is required before completion.
|
|
147
148
|
|
|
148
149
|
Plus: atomic, locked, path-safe writes under `.flow/**`; schema validation of all tool payloads; and the compaction hook that keeps Flow state intact when OpenCode compacts a long session.
|
|
149
150
|
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{homedir as
|
|
2
|
+
import{homedir as he}from"node:os";import{readdir as ie,readFile as ae,rm as h,rmdir as se}from"node:fs/promises";import{dirname as oe,join as c,normalize as ne,sep as re}from"node:path";var J={fast:"low",balanced:"medium",deep:"high"},j={"flow-reviewer":{mode:"all",description:"Review Flow work read-only and record a reviewer decision.",prompt:"You are the Flow reviewer. Load the `flow-review` skill, review the requested work read-only, then record your decision with flow_review_record.",reasoningEffort:J.deep,permission:{edit:"deny",bash:"deny",task:{"*":"deny"},"flow_*":"deny",flow_status:"allow",flow_review_record:"allow"}}},W={"flow-plan":{description:"Create, update, or approve a Flow plan",template:"Load the `flow-plan` skill and plan: $ARGUMENTS"},"flow-run":{description:"Run one approved Flow feature",template:"Load the `flow-run` skill and execute the next approved Flow feature. $ARGUMENTS"},"flow-auto":{description:"Drive the Flow loop autonomously until completion or a real blocker",template:"Load the `flow` skill and drive the Flow loop (status, plan, run, review) until completion or a real blocker: $ARGUMENTS"},"flow-status":{description:"Inspect the active Flow session and workspace readiness",template:"Call flow_status (detailed) and report session state, readiness checks, and the suggested next step."},"flow-review":{description:"Run a read-only Flow review with a fresh context",agent:"flow-reviewer",subtask:!0,template:"Load the `flow-review` skill and review: $ARGUMENTS"}},k=["flow-doctor","flow-history","flow-reset","flow-session"];import{createHash as Z}from"node:crypto";import{join as w}from"node:path";var x=w(".config","opencode","skills"),g=w(".config","opencode","commands"),_=w(".config","opencode","agents"),v=".flow-skill-version",E="SKILL.md.backup",A=w(".config","opencode","plugins","flow.js"),R=`// Managed by flow-opencode install/uninstall
|
|
3
3
|
`,B="flow-opencode-generated-skill",$=`<!-- ${B} `,K=new RegExp(`^<!-- ${B} name=([a-z0-9]+(?:-[a-z0-9]+)*) version=([0-9]+) hash=sha256:([a-f0-9]{64}) -->$`,"u");function p(e){return Z("sha256").update(e,"utf8").digest("hex")}function y(e){let t=e.split(`
|
|
4
|
-
`),a=t.flatMap((
|
|
4
|
+
`),a=t.flatMap((u,Q)=>u.startsWith($)?[Q]:[]);if(a.length===0)return{kind:"not_generated"};if(a.length>1)return{kind:"invalid_generated",reason:"duplicate_marker"};let i=a[0];if(i===void 0)return{kind:"not_generated"};let s=t[i];if(s===void 0)return{kind:"invalid_generated",reason:"malformed_marker"};let o=s.match(K);if(!o)return{kind:"invalid_generated",reason:"malformed_marker"};let[,n,r,d]=o;if(n===void 0||r===void 0||d===void 0)return{kind:"invalid_generated",reason:"malformed_marker"};let l=[...t.slice(0,i),...t.slice(i+1)].join(`
|
|
5
5
|
`);if(p(l)!==d)return{kind:"invalid_generated",reason:"hash_mismatch"};return{kind:"valid_generated",marker:{name:n,version:r,hash:d}}}var z="opencode-plugin-flow",O="file=",N="=sha256:";function m(e){let t=new Map;for(let a of e.split(`
|
|
6
6
|
`)){if(!a.startsWith(O))continue;let i=a.slice(O.length),s=i.lastIndexOf(N);if(s===-1)continue;let o=i.slice(0,s),n=i.slice(s+N.length);if(o.length>0&&/^[a-f0-9]{64}$/.test(n))t.set(o,n)}return t}function b(e,t,a){let i=new Map;for(let n of e.split(`
|
|
7
7
|
`)){let r=n.indexOf("=");if(r===-1)continue;i.set(n.slice(0,r),n.slice(r+1))}let s=i.get("version"),o=i.get("hash");if(i.get("plugin")!==z||i.get("kind")!==t||i.get("name")!==a||!s||!o?.startsWith("sha256:"))return null;return{kind:t,name:a,version:s,hash:o.slice(7)}}function T(e){let t=new Map;for(let o of e.split(`
|
|
@@ -13,7 +13,7 @@ ${e.template}
|
|
|
13
13
|
`)}
|
|
14
14
|
|
|
15
15
|
${e.prompt}
|
|
16
|
-
`}function G(){return new Map(Object.entries(W).map(([e,t])=>[e,P(t)]))}function U(){return new Map(Object.entries(j).map(([e,t])=>[e,ee(t)]))}async function Y(e){let t=[],a=[];for(let i of e.names){let s=V(e.root,`${i}.md`),o=V(e.root,`.${i}.flow-version`),n=await L(o),r=n===null?null:b(n,e.kind,i);if(r===null)continue;let d=await L(s);if(d!==null&&p(d)!==r.hash){a.push(s);continue}if(!e.dryRun)await I(s,{force:!0}),await I(o,{force:!0}),await I(`${s}.backup`,{force:!0});t.push(s)}return{removed:t,keptUserEdited:a}}async function L(e){try{return await D(e,"utf8")}catch(t){if(t.code==="ENOENT")return null;throw t}}function te(e){if(!e)return[];let t=["permission:"];for(let[a,i]of Object.entries(e)){if(typeof i==="string"){t.push(` ${JSON.stringify(a)}: ${JSON.stringify(i)}`);continue}if(i&&typeof i==="object"){t.push(` ${JSON.stringify(a)}:`);for(let[s,o]of Object.entries(i))t.push(` ${JSON.stringify(s)}: ${JSON.stringify(o)}`)}}return t}async function H({homeDir:e,dryRun:t=!1,logger:a}){let i={removedSkills:[],keptUserEditedSkills:[],removedCommands:[],keptUserEditedCommands:[],removedAgents:[],keptUserEditedAgents:[],removedPreNpmPlugin:null,keptForeignPreNpmPlugin:null},s=c(e,x);for(let d of await le(s)){if(d!=="flow"&&!d.startsWith("flow-"))continue;let l=c(s,d),
|
|
16
|
+
`}function G(){return new Map(Object.entries(W).map(([e,t])=>[e,P(t)]))}function U(){return new Map(Object.entries(j).map(([e,t])=>[e,ee(t)]))}async function Y(e){let t=[],a=[];for(let i of e.names){let s=V(e.root,`${i}.md`),o=V(e.root,`.${i}.flow-version`),n=await L(o),r=n===null?null:b(n,e.kind,i);if(r===null)continue;let d=await L(s);if(d!==null&&p(d)!==r.hash){a.push(s);continue}if(!e.dryRun)await I(s,{force:!0}),await I(o,{force:!0}),await I(`${s}.backup`,{force:!0});t.push(s)}return{removed:t,keptUserEdited:a}}async function L(e){try{return await D(e,"utf8")}catch(t){if(t.code==="ENOENT")return null;throw t}}function te(e){if(!e)return[];let t=["permission:"];for(let[a,i]of Object.entries(e)){if(typeof i==="string"){t.push(` ${JSON.stringify(a)}: ${JSON.stringify(i)}`);continue}if(i&&typeof i==="object"){t.push(` ${JSON.stringify(a)}:`);for(let[s,o]of Object.entries(i))t.push(` ${JSON.stringify(s)}: ${JSON.stringify(o)}`)}}return t}async function H({homeDir:e,dryRun:t=!1,logger:a}){let i={removedSkills:[],keptUserEditedSkills:[],removedCommands:[],keptUserEditedCommands:[],removedAgents:[],keptUserEditedAgents:[],removedPreNpmPlugin:null,keptForeignPreNpmPlugin:null},s=c(e,x);for(let d of await le(s)){if(d!=="flow"&&!d.startsWith("flow-"))continue;let l=c(s,d),u=await de(l);if(u==="foreign")continue;if(u==="user_edited"){i.keptUserEditedSkills.push(l),a?.(`Kept user-edited Flow skill at ${l}; remove it manually if it is no longer needed.`);continue}if(!t)await ce(l);i.removedSkills.push(l),a?.(`${t?"Would remove":"Removed"} Flow skill at ${l}.`)}let o=await Y({kind:"command",root:c(e,g),names:k,dryRun:t});for(let d of o.removed)i.removedCommands.push(d),a?.(`${t?"Would remove":"Removed"} retired Flow command at ${d}.`);for(let d of o.keptUserEdited)i.keptUserEditedCommands.push(d),a?.(`Kept user-edited Flow command at ${d}; remove it manually if it is no longer needed.`);await X({homeDir:e,dryRun:t,logger:a,kind:"command",root:c(e,g),files:G(),removed:i.removedCommands,keptUserEdited:i.keptUserEditedCommands}),await X({homeDir:e,dryRun:t,logger:a,kind:"agent",root:c(e,_),files:U(),removed:i.removedAgents,keptUserEdited:i.keptUserEditedAgents});let n=c(e,A),r=await f(n);if(r!==null)if(r.startsWith(R)){if(!t)await h(n,{force:!0});i.removedPreNpmPlugin=n,a?.(`${t?"Would remove":"Removed"} pre-npm Flow plugin copy at ${n}.`)}else i.keptForeignPreNpmPlugin=n,a?.(`Kept ${n}: it is not managed by Flow. Remove it manually if it is a stale Flow copy.`);return a?.('Finally, remove "opencode-plugin-flow" from the plugin array in opencode.json and restart OpenCode.'),i}async function de(e){let t=c(e,"SKILL.md"),a=await f(t),i=await f(c(e,v)),s=i===null?null:T(i);if(s!==null&&i!==null){for(let[r,d]of m(i)){if(r==="SKILL.md")continue;let l=C(e,r);if(l===null)continue;let u=await f(l);if(u!==null&&p(u)!==d)return"user_edited"}if(a===null)return"pristine";if(s.hash!==null&&p(a)===s.hash)return"pristine";return y(a).kind==="valid_generated"?"pristine":"user_edited"}if(a===null)return"foreign";let o=y(a);if(o.kind==="valid_generated")return"pristine";if(o.kind==="invalid_generated")return"user_edited";return"foreign"}function C(e,t){let a=ne(c(e,...t.split("/")));if(a!==e&&a.startsWith(`${e}${re}`))return a;return null}async function ce(e){let t=await f(c(e,v)),a=new Set;if(t!==null)for(let i of m(t).keys()){let s=C(e,i);if(s===null)continue;await h(s,{force:!0}),await h(`${s}.backup`,{force:!0});let o=oe(s);if(o!==e)a.add(o)}await h(c(e,"SKILL.md"),{force:!0}),await h(c(e,v),{force:!0}),await h(c(e,E),{force:!0});for(let i of[...a].sort((s,o)=>o.length-s.length))await F(i);await F(e)}async function F(e){try{await se(e)}catch(t){let a=t.code;if(a!=="ENOENT"&&a!=="ENOTEMPTY")throw t}}async function le(e){try{return(await ie(e,{withFileTypes:!0})).filter((a)=>a.isDirectory()).map((a)=>a.name).sort()}catch(t){if(t.code==="ENOENT")return[];throw t}}async function f(e){try{return await ae(e,"utf8")}catch(t){if(t.code==="ENOENT")return null;throw t}}async function X(e){for(let[t,a]of e.files){let i=c(e.root,`${t}.md`),s=c(e.root,`.${t}.flow-version`),o=await f(i),n=await f(s),r=n===null?null:b(n,e.kind,t);if(o===null&&r===null)continue;if(!(r!==null||o===a))continue;if(o!==null&&r!==null&&p(o)!==r.hash){e.keptUserEdited.push(i),e.logger?.(`Kept user-edited Flow ${e.kind} at ${i}; remove it manually if it is no longer needed.`);continue}if(!e.dryRun)await h(i,{force:!0}),await h(s,{force:!0}),await h(`${i}.backup`,{force:!0});e.removed.push(i),e.logger?.(`${e.dryRun?"Would remove":"Removed"} Flow ${e.kind} at ${i}.`)}if(!e.dryRun)await F(e.root)}var q=`opencode-plugin-flow — Flow plugin lifecycle commands
|
|
17
17
|
|
|
18
18
|
Usage:
|
|
19
19
|
bunx opencode-plugin-flow uninstall [--dry-run]
|
|
@@ -28,8 +28,8 @@ Options:
|
|
|
28
28
|
--dry-run Show what would be removed without deleting anything
|
|
29
29
|
--help Show this message`;function M(e){process.stdout.write(`${e}
|
|
30
30
|
`)}function S(e){process.stderr.write(`${e}
|
|
31
|
-
`)}async function
|
|
31
|
+
`)}async function ue(e){let t=[...e];if(t.length===0||t.includes("--help")||t.includes("-h"))return M(q),0;let a=t.shift();if(a!=="uninstall")return S(`Unknown command: ${a}
|
|
32
32
|
|
|
33
33
|
${q}`),1;let i=!1;for(let s of t){if(s==="--dry-run"){i=!0;continue}return S(`Unknown argument: ${s}
|
|
34
34
|
|
|
35
|
-
${q}`),1}return await H({homeDir:process.env.HOME??
|
|
35
|
+
${q}`),1}return await H({homeDir:process.env.HOME??he(),dryRun:i,logger:M}),0}try{process.exitCode=await ue(process.argv.slice(2))}catch(e){S(e instanceof Error?e.message:String(e)),process.exitCode=1}
|