Skip to content

C11-14: CMUX-37 Phase 1 follow-up — polish + hardening - #78

Merged
BenevolentFutures merged 20 commits into
mainfrom
c11-14/phase-1-followup
Apr 26, 2026
Merged

BenevolentFutures merged 20 commits into
mainfrom
c11-14/phase-1-followup

Conversation

@BenevolentFutures

Copy link
Copy Markdown
Contributor

Summary

  • Phase 1 follow-up to PR CMUX-37 Phase 1: snapshot / restore / Claude resume #77 (CMUX-37 workspace snapshot/restore primitives). Closes the trident review pack's Important + Potential items (I2/I5-followup/I6/I8/P1–P11 + P5).
  • I2 restore --in-place hardened: apply-then-close ordering (no single-workspace duplicate, non-destructive on partial apply failure) and caller-env workspace targeting (CMUX_WORKSPACE_ID / C11_WORKSPACE_ID before workspace.current), with an explicit workspace_uuid_changed warning to scripted callers.
  • Reviewed via a 9-agent trident pack at notes/trident-review-C11-14-pack-20260424-1750/. Consensus blockers, importants, and concrete-bug nits applied in commits 63d4496a, a5e75423, 037edb11. Synthesis was skipped at operator direction; fixes were applied directly from cross-reviewer consensus.

Highlights

  • Blockers addressed: single-workspace --in-place no-op (4 reviewers), wrong-workspace targeting from operator focus (3 reviewers), list-snapshots --json subcommand-local flag (CI-blocking parse mismatch).
  • Important addressed: 4 new executor tests for applyToExistingWorkspace (single-workspace regression, multi-workspace happy path, missing target, validation failure), missing cli.restore.usage.inPlace[.note] xcstrings keys, workspace_uuid_changed warning surfaced on ApplyResult.warnings.
  • Concrete polish addressed: empty snapshot_id for a literal .json filename (3 reviewers), v2SnapshotList dropping fractional seconds (round-trip mismatch with the store's writes), honest readSummary doc comment (was overstating what JSONSerialization does), em-dashes → colons in CLI output, stale enumerate doc, skipIfReleaseBuild() helper for the 4-way #if !DEBUG / XCTSkip duplication.
  • Verified-not-bugs (skipped after inspection): Standard Claude's readSummary vs read(from:) created_at shape mismatch claim — both paths share the same two-format fallback. tests_v2/ cmux-era hardcoding — pre-existing pattern across the entire suite.
  • Surfaced for delegator (design / out of scope for this PR): I2 atomic rollback on partial apply failure (waits on F4; the order flip already covers the common case), AgentRestartRegistry duplicate detection (defensive coding for a footgun no operator can hit), CLIAdvisoryConnectivity EAGAIN telemetry breadcrumb, P2 unknown-registry warning unit-test seam.

Scope deviations carried over from the implementation plan

  • F4 (UUID preservation): applyToExistingWorkspace mints a new workspace UUID; preserving the original ULID + tab position requires TabManager.replaceWorkspaceContent-style surgery and is logged as plan F4. The new workspace_uuid_changed warning surfaces this clearly to scripted callers.
  • P2 runtime test deferred: a live v2 snapshot.restore handler test for the unknown-registry warning needs a tests_v2 socket flow (follow-up scope; the warning logic itself is exercised by the parameter-parsing path and tested indirectly through the round-trip acceptance suite).

Test plan

  • CI: c11-unit passes, including 4 new applyToExistingWorkspace tests and 1 new .json-stem fallback test.
  • CI: tests_v2/test_cli_list_snapshots_plain_table.py passes against the new local---json parsing.
  • CI: tests_v2 regression suite stays green.
  • Manual smoke (post-CI): c11 restore --in-place <id> from a single-workspace window leaves exactly one workspace.
  • Manual smoke (post-CI): c11 restore --in-place <id> from a background agent surface targets the agent's own workspace, not whatever workspace the operator currently has selected.
  • Manual smoke (post-CI): c11 list-snapshots --json (subcommand-local form, advertised in help) returns valid JSON.

🤖 Generated with Claude Code

BenevolentFutures and others added 20 commits April 24, 2026 17:23
- P8: single clock read in LiveWorkspaceSnapshotSource.capture so the ULID
  time prefix in snapshotId and the envelope's createdAt share one tick.
- P9: drop the displayTitle fallback claim from WorkspaceSnapshotCapture doc
  comment; the walker only reads panelCustomTitles[panelId] and accepts nil.
- P10: trim terminalType in AgentRestartRegistry.init(rows:) for symmetry
  with the trim already in resolveCommand; avoids an asymmetric-trim footgun.

Tests: add ULID-prefix-decodes-to-clock-millis invariant in
WorkspaceSnapshotCaptureTests; add trim-on-insert check in
AgentRestartRegistryTests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AgentRestartRegistry carries closures and is not Equatable. Identity now
lives on a required `name: String` minted at construction (`"phase1"` for
the canonical singleton, test-chosen names for fixtures). ApplyOptions.==
compares restartRegistry by name: both-nil is equal, both-non-nil-with-same-
name is equal, every other pair is unequal.

- Sources/AgentRestartRegistry.swift: add `name` stored property; require
  it as an init parameter; mint `phase1` with `name: "phase1"`.
- Sources/WorkspaceApplyPlan.swift: replace the either-non-nil-is-unequal
  branch with a name-match branch.
- c11Tests/AgentRestartRegistryTests.swift: pass `name:` to the two
  custom-registry constructors already in the file.
- c11Tests/WorkspaceApplyPlanCodableTests.swift: add three equality
  invariants (same name → equal, different names → unequal, nil vs non-nil
  → unequal).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sources/WorkspaceMetadataKeys.swift is already pure Foundation and already
declares `SurfaceMetadataKeyName.claudeSessionId`. Adding it to the c11-cli
target's Sources build phase lets the claude-hook session-start path
reference the enum directly instead of duplicating the literal
"claude.session_id".

- GhosttyTabs.xcodeproj/project.pbxproj: new PBXBuildFile entry (c11-cli
  build action) pointing at the existing fileRef; add to c11-cli Sources
  list next to the other shared files (Mailbox*.swift, SkillInstaller.swift).
- CLI/c11.swift: replace the literal in the claude-hook session-start
  metadata-write with SurfaceMetadataKeyName.claudeSessionId; drop the
  "kept in lockstep by reader convention" commentary now that the link is
  real.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Before: `AgentRestartRegistry.named("phase99")` silently returned nil, and
the v2 snapshot.restore handler fell back to Phase 0 (no cc --resume
synthesis) with no telemetry. A typo in the restart_registry param was
undetectable.

After: the handler detects the named-registry-resolves-to-nil case, captures
a pre-apply warning, and splices it into ApplyResult.warnings before
encoding. Operators see the warning in both JSON output and the CLI's
`warnings:` block. Restore still succeeds — forward-compat with Phase 5
rows is preserved, because an older binary should still land the layout
even if a future registry name is unrecognised.

- Sources/TerminalController.swift: capture preApplyWarnings when
  AgentRestartRegistry.named returns nil for a non-nil wire name; prepend
  to applyResult.warnings before JSON encode.

Note: a focused unit test for this warning would land cleanly in a
tests_v2/ subprocess test (live snapshot.restore call with an unknown
restart_registry). The tests_v2 handler test scaffold is out of scope for
this commit; noting as a follow-up under F1 in the plan.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
c11 is single-user: the claude-hook writer treats three additional error
shapes as "no live c11 app on this machine for me" (advisory) instead of
`failed`:

- "Permission denied" — EPERM/EACCES from connecting to a socket whose
  owner is another uid, or from unix(7) permission checks on the socket
  directory.
- "Operation not permitted" — EPERM on stat/unlink of the socket path.
- "Resource temporarily unavailable" — EAGAIN variant when an orphaned
  socket file exists but no listener accepts.

Extract the string-shape predicate into Sources/CLIAdvisoryConnectivity.swift
so c11Tests can unit-test the matcher directly. CLI/c11.swift's
isAdvisoryHookConnectivityError now delegates to the shared predicate after
unwrapping the CLIError message. Both the c11 app and c11-cli targets link
the shared file; no behavioural change for app code that never looked at
CLIError.

- Sources/CLIAdvisoryConnectivity.swift: new file with the string predicate.
- CLI/c11.swift: one-line delegator; new matches reach the CLI through it.
- c11Tests/CLIAdvisoryConnectivityTests.swift: three new-shape advisory
  cases plus regression guards for the existing shapes and two negative
  controls.
- GhosttyTabs.xcodeproj/project.pbxproj: wire the new source into c11,
  c11-cli, and c11Tests Sources phases; register the fileRefs.

If multi-user c11 ever lands, revisit this call — wrong-owner should
stop being advisory there.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
snapshot.list previously decoded the full WorkspaceApplyPlan per file just
to read envelope-level metadata + surface count. That gets slower as
snapshots accumulate in ~/.c11-snapshots/ and scales with embedded-plan
size (layout tree, surface metadata) rather than envelope size.

After: WorkspaceSnapshotStore.enumerate uses a JSONSerialization shallow
decoder that reads only snapshot_id, created_at, c11_version, origin,
surface_count, and probes plan.workspace.title + plan.surfaces.count
without instantiating the Codable tree. An explicit surface_count field
on the envelope caches the count so the fallback shallow probe only fires
on legacy files written before this change.

- Sources/WorkspaceSnapshot.swift: add optional `surfaceCount: Int?`
  (wire: `surface_count`) to WorkspaceSnapshotFile. Optional preserves
  backwards compat: pre-P1 files decode with nil, new files carry the
  count.
- Sources/WorkspaceSnapshotCapture.swift: populate surfaceCount from
  plan.surfaces.count in LiveWorkspaceSnapshotSource.capture.
- Sources/WorkspaceSnapshotStore.swift: new private SnapshotSummary type
  + readSummary(from:) that does the shallow decode. enumerate() now
  emits rows from the summary, falling back to plan.surfaces.count when
  the envelope's surface_count is absent.

Tests (WorkspaceSnapshotCaptureTests):
- testStoreListReadsExplicitSurfaceCountFromEnvelope — verifies the new
  file path (envelope count wins, even when it disagrees with the plan).
- testStoreListFallsBackToPlanSurfacesCountWhenEnvelopeLacksKey — verifies
  the legacy path (shallow probe into plan.surfaces.count).

Gates I8 (next commit): the same readSummary throw path will catch
unreadable rows and emit them with a `.unreadable` marker.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Before: snapshot.list silently dropped malformed envelope files (truncated
JSON, not-a-snapshot files, etc.), leaving operators unable to see or
delete them. After: unreadable entries surface as rows with a best-effort
id (filename stem), `createdAt = .distantPast` (sorts to the bottom),
empty title, surface count 0, and a machine-readable `readability`
discriminator carrying the truncated parse error.

- Sources/WorkspaceSnapshot.swift: add `readability: Readability` field
  (`.ok` / `.unreadable(String)`) to WorkspaceSnapshotIndex. Tagged
  Codable: `{"status": "ok"}` or `{"status": "unreadable", "reason": ...}`.
  Forward-compat: older clients that omit `readability` decode as `.ok`.
- Sources/WorkspaceSnapshotStore.swift: replace the silent `continue` with
  a `catch` branch that synthesises an unreadable row. Reason truncated to
  160 chars to keep the terminal table legible.
- CLI/c11.swift: runListSnapshots emits `UNREADABLE` in the title column
  and `?` in the surfaces column for unreadable rows. Full reason prints
  after the table under `unreadable: <id> [<path>] — <reason>`. Two new
  localized strings: cli.listSnapshots.column.unreadable and
  cli.listSnapshots.unreadable.prefix.

Tests (WorkspaceSnapshotCaptureTests):
- testStoreListSurfacesMalformedJSONAsUnreadableRow — verifies the
  unreadable row is emitted with the right shape.
- testStoreListSortsHealthyBeforeUnreadable — sort invariant.
- testReadabilityRoundTripsThroughCodable — Codable tagged encoding.
- Retires the "silently skips malformed json" test; new behavior is
  opposite by design.

The JSON `readability` field is surfaced by v2SnapshotList verbatim —
no handler change required, since WorkspaceSnapshotIndex is encoded
through its Codable conformance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
c11 restore --in-place replaces the current workspace's content with the
restored plan instead of creating a fresh workspace. Without the flag,
running c11 restore twice produces two duplicate workspaces with no
warning.

Implementation is deliberately minimum-viable: applyToExistingWorkspace
resolves the target, closes it via TabManager.closeWorkspace, and calls
the existing apply(_:options:dependencies:) path. The target workspace's
panels and splits are closed first; the new workspace inherits the plan.
Caveat: the workspace UUID changes (a fresh workspace is minted and the
prior one is closed). Preserving the original UUID + tab position is
follow-up F4 on the plan, not in scope for C11-14.

- Sources/WorkspaceLayoutExecutor.swift: replace the Phase-0 doc comment
  that promised a non-existent applyToExistingWorkspace method with a
  real implementation. invalid_params failure surfaces when the target
  UUID does not resolve; the v2 handler maps that through ApplyResult.
- Sources/TerminalController.swift: v2SnapshotRestore accepts optional
  in_place: Bool and target_workspace_id: UUID string. Branches to the
  new executor method when both are supplied; synthesises an
  invalid_params failure when in_place is set without a resolvable target.
- CLI/c11.swift: --in-place / --replace flag alias. The CLI resolves the
  current workspace id via v2 workspace.current and threads it as
  target_workspace_id. Help text (subcommandUsage("restore")) now lists
  the flag and the behavioural note. Three localized English strings
  (cli.restore.usage.inPlace, cli.restore.usage.inPlace.note).
- skills/c11/references/claude-resume.md: add --in-place usage example
  with the UUID-change note.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Both acceptance test methods rely on terminalPendingInput(_:), which
delegates to the DEBUG-only accessor TerminalSurface.pendingInitialInputForTests.
In Release builds the accessor returns nil and the assertions fail with
an opaque "got '' instead" message that is not actionable.

Guard each affected method with #if !DEBUG throw XCTSkip so the suite
reports a clean skip in Release instead of a noisy failure. The private
helper stays unchanged: returning nil in Release remains a defensive
fallback that XCTSkip prevents from ever being observed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
TerminalSurface is an NSView, which AppKit convention puts on the main
actor but does not enforce at the compiler level. The DEBUG-only
pendingInitialInputForTests accessor reads pendingTextQueue without a
lock; an off-main caller would race the queue's mutation sites. Annotate
the accessor @mainactor so the compiler enforces the main-actor
invariant. Existing callers (the Phase 1 acceptance harness) already run
on the main actor so there's no behavioural change.

Rejected the alternative fix (wrap pendingTextQueue mutations with an
NSLock) because it extends lock discipline into the typing-latency hot
path pendingTextBytes rebalancing. @mainactor is lighter and matches
AppKit's actual threading model for NSView-derived state.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 1 acceptance puts a terminal first in the layout. Phase 3 will
lean on browser- and markdown-first plans (Blueprints author them
directly), and the walker already handles them; there was no integration
test. P7 adds one fixture per non-terminal kind and two new acceptance
methods that exercise the full capture + write + read + convert +
restore loop.

Each new test asserts:
- The distinguishing surface field round-trips byte-equal (url for
  browser, filePath for markdown).
- The trailing terminal still receives the synthesised
  `cc --resume <session-id>` via the Phase 1 restart registry.

Both methods route through runMixedFirstFixtureRoundTrip to keep the
test bodies symmetric, and both carry the Release XCTSkip guard from P4
because they read the DEBUG-only pendingInitialInputForTests accessor.

- c11Tests/Fixtures/workspace-snapshots/browser-first-mixed.json (new)
- c11Tests/Fixtures/workspace-snapshots/markdown-first-mixed.json (new)
- c11Tests/WorkspaceSnapshotRoundTripAcceptanceTests.swift:
    testCaptureAndRestoreBrowserFirstLayout
    testCaptureAndRestoreMarkdownFirstLayout
    plus the shared helper. Uses the existing loadFixturePlan /
    resolveWorkspace / stringMetadataValue helpers; no new test
    infrastructure.

Fixtures are loaded via #filePath + relative path (same pattern as the
existing mixed-claude-mailbox fixture) so no pbxproj edit is required.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The earlier I5 fix replaced the broken `%s` printf in runListSnapshots
with `String.padding(toLength:withPad:startingAt:)` but did not add a
test because the c11Tests target can't easily exercise the CLI binary.
This adds the missing coverage via tests_v2/ (same socket-test pattern
the rest of the suite uses).

