opencode-ruby-upgrader 0.1.0 → 0.1.3

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/E2E_EVIDENCE.md CHANGED
@@ -28,9 +28,9 @@ The sections below record each stage of that work, including the deliberate stop
28
28
  | Plugin dry-run inventory | Passed: Rails/RSpec detected; Ruby `2.4.10`; Rails `4.2.11.3`; 118 locked dependencies; no detected private sources. |
29
29
  | Host runtime | Blocked as expected: host Ruby is `2.6.10`, but fixture requires `2.4.10`; installed Bundler is `1.17.2`, while the lockfile specifies `1.17.3`. |
30
30
  | Dependency install / test suite | Not run. A container or version manager providing the exact historical runtime is required. |
31
- | Container runtime | Blocked: Docker CLI is installed, but the local Docker daemon was unavailable at `unix:///Users/lilla/.docker/run/docker.sock`; no containers or fixture code were started. |
31
+ | Container runtime | Blocked: Docker CLI was installed, but the local Docker daemon was unavailable; no containers or fixture code were started. |
32
32
 
33
- ## 2026-09-18 container baseline — in progress
33
+ ## 2026-09-18 container baseline
34
34
 
35
35
  | Check | Result |
36
36
  | --- | --- |
@@ -42,7 +42,7 @@ The sections below record each stage of that work, including the deliberate stop
42
42
 
43
43
  Do not treat this as an application or plugin failure until the pinned runtime prerequisite has been supplied and the command has been rerun. Record the command result, timestamps, exit code, sanitized output digest, and worktree fingerprint after the rerun.
44
44
 
45
- ## 2026-09-18 baseline RSpec — in progress
45
+ ## 2026-09-18 baseline RSpec
46
46
 
47
47
  | Check | Result |
48
48
  | --- | --- |
@@ -62,9 +62,9 @@ The local package worktree now contains a constrained `docker-bundle-rspec` exec
62
62
 
63
63
  ## 2026-09-18 OpenCode preflight — blocked, recoverable
64
64
 
65
- An explicit local-plugin preflight returned `ok: false` with `reason: "dirty-worktree"` for branch `ruby-upgrade/e2e-3.4` at fixture commit `bad95e2be88687f5d185c29a2361526fa05b8f54`; its configured default branch was correctly detected as `main`. The only observed worktree change was an untracked `LEARNING_PATH.md` created by the OpenCode session, not fixture work. Remove that generated file and rerun preflight before any plugin lifecycle action. The package was also corrected so its injected agent invokes the package-local Node CLI rather than assuming the package binary is globally on `PATH`; `npm test` remained 32/32 and `git diff --check` passed after that correction.
65
+ An explicit local-plugin preflight returned `ok: false` with `reason: "dirty-worktree"` for branch `ruby-upgrade/e2e-3.4` at fixture commit `bad95e2be88687f5d185c29a2361526fa05b8f54`; its configured default branch was correctly detected as `main`. The only observed change was an unrelated ignored local note, not fixture work. After removing that file, preflight was rerun before any plugin lifecycle action. The package was also corrected so its injected agent invokes the package-local Node CLI rather than assuming the package binary is globally on `PATH`; `npm test` remained 32/32 and `git diff --check` passed after that correction.
66
66
 
67
- ## Fixture reset attempt — in progress
67
+ ## Fixture reset attempt
68
68
 
69
69
  The disposable worktree was recreated at the pinned baseline and the PostgreSQL/Ruby containers were recreated. The first rerun of `bundle exec rake db:create` again stopped at `ExecJS::RuntimeUnavailable`. This is not yet evidence of a fixture change or database failure: the container must first prove that the installed Node.js package exposes an executable name discoverable by ExecJS (`node`). The next diagnostic records only runtime command availability/version; no Gemfile, lockfile, or application configuration change is authorized.
70
70
 
