Skip to content

Feat(installer): Phase 4 install/update integration#3402

Merged
trek-e merged 6 commits into
mainfrom
codex/installer-migrations-phase-four
May 11, 2026
Merged

Feat(installer): Phase 4 install/update integration#3402
trek-e merged 6 commits into
mainfrom
codex/installer-migrations-phase-four

Conversation

@trek-e
Copy link
Copy Markdown
Collaborator

@trek-e trek-e commented May 11, 2026

Feature PR

Linked Issue

Closes #3395

Linked issue labels: approved-feature, in-progress

Approved Issue Description

What to build

Build a first-class Installer Migration Module that replaces ad hoc install cleanup with a planned, journaled, reviewable migration system.

This should future-proof GSD updates when features move, rename, replace, or retire installed files. The migration tool must clean up stale GSD-owned artifacts, preserve user-owned data, back up locally modified managed files, support dry-run planning, and avoid leaving hybrid installs after failures.

Design references:

  • docs/installer-migrations.md
  • docs/adr/0008-installer-migration-module.md

Phases

Phase 1: Foundational migration runner

Create the core migration infrastructure in one coherent pass:

  • install-state read/write next to gsd-file-manifest.json
  • migration record discovery
  • checksum tracking
  • pending/applied migration detection
  • dry-run planning
  • shared action model
  • managed/user-owned/unknown artifact classification
  • journaled apply and rollback helpers

Phase 2: Port existing cleanup behavior

Move one or more existing installer cleanup paths into the migration runner:

  • orphaned hook/file cleanup
  • stale runtime-generated artifacts
  • config marker/section cleanup where ownership is provable

Phase 3: First-time baseline scanner

Add a baseline migration for existing installs that predate migration tracking:

  • scan known install surfaces
  • classify files
  • preserve unknowns by default
  • report ambiguous artifacts
  • offer user choices in interactive mode
  • block or dry-run in non-interactive mode when destructive action is ambiguous

Phase 4: Install/update integration

Wire the migration runner into normal install/update flow:

  • run plan before package materialization
  • apply safe migrations
  • write install state after success
  • report removed, backed up, preserved, skipped, and blocked actions
  • keep existing install transforms working during migration adoption

Phase 5: Authoring guardrails

Add contributor-facing guardrails:

  • tests for migration records
  • dry-run/apply regression tests
  • user-owned preservation tests
  • locally modified managed-file tests
  • docs for when a feature retirement requires a migration

Acceptance criteria

  • Installer migrations are versioned and tracked in install state.
  • The same planner powers dry-run and apply behavior.
  • Managed, user-owned, and unknown artifacts are classified consistently.
  • Unknown files are preserved by default.
  • Locally modified managed files are backed up before removal or replacement.
  • Migration apply writes a rollback journal and restores touched paths on failure where possible.
  • At least one existing ad hoc cleanup path is ported into the migration runner.
  • Existing installs can run a baseline scan without losing user data.
  • Install/update output clearly reports what changed and what was preserved.
  • Tests cover fresh install, reinstall, upgrade, modified managed files, user-owned files, unknown files, and failed apply rollback.

Blocked by

None - can start immediately

Standards Followed

  • CONTEXT.md: contributor gate order, changeset PR-number rule, and domain vocabulary requirements.
  • CONTRIBUTING.md: issue-first process, typed PR template, closing keyword, one-concern scope, changeset fragment, and test requirements.
  • docs/contributor-standards.md: CONTEXT/ADR hierarchy, isolated worktree expectation, AI-assisted PR body requirements, and architecture-aware testing guidance.
  • docs/adr/0008-installer-migration-module.md: Installer Migration Module decision, runtime contract decision, and migration authoring contract.
  • docs/installer-migrations.md: migration architecture, runtime configuration contract registry, ownership boundaries, source snapshots, and Phase 4 requirements.

Feature Summary

This PR implements Phase 4: Install/update integration of the approved first-class Installer Migration Module feature.

What Changed

  • Wired installer migrations into normal install/update before package materialization for every supported runtime.
  • Applies safe migrations, blocks ambiguous user-choice actions, reports changed/preserved/skipped/blocked actions, and writes install state after success.
  • Rolls back applied migrations when package materialization or multi-runtime finalization fails.
  • Cleans generated migration journal, rollback, backup, and lock artifacts so failed installs do not leave stale installer state behind.
  • Preserves user data by keeping unknown files, backing up locally modified managed files under run-scoped journal paths, and restoring touched files on rollback.
  • Fails closed on malformed install state instead of treating corrupt state as “no migrations applied.”
  • Collapses first-time baseline report output into count rows while keeping destructive and blocked actions path-specific.
  • Reuses discovered migration records across multi-runtime installs and avoids repeated checksum/sort/hash work in hot paths.
  • Keeps existing install transforms working during migration adoption.
  • Added code comments beside runtime-sensitive blocks linking to the researched runtime contract, ownership, and Phase 4 install/update documentation.

Spec Compliance For This Phase

  • Run plan before package materialization.
  • Apply safe migrations.
  • Write install state after success.
  • Report removed, backed up, preserved, skipped, and blocked actions.
  • Keep existing install transforms working during migration adoption.
  • The above behavior is covered for every supported runtime, not only Codex.

Testing

  • TMPDIR=/private/tmp node --test tests/installer-migrations.test.cjs tests/installer-migration-install-integration.test.cjs tests/installer-migration-report.test.cjs tests/bug-3357-codex-legacy-hooks-json-migration.test.cjs passed with 69 tests and 0 failures.
  • TMPDIR=/private/tmp npm run lint:tests passed.
  • TMPDIR=/private/tmp npm run lint:descriptions passed.
  • TMPDIR=/private/tmp npm run lint:changeset passed.
  • git diff --check passed.
  • TMPDIR=/private/tmp npm test passed with 8,795 tests and 0 failures.

Platforms Tested

  • macOS
  • Windows (including backslash path handling)
  • Linux

Runtimes Tested

  • Claude Code
  • Gemini CLI
  • OpenCode
  • Codex
  • Copilot
  • Other: Antigravity, Augment, Cline, CodeBuddy, Cursor, Hermes Agent, Kilo, Qwen Code, Trae, Windsurf.

Scope Confirmation

Checklist

  • Issue linked above with Closes #3395.
  • Linked issue has the approved-feature label.
  • Feature PR body uses the repo-required feature template structure.
  • PR title uses required typed scope prefix: Feat(installer):.
  • Phase-specific .changeset/ fragment added: .changeset/vivid-foxes-romp.md.
  • Documentation updated where this phase changes architecture, behavior, or authoring rules.
  • Tests added or updated for this phase.
  • No unnecessary external dependencies added.
  • Windows manually tested.
  • Linux manually tested.

Breaking Changes

None.

Phase 4: Install/update integration

This final section is intentionally appended after the approved issue description so reviewers can compare the whole approved feature request above against the concrete phase delivered by this PR.

Summary by CodeRabbit

  • New Features

    • Integrated installer migrations into the standard install flow across all supported runtimes, including baseline configuration scanning, action reporting, rollback support on failure, and persistent install-state tracking.
    • Added legacy file cleanup during installation.
  • Bug Fixes

    • Fixed SDK flag wiring when throwOnFailure is enabled.
  • Tests

    • Added installer migration integration and unit test coverage.

Review Change Stack

@trek-e trek-e requested a review from glittercowboy as a code owner May 11, 2026 13:12
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This PR implements Phases 3–4 of the installer migrations feature, integrating baseline scanning and install-flow wiring. It introduces migration planning and execution with transactional rollback, reporting, and integration into the multi-runtime installer to run migrations before package materialization.

Changes

Installer Migrations Phase 3–4: Baseline Scanning and Install Integration

Layer / File(s) Summary
Migration Reporting API
get-shit-done/bin/lib/installer-migration-report.cjs
Exports summarizeInstallerMigrationResult(result) and assertInstallerMigrationsUnblocked(result) to map action types to user-facing labels, aggregate baseline rows into summary counts, and enforce unblocked state.
Core Migration Infrastructure
get-shit-done/bin/lib/installer-migrations.cjs
716-line module exporting discovery, planning, apply, and run functions; implements artifact classification, manifest/state I/O, migration checksumming, lock-guarded concurrency, journaled apply/rollback with file backup and JSON rewrite support.
First-Time Baseline Migration
get-shit-done/bin/lib/installer-migrations/000-first-time-baseline.cjs
Enumerates runtime install surfaces, classifies discovered artifacts, records baselines for managed files, preserves user-owned/unknown artifacts, and emits prompt-user actions for stale GSD-looking artifacts.
Legacy Orphan Files Cleanup Migration
get-shit-done/bin/lib/installer-migrations/001-legacy-orphan-files.cjs
Declares legacy orphan hook file paths and emits remove-managed actions for managed-pristine or managed-modified classifications.
Install Flow Wiring
bin/install.js
Discovers migrations once in installAllRuntimes, passes into each install() via options, runs per-runtime migrations before materialization, reports results, asserts unblocked state, and integrates rollback across completed runtimes on failure.
Documentation & Inventory
docs/installer-migrations.md, docs/INVENTORY.md, docs/INVENTORY-MANIFEST.json
Adds Phase 4 install integration section describing baseline scanning, action projection, safe apply before materialization, install-state persistence, and blocked-action handling; updates module inventory.
Reporting Tests
tests/installer-migration-report.test.cjs
Tests summarizeInstallerMigrationResult row output, baseline aggregation, and assertInstallerMigrationsUnblocked error behavior.
Core Migration Tests
tests/installer-migrations.test.cjs
Tests baseline scanning, legacy orphan planning, modified-file backups with hash-suffixed journal paths, rollback/restore, lock enforcement, checksum evaluation, and install-state JSON validation.
End-to-End Integration Tests
tests/installer-migration-install-integration.test.cjs
Tests migration reporting before materialization, baseline blocking on user-choice artifacts, rollback on materialization failure (non-Codex and multi-runtime), per-runtime orphan cleanup, and GSD-looking artifact blocking.
Release Notes
.changeset/vivid-foxes-romp.md
Changeset documenting installer migrations wiring with baseline scanning, collapsed reporting, blocked-action protection, transactional rollback, and install-state persistence.
SDK Regression Test
tests/bug-3033-sdk-flag-wired.test.cjs
Regression test for installSdkIfNeeded with throwOnFailure: true when SDK dist is missing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • gsd-build/get-shit-done#3395: This PR directly implements Phases 3–4 of the installer migration feature specified in the linked issue, completing baseline scanning and install-flow integration objectives.