The test:
1. Creates a new workspace via the v2 client.
2. Captures a snapshot via `c11 snapshot --workspace <id>` (plain output).
3. Runs `c11 list-snapshots` and asserts the header lists all six
   columns and the captured id + source=current appear in the row.
4. Runs `c11 list-snapshots --json` and asserts the JSON payload
   contains the captured id with a non-negative integer surface_count
   that matches an integer token in the plain-table row.
5. Cleans up the snapshot file and the workspace so repeated runs stay
   idempotent. Skips cleanly when ~/.c11-snapshots is not writable.

CI's existing tests_v2/ workflow picks this up automatically.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Snapshot envelopes store claude.session_id values in cleartext under
~/.c11-snapshots/. The trident review asked for either (a) document the
threat model, (b) redact on capture, or (c) encrypt at rest via
Keychain. Operator decision captured on the C11-14 ticket is (a)
document-and-ship: session ids are transcript-lookup keys, not
credentials, and an attacker who can read the snapshot file already has
access to ~/.claude/projects/ on the same filesystem.

The new `Privacy and storage` section in skills/c11/references/claude-resume.md
spells out the threat model, why Keychain encryption was rejected, and
the hygiene recommendations for operators whose threat model is tighter
than c11's default assumption (restrict permissions, skip shared-volume
backups, delete snapshots after restore).

