opencode-plugin-flow 3.0.0 → 3.1.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 +24 -0
- package/README.md +16 -16
- package/dist/cli.js +20 -11
- package/dist/index.js +58 -48
- package/dist/index.js.map +11 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.1.0] - 2026-06-12
|
|
6
|
+
|
|
7
|
+
Only the v3 surface remains: five commands, seven tools, no v2 leftovers
|
|
8
|
+
|
|
9
|
+
The command surface trims from nine to five: `/flow-auto`, `/flow-plan`, `/flow-run`, `/flow-review`, and `/flow-status`. The retired four (`/flow-doctor`, `/flow-history`, `/flow-session`, `/flow-reset`) were thin wrappers over a single tool call that work just as well as a plain request — "show the flow history", "close this session as completed", "reset feature X" — and `/flow-doctor` was the same `flow_status` view as `/flow-status`. Startup sync now also cleans up: Flow-owned files that a 3.0.x install synced for the retired names are removed from `~/.config/opencode/commands/` (user-edited copies are kept untouched), and `bunx opencode-plugin-flow uninstall` does the same.
|
|
10
|
+
|
|
11
|
+
The 15 v2 tool-name redirect stubs (`flow_doctor`, `flow_run_complete_feature`, `flow_session_close`, …) are removed as scheduled — the seven canonical tools are now the entire registered surface. The session schema is still v1: v2-created `.flow/**` sessions continue to resume unchanged.
|
|
12
|
+
|
|
13
|
+
Runtime next-step hints now reference things that exist: where a suggestion used to name a retired command (`/flow-reset feature <id>`, `/flow-history`, `/flow-session activate <id>`), it now names the tool call the skills teach (`flow_feature_complete reset <id>`, `flow_session history`, `flow_session activate <id>`). The five surviving commands are still suggested as slash commands.
|
|
14
|
+
|
|
15
|
+
Restart OpenCode once after upgrading so the retired command files are cleaned up and the trimmed surface is discovered.
|
|
16
|
+
|
|
17
|
+
Not-tested: Live OpenCode UI slash-command discovery after a real npm upgrade (verified via the packed-tarball install smoke).
|
|
18
|
+
|
|
19
|
+
## [3.0.1] - 2026-06-12
|
|
20
|
+
|
|
21
|
+
Slash commands and the reviewer agent are now synced as real files so OpenCode discovers them
|
|
22
|
+
|
|
23
|
+
In 3.0.0 the nine `/flow-*` commands and the `flow-reviewer` agent were injected only through the plugin config hook, and OpenCode did not surface them as slash commands. Plugin startup now also writes them to OpenCode's normal discovery paths: thin command markdown files to `~/.config/opencode/commands/<name>.md` and the reviewer agent to `~/.config/opencode/agents/flow-reviewer.md`, rendered from the same definitions the config hook injects so the two surfaces cannot drift. Restart OpenCode once after upgrading so the new files are discovered.
|
|
24
|
+
|
|
25
|
+
The sync follows the same ownership rules as skills: each file gets a sidecar `.{name}.flow-version` marker (plugin version plus content sha256), files without a Flow marker are never touched, and a user-edited Flow-owned file is backed up next to itself before an update replaces it. `bunx opencode-plugin-flow uninstall` removes the Flow-owned command/agent files (keeping user-edited ones), and `/flow-doctor` warns when the synced command/agent surface is missing or stale.
|
|
26
|
+
|
|
27
|
+
Not-tested: Live OpenCode UI slash-command discovery after a real npm upgrade (verified via the packed-tarball install smoke).
|
|
28
|
+
|
|
5
29
|
## [3.0.0] - 2026-06-12
|
|
6
30
|
|
|
7
31
|
The skills-first inversion: skills carry the workflow, the plugin shrinks to a state backend
|
package/README.md
CHANGED
|
@@ -35,18 +35,20 @@ Add Flow to the `plugin` array in your `opencode.json` (global `~/.config/openco
|
|
|
35
35
|
|
|
36
36
|
OpenCode installs the package from npm on startup. Pin the major version you install (currently `@3`) so restarts pick up fixes without crossing a breaking release.
|
|
37
37
|
|
|
38
|
-
On startup the plugin syncs its global skills into
|
|
38
|
+
On startup the plugin syncs its global skills, commands, and review agent into OpenCode's normal discovery paths:
|
|
39
39
|
|
|
40
40
|
```text
|
|
41
|
-
flow/SKILL.md #
|
|
42
|
-
flow-plan/SKILL.md # decomposition heuristics
|
|
43
|
-
flow-run/SKILL.md #
|
|
44
|
-
flow-review/SKILL.md # review
|
|
41
|
+
~/.config/opencode/skills/flow/SKILL.md # driving loop
|
|
42
|
+
~/.config/opencode/skills/flow-plan/SKILL.md # decomposition heuristics
|
|
43
|
+
~/.config/opencode/skills/flow-run/SKILL.md # validation discipline
|
|
44
|
+
~/.config/opencode/skills/flow-review/SKILL.md # review rubric
|
|
45
|
+
~/.config/opencode/commands/flow-auto.md # slash command pointers
|
|
46
|
+
~/.config/opencode/agents/flow-reviewer.md # read-only review agent
|
|
45
47
|
```
|
|
46
48
|
|
|
47
|
-
**Restart OpenCode once after the first install or after an update** so freshly synced skills are discovered.
|
|
49
|
+
**Restart OpenCode once after the first install or after an update** so freshly synced skills, commands, and agents are discovered.
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
Sync is ownership-aware: each Flow-owned skill folder carries a `.flow-skill-version` marker with a sha256 line per shipped file, and each synced command/agent file has a sidecar `.flow-version` marker. Folders or files without Flow markers are never touched, and if you edit a Flow-owned file by hand the previous content is backed up next to it before an update replaces it.
|
|
50
52
|
|
|
51
53
|
### Per-project skill overrides
|
|
52
54
|
|
|
@@ -68,7 +70,7 @@ Releases before 2.1.0 installed a bundled plugin file at `~/.config/opencode/plu
|
|
|
68
70
|
bunx opencode-plugin-flow uninstall
|
|
69
71
|
```
|
|
70
72
|
|
|
71
|
-
This removes the Flow-owned global skill folders
|
|
73
|
+
This removes the Flow-owned global skill folders, command files, agent files, and a pre-npm `flow.js` copy if one exists, then reminds you to remove `"opencode-plugin-flow"` from the `plugin` array in `opencode.json`. Files you created yourself are never deleted. Use `--dry-run` to preview.
|
|
72
74
|
|
|
73
75
|
## Skills, commands, and tools
|
|
74
76
|
|
|
@@ -85,7 +87,7 @@ Deeper methodology (worked plan examples, validation and review rubrics) lives i
|
|
|
85
87
|
|
|
86
88
|
### Commands
|
|
87
89
|
|
|
88
|
-
Commands are thin pointers into the skills — the skill content is the real instruction surface.
|
|
90
|
+
Commands are thin pointers into the skills — the skill content is the real instruction surface.
|
|
89
91
|
|
|
90
92
|
| Command | Use it when... |
|
|
91
93
|
| --- | --- |
|
|
@@ -94,10 +96,8 @@ Commands are thin pointers into the skills — the skill content is the real ins
|
|
|
94
96
|
| `/flow-run [feature-id]` | You want to execute exactly one approved feature. |
|
|
95
97
|
| `/flow-review <goal>` | You want a read-only review and findings report (runs in the fresh-context `flow-reviewer` subagent). |
|
|
96
98
|
| `/flow-status` | You want session state, readiness checks, and the suggested next step. |
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
| `/flow-session activate <id>` / `close <completed\|deferred\|abandoned>` / `show <id>` | You want to switch, close, or inspect sessions. |
|
|
100
|
-
| `/flow-reset <feature-id>` | You want to reset a feature back to pending (convenience over `flow_feature_complete` with `reset: true`). |
|
|
99
|
+
|
|
100
|
+
These five are the whole command surface since v3.1. The v2/v3.0 convenience commands (`/flow-doctor`, `/flow-history`, `/flow-session`, `/flow-reset`) were retired: each was a thin wrapper over a single tool call that works as a plain request — "show the flow history", "close this session as completed", "reset feature X" — and `/flow-doctor` duplicated `/flow-status`. Startup sync removes the retired command files from earlier installs (user-edited copies are kept).
|
|
101
101
|
|
|
102
102
|
### Tools
|
|
103
103
|
|
|
@@ -113,7 +113,7 @@ The plugin registers a small tool surface (7 tools) that owns all `.flow/**` mut
|
|
|
113
113
|
| `flow_review_record` | Record a reviewer decision (`scope: feature` or `final`). |
|
|
114
114
|
| `flow_session` | Activate or close a session, list history, or show a stored session. |
|
|
115
115
|
|
|
116
|
-
These seven tools are the whole
|
|
116
|
+
These seven 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).
|
|
117
117
|
|
|
118
118
|
### Agents
|
|
119
119
|
|
|
@@ -151,10 +151,10 @@ Flow refuses to write session state at filesystem roots or directly in `$HOME`,
|
|
|
151
151
|
## Troubleshooting
|
|
152
152
|
|
|
153
153
|
```text
|
|
154
|
-
/flow-
|
|
154
|
+
/flow-status
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
-
shows the workspace readiness checks (skills in sync, no stale pre-npm copy, workspace writable) with remediation steps
|
|
157
|
+
shows the workspace readiness checks (skills in sync, no stale pre-npm copy, workspace writable) with remediation steps, plus the active session state, the current blocker, and the suggested next step.
|
|
158
158
|
|
|
159
159
|
## OpenCode references
|
|
160
160
|
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{homedir as
|
|
3
|
-
`,
|
|
4
|
-
`),
|
|
5
|
-
`);if(
|
|
6
|
-
`)){if(!
|
|
7
|
-
`)){let
|
|
2
|
+
import{homedir as ue}from"node:os";import{readdir as ae,readFile as se,rm as u,rmdir as ie}from"node:fs/promises";import{dirname as oe,join as d,normalize as ne,sep as re}from"node:path";var $={fast:"low",balanced:"medium",deep:"high"},W={"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:$.deep,permission:{edit:"deny",bash:"deny",task:{"*":"deny"},"flow_*":"deny",flow_status:"allow",flow_review_record:"allow"}}},j={"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"}},_=["flow-doctor","flow-history","flow-reset","flow-session"];import{createHash as K}from"node:crypto";import{join as w}from"node:path";var x=w(".config","opencode","skills"),m=w(".config","opencode","commands"),A=w(".config","opencode","agents"),v=".flow-skill-version",O="SKILL.md.backup",k=w(".config","opencode","plugins","flow.js"),R=`// Managed by flow-opencode install/uninstall
|
|
3
|
+
`,B="flow-opencode-generated-skill",M=`<!-- ${B} `,C=new RegExp(`^<!-- ${B} name=([a-z0-9]+(?:-[a-z0-9]+)*) version=([0-9]+) hash=sha256:([a-f0-9]{64}) -->$`,"u");function h(e){return K("sha256").update(e,"utf8").digest("hex")}function y(e){let t=e.split(`
|
|
4
|
+
`),s=t.flatMap((p,Z)=>p.startsWith(M)?[Z]:[]);if(s.length===0)return{kind:"not_generated"};if(s.length>1)return{kind:"invalid_generated",reason:"duplicate_marker"};let a=s[0];if(a===void 0)return{kind:"not_generated"};let i=t[a];if(i===void 0)return{kind:"invalid_generated",reason:"malformed_marker"};let o=i.match(C);if(!o)return{kind:"invalid_generated",reason:"malformed_marker"};let[,n,r,c]=o;if(n===void 0||r===void 0||c===void 0)return{kind:"invalid_generated",reason:"malformed_marker"};let l=[...t.slice(0,a),...t.slice(a+1)].join(`
|
|
5
|
+
`);if(h(l)!==c)return{kind:"invalid_generated",reason:"hash_mismatch"};return{kind:"valid_generated",marker:{name:n,version:r,hash:c}}}var z="opencode-plugin-flow",N="file=",E="=sha256:";function g(e){let t=new Map;for(let s of e.split(`
|
|
6
|
+
`)){if(!s.startsWith(N))continue;let a=s.slice(N.length),i=a.lastIndexOf(E);if(i===-1)continue;let o=a.slice(0,i),n=a.slice(i+E.length);if(o.length>0&&/^[a-f0-9]{64}$/.test(n))t.set(o,n)}return t}function b(e,t,s){let a=new Map;for(let n of e.split(`
|
|
7
|
+
`)){let r=n.indexOf("=");if(r===-1)continue;a.set(n.slice(0,r),n.slice(r+1))}let i=a.get("version"),o=a.get("hash");if(a.get("plugin")!==z||a.get("kind")!==t||a.get("name")!==s||!i||!o?.startsWith("sha256:"))return null;return{kind:t,name:s,version:i,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 s=t.get("plugin"),a=t.get("version");if(s!==z||!a)return null;let i=t.get("hash");return{plugin:s,version:a,hash:i?.startsWith("sha256:")?i.slice(7):null}}import{mkdir as Ye,readFile as D,rm as I,writeFile as He}from"node:fs/promises";import{dirname as Qe,join as V,sep as Xe}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
|
+
`)}
|
|
10
|
+
|
|
11
|
+
${e.template}
|
|
12
|
+
`}function ee(e){return`${["---",`description: ${JSON.stringify(e.description)}`,`mode: ${e.mode}`,...e.reasoningEffort?[`reasoningEffort: ${e.reasoningEffort}`]:[],...e.permission?te(e.permission):[],"---"].join(`
|
|
13
|
+
`)}
|
|
14
|
+
|
|
15
|
+
${e.prompt}
|
|
16
|
+
`}function G(){return new Map(Object.entries(j).map(([e,t])=>[e,P(t)]))}function Y(){return new Map(Object.entries(W).map(([e,t])=>[e,ee(t)]))}async function H(e){let t=[],s=[];for(let a of e.names){let i=V(e.root,`${a}.md`),o=V(e.root,`.${a}.flow-version`),n=await L(o),r=n===null?null:b(n,e.kind,a);if(r===null)continue;let c=await L(i);if(c!==null&&h(c)!==r.hash){s.push(i);continue}if(!e.dryRun)await I(i,{force:!0}),await I(o,{force:!0}),await I(`${i}.backup`,{force:!0});t.push(i)}return{removed:t,keptUserEdited:s}}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[s,a]of Object.entries(e)){if(typeof a==="string"){t.push(` ${JSON.stringify(s)}: ${JSON.stringify(a)}`);continue}if(a&&typeof a==="object"){t.push(` ${JSON.stringify(s)}:`);for(let[i,o]of Object.entries(a))t.push(` ${JSON.stringify(i)}: ${JSON.stringify(o)}`)}}return t}async function Q({homeDir:e,dryRun:t=!1,logger:s}){let a={removedSkills:[],keptUserEditedSkills:[],removedCommands:[],keptUserEditedCommands:[],removedAgents:[],keptUserEditedAgents:[],removedPreNpmPlugin:null,keptForeignPreNpmPlugin:null},i=d(e,x);for(let c of await le(i)){if(c!=="flow"&&!c.startsWith("flow-"))continue;let l=d(i,c),p=await ce(l);if(p==="foreign")continue;if(p==="user_edited"){a.keptUserEditedSkills.push(l),s?.(`Kept user-edited Flow skill at ${l}; remove it manually if it is no longer needed.`);continue}if(!t)await de(l);a.removedSkills.push(l),s?.(`${t?"Would remove":"Removed"} Flow skill at ${l}.`)}let o=await H({kind:"command",root:d(e,m),names:_,dryRun:t});for(let c of o.removed)a.removedCommands.push(c),s?.(`${t?"Would remove":"Removed"} retired Flow command at ${c}.`);for(let c of o.keptUserEdited)a.keptUserEditedCommands.push(c),s?.(`Kept user-edited Flow command at ${c}; remove it manually if it is no longer needed.`);await U({homeDir:e,dryRun:t,logger:s,kind:"command",root:d(e,m),files:G(),removed:a.removedCommands,keptUserEdited:a.keptUserEditedCommands}),await U({homeDir:e,dryRun:t,logger:s,kind:"agent",root:d(e,A),files:Y(),removed:a.removedAgents,keptUserEdited:a.keptUserEditedAgents});let n=d(e,k),r=await f(n);if(r!==null)if(r.startsWith(R)){if(!t)await u(n,{force:!0});a.removedPreNpmPlugin=n,s?.(`${t?"Would remove":"Removed"} pre-npm Flow plugin copy at ${n}.`)}else a.keptForeignPreNpmPlugin=n,s?.(`Kept ${n}: it is not managed by Flow. Remove it manually if it is a stale Flow copy.`);return s?.('Finally, remove "opencode-plugin-flow" from the plugin array in opencode.json and restart OpenCode.'),a}async function ce(e){let t=d(e,"SKILL.md"),s=await f(t),a=await f(d(e,v)),i=a===null?null:T(a);if(i!==null&&a!==null){for(let[r,c]of g(a)){if(r==="SKILL.md")continue;let l=X(e,r);if(l===null)continue;let p=await f(l);if(p!==null&&h(p)!==c)return"user_edited"}if(s===null)return"pristine";if(i.hash!==null&&h(s)===i.hash)return"pristine";return y(s).kind==="valid_generated"?"pristine":"user_edited"}if(s===null)return"foreign";let o=y(s);if(o.kind==="valid_generated")return"pristine";if(o.kind==="invalid_generated")return"user_edited";return"foreign"}function X(e,t){let s=ne(d(e,...t.split("/")));if(s!==e&&s.startsWith(`${e}${re}`))return s;return null}async function de(e){let t=await f(d(e,v)),s=new Set;if(t!==null)for(let a of g(t).keys()){let i=X(e,a);if(i===null)continue;await u(i,{force:!0}),await u(`${i}.backup`,{force:!0});let o=oe(i);if(o!==e)s.add(o)}await u(d(e,"SKILL.md"),{force:!0}),await u(d(e,v),{force:!0}),await u(d(e,O),{force:!0});for(let a of[...s].sort((i,o)=>o.length-i.length))await F(a);await F(e)}async function F(e){try{await ie(e)}catch(t){let s=t.code;if(s!=="ENOENT"&&s!=="ENOTEMPTY")throw t}}async function le(e){try{return(await ae(e,{withFileTypes:!0})).filter((s)=>s.isDirectory()).map((s)=>s.name).sort()}catch(t){if(t.code==="ENOENT")return[];throw t}}async function f(e){try{return await se(e,"utf8")}catch(t){if(t.code==="ENOENT")return null;throw t}}async function U(e){for(let[t,s]of e.files){let a=d(e.root,`${t}.md`),i=d(e.root,`.${t}.flow-version`),o=await f(a),n=await f(i),r=n===null?null:b(n,e.kind,t);if(o===null&&r===null)continue;if(!(r!==null||o===s))continue;if(o!==null&&r!==null&&h(o)!==r.hash){e.keptUserEdited.push(a),e.logger?.(`Kept user-edited Flow ${e.kind} at ${a}; remove it manually if it is no longer needed.`);continue}if(!e.dryRun)await u(a,{force:!0}),await u(i,{force:!0}),await u(`${a}.backup`,{force:!0});e.removed.push(a),e.logger?.(`${e.dryRun?"Would remove":"Removed"} Flow ${e.kind} at ${a}.`)}if(!e.dryRun)await F(e.root)}var q=`opencode-plugin-flow — Flow plugin lifecycle commands
|
|
8
17
|
|
|
9
18
|
Usage:
|
|
10
19
|
bunx opencode-plugin-flow uninstall [--dry-run]
|
|
@@ -17,10 +26,10 @@ Commands:
|
|
|
17
26
|
|
|
18
27
|
Options:
|
|
19
28
|
--dry-run Show what would be removed without deleting anything
|
|
20
|
-
--help Show this message`;function
|
|
21
|
-
`)}function
|
|
22
|
-
`)}async function
|
|
29
|
+
--help Show this message`;function J(e){process.stdout.write(`${e}
|
|
30
|
+
`)}function S(e){process.stderr.write(`${e}
|
|
31
|
+
`)}async function pe(e){let t=[...e];if(t.length===0||t.includes("--help")||t.includes("-h"))return J(q),0;let s=t.shift();if(s!=="uninstall")return S(`Unknown command: ${s}
|
|
23
32
|
|
|
24
|
-
${
|
|
33
|
+
${q}`),1;let a=!1;for(let i of t){if(i==="--dry-run"){a=!0;continue}return S(`Unknown argument: ${i}
|
|
25
34
|
|
|
26
|
-
${
|
|
35
|
+
${q}`),1}return await Q({homeDir:process.env.HOME??ue(),dryRun:a,logger:J}),0}try{process.exitCode=await pe(process.argv.slice(2))}catch(e){S(e instanceof Error?e.message:String(e)),process.exitCode=1}
|