Skip to content

feat: add safe Relay setup and agent configuration - #48

Merged
krishna916 merged 11 commits into
mainfrom
feature/issue-41-safe-setup-agent-configuration
Aug 2, 2026
Merged

feat: add safe Relay setup and agent configuration#48
krishna916 merged 11 commits into
mainfrom
feature/issue-41-safe-setup-agent-configuration

Conversation

@krishna916

@krishna916 krishna916 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Summary

Implements Issue #41 and the authoritative plan at docs/superpowers/plans/2026-08-02-issue-41-safe-setup-and-agent-configuration.md.

  • Adds safe relay setup initialization with migration and task-data retention.
  • Adds explicit, preview-first Codex TOML and Claude Code JSON/JSONC setup workflows.
  • Adds generic MCP snippets plus config paths, config integrations, config snippet, disable, and remove commands.
  • Adds ownership metadata, exact-entry matching, fail-closed conflict handling, collision-safe backups, race checks, atomic replacement, rollback validation, and permission preservation.
  • Applies PR review remediation from docs/superpowers/plans/2026-08-02-pr-48-review-remediation.md:
    • host-native absolute-path parser tests;
    • exclusive ownership metadata lock with bounded conflict retry and update-only mutation API;
    • concurrent Codex/Claude ownership regression coverage;
    • exact rollback of originally absent configs while retaining backups;
    • existing-file byte and mode restoration coverage.
  • CodeRabbit did not review this draft PR; its bot comment explicitly reports review skipped.

Validation

  • pnpm.cmd verify — passed: 63 test files passed, 622 tests passed, 5 skipped; coverage 88.71% statements, 80.83% branches, 88.99% functions, 91.03% lines; formatting, lint, typecheck, build, assets, and audit stages passed.
  • RELAY_RUN_PACKAGE_SMOKE=1 pnpm.cmd verify:package — passed installed-package setup/config smoke, including preview/apply, idempotency, disable/re-enable/remove, MCP/UI startup, and task-data retention.
  • Disposable cross-process smoke — two concurrent Codex/Claude setup applies exited 0/0 and config integrations --output json returned 2 ownership records.
  • The previous Linux path-test failure is fixed.
  • Manual real Codex/Claude client install/restart/backup validation remains the human acceptance gate.

Summary by CodeRabbit

  • New Features
    • Added relay setup and configuration commands for Codex and Claude Code integrations.
    • Supports preview-first changes with optional --apply, disable, removal, snippets, and path inspection.
    • Validates configuration paths, detects conflicts, preserves unrelated settings, creates collision-safe backups, and supports rollback.
    • Standardized integrations on the installed relay mcp command.
  • Documentation
    • Added setup, installation, troubleshooting, verification, and recovery guidance.
  • Tests
    • Added coverage for configuration preservation, conflicts, concurrency, backups, rollback, and installed-package workflows.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@krishna916, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8b3bb6ce-8309-4333-bb82-1b166601c33f

📥 Commits

Reviewing files that changed from the base of the PR and between b33f7cc and 08fe6c4.

📒 Files selected for processing (7)
  • src/distribution/setup/apply-integration-change.ts
  • src/distribution/setup/file-lock.ts
  • src/distribution/setup/integration-transaction-journal.ts
  • src/interfaces/cli/run-operational-command.ts
  • tests/unit/distribution/setup/apply-integration-change.test.ts
  • tests/unit/distribution/setup/integration-transaction-journal.test.ts
  • tests/unit/interfaces/cli/run-operational-command.test.ts
📝 Walkthrough

Walkthrough

The PR adds safe installed setup for Codex and Claude Code. It introduces configuration planning, atomic writes, ownership metadata, transaction recovery, operational CLI commands, package validation, documentation, fixtures, and workflow tests.

Changes

Installed setup workflow