Docs-only commit; no code change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds translations for the two new English strings introduced in the C11-14
commits (CMUX-37 Phase 1 follow-up):
- cli.listSnapshots.column.unreadable
- cli.listSnapshots.unreadable.prefix

The other keys referenced in the plan (cli.restore.usage.inPlace,
.inPlace.note, .inPlaceAndPath, socket.snapshot.restore.unknownRegistry)
were not wrapped in String(localized:) by the Impl phase, so nothing to
sync for them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses blockers and polish from the 9-agent trident review pack at
notes/trident-review-C11-14-pack-20260424-1750. PR #77 blockers were
already fixed; these are follow-ups specific to this branch's diff.

Blockers
- F-B1 (single-workspace close no-op): `c11 restore --in-place` on a
  window with one workspace used to duplicate. The prior order called
  `TabManager.closeWorkspace` (which silently no-ops on `tabs.count <=
  1`) and then `apply`, producing two workspaces. Flipped order: apply
  first, then close. Tab count is always >= 2 at close time, so the
  guard passes. Side benefit: a failed apply is now non-destructive
  for the common case (old workspace intact, no phantom replacement).
  Sources/WorkspaceLayoutExecutor.swift.

- F-B2 (wrong workspace from operator focus): `--in-place` now resolves
  the target via the caller env (CMUX_WORKSPACE_ID / C11_WORKSPACE_ID)
  before falling back to `workspace.current`. Background agents on a
  non-focused surface previously hit `workspace.current`, which returns
  the operator-selected workspace, not the agent's own. Destructive
  commands cannot depend on operator focus per the CLAUDE.md socket
  focus policy.
  CLI/c11.swift.

