opencode-plugin-flow 3.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Douwe de Vries
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # Flow Plugin for OpenCode
2
+
3
+ `opencode-plugin-flow` adds a resumable planning, execution, validation, and review workflow to OpenCode. Use it when a task is important enough that you want a plan, recorded validation evidence, review gates, and a clear way to resume later.
4
+
5
+ Flow is skills-first: four hand-authored skills carry the workflow guidance, and a small plugin provides durable `.flow/**` session state plus a few hard, code-enforced invariants.
6
+
7
+ ## Quick start
8
+
9
+ Install Flow, open OpenCode in the project you want to work on, then run:
10
+
11
+ ```text
12
+ /flow-auto <your goal>
13
+ ```
14
+
15
+ Common examples:
16
+
17
+ ```text
18
+ /flow-auto Add CSV export to the reports page
19
+ /flow-plan Refactor the authentication middleware
20
+ /flow-review Review this repository for release risks
21
+ /flow-status
22
+ ```
23
+
24
+ Flow creates a tracked session under `.flow/**`, drafts a plan, executes one feature at a time, records validation evidence, reviews, and either continues, recovers, or stops with a concrete blocker.
25
+
26
+ ## Install
27
+
28
+ Add Flow to the `plugin` array in your `opencode.json` (global `~/.config/opencode/opencode.json` or per-project):
29
+
30
+ ```json
31
+ {
32
+ "plugin": ["opencode-plugin-flow@3"]
33
+ }
34
+ ```
35
+
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
+
38
+ On startup the plugin syncs its global skills into `~/.config/opencode/skills/`:
39
+
40
+ ```text
41
+ flow/SKILL.md # the driving loop: status → plan → run → review → repeat
42
+ flow-plan/SKILL.md # decomposition heuristics, feature sizing, approval criteria
43
+ flow-run/SKILL.md # one-feature discipline, validation evidence standards
44
+ flow-review/SKILL.md # review depth criteria, finding taxonomy, report format
45
+ ```
46
+
47
+ **Restart OpenCode once after the first install or after an update** so freshly synced skills are discovered.
48
+
49
+ Skill sync is ownership-aware: each Flow-owned skill folder carries a `.flow-skill-version` marker with a sha256 line per shipped file. Folders without the marker are never touched, and if you edit a Flow-owned file by hand — `SKILL.md` or a `references/` file — the previous content is backed up next to it (`SKILL.md.backup`, `references/<name>.md.backup`) before an update replaces it.
50
+
51
+ ### Per-project skill overrides
52
+
53
+ Skills are plain markdown and deliberately overridable. To customize Flow's behavior for one project — for example a team-specific planning or review rubric — place a project-local skill at:
54
+
55
+ ```text
56
+ .opencode/skills/flow-plan/SKILL.md
57
+ ```
58
+
59
+ OpenCode's project skill takes precedence over the global one. This is a supported feature, not a hack: edit the global synced copy for personal defaults (it will be backed up on update), or commit a project override for team conventions.
60
+
61
+ ### Upgrading from the pre-npm (curl) install
62
+
63
+ Releases before 2.1.0 installed a bundled plugin file at `~/.config/opencode/plugins/flow.js`. Once you add the npm plugin entry, that copy would load Flow twice — the plugin warns about this at startup. Remove it with the uninstall command below.
64
+
65
+ ### Uninstall
66
+
67
+ ```bash
68
+ bunx opencode-plugin-flow uninstall
69
+ ```
70
+
71
+ This removes the Flow-owned global skill folders (those carrying the Flow marker) 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`. Skills you created yourself are never deleted. Use `--dry-run` to preview.
72
+
73
+ ## Skills, commands, and tools
74
+
75
+ ### The four skills
76
+
77
+ | Skill | What it carries |
78
+ | --- | --- |
79
+ | `flow` | The driving loop: check status, plan, run, review, repeat; stop conditions, when to ask the user, recovery playbook. |
80
+ | `flow-plan` | How to decompose work into features, size them, profile the repo, and when a plan is safe to auto-approve. |
81
+ | `flow-run` | One-feature-at-a-time discipline and what counts as validation evidence. |
82
+ | `flow-review` | Review depth criteria, finding taxonomy, and report format. |
83
+
84
+ Deeper methodology (worked plan examples, validation and review rubrics) lives in `references/` files next to the `flow-plan`, `flow-run`, and `flow-review` skills and is loaded only when needed.
85
+
86
+ ### Commands
87
+
88
+ Commands are thin pointers into the skills — the skill content is the real instruction surface. Command names are stable across the v2 → v3 upgrade.
89
+
90
+ | Command | Use it when... |
91
+ | --- | --- |
92
+ | `/flow-auto <goal>` | You want Flow to drive the whole loop — plan, run, validate, review — until completion or a real blocker. Run without a goal to resume. |
93
+ | `/flow-plan <goal>` | You want to create, inspect, or shape the plan before execution. |
94
+ | `/flow-run [feature-id]` | You want to execute exactly one approved feature. |
95
+ | `/flow-review <goal>` | You want a read-only review and findings report (runs in the fresh-context `flow-reviewer` subagent). |
96
+ | `/flow-status` | You want session state, readiness checks, and the suggested next step. |
97
+ | `/flow-doctor` | You want the workspace readiness checks with remediation steps (same `flow_status` view, doctor framing). |
98
+ | `/flow-history` | You want to list saved sessions. |
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`). |
101
+
102
+ ### Tools
103
+
104
+ The plugin registers a small tool surface (7 tools) that owns all `.flow/**` mutations:
105
+
106
+ | Tool | Purpose |
107
+ | --- | --- |
108
+ | `flow_status` | Session state, doctor-style readiness, and a computed suggested next step. |
109
+ | `flow_plan_save` | Create or update the draft plan (planning context plus features). |
110
+ | `flow_plan_approve` | Approve the plan, optionally restricted to a feature subset. |
111
+ | `flow_run_start` | Start the next runnable feature. |
112
+ | `flow_feature_complete` | Record a validated feature result; also handles feature reset. |
113
+ | `flow_review_record` | Record a reviewer decision (`scope: feature` or `final`). |
114
+ | `flow_session` | Activate or close a session, list history, or show a stored session. |
115
+
116
+ These seven tools are the whole canonical surface. For one minor cycle, the retired v2 tool names (for example `flow_run_complete_feature` or `flow_session_close`) remain registered as hidden redirect stubs: calling one returns an error naming its replacement and the key arguments, so resumed v2 sessions and old transcripts degrade gracefully instead of failing on an unknown tool. The stubs are scheduled for removal in v3.1. Existing v2 sessions migrate seamlessly (the session schema is unchanged).
117
+
118
+ ### Agents
119
+
120
+ Flow ships one dedicated subagent: `flow-reviewer`, a read-only reviewer used for fresh-context review passes. Its read-only boundary is enforced by OpenCode's native per-agent permissions, not by prompt text. Everything else runs in your normal agent, guided by the skills.
121
+
122
+ ## What the plugin enforces vs. what skills guide
123
+
124
+ The plugin code enforces only hard invariants and persistence safety:
125
+
126
+ 1. A feature cannot be completed without recorded validation evidence.
127
+ 2. A session cannot close as `completed` with unfinished features.
128
+ 3. An approved plan cannot be mutated without an explicit reset.
129
+ 4. If the session's review policy is strict, a reviewer decision must be recorded before completion.
130
+
131
+ 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.
132
+
133
+ 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.
134
+
135
+ ## What Flow writes
136
+
137
+ Flow stores workflow state in the project/worktree where OpenCode is running:
138
+
139
+ ```text
140
+ .flow/active/<session-id>/session.json # active session (source of truth)
141
+ .flow/active/<session-id>/docs/** # readable derived views
142
+ .flow/stored/<session-id>/session.json # parked resumable sessions
143
+ .flow/completed/<session-id>-<timestamp>/** # closed session history
144
+ .flow/locks/
145
+ ```
146
+
147
+ The JSON session files are the source of truth; markdown files are derived views. There is one active session per worktree — activating another parks the current one under `.flow/stored/**`.
148
+
149
+ Flow refuses to write session state at filesystem roots or directly in `$HOME`, and asks before writing `.flow/**` under unusual workspace roots. Existing v2 sessions resume under v3 (the session schema is unchanged).
150
+
151
+ ## Troubleshooting
152
+
153
+ ```text
154
+ /flow-doctor
155
+ ```
156
+
157
+ shows the workspace readiness checks (skills in sync, no stale pre-npm copy, workspace writable) with remediation steps; `/flow-status` adds the active session state, the current blocker, and the suggested next step.
158
+
159
+ ## OpenCode references
160
+
161
+ - Plugins: https://opencode.ai/docs/plugins
162
+ - Skills: https://opencode.ai/docs/skills
163
+ - Agents: https://opencode.ai/docs/agents
164
+
165
+ ## Releases
166
+
167
+ Release notes live in [`CHANGELOG.md`](CHANGELOG.md) and under [`docs/releases/`](docs/releases/).
168
+
169
+ ## Working on Flow itself
170
+
171
+ - Development guide: [`docs/development.md`](docs/development.md)
172
+ - Maintainer contract: [`docs/maintainer-contract.md`](docs/maintainer-contract.md)
173
+ - Codebase map: [`docs/contributor-map.md`](docs/contributor-map.md)
174
+
175
+ ### Package API boundary
176
+
177
+ `opencode-plugin-flow` supports root-only imports:
178
+
179
+ ```ts
180
+ import flowPlugin from "opencode-plugin-flow";
181
+ ```
182
+
183
+ Deep imports such as `opencode-plugin-flow/dist/...` are intentionally not exported.
184
+
185
+ ## License
186
+
187
+ MIT. See [`LICENSE`](LICENSE) for the full text.
package/dist/cli.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import{homedir as a}from"node:os";import{readdir as k,readFile as g,rm as X,rmdir as p}from"node:fs/promises";import{dirname as f,join as Z,normalize as h,sep as m}from"node:path";import{createHash as R}from"node:crypto";import{join as x}from"node:path";var S=x(".config","opencode","skills"),O=".flow-skill-version",w="SKILL.md.backup",F=x(".config","opencode","plugins","flow.js"),C=`// Managed by flow-opencode install/uninstall
3
+ `,I="flow-opencode-generated-skill",c=`<!-- ${I} `,P=new RegExp(`^<!-- ${I} name=([a-z0-9]+(?:-[a-z0-9]+)*) version=([0-9]+) hash=sha256:([a-f0-9]{64}) -->$`,"u");function W(j){return R("sha256").update(j,"utf8").digest("hex")}function K(j){let q=j.split(`
4
+ `),z=q.flatMap((M,L)=>M.startsWith(c)?[L]:[]);if(z.length===0)return{kind:"not_generated"};if(z.length>1)return{kind:"invalid_generated",reason:"duplicate_marker"};let B=z[0];if(B===void 0)return{kind:"not_generated"};let J=q[B];if(J===void 0)return{kind:"invalid_generated",reason:"malformed_marker"};let Q=J.match(P);if(!Q)return{kind:"invalid_generated",reason:"malformed_marker"};let[,V,$,Y]=Q;if(V===void 0||$===void 0||Y===void 0)return{kind:"invalid_generated",reason:"malformed_marker"};let T=[...q.slice(0,B),...q.slice(B+1)].join(`
5
+ `);if(W(T)!==Y)return{kind:"invalid_generated",reason:"hash_mismatch"};return{kind:"valid_generated",marker:{name:V,version:$,hash:Y}}}var u="opencode-plugin-flow",A="file=",D="=sha256:";function U(j){let q=new Map;for(let z of j.split(`
6
+ `)){if(!z.startsWith(A))continue;let B=z.slice(A.length),J=B.lastIndexOf(D);if(J===-1)continue;let Q=B.slice(0,J),V=B.slice(J+D.length);if(Q.length>0&&/^[a-f0-9]{64}$/.test(V))q.set(Q,V)}return q}function _(j){let q=new Map;for(let Q of j.split(`
7
+ `)){let V=Q.indexOf("=");if(V===-1)continue;q.set(Q.slice(0,V),Q.slice(V+1))}let z=q.get("plugin"),B=q.get("version");if(z!==u||!B)return null;let J=q.get("hash");return{plugin:z,version:B,hash:J?.startsWith("sha256:")?J.slice(7):null}}async function E({homeDir:j,dryRun:q=!1,logger:z}){let B={removedSkills:[],keptUserEditedSkills:[],removedPreNpmPlugin:null,keptForeignPreNpmPlugin:null},J=Z(j,S);for(let $ of await i(J)){if($!=="flow"&&!$.startsWith("flow-"))continue;let Y=Z(J,$),T=await d(Y);if(T==="foreign")continue;if(T==="user_edited"){B.keptUserEditedSkills.push(Y),z?.(`Kept user-edited Flow skill at ${Y}; remove it manually if it is no longer needed.`);continue}if(!q)await s(Y);B.removedSkills.push(Y),z?.(`${q?"Would remove":"Removed"} Flow skill at ${Y}.`)}let Q=Z(j,F),V=await H(Q);if(V!==null)if(V.startsWith(C)){if(!q)await X(Q,{force:!0});B.removedPreNpmPlugin=Q,z?.(`${q?"Would remove":"Removed"} pre-npm Flow plugin copy at ${Q}.`)}else B.keptForeignPreNpmPlugin=Q,z?.(`Kept ${Q}: it is not managed by Flow. Remove it manually if it is a stale Flow copy.`);return z?.('Finally, remove "opencode-plugin-flow" from the plugin array in opencode.json and restart OpenCode.'),B}async function d(j){let q=Z(j,"SKILL.md"),z=await H(q),B=await H(Z(j,O)),J=B===null?null:_(B);if(J!==null&&B!==null){for(let[$,Y]of U(B)){if($==="SKILL.md")continue;let T=N(j,$);if(T===null)continue;let M=await H(T);if(M!==null&&W(M)!==Y)return"user_edited"}if(z===null)return"pristine";if(J.hash!==null&&W(z)===J.hash)return"pristine";return K(z).kind==="valid_generated"?"pristine":"user_edited"}if(z===null)return"foreign";let Q=K(z);if(Q.kind==="valid_generated")return"pristine";if(Q.kind==="invalid_generated")return"user_edited";return"foreign"}function N(j,q){let z=h(Z(j,...q.split("/")));if(z!==j&&z.startsWith(`${j}${m}`))return z;return null}async function s(j){let q=await H(Z(j,O)),z=new Set;if(q!==null)for(let B of U(q).keys()){let J=N(j,B);if(J===null)continue;await X(J,{force:!0}),await X(`${J}.backup`,{force:!0});let Q=f(J);if(Q!==j)z.add(Q)}await X(Z(j,"SKILL.md"),{force:!0}),await X(Z(j,O),{force:!0}),await X(Z(j,w),{force:!0});for(let B of[...z].sort((J,Q)=>Q.length-J.length))await v(B);await v(j)}async function v(j){try{await p(j)}catch(q){let z=q.code;if(z!=="ENOENT"&&z!=="ENOTEMPTY")throw q}}async function i(j){try{return(await k(j,{withFileTypes:!0})).filter((z)=>z.isDirectory()).map((z)=>z.name).sort()}catch(q){if(q.code==="ENOENT")return[];throw q}}async function H(j){try{return await g(j,"utf8")}catch(q){if(q.code==="ENOENT")return null;throw q}}var b=`opencode-plugin-flow — Flow plugin lifecycle commands
8
+
9
+ Usage:
10
+ bunx opencode-plugin-flow uninstall [--dry-run]
11
+
12
+ Commands:
13
+ uninstall Remove Flow-owned global skills from ~/.config/opencode/skills/
14
+ and the pre-npm plugin copy at ~/.config/opencode/plugins/flow.js.
15
+ Prints the opencode.json cleanup step; never touches files that
16
+ are not Flow-owned.
17
+
18
+ Options:
19
+ --dry-run Show what would be removed without deleting anything
20
+ --help Show this message`;function y(j){process.stdout.write(`${j}
21
+ `)}function G(j){process.stderr.write(`${j}
22
+ `)}async function n(j){let q=[...j];if(q.length===0||q.includes("--help")||q.includes("-h"))return y(b),0;let z=q.shift();if(z!=="uninstall")return G(`Unknown command: ${z}
23
+
24
+ ${b}`),1;let B=!1;for(let J of q){if(J==="--dry-run"){B=!0;continue}return G(`Unknown argument: ${J}
25
+
26
+ ${b}`),1}return await E({homeDir:process.env.HOME??a(),dryRun:B,logger:y}),0}try{process.exitCode=await n(process.argv.slice(2))}catch(j){G(j instanceof Error?j.message:String(j)),process.exitCode=1}