Possibly related PRs

  • gsd-build/get-shit-done#3398: Main PR builds on and integrates the installer-migrations system introduced in PR #3398—both modify bin/install.js and consume installer-migrations modules with the main PR wiring Phase 4 install-time baseline/reporting/rollback into the install flow.
  • gsd-build/get-shit-done#3203: Both PRs modify bin/install.js's installer entry flow and installAllRuntimes call sites, so the changes are directly related.

Suggested labels

area: installer

Suggested reviewers

  • glittercowboy
  • Ari4ka

Poem

A rabbit hops through fresh migrations,
Scanning baselines with precision,
Orphaned files cleaned with care,
Blocked actions block what shouldn't change,
Rollback journeys safe and sound! 🐰✨

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feat(installer): Phase 4 install/update integration' clearly and specifically summarizes the main change—implementing Phase 4 of the installer migration feature.
Description check ✅ Passed The PR description follows the required feature template structure with linked issue, approved issue description, detailed phases, acceptance criteria, comprehensive scope confirmation, and complete checklist.
Linked Issues check ✅ Passed The implementation delivers all Phase 4 requirements from #3395: wiring migration runner before package materialization, applying safe migrations, blocking ambiguous actions, reporting outcomes, writing install state after success, and rolling back on failures.
Out of Scope Changes check ✅ Passed All changes are directly aligned with Phase 4 scope: bin/install.js integration, migration reporting, baseline scanning, integration tests, documentation updates, and inventory metadata—no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/installer-migrations-phase-four

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added size/L and removed size/L labels May 11, 2026
@trek-e trek-e changed the title Wire installer migrations into install flow Feature Phase 4: Install/update integration May 11, 2026
@trek-e trek-e changed the base branch from codex/installer-migrations-phase-three to main May 11, 2026 13:18
@trek-e trek-e changed the title Feature Phase 4: Install/update integration Feat(installer): Phase 4 install/update integration May 11, 2026
@github-actions github-actions Bot added size/XL and removed size/L labels May 11, 2026
@github-actions github-actions Bot added size/XL and removed size/XL labels May 11, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
tests/installer-migration-install-integration.test.cjs (1)

60-72: 💤 Low value

captureConsole only intercepts console.log/console.warn.

If bin/install.js ever emits the “Installer migrations” banner or row labels through process.stdout.write, console.info, or console.error, the in-process tests (lines 127-138, 145-153) will silently see an empty/partial output and the assert.match could fail in confusing ways or pass for the wrong reason. The CLI-mode tests (line 215+) are unaffected because they use spawnSync and read combined stdout/stderr.

Worth either widening the patch to cover console.info/console.error (and optionally process.stdout.write) or documenting the assumption.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/installer-migration-install-integration.test.cjs` around lines 60 - 72,
The captureConsole helper currently only overrides console.log and console.warn;
update captureConsole to also intercept console.info and console.error and
optionally process.stdout.write so tests capture all banner output from
bin/install.js: save originals (console.info, console.error, and
process.stdout.write), override them to push their stringified output into the
existing lines array (keeping the same join behavior), call fn(), then restore
all originals in the finally block; reference the captureConsole function to
make these changes so in-process tests correctly assert on installer output.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bin/install.js`:
- Around line 10615-10623: The finalize() catch/rollback path is bypassed
because installSdkIfNeeded() calls process.exit(1) on failure; change
installSdkIfNeeded() to surface failures by throwing an Error or returning a
rejected Promise instead of calling process.exit(1) so finalize() can catch it
and call rollbackFinalizedInstallerMigrations(); update any callers that expect
exit behavior to propagate the error (e.g., in the same module where finalize()
invokes installSdkIfNeeded()) and remove direct process.exit calls from
installSdkIfNeeded() so abort logic is centralized in finalize().
- Around line 10589-10591: The loop over runtimes that calls install(isGlobal,
runtime, { installerMigrations }) must be wrapped in a try/catch so that if any
install() throws, you iterate the previously pushed results array in reverse and
invoke their rollback/cleanup method (the rollback handle returned by install())
before rethrowing the original error; use the runtimes, results and install
identifiers to locate the code and ensure rollback calls are awaited/handled in
reverse order and that the caught error is rethrown after cleanup.