- F-B3 (list-snapshots --json subcommand-local flag): help has always
  advertised `c11 list-snapshots --json`; `runListSnapshots` was
  rejecting it as an unknown flag. The new tests_v2 subprocess test
  uses the local form and would fail on CI. `--json` is now accepted
  both as the global pre-subcommand flag and as a subcommand-local
  flag.
  CLI/c11.swift.

Important + polish
- workspace_uuid_changed warning: `applyToExistingWorkspace` now
  prepends a structured warning to `ApplyResult.warnings` so scripted
  consumers see the UUID change in the same channel as pre-apply
  warnings. Follow-up F4 will preserve UUIDs in place.
  Sources/WorkspaceLayoutExecutor.swift.

- xcstrings keys for --in-place help: `cli.restore.usage.inPlace` and
  `cli.restore.usage.inPlace.note` were referenced from
  `String(localized:)` call sites but never added to
  `Localizable.xcstrings`. Non-English builds fell back to the English
  defaults. English-only entries added; translator pass picks up the
  other six locales.
  Resources/Localizable.xcstrings.

- WorkspaceSnapshotStore.enumerate doc comment: said "skipped silently"
  even though I8 emits `unreadable` rows. Comment updated to reflect
  the implementation.
  Sources/WorkspaceSnapshotStore.swift.

