opencode-ship 0.2.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.
@@ -0,0 +1,168 @@
1
+ # ADR 0001 — `opencode-ship`: npm-distributed, single-command installer
2
+
3
+ Status: Accepted
4
+
5
+ ## Context
6
+
7
+ The current package, `opencode-delivery`, is a private source-level ESM
8
+ core library. Consumer projects adopt it by copying a project plugin
9
+ shim into `opencode.json` and pinning a Git commit hash. Each new
10
+ release is a manual PR into every consumer repo. There is no CLI, no
11
+ generated plugin bundle, no managed-file policy, and no upgrade path
12
+ short of consumer-side edits.
13
+
14
+ Consumer pain observed in the wild:
15
+
16
+ - Every onboarding requires editing `opencode.json`, copying the shim,
17
+ copying the two agents, copying the two skills, copying the
18
+ `delivery.json` adapter, copying the workflow lock, and remembering to
19
+ rename the plugin when the package moves.
20
+ - Updates require a Git ref ping and a manual diff; tooling cannot
21
+ reason about who owns which file.
22
+ - Cleanup is only triggered manually and silently fails when the local
23
+ branch head SHA drifts from the manifest.
24
+
25
+ ## Decision
26
+
27
+ Re-establish the package as `opencode-ship`, a publishable npm module
28
+ with three surfaces:
29
+
30
+ 1. A self-contained compiled ESM OpenCode plugin at the package root
31
+ (`opencode-ship`) that registers the canonical nine `delivery_*`
32
+ tools and removes the need for any consumer-side plugin wrapper.
33
+ 2. A first-class CLI (`opencode-ship` binary) materialising every
34
+ required asset in the consumer repo through five idempotent commands.
35
+ 3. A `opencode-ship/core` subpath that preserves the existing library
36
+ surface so consumer scripts that already import from the package can
37
+ migrate at their own pace.
38
+
39
+ ### Managed-file policy
40
+
41
+ Five output classes are installer-managed and recorded in the lock:
42
+
43
+ - `.opencode/plugin/opencode-ship.js` — the bundled plugin.
44
+ - `.opencode/agents/delivery-reviewer.md`,
45
+ `.opencode/agents/delivery-verifier.md`.
46
+ - `.opencode/skills/delivery-workflow/SKILL.md`,
47
+ `.opencode/skills/planning-research-checkpoint/SKILL.md`.
48
+ - `.opencode/ship.config.json` — user-owned, written only on the
49
+ first `init` if missing.
50
+ - `.opencode/ship.lock.json` — installer-managed.
51
+
52
+ Root `opencode.json` / `opencode.jsonc` is a shared document. The
53
+ installer owns only the Build-agent delivery permissions and the
54
+ delivery subagent delegation allow-list; everything else stays.
55
+
56
+ `update` only replaces managed files whose hashes still match the
57
+ previous manifest. Modified managed files are refused with a precise
58
+ conflict report; the user either restores the upstream bytes (the
59
+ preferred repair) or opts into `--replace-managed`. We never silently
60
+ overwrite.
61
+
62
+ ### Reconciliation algorithm
63
+
64
+ For each managed target, three hashes are computed:
65
+
66
+ - `B` = the SHA-256 stored in the previous lock for that path.
67
+ - `C` = the SHA-256 of the bytes currently on disk.
68
+ - `D` = the SHA-256 of the bytes we want to install.
69
+
70
+ The combination determines the action:
71
+
72
+ | Condition | Plan |
73
+ | --- | --- |
74
+ | No lock, target absent | `create` |
75
+ | No lock, target present | `report` (unowned collision) |
76
+ | `C == B == D` | `noop` |
77
+ | `C == B`, `D != B` | `update` |
78
+ | `C == D`, `C != B` | `converge` (refresh lock only) |
79
+ | `C != B`, `C != D` | `conflict` (refuse) |
80
+ | Target removed, `C == B` | `delete` |
81
+ | Target removed, `C != B` | `conflict` |
82
+
83
+ Conflict semantics are uniform across `init`, `update`, and `uninstall`.
84
+
85
+ ### Transactions
86
+
87
+ Multi-file updates are recoverable but not truly atomic. The transaction
88
+ layer:
89
+
90
+ 1. Acquires an exclusive repository lock file under `.git/opencode-ship/`.
91
+ 2. Re-reads and re-hashes every target before staging.
92
+ 3. Stages each target as a sibling temporary file and `fsync`s it.
93
+ 4. Writes a journal recording original paths, backup paths, hashes,
94
+ and operation order.
95
+ 5. Renames each managed target to a backup, then promotes the staged
96
+ copy into place, then `fsync`s the parent directory.
97
+ 6. Promotes the new lock last as the commit marker.
98
+ 7. Removes backups and journal after commit.
99
+
100
+ Pre-commit failures roll back in reverse order. Post-commit failure
101
+ treats the plan as committed and surfaces a degraded-cleanup warning;
102
+ the journal is cleaned on the next mutating command.
103
+
104
+ ### CLI surface
105
+
106
+ ```
107
+ opencode-ship init [--root <path>] [--config <path>] [--force-config]
108
+ opencode-ship diff [--root <path>] [--json] [--config <path>]
109
+ opencode-ship update [--root <path>] [--json] [--replace-managed]
110
+ opencode-ship doctor [--root <path>] [--json]
111
+ opencode-ship uninstall [--root <path>] [--json] [--purge-config]
112
+ opencode-ship --version
113
+ ```
114
+
115
+ Exit codes:
116
+
117
+ - `0` success / no-op
118
+ - `1` expected negative result (`diff` saw changes; `doctor` unhealthy)
119
+ - `2` invalid input, unsupported project, ambiguous detection
120
+ - `3` ownership/hash/structural conflict
121
+ - `4` filesystem, staging, rollback, or transaction failure
122
+ - `5` unsupported lock/config schema
123
+
124
+ ### Plugin + cleanup hardening
125
+
126
+ The bundled plugin no longer assumes `minimax/MiniMax-M3`; model
127
+ inheritance is the default. The verifier agent gains explicit denials
128
+ for the other eight tools so isolation is symmetric to the reviewer.
129
+ The Build agent gains explicit denials for `delivery_review` and
130
+ `delivery_verify`, plus an `ask` rule for `delivery_merge`. The
131
+ GitHub driver replaces the unsupported `gh pr view --json merged` field
132
+ with a typed `gh pr view ... --json state,mergedAt`.
133
+
134
+ Post-merge cleanup moves to immediate, atomic-enough execution after
135
+ an explicit user-authorised merge: validate the preconditions, persist
136
+ `cleanup-pending`, perform the worktree removal and CAS-style branch
137
+ deletion, seal the manifest, retry on the next delivery task if any
138
+ step failed. No force removal and no OpenCode restart.
139
+
140
+ ### Compatibility strategy
141
+
142
+ Legacy `.opencode/delivery.json`, `.opencode/delivery.lock.json`, the
143
+ two agents, the two skills, and the generic `delivery.ts` wrapper
144
+ shape are recognised. Migration is opt-in by running `init` from a
145
+ checkout that already has those files. The consumer keeps the
146
+ existing manifest directory (`opencode-delivery/` under the Git common
147
+ dir) untouched to avoid stranding in-flight deliveries.
148
+
149
+ ## Consequences
150
+
151
+ - Consumer repo touches only `.opencode/` and root
152
+ `opencode.json`/`.jsonc`. Initial onboarding becomes
153
+ `pnpm dlx opencode-ship init`.
154
+ - Upgrades become `opencode-ship update`, with conflicts surfaced
155
+ instead of overwritten.
156
+ - The library surface stays public through `opencode-ship/core`.
157
+ - v0.1.x becomes a legacy package; v0.2.0 is the first npm-distributed
158
+ release.
159
+ - The lock introduces a migration concern that the installer's
160
+ `doctor` must surface if the lock format is too old.
161
+ - A `cleanupPending` schedule runs only on consumer demand (the next
162
+ delivery start), never on package installation.
163
+
164
+ ## Non-goals
165
+
166
+ - Publishing to a registry is deferred until this PR is approved.
167
+ - Migrating Leo into `opencode-ship` consumers is a separate follow-up.
168
+ - Changing the lifecycle state machine semantics.
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "opencode-ship",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "description": "npm-distributed OpenCode installer that materializes the delivery plugin, reviewer/verifier agents, skills, ship config and lock into any consumer repository.",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "engines": {
9
+ "node": ">=22.6.0",
10
+ "opencode": ">=1.15.5"
11
+ },
12
+ "peerDependencies": {
13
+ "@opencode-ai/plugin": ">=1.15.5 <2"
14
+ },
15
+ "main": "./dist/plugin.js",
16
+ "types": "./dist/plugin.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/plugin.d.ts",
20
+ "import": "./dist/plugin.js"
21
+ },
22
+ "./core": {
23
+ "import": "./dist/core.js"
24
+ },
25
+ "./schema/project-adapter.schema.json": "./schema/project-adapter.schema.json",
26
+ "./schema/ship-config.schema.json": "./schema/ship-config.schema.json",
27
+ "./schema/ship-lock.schema.json": "./schema/ship-lock.schema.json",
28
+ "./package.json": "./package.json"
29
+ },
30
+ "bin": {
31
+ "opencode-ship": "./dist/cli.js"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "assets",
36
+ "schema",
37
+ "docs",
38
+ "README.md",
39
+ "CHANGELOG.md",
40
+ "LICENSE"
41
+ ],
42
+ "scripts": {
43
+ "build": "node scripts/build.mjs",
44
+ "prepack": "node scripts/prepack.mjs",
45
+ "lint": "node scripts/lint.mjs",
46
+ "typecheck": "node scripts/typecheck.mjs",
47
+ "test": "node scripts/run-all-tests.mjs",
48
+ "test:all": "node scripts/run-all-tests.mjs",
49
+ "test:installer": "node --test --test-concurrency=1 --test-reporter=spec tests/installer/**/*.test.mjs",
50
+ "test:plugin": "node --test --test-concurrency=1 --test-reporter=spec tests/plugin/**/*.test.mjs",
51
+ "test:package": "node --test --test-concurrency=1 --test-reporter=spec tests/package/*.test.mjs",
52
+ "format:check": "node scripts/format-check.mjs",
53
+ "verify": "node scripts/verify.mjs"
54
+ },
55
+ "devDependencies": {
56
+ "@opencode-ai/plugin": "1.18.10",
57
+ "@types/node": "^22.10.2",
58
+ "esbuild": "^0.25.0",
59
+ "tsx": "^4.23.1",
60
+ "typescript": "^5.9.3"
61
+ }
62
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "$schema": "https://github.com/Viktorxyz/opencode-delivery/schema/project-adapter.schema.json",
3
+ "contractVersion": 1,
4
+ "repository": {
5
+ "remote": "origin",
6
+ "defaultBranch": { "discover": true }
7
+ },
8
+ "forge": {
9
+ "driver": "github",
10
+ "issueRequired": true,
11
+ "draftAfterFirstCommit": true,
12
+ "issueClosingSyntax": true
13
+ },
14
+ "worktree": {
15
+ "root": ".worktrees",
16
+ "branchTemplate": "{actor}/{slug}",
17
+ "bootstrap": [["<project-owned-bootstrap-argv>"]]
18
+ },
19
+ "verification": {
20
+ "commands": [
21
+ { "id": "canonical", "argv": ["<project-owned-canonical-argv>"], "timeoutMs": 1200000 }
22
+ ],
23
+ "requireCleanDiffAfter": true,
24
+ "invalidateOnHeadChange": true
25
+ },
26
+ "review": {
27
+ "agent": "delivery-reviewer",
28
+ "required": true,
29
+ "invalidateOnHeadChange": true
30
+ },
31
+ "ci": {
32
+ "driver": "github-status-checks",
33
+ "requiredChecks": ["delivery-verify"],
34
+ "wait": true,
35
+ "flakyRetry": 1
36
+ },
37
+ "ready": {
38
+ "requires": ["review", "local-verification", "remote-ci"],
39
+ "stopAfterReady": true
40
+ },
41
+ "merge": {
42
+ "strategy": "squash",
43
+ "policy": "explicit-user-request-only",
44
+ "requireFreshGates": true
45
+ },
46
+ "cleanup": {
47
+ "when": "next-task",
48
+ "requires": ["pr-merged", "worktree-clean", "no-unpublished-commits"]
49
+ }
50
+ }
@@ -0,0 +1,132 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/Viktorxyz/opencode-delivery/schema/project-adapter.schema.json",
4
+ "title": "opencode-ship project adapter",
5
+ "type": "object",
6
+ "required": ["contractVersion"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "contractVersion": { "const": 1 },
10
+ "repository": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "properties": {
14
+ "remote": { "type": "string", "minLength": 1 },
15
+ "defaultBranch": {
16
+ "anyOf": [
17
+ { "type": "string", "minLength": 1 },
18
+ { "type": "object", "additionalProperties": false, "properties": { "discover": { "type": "boolean" }, "name": { "type": "string", "minLength": 1 } } }
19
+ ]
20
+ }
21
+ }
22
+ },
23
+ "forge": {
24
+ "type": "object",
25
+ "additionalProperties": false,
26
+ "properties": {
27
+ "driver": { "const": "github" },
28
+ "issueRequired": { "type": "boolean" },
29
+ "draftAfterFirstCommit": { "type": "boolean" },
30
+ "issueClosingSyntax": { "type": "boolean" }
31
+ }
32
+ },
33
+ "worktree": {
34
+ "type": "object",
35
+ "additionalProperties": false,
36
+ "properties": {
37
+ "root": { "type": "string", "minLength": 1 },
38
+ "branchTemplate": { "type": "string", "minLength": 1 },
39
+ "bootstrap": {
40
+ "type": "array",
41
+ "items": {
42
+ "type": "array",
43
+ "items": { "type": "string", "minLength": 1 },
44
+ "minItems": 1
45
+ }
46
+ }
47
+ }
48
+ },
49
+ "verification": {
50
+ "type": "object",
51
+ "additionalProperties": false,
52
+ "properties": {
53
+ "commands": {
54
+ "type": "array",
55
+ "minItems": 1,
56
+ "items": {
57
+ "type": "object",
58
+ "required": ["id", "argv"],
59
+ "additionalProperties": false,
60
+ "properties": {
61
+ "id": { "type": "string", "minLength": 1 },
62
+ "argv": {
63
+ "type": "array",
64
+ "items": { "type": "string", "minLength": 1 },
65
+ "minItems": 1
66
+ },
67
+ "timeoutMs": { "type": "integer", "minimum": 1 }
68
+ }
69
+ }
70
+ },
71
+ "requireCleanDiffAfter": { "type": "boolean" },
72
+ "invalidateOnHeadChange": { "type": "boolean" }
73
+ }
74
+ },
75
+ "review": {
76
+ "type": "object",
77
+ "additionalProperties": false,
78
+ "properties": {
79
+ "agent": { "type": "string", "minLength": 1 },
80
+ "required": { "type": "boolean" },
81
+ "invalidateOnHeadChange": { "type": "boolean" }
82
+ }
83
+ },
84
+ "ci": {
85
+ "type": "object",
86
+ "additionalProperties": false,
87
+ "properties": {
88
+ "driver": { "const": "github-status-checks" },
89
+ "requiredChecks": {
90
+ "type": "array",
91
+ "items": { "type": "string", "minLength": 1 },
92
+ "uniqueItems": true
93
+ },
94
+ "wait": { "type": "boolean" },
95
+ "flakyRetry": { "type": "integer", "enum": [0, 1] }
96
+ }
97
+ },
98
+ "ready": {
99
+ "type": "object",
100
+ "additionalProperties": false,
101
+ "properties": {
102
+ "requires": {
103
+ "type": "array",
104
+ "items": { "enum": ["review", "local-verification", "remote-ci"] },
105
+ "uniqueItems": true
106
+ },
107
+ "stopAfterReady": { "type": "boolean" }
108
+ }
109
+ },
110
+ "merge": {
111
+ "type": "object",
112
+ "additionalProperties": false,
113
+ "properties": {
114
+ "strategy": { "const": "squash" },
115
+ "policy": { "const": "explicit-user-request-only" },
116
+ "requireFreshGates": { "type": "boolean" }
117
+ }
118
+ },
119
+ "cleanup": {
120
+ "type": "object",
121
+ "additionalProperties": false,
122
+ "properties": {
123
+ "when": { "const": "next-task" },
124
+ "requires": {
125
+ "type": "array",
126
+ "items": { "enum": ["pr-merged", "worktree-clean", "no-unpublished-commits"] },
127
+ "uniqueItems": true
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "description": "Project plugin shim that loads the reusable opencode-delivery package. Pin the version by replacing <commit-sha> with a real commit SHA before adopting.",
4
+ "plugin": ["https://github.com/Viktorxyz/opencode-delivery#<commit-sha>"],
5
+ "agent": {
6
+ "build": {
7
+ "permission": {
8
+ "task": {
9
+ "*": "deny",
10
+ "explore": "allow",
11
+ "general": "allow",
12
+ "reviewer": "allow",
13
+ "verifier": "allow",
14
+ "delivery-reviewer": "allow",
15
+ "delivery-verifier": "allow",
16
+ "plan": "deny"
17
+ }
18
+ }
19
+ }
20
+ },
21
+ "permission": {
22
+ "bash": {
23
+ "git commit --no-verify *": "deny",
24
+ "git push --force *": "deny",
25
+ "git push --force-with-lease *": "deny",
26
+ "git push -f *": "deny",
27
+ "git reset --hard *": "deny",
28
+ "git stash *": "deny",
29
+ "git worktree remove --force *": "deny",
30
+ "git branch -D *": "deny",
31
+ "git worktree remove *": "ask",
32
+ "git rebase *": "ask",
33
+ "git merge *": "ask",
34
+ "gh pr merge --auto *": "deny",
35
+ "gh pr merge *": "ask",
36
+ "gh pr close *": "ask",
37
+ "gh issue delete *": "deny",
38
+ "gh api *": "ask",
39
+ "*": "allow"
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,136 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/Viktorxyz/opencode-delivery/schema/ship-config.schema.json",
4
+ "title": "opencode-ship user config",
5
+ "type": "object",
6
+ "required": ["schemaVersion"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "schemaVersion": { "const": 1 },
10
+ "owner": {
11
+ "type": "string",
12
+ "description": "Optional override for the issue/manifest owner field. Defaults to the agent's local user.name."
13
+ },
14
+ "project": {
15
+ "type": "object",
16
+ "additionalProperties": false,
17
+ "properties": {
18
+ "remote": { "type": "string", "minLength": 1 },
19
+ "repository": { "type": "string", "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$" },
20
+ "defaultBranch": { "type": "string", "minLength": 1 },
21
+ "packageManager": { "enum": ["npm", "pnpm", "yarn", "bun"] },
22
+ "detectOverrides": { "type": "boolean", "description": "Permit detection to refresh previously persisted values." }
23
+ }
24
+ },
25
+ "delivery": {
26
+ "type": "object",
27
+ "additionalProperties": false,
28
+ "properties": {
29
+ "worktree": {
30
+ "type": "object",
31
+ "additionalProperties": false,
32
+ "properties": {
33
+ "root": { "type": "string", "minLength": 1 },
34
+ "branchTemplate": { "type": "string", "minLength": 1 },
35
+ "bootstrap": {
36
+ "type": "array",
37
+ "items": {
38
+ "type": "array",
39
+ "items": { "type": "string", "minLength": 1 },
40
+ "minItems": 1
41
+ }
42
+ }
43
+ }
44
+ },
45
+ "verification": {
46
+ "type": "object",
47
+ "additionalProperties": false,
48
+ "properties": {
49
+ "commands": {
50
+ "type": "array",
51
+ "minItems": 1,
52
+ "items": {
53
+ "type": "object",
54
+ "required": ["id", "argv"],
55
+ "additionalProperties": false,
56
+ "properties": {
57
+ "id": { "type": "string", "minLength": 1 },
58
+ "argv": {
59
+ "type": "array",
60
+ "items": { "type": "string", "minLength": 1 },
61
+ "minItems": 1
62
+ },
63
+ "timeoutMs": { "type": "integer", "minimum": 1 }
64
+ }
65
+ }
66
+ },
67
+ "requireCleanDiffAfter": { "type": "boolean" },
68
+ "invalidateOnHeadChange": { "type": "boolean" }
69
+ }
70
+ },
71
+ "review": {
72
+ "type": "object",
73
+ "additionalProperties": false,
74
+ "properties": {
75
+ "agent": { "type": "string", "minLength": 1 },
76
+ "required": { "type": "boolean" },
77
+ "invalidateOnHeadChange": { "type": "boolean" }
78
+ }
79
+ },
80
+ "ci": {
81
+ "type": "object",
82
+ "additionalProperties": false,
83
+ "properties": {
84
+ "driver": { "const": "github-status-checks" },
85
+ "requiredChecks": {
86
+ "type": "array",
87
+ "items": { "type": "string", "minLength": 1 },
88
+ "uniqueItems": true
89
+ },
90
+ "wait": { "type": "boolean" },
91
+ "flakyRetry": { "type": "integer", "enum": [0, 1] }
92
+ }
93
+ },
94
+ "ready": {
95
+ "type": "object",
96
+ "additionalProperties": false,
97
+ "properties": {
98
+ "requires": {
99
+ "type": "array",
100
+ "items": { "enum": ["review", "local-verification", "remote-ci"] },
101
+ "uniqueItems": true
102
+ },
103
+ "stopAfterReady": { "type": "boolean" }
104
+ }
105
+ },
106
+ "merge": {
107
+ "type": "object",
108
+ "additionalProperties": false,
109
+ "properties": {
110
+ "strategy": { "const": "squash" },
111
+ "policy": { "const": "explicit-user-request-only" },
112
+ "requireFreshGates": { "type": "boolean" }
113
+ }
114
+ },
115
+ "cleanup": {
116
+ "type": "object",
117
+ "additionalProperties": false,
118
+ "properties": {
119
+ "when": { "const": "next-task" },
120
+ "requireUnpublishedGuard": { "type": "boolean" }
121
+ }
122
+ }
123
+ }
124
+ },
125
+ "tasks": {
126
+ "type": "object",
127
+ "description": "Optional override of the managed-file paths. Use only to relocate a target.",
128
+ "additionalProperties": false,
129
+ "properties": {
130
+ "pluginPath": { "type": "string", "pattern": "^\\.opencode/.+\\.js$" },
131
+ "agentsDir": { "type": "string", "pattern": "^\\.opencode/agents/?$" },
132
+ "skillsDir": { "type": "string", "pattern": "^\\.opencode/skills/?$" }
133
+ }
134
+ }
135
+ }
136
+ }