opencode-plugin-flow 3.3.7 → 3.3.8

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 CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [3.3.8] - 2026-06-14
6
+
7
+ Make workflow readiness answer for scope drift
8
+
9
+ Flow 3.3.7 made planned context visible after compaction, but the next useful step was to make that context operational. A reviewer could see weak planning context and changed artifacts outside the planned targets, yet the status surface still mostly exposed those facts as diagnostics. That made the evidence visible, but it did not give the operator a compact answer to the practical question: is this session ready to keep moving?
10
+
11
+ This release adds derived `workflowReadiness` and `contextTraceability` projections to `/flow-status` and to the rendered `.flow/active/<session-id>/docs/context.md` context pack. Readiness now reports whether the session is ready for planning, execution, feature review, final review, or release, and explains blocking context, validation, or review gaps. Traceability records planned targets, review scope, changed artifacts, validation commands, reviewer decisions, review payload status, and per-feature gaps without adding a persisted session field.
12
+
13
+ The follow-up review fix makes scope drift answerable instead of merely visible. Changed artifacts outside planned file targets or review scope now drive `workflowReadiness.state` to `blocked_by_context`, including both the session-level `changed_artifacts_outside_planned_context` diagnostic and the per-feature `feature_changed_artifacts_outside_scope` traceability gap. The skills now tell planning, execution, and review agents to act on readiness and traceability before claiming a feature, final review, or release is ready.
14
+
15
+ No commands, tools, state paths, persisted schemas, synced file ownership rules, runtime transitions, completion gates, review policy defaults, validation strictness, package exports, or public mutation payloads changed. The status payload gained advisory derived fields only, and `docs/context.md` remains a rendered projection of existing session state.
16
+
17
+ Constraint: Make readiness and traceability operational without adding a command, tool, schema field, or persisted readiness state
18
+ Constraint: Treat scope drift as blocking readiness while keeping judgment-heavy context quality in skills and review
19
+ Rejected: Persist a readiness ledger | readiness is derivable from the session snapshot and should not become another state source to reconcile
20
+ Rejected: Add `/flow-context` or a new `flow_context` tool | `/flow-status` and `docs/context.md` already own operator inspection without widening the public surface
21
+ Confidence: high
22
+ Scope-risk: medium
23
+ Reversibility: clean - the projection, status fields, rendered markdown, skill wording, and snapshots can be reverted without migrating sessions
24
+ Directive: Treat `workflowReadiness.state` as the first operator signal for whether context, validation, or review must be repaired before moving forward
25
+ Tested: `bun run check` (typecheck, lint, build, 435-test suite, architecture seam enforcement, npm-tarball install smoke asserting the 3.3.8 README pin, bundle sanity); `bun run smoke:release` (packed candidate tarball retained under release-smoke evidence, install smoke run against that tarball, manual live OpenCode checklist generated with the retained tarball install spec); `bun run report:architecture-metrics`; focused context-pack/status/render tests; `git diff --check`; `.agents/skills/flow-contribution-check/scripts/preflight.sh commit`; `.agents/skills/flow-contribution-check/scripts/preflight.sh push`
26
+ Not-tested: Live OpenCode UI restart against the release candidate; this release changes derived status/readiness projections, rendered session docs, skill guidance, docs, tests, and release metadata only.
27
+
5
28
  ## [3.3.7] - 2026-06-14
6
29
 
7
30
  Make the planned context visible enough to review