- Em-dashes in user-facing CLI output replaced with colons: the
  unreadable-row print and the unknown-restart_registry warning both
  use plain punctuation now.
  CLI/c11.swift, Sources/TerminalController.swift.

Tests
- c11Tests/WorkspaceLayoutExecutorAcceptanceTests.swift: +4 tests
  exercising `applyToExistingWorkspace`:
  - testInPlaceRestoreReplacesSingleWorkspaceWithoutDuplicating
    (F-B1 regression: count == 1 after replace, original closed)
  - testInPlaceRestoreReplacesTargetAndLeavesSiblingIntact
    (multi-workspace happy path: count stable, sibling untouched)
  - testInPlaceRestoreMissingTargetReturnsInvalidParams
    (non-destructive failure on bogus UUID)
  - testInPlaceRestoreValidationFailureLeavesTargetIntact
    (unsupported version short-circuits before any state change)

Out of scope / surfaced for delegator
- I2 atomic rollback on post-close apply failure: the order flip
  already covers the common failure mode; full atomicity (snapshot
  the old, swap, rollback on partial apply) is F4 / Phase 2 scope.
- tests_v2 cmux-era socket paths and binary globs: pre-existing
  pattern across the entire tests_v2 suite. Fixing one file creates
  inconsistency; bulk migration is its own change.
