opencode-plugin-flow 3.2.1 → 3.3.0
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 +26 -0
- package/README.md +1 -1
- package/dist/cli.js +12 -12
- package/dist/index.js +102 -42
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.3.0] - 2026-06-13
|
|
6
|
+
|
|
7
|
+
Audit findings must now survive refutation: a new audit rubric for the run lane, and adversarial review of findings reports
|
|
8
|
+
|
|
9
|
+
A real Flow audit session (a `goalMode: review` codebase audit, externally verified finding-by-finding) showed the gap this release closes: six of nine findings held up, and the three that did not all failed the same way — the auditor stopped at the suspicious call site without reading the mitigating path (the backend handler that already enforced one-to-one mapping, the workflow that validated before returning, the effect that already reset the stale state). Every one of those wrong findings cited real code accurately, and every gate passed, because the quality bar for audit deliverables was "findings cite code actually read" — citation accuracy, which wrong findings satisfy just fine.
|
|
10
|
+
|
|
11
|
+
The run lane gains `flow-run/references/audit-rubric.md`, which governs the findings themselves when a feature's deliverable is a findings report. Blocking-severity findings must survive the author's own refutation attempt (trace callers, cross the layer boundary, check surrounding lifecycle guards) and record a "guards checked" line naming the mitigating paths traced and why they fall short — no guards-checked line means the finding is downgraded to advisory. Hypothesized findings ("if the backend ever returns…") are capped at advisory defense-in-depth notes; the review rubric always banned hypothesizing, but that rule lived in the review lane and never reached the lane that writes audits. Reports also state the product's actual deployment model in the header and rate severity within it, so single-user desktop processes stop collecting shared-server severities.
|
|
12
|
+
|
|
13
|
+
The review lane stops citation-checking audit deliverables: `flow-review` now reviews a findings report by attempting to refute every blocking finding and recording a confirmed / refuted / uncertain verdict. A refuted finding — or a blocking finding with no guards-checked line — is a blocking finding against the report itself, so the decision is `needs_fix` and the report sheds it before shipping. The planning example for review-first decomposition raises its validation bar to match.
|
|
14
|
+
|
|
15
|
+
Skill content plus one sync registration (the new reference file is embedded and synced like the others). Bump the pin and restart twice so the re-synced skills are picked up.
|
|
16
|
+
|
|
17
|
+
Not-tested: a live end-to-end audit session under the new rubric; the rubric was validated against the verified audit transcript that motivated it (all three refuted findings fail its checks, all six confirmed ones pass).
|
|
18
|
+
|
|
19
|
+
## [3.2.2] - 2026-06-13
|
|
20
|
+
|
|
21
|
+
Critical fix: plugin failed to load on current OpenCode hosts (unbound SDK log method)
|
|
22
|
+
|
|
23
|
+
Every plugin load since the host SDK moved to generated client classes crashed at init with `Cannot read properties of undefined (reading '_client')`: plugin startup detached `app.log` from the host client (`const log = ctx.client?.app?.log`), and the SDK's `app.log` is a class method that reads `this._client`. OpenCode caught the throw, reported "failed to load plugin", and dropped Flow entirely — no tools, no config injection, no skill sync. This is the real cause behind `/flow-auto` reporting that `flow_status` was unavailable. The log call also passed the entry as top-level options where the SDK expects it under `body`; both are fixed — logging now goes through a bound, fail-silent wrapper posting `{ body: { service: "opencode-plugin-flow", level, message } }`.
|
|
24
|
+
|
|
25
|
+
The bug was invisible to every gate because all of them faked `app.log` as a plain function. The install smoke, bundle sanity, and the dist-load test now drive plugin init through an SDK-shaped fake client (a class whose `log` prototype method reads `this._client` and expects the `body` payload) and assert the startup log arrives through it.
|
|
26
|
+
|
|
27
|
+
Also fixed: bundle sanity and the dist-load test invoked the real plugin factory without sandboxing `$HOME`, so every `bun run check` silently synced dev-versioned skills, commands, and agents into the developer's real `~/.config/opencode` (the `version=0.0.0` markers some installs ended up with). Both now run against a temp home like the install smoke always did.
|
|
28
|
+
|
|
29
|
+
Upgrade by bumping the pin and restarting twice; the synced surface then re-syncs with real version markers.
|
|
30
|
+
|
|
5
31
|
## [3.2.1] - 2026-06-13
|
|
6
32
|
|
|
7
33
|
A missing Flow backend is now a stop condition in the skills, not a silent downgrade
|
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{homedir as ue}from"node:os";import{readdir as
|
|
3
|
-
`,B="flow-opencode-generated-skill"
|
|
4
|
-
`),
|
|
5
|
-
`);if(
|
|
6
|
-
`)){if(!
|
|
7
|
-
`)){let r=n.indexOf("=");if(r===-1)continue;
|
|
8
|
-
`)){let n=
|
|
2
|
+
import{homedir as ue}from"node:os";import{readdir as ie,readFile as ae,rm as u,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
|
+
`,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((h,Q)=>h.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
|
+
`);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
|
+
`)){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
|
+
`)){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(`
|
|
8
|
+
`)){let n=o.indexOf("=");if(n===-1)continue;t.set(o.slice(0,n),o.slice(n+1))}let a=t.get("plugin"),i=t.get("version");if(a!==z||!i)return null;let s=t.get("hash");return{plugin:a,version:i,hash:s?.startsWith("sha256:")?s.slice(7):null}}import{mkdir as He,readFile as D,rm as I,writeFile as Ce}from"node:fs/promises";import{dirname as Qe,join as V,sep as Je}from"node:path";function P(e){return`${["---",`description: ${JSON.stringify(e.description)}`,...e.agent?[`agent: ${JSON.stringify(e.agent)}`]:[],...e.subtask===void 0?[]:[`subtask: ${e.subtask}`],"---"].join(`
|
|
9
9
|
`)}
|
|
10
10
|
|
|
11
11
|
${e.template}
|
|
@@ -13,7 +13,7 @@ ${e.template}
|
|
|
13
13
|
`)}
|
|
14
14
|
|
|
15
15
|
${e.prompt}
|
|
16
|
-
`}function G(){return new Map(Object.entries(
|
|
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),h=await de(l);if(h==="foreign")continue;if(h==="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 u(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 h=await f(l);if(h!==null&&p(h)!==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 u(s,{force:!0}),await u(`${s}.backup`,{force:!0});let o=oe(s);if(o!==e)a.add(o)}await u(c(e,"SKILL.md"),{force:!0}),await u(c(e,v),{force:!0}),await u(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 u(i,{force:!0}),await u(s,{force:!0}),await u(`${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]
|
|
@@ -26,10 +26,10 @@ Commands:
|
|
|
26
26
|
|
|
27
27
|
Options:
|
|
28
28
|
--dry-run Show what would be removed without deleting anything
|
|
29
|
-
--help Show this message`;function
|
|
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 he(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
|
-
${q}`),1;let
|
|
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
|
|
35
|
+
${q}`),1}return await H({homeDir:process.env.HOME??ue(),dryRun:i,logger:M}),0}try{process.exitCode=await he(process.argv.slice(2))}catch(e){S(e instanceof Error?e.message:String(e)),process.exitCode=1}
|