opencode-plugin-flow 3.3.1 → 3.3.3
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 +44 -0
- package/README.md +7 -6
- package/dist/cli.js +5 -5
- package/dist/index.js +56 -55
- package/dist/index.js.map +15 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.3.3] - 2026-06-13
|
|
6
|
+
|
|
7
|
+
Split the action hotspot without changing Flow's runtime contract
|
|
8
|
+
|
|
9
|
+
Flow's largest runtime application hotspot was `src/runtime/application/actions.ts`: one file owned mutation registries, workspace lifecycle handlers, read handlers, dispatch overloads, and local recovery helpers. That made the state boundary harder to review than it needed to be, even though the underlying action engine and public tool surface were already stable.
|
|
10
|
+
|
|
11
|
+
This release splits that hotspot by runtime responsibility. Mutation actions now live with mutation-only recovery helpers, workspace lifecycle actions own planning-session construction and activation/close responses, read actions stay isolated, and dispatch owns only command/query routing. The original `actions.ts` path remains as a compatibility facade, so adapters and tests keep importing the same public application surface.
|
|
12
|
+
|
|
13
|
+
No commands, tools, state paths, schemas, synced file ownership rules, runtime transitions, completion gates, review policy defaults, validation strictness, package exports, or public payloads changed.
|
|
14
|
+
|
|
15
|
+
Constraint: Improve maintainability by reducing the highest-risk application hotspot while preserving the existing Flow action facade
|
|
16
|
+
Constraint: Keep helpers local to their owning action boundary; do not create a new shared utility dumping ground
|
|
17
|
+
Rejected: Change public application imports or package exports as part of the split | this is an internal maintainability release, not a surface release
|
|
18
|
+
Rejected: Refactor `render.ts` or `skill-sync.ts` in the same patch | mixing hotspots would make review and rollback less precise
|
|
19
|
+
Confidence: high
|
|
20
|
+
Scope-risk: low
|
|
21
|
+
Reversibility: clean — the split is mechanical and the compatibility facade preserves callers
|
|
22
|
+
Directive: Future action work should add handlers in the owning action module and keep `actions.ts` as the stable facade unless a release explicitly changes the public runtime application surface
|
|
23
|
+
Tested: `bun run check` (typecheck, lint, build, full test suite, npm-tarball install smoke asserting the 3.3.3 README pin, bundle sanity); `bun run smoke:release` (packed candidate tarball install smoke and release evidence bundle); `bun run check:pack-invariants`; `bun run check:architecture-seams:enforce`
|
|
24
|
+
Not-tested: Live OpenCode UI restart against the release candidate; this release changes internal module boundaries and release metadata only.
|
|
25
|
+
|
|
26
|
+
## [3.3.2] - 2026-06-13
|
|
27
|
+
|
|
28
|
+
Document the real completion contract: Flow's public docs now match the runtime gates
|
|
29
|
+
|
|
30
|
+
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.
|
|
31
|
+
|
|
32
|
+
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`.
|
|
33
|
+
|
|
34
|
+
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.
|
|
35
|
+
|
|
36
|
+
No commands, tools, state paths, schemas, synced file ownership rules, runtime transitions, review policy defaults, or validation strictness changed.
|
|
37
|
+
|
|
38
|
+
Constraint: Preserve the public tool surface, session schema, runtime transitions, review policy defaults, and validation strictness while making the documented contract honest
|
|
39
|
+
Constraint: Keep documentation hand-authored; do not reintroduce generated docs or projection machinery for this anti-drift fix
|
|
40
|
+
Rejected: Relaxing the `featureReview` or final `finalReview` completion gates | that would be a behavior release, not a trust/clarity patch
|
|
41
|
+
Rejected: Refactoring runtime hotspots in the same release | large-file maintainability work would obscure this contract-only change
|
|
42
|
+
Confidence: high
|
|
43
|
+
Scope-risk: low
|
|
44
|
+
Reversibility: clean — docs, skills wording, one cross-area test, and release metadata only
|
|
45
|
+
Directive: When a runtime semantic invariant names a completion gate, current-facing docs and skills must name the same gate explicitly
|
|
46
|
+
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)
|
|
47
|
+
Not-tested: Live OpenCode UI restart against the release candidate; this release changes documentation, skills wording, release metadata, and anti-drift tests only.
|
|
48
|
+
|
|
5
49
|
## [3.3.1] - 2026-06-13
|
|
6
50
|
|
|
7
51
|
Finish the SDK log-contract fix: tool-surface logging now uses the same safe wrapper as plugin startup
|
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.3"]
|
|
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}
|