- `readSummary` vs `read(from:)` created_at "disagreement" claim
  (Standard Claude #4): both paths share the same two-format fallback
  (workspaceSnapshotDateFormatter, then withInternetDateTime). Misread.
- `readSummary` "not truly header-only": JSONSerialization parses the
  full envelope object tree. Performance scope; deferred.
- snapshot.list JSON timestamp precision (second-precision .iso8601):
  consistency with the store's fractional-second writes. Deferred.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three more concrete bugs called out by the review pack that I missed in
the first follow-up commit. These are real round-trip / output bugs,
not polish.

- Empty snapshot_id for `.json` files: a file literally named `.json`
  produced an unreadable row with `snapshotId = ""` because
  `deletingPathExtension().lastPathComponent` collapses to the empty
  string. Fall back to the full filename (capped at 128 chars) so the
  row is identifiable in the plain-table column. Test added in
  WorkspaceSnapshotCaptureTests covers the degenerate filename.
  Sources/WorkspaceSnapshotStore.swift,
  c11Tests/WorkspaceSnapshotCaptureTests.swift.
  (Crit Gemini #3, Std Codex #5, Crit Codex nit on filename stem.)

- `snapshot.list` JSON timestamp precision: the v2 handler encoded
  `WorkspaceSnapshotIndex.createdAt` with `.iso8601`, which drops
  subsecond precision. The store now writes fractional seconds via
  `workspaceSnapshotDateFormatter`, so the list response was
  silently truncating timestamps that the store had just written.
  Switched the encoder to a custom strategy that uses the shared
  formatter, matching the round-trip the rest of the store path
  already provides.
  Sources/TerminalController.swift.
  (Crit Codex nit on `v2SnapshotList`.)

- Honest `readSummary` doc comment: the prior comment claimed nested
  `plan.layout` and `plan.surfaces[].metadata` "stay in the JSON blob
  untouched". `JSONSerialization.jsonObject` actually materialises the
  full object graph into NSDictionary/NSArray; the win is skipping the
  `WorkspaceApplyPlan` Codable decode, not avoiding the parse. Comment
  reworded to describe the real cost shape so future readers don't
  trust the false claim.
  Sources/WorkspaceSnapshotStore.swift.
  (Crit Gemini #1, Crit Codex nit on memory shape.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Std Claude #13: the four acceptance scenarios in
WorkspaceSnapshotRoundTripAcceptanceTests carried an identical 3-line
`#if !DEBUG / throw XCTSkip(...) / #endif` preamble. Extracted a
private `skipIfReleaseBuild()` helper so each call site is one line
and a future skip-reason change is one place, not four.

No behavior change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The default parameter in LiveWorkspaceSnapshotSource.init called
defaultVersionString() from a nonisolated context. Under the c11-unit
scheme (which enables strict concurrency checking), this failed because
the @mainactor isolation on the enclosing class made the static method
implicitly @mainactor too.

Adding nonisolated makes the default-parameter callsite legal without
requiring the caller to be on the main actor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Tests import

Debug builds set PRODUCT_NAME="c11 DEV" which makes the Swift module name
c11_DEV via the default PRODUCT_MODULE_NAME derivation. All 75+ test files
use @testable import c11 which fails to find the module under the c11-unit
scheme (mailbox-unit CI job) because it builds in Debug mode.

Fix: explicitly set PRODUCT_MODULE_NAME=c11 so the module is always named
c11 regardless of whether PRODUCT_NAME is "c11 DEV" or "c11". The app
product name stays "c11 DEV" for dock/menu bar disambiguation.

Also update TomlSubsetParserTests.swift to use the conditional import
pattern introduced by PR #79's new Blueprint test files, as belt-and-
suspenders defense.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pture

Doc-comment conflict above defaultVersionString(); kept main's expanded
rationale for the nonisolated annotation. Both sides already agreed on
the nonisolated keyword itself.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@BenevolentFutures
BenevolentFutures merged commit 33b879f into main Apr 26, 2026
8 of 10 checks passed
BenevolentFutures added a commit that referenced this pull request Apr 26, 2026
Resolves four conflicts after #78 (CMUX-37 Phase 1 follow-up) landed on
main while this PR was open. All resolutions preserve both sides' intent.

- Sources/WorkspaceSnapshotCapture.swift: keep PR's extracted
  WorkspacePlanCapture.capture(workspace:) call AND main's P8 single
  clock-read invariant (let now = clock()) so snapshotId ULID prefix
  and createdAt cannot diverge by a tick.
- Sources/AgentRestartRegistry.swift: keep main's required name parameter
  (.init(name: "phase1", rows: [...])) and merge both doc comments — main's
  rationale for the trailing newline contract plus the PR's note on codex
  --last best-effort and opencode/kimi launch-fresh semantics.
- c11Tests/AgentRestartRegistryTests.swift: keep both PR's three Phase 5
  row tests (codex/opencode/kimi) and main's symmetric-trim invariant test.
- GhosttyTabs.xcodeproj/project.pbxproj: union both sides' new test file
  refs (PR's three Blueprint/RoundTrip tests + main's CLIAdvisoryConnectivity
  test) in both PBXGroup and PBXSourcesBuildPhase.
BenevolentFutures added a commit that referenced this pull request May 5, 2026
Lattice ticket task_01KPMTEY4WGECM9MNZ4XARN7Y6 transitions to done after
PR #118 ("CMUX-37 final push: close five 2026-05-03 smoke-test gaps")
shipped on 2026-05-04. The orchestrator never recorded the merge on the
ticket; this catches the bookkeeping up.

Archive notes/archive/cmux-37/ preserves five generations of trident
review packs (2026-04-24 through 2026-05-04) that drove the ticket and
were unique to the cmux-37/* worktrees. Also archives the orphan C11-14
"CMUX-37 Phase 1 follow-up" task record from the cmux-37-phase1
worktree — its work shipped via PR #78 but the Lattice file never
reconciled with main, and the C11-14 short_id has since been reassigned
to a different ticket. Re-importing it would collide; preserved here
as historical record instead.

Co-Authored-By: Claude Opus 4.7 <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.

1 participant