opencode-plugin-flow 3.3.8 → 3.3.9
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 +23 -0
- package/README.md +6 -5
- package/dist/cli.js +9 -9
- package/dist/index.js +84 -69
- package/dist/index.js.map +19 -15
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.3.9] - 2026-06-14
|
|
6
|
+
|
|
7
|
+
Make context handoff inspectable without bloating session docs
|
|
8
|
+
|
|
9
|
+
Flow's context pack had become useful enough that operators needed a direct read-only inspection surface, but the first implementation risked pushing too much product judgment and project-map detail into the always-rendered session docs. That made the feature feel heavier than the workflow backend should be.
|
|
10
|
+
|
|
11
|
+
This release adds `flow_context`, an eighth canonical runtime tool that reads the active session without mutating `.flow/**`. It exposes the derived context pack in summary, feature, or full views, including workflow profile, context quality, readiness, traceability, diagnostics, and an optional lightweight project structure map for handoff and review. The existing `flow_status` surface keeps the compact operator answer, while `flow_context` owns deeper inspection.
|
|
12
|
+
|
|
13
|
+
The release deliberately keeps the simplification boundary tight. Context quality is advisory instead of a readiness blocker, project structure mapping is on-demand through `flow_context` instead of embedded in every rendered `.flow/**/docs/context.md`, and shared path matching now drives both traceability and project-map role classification. Broad planned targets such as `.`, `./`, `*`, and `**/*` warn for specificity without falsely reporting in-scope changed artifacts as scope drift. Sensitive, ignored, absolute, and parent-relative focus paths are redacted from the project map focus summary.
|
|
14
|
+
|
|
15
|
+
No commands, state paths, persisted session schema version, runtime transition semantics, completion gates, review policy defaults, package exports, synced file ownership rules, or public mutation payloads changed. The public tool surface expands from seven to eight tools by adding one read-only projection tool, and `ignore` is added as a small runtime dependency for `.gitignore`-aware project-map filtering.
|
|
16
|
+
|
|
17
|
+
Constraint: Add deeper context inspection without turning context quality into a hard workflow gate
|
|
18
|
+
Constraint: Keep persisted session docs lightweight and source-of-truth state unchanged
|
|
19
|
+
Rejected: Revert the context work wholesale | the useful inspection and traceability improvements are worth keeping after simplification
|
|
20
|
+
Rejected: Embed the project structure map in every `docs/context.md` render | on-demand inspection avoids routine filesystem walks, snapshot churn, and unnecessary privacy surface
|
|
21
|
+
Confidence: high
|
|
22
|
+
Scope-risk: medium
|
|
23
|
+
Reversibility: clean - the read-only tool, derived projections, docs, tests, and dependency can be reverted without migrating sessions
|
|
24
|
+
Directive: Use `flow_context` for deep handoff inspection; keep `flow_status` as the compact operator readiness answer
|
|
25
|
+
Tested: `bun run check` (typecheck, lint, build, 440-test suite, architecture seam enforcement, npm-tarball install smoke asserting the 3.3.9 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/render tests; `.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 a read-only tool surface, derived projections, rendered docs, skill guidance, docs, tests, dependency metadata, and release metadata only.
|
|
27
|
+
|
|
5
28
|
## [3.3.8] - 2026-06-14
|
|
6
29
|
|
|
7
30
|
Make workflow readiness answer for scope drift
|
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.
|
|
34
|
+
"plugin": ["opencode-plugin-flow@3.3.9"]
|
|
35
35
|
}
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -120,11 +120,12 @@ These five are the whole command surface since v3.1. The v2/v3.0 convenience com
|
|
|
120
120
|
|
|
121
121
|
### Tools
|
|
122
122
|
|
|
123
|
-
The plugin registers a small tool surface (
|
|
123
|
+
The plugin registers a small tool surface (8 tools). All `.flow/**` mutations go through the state-changing tools; `flow_context` is read-only:
|
|
124
124
|
|
|
125
125
|
| Tool | Purpose |
|
|
126
126
|
| --- | --- |
|
|
127
127
|
| `flow_status` | Session state, doctor-style readiness, and a computed suggested next step. |
|
|
128
|
+
| `flow_context` | Read-only context pack, quality score, traceability, and project structure map for planning/review handoff. |
|
|
128
129
|
| `flow_plan_save` | Create or update the draft plan (planning context plus features). |
|
|
129
130
|
| `flow_plan_approve` | Approve the plan, optionally restricted to a feature subset. |
|
|
130
131
|
| `flow_run_start` | Start the next runnable feature. |
|
|
@@ -132,7 +133,7 @@ The plugin registers a small tool surface (7 tools) that owns all `.flow/**` mut
|
|
|
132
133
|
| `flow_review_record` | Record a reviewer decision (`scope: feature` or `final`). |
|
|
133
134
|
| `flow_session` | Activate or close a session, list history, or show a stored session. |
|
|
134
135
|
|
|
135
|
-
These
|
|
136
|
+
These eight tools are the whole registered surface — the v2 tool-name redirect stubs that shipped in 3.0 were removed in v3.1 as scheduled. Existing v2 sessions still migrate seamlessly (the session schema is unchanged).
|
|
136
137
|
|
|
137
138
|
### Agents
|
|
138
139
|
|
|
@@ -152,7 +153,7 @@ Plus: atomic, locked, path-safe writes under `.flow/**`; schema validation of al
|
|
|
152
153
|
|
|
153
154
|
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
155
|
|
|
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
|
+
`/flow-status` also reports advisory `workflowReadiness`, `contextQuality`, `contextTraceability`, and `contextDiagnostics`. These fields show whether the session is ready for planning, execution, feature review, final review, or release; how strong the planned context is; which planned targets changed; which validation and review evidence exists; and where planned context is weak. `flow_context` exposes the same derived handoff in summary, feature, or full views, optionally with a lightweight project structure map. They do not replace review judgment, but they make context gaps visible before they become unverifiable completion claims.
|
|
156
157
|
|
|
157
158
|
## What Flow writes
|
|
158
159
|
|
|
@@ -161,7 +162,7 @@ Flow stores workflow state in the project/worktree where OpenCode is running:
|
|
|
161
162
|
```text
|
|
162
163
|
.flow/active/<session-id>/session.json # active session (source of truth)
|
|
163
164
|
.flow/active/<session-id>/docs/** # readable derived views
|
|
164
|
-
.flow/active/<session-id>/docs/context.md # derived context pack, readiness, traceability, and diagnostics
|
|
165
|
+
.flow/active/<session-id>/docs/context.md # derived context pack, readiness, quality, traceability, and diagnostics
|
|
165
166
|
.flow/stored/<session-id>/session.json # parked resumable sessions
|
|
166
167
|
.flow/completed/<session-id>-<timestamp>/** # closed session history
|
|
167
168
|
.flow/locks/
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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
|
|
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
|
|
4
|
-
`),a=t.flatMap((u,
|
|
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
|
|
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",N="SKILL.md.backup",A=w(".config","opencode","plugins","flow.js"),R=`// Managed by flow-opencode install/uninstall
|
|
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 y(e){let t=e.split(`
|
|
4
|
+
`),a=t.flatMap((u,H)=>u.startsWith($)?[H]:[]);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 V="opencode-plugin-flow",O="file=",B="=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(B);if(s===-1)continue;let o=i.slice(0,s),n=i.slice(s+B.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")!==V||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!==V||!i)return null;let s=t.get("hash");return{plugin:a,version:i,hash:s?.startsWith("sha256:")?s.slice(7):null}}import{mkdir as
|
|
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!==V||!i)return null;let s=t.get("hash");return{plugin:a,version:i,hash:s?.startsWith("sha256:")?s.slice(7):null}}import{mkdir as Qe,readFile as D,rm as I,writeFile as Me}from"node:fs/promises";import{dirname as He,join as z,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(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
|
|
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 Q({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 m(i)){if(r==="SKILL.md")continue;let l=M(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 M(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=M(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]
|
|
@@ -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 X(e){process.stdout.write(`${e}
|
|
30
30
|
`)}function S(e){process.stderr.write(`${e}
|
|
31
|
-
`)}async function ue(e){let t=[...e];if(t.length===0||t.includes("--help")||t.includes("-h"))return
|
|
31
|
+
`)}async function ue(e){let t=[...e];if(t.length===0||t.includes("--help")||t.includes("-h"))return X(F),0;let a=t.shift();if(a!=="uninstall")return S(`Unknown command: ${a}
|
|
32
32
|
|
|
33
33
|
${F}`),1;let i=!1;for(let s of t){if(s==="--dry-run"){i=!0;continue}return S(`Unknown argument: ${s}
|
|
34
34
|
|
|
35
|
-
${F}`),1}return await
|
|
35
|
+
${F}`),1}return await Q({homeDir:process.env.HOME??he(),dryRun:i,logger:X}),0}try{process.exitCode=await ue(process.argv.slice(2))}catch(e){S(e instanceof Error?e.message:String(e)),process.exitCode=1}
|