package/README.md CHANGED
@@ -31,7 +31,7 @@ Add Flow to the `plugin` array in your `opencode.json` (global `~/.config/openco
31
31
 
32
32
  ```json
33
33
  {
34
- "plugin": ["opencode-plugin-flow@3.3.7"]
34
+ "plugin": ["opencode-plugin-flow@3.3.8"]
35
35
  }
36
36
  ```
37
37
 
@@ -152,7 +152,7 @@ Plus: atomic, locked, path-safe writes under `.flow/**`; schema validation of al
152
152
 
153
153
  Everything judgment-shaped — how to decompose a plan, how deep to review, what counts as good evidence, when to stop and ask — lives in the skills, where you can read and override it.
154
154
 
155
- `/flow-status` also reports advisory `contextDiagnostics` when the planned context is weak, such as missing repo profile entries, empty feature file targets, or missing validation plans. These warnings do not replace review judgment, but they make context gaps visible before they become unverifiable completion claims.
155
+ `/flow-status` also reports advisory `workflowReadiness`, `contextTraceability`, and `contextDiagnostics`. These fields show whether the session is ready for planning, execution, feature review, final review, or release; which planned targets changed; which validation and review evidence exists; and where planned context is weak. They do not replace review judgment, but they make context gaps visible before they become unverifiable completion claims.
156
156
 
157
157
  ## What Flow writes
158
158
 
@@ -161,7 +161,7 @@ Flow stores workflow state in the project/worktree where OpenCode is running:
161
161
  ```text
162
162
  .flow/active/<session-id>/session.json # active session (source of truth)
163
163
  .flow/active/<session-id>/docs/** # readable derived views
164
- .flow/active/<session-id>/docs/context.md # derived context pack and diagnostics
164
+ .flow/active/<session-id>/docs/context.md # derived context pack, readiness, traceability, and diagnostics
165
165
  .flow/stored/<session-id>/session.json # parked resumable sessions
166
166
  .flow/completed/<session-id>-<timestamp>/** # closed session history
167
167
  .flow/locks/
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
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 Q={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:Q.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"),A=w(".config","opencode","agents"),v=".flow-skill-version",N="SKILL.md.backup",_=w(".config","opencode","plugins","flow.js"),R=`// Managed by flow-opencode install/uninstall
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 Q={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:Q.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",N="SKILL.md.backup",A=w(".config","opencode","plugins","flow.js"),R=`// Managed by flow-opencode install/uninstall
3
3
  `,E="flow-opencode-generated-skill",$=`<!-- ${E} `,K=new RegExp(`^<!-- ${E} 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 m(e){let t=e.split(`
4
4
  `),a=t.flatMap((u,J)=>u.startsWith($)?[J]:[]);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 V="opencode-plugin-flow",O="file=",B="=sha256:";function y(e){let t=new Map;for(let a 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 L(){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=z(e.root,`${i}.md`),o=z(e.root,`.${i}.flow-version`),n=await U(o),r=n===null?null:b(n,e.kind,i);if(r===null)continue;let d=await U(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 U(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 M({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 C({homeDir:e,dryRun:t,logger:a,kind:"command",root:c(e,g),files:G(),removed:i.removedCommands,keptUserEdited:i.keptUserEditedCommands}),await C({homeDir:e,dryRun:t,logger:a,kind:"agent",root:c(e,A),files:L(),removed:i.removedAgents,keptUserEdited:i.keptUserEditedAgents});let n=c(e,_),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 y(i)){if(r==="SKILL.md")continue;let l=X(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 m(a).kind==="valid_generated"?"pristine":"user_edited"}if(a===null)return"foreign";let o=m(a);if(o.kind==="valid_generated")return"pristine";if(o.kind==="invalid_generated")return"user_edited";return"foreign"}function X(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 y(t).keys()){let s=X(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,N),{force:!0});for(let i of[...a].sort((s,o)=>o.length-s.length))await q(i);await q(e)}async function q(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 C(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 q(e.root)}var F=`opencode-plugin-flow — Flow plugin lifecycle commands
16
+ `}function G(){return new Map(Object.entries(W).map(([e,t])=>[e,P(t)]))}function L(){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=z(e.root,`${i}.md`),o=z(e.root,`.${i}.flow-version`),n=await U(o),r=n===null?null:b(n,e.kind,i);if(r===null)continue;let d=await U(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 U(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 M({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 C({homeDir:e,dryRun:t,logger:a,kind:"command",root:c(e,g),files:G(),removed:i.removedCommands,keptUserEdited:i.keptUserEditedCommands}),await C({homeDir:e,dryRun:t,logger:a,kind:"agent",root:c(e,_),files:L(),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 y(i)){if(r==="SKILL.md")continue;let l=X(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 m(a).kind==="valid_generated"?"pristine":"user_edited"}if(a===null)return"foreign";let o=m(a);if(o.kind==="valid_generated")return"pristine";if(o.kind==="invalid_generated")return"user_edited";return"foreign"}function X(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 y(t).keys()){let s=X(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,N),{force:!0});for(let i of[...a].sort((s,o)=>o.length-s.length))await q(i);await q(e)}async function q(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 C(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 q(e.root)}var F=`opencode-plugin-flow — Flow plugin lifecycle commands
17
17
 
18
18
  Usage:
19
19
  bunx opencode-plugin-flow uninstall [--dry-run]