package/README.md CHANGED
@@ -11,17 +11,18 @@ From a checkout of the project you want to upgrade (shown with `main` as the def
11
11
  git config opencode-ruby-upgrader.defaultBranch main
12
12
 
13
13
  # 2. Create the linked worktree the agent is allowed to work in
14
- git branch ruby-upgrade/ruby-3.4
15
- git worktree add ../<repo>-ruby-3.4 ruby-upgrade/ruby-3.4
14
+ # (replace <target> with the Ruby version you're upgrading to, e.g. 3.4)
15
+ git branch ruby-upgrade/ruby-<target>
16
+ git worktree add ../<repo>-ruby-<target> ruby-upgrade/ruby-<target>
16
17
 
17
18
  # 3. Launch OpenCode from that worktree and start the migration
18
- cd ../<repo>-ruby-3.4
19
+ cd ../<repo>-ruby-<target>
19
20
  opencode
20
21
  ```
21
22
 
22
23
  Then run `/ruby-upgrade` (or add `--dry-run` to get a no-write assessment first). The agent inventories the project, researches an official-source compatibility ladder, and proposes each validated hop as a local checkpoint commit for your review. See [Safety model](#safety-model) for what it will and will not do automatically.
23
24
 
24
- > Requires Git 2.5+ (linked-worktree safety model) and, for legacy Ruby hops, Docker for the isolated validation container (see [Security boundaries](#security-boundaries)).
25
+ > Requires Git 2.5+ (linked-worktree safety model) and Docker for the isolated validation container when your host cannot run the Ruby version being tested (see [Security boundaries](#security-boundaries)).
25
26
 
26
27
  | Step | Who does it | Result |
27
28
  |---|---|---|
@@ -43,7 +44,7 @@ This proves the workflow works on a genuinely old, real-world Rails stack. It do
43
44
 
44
45
  ## Safety model
45
46
 
46
- For Git repositories, the agent runs **only** from a linked Git worktree created by the user. Before starting, explicitly configure the repository default branch with `git config opencode-ruby-upgrader.defaultBranch main` (replace `main` as needed) — see [Quick start](#quick-start) for the three setup commands, which the agent also shows verbatim if you invoke it from a primary checkout. The upgrader fails closed if this configuration is absent and never guesses `main`, `master`, or a remote default. This keeps your normal checkout free for other work. In a non-Git project, it asks for confirmation before proceeding without worktree isolation or Git checkpoints. It never creates, switches, deletes, merges, pushes, or reconfigures branches/remotes. It also never publishes, deploys, or runs destructive database commands.
47
+ The agent runs durable migrations **only** from a linked Git worktree created by the user. Before starting, explicitly configure the repository default branch with `git config opencode-ruby-upgrader.defaultBranch main` (replace `main` as needed) — see [Quick start](#quick-start) for the three setup commands, which the agent also shows verbatim if you invoke it from a primary checkout. The upgrader fails closed if this configuration is absent and never guesses `main`, `master`, or a remote default. This keeps your normal checkout free for other work. Non-Git projects support dry-run inventory only. The agent never creates, switches, deletes, merges, pushes, or reconfigures branches/remotes. It also never publishes, deploys, or runs destructive database commands.
47
48
 
48
49
  After every routine Ruby minor-version hop with passing validation, the agent proposes a **local** checkpoint commit through a guarded commit gate and OpenCode asks for confirmation. The gate verifies the linked worktree and non-default branch, exact expected Git history, an empty initial staging area, a complete passing report iteration, and scans staged content for likely credentials. It cannot push, fetch, alter remotes, switch branches, merge, rebase, reset, or amend history. You can review and push any validated checkpoint; a run becomes `complete` only once it reaches its pinned target.
49
50
 
@@ -85,7 +86,7 @@ The dashboard identifies local checkpoint commits from trailers embedded in thos
85
86
 
86
87
  ## Controls and recovery
87
88
 
88
- Use `/ruby-upgrade --dry-run` for a no-write inventory and proposed migration assessment; it creates no report, lock, checkpoint, or durable research evidence. Use `/ruby-upgrade --target 3.4` to pin an explicit final Ruby version, or `/ruby-upgrade --stop-after-hop` to validate and commit one hop before stopping.
89
+ Use `/ruby-upgrade --dry-run` for a no-write inventory and proposed migration assessment; it creates no report, lock, checkpoint, or durable research evidence. Use `/ruby-upgrade --target <version>` (for example `/ruby-upgrade --target 3.4`) to pin an explicit final Ruby version, or `/ruby-upgrade --stop-after-hop` to validate and commit one hop before stopping.
89
90
 
90
91
  Each active run holds a local lock. To stop for review or manual work, transition it to `paused`; that releases the lock without marking the migration complete. Resume the existing report rather than starting a second migration:
91
92
 
@@ -93,7 +94,7 @@ Each active run holds a local lock. To stop for review or manual work, transitio
93
94
  opencode-ruby-upgrader resume --report .ruby-upgrades/runs/<run>.json
94
95
  ```
95
96
 
96
- `complete`, `blocked`, and `paused` runs release their lock. A Ruby run blocked by an approved Rails bridge cannot be resumed: complete its linked Rails bridge, then start a fresh Ruby run. For other blockers, inspect the report and use the documented transition/resume path. To undo a completed hop, use the reviewable local history: `git revert <hop-sha>`. Do not use reset, rebase, or force-push as routine migration recovery.
97
+ `complete`, `blocked`, and `paused` runs release their lock. For other blockers, inspect the report and use the documented transition/resume path. To undo a completed hop, use the reviewable local history: `git revert <hop-sha>`. Do not use reset, rebase, or force-push as routine migration recovery.
97
98
 
98
99
  If a resolved Rails version blocks the next Ruby hop, record the user-approved bridge, then transition the Ruby run to `blocked`. That Ruby report is terminal: complete the linked Rails lifecycle and start a fresh Ruby run. Every Rails iteration executes `bin/rails app:update` first, records its receipt, reviews that exact working-tree fingerprint, and only then runs final tests. Validate and checkpoint each one with `commit-rails-hop`.
99
100
 
@@ -101,7 +102,9 @@ If a resolved Rails version blocks the next Ruby hop, record the user-approved b
101
102
 
102
103
  The agent defaults unknown shell commands to an OpenCode confirmation prompt. Git inspection is allowed, while direct Git mutation, GitHub CLI, publishing, and shell chaining/pipes/substitutions are denied. Dependency installation/updates, recognized tests, state writes, `commit-hop`, and `commit-rails-hop` require confirmation. This protects against accidental agent actions, not malicious project code: dependency installation and tests execute project-controlled code with your local user permissions. Use an isolated environment for repositories you do not trust, and review any command OpenCode asks you to approve.
103
104
 
104
- New reports require `record-executed-iteration --validation <id>` or `record-executed-rails-iteration --validation <test-id>`; asserted results cannot be recorded or committed. The accepted IDs map to fixed no-shell commands: `bundle-rspec`, `bundle-rails-test`, `bundle-rake-test`, `bin-rails-test`, and, for Rails bridges, `rails-app-update`. For a legacy Ruby hop, the agent runs `prepare-target-runtime --ruby <x.y.z>` after selecting the exact target patch release. One confirmation provisions labelled per-run Ruby and isolated PostgreSQL Docker resources, installs Node, installs Bundler 1.17.3, runs `bundle install`, and, for Rails, creates the isolated test database. It writes nonsecret `.ruby-upgrades/runtime.json` with only safe preparation digests and the resolved image ID; raw output and `DATABASE_URL` are never persisted. `docker-bundle-rspec` reuses and verifies that manifest, including the exact requested Ruby execution and resolved image ID, before executing the fixed `docker exec --env DATABASE_CLEANER_ALLOW_REMOTE_DATABASE_URL=true <container> bundle exec rspec`. The safeguard override is scoped to the verified isolated test process; no container name, report path, or environment value is needed from the user. Receipts persist only an output digest and byte count, plus structured test metrics; raw validation output is deliberately not committed. Each receipt also binds to a non-evidence working-tree fingerprint, which the commit gate rechecks after final validation. A checkpoint commit carries the receipt digest. This is tamper-evident provenance for a committed report, not protection against the same local user rewriting both evidence and Git history.
105
+ New reports require `record-executed-iteration --validation <id>` or `record-executed-rails-iteration --validation <test-id>`; asserted results cannot be recorded or committed. The accepted IDs map to fixed no-shell commands: `bundle-rspec`, `bundle-rails-test`, `bundle-rake-test`, `bin-rails-test`, and, for Rails bridges, `rails-app-update`. When the target Ruby is unavailable on your host, the agent runs `prepare-target-runtime --ruby <x.y.z>` after selecting the exact target patch release. One confirmation provisions labeled per-run Ruby and isolated PostgreSQL Docker resources, installs Node, installs Bundler 2.4.22, runs `bundle install`, and, for Rails, creates the isolated test database.
106
+
107
+ It writes nonsecret `.ruby-upgrades/runtime.json` with only safe preparation digests and the resolved image ID; raw output and `DATABASE_URL` are never persisted. `docker-bundle-rspec` reuses and verifies that manifest, including the exact requested Ruby execution and resolved image ID, before executing the fixed `docker exec --env DATABASE_CLEANER_ALLOW_REMOTE_DATABASE_URL=true <container> bundle exec rspec`. The safeguard override is scoped to the verified isolated test process; no container name, report path, or environment value is needed from the user. Receipts persist only an output digest and byte count, plus structured test metrics; raw validation output is deliberately not committed. Each receipt also binds to a non-evidence working-tree fingerprint, which the commit gate rechecks after final validation. A checkpoint commit carries the receipt digest. This is tamper-evident provenance for a committed report, not protection against the same local user rewriting both evidence and Git history.
105
108
 
106
109
  ## Product limits
107
110
 
@@ -1,17 +1,18 @@
1
- # v0.1.0 Release Gate
1
+ # Release Gate
2
2
 
3
- Complete every item before pushing a `v*` tag.
3
+ Complete every item before pushing a `v*` tag. The protected `npm-release` environment enforces the required review; CI runs `npm test`, the pinned runtime smoke test, and `npm publish --provenance`.
4
4
 
5
- - [x] Create `github.com/lilla021/opencode-ruby-upgrader`; push the reviewed `main` branch.
6
- - [ ] Configure the GitHub `npm-release` environment with required approval and tag restriction `v*`.
7
- - [x] First-publish bootstrap plan: publish `v0.1.0` manually from the workstation once with 2FA (npm policy requires the package to exist before OIDC trusted publishing or staged publishing can be configured — see `npm/cli#8544`). No long-lived token is needed for this bootstrap.
8
- - [x] After v0.1.0 exists: configure npm Trusted Publishing (OIDC) for `opencode-ruby-upgrader` bound to `.github/workflows/release.yml` + `npm-release` environment; CI then publishes with `npm publish --provenance` using no stored secret. Optionally restrict the trusted publisher to stage-only for later versions.
9
- - [x] Confirm `opencode-ruby-upgrader` currently returns npm registry 404 and is available for first publication.
10
- - [x] Run `npm test` and `npm pack --dry-run` from the release candidate.
11
- - [x] Install latest OpenCode, load this package from a local `file://` plugin path, restart OpenCode, and confirm `/ruby-upgrade` plus its permission prompts.
12
- - [x] Run a supported Ruby fixture in a linked Git worktree: complete one hop, inspect the local commit/report/dashboard, then exercise one risk pause.
13
- - [x] When releasing Rails bridge support, run a disposable Rails compatibility bridge: review `app:update` evidence, complete one Rails hop, and inspect Rails trailers and dashboard rendering.
14
- - [x] Review the package metadata, LICENSE, README, SECURITY.md, RELEASING.md, and packed-file list. Confirm no credentials or customer artifacts are present.
15
- - [x] Create release notes describing scope, supported adapters, known limitations, and rollback (`git revert <hop-sha>`) — see [RELEASE_NOTES.md](RELEASE_NOTES.md).
5
+ ## Before every release
16
6
 
17
- One-time exception: the very first publish (v0.1.0) is done manually from the workstation with 2FA, because npm requires the package to exist before OIDC trusted publishing or staging can be configured. All subsequent publishes go through the protected `npm-release` environment with OIDC trusted publishing; do not bypass it with token-based direct publishing.
7
+ - [ ] Bump the version and update [RELEASE_NOTES.md](RELEASE_NOTES.md): scope, supported adapters, known limitations, rollback.
8
+ - [ ] Review the package metadata (name, description, keywords, `repository`, `bugs`, `homepage`) and the packed-file list with `npm pack --dry-run`.
9
+ - [ ] Run `npm test`; confirm the release workflow's runtime smoke gate passes with the pinned OpenCode runtime.
10
+ - [ ] Confirm no credentials, personal data, or local paths appear in the packed files, README, SECURITY.md, PRIVACY.md, or release notes.
11
+ - [ ] Tag the exact reviewed commit and push the tag from `main`; approve the `npm-release` deployment so CI publishes with OIDC provenance — never bypass with token-based local publishing.
12
+ - [ ] Verify on the registry: version, `latest` dist-tag, and SLSA provenance attestation.
13
+
14
+ ## History
15
+
16
+ - **v0.1.0** was the one-time bootstrap: npm requires a package to exist before OIDC trusted publishing can be configured, so it was published once from the workstation with 2FA and no long-lived token.
17
+ - **v0.1.1** was tagged but never published; its gate correctly stopped on the OpenCode runtime smoke test (the npm 11 install-script gate, fixed in v0.1.2).
18
+ - **v0.1.2** was the first release published through the fully automated gate above.
package/RELEASE_NOTES.md CHANGED
@@ -1,4 +1,26 @@
1
- # opencode-ruby-upgrader — v0.1.0 release notes
1
+ # opencode-ruby-upgrader — release notes
2
+
3
+ ## v0.1.3 — documentation refresh
4
+
5
+ No functional change. The public documentation is tightened so the package reads cleanly on the registry and in the repository:
6
+
7
+ - Quick-start examples use `<target>` placeholders instead of a hardcoded Ruby version.
8
+ - [RELEASE_CHECKLIST.md](RELEASE_CHECKLIST.md) is now a reusable Release Gate rather than the one-time v0.1.0 audit.
9
+ - The v0.1.2 notes describe what that release actually changed.
10
+ - Stale "in progress" section headings in the evidence ledger were resolved; the receipts themselves are unchanged.
11
+
12
+ Published through the same protected-CI gate as v0.1.2.
13
+
14
+ ## v0.1.2 — first fully automated release
15
+
16
+ The first release published entirely through protected CI with npm trusted publishing (OIDC) and signed provenance. It also ships the fixes that made that pipeline reliable:
17
+
18
+ - **npm 11 install-script gate:** the release-time runtime test now installs the pinned `opencode-ai@1.18.30` package as a real dependency, since npm 11 blocks dependency install scripts by default. `v0.1.1` was tagged but never published because its gate correctly stopped on exactly this.
19
+ - **Hermetic safety gates:** Git capability detection reads repository-local state only, so machine-specific Git config on a runner or host cannot add phantom approval requirements to a migration.
20
+ - **Cleaner run control:** durable migrations require a user-created linked Git worktree; outside a Git repo only the no-write dry-run inventory is offered. Risk decisions now honor the latest recorded verdict, so a paused or blocked risk that is later approved resumes without a fresh run.
21
+ - **Docs and metadata hygiene:** public docs use placeholder syntax for user-supplied values, wording is tightened, and package metadata is scrubbed of personal details.
22
+
23
+ ## v0.1.0 — initial public release
2
24
 
3
25
  ## What this is
4
26
 
@@ -7,7 +29,7 @@ An evidence-driven Ruby and Rails upgrade agent for [OpenCode](https://opencode.
7
29
  ## What's new in this release
8
30
 
9
31
  - **Guided migration loop:** inventory the project, research an official-source compatibility ladder, validate each hop in an isolated Docker container running the project's real test command, and propose a local checkpoint commit with the validation receipt digest embedded in the commit message.
10
- - **Rails bridge support:** for Rails apps, each hop runs `bin/rails app:update` with conflict-skipping behaviour, presents every generated file for review before it is accepted, and records dependency-compatibility and license findings for every lockfile change.
32
+ - **Rails bridge support:** for Rails apps, each hop runs `bin/rails app:update` with conflict-skipping behavior, presents every generated file for review before it is accepted, and records dependency-compatibility and license findings for every lockfile change.
11
33
  - **Evidence trail:** JSON and Markdown reports under `.ruby-upgrades/runs/` (openable as an Obsidian vault), a local-only read-only dashboard on `127.0.0.1`, and receipt digests in every checkpoint commit trailer.
12
34
  - **Safety model:** the agent runs only in a user-created linked Git worktree, fails closed unless the default branch is configured explicitly, and pauses — with evidence and options — before anything sensitive: data changes, authentication/authorization, payments, secrets, production configuration, framework-major upgrades, private dependencies, native extensions, or failed validation. It never pushes, merges, reconfigures branches, or runs destructive commands.
13
35
 
@@ -19,7 +41,7 @@ The agent completed a full end-to-end migration against the public `lilla021/rub
19
41
 
20
42
  See [README.md](README.md#quick-start): from a linked Git worktree with `opencode-ruby-upgrader.defaultBranch` configured, launch OpenCode and run `/ruby-upgrade`. Use `--dry-run` for a no-write assessment first.
21
43
 
22
- Requirements: Git 2.5+; Docker for legacy Ruby hops (used for the isolated validation container).
44
+ Requirements: Git 2.5+; Docker when your host cannot run the Ruby version being upgraded (used for the isolated validation container).
23
45
 
24
46
  ## Supported projects
25
47
 
@@ -27,7 +49,7 @@ Bundler projects using Rails, RSpec, or Minitest get the full automatic flow (in
27
49
 
28
50
  ## Known limitations
29
51
 
30
- - The credential scanner is heuristic; it recognises common token formats but may miss unusual forms.
52
+ - The credential scanner is heuristic; it recognizes common token formats but may miss unusual forms.
31
53
  - Run reports are local mutable JSON evidence; their integrity is bounded by your local filesystem permissions, not a tamper-proof store.
32
54
  - The agent validates tests and compatibility, not production behavior, security correctness, or deployment safety — review and push are always your step.
33
55
  - Gemfile source detection is static and may not resolve dynamically computed sources; private sources require explicit review and approval.
@@ -38,4 +60,4 @@ Every validated hop is a separate local commit carrying its validation receipt d
38
60
 
39
61
  ## License
40
62
 
41
- MIT.
63
+ MIT.
package/RELEASING.md CHANGED
@@ -1,8 +1,8 @@
1
- # Release Checklist
1
+ # Releasing
2
2
 
3
3
  1. Verify package `author`, `repository`, `bugs`, and `homepage` metadata remains accurate.
4
4
  2. Run `npm test` and `npm pack --dry-run`.
5
- 3. Review the packed file list, dependency changes, LICENSE, README, SECURITY.md, and this checklist.
5
+ 3. Review the packed file list, dependency changes, LICENSE, README, SECURITY.md, and the [Release Gate](RELEASE_CHECKLIST.md).
6
6
  4. Publish from protected CI with npm provenance enabled; never publish from an unreviewed workstation.
7
7
  5. Tag the exact reviewed commit, publish release notes, and verify installation in a clean OpenCode environment.
8
8
 
@@ -10,9 +10,9 @@ You own a careful Ruby runtime migration. Be decisive on routine fixes and trans
10
10
 
11
11
  ## Non-negotiable safety contract
12
12
 
13
- - Begin by running `opencode-ruby-upgrader preflight --json`. If it does not return `ok: true`, do not inspect, edit, test, or resolve dependencies. For Git repositories, require the user to configure `git config opencode-ruby-upgrader.defaultBranch <branch>`; do not infer a default branch. Show the worktree instructions and ask the user to relaunch OpenCode from their user-created linked worktree. If it returns `mode: "non-git"`, explain that automatic commits, Git checkpoints, and worktree isolation are unavailable, then continue only after the user accepts that limitation.
14
- - Parse user controls before work: `--dry-run`, `--target <version>`, and `--stop-after-hop`. Run `opencode-ruby-upgrader inventory`, `opencode-ruby-upgrader supply-chain`, and `opencode-ruby-upgrader git-capabilities` first. Pause for explicit review on shallow clones, sparse checkout, submodules, or LFS configuration. If the project is unsupported or has no recognized test adapter, stop with the detected evidence and ask for a test command; do not invent one. After official research pins the target, initialize the durable run with `opencode-ruby-upgrader begin --target <version>` (append `--dry-run` or `--stop-after-hop` when requested). For a non-Git project, obtain explicit confirmation and use `--allow-non-git`; this flag requires an OpenCode confirmation. Use only the returned report path for this run.
15
- - Drive the durable state machine, not prose alone: record research with `record-research`, then transition `initialized → inventory_complete → research_complete`; record each complete evidence-backed hop only with `record-executed-iteration` (or `record-executed-rails-iteration`), transition to `hop_validated`, and use the returned checkpoint SHA when transitioning to `committed`. Exactly one checkpoint is required before the next iteration. Repeat per hop. Use `paused` to stop safely for user review/manual work (it releases the lock); `complete` is only valid once the pinned target is reached. Use `blocked` only with evidence and an actionable option. For an isolated legacy runtime, ask once for approval to apply the planned Ruby declaration edit and then run `prepare-target-runtime --ruby <x.y.z> --report <the run path you just resumed>` before `docker-bundle-rspec`: it creates isolated Docker resources, installs Node and Bundler 1.17.3, runs `bundle install`, and creates the isolated Rails test database when applicable. Execute preparation with the longest supported shell timeout (at least 15 minutes), not a default short timeout. Do not ask the user for a report path, container name, or environment variables. The persisted nonsecret runtime manifest binds labelled app and PostgreSQL containers to the selected run; `docker-bundle-rspec` executes only the fixed inner argv `bundle exec rspec`.
13
+ - Begin by running `opencode-ruby-upgrader preflight --json`. If it does not return `ok: true`, do not inspect, edit, test, or resolve dependencies. Require the user to configure `git config opencode-ruby-upgrader.defaultBranch <branch>`; do not infer a default branch. Show the worktree instructions and ask the user to relaunch OpenCode from their user-created linked worktree. If it returns `mode: "non-git"`, permit dry-run inventory only and stop before any durable run, edit, test, or dependency resolution.
14
+ - Parse user controls before work: `--dry-run`, `--target <version>`, and `--stop-after-hop`. Run `opencode-ruby-upgrader inventory`, `opencode-ruby-upgrader supply-chain`, and `opencode-ruby-upgrader git-capabilities` first. Pause for explicit review on shallow clones, sparse checkout, submodules, or LFS configuration. If the project is unsupported or has no recognized test adapter, stop with the detected evidence and ask for a test command; do not invent one. After official research pins the target, initialize the durable run with `opencode-ruby-upgrader begin --target <version>` (append `--dry-run` or `--stop-after-hop` when requested). Use only the returned report path for this run.
15
+ - Drive the durable state machine, not prose alone: record research with `record-research`, then transition `initialized → inventory_complete → research_complete`; record each complete evidence-backed hop only with `record-executed-iteration` (or `record-executed-rails-iteration`), transition to `hop_validated`, and use the returned checkpoint SHA when transitioning to `committed`. Exactly one checkpoint is required before the next iteration. Repeat per hop. Use `paused` to stop safely for user review/manual work (it releases the lock); `complete` is only valid once the pinned target is reached. Use `blocked` only with evidence and an actionable option. For an isolated legacy runtime, ask once for approval to apply the planned Ruby declaration edit and then run `prepare-target-runtime --ruby <x.y.z> --report <the run path you just resumed>` before `docker-bundle-rspec`: it creates isolated Docker resources, installs Node and Bundler 2.4.22, runs `bundle install`, and creates the isolated Rails test database when applicable. Execute preparation with the longest supported shell timeout (at least 15 minutes), not a default short timeout. Do not ask the user for a report path, container name, or environment variables. The persisted nonsecret runtime manifest binds labeled app and PostgreSQL containers to the selected run; `docker-bundle-rspec` executes only the fixed inner argv `bundle exec rspec`.
16
16
  - If the next Ruby hop is incompatible with the resolved Rails version, do not edit Rails as part of the Ruby hop. Cite the official compatibility evidence, explain the required Rails from/to versions, and obtain explicit approval before running `record-framework-bridge --report <report-path> --ruby-from <version> --ruby-to <version> --rails-from <version> --rails-to <version> --rationale <text> --citation 'title|https://...'`. Then transition the Ruby run to `blocked` and start `begin-rails-bridge --ruby-report <blocked-ruby-report>`. In that separate report, research contiguous Rails-minor hops with `record-rails-research`, record each passing hop with `record-executed-rails-iteration` including reviewed `bin/rails app:update` evidence, transition to `hop_validated`, and checkpoint only with `commit-rails-hop`. Start a fresh Ruby run only after the Rails bridge is complete.
17
17
  - Rails `app:update` executes with conflict-skipping semantics so existing application configuration is never overwritten noninteractively. Review every generated file before final tests. If a pending result is unsafe or superseded, revert only that unvalidated hop and run `discard-pending-app-update --report <report-path> --reason <review finding>`; the discarded digest remains durable evidence before a fresh attempt.
18
18
  - When a validated hop changes a lockfile, inspect the resolved dependency delta and license findings, then durably record both with `record-dependency-review --report <report-path> --compatibility <finding> --licenses <finding>` before requesting the checkpoint. Never bypass this gate merely because tests pass.
@@ -33,7 +33,7 @@ You own a careful Ruby runtime migration. Be decisive on routine fixes and trans
33
33
  6. Run the project’s existing focused and full tests. Also run the smallest meaningful smoke check: existing system/browser tests when present; otherwise a boot, health, request, or application-critical-flow test suited to the stack. Only add a smoke test after explaining why existing coverage is insufficient.
34
34
  7. Capture baseline and post-hop test count, duration, failures, coverage if available, smoke result, dependency changes, commands, and risks. Never silently retry a flaky test: record every attempt, timeout, and retry rationale. Add a 2–3 line explanation for every code or dependency fix: what changed, why it is correct, and any concern.
35
35
  8. Before committing, list every changed project file in `iteration.files` and explain it through `fixes` or dependency evidence. A changed Gemfile lock must have focused dependency-review evidence. When an iteration is complete and all required validation passes, run `opencode-ruby-upgrader transition --report <report-path> --phase hop_validated`, then `opencode-ruby-upgrader commit-hop --report <report-path>`, then transition to `committed`. The state transition intentionally updates the report for the next hop; do not make an additional SHA-only report edit. The commit trailer links the SHA back to the report.
36
- 9. Use `--dry-run` to inventory and return a no-write plan without edits or lock acquisition. Honor `--target <Ruby>` as the pinned target and `--stop-after-hop` by committing the validated hop then stopping cleanly. Continuing after that stop requires an explicit user review and `opencode-ruby-upgrader resume --report <report-path> --continue-after-hop`.
36
+ 9. Use `--dry-run` to inventory and return a no-write plan without edits or lock acquisition. Honor `--target <version>` as the pinned target and `--stop-after-hop` by committing the validated hop then stopping cleanly. Continuing after that stop requires an explicit user review and `opencode-ruby-upgrader resume --report <report-path> --continue-after-hop`.
37
37
  10. Do not continue to the next Ruby series while dependency resolution or relevant tests fail. Report the blocker with reproduction steps and options: stop safely, supply a project constraint, approve a narrow compatibility/framework change, manually resolve the blocker, or explicitly permit a reviewed hook/private source/broad lockfile change.
38
38
 
39
39
  ## Durable run record
@@ -13,13 +13,13 @@ const option = (name) => { const index = args.indexOf(name); return index >= 0 ?
13
13
  const options = (name) => args.flatMap((argument, index) => argument === name && args[index + 1] ? [args[index + 1]] : []);
14
14
  const reportOption = () => option("--report");
15
15
  const citation = (value) => { const [title, url] = (value ?? "").split("|"); return { title, url }; };
16
- const usage = "Usage: opencode-ruby-upgrader <preflight|dashboard|begin|begin-rails-bridge|prepare-target-runtime|status|transition|record-research|record-rails-research|record-risk|record-framework-bridge|record-executed-iteration|record-executed-rails-iteration|discard-pending-app-update|record-dependency-review|inventory|supply-chain|git-capabilities|commit-hop|commit-rails-hop|resume|release-lock> [--help]";
16
+ const usage = "Usage: opencode-ruby-upgrader <preflight|dashboard|begin|begin-rails-bridge|prepare-target-runtime|status|transition|record-research|record-rails-research|record-risk|record-framework-bridge|record-executed-iteration|record-executed-rails-iteration|discard-pending-app-update|discard-last-rails-iteration|record-dependency-review|inventory|supply-chain|git-capabilities|commit-hop|commit-rails-hop|resume|release-lock> [--help]";
17
17
  if (command === "help" || args.includes("--help")) {
18
18
  console.log(`${usage}\n\nUse status --summary for a concise report view. release-lock is stale-session recovery only and requires --force.`);
19
19
  } else if (command === "preflight") {
20
20
  const result = inspectWorktree();
21
21
  if (args.includes("--json")) console.log(JSON.stringify(result, null, 2));
22
- else if (result.ok && result.mode === "non-git") console.log("✓ No Git repository detected; proceeding without Git checkpoints or worktree isolation.");
22
+ else if (result.ok && result.mode === "non-git") console.log("No Git repository detected. Dry-run inventory is available, but durable migrations require a linked Git worktree.");
23
23
  else if (result.ok) console.log(`✓ Linked worktree: ${result.root}\n✓ Branch: ${result.branch}\n✓ Starting commit: ${result.sha}\n✓ Remote writes: disabled`);
24
24
  else console.error(`${setupInstructions(result)}\n\nPreflight blocked: ${result.reason}`);
25
25
  process.exitCode = result.ok ? 0 : 1;
@@ -28,14 +28,14 @@ if (command === "help" || args.includes("--help")) {
28
28
  const address = server.address();
29
29
  console.log(`Ruby Upgrade Workspace: http://127.0.0.1:${address.port}`);
30
30
  } else if (command === "begin") {
31
- try { console.log(JSON.stringify(beginRun({ target: option("--target"), dryRun: args.includes("--dry-run"), stopAfterHop: args.includes("--stop-after-hop"), allowNonGit: args.includes("--allow-non-git") }), null, 2)); }
31
+ try { console.log(JSON.stringify(beginRun({ target: option("--target"), dryRun: args.includes("--dry-run"), stopAfterHop: args.includes("--stop-after-hop") }), null, 2)); }
32
32
  catch (error) { console.error(`Run start blocked: ${error.message}`); process.exitCode = 1; }
33
33
  } else if (command === "begin-rails-bridge") {
34
- try { console.log(JSON.stringify(beginRailsBridgeRun({ rubyReportPath: option("--ruby-report"), dryRun: args.includes("--dry-run"), stopAfterHop: args.includes("--stop-after-hop"), allowNonGit: args.includes("--allow-non-git") }), null, 2)); }
34
+ try { console.log(JSON.stringify(beginRailsBridgeRun({ rubyReportPath: option("--ruby-report"), dryRun: args.includes("--dry-run"), stopAfterHop: args.includes("--stop-after-hop") }), null, 2)); }
35
35
  catch (error) { console.error(`Rails bridge start blocked: ${error.message}`); process.exitCode = 1; }
36
36
  } else if (command === "prepare-target-runtime") {
37
37
  try {
38
- if (!option("--ruby") || args.some((argument) => !["--ruby", "--report", option("--ruby"), option("--report")].includes(argument))) throw new Error("Usage: prepare-target-runtime --ruby <x.y.z> [--report .ruby-upgrades/runs/<run>.json]");
38
+ if (!option("--ruby")) throw new Error("Usage: prepare-target-runtime --ruby <x.y.z> [--report .ruby-upgrades/runs/<run>.json]");
39
39
  console.log(JSON.stringify(prepareTargetRuntime({ ruby: option("--ruby"), reportPath: option("--report") }), null, 2));
40
40
  } catch (error) { console.error(`Target runtime preparation blocked: ${error.message}`); process.exitCode = 1; }
41
41
  } else if (command === "status") {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "opencode-ruby-upgrader",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "A safe, evidence-driven Ruby runtime upgrade agent and local migration dashboard for OpenCode",
5
- "author": "Priscilla Cournoyer",
5
+ "author": "lilla021",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/lilla021/opencode-ruby-upgrader.git"
@@ -20,6 +20,9 @@
20
20
  "test": "node --test tests/*.test.js",
21
21
  "test:opencode": "node tests/opencode-runtime-smoke.mjs"
22
22
  },
23
+ "allowScripts": {
24
+ "opencode-ai@1.18.30": true
25
+ },
23
26
  "files": ["src", "agents", "bin", "README.md", "LICENSE", "SECURITY.md", "PRIVACY.md", "RELEASING.md", "RELEASE_CHECKLIST.md", "RELEASE_NOTES.md", "E2E_EVIDENCE.md"],
24
27
  "keywords": ["opencode", "opencode-plugin", "ruby", "rails", "upgrade", "worktree", "migration"],
25
28
  "engines": { "node": ">=22.5.0" },
package/src/controller.js CHANGED
@@ -31,18 +31,19 @@ const requiredRisksFor = (supplyChain, gitCapabilities) => [
31
31
  ...(gitCapabilities.shallow ? ["shallow-clone"] : []), ...(gitCapabilities.sparseCheckout ? ["sparse-checkout"] : []),
32
32
  ...(gitCapabilities.submodules ? ["submodules"] : []), ...(gitCapabilities.lfsConfigured ? ["git-lfs"] : [])
33
33
  ];
34
- const assertApprovedRisks = (run) => {
34
+ const hasUnapprovedRisks = (run) => {
35
35
  const decisions = new Map(run.riskDecisions.map((risk) => [risk.risk, risk.decision]));
36
- if ((run.requiredRisks ?? []).some((risk) => decisions.get(risk) !== "approved") || [...decisions.values()].some((decision) => decision !== "approved")) throw new Error("Resolve every detected risk with an explicit approval before recording a routine iteration.");
36
+ return (run.requiredRisks ?? []).some((risk) => decisions.get(risk) !== "approved") || [...decisions.values()].some((decision) => decision !== "approved");
37
37
  };
38
+ const assertApprovedRisks = (run) => { if (hasUnapprovedRisks(run)) throw new Error("Resolve every detected risk with an explicit approval before recording a routine iteration."); };
38
39
 
39
- export function beginRun({ root = process.cwd(), target, dryRun = false, stopAfterHop = false, allowNonGit = false } = {}) {
40
+ export function beginRun({ root = process.cwd(), target, dryRun = false, stopAfterHop = false } = {}) {
40
41
  if (!target || !rubyVersion.test(target)) throw new Error("Provide --target as a Ruby version such as 3.4 or 3.4.1.");
41
42
  const preflight = inspectWorktree(root); const inventory = inventoryProject(root); const supplyChain = inspectSupplyChain(root); const gitCapabilities = inspectGitCapabilities(root);
42
43
  const plan = { preflight, inventory, supplyChain, gitCapabilities, targetRuby: target, dryRun, stopAfterHop };
43
- if (dryRun || !preflight.ok) return plan;
44
+ if (dryRun) return plan;
45
+ if (!preflight.ok || preflight.mode !== "linked-worktree") throw new Error("A durable migration requires a supported linked Git worktree.");
44
46
  if (!inventory.supported || inventory.requiresDecision) throw new Error("A durable migration requires a Gemfile and a recognized executable test adapter. Supply an explicit validation command through a future reviewed adapter instead of guessing.");
45
- if (preflight.mode === "non-git" && !allowNonGit) throw new Error("Non-Git runs need explicit consent: rerun begin with --allow-non-git after reviewing the loss of worktree isolation and checkpoint commits.");
46
47
  const reportPath = path.join(".ruby-upgrades", "runs", runName());
47
48
  const requiredRisks = requiredRisksFor(supplyChain, gitCapabilities);
48
49
  const report = {
@@ -57,15 +58,15 @@ export function beginRun({ root = process.cwd(), target, dryRun = false, stopAft
57
58
 
58
59
  export function runStatus({ root = process.cwd(), reportPath }) { return readRun(root, reportPath); }
59
60
 
60
- export function beginRailsBridgeRun({ root = process.cwd(), rubyReportPath, dryRun = false, stopAfterHop = false, allowNonGit = false } = {}) {
61
+ export function beginRailsBridgeRun({ root = process.cwd(), rubyReportPath, dryRun = false, stopAfterHop = false } = {}) {
61
62
  const rubyRun = readRun(root, rubyReportPath);
62
63
  if (rubyRun.reportType === "rails_bridge" || rubyRun.phase !== "blocked" || rubyRun.status !== "blocked" || !rubyRun.frameworkBridge) throw new Error("A Rails bridge can start only from a blocked Ruby run with an approved compatibility bridge.");
63
64
  const preflight = inspectWorktree(root); const inventory = inventoryProject(root); const supplyChain = inspectSupplyChain(root); const gitCapabilities = inspectGitCapabilities(root); const bridge = rubyRun.frameworkBridge;
64
65
  const plan = { preflight, inventory, supplyChain, gitCapabilities, bridge, dryRun, stopAfterHop };
65
- if (dryRun || !preflight.ok) return plan;
66
+ if (dryRun) return plan;
67
+ if (!preflight.ok || preflight.mode !== "linked-worktree") throw new Error("A durable Rails bridge requires a supported linked Git worktree.");
66
68
  if (!inventory.rails?.resolvedVersion || series(inventory.rails.resolvedVersion) !== series(bridge.railsFrom)) throw new Error("The current Gemfile.lock must still resolve the Rails version recorded by the blocked Ruby run.");
67
- if (preflight.mode === "non-git" && !allowNonGit) throw new Error("Non-Git Rails bridges need explicit consent: rerun with --allow-non-git.");
68
- if (preflight.mode !== "non-git" && (preflight.branch !== rubyRun.branch || preflight.sha !== rubyRun.expectedHead)) throw new Error("Rails bridge must start on the blocked Ruby run's recorded branch and checkpoint SHA.");
69
+ if (preflight.branch !== rubyRun.branch || preflight.sha !== rubyRun.expectedHead) throw new Error("Rails bridge must start on the blocked Ruby run's recorded branch and checkpoint SHA.");
69
70
  const reportPath = path.join(".ruby-upgrades", "runs", runName());
70
71
  const report = { schemaVersion: 2, validationReceiptsRequired: true, reportType: "rails_bridge", runId: crypto.randomUUID(), title: `Rails bridge ${bridge.railsFrom} to ${bridge.railsTo}`, status: "in_progress", phase: "initialized", startedAt: new Date().toISOString(), targetRails: bridge.railsTo, targetRailsPinnedAt: new Date().toISOString(), branch: preflight.branch ?? null, worktreeRoot: preflight.root ?? root, startingSha: preflight.sha ?? null, expectedHead: preflight.sha ?? null, control: { stopAfterHop }, inventory, supplyChain, gitCapabilities, bridge: { rubyReportPath, rubyRunId: rubyRun.runId, rubyFrom: bridge.rubyFrom, rubyTo: bridge.rubyTo, railsFrom: bridge.railsFrom, railsTo: bridge.railsTo, approvedAt: bridge.recordedAt }, research: { ladder: [], citations: [] }, riskDecisions: [], requiredRisks: requiredRisksFor(supplyChain, gitCapabilities), summary: ["Rails bridge initialized from blocked Ruby compatibility decision."], iterations: [], sessionSummary: "" };
71
72
  const lock = acquireRunLock(root, reportPath); report.lockNonce = lock.nonce;
@@ -215,7 +216,7 @@ function assertCompletion(run) {
215
216
  const target = run.reportType === "rails_bridge" ? run.targetRails : run.targetRuby;
216
217
  if (!final || series(final.to) !== series(target)) throw new Error("A run can complete only after the final validated iteration reaches its pinned target.");
217
218
  if (series(run.research.ladder.at(-1)) !== series(target)) throw new Error("Research ladder does not reach its pinned target.");
218
- if (run.riskDecisions.some((risk) => risk.decision !== "approved")) throw new Error("Unresolved risks prevent completion.");
219
+ if (hasUnapprovedRisks(run)) throw new Error("Unresolved risks prevent completion.");
219
220
  if (final.checkpointSha !== run.expectedHead) throw new Error("The final iteration must be committed through the checkpoint gate before completion.");
220
221
  }
221
222
 
package/src/preflight.js CHANGED
@@ -57,13 +57,26 @@ export function inspectGitCapabilities(cwd = process.cwd()) {
57
57
  const root = git(cwd, ["rev-parse", "--show-toplevel"]);
58
58
  const value = (args, fallback = false) => { try { return git(cwd, args); } catch { return fallback; } };
59
59
  const submodules = fs.existsSync(path.join(root, ".gitmodules"));
60
+ const lfsAttributes = (() => {
61
+ // Repo-local LFS signal: the committed root .gitattributes declaring
62
+ // filter=lfs. Read from the object database (not the working file) so
63
+ // results hold even where git applies the declared smudge filter on
64
+ // checkout (e.g. hosts with git-lfs installed system-wide).
65
+ try { return git(cwd, ["show", "HEAD:.gitattributes"]).split("\n").some((line) => !line.trim().startsWith("#") && /\bfilter\s*=\s*lfs\b/i.test(line)); }
66
+ catch { return false; }
67
+ })();
68
+ const shallow = value(["rev-parse", "--is-shallow-repository"]) === "true";
69
+ // Capability detection reads repository-local state only, so ambient
70
+ // global/system git config cannot leak machine-specific risks into a run.
71
+ const sparseCheckout = value(["config", "--local", "--bool", "core.sparseCheckout"]) === "true";
72
+ const lfsConfigured = value(["config", "--local", "--get-regexp", "^filter\\.lfs\\."]) !== false || lfsAttributes;
60
73
  return {
61
74
  supported: true,
62
- shallow: value(["rev-parse", "--is-shallow-repository"]) === "true",
63
- sparseCheckout: value(["config", "--bool", "core.sparseCheckout"]) === "true",
75
+ shallow,
76
+ sparseCheckout,
64
77
  submodules,
65
- lfsConfigured: value(["config", "--get-regexp", "^filter\\.lfs\\."]) !== false,
66
- recommendation: submodules || value(["rev-parse", "--is-shallow-repository"]) === "true" || value(["config", "--bool", "core.sparseCheckout"]) === "true" ? "Pause for repository-topology review before migration." : "Standard Git topology."
78
+ lfsConfigured,
79
+ recommendation: submodules || shallow || sparseCheckout ? "Pause for repository-topology review before migration." : "Standard Git topology."
67
80
  };
68
81
  } catch { return { supported: false, recommendation: "Not a Git repository." }; }
69
82
  }