In `@get-shit-done/bin/lib/installer-migrations.cjs`:
- Around line 625-633: The zero-action branch returns early without persisting
that pending migrations were evaluated; update the branch that checks
plan.actions.length === 0 so it records plan.pendingMigrationIds into the
install state (the same place where successful runs persist
appliedMigrationIds/journalRelPath in gsd-install-state.json), set completed =
true and return the object with appliedMigrationIds: plan.pendingMigrationIds
and journalRelPath: null (keeping the plan), ensuring the state write path used
elsewhere in this module (the code that updates gsd-install-state.json after
normal runs) is invoked or duplicated so pendingMigrationIds are saved.
- Around line 492-553: The journal is only written after performing filesystem
mutations; persist a prewritten journal before any destructive action and update
it after each action so recovery can resume. Specifically, before iterating
plan.actions in the function that uses journal, journalPath, plan.actions,
journalAction, rollback, rollbackRootRelPath and backupRootRelPath, create the
journal directory and write an initial journal entry (e.g., status "in-progress"
or actions marked "pending") to journalPath; then after each successful action
commit (where the code currently pushes journal.actions and does
fs.copyFileSync/fs.rmSync/writeFileAtomicSync), immediately fs.writeFileSync (or
writeFileAtomicSync) the updated journal to journalPath to flush the new state;
ensure you create the directory once (fs.mkdirSync(path.dirname(journalPath), {
recursive: true })) before the loop and use durable writes so a crash still
leaves a recoverable journal.
- Around line 193-199: The current flatMap logic sets checksum =
`sha256File(source)` and applies it to every exported record, coupling siblings
to one file checksum; change the handling in the flatMap/export processing so
that if exported is an array you either (a) reject array exports with an
explicit error (i.e., disallow multi-exports) or (b) derive a unique per-record
checksum instead of the file hash — for example call sha256 on a stable
serialization of each record (or combine the file hash with a record-specific
identifier/index) and pass record.checksum || derivedChecksum into
validateMigrationRecord; update the code around exported, records, sha256File
and validateMigrationRecord to implement the chosen approach.
- Around line 88-97: normalizeRelPath currently allows inputs like "." or
"hooks/.." which normalize to the config root and later cause EISDIR; update
normalizeRelPath to run a posix-safe normalization (e.g., path.posix.normalize
after replacing backslashes), then reject any result that is empty, equal to "."
or "..", starts with "/" or with "..", or contains a ".." path segment (e.g.,
normalized.split('/').includes('..')); this ensures values that would resolve to
the configDir root are rejected before ensureInsideConfig and file operations.

In `@tests/bug-2771-user-profile-manifest.test.cjs`:
- Around line 239-258: The test leaks a file because `outside` is created as a
sibling of `tmpDir` in the system temp root and only `cleanup(tmpDir)` is
called; fix by isolating the external file into a second temp directory you
control (e.g., create a new temp dir variable for the "outside" file) or ensure
the test removes it by unlinking `outside` in the test teardown (e.g., in
`afterEach`), updating references to `outside` and the manifest content where
`MANIFEST_NAME` and `mod.saveLocalPatches(tmpDir)` are used so the cleanup
removes all created temp artifacts.

---

Nitpick comments:
In `@tests/installer-migration-install-integration.test.cjs`:
- Around line 60-72: The captureConsole helper currently only overrides
console.log and console.warn; update captureConsole to also intercept
console.info and console.error and optionally process.stdout.write so tests
capture all banner output from bin/install.js: save originals (console.info,
console.error, and process.stdout.write), override them to push their
stringified output into the existing lines array (keeping the same join
behavior), call fn(), then restore all originals in the finally block; reference
the captureConsole function to make these changes so in-process tests correctly
assert on installer output.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ddf992a4-ae6b-48af-81a5-c1a26c0e5c0b

📥 Commits

Reviewing files that changed from the base of the PR and between 574d1f4 and 7fe75c2.

📒 Files selected for processing (22)
  • .changeset/gentle-jays-zip.md
  • .changeset/merry-quails-roam.md
  • .changeset/steady-yaks-caper.md
  • .changeset/vivid-foxes-romp.md
  • bin/install.js
  • docs/ARCHITECTURE.md
  • docs/INVENTORY-MANIFEST.json
  • docs/INVENTORY.md
  • docs/README.md
  • docs/adr/0008-installer-migration-module.md
  • docs/adr/README.md
  • docs/installer-migrations.md
  • get-shit-done/bin/lib/installer-migration-report.cjs
  • get-shit-done/bin/lib/installer-migrations.cjs
  • get-shit-done/bin/lib/installer-migrations/000-first-time-baseline.cjs
  • get-shit-done/bin/lib/installer-migrations/001-legacy-orphan-files.cjs
  • get-shit-done/bin/lib/installer-migrations/002-codex-legacy-hooks-json.cjs
  • tests/bug-2771-user-profile-manifest.test.cjs
  • tests/bug-3357-codex-legacy-hooks-json-migration.test.cjs
  • tests/installer-migration-install-integration.test.cjs
  • tests/installer-migration-report.test.cjs
  • tests/installer-migrations.test.cjs

Comment thread bin/install.js Outdated
Comment thread bin/install.js
Comment thread get-shit-done/bin/lib/installer-migrations.cjs
Comment thread get-shit-done/bin/lib/installer-migrations.cjs Outdated
Comment thread get-shit-done/bin/lib/installer-migrations.cjs
Comment thread get-shit-done/bin/lib/installer-migrations.cjs
Comment thread tests/bug-2771-user-profile-manifest.test.cjs Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
docs/installer-migrations.md (1)

272-275: ⚡ Quick win

Document failure-time cleanup of migration artifacts explicitly.

This rollback section should also state that run-scoped journal/rollback/backup/lock artifacts are cleaned on failure, matching the Phase 4 behavior and preventing ambiguity for maintainers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/installer-migrations.md` around lines 272 - 275, Update the rollback
section to explicitly document failure-time cleanup: state that when an apply
step fails the executor uses the journal to restore modified paths and also
removes any run-scoped artifacts (journal, rollback metadata, backups, and
locks) created for that installer run, matching the Phase 4 cleanup behavior so
maintainers know these files are cleaned on failure and not left behind.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/installer-migrations.md`:
- Around line 261-264: Update the sentence that currently begins "with
`baselineScan: true`, reports the projected action rows..." to explicitly state
that the install-state is persisted only after package
materialization/finalization completes successfully; reference the Phase 4
success semantics and the earlier execution order (the lines describing applying
actions and materialization) and change wording so it cannot be read as "written
after migration apply"—make clear that if materialization/finalization fails or
runner returns blocked user-choice actions, install-state is not written.

---

Nitpick comments:
In `@docs/installer-migrations.md`:
- Around line 272-275: Update the rollback section to explicitly document
failure-time cleanup: state that when an apply step fails the executor uses the
journal to restore modified paths and also removes any run-scoped artifacts
(journal, rollback metadata, backups, and locks) created for that installer run,
matching the Phase 4 cleanup behavior so maintainers know these files are
cleaned on failure and not left behind.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ecb08f94-12b4-4108-8307-da45cb1bd6ea

📥 Commits

Reviewing files that changed from the base of the PR and between 7fe75c2 and f5510fe.

📒 Files selected for processing (1)
  • docs/installer-migrations.md

Comment thread docs/installer-migrations.md
@github-actions github-actions Bot added size/XL and removed size/XL labels May 11, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
get-shit-done/bin/lib/installer-migrations.cjs (1)

497-559: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Flush the journal after each committed action.

The journal is pre-created now, but it is still only rewritten once after the loop. If the process dies after a remove/rewrite and before Line 559, the rollback snapshots exist but the durable journal does not describe what was already applied.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@get-shit-done/bin/lib/installer-migrations.cjs` around lines 497 - 559, The
journal is only written once at the end (fs.writeFileSync(journalPath...)), so
if the process dies mid-loop applied actions won’t be durable; after each
committed action update (every place you push into journal.actions inside the
loop — e.g., where journalAction is added for 'missing', 'recorded'/'preserved',
'removed', 'rewritten') immediately flush the journal to disk by calling the
same write used elsewhere (fs.writeFileSync(journalPath, JSON.stringify(journal,
null, 2) + '\n', 'utf8') or an atomic equivalent) so the journal reflects each
applied action as it happens.
🧹 Nitpick comments (2)
tests/bug-3033-sdk-flag-wired.test.cjs (2)

161-162: ⚡ Quick win

Use assert.strictEqual for consistency.

The rest of the test file uses assert.strictEqual (e.g., line 189-192), which is generally preferred as it enforces strict equality (===) rather than coercive equality (==).

♻️ Proposed fix
-        assert.equal(error.code, 'GSD_SDK_MISSING_DIST');
-        assert.equal(error.exitCode, 1);
+        assert.strictEqual(error.code, 'GSD_SDK_MISSING_DIST');
+        assert.strictEqual(error.exitCode, 1);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/bug-3033-sdk-flag-wired.test.cjs` around lines 161 - 162, Replace the
two coercive equality assertions that use assert.equal for error.code and
error.exitCode with strict equality assertions: change the calls referencing
error.code and error.exitCode (the assert.equal lines in the test block) to use
assert.strictEqual so the test enforces === semantics and matches the rest of
the file.

160-164: 💤 Low value

Consider simplifying the validator function.

The current validator uses assert.equal calls that throw on failure, which can produce confusing error messages when the assertion fails inside the validator. A cleaner pattern would be to check conditions and return a boolean, or pass an error object/class directly to assert.throws.

♻️ Alternative patterns

Option 1: Return boolean

     (error) => {
-      assert.equal(error.code, 'GSD_SDK_MISSING_DIST');
-      assert.equal(error.exitCode, 1);
-      return true;
+      return error.code === 'GSD_SDK_MISSING_DIST' && error.exitCode === 1;
     }

Option 2: Assert outside validator

+    let thrownError;
     assert.throws(
       () => captureConsole(() => {
         installSdkIfNeeded({ sdkDir, isLocal: true, forceSdk: true, throwOnFailure: true });
       }),
-      (error) => {
-        assert.equal(error.code, 'GSD_SDK_MISSING_DIST');
-        assert.equal(error.exitCode, 1);
-        return true;
-      }
+      (error) => { thrownError = error; return true; }
     );
+    assert.strictEqual(thrownError.code, 'GSD_SDK_MISSING_DIST');
+    assert.strictEqual(thrownError.exitCode, 1);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/bug-3033-sdk-flag-wired.test.cjs` around lines 160 - 164, The inline
validator arrow function currently uses assert.equal calls which throw inside
the validator; replace it with a pure boolean check that returns true when
error.code === 'GSD_SDK_MISSING_DIST' and error.exitCode === 1 (e.g., (error) =>
error.code === 'GSD_SDK_MISSING_DIST' && error.exitCode === 1) so the test
framework receives a simple true/false result instead of catching assertion
exceptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/installer-migrations.md`:
- Around line 102-126: The example install-state JSON in docs is incorrect:
update the example to match the actual shape written by the runner/installer (as
implemented in installer-migrations.cjs) by using the keys schemaVersion and
appliedMigrations (an array of entries containing appliedAt, journal and
checksum) instead of schema, runtime, scope, installed_version, install_mode,
and applied_migrations; ensure the example object and the prose describe the
checksum being computed from the migration definition and that checksum changes
trigger a warning/refusal, and remove or clearly separate any fields not
produced by installer-migrations.cjs so the docs match the code.
- Around line 148-160: The docs' plan(ctx) authoring contract advertises helper
predicates and actions that the runner doesn't currently provide; update the
section around the `plan(ctx)` description (and the repeated block at lines
182-201) to reflect the actual runtime surface: list only the properties the
runner passes (`configDir`, `manifest`, `state`, `baselineScan`, `now`,
`classifyArtifact`, `readJson`) and the supported action types, and either
remove references to helpers like `isManaged`, `isUserOwned`, `exists`,
`readToml` and actions like `move-managed`/`rewrite-config` or mark them
explicitly as "future work / not yet supported" so contributors don't author
incompatible migrations.

In `@get-shit-done/bin/lib/installer-migrations.cjs`:
- Around line 59-67: readInstallState currently treats any JSON (even with the
wrong shape) as absent; instead validate the structure returned by
readJsonIfPresent and reject it if it doesn't match expectations. In the
readInstallState function (which reads INSTALL_STATE_NAME via readJsonIfPresent
with STRICT_JSON), check that state is a plain object, that state.schemaVersion
is a finite number, and that state.appliedMigrations is an array (and optionally
that its items are the expected type); if the shape is invalid, throw an Error
(or otherwise fail the install) rather than returning a default/empty state so
migrations/checksums cannot be skipped.
- Around line 191-203: discoverInstallerMigrations currently returns validated
records but doesn't check for duplicate migration ids; update
discoverInstallerMigrations to collect all validateMigrationRecord outputs (from
function discoverInstallerMigrations) into a single array and then scan that
array for duplicate id values, throwing an error (including the duplicated id
and involved source filenames) if any duplicates are found so discovery fails
fast; reference validateMigrationRecord and the local variable/array produced by
the .flatMap/.map so you detect duplicates after validation and before returning
the records.
- Around line 688-699: The finally block for releaseLock() currently throws
releaseError (in the catch) which can override an earlier exception; refactor to
capture the operation result/return in a local variable instead of returning
early inside the try, ensure primaryError is set rather than throwing inside the
finally catch, and after the finally block re-throw primaryError if present;
specifically update the block around releaseLock(), primaryError, and completed
so the try assigns a result variable and any releaseError only augments
primaryError (e.g., push to primaryError.suppressed or set primaryError =
releaseError when none exists) and do not throw inside finally, then after
finally if primaryError is set throw it.

---

Duplicate comments:
In `@get-shit-done/bin/lib/installer-migrations.cjs`:
- Around line 497-559: The journal is only written once at the end
(fs.writeFileSync(journalPath...)), so if the process dies mid-loop applied
actions won’t be durable; after each committed action update (every place you
push into journal.actions inside the loop — e.g., where journalAction is added
for 'missing', 'recorded'/'preserved', 'removed', 'rewritten') immediately flush
the journal to disk by calling the same write used elsewhere
(fs.writeFileSync(journalPath, JSON.stringify(journal, null, 2) + '\n', 'utf8')
or an atomic equivalent) so the journal reflects each applied action as it
happens.

---

Nitpick comments:
In `@tests/bug-3033-sdk-flag-wired.test.cjs`:
- Around line 161-162: Replace the two coercive equality assertions that use
assert.equal for error.code and error.exitCode with strict equality assertions:
change the calls referencing error.code and error.exitCode (the assert.equal
lines in the test block) to use assert.strictEqual so the test enforces ===
semantics and matches the rest of the file.
- Around line 160-164: The inline validator arrow function currently uses
assert.equal calls which throw inside the validator; replace it with a pure
boolean check that returns true when error.code === 'GSD_SDK_MISSING_DIST' and
error.exitCode === 1 (e.g., (error) => error.code === 'GSD_SDK_MISSING_DIST' &&
error.exitCode === 1) so the test framework receives a simple true/false result
instead of catching assertion exceptions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f833becc-8dfd-448c-8f2b-7c62961f420c

📥 Commits

Reviewing files that changed from the base of the PR and between f5510fe and 908a19c.

📒 Files selected for processing (7)
  • bin/install.js
  • docs/installer-migrations.md
  • get-shit-done/bin/lib/installer-migrations.cjs
  • tests/bug-2771-user-profile-manifest.test.cjs
  • tests/bug-3033-sdk-flag-wired.test.cjs
  • tests/installer-migration-install-integration.test.cjs
  • tests/installer-migrations.test.cjs
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/bug-2771-user-profile-manifest.test.cjs
  • tests/installer-migration-install-integration.test.cjs
  • bin/install.js
  • tests/installer-migrations.test.cjs

Comment on lines +102 to +126
The installer writes an install-state file next to the manifest.

Required fields:

```json
{
"schema": 1,
"runtime": "codex",
"scope": "global",
"installed_version": "1.50.0",
"install_mode": "full",
"applied_migrations": [
{
"id": "2026-05-11-codex-hooks-layout",
"package_version": "1.50.0",
"checksum": "sha256:...",
"applied_at": "2026-05-11T00:00:00.000Z"
}
]
}
```

The checksum is calculated from the migration definition. If an applied
migration's checksum changes, the installer must warn and refuse to silently
re-run it. Fix-forward migrations should use a new migration id.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align the install-state example with the schema the runner actually writes.

The implementation persists schemaVersion / appliedMigrations with appliedAt, journal, and checksum; it does not write schema, runtime, scope, installed_version, install_mode, or applied_migrations. As written, this section documents a different file shape than installer-migrations.cjs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/installer-migrations.md` around lines 102 - 126, The example
install-state JSON in docs is incorrect: update the example to match the actual
shape written by the runner/installer (as implemented in
installer-migrations.cjs) by using the keys schemaVersion and appliedMigrations
(an array of entries containing appliedAt, journal and checksum) instead of
schema, runtime, scope, installed_version, install_mode, and applied_migrations;
ensure the example object and the prose describe the checksum being computed
from the migration definition and that checksum changes trigger a
warning/refusal, and remove or clearly separate any fields not produced by
installer-migrations.cjs so the docs match the code.

Comment on lines +148 to +160
The `plan(ctx)` function receives an install context with runtime, scope,
target directory, previous manifest, install state, package manifest, and
filesystem helpers. It returns actions. It must not mutate disk.

Migrations may use helper predicates such as:

- `isManaged(relPath)`
- `isUserOwned(relPath)`
- `hashMatchesManifest(relPath)`
- `exists(relPath)`
- `readJson(relPath)`
- `readToml(relPath)`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Trim this authoring contract to the currently supported surface.

This section advertises helpers like isManaged, exists, readToml and actions like move-managed / rewrite-config, but the runner currently passes configDir, manifest, state, baselineScan, now, classifyArtifact, and readJson, and the executor rejects unsupported action types. Please either mark the extra helpers/actions as future work or document the current surface so contributors do not author migrations that cannot run.

Also applies to: 182-201

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/installer-migrations.md` around lines 148 - 160, The docs' plan(ctx)
authoring contract advertises helper predicates and actions that the runner
doesn't currently provide; update the section around the `plan(ctx)` description
(and the repeated block at lines 182-201) to reflect the actual runtime surface:
list only the properties the runner passes (`configDir`, `manifest`, `state`,
`baselineScan`, `now`, `classifyArtifact`, `readJson`) and the supported action
types, and either remove references to helpers like `isManaged`, `isUserOwned`,
`exists`, `readToml` and actions like `move-managed`/`rewrite-config` or mark
them explicitly as "future work / not yet supported" so contributors don't
author incompatible migrations.

Comment on lines +59 to +67
function readInstallState(configDir) {
const state = readJsonIfPresent(path.join(configDir, INSTALL_STATE_NAME), STRICT_JSON);
if (!state || typeof state !== 'object') {
return { schemaVersion: 1, appliedMigrations: [] };
}
return {
schemaVersion: state.schemaVersion || 1,
appliedMigrations: Array.isArray(state.appliedMigrations) ? state.appliedMigrations : [],
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject structurally invalid install-state, not just invalid JSON.

STRICT_JSON only catches parse failures. Valid JSON with the wrong shape currently falls back to an empty state, which can re-run old migrations and skip checksum-drift enforcement instead of failing the install as Phase 4 intends.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@get-shit-done/bin/lib/installer-migrations.cjs` around lines 59 - 67,
readInstallState currently treats any JSON (even with the wrong shape) as
absent; instead validate the structure returned by readJsonIfPresent and reject
it if it doesn't match expectations. In the readInstallState function (which
reads INSTALL_STATE_NAME via readJsonIfPresent with STRICT_JSON), check that
state is a plain object, that state.schemaVersion is a finite number, and that
state.appliedMigrations is an array (and optionally that its items are the
expected type); if the shape is invalid, throw an Error (or otherwise fail the
install) rather than returning a default/empty state so migrations/checksums
cannot be skipped.

Comment on lines +191 to +203
function discoverInstallerMigrations({ migrationsDir }) {
if (!migrationsDir || !fs.existsSync(migrationsDir)) return [];
return fs.readdirSync(migrationsDir, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith('.cjs'))
.map((entry) => entry.name)
.sort()
.flatMap((fileName) => {
const source = path.join(migrationsDir, fileName);
delete require.cache[require.resolve(source)];
const exported = require(source);
const records = Array.isArray(exported) ? exported : [exported];
return records.map((record) => validateMigrationRecord(record, source));
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast on duplicate migration IDs.

Discovery validates each record individually, but it never rejects two records with the same id. That lets both plans execute on first run while only one applied-state entry is persisted, which makes follow-up checksum/state handling ambiguous.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@get-shit-done/bin/lib/installer-migrations.cjs` around lines 191 - 203,
discoverInstallerMigrations currently returns validated records but doesn't
check for duplicate migration ids; update discoverInstallerMigrations to collect
all validateMigrationRecord outputs (from function discoverInstallerMigrations)
into a single array and then scan that array for duplicate id values, throwing
an error (including the duplicated id and involved source filenames) if any
duplicates are found so discovery fails fast; reference validateMigrationRecord
and the local variable/array produced by the .flatMap/.map so you detect
duplicates after validation and before returning the records.

Comment on lines +688 to +699
} finally {
try {
releaseLock();
} catch (releaseError) {
if (primaryError) {
primaryError.suppressed = [...(primaryError.suppressed || []), releaseError];
} else if (completed) {
throw releaseError;
} else {
throw releaseError;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cd get-shit-done && find . -name "installer-migrations.cjs" -type f

Repository: gsd-build/get-shit-done

Length of output: 102


🏁 Script executed:

cat -n ./bin/lib/installer-migrations.cjs | sed -n '680,710p'

Repository: gsd-build/get-shit-done

Length of output: 134


🏁 Script executed:

find . -name "installer-migrations.cjs" -type f 2>/dev/null

Repository: gsd-build/get-shit-done

Length of output: 116


🏁 Script executed:

cat -n ./get-shit-done/bin/lib/installer-migrations.cjs | sed -n '680,710p'

Repository: gsd-build/get-shit-done

Length of output: 995


🏁 Script executed:

cat -n ./get-shit-done/bin/lib/installer-migrations.cjs | sed -n '650,710p'

Repository: gsd-build/get-shit-done

Length of output: 2166


🏁 Script executed:

cat -n ./get-shit-done/bin/lib/installer-migrations.cjs | sed -n '645,660p'

Repository: gsd-build/get-shit-done

Length of output: 631


Move lock-release error handling out of finally block.

The current code has a control flow issue: if releaseLock() throws an error while handling an already-caught exception or during a normal return, the exception from finally (lines 695 and 697) overrides the original error. This is the anti-pattern Biome flags.

The fix is to avoid throwing from finally. Instead:

  1. Remove early return statements from the try block and assign results to a variable instead
  2. In the finally block's catch, don't throw—only set primaryError when appropriate
  3. After finally completes, throw primaryError if it exists
Suggested control-flow shape
-  try {
-    const plan = planInstallerMigrations({ configDir, runtime, scope, migrations, baselineScan, now });
-    if (plan.actions.length === 0) {
-      const appliedMigrationIds = markPendingMigrationsApplied({ configDir, plan, now });
-      completed = true;
-      return {
-        appliedMigrationIds,
-        journalRelPath: null,
-        plan,
-      };
-    }
-    if (plan.blocked.length > 0) {
-      completed = true;
-      return {
-        appliedMigrationIds: [],
-        journalRelPath: null,
-        plan,
-        blocked: plan.blocked,
-      };
-    }
-    const result = applyInstallerMigrationPlan({ configDir, plan, now });
-    completed = true;
-    return { ...result, plan };
-  } catch (error) {
-    primaryError = error;
-    throw error;
-  } finally {
-    try {
-      releaseLock();
-    } catch (releaseError) {
-      if (primaryError) {
-        primaryError.suppressed = [...(primaryError.suppressed || []), releaseError];
-      } else if (completed) {
-        throw releaseError;
-      } else {
-        throw releaseError;
-      }
-    }
-  }
+  let result;
+  try {
+    const plan = planInstallerMigrations({ configDir, runtime, scope, migrations, baselineScan, now });
+    if (plan.actions.length === 0) {
+      const appliedMigrationIds = markPendingMigrationsApplied({ configDir, plan, now });
+      completed = true;
+      result = {
+        appliedMigrationIds,
+        journalRelPath: null,
+        plan,
+      };
+    } else if (plan.blocked.length > 0) {
+      completed = true;
+      result = {
+        appliedMigrationIds: [],
+        journalRelPath: null,
+        plan,
+        blocked: plan.blocked,
+      };
+    } else {
+      result = { ...applyInstallerMigrationPlan({ configDir, plan, now }), plan };
+      completed = true;
+    }
+  } catch (error) {
+    primaryError = error;
+    throw error;
+  } finally {
+    try {
+      releaseLock();
+    } catch (releaseError) {
+      if (primaryError) {
+        primaryError.suppressed = [...(primaryError.suppressed || []), releaseError];
+      } else {
+        primaryError = releaseError;
+      }
+    }
+  }
+  if (primaryError) throw primaryError;
+  return result;
🧰 Tools
🪛 Biome (2.4.15)

[error] 695-695: Unsafe usage of 'throw'.

(lint/correctness/noUnsafeFinally)


[error] 697-697: Unsafe usage of 'throw'.

(lint/correctness/noUnsafeFinally)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@get-shit-done/bin/lib/installer-migrations.cjs` around lines 688 - 699, The
finally block for releaseLock() currently throws releaseError (in the catch)
which can override an earlier exception; refactor to capture the operation
result/return in a local variable instead of returning early inside the try,
ensure primaryError is set rather than throwing inside the finally catch, and
after the finally block re-throw primaryError if present; specifically update
the block around releaseLock(), primaryError, and completed so the try assigns a
result variable and any releaseError only augments primaryError (e.g., push to
primaryError.suppressed or set primaryError = releaseError when none exists) and
do not throw inside finally, then after finally if primaryError is set throw it.

@trek-e trek-e merged commit ae3ac05 into main May 11, 2026
1 check was pending
@github-actions github-actions Bot added size/XL and removed size/XL labels May 11, 2026
@github-actions github-actions Bot deleted the codex/installer-migrations-phase-four branch May 11, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: first-class installer migration tool for safe upgrades

1 participant