Layer / File(s) Summary
Setup contracts, adapters, and planning
src/distribution/setup/*, tests/unit/distribution/setup/*
Defines setup contracts, validates Codex and Claude Code configurations, renders relay mcp snippets, and plans setup, disable, and remove operations.
Ownership storage and atomic configuration updates
src/distribution/setup/ownership-store.ts, src/distribution/setup/backup-and-atomic-write.ts, src/distribution/setup/integration-transaction-journal.ts, src/distribution/setup/apply-integration-change.ts
Adds locked ownership updates, collision-safe backups, atomic replacement, transaction journals, rollback, and mode preservation.
Operational CLI execution
src/interfaces/cli/*, src/interfaces/production-dependencies.ts
Adds command parsing, setup/config dispatch, dependency wiring, structured output, preview handling, recovery, and applied changes.
Installed distribution and asset validation
package.json, tsup.config.ts, scripts/*, integrations/*
Updates installed command templates, runtime dependencies, package smoke coverage, JSONC validation, and required distribution assets.
Integration guidance and verification assets
README.md, docs/*, tests/fixtures/*, tests/integration/*, .prettierignore
Documents preview, apply, and recovery workflows and adds fixtures and tests for parsing, conflicts, concurrency, rollback, idempotency, and lifecycle actions.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

  • krishna916/relay#46 — Defines the distribution, CLI, ownership, setup, backup, and lifecycle contracts implemented here.
  • krishna916/relay#47 — Provides the publishable package extended here with setup and MCP configuration workflows.
  • krishna916/relay#33 — Established the agent integration assets and validation extended by this PR.

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant RelayCLI
  participant SetupPlanner
  participant ClientConfig
  participant OwnershipStore
  Operator->>RelayCLI: relay setup --client --config-file
  RelayCLI->>SetupPlanner: parse and plan change
  SetupPlanner->>ClientConfig: inspect configuration
  SetupPlanner->>OwnershipStore: read ownership metadata
  RelayCLI->>ClientConfig: atomically apply reviewed change
  RelayCLI->>OwnershipStore: persist locked ownership update
  RelayCLI-->>Operator: return JSON result
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the PR's main change: safe Relay setup and agent configuration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/issue-41-safe-setup-agent-configuration

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.

@krishna916 krishna916 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Review verdict: changes required

The overall decomposition is good and the client-file safety path is substantially aligned with issue #41, but I found two blocking correctness problems.

High — ownership metadata has a lost-update race

applyIntegrationChange() reads the ownership file, merges its own record in memory, and calls ownershipStore.write(). The store then performs an unconditional atomic replacement. Two concurrent setup operations for different clients can both read the same old metadata and each write a valid replacement; the last writer silently deletes the other integration record while both client files retain Relay entries.

That leaves one configured entry stranded as an apparently unowned matching entry, so later setup/disable/remove fails closed and cannot manage it. This is especially plausible for Relay because multiple local agents/processes may invoke setup concurrently.

Please add optimistic concurrency or a short exclusive metadata lock around the read-modify-write transaction. A practical shape is readWithFingerprint() plus write(next, expectedFingerprint), with the fingerprint rechecked immediately before replacement and a conflict returned when it changed. Add a deterministic two-writer test proving records cannot be lost.

High — CI is failing on the supported Linux runner

The new parser test uses C:/tmp/codex.toml as the successful absolute-path case. node:path.isAbsolute() uses host semantics, so that path is relative on Linux. CI run 122 fails in operational-commands.test.ts with --config-file must be an absolute path; the gate reports 615 passed and 1 failed.

Use a host-native absolute path for this parser test, or explicitly test platform-specific path parsing through an injected path strategy. The production parser currently follows host semantics, which is appropriate for the running client; the test must not assume Windows while executing on Linux.

Medium — failed metadata persistence does not restore an originally absent file

For a new config path, backupAndAtomicWrite() represents the original as an empty buffer. If the client file is created successfully but metadata persistence then fails, restoreFile() restores the empty backup as an empty file. The pre-operation state was “file absent,” not “empty file present.” Track whether the target existed and delete the newly created target during rollback when it did not.

After these fixes, rerun pnpm verify on CI and add focused regression tests for metadata concurrency and absent-file rollback.

Copy link
Copy Markdown
Owner Author

Luna remediation plan

I added an execution-ready Superpowers remediation plan directly to this PR branch:

docs/superpowers/plans/2026-08-02-pr-48-review-remediation.md

Commit: ce18aa9166b8a0ece331f71289631686b36b403b

Luna should execute it task-by-task with superpowers:executing-plans.

The plan locks the implementation decisions rather than leaving them to the agent:

  1. Replace host-specific Windows test paths with host-native absolute paths so Ubuntu CI passes.
  2. Replace the unsafe ownership read() + write() sequence with one process-safe OwnershipStore.update() boundary.
  3. Serialize metadata mutations using an exclusive sibling lock file with bounded retry and conflict failure; do not add a dependency or auto-break unknown locks.
  4. Merge each integration change against the latest ownership document while holding the lock, preserving concurrent Codex and Claude records.
  5. Carry originalExisted and originalMode through the backup result.
  6. Restore a previously absent client configuration to absence after metadata failure while retaining the backup.
  7. Preserve exact contents and permissions when rolling back an existing file.
  8. Run focused regression tests, full pnpm verify, installed-package smoke, disposable concurrency validation, and GitHub CI before marking ready.

Do not merge while the PR is draft or CI is red. The manual real-client restart/backup check remains the final human acceptance gate.

@krishna916 krishna916 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Re-review result: the three previously blocking findings are correctly addressed.

  • Ownership metadata updates now use a cross-process exclusive lock around the complete read-modify-write cycle, and concurrent Codex/Claude regression coverage verifies both records survive.
  • Rollback now distinguishes an originally absent target from an existing file, removes newly created files on metadata failure, restores existing bytes/mode, and retains backups.
  • The parser test now uses a host-native absolute path, and CI run 124 passes.

One remaining documentation correctness issue should be fixed before merge:

Medium — docs/setup-and-configuration.md contradicts the implemented and locked CLI contract. The document lists relay config disable/remove ... without --apply and states that without --apply these commands return a preview. The parser intentionally requires --apply for disable/remove and returns exit code 2 when it is omitted; the authoritative issue #41 plan also locks this behavior. Update the command examples and prose so only setup is preview-first, while disable/remove require --apply and users inspect ownership through relay config integrations first.

After that documentation correction and a green CI rerun, I consider the PR merge-ready subject to the already documented manual real-client acceptance gate.

@krishna916 krishna916 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Final re-review complete. The documentation now matches the accepted CLI contract: setup is preview-first, while config disable/remove require --apply and omission is documented as exit code 2. The three earlier correctness blockers remain resolved, and CI run 125 passed on the latest head. No further code or documentation issues found in this review. PR is merge-ready after the planned manual real-client acceptance check.

@krishna916
krishna916 marked this pull request as ready for review August 2, 2026 07:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 17

🧹 Nitpick comments (10)
src/interfaces/production-dependencies.ts (1)

22-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the ownership metadata path into one helper.

join(runtimePaths.configRoot, 'config.json') appears here and twice in src/interfaces/cli/run-operational-command.ts (the fallback createOwnershipStore call and the config paths output). The three sites must agree. If one changes, relay config paths reports a path that the ownership store does not use.

Export a single resolveOwnershipMetadataPath(runtimePaths) helper next to resolveRuntimePaths and call it from all three sites.

🤖 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 `@src/interfaces/production-dependencies.ts` around lines 22 - 25, Extract the
shared ownership metadata path calculation into an exported
resolveOwnershipMetadataPath(runtimePaths) helper beside resolveRuntimePaths,
then replace the direct join(runtimePaths.configRoot, 'config.json') usage in
production dependencies and both operational-command sites, including the
fallback ownership store and config-paths output.
scripts/validate-repository-assets.ts (1)

403-408: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

This existence check is already performed.

validateRepositoryAssets builds requiredPaths from ...requiredDistributionAssets and fails on any missing entry. Lines 40-60 add every setup fixture to requiredDistributionAssets, so this loop re-checks paths that are already checked, with a second error message for the same failure. Remove the loop.

♻️ Proposed fix
-
-  for (const path of requiredDistributionAssets.filter((asset) =>
-    asset.startsWith('tests/fixtures/setup/'),
-  )) {
-    if (!existsSync(join(rootDir, path))) fail(`Setup fixture is missing: ${path}`);
-  }
 }
🤖 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 `@scripts/validate-repository-assets.ts` around lines 403 - 408, Remove the
setup-fixture iteration that filters requiredDistributionAssets and calls
existsSync/fail, since validateRepositoryAssets already validates every entry
through requiredPaths. Keep the existing requiredDistributionAssets validation
unchanged and eliminate only this duplicate check and its error path.
src/distribution/setup/apply-integration-change.ts (1)

37-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated ownership literals risk drifting from the exact-entry contract.

entryId: 'relay', command: 'relay' as const, and args: ['mcp'] as const are hardcoded here (and entryId: 'relay' is repeated at Line 21 and Line 90). The documentation states Relay proves ownership by matching "the exact relay entry, the relay command, ['mcp'] arguments" against what the adapters actually write. If a client adapter or snippet template ever changes its generated command/args independently of this file, ownership records would silently mismatch the file content, weakening the exact-matching guarantee this PR is built around.

Extract these values into a single shared constant (for example, exported from snippets.ts or setup-types.ts) and import it here and wherever adapters generate the entry, so the ownership record and the generated content cannot diverge.

🤖 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 `@src/distribution/setup/apply-integration-change.ts` around lines 37 - 47,
Extract the canonical Relay entry values—entryId, command, and args—into one
shared exported constant, then update applyIntegrationChange and the adapter or
snippet-generation code to reuse it everywhere, including the repeated entryId
references. Remove the duplicated literals while preserving the existing exact
values so ownership records and generated content remain synchronized.
scripts/validate-agent-integration-assets.ts (2)

279-281: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the redundant re-parse of the three template assets.

Line 278 calls validateTemplateShape(rootDir). That function already reads and parses generic-mcp/server-config.json.example, claude-code/.mcp.json.example, and codex/config.toml.example. Lines 279-281 parse the same three files again and discard the results. The parse-validity check is already covered, so these lines add no assertion.

♻️ Proposed removal
   validateTemplateShape(rootDir);
-  JSON.parse(readFileSync(join(integrationRoot, 'generic-mcp/server-config.json.example'), 'utf8'));
-  JSON.parse(readFileSync(join(integrationRoot, 'claude-code/.mcp.json.example'), 'utf8'));
-  parse(readFileSync(join(integrationRoot, 'codex/config.toml.example'), 'utf8'));
 }
🤖 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 `@scripts/validate-agent-integration-assets.ts` around lines 279 - 281, Remove
the redundant JSON.parse and parse calls immediately after
validateTemplateShape(rootDir) in the validation flow. Keep
validateTemplateShape responsible for reading and parsing the three template
assets, and leave the remaining validation behavior unchanged.

173-206: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract one shared check for the command/args contract.

The same rule is now expressed three times: the JSON branch at lines 179-183 and the TOML branch at lines 197-204. The two branches also use different comparison styles for args. The JSON branch uses JSON.stringify equality. The TOML branch uses Array.isArray plus length plus index. Both must stay in sync when the installed command changes.

Line 200 also applies optional chaining to codexServer?.command after line 198 already rejects undefined. Remove the redundant ?..

♻️ Proposed helper extraction
+function invokesInstalledRelayMcp(server: Record<string, unknown>): boolean {
+  return (
+    JSON.stringify(Object.keys(server).sort()) === JSON.stringify(['args', 'command']) &&
+    server.command === 'relay' &&
+    JSON.stringify(server.args) === JSON.stringify(['mcp'])
+  );
+}
-    if (
-      JSON.stringify(Object.keys(server).sort()) !== JSON.stringify(['args', 'command']) ||
-      server.command !== 'relay' ||
-      JSON.stringify(server.args) !== JSON.stringify(['mcp'])
-    )
+    if (!invokesInstalledRelayMcp(server as Record<string, unknown>))
       fail(`${path} must use separate command and arguments for the installed relay mcp command.`);
-  if (
-    codexServer === undefined ||
-    JSON.stringify(Object.keys(codexServer).sort()) !== JSON.stringify(['args', 'command']) ||
-    codexServer?.command !== 'relay' ||
-    !Array.isArray(codexServer.args) ||
-    codexServer.args.length !== 1 ||
-    codexServer.args[0] !== 'mcp'
-  ) {
+  if (codexServer === undefined || !invokesInstalledRelayMcp(codexServer)) {
     fail('integrations/codex/config.toml.example must invoke the installed relay mcp command.');
   }
🤖 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 `@scripts/validate-agent-integration-assets.ts` around lines 173 - 206, Extract
a shared helper near the validation logic to validate the installed MCP server
command/args contract, requiring exactly the command and args keys, command
“relay”, and args equivalent to [“mcp”]. Replace both the JSON server check and
TOML codexServer check with this helper, using one consistent args comparison
style, and change codexServer?.command to codexServer.command after its
undefined guard.
tests/unit/interfaces/cli/operational-commands.test.ts (1)

22-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add positive-path assertions for config disable and config remove.

The tests cover the failure case for disable without --apply at line 27. No test asserts the parsed result for a valid disable or remove command. The action-to-kind mapping in src/interfaces/cli/parse-operational-command.ts lines 49-54 is therefore untested. A swap of config-disable and config-remove passes this suite. The coding guidelines require coverage of normal flows in the layer where they belong.

💚 Proposed additional assertions
   it('parses snippet and inspection commands', () => {
+    expect(
+      parseOperationalCommand([
+        'config',
+        'disable',
+        '--client',
+        'codex',
+        '--config-file',
+        absoluteConfigPath,
+        '--apply',
+      ]),
+    ).toEqual({
+      kind: 'config-disable',
+      client: 'codex',
+      configFile: absoluteConfigPath,
+      apply: true,
+    });
+    expect(
+      parseOperationalCommand([
+        'config',
+        'remove',
+        '--client',
+        'claude-code',
+        '--config-file',
+        absoluteConfigPath,
+        '--apply',
+      ]),
+    ).toEqual({
+      kind: 'config-remove',
+      client: 'claude-code',
+      configFile: absoluteConfigPath,
+      apply: true,
+    });
     expect(parseOperationalCommand(['config', 'paths'])).toEqual({ kind: 'config-paths' });

As per coding guidelines: "Cover normal flows, validation failures, and lifecycle boundaries in the layer where they belong."

🤖 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/unit/interfaces/cli/operational-commands.test.ts` around lines 22 - 50,
Add positive-path assertions in the “parses snippet and inspection commands”
test for valid `config disable` and `config remove` invocations, including their
expected `config-disable` and `config-remove` kinds and parsed
client/config-file values. Use the existing absoluteConfigPath fixture and
include `--apply` so both action-to-kind mappings in parseOperationalCommand are
directly verified.

Source: Coding guidelines

src/interfaces/cli/operational-output.ts (1)

41-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Preserve diagnostics for unexpected internal errors.

Line 41 replaces the original error with a fixed message. No stack trace and no cause reach stdout, stderr, or any log. A user who hits an unexpected failure has no information to report. Emit the detail on stderr, or attach it behind a debug flag, while keeping the stdout envelope generic.

🤖 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 `@src/interfaces/cli/operational-output.ts` at line 41, Update the unexpected
internal-error handling in the operational output builder to keep the returned
stdout envelope generic while emitting the original error details, including its
cause or stack when available, to stderr or through the existing debug logging
path. Preserve the INTERNAL_ERROR code and exitCode values.
tests/unit/interfaces/cli/run-operational-command.test.ts (1)

13-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the shared dependency literal and assert the envelope before reading nested fields.

Both tests duplicate the same runtimePaths, openRuntime, applicationVersion, and stream stubs. Extract a createDependencies(root, output) helper.

Lines 39 and 63 use JSON.parse(output[0] ?? '{}'). If no output is written, the parse succeeds and the following member access throws a TypeError. The failure message then hides the real cause. Assert output has one entry first, then read the parsed fields.

🤖 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/unit/interfaces/cli/run-operational-command.test.ts` around lines 13 -
64, Extract the duplicated runtimePaths, openRuntime, applicationVersion,
stdout, and stderr setup into a shared createDependencies(root, output) helper,
then use it in both tests. Before parsing output in each test, assert that
output contains exactly one entry; only afterward parse output[0] and inspect
the nested data fields.
src/interfaces/cli/run-operational-command.ts (1)

97-98: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the cast and the non-null assertion with union narrowing.

command.client as MutableIntegrationClient and command.configFile! discard the type guarantees of OperationalCommand. The invariant holds today only because parseSetup in src/interfaces/cli/parse-operational-command.ts always sets configFile for codex and claude-code. A future parser change breaks this silently at runtime instead of at compile time. Add an explicit guard.

♻️ Proposed narrowing
-    const client = command.client as MutableIntegrationClient;
-    const configPath = command.configFile!;
+    const { client, configFile } = command;
+    if (client === undefined || client === 'generic-mcp' || configFile === undefined)
+      throw new CliUsageError('--client and --config-file are required for this operation.');
+    const configPath = configFile;
🤖 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 `@src/interfaces/cli/run-operational-command.ts` around lines 97 - 98, In the
operational command handling around client and configPath, remove the
MutableIntegrationClient cast and configFile non-null assertion. Add an explicit
guard that narrows the command union to the variants guaranteeing both a
compatible client and configFile before accessing them, and handle or reject
other variants explicitly so future parser changes fail safely and compile-time
guarantees are preserved.
scripts/package/smoke-installed-package.ts (1)

292-300: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Parse the rerun envelope instead of matching a raw substring.

Line 299 checks rerun.stdout.includes('"changed":false'). The check depends on the exact serialization produced by JSON.stringify. A formatting change, a key reorder, or added whitespace makes the assertion pass or fail for the wrong reason. The envelope helper at line 126 already parses and validates single-line JSON output.

♻️ Proposed change
-    if (rerun.status !== 0 || !rerun.stdout.includes('"changed":false'))
+    const rerunData = rerun.status === 0 ? (envelope(rerun).data as { changed?: boolean }) : undefined;
+    if (rerun.status !== 0 || rerunData?.changed !== false)
       throw new Error('Installed setup rerun was not idempotent.');
🤖 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 `@scripts/package/smoke-installed-package.ts` around lines 292 - 300, Update
the rerun validation in the smoke test to use the existing envelope helper for
parsing and validating rerun.stdout, then assert the parsed changed field is
false instead of matching a raw JSON substring. Preserve the nonzero-status
failure check and the existing idempotency error behavior.
🤖 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 `@scripts/package/smoke-installed-package.ts`:
- Around line 241-248: Update the setupEnvironment used by the installed-package
smoke test and the related runCli setup invocation to override platform-specific
configuration roots on Linux and macOS as well as Windows, using temporaryRoot
for the XDG and HOME-based paths consumed by getPlatformDefaultPaths(). Keep all
smoke-test data isolated from the real user directories while preserving the
existing setup validation.

In `@scripts/validate-repository-assets.ts`:
- Around line 132-143: Update the asset validation flow around normalizedPath
and parseJsonc so only the intended Claude JSONC fixtures are parsed with JSONC
support; validate all other .json assets with JSON.parse to reject comments
consistently with contract parsing. Remove the formatted.json exception from the
skip list, retaining only the intentionally malformed setup fixtures.

In `@src/distribution/setup/apply-integration-change.ts`:
- Around line 76-81: Update the restore-failure branch in the outer catch of the
ownershipStore.update flow to preserve both the original metadata failure and
the subsequent restoreError when constructing SetupStorageError. Keep the
existing restore context and backup-path message, but pass or append both error
details so neither cause is discarded.
- Around line 26-35: Update the no-rewrite branch around clientChanged and
before persisting nextRecord to reread input.plan.configPath, recompute its
fingerprint, and compare it with input.plan.beforeFingerprint. Throw
SetupConflictError on any mismatch, while preserving the existing
backupAndAtomicWrite path when clientChanged is true.

In `@src/distribution/setup/backup-and-atomic-write.ts`:
- Around line 26-34: Update the backup flow around readOriginalFile and
createExclusiveBackup to create and return a backup path only when
original.existed is true; otherwise leave backupPath undefined. Propagate the
optional BackupAndAtomicWriteResult.backupPath through applyIntegrationChange so
lastBackupPath is not recorded for targets that did not previously exist, while
preserving restoreOriginalFile’s existing originalExisted === false behavior.
- Around line 50-58: Add an exclusion lock keyed by input.plan.configPath around
the complete client-config replacement workflow, including backup, write,
validation, replacement, and the subsequent ownershipStore.update in
applyIntegrationChange. Ensure concurrent setup operations targeting the same
config path are serialized, while preserving the existing fingerprint conflict
check and cleanup behavior.

In `@src/distribution/setup/clients/claude-json-adapter.ts`:
- Around line 14-21: Add an empty Claude JSON fixture representing a
configuration with no mcpServers key, and update claude-json-adapter.test.ts to
cover the 'absent' path with that fixture alongside the existing unrelated.json
case. Keep the fixture valid JSON and preserve current adapter behavior.

In `@src/distribution/setup/clients/codex-toml-adapter.ts`:
- Line 1: Update the TOML parser dependency used by the Codex configuration
adapter and its import in parseDocument to a version supporting TOML 1.0+
syntax, while preserving the existing malformed-configuration error behavior for
genuinely invalid input.

In `@src/distribution/setup/initialize-relay.ts`:
- Around line 27-28: Update the directory tracking in the initialization flow
around dependencies.mkdir so that, whenever mkdir reports creation, it pushes
the requested directory rather than the returned ancestor path. Add a test
covering dataRoot and configRoot with an absent shared parent, verifying
createdDirectories and the resulting CLI output report both requested roots.

In `@src/distribution/setup/ownership-store.ts`:
- Around line 115-133: Update withOwnershipLock’s acquisition retry path to
inspect an existing lock via the lock payload and invoke tryBreakStaleLock when
acquiredAt exceeds staleAfterMs, then retry opening the lock after unlinking
stale files. Preserve active-lock conflict behavior, and include the lock path
in SetupConflictError so users can locate and remove an unrecoverable lock.
- Around line 34-43: Update the SetupStorageError construction in the ownership
metadata parsing catch block to pass the caught error directly as the
constructor’s second argument, matching the existing storage read/write patterns
and preserving the actual cause.

In `@src/interfaces/cli/operational-output.ts`:
- Around line 35-41: The error mapping currently classifies every RelayError as
VALIDATION_ERROR, including infrastructure failures. Update the error
classification condition to exclude the base RelayError while preserving
existing CliUsageError, SetupUsageError, and explicitly handled
SetupStorageError behavior; let other RelayError instances fall through to
INTERNAL_ERROR.

In `@tests/fixtures/agent-integrations/valid/integrations/generic-mcp/README.md`:
- Line 1: Update the validation guidance in the README to explicitly require an
isolated, non-empty absolute path for RELAY_DB_PATH, while preserving the
existing allowance for omission outside validation use and the instruction that
the SQLite database remains untouched.

In `@tests/fixtures/setup/metadata/disabled.json`:
- Around line 5-12: Replace the static Windows paths in
tests/fixtures/setup/metadata/disabled.json#L5-L12 and
tests/fixtures/setup/metadata/enabled.json#L5-L12 with test-generated
host-native paths using join(tmpdir(), ...), and update the consuming tests to
construct these records. In tests/fixtures/setup/metadata/duplicate.json#L5-L22,
generate two host-native equivalent paths, retaining a "." segment so duplicate
detection remains covered.

In `@tests/fixtures/setup/metadata/wrong-command.json`:
- Line 6: Update the configPath value in the wrong-command fixture to use a
POSIX absolute path or a host-aware platform-native fixture, matching the
established formatted.toml fixture pattern. Preserve the fixture’s invalid
metadata and wrong-command behavior while ensuring ownership-store absolute-path
validation succeeds on Linux and macOS.

In `@tests/unit/distribution/setup/backup-and-atomic-write.test.ts`:
- Around line 62-79: Add a test alongside the existing restore test for a
pre-existing target file, using known content and permissions. Exercise
backupAndAtomicWrite and restoreOriginalFile, then assert the restored file’s
exact bytes and that stat.mode & 0o777 matches the original mode, covering the
existing-file rollback path.

In `@tests/unit/distribution/setup/plan-integration-change.test.ts`:
- Around line 13-62: Extend the planIntegrationChange test suite with focused
cases for a missing configuration file, successful disable, successful remove,
and setup transitioning a disabled Relay entry to enabled. Assert each plan’s
operation and changed values, and verify the resulting configuration content
where the transition writes it, while preserving the existing validation tests.

---

Nitpick comments:
In `@scripts/package/smoke-installed-package.ts`:
- Around line 292-300: Update the rerun validation in the smoke test to use the
existing envelope helper for parsing and validating rerun.stdout, then assert
the parsed changed field is false instead of matching a raw JSON substring.
Preserve the nonzero-status failure check and the existing idempotency error
behavior.

In `@scripts/validate-agent-integration-assets.ts`:
- Around line 279-281: Remove the redundant JSON.parse and parse calls
immediately after validateTemplateShape(rootDir) in the validation flow. Keep
validateTemplateShape responsible for reading and parsing the three template
assets, and leave the remaining validation behavior unchanged.
- Around line 173-206: Extract a shared helper near the validation logic to
validate the installed MCP server command/args contract, requiring exactly the
command and args keys, command “relay”, and args equivalent to [“mcp”]. Replace
both the JSON server check and TOML codexServer check with this helper, using
one consistent args comparison style, and change codexServer?.command to
codexServer.command after its undefined guard.

In `@scripts/validate-repository-assets.ts`:
- Around line 403-408: Remove the setup-fixture iteration that filters
requiredDistributionAssets and calls existsSync/fail, since
validateRepositoryAssets already validates every entry through requiredPaths.
Keep the existing requiredDistributionAssets validation unchanged and eliminate
only this duplicate check and its error path.

In `@src/distribution/setup/apply-integration-change.ts`:
- Around line 37-47: Extract the canonical Relay entry values—entryId, command,
and args—into one shared exported constant, then update applyIntegrationChange
and the adapter or snippet-generation code to reuse it everywhere, including the
repeated entryId references. Remove the duplicated literals while preserving the
existing exact values so ownership records and generated content remain
synchronized.

In `@src/interfaces/cli/operational-output.ts`:
- Line 41: Update the unexpected internal-error handling in the operational
output builder to keep the returned stdout envelope generic while emitting the
original error details, including its cause or stack when available, to stderr
or through the existing debug logging path. Preserve the INTERNAL_ERROR code and
exitCode values.

In `@src/interfaces/cli/run-operational-command.ts`:
- Around line 97-98: In the operational command handling around client and
configPath, remove the MutableIntegrationClient cast and configFile non-null
assertion. Add an explicit guard that narrows the command union to the variants
guaranteeing both a compatible client and configFile before accessing them, and
handle or reject other variants explicitly so future parser changes fail safely
and compile-time guarantees are preserved.

In `@src/interfaces/production-dependencies.ts`:
- Around line 22-25: Extract the shared ownership metadata path calculation into
an exported resolveOwnershipMetadataPath(runtimePaths) helper beside
resolveRuntimePaths, then replace the direct join(runtimePaths.configRoot,
'config.json') usage in production dependencies and both operational-command
sites, including the fallback ownership store and config-paths output.

In `@tests/unit/interfaces/cli/operational-commands.test.ts`:
- Around line 22-50: Add positive-path assertions in the “parses snippet and
inspection commands” test for valid `config disable` and `config remove`
invocations, including their expected `config-disable` and `config-remove` kinds
and parsed client/config-file values. Use the existing absoluteConfigPath
fixture and include `--apply` so both action-to-kind mappings in
parseOperationalCommand are directly verified.

In `@tests/unit/interfaces/cli/run-operational-command.test.ts`:
- Around line 13-64: Extract the duplicated runtimePaths, openRuntime,
applicationVersion, stdout, and stderr setup into a shared
createDependencies(root, output) helper, then use it in both tests. Before
parsing output in each test, assert that output contains exactly one entry; only
afterward parse output[0] and inspect the nested data fields.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f8152e7-dbcc-4227-a709-61c81a11a3fa

📥 Commits

Reviewing files that changed from the base of the PR and between b0b461f and e2e49f0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (74)
  • .prettierignore
  • README.md
  • docs/agent-integration.md
  • docs/distribution/npm-package.md
  • docs/setup-and-configuration.md
  • docs/superpowers/plans/2026-08-02-issue-41-safe-setup-and-agent-configuration.md
  • docs/superpowers/plans/2026-08-02-pr-48-review-remediation.md
  • docs/troubleshooting-agent-integration.md
  • integrations/claude-code/.mcp.json.example
  • integrations/claude-code/README.md
  • integrations/codex/README.md
  • integrations/codex/config.toml.example
  • integrations/generic-mcp/README.md
  • integrations/generic-mcp/server-config.json.example
  • package.json
  • scripts/package/smoke-installed-package.ts
  • scripts/validate-agent-integration-assets.ts
  • scripts/validate-repository-assets.ts
  • src/distribution/setup/apply-integration-change.ts
  • src/distribution/setup/backup-and-atomic-write.ts
  • src/distribution/setup/clients/claude-json-adapter.ts
  • src/distribution/setup/clients/client-adapter.ts
  • src/distribution/setup/clients/codex-toml-adapter.ts
  • src/distribution/setup/initialize-relay.ts
  • src/distribution/setup/ownership-store.ts
  • src/distribution/setup/plan-integration-change.ts
  • src/distribution/setup/setup-errors.ts
  • src/distribution/setup/setup-types.ts
  • src/distribution/setup/snippets.ts
  • src/interfaces/cli/main.ts
  • src/interfaces/cli/operational-output.ts
  • src/interfaces/cli/parse-operational-command.ts
  • src/interfaces/cli/run-operational-command.ts
  • src/interfaces/cli/run-relay.ts
  • src/interfaces/production-dependencies.ts
  • tests/fixtures/agent-integrations/valid/integrations/claude-code/.mcp.json.example
  • tests/fixtures/agent-integrations/valid/integrations/codex/config.toml.example
  • tests/fixtures/agent-integrations/valid/integrations/generic-mcp/README.md
  • tests/fixtures/agent-integrations/valid/integrations/generic-mcp/server-config.json.example
  • tests/fixtures/setup/claude-code/conflicting.json
  • tests/fixtures/setup/claude-code/crlf.json
  • tests/fixtures/setup/claude-code/empty.json
  • tests/fixtures/setup/claude-code/formatted.json
  • tests/fixtures/setup/claude-code/malformed.json
  • tests/fixtures/setup/claude-code/matching.json
  • tests/fixtures/setup/claude-code/unrelated.json
  • tests/fixtures/setup/codex/conflicting.toml
  • tests/fixtures/setup/codex/crlf.toml
  • tests/fixtures/setup/codex/empty.toml
  • tests/fixtures/setup/codex/formatted.toml
  • tests/fixtures/setup/codex/malformed.toml
  • tests/fixtures/setup/codex/matching.toml
  • tests/fixtures/setup/codex/unrelated.toml
  • tests/fixtures/setup/metadata/disabled.json
  • tests/fixtures/setup/metadata/duplicate.json
  • tests/fixtures/setup/metadata/empty.json
  • tests/fixtures/setup/metadata/enabled.json
  • tests/fixtures/setup/metadata/malformed.json
  • tests/fixtures/setup/metadata/unsupported-schema.json
  • tests/fixtures/setup/metadata/wrong-command.json
  • tests/integration/setup-workflow.test.ts
  • tests/unit/distribution/setup/apply-integration-change.test.ts
  • tests/unit/distribution/setup/backup-and-atomic-write.test.ts
  • tests/unit/distribution/setup/claude-json-adapter.test.ts
  • tests/unit/distribution/setup/codex-toml-adapter.test.ts
  • tests/unit/distribution/setup/initialize-relay.test.ts
  • tests/unit/distribution/setup/ownership-store.test.ts
  • tests/unit/distribution/setup/plan-integration-change.test.ts
  • tests/unit/distribution/setup/setup-types.test.ts
  • tests/unit/distribution/setup/snippets.test.ts
  • tests/unit/interfaces/cli/operational-commands.test.ts
  • tests/unit/interfaces/cli/run-operational-command.test.ts
  • tests/unit/scripts/validate-agent-integration-assets.test.ts
  • tsup.config.ts

Comment thread scripts/package/smoke-installed-package.ts
Comment thread scripts/validate-repository-assets.ts Outdated
Comment thread src/distribution/setup/apply-integration-change.ts Outdated
Comment thread src/distribution/setup/apply-integration-change.ts Outdated
Comment thread src/distribution/setup/backup-and-atomic-write.ts Outdated
Comment thread tests/fixtures/agent-integrations/valid/integrations/generic-mcp/README.md Outdated
Comment on lines +5 to +12
"client": "claude-code",
"configPath": "C:/tmp/claude.json",
"entryId": "relay",
"command": "relay",
"args": ["mcp"],
"status": "disabled",
"applicationVersion": "0.1.0",
"lastSuccessfulSetupAt": "2026-08-02T00:00:00.000Z"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use host-native absolute paths for valid ownership fixtures.

These fixtures use C:/tmp/... paths. Those paths are not host-native absolute paths on POSIX runners. The tests can validate a different normalization result, or reject records intended to be valid.

Construct these fixture records in the consuming tests with join(tmpdir(), ...). Preserve the duplicate-path case by creating a host-native equivalent with a . path segment.

  • tests/fixtures/setup/metadata/disabled.json#L5-L12: replace the static Windows path with test-generated host-native data.
  • tests/fixtures/setup/metadata/duplicate.json#L5-L22: generate two host-native equivalent paths for duplicate detection.
  • tests/fixtures/setup/metadata/enabled.json#L5-L12: replace the static Windows path with test-generated host-native data.
📍 Affects 3 files
  • tests/fixtures/setup/metadata/disabled.json#L5-L12 (this comment)
  • tests/fixtures/setup/metadata/duplicate.json#L5-L22
  • tests/fixtures/setup/metadata/enabled.json#L5-L12
🤖 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/fixtures/setup/metadata/disabled.json` around lines 5 - 12, Replace the
static Windows paths in tests/fixtures/setup/metadata/disabled.json#L5-L12 and
tests/fixtures/setup/metadata/enabled.json#L5-L12 with test-generated
host-native paths using join(tmpdir(), ...), and update the consuming tests to
construct these records. In tests/fixtures/setup/metadata/duplicate.json#L5-L22,
generate two host-native equivalent paths, retaining a "." segment so duplicate
detection remains covered.

"integrations": [
{
"client": "codex",
"configPath": "C:/tmp/codex.toml",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find consumers of the wrong-command.json fixture and inspect how configPath is used.
set -euo pipefail

rg -n "wrong-command" -C 8 tests/unit/distribution/setup/ownership-store.test.ts 2>/dev/null || \
  rg -rn "wrong-command" -C 8 tests

Repository: krishna916/relay

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg 'wrong-command\.json|ownership-store|metadata|setup' || true

echo
echo "== search configPath and ownership usages =="
rg -n "configPath|wrong-command|Ownership|ownership-store" -S . --glob '!node_modules' | head -200

Repository: krishna916/relay

Length of output: 26220


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ownership-store.ts validation/read/update slices =="
sed -n '1,130p' src/distribution/setup/ownership-store.ts
sed -n '175,230p' src/distribution/setup/ownership-store.ts

echo
echo "== setup-types.ts =="
sed -n '1,80p' src/distribution/setup/setup-types.ts

echo
echo "== plan-integration-change.ts slices =="
sed -n '1,90p' src/distribution/setup/plan-integration-change.ts
sed -n '100,125p' src/distribution/setup/plan-integration-change.ts

echo
echo "== tests/fixtures/setup/metadata/wrong-command.json =="
cat -n tests/fixtures/setup/metadata/wrong-command.json

echo
echo "== metadata fixture references in validate script =="
sed -n '45,80p' scripts/validate-repository-assets.ts
sed -n '270,315p' scripts/validate-repository-assets.ts

echo
echo "== Node path behavior for sample paths (non-modifying probe) =="
node - <<'JS'
const path = require('node:path');
const samples = [
  'C:/tmp/codex.toml',
  '/tmp/codex.toml',
  'C:/tmp/./codex.toml',
];
for (const sample of samples) {
  const resolved = path.resolve(sample);
  const normalized = path.normalize(sample);
  console.log(JSON.stringify({
    sample,
    type: path.isAbsolute(sample) ? 'absolute' : 'relative',
    resolved,
    normalized,
    dir: path.dirname(resolved),
    existsSync: require('node:fs').existsSync(resolved),
  }));
}
JS

Repository: krishna916/relay

Length of output: 16988


Use a POSIX absolute path here or make this fixture host-aware.

C:/tmp/codex.toml is accepted by ownership-store.ts only on Windows; on Linux/macOS it is a POSIX relative path that resolves under the current working directory and fails the ownership-record absolute-path check before matching the wrong command. Since this fixture must load with invalid metadata but not an invalid absolute path, use tests/fixtures/setup/codex/formatted.toml-style platform-native input or host-native fixtures for this test.

🤖 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/fixtures/setup/metadata/wrong-command.json` at line 6, Update the
configPath value in the wrong-command fixture to use a POSIX absolute path or a
host-aware platform-native fixture, matching the established formatted.toml
fixture pattern. Preserve the fixture’s invalid metadata and wrong-command
behavior while ensuring ownership-store absolute-path validation succeeds on
Linux and macOS.

Comment thread tests/unit/distribution/setup/backup-and-atomic-write.test.ts Outdated
Comment thread tests/unit/distribution/setup/plan-integration-change.test.ts

@krishna916 krishna916 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Re-review of latest commit c45390a after the broad CodeRabbit remediation.

Verdict: one blocking operational-safety issue remains

High — crashed client-config operations can leave a permanent lock and an unrecoverable ownership mismatch

applyIntegrationChange() now acquires <configPath>.relay-lock around the complete client write and metadata update, which correctly serializes concurrent writers. However, withExclusiveFileLock() only removes the lock in normal cleanup and never inspects or recovers an abandoned lock.

Failure sequence:

  1. Relay acquires <configPath>.relay-lock.
  2. It replaces the Codex/Claude config successfully.
  3. The process is killed before ownership metadata is persisted or before lock cleanup.
  4. The lock remains permanently.
  5. Every later setup/disable/remove operation for that config fails after the retry window.
  6. If the user manually removes the lock and retries setup, the client contains a matching Relay entry but ownership metadata may be absent, so planning fails closed as an unowned matching entry.

This is more than a temporary availability issue: the command may have changed the client file while reporting no success, and normal Relay commands cannot reconcile the state afterward.

Required correction should preserve the accepted fail-closed behavior. Do not blindly delete active locks. Add an explicit recovery design, for example:

  • inspect lock metadata and safely break only provably stale locks, plus reconcile using the exact entry and retained backup; or
  • keep manual stale-lock removal but document and implement a deterministic recovery command/workflow that restores the retained backup or adopts only an exact Relay entry after explicit operator confirmation.

At minimum, add an integration test that simulates an abandoned client lock after the client file is changed but before metadata persistence, and prove the documented recovery path restores a usable state.

Other review notes

  • The same-config concurrency lock, no-rewrite fingerprint check, optional backup behavior, error-cause preservation, host-isolated package smoke, JSON/JSONC validation split, initialization reporting, and lifecycle tests look correct.
  • The js-toml 1.2.1 replacement supports TOML 1.1 and is above the patched 1.0.2 security floor; package audit and CI pass.
  • CI run 126 passed.
  • CodeRabbit is still processing the latest commit, and several threads remain unresolved, though the static Windows-path fixture comments do not affect the production implementation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/distribution/setup/clients/codex-toml-adapter.ts (1)

74-88: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Preserve comments outside the removed table.

The section boundary falls back to the end of the file. When Relay is the last table, removeRelayEntry deletes trailing comments. When another table follows, it also deletes comments between the tables. This silently removes user content during config remove.

Preserve comment-only content outside the Relay table. Add EOF and inter-table comment cases.

🤖 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 `@src/distribution/setup/clients/codex-toml-adapter.ts` around lines 74 - 88,
The section range calculated by singleRelayHeader must exclude comments outside
the Relay table. Adjust the boundary logic used by removeRelayEntry so trailing
EOF comments and comment-only content between Relay and the next table remain
preserved, while the Relay table itself is still removed. Add coverage for both
EOF and inter-table comment cases.
🧹 Nitpick comments (3)
src/distribution/setup/ownership-store.ts (1)

113-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Derive the argument check from RELAY_ARGS.

The length check hardcodes 1 and compares only args[0]. If RELAY_ARGS gains an element, this validation rejects valid records and ignores the extra argument. Compare against the constant instead.

♻️ Proposed refactor
-    !Array.isArray(value.args) ||
-    value.args.length !== 1 ||
-    value.args[0] !== RELAY_ARGS[0] ||
+    !Array.isArray(value.args) ||
+    value.args.length !== RELAY_ARGS.length ||
+    RELAY_ARGS.some((arg, index) => value.args[index] !== arg) ||
🤖 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 `@src/distribution/setup/ownership-store.ts` around lines 113 - 115, Update the
argument validation in the ownership record check to compare value.args directly
with RELAY_ARGS, including matching length and every element. Remove the
hardcoded length and single-index comparison while preserving the existing array
validation.
tests/unit/distribution/setup/apply-integration-change.test.ts (1)

331-334: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert the error type directly instead of matching constructor.

toMatchObject compares constructor by reference through the prototype chain. The assertion works, but it is indirect and it breaks silently if the error class is wrapped. Capture the rejection, then assert the instance and the message.

♻️ Proposed refactor
-      ).rejects.toMatchObject({
-        constructor: SetupConflictError,
-        message: expect.stringMatching(/relay-lock/i),
-      });
+      ).rejects.toThrowError(
+        expect.objectContaining({ message: expect.stringMatching(/relay-lock/i) }),
+      );

Alternatively, capture the error with .catch() and assert toBeInstanceOf(SetupConflictError) plus the message, as the test at lines 287-298 already does.

🤖 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/unit/distribution/setup/apply-integration-change.test.ts` around lines
331 - 334, Update the rejection assertion in the integration-change test to
capture the rejected error, then assert it with
toBeInstanceOf(SetupConflictError) and a separate message expectation matching
/relay-lock/i. Follow the existing pattern used by the nearby test rather than
matching the constructor through toMatchObject.
scripts/package/smoke-installed-package.ts (1)

308-318: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert lifecycle effects, not only exit status.

This loop only checks result.status. A packaged CLI that exits successfully without disabling, re-enabling, or removing the entry would pass the smoke test.

After each action, inspect the Codex file and ownership metadata. Verify disabled, enabled, and removed states.

🤖 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 `@scripts/package/smoke-installed-package.ts` around lines 308 - 318, The
action loop in the smoke test currently validates only process status; update it
to inspect the Codex configuration file and ownership metadata after each
action. Assert that config disable produces the disabled state, setup restores
the enabled state, and config remove removes the entry and its ownership
metadata, while preserving the existing nonzero-status failure handling.
🤖 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 `@src/distribution/setup/clients/codex-toml-adapter.ts`:
- Around line 7-9: Update the header matching used by inspect and
removeRelayEntry to recognize whitespace around the table path components and
quoted relay table names, consistent with the parser’s accepted TOML forms.
Adjust headerPattern or the shared matching logic without changing unrelated
parsing behavior, and add regression cases covering spaced headers.

In `@src/distribution/setup/file-lock.ts`:
- Around line 33-46: The lock acquisition loop around open and
SetupConflictError must detect and recover stale lock files. Read the existing
lock payload’s acquiredAt and, when its age exceeds a bounded threshold, unlink
the lock before retrying; otherwise preserve the current retry behavior and
final SetupConflictError for active locks. Reuse the lock payload format written
by the setup lock implementation rather than introducing a separate marker.
- Around line 67-81: Update the cleanup/error propagation in the lock-release
flow so an action failure remains the primary thrown error when both the action
and lock cleanup fail. Preserve the existing cleanup error reporting when the
action succeeds, and when both fail, attach or aggregate the cleanup failure
without replacing actionError, including errors from handle.close and unlink.

In `@tests/unit/distribution/setup/apply-integration-change.test.ts`:
- Around line 313-330: Update the applyIntegrationChange invocation in the
lock-contention test to configure the directly acquired configuration lock,
using its lock options input for retry delay, maximum attempts, and sleep.
Remove the inert lockRetryDelayMs, lockMaxAttempts, and sleep settings from
createOwnershipStore unless they are also needed elsewhere, and ensure the
test’s existing lockPath contention uses the tunable settings.

In `@tests/unit/interfaces/cli/run-operational-command.test.ts`:
- Line 43: Update the test assertions around the setup command to verify the
exact configuration paths: assert that the setup command’s actual configFile
remains absent rather than checking the ownership metadata path, and change the
path assertion to equal join(root, 'config', 'config.json') so it validates
runtimePaths.configRoot.

---

Outside diff comments:
In `@src/distribution/setup/clients/codex-toml-adapter.ts`:
- Around line 74-88: The section range calculated by singleRelayHeader must
exclude comments outside the Relay table. Adjust the boundary logic used by
removeRelayEntry so trailing EOF comments and comment-only content between Relay
and the next table remain preserved, while the Relay table itself is still
removed. Add coverage for both EOF and inter-table comment cases.

---

Nitpick comments:
In `@scripts/package/smoke-installed-package.ts`:
- Around line 308-318: The action loop in the smoke test currently validates
only process status; update it to inspect the Codex configuration file and
ownership metadata after each action. Assert that config disable produces the
disabled state, setup restores the enabled state, and config remove removes the
entry and its ownership metadata, while preserving the existing nonzero-status
failure handling.

In `@src/distribution/setup/ownership-store.ts`:
- Around line 113-115: Update the argument validation in the ownership record
check to compare value.args directly with RELAY_ARGS, including matching length
and every element. Remove the hardcoded length and single-index comparison while
preserving the existing array validation.

In `@tests/unit/distribution/setup/apply-integration-change.test.ts`:
- Around line 331-334: Update the rejection assertion in the integration-change
test to capture the rejected error, then assert it with
toBeInstanceOf(SetupConflictError) and a separate message expectation matching
/relay-lock/i. Follow the existing pattern used by the nearby test rather than
matching the constructor through toMatchObject.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b4392291-d2fb-4adc-a0aa-2674b00b649f

📥 Commits

Reviewing files that changed from the base of the PR and between e2e49f0 and c45390a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (33)
  • package.json
  • scripts/package/smoke-installed-package.ts
  • scripts/validate-agent-integration-assets.ts
  • scripts/validate-repository-assets.ts
  • src/distribution/setup/apply-integration-change.ts
  • src/distribution/setup/backup-and-atomic-write.ts
  • src/distribution/setup/clients/claude-json-adapter.ts
  • src/distribution/setup/clients/codex-toml-adapter.ts
  • src/distribution/setup/file-lock.ts
  • src/distribution/setup/initialize-relay.ts
  • src/distribution/setup/ownership-store.ts
  • src/distribution/setup/plan-integration-change.ts
  • src/distribution/setup/relay-entry.ts
  • src/distribution/setup/snippets.ts
  • src/interfaces/cli/operational-output.ts
  • src/interfaces/cli/run-operational-command.ts
  • src/interfaces/production-dependencies.ts
  • tests/fixtures/agent-integrations/valid/integrations/generic-mcp/README.md
  • tests/fixtures/setup/claude-code/no-mcp-servers.json
  • tests/integration/cli.test.ts
  • tests/integration/setup-workflow.test.ts
  • tests/unit/contracts/distribution-contract.test.ts
  • tests/unit/distribution/setup/apply-integration-change.test.ts
  • tests/unit/distribution/setup/backup-and-atomic-write.test.ts
  • tests/unit/distribution/setup/claude-json-adapter.test.ts
  • tests/unit/distribution/setup/initialize-relay.test.ts
  • tests/unit/distribution/setup/ownership-store.test.ts
  • tests/unit/distribution/setup/plan-integration-change.test.ts
  • tests/unit/distribution/setup/snippets.test.ts
  • tests/unit/interfaces/cli/operational-commands.test.ts
  • tests/unit/interfaces/cli/operational-output.test.ts
  • tests/unit/interfaces/cli/run-operational-command.test.ts
  • tests/unit/scripts/validate-agent-integration-assets.test.ts
🚧 Files skipped from review as they are similar to previous changes (17)
  • tests/fixtures/agent-integrations/valid/integrations/generic-mcp/README.md
  • tests/unit/scripts/validate-agent-integration-assets.test.ts
  • src/distribution/setup/snippets.ts
  • src/distribution/setup/clients/claude-json-adapter.ts
  • tests/unit/distribution/setup/snippets.test.ts
  • tests/unit/interfaces/cli/operational-commands.test.ts
  • scripts/validate-repository-assets.ts
  • tests/unit/distribution/setup/initialize-relay.test.ts
  • tests/integration/setup-workflow.test.ts
  • tests/unit/distribution/setup/ownership-store.test.ts
  • scripts/validate-agent-integration-assets.ts
  • src/interfaces/cli/operational-output.ts
  • tests/unit/distribution/setup/claude-json-adapter.test.ts
  • src/distribution/setup/apply-integration-change.ts
  • src/distribution/setup/plan-integration-change.ts
  • src/distribution/setup/backup-and-atomic-write.ts
  • src/interfaces/cli/run-operational-command.ts

Comment on lines +7 to +9
const headerPattern = new RegExp(
`^\\s*\\[mcp_servers\\.${RELAY_ENTRY_ID}\\][^\\r\\n]*(?:\\r?\\n|$)`,
'gm',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
fd -a 'codex-toml-adapter\.ts$' . || true

echo "== file outline =="
FILE="$(fd 'codex-toml-adapter\.ts$' . | head -n1 || true)"
if [ -n "${FILE:-}" ]; then
  ast-grep outline "$FILE" --view expanded || true
  echo "== file line count =="
  wc -l "$FILE"
  echo "== relevant content =="
  cat -n "$FILE"
fi

echo "== search related symbols =="
rg -n "RELAY_ENTRY_ID|mcp_servers|singleRelayHeader|headerPattern|removeRelay|parseDocument|inspect" . --glob '*.ts' --glob '*.tsx' || true

Repository: krishna916/relay

Length of output: 14745


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Behavioral probe for the exact regex semantics used in this adapter.
node - <<'JS'
const RELAY_ENTRY_ID = "relay";
const headerPattern = new RegExp(
  `^\\s*\\[mcp_servers\\.${RELAY_ENTRY_ID}\\][^\\r\\n]*(?:\\r?\\n|$)`,
  'gm',
);
const headerRemoverPattern = /(^|\n)\s*\[mcp_servers\.relay\][^\n]*(\n|$)/s;

const samples = [
  "[mcp_servers.relay]\nkey = 'val'",
  "\n[mcp_servers.relay]\nkey = 'val'",
  "[ mcp_servers . relay ]\nkey = 'val'",
  '[mcp_servers."relay"]\nkey = "val"',
  '[ mcp_servers . "relay" ]\nkey = "val"',
  `[mcp_servers.relay]`;
];

console.log("headerPattern matches each sample:", samples.map(s => [{text: JSON.stringify(s).slice(0, 80), found: !!headerPattern.test(s)}]).flat());
console.log("headerRemoverPattern matches each sample:", samples.map(s => ({text: JSON.stringify(s).slice(0, 80), found: !!(s.match(headerRemoverPattern))})));
JS

Repository: krishna916/relay

Length of output: 762


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== codex unit tests =="
FILE="$(fd 'codex-toml-adapter\.test\.ts$' . | head -n1 || true)"
if [ -n "${FILE:-}" ]; then
  wc -l "$FILE"
  cat -n "$FILE"
fi

echo "== package parser dependency =="
if [ -f package.json ]; then
  jq -r '.dependencies["js-toml"], .devDependencies["js-toml"]' package.json
fi

echo "== node string-array behavior probe =="
node - <<'JS'
const RELAY_ENTRY_ID = "relay";
const headerPattern = new RegExp(
  `^\\s*\\[mcp_servers\\.${RELAY_ENTRY_ID}\\][^\\r\\n]*(?:\\r?\\n|$)`,
  'gm',
);
const headerRemoverPattern = /(^|\n)\s*\[mcp_servers\.relay\][^\n]*(\n|$)/s;

const samples = [
  "[mcp_servers.relay]\nkey = 'val'",
  "\n[mcp_servers.relay]\nkey = 'val'",
  "[ mcp_servers . relay ]\nkey = 'val'",
  '[mcp_servers."relay"]\nkey = "val"',
  '[ mcp_servers . "relay" ]\nkey = "val"',
];

console.log("headerPattern matches each sample:", samples.map(s => [JSON.stringify(s).slice(0, 80), !!headerPattern.test(s)]).flat());
console.log("headerRemoverPattern matches each sample:", samples.map(s => [JSON.stringify(s).slice(0, 80), !!(s.match(headerRemoverPattern))]));
JS

echo "== behavioral probe using js-toml if installed =="
node - <<'JS'
try {
  const { load } = require('js-toml');
  const samples = [
    "[mcp_servers.relay]\ncommand = 'relay'\nargs = ['mcp']",
    "[ mcp_servers . relay ]\ncommand = 'relay'\nargs = ['mcp']",
    '[mcp_servers."relay"]\ncommand = "relay"\nargs = ["mcp"]',
    '[ mcp_servers . "relay" ]\ncommand = "relay"\nargs = ["mcp"]',
  ];
  for (const content of samples) {
    try {
      const value = load(content);
      console.log("js-toml parses", JSON.stringify(content.split("\\n")[0]).slice(0, 60), ":", JSON.stringify(value.mcp_servers?.relay));
    } catch (error) {
      console.log("js-toml rejects", JSON.stringify(content.split("\\n")[0]).slice(0, 60), ":", error.message);
    }
  }
} catch (error) {
  console.log("js-toml require failed:", error && error.code === 'MODULE_NOT_FOUND' ? 'module-not-found' : error.message || String(error));
}
JS

Repository: krishna916/relay

Length of output: 3087


🌐 Web query:

TOML 1.1.0 bare key segments whitespace brackets quoted strings key segments allowed characters

💡 Result:

In TOML 1.1.0, keys are categorized as bare, quoted, or dotted [1][2]. Their specifications are as follows: Bare Keys: Bare keys may only contain ASCII letters (A-Z, a-z), ASCII digits (0-9), underscores (_), and dashes (-) [1][3]. They must be non-empty [1]. Although they may consist solely of ASCII digits (e.g., 1234), they are always interpreted as strings [1][2]. Quoted Keys: Quoted keys use the same syntax rules as basic strings or literal strings [1][2]. They allow for a much broader range of characters than bare keys, including Unicode characters, spaces, and special symbols [1][2]. They cannot use multi-line strings [1][3]. An empty quoted key is permitted but discouraged [1]. Dotted Keys: Dotted keys are a sequence of bare or quoted keys joined by a dot (.) [1][2]. They are used to group properties and implicitly define nested tables [1][4]. Whitespace and Brackets: Whitespace (spaces and tabs) around keys, around the equals sign, and around the dot separators in dotted keys is ignored [1][5]. While allowed, the use of extraneous whitespace is discouraged [1][3]. Regarding brackets, while curly braces ({}) are used to define inline tables, they are not part of the key name itself; however, keys within inline tables follow the same rules as standard keys [4]. Top results: [1], [5], [2]

Citations:


Accept TOML table headers with whitespace.

The parser accepts [mcp_servers.relay], [ mcp_servers . relay ], and quoted relay table names, but inspect and removeRelayEntry only recognize the exact bare spelling. Add matching removal logic and regression cases for spaced headers.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 6-9: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(
^\\s*\\[mcp_servers\\.${RELAY_ENTRY_ID}\\][^\\r\\n]*(?:\\r?\\n|$),
'gm',
)
Note: [CWE-1333] Inefficient Regular Expression Complexity

(regexp-from-variable)

🤖 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 `@src/distribution/setup/clients/codex-toml-adapter.ts` around lines 7 - 9,
Update the header matching used by inspect and removeRelayEntry to recognize
whitespace around the table path components and quoted relay table names,
consistent with the parser’s accepted TOML forms. Adjust headerPattern or the
shared matching logic without changing unrelated parsing behavior, and add
regression cases covering spaced headers.

Comment on lines +33 to +46
while (handle === undefined) {
try {
handle = await open(lockPath, 'wx', 0o600);
} catch (error) {
if (!isExists(error))
throw new SetupStorageError(`Setup lock could not be opened at ${lockPath}.`, error);
attempts += 1;
if (attempts >= maxAttempts)
throw new SetupConflictError(
`Another Relay configuration operation is in progress. Retry after it completes. Lock: ${lockPath}`,
);
await sleep(retryDelayMs);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

A crashed process still leaves a permanent lock file.

The lock payload records pid and acquiredAt at line 54, but no code reads it. If the holder is killed, the lock file survives. Every later relay setup, relay config disable, and relay config remove then fails with SetupConflictError until the user removes the file manually. The message now names the lock path, which helps recovery, but automatic stale-lock breaking is still missing. Read acquiredAt, treat the lock as stale after a bounded age, and unlink it before the final conflict.

🤖 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 `@src/distribution/setup/file-lock.ts` around lines 33 - 46, The lock
acquisition loop around open and SetupConflictError must detect and recover
stale lock files. Read the existing lock payload’s acquiredAt and, when its age
exceeds a bounded threshold, unlink the lock before retrying; otherwise preserve
the current retry behavior and final SetupConflictError for active locks. Reuse
the lock payload format written by the setup lock implementation rather than
introducing a separate marker.

Comment thread src/distribution/setup/file-lock.ts
Comment thread tests/unit/distribution/setup/apply-integration-change.test.ts
createDependencies(root, output),
);
expect(code).toBe(0);
expect(existsSync(join(root, 'config', 'config.json'))).toBe(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the actual configuration paths.

Line 43 checks join(root, 'config', 'config.json'). This is the ownership metadata path, not the mutable client configuration path. A preview could write the client configuration and still pass this test. Assert that the exact configFile used by the setup command remains absent.

Line 55 only checks that the path contains config.json. Assert toBe(join(root, 'config', 'config.json')) so the test verifies runtimePaths.configRoot.

As per coding guidelines, tests must cover normal flows, validation failures, and lifecycle boundaries in the layer where they belong.

Also applies to: 55-55

🤖 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/unit/interfaces/cli/run-operational-command.test.ts` at line 43, Update
the test assertions around the setup command to verify the exact configuration
paths: assert that the setup command’s actual configFile remains absent rather
than checking the ownership metadata path, and change the path assertion to
equal join(root, 'config', 'config.json') so it validates
runtimePaths.configRoot.

Source: Coding guidelines

@krishna916 krishna916 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Review verdict: one blocking recovery issue remains

The interrupted-transaction journal, recovery-before-replan orchestration, lock guidance, rollback coverage, and CI are all substantial improvements. CI run 127 passed.

High — client-written recovery can overwrite or silently accept a post-crash third-party edit

In recoverIntegrationTransaction(), the client-written path reads ownership and then either deletes the journal when ownership matches or restores the backup when ownership does not match. It never first verifies that the current client file still has journal.nextFingerprint.

This creates two unsafe cases:

  1. After Relay writes the client config and crashes, another tool/user changes that config. If ownership happens to reflect the intended state, recovery deletes the journal and reports completed even though the on-disk entry may no longer match the transaction.
  2. If ownership does not reflect the intended state, recovery restores the old backup over the newer third-party edit, violating the fail-closed/no-unknown-overwrite contract.

Before any client-written completion or rollback decision, read the current fingerprint and require it to equal nextFingerprint. If it differs, throw SetupConflictError, retain the journal and backup, and do not modify ownership or the client file.

Add regression tests for both ownership-matching and ownership-missing cases where the current file has a third fingerprint. Both must conflict and preserve the file, journal, backup, and ownership unchanged.

Until this is fixed, I would not merge the PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (4)
src/distribution/setup/integration-transaction-journal.ts (1)

52-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused applicationVersion input.

recoverIntegrationTransaction declares applicationVersion, but no code in the function reads it. Both callers pass it. Remove the field, or read it to record the recovering version.

As per coding guidelines, "Avoid any, unhandled promises, and unused arguments; prefix intentionally unused arguments with _."

🤖 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 `@src/distribution/setup/integration-transaction-journal.ts` around lines 52 -
58, Remove the unused applicationVersion field from
recoverIntegrationTransaction’s input type and update both callers to stop
passing it. Do not add replacement handling unless the recovery logic is
explicitly changed to use the version.

Source: Coding guidelines

src/interfaces/cli/run-operational-command.ts (1)

135-141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Surface the recovery outcome.

recoverIntegrationTransaction returns 'none' | 'rolled-back' | 'completed', and the CLI discards it. When Relay rolls back or completes an interrupted transaction, the user sees only the new setup result. Include the recovery outcome in the success payload so operators can see that a repair occurred.

🤖 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 `@src/interfaces/cli/run-operational-command.ts` around lines 135 - 141,
Capture the return value from recoverIntegrationTransaction in the operational
command flow and include it in the success payload, preserving the existing
setup result while exposing whether recovery was 'none', 'rolled-back', or
'completed'.
tests/unit/distribution/setup/integration-transaction-journal.test.ts (1)

355-363: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Build the backup path from root.

createCase() returns root. join(journalPath, '..', 'codex.toml.relay-backup') resolves to the same directory, but it reads as a path inside a file. Use join(root, 'codex.toml.relay-backup') here and at line 416.

🤖 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/unit/distribution/setup/integration-transaction-journal.test.ts` around
lines 355 - 363, Update the backup path construction in the test cases around
the existing-client and line-416 scenarios to use createCase()’s root value with
join(root, 'codex.toml.relay-backup') instead of deriving it from journalPath.
Apply the same change in both locations.
src/distribution/setup/apply-integration-change.ts (1)

108-123: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Delete the journal when no rollback is possible.

Line 110 returns early when backup === undefined. The no-content-change branch (lines 60-64) never sets backup, so a failure during ownershipStore.update leaves the journal file on disk with phase client-written. Nothing on the client file needs restoration in that branch, because beforeFingerprint === nextFingerprint. Recovery does clean this up on the next run, but the leftover journal makes the state look interrupted until then. Consider deleting the journal in that branch before rethrowing.

♻️ Suggested change
       if (ownershipPersisted) throw error;
-      if (journal === undefined || backup === undefined) throw error;
+      if (journal === undefined) throw error;
+      if (backup === undefined) {
+        await deleteIntegrationTransactionJournal(journalPath).catch(() => undefined);
+        throw error;
+      }
🤖 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 `@src/distribution/setup/apply-integration-change.ts` around lines 108 - 123,
Update the catch path in applyIntegrationChange so that when backup is
undefined, it deletes the existing integration transaction journal before
rethrowing the error. Preserve the current behavior for ownershipPersisted and
rollback-capable cases, and target the no-content-change path where
beforeFingerprint equals nextFingerprint.
🤖 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 `@src/distribution/setup/file-lock.ts`:
- Around line 89-95: Update formatConflictMessage so the recoveryJournalPath
branch's journal text ends with a period after “Preserve the journal and
backup,” ensuring the composed conflict message separates it from the following
“If no Relay process is active” sentence.

---

Nitpick comments:
In `@src/distribution/setup/apply-integration-change.ts`:
- Around line 108-123: Update the catch path in applyIntegrationChange so that
when backup is undefined, it deletes the existing integration transaction
journal before rethrowing the error. Preserve the current behavior for
ownershipPersisted and rollback-capable cases, and target the no-content-change
path where beforeFingerprint equals nextFingerprint.

In `@src/distribution/setup/integration-transaction-journal.ts`:
- Around line 52-58: Remove the unused applicationVersion field from
recoverIntegrationTransaction’s input type and update both callers to stop
passing it. Do not add replacement handling unless the recovery logic is
explicitly changed to use the version.

In `@src/interfaces/cli/run-operational-command.ts`:
- Around line 135-141: Capture the return value from
recoverIntegrationTransaction in the operational command flow and include it in
the success payload, preserving the existing setup result while exposing whether
recovery was 'none', 'rolled-back', or 'completed'.

In `@tests/unit/distribution/setup/integration-transaction-journal.test.ts`:
- Around line 355-363: Update the backup path construction in the test cases
around the existing-client and line-416 scenarios to use createCase()’s root
value with join(root, 'codex.toml.relay-backup') instead of deriving it from
journalPath. Apply the same change in both locations.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a5fbae1b-a28d-4bb9-9e74-3e6254e89df0

📥 Commits

Reviewing files that changed from the base of the PR and between c45390a and b33f7cc.

📒 Files selected for processing (10)
  • src/distribution/setup/apply-integration-change.ts
  • src/distribution/setup/backup-and-atomic-write.ts
  • src/distribution/setup/file-lock.ts
  • src/distribution/setup/integration-transaction-journal.ts
  • src/interfaces/cli/run-operational-command.ts
  • tests/integration/setup-workflow.test.ts
  • tests/unit/distribution/setup/apply-integration-change.test.ts
  • tests/unit/distribution/setup/integration-transaction-journal.test.ts
  • tests/unit/distribution/setup/ownership-store.test.ts
  • tests/unit/interfaces/cli/run-operational-command.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/unit/distribution/setup/ownership-store.test.ts
  • tests/unit/distribution/setup/apply-integration-change.test.ts
  • src/distribution/setup/backup-and-atomic-write.ts

Comment thread src/distribution/setup/file-lock.ts

@krishna916 krishna916 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Final re-review of the crash-recovery fix: the remaining blocker is resolved. recoverIntegrationTransaction() now fingerprints the current target before ownership completion or rollback decisions, requires currentFingerprint === nextFingerprint, and fails closed while preserving the external file, journal, backup, and ownership on mismatch. The requested ownership-present, ownership-missing, and no-content-change regression cases are covered. CI run 129 is green. No additional correctness issues found in this delta. PR remains subject to the planned real Codex/Claude manual acceptance check.

@krishna916
krishna916 merged commit 4299889 into main Aug 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant