Skip to content

feat(adopt): reverse-projection for settings.json mcpServers#61

Merged
triologue-agents[bot] merged 1 commit intomasterfrom
feat/adopt-mcp-servers
May 3, 2026
Merged

feat(adopt): reverse-projection for settings.json mcpServers#61
triologue-agents[bot] merged 1 commit intomasterfrom
feat/adopt-mcp-servers

Conversation

@LanNguyenSi
Copy link
Copy Markdown
Owner

Summary

Closes task 7059d92b, the round-trip gap from PR #59. Hand-edits to settings.json's mcpServers block now flow back into the manifest via harness adopt, completing the apply ↔ adopt cycle for MCP servers.

Drift detection comes in two flavors:

  • new: settings.json has an entry the manifest doesn't → append.
  • modified: same name, different command/env → replace by name.

Replacement preserves manifest-only fields (health, explicit enabled: false) so that hand-editing the command does not silently wipe doctor/probe/policy metadata or re-enable a server the user explicitly turned off.

CLI output reports MCP counts + replacements separately:

adopted 1 hook (h) + 2 MCP servers (a, b); replaced existing manifest entry for: b from settings.json into harness.yaml

Test plan

  • 18 new tests in tests/cli/adopt.test.ts:
    • 7 derive unit tests (parse / projection / drift / mcpEqual length-mismatch / env-mismatch / locked-in silent-drop for non-string args).
    • 11 integration tests including new-entry adoption, replace-modified, env preservation, idempotent re-adopt, hook+mcp combined drift, declined-no-write, health preservation, enabled:false preservation, and a full byte-identical round-trip: apply seeds settings.json, hand-edit changes the command, adopt captures it, re-apply produces settings.json bytes equal to the hand-edited input.
  • 655/655 total tests pass; existing adopt behavior unchanged when no mcpServers block is present.
  • Manual smoke: end-to-end round-trip in /tmp confirms apply → hand-edit → adopt → apply produces identical settings.json output.
  • Review subagent ran (general-purpose); flagged BLOCKER on silent loss of health / enabled: false on replace, plus a SHOULD-FIX that the round-trip test was structural-only (not byte-identical). Both addressed inline before push: buildMcpEntry now takes the existing McpServer and merges health / enabled: false; the round-trip test now does the full four-step cycle with bytes-equal assertion. Three additional NIT-level coverage gaps (length-mismatch mcpEqual, env-mismatch, silent-drop documentation) closed with new tests.

Reviewer notes

This completes the four-PR adoption-blocker cycle from the 2026-05-03 readiness review:

End user can now: harness apply --target ~/.claude/settings.json --merge once → hand-edit mcpServers directly in their canonical settings file → harness adopt ~/.claude/settings.json to bring the change back into the manifest → harness apply re-emits the same bytes. No drift-refuse cul-de-sac.

Refs: agent-tasks 7059d92b

Closes the round-trip gap from PR #59 (task 7059d92b). Previously
adopt only captured hooks; hand-edits to settings.json's mcpServers
block had no migration path back into the manifest, leaving the user
stuck on drift-refuse the next apply.

derive.ts gains parseSettingsMcpServers + manifestMcpProjection +
computeMcpDrift, the inverse of buildMcpServers in
generate-settings.ts. Drift comes in two flavors:
- "new": settings has an entry the manifest doesn't → append.
- "modified": same name, different command/env → replace by name.

Replace preserves manifest-only fields (health, enabled: false). The
review subagent flagged this as a blocker: without preservation,
hand-editing the command field would silently wipe doctor/probe/policy
metadata. buildMcpEntry now takes the existing McpServer (if any) and
carries health + enabled-false forward; enabled: true is the schema
default and stays implicit.

mutate.ts gains an mcp_replace action via replaceOrAppendByName that
finds the first map node with a matching name and overwrites it;
appends if not found. The replacement is whole-node (no field-level
merge in the YAML AST), so the field-preservation merge happens at
the buildMcpEntry layer where it has type-safe access to the
existing McpServer.

CLI output extended to report MCP counts and replacements separately:
  adopted 1 hook (h) + 2 MCP servers (a, b); replaced existing
  manifest entry for: b from settings.json into harness.yaml

Tests: 18 new in tests/cli/adopt.test.ts:
- 7 derive unit tests covering parse / projection / drift / mcpEqual
  edge cases (length mismatch, env mismatch, locked-in silent-drop
  for non-string args).
- 11 integration tests including: new-entry adoption, replace-modified,
  env preservation, idempotent re-adopt, hook+mcp combined drift,
  declined-no-write, health preservation, enabled:false preservation,
  AND a full byte-identical round-trip (apply → hand-edit → adopt →
  apply, with strict bytes-equal on the settings.json output).
- 655/655 total tests pass; existing adopt behavior unchanged when no
  mcpServers block is present.

Manual smoke verified end-to-end in /tmp:
  apply seeds settings.json
  → hand-edit changes command + adds new server
  → adopt --yes captures both (replaced + new)
  → apply re-emits settings.json matching the hand-edit verbatim.

Refs: agent-tasks 7059d92b

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@triologue-agents triologue-agents Bot merged commit 693e13c into master May 3, 2026
1 check passed
@triologue-agents triologue-agents Bot deleted the feat/adopt-mcp-servers branch May 3, 2026 18:37
@LanNguyenSi LanNguyenSi mentioned this pull request May 3, 2026
5 tasks
triologue-agents Bot pushed a commit that referenced this pull request May 3, 2026
* chore(release): v0.6.0

Cuts harness v0.6.0, the first minor release on top of the 0.5.0 npm
distribution.

Headline: the Phase-5 adoption-blocker cycle closes end-to-end.
- harness apply --target / --merge / --force (PR #58): write the
  rendered settings.json directly into a Claude Code settings
  discovery path, with 3-way merge that preserves user-managed
  top-level keys.
- apply translates tools.mcp[] into the settings.json mcpServers
  block (PR #59): closes the Phase 5 #1a caveat where init.mcp_servers
  in a `claude -p --settings <apply'd>` session was empty.
- apply prints a Next-steps hint after a successful run, plus --quiet
  and --json (PR #60): copy-pasteable wire-up commands prevent the
  hallucination loop that motivated this work.
- adopt reverse-projection for mcpServers into tools.mcp[] (PR #61):
  hand-edits to settings.json's mcpServers block round-trip back into
  the manifest. Manifest-only fields (health, enabled: false) are
  preserved on replace. The full apply → hand-edit → adopt → apply
  cycle is byte-identical on the settings.json bytes.

Bumps:
- package.json 0.5.0 → 0.6.0 (npm-published version)
- src/cli/index.ts .version() 0.4.0 → 0.6.0 (was already stale at
  v0.5.0 cut; corrected here)
- package-lock.json regenerated

Dogfood (7 smokes against the built dist):
- --version emits 0.6.0
- apply --target --merge preserves env + permissions, adds hooks +
  mcpServers
- --quiet suppresses Next-steps hint
- Next-steps hint present without --quiet, verify line includes
  --settings, no --output-dir hallucination
- apply → hand-edit mcpServers → adopt → re-apply produces
  byte-identical settings.json
- --json emits machine-readable outcome
- validate --check-lock detects target tampering

655/655 unit tests pass.

After tag v0.6.0:
- release.yml creates the GitHub Release with the CHANGELOG body
- publish-npm.yml publishes @lannguyensi/harness@0.6.0

Refs: agent-tasks b55f7b2b

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(release): bump remaining 0.4.0 version strings to 0.6.0

Caught by the v0.6.0 release review. The CLI Commander .version() at
src/cli/index.ts was already bumped, but four parallel sites still
hardcoded 0.4.0:

- src/cli/doctor/format.ts header banner ("harness 0.4.0")
- src/probes/mcp.ts MCP clientInfo
- src/policies/ledger-client.ts MCP clientInfo
- src/runtime/ledger-record.ts MCP clientInfo

Plus the corresponding test assertions in
tests/cli/{doctor,program}.test.ts. All bumped here to 0.6.0.

Long-term fix tracked as agent-tasks e4a4f118 (source version from
package.json instead of hand-maintaining 6 sites).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Lan Nguyen Si <nguyen-si@publicplan.de>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants