Skip to content

CMUX-37: Phases 2-5 — Blueprints, snapshot --all, browser/markdown round-trip, agent registry rows - #79

Merged
BenevolentFutures merged 20 commits into
mainfrom
cmux-37/remaining-phases
Apr 26, 2026
Merged

BenevolentFutures merged 20 commits into
mainfrom
cmux-37/remaining-phases

Conversation

@BenevolentFutures

Copy link
Copy Markdown
Contributor

Summary

  • Phase 2 — Blueprints: WorkspaceBlueprintFile + WorkspaceBlueprintStore (per-repo / per-user / built-in search path merge) + WorkspaceBlueprintExporter + three built-in starter blueprints (basic-terminal, side-by-side, agent-room). WorkspacePlanCapture extracted as shared helper. CLI: c11 workspace new [--blueprint <path|name>] (interactive numbered picker when no arg) and c11 workspace export-blueprint. Socket: workspace.list_blueprints, workspace.export_blueprint (with BLUEPRINT_ALREADY_EXISTS guard + --force). Picker UI strings localized for all 7 locales.
  • Phase 3 — Snapshot completeness: c11 snapshot --all captures every workspace in one call (CLI + socket all: bool param). New tests: browser and markdown surface spec round-trips through WorkspacePlanCapture. docs/workspace-apply-plan-schema.md — full field reference.
  • Phase 4 — Skill docs: Added "Session Resume and Blueprints" section to ~/.claude/skills/c11/SKILL.md covering snapshot/restore/blueprint commands and the agent.*.session_id convention.
  • Phase 5 — Registry rows: AgentRestartRegistry rows for codex (codex --last), opencode (bare), kimi (bare) + tests. B1 fix: correct codex command (--last vs wrong flag).
  • Review fixes (B1-B2, I1-I5, M1-M2, S1-S4): codex command correction, camelCase wire names in schema + test, CLI snapshot UX improvements, pbxproj cleanup, selectedIndex guard, opencode/kimi bare-launch revert, blueprint store isRegularFile check, JSON-mode picker error, overwrite guard.

What's already shipped (Phase 0 + 1, PRs #75 + #77)

WorkspaceApplyPlan, WorkspaceLayoutExecutor, WorkspaceSnapshotCapture/Restore, AgentRestartRegistry claude-code row, Phase 1 tests.

Test plan

  • c11 snapshot and c11 snapshot --all produce valid JSON snapshots
  • c11 restore <id> replays layout
  • c11 workspace list-blueprints returns built-in entries
  • c11 workspace new shows interactive picker; --blueprint basic-terminal opens single-terminal workspace
  • c11 workspace export-blueprint --name foo --out /tmp/foo.json writes valid blueprint file
  • Second export to same path without --force returns BLUEPRINT_ALREADY_EXISTS; --force overwrites
  • c11 workspace new --blueprint /tmp/foo.json opens the exported layout
  • --json without --blueprint on workspace new returns JSON error (PICKER_NOT_SUPPORTED_IN_JSON_MODE)
  • Tagged build smoke-test: no regressions in workspace creation, snapshot, restore flows

Tagged build for smoke-test

/Users/atin/Library/Developer/Xcode/DerivedData/c11-cmux-37-remaining/Build/Products/Debug/c11 DEV cmux-37-remaining.app
socket: /tmp/c11-debug-cmux-37-remaining.sock
CLI: /tmp/c11-cli

🤖 Generated with Claude Code

BenevolentFutures and others added 17 commits April 24, 2026 23:58
…ight minor fixes)

New files:
- Sources/WorkspacePlanCapture.swift -- shared plan walker extracted from
  WorkspaceSnapshotCapture; both snapshot and blueprint export now call it.
- Sources/WorkspaceBlueprintFile.swift -- WorkspaceBlueprintFile + WorkspaceBlueprintIndex
  value types (JSON wrapper format, same Codable pattern as snapshots).
- Sources/WorkspaceBlueprintStore.swift -- per-repo/per-user/built-in discovery,
  recency-sorted merge, atomic read/write.
- Sources/WorkspaceBlueprintExporter.swift -- thin @mainactor struct wrapping
  WorkspacePlanCapture for live-workspace-to-blueprint export.
- c11Tests/WorkspaceBlueprintFileCodableTests.swift -- round-trip Codable tests.
- c11Tests/WorkspaceBlueprintStoreTests.swift -- store tests against temp dirs.

Pre-flight fixes absorbed from Cycle-2 open items:
- B-IM2: gate bonsplitController.selectTab with selectAllowed (mirrors
  ApplyOptions.select) so background applies don't steal tab focus.
- Codex-P1: mirror metadata_override ApplyFailure to ApplyResult.warnings
  so CLI summary counts them correctly.
- StepClock rename: Clock -> StepClock throughout executor to avoid
  shadowing the stdlib Clock protocol.
- Claude-P5: detect multi-tab pane paneMetadata collision; emit
  pane_metadata_collision ApplyFailure + warning instead of silent overwrite.
- WorkspaceSnapshotCapture.swift: delegate to WorkspacePlanCapture.capture;
  remove the now-redundant inline Walker.
Adds basic-terminal.json, side-by-side.json, and agent-room.json to
Resources/Blueprints/ and registers the folder in the Xcode project so
it lands in the app bundle (accessible via Bundle.main + "Blueprints"
subdirectory for WorkspaceBlueprintStore.builtInBlueprintURLs()).
…picker

Add workspace new and export-blueprint subcommands to c11 workspace dispatch.
workspace new without --blueprint drops into a numbered interactive picker
that fetches workspace.list_blueprints and dispatches workspace.apply on
selection. workspace export-blueprint captures a live workspace layout to
the user blueprint directory via workspace.export_blueprint.

Also fix LiveWorkspaceSnapshotSource.defaultVersionString nonisolated
annotation (main actor isolation was spuriously inherited from struct).
Add 5 xcstrings keys for all 7 locales (en, ja, ko, ru, uk, zh-Hans, zh-Hant):
- workspace.blueprint.picker.prompt
- workspace.blueprint.picker.noBlueprints
- workspace.blueprint.source.repo / .user / .builtIn

Update CLI picker to use String(localized:) at each call site.
Four tests exercising browser and markdown surface kinds through the
apply -> capture -> verify cycle:
- testBrowserSurfaceKindRoundTrips: verifies kind preserved, url=nil in headless
- testMarkdownFilePathRoundTrips: verifies filePath round-trips (set at init)
- testMixedThreeSurfacePlanRoundTrips: terminal+browser+markdown in one plan
- testBlueprintExporterBrowserMarkdownKinds: exporter captures correct kinds
Add --all flag to c11 snapshot that captures every open workspace in one
call. CLI output: one OK line per workspace. Mutually exclusive with
--workspace and --out. Socket handler v2SnapshotCreate accepts all:true
and returns {snapshots: [{snapshot_id, path, surface_count, workspace_ref}]}.
Add three best-effort rows to AgentRestartRegistry.phase1:
- codex: returns "codex --last\n" (resumes most recent session globally)
- opencode: returns "opencode --continue\n" (best-effort)
- kimi: returns "kimi --continue\n" (best-effort)

These rows use --last/--continue flags rather than session IDs because
those CLIs resume the most recent session globally. Best-effort is
preferable to no resume for these tools.

Also update testUnknownTerminalTypeReturnsNil to use "cursor" instead of
"codex" (now a known type), and add 3 new test methods for the new rows.
codex resume --last is the correct CLI syntax; the prior codex --last
would fail with "unknown command". Tests updated to match.

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

WorkspaceSpec and SurfaceSpec have no explicit CodingKeys so Swift
synthesizes verbatim camelCase keys on the wire. The schema doc claimed
snake_case (customColor as custom_color, etc.) and the blueprint test at
line 94 used wrong nesting and a snake_case key (surface_ids). Fixed both.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
I1: Pass cwd to workspace.list_blueprints so per-repo .cmux/blueprints/
    discovery works from the CLI blueprint picker.
I2: snapshot --all now checks for snap["error"] per workspace; prints
    ERROR workspace=... reason=... and exits non-zero on any failure.
M1: snapshot help text now documents --all flag and mutual exclusivity.
M2: Single-workspace snapshot print now includes workspace= field for
    consistency with --all output format.

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

I3: Add WorkspaceSnapshotBrowserMarkdownRoundTripTests.swift to the
    c11Tests PBXSourcesBuildPhase so it compiles in CI.
I4: Fix stale comment on WorkspaceBlueprintIndex.Source.user: path is
    ~/.config/cmux/blueprints/, not ~/.c11-blueprints/.
I5: Fix selectedIndex > 0 guard to >= 0 so tab index 0 (first tab) is
    correctly selected when restored from a snapshot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both --continue flags were unverified against those CLIs. Reverts to
bare "opencode\n" and "kimi\n" with updated comments. Tests updated to
match.

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

S2: blueprintURLs(in:) now checks isRegularFile so directories named
*.json cannot slip through the extension filter.

S3: workspace new --json without --blueprint returns a JSON error
instead of polluting stdout with the interactive picker UI.

S4: workspace.export_blueprint errors on existing files rather than
silently overwriting; --force flag added to allow explicit overwrite.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t→restore loop

Drives the full Phase 0+1+2 path against a tagged build (own bundle id,
own socket, own DerivedData) so it can quit & relaunch the app without
disturbing the operator's primary c11.

Loop: build → apply blueprint → snapshot → osascript quit → open
relaunch → restore → re-snapshot → diff plan shape (sorted surfaces,
layout SHA1). Tolerates the snapshot-writer drift codes
`metadata_override` and `working_directory_not_applied` as informational
warnings; any other failure code is fatal.

First run surfaced two real snapshot-writer bugs worth follow-up:
  - SurfaceSpec.title double-written into metadata["title"]
  - workingDirectory captured on the seed terminal that the executor
    can't accept (cwd is fixed at workspace creation)

Round-trip is still structurally faithful — the layout fingerprint and
surface count match exactly across the quit/relaunch cycle.
Generates a blueprint with a synthesized claude.session_id and
terminal_type=claude-code metadata on s1 — the same shape the
operator-installed SessionStart hook would produce. With C11_SESSION_RESUME=1
threaded through restore, this walks the full Phase 1 path:

  snapshot capture of reserved metadata keys
    -> snapshot.restore with restart_registry=phase1
    -> AgentRestartRegistry.phase1 synthesizes "cc --resume <uuid>\n"
    -> TerminalPanel.sendText delivers the command to s1
    -> verified present in s1 scrollback after restore

Two runtime quirks the test had to work around:

1. Background-launched apps (`open -g`) leave Ghostty surfaces detached
   until something interacts with them. read-screen returns "Terminal
   surface not found" in that state. The test sends a no-op (space +
   ctrl-u) to attach the surface before reading.

2. The previous --skip-build run cleanly quit the tagged app. Re-runs now
   `open -g` if the socket is missing, before the wait-for-socket loop.

Mocking the metadata directly avoids touching ~/.claude/settings.json,
preserving c11's "unopinionated about the terminal" principle: the test
validates CMUX-37's mechanism without requiring any operator-installed
hook.
The Debug build config had PRODUCT_MODULE_NAME=c11_DEV (derived from
PRODUCT_NAME="c11 DEV"). The c11-unit scheme runs tests in Debug, so all
75+ test files that do @testable import c11 fail with "no such module 'c11'"
-- the module was named c11_DEV. Pre-existing since PRODUCT_MODULE_NAME was
explicitly set.

Fix: set PRODUCT_MODULE_NAME=c11 in the Debug build config. The product
name "c11 DEV" stays unchanged (dock/menu bar distinction preserved);
only the Swift module name is normalized.

Also update TomlSubsetParserTests.swift to the conditional import pattern
used by the new Blueprint test files (belt-and-suspenders, correct for
both module name schemes).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BenevolentFutures added a commit that referenced this pull request Apr 26, 2026
…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>
BenevolentFutures and others added 2 commits April 26, 2026 00:08
WorkspaceApplyPlanCodableTests and WorkspaceLayoutExecutorAcceptanceTests
used canImport(cmux_DEV)/import cmux — the pre-rename module names. After
PRODUCT_MODULE_NAME=c11 landed the conditional compiled away entirely,
leaving all referenced types (WorkspaceApplyPlan, SurfaceSpec, TabManager,
etc.) unresolved. Update to canImport(c11_DEV)/c11 to match all other
test files.

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

Took the more informative doc comment from main explaining why
defaultVersionString() must be nonisolated. The keyword itself is
unchanged on both sides; PR-side walker structure (WorkspacePlanCapture
extraction) preserved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit that referenced this pull request Apr 26, 2026
* C11-14: P9 P8 P10 trivial nits

- 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>

* C11-14: P3 registry identity by name

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>

* C11-14: I6 shared claudeSessionId constant across app and CLI

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>

* C11-14: P2 warn on unknown restart_registry wire names

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-14: P6 orphan-socket / wrong-owner advisory errors

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>

* C11-14: P1 header-only snapshot list summary

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>

* C11-14: I8 surface unreadable snapshot.list rows

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-14: I2 restore --in-place

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>

* C11-14: P4 acceptance suite skips in Release

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>

* C11-14: P11 thread-safe pendingInitialInputForTests

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>

* C11-14: P7 browser-first / markdown-first acceptance fixtures

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>

* C11-14: I5 followup — list-snapshots plain-table subprocess test

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>

* C11-14: P5 document claude.session_id threat model

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>

* C11-14: sync Localizable.xcstrings for ja/uk/ko/zh-Hans/zh-Hant/ru

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>

* C11-14: trident-followup B1/B2/B3 + polish

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>

* C11-14: trident-followup additional fixes

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>

* C11-14: extract skipIfReleaseBuild helper for round-trip tests

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>

* fix: nonisolated on defaultVersionString to satisfy strict concurrency

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>

* fix: set PRODUCT_MODULE_NAME=c11 in Debug build; fix TomlSubsetParserTests 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>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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
BenevolentFutures merged commit dab51bd into main Apr 26, 2026
8 of 10 checks passed
BenevolentFutures added a commit that referenced this pull request May 4, 2026
* CMUX-37: capture orchestrator handoff plan + lattice state

Final-Push Plan section drafted by orchestrator pre-handoff covers all
five workstreams (markdown blueprints, manifest layer, diagnostic
cleanup, workspace --help routing, C11_SOCKET safety) with commit
grouping, parallelization table, acceptance criteria, and Do-NOT-ship
list. Delegator phase begins from this state.

* CMUX-37: refresh plan — five-workstream close-out

Captures the prior delegator session's plan rewrite plus the Lattice state
changes (status/comments) that went with it. The plan replaces the older
phase-based shape with a single five-workstream close-out PR:

  1. Markdown blueprint format
  2. Snapshot manifest layer for `--all`
  3. Restore diagnostic cleanup
  4. `c11 workspace --help` routing fix
  5. CLI socket safety (`C11_SOCKET` + auto-discovery breadcrumb)

Driven by the 2026-05-03 smoke validation report
(/tmp/cmux-37-smoke-report.md) on tagged build cmux-37-smoke.
Original ticket Phases 0–5 already shipped via PRs #75 and #79.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* CMUX-37: workspace --help routing — two-level dispatch for `c11 workspace <sub> --help`

`c11 workspace new --help` previously printed `Unknown command 'workspace'`
because the help dispatcher had no entry for `workspace`. Add a `case
"workspace":` to subcommandUsage that recognizes the three real subcommands
(apply, new, export-blueprint), thread `commandArgs` through so the
sub-token resolves, and update dispatchSubcommandHelp to include the
resolved subcommand in the header. Top-level usage gains the four
workspace lines too.

Workstream 4 of the five-workstream close-out (smoke report 2026-05-03).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* CMUX-37 Impl A: blueprint markdown parser + writer

New `Sources/WorkspaceBlueprintMarkdown.swift` provides Foundation-only
parse/serialise for the operator-approved markdown blueprint format
(Obsidian-friendly frontmatter + fenced YAML layout block). A vendored
tiny YAML subset parser handles the indented mapping/list grammar the
schema needs without an external SwiftPM dependency.

`WorkspaceBlueprintStore.read(url:)` and `write(_:to:)` now dispatch on
the URL extension: `.md` files round-trip through the new parser, JSON
files keep the existing path. `indexEntries` reads the markdown
frontmatter to populate name and description (falling back to filename
stem when the frontmatter is missing or unparseable), so .md blueprints
appear in the picker with their authored titles.

Round-trip semantics: name ↔ frontmatter title; description ↔
frontmatter description; workspace.customColor ↔ custom_color;
workspace.title falls back to frontmatter title unless `workspace_title`
is set explicitly. SurfaceSpec metadata, paneMetadata, and description
are dropped on serialise — markdown is the hand-authored, legible form;
fully-faithful captures stay on the JSON path. Per-repo and per-user
discovery still scan only the legacy `.cmux` paths in this commit; the
filesystem rename to `.c11`/`~/.config/c11/blueprints/` lands in the
next commit alongside the exporter and CLI integration.

Tests cover round-trip equality at the value level (single-leaf,
horizontal split, nested split mirroring the operator schema), custom
color preservation, separate workspace_title, schema-example parse,
and error surface for missing layout section / missing fenced block /
unknown surface kind / split without children. Plus a store-level .md
round-trip and an updated .md picker test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* CMUX-37 Impl A: c11 directory + markdown by default; legacy paths still read

Switch the canonical blueprint write target from
~/.config/cmux/blueprints/<name>.json to
~/.config/c11/blueprints/<name>.md. Markdown is the default format;
operators who still want JSON pass --format json (also threaded over
the v2 workspace.export_blueprint method as a format param).

Per-user discovery now scans both the new and the legacy directory; the
new directory ships first so the picker prefers canonical files when
both exist. Per-repo discovery walks .c11/blueprints/ alongside
.cmux/blueprints/ at each ancestor — .c11/ entries lead the merged
list. Legacy JSON envelopes at ~/.config/cmux/blueprints/*.json
remain discoverable and applicable; the smoke acceptance criteria
require this.

runWorkspaceBlueprintNew and workspaceBlueprintPicker now sniff the
file extension on --blueprint / picker selection. .md files are
routed through a new pure workspace.parse_blueprint v2 method that
parses content received in memory and returns the plan dict — keeping
the markdown parser inside the app's link surface, not the CLI binary,
and not turning the parse path into another arbitrary-file-read.
.json files keep the existing in-process JSONSerialization path.

Help text under `c11 workspace new --help` and
`c11 workspace export-blueprint --help` is updated to name the c11
canonical paths, list both file extensions, document --format, and
show an .md example.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* CMUX-37 Impl B: cli — honor C11_SOCKET; log auto-discovery to stderr

Workstream 5 of the CMUX-37 close-out. Closes the wrong-socket footgun
the smoke validator hit on 2026-05-03: orchestrator set C11_SOCKET=...,
CLI read CMUX_SOCKET_PATH instead, fell through to auto-discovery via
last-socket-path, and silently mutated the operator's live workspace.

Changes:
- Socket-path resolution precedence is now: --socket → C11_SOCKET →
  CMUX_SOCKET_PATH → CMUX_SOCKET → auto-discovery. Both legacy keys
  remain accepted for back-compat (no deprecation warning yet — the
  plan defers that).
- Three call sites updated consistently: defaultSocketPath, run()'s
  env-var loop, claudeTeamsResolvedSocketPath. CLISocketSentryTelemetry
  also reads C11_SOCKET first so the breadcrumb context is accurate.
- After socket.path.autodiscovered fires, c11 now prints one stderr
  line attributing the resolved socket. CLISocketPathResolver gains a
  best-effort discoverySourceHint() that names the pointer file
  (last-socket-path) when the resolver matched it, or the CMUX_TAG=...
  derivation when matching a tag-derived candidate. Falls back to a
  generic "(auto-discovered)" suffix when the source is unclear.
- C11_QUIET_DISCOVERY=1 (any non-empty, non-"0" value) suppresses the
  stderr line. A global --quiet flag would touch every command's print
  loop; that's deferred per the plan's "Out of scope" note.
- usage() gains a Socket Path subsection and the Environment block now
  documents C11_SOCKET, CMUX_SOCKET_PATH, CMUX_SOCKET, and
  C11_QUIET_DISCOVERY.

New String(localized:) keys for the Translator phase:
- cli.socket.autoDiscoveredWithSource —
  "c11: using socket %@ (auto-discovered from %@)"
- cli.socket.autoDiscovered —
  "c11: using socket %@ (auto-discovered)"

Build: c11-cli scheme compiles clean against impl-A's commit
7911a41 at HEAD.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* CMUX-37 Impl B: cli — classify expected restore diagnostics as info

Workstream 3 of the CMUX-37 close-out, apply-side / CLI-side. The smoke
validator on 2026-05-03 saw `c11 restore <id>` exit 0 but emit seven
`failure:` lines for behaviors the executor deliberately reports on
every clean round trip — six `metadata_override` (one per surface with
a title) plus one `working_directory_not_applied` for the seed
terminal. Operator framing: stop scaring automation that grep-greps
for `failure:`.

Approach: Option B from the plan (CLI-side reclassification). The
ApplyFailure wire shape stays bit-exact — only the human-readable
print classifies. A future caller with a different opinion can read
the structured payload and ignore the classification.

Changes (CLI/c11.swift):
- Add CMUXCLI.FailureSeverity (.failure | .info) and two static
  helpers: failureSeverity(code:message:) and partitionFailures(_:).
  partitionFailures preserves the original order for stable output.
- Two codes get reclassified as .info:
  - `metadata_override` — fires when SurfaceSpec carries both .title
    and metadata["title"]. The capture-side fix (next commit) strips
    the dup; this is the belt for the suspenders.
  - `working_directory_not_applied` whose message contains
    "seed terminal reuse" — fires on every restore whose first
    terminal had a captured cwd. The seed shell is already running by
    the time the executor sees the spec, so the cwd cannot apply.
    Other emission sites (browser/markdown split, in-pane creation)
    stay classified as real failures.
- Three print sites updated to partition-then-print:
  - runWorkspaceApply (~L2975)
  - runWorkspaceBlueprintNew (~L3045)
  - runSnapshotRestore (~L3466)
  Real failures keep their existing prefix ("failures: N" or
  "  failure: ..."); info-level entries print under "info: N" or
  "  info: ...". Format consistency with the existing prefix per
  call site is preserved.
- DEBUG-only test hooks (debugFailureSeverityForTesting,
  debugPartitionFailuresForTesting) on CMUXCLI for future c11-cli
  test infrastructure.

CAUTION: detection of the seed-terminal case relies on the executor's
emitted message containing the substring "seed terminal reuse". If
Sources/WorkspaceLayoutExecutor.swift's
reportWorkingDirectoryNotApplicable call for the seedTerminal context
(around L656) ever changes its `context:` argument, update the matcher
in failureSeverity().

Tests: skipped — CMUXCLI lives in the c11-cli target only; c11Tests
links the c11 module, so unit-testing partitionFailures from there
would require adding c11.swift (~17k LoC) to the test target's build
phase or a new shared file with cross-target build-phase membership.
The classifier is testable via the smoke fixture during the Validate
phase (acceptance criterion: `c11 restore <id>` on the mixed
6-surface fixture prints `failures: 0`). DEBUG hooks are wired so
future c11-cli test infrastructure can exercise the partition.

No new String(localized:) keys. Existing print prefixes (`failures:`,
`failure:`) are not localized in the surrounding code, so the new
`info:` prefix follows that convention.

Build: c11-cli scheme compiles clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* CMUX-37 Impl B: capture — drop redundant title metadata to silence override warnings

Workstream 3 of the CMUX-37 close-out, capture-side. Pair to the
preceding CLI-side reclassification (3629b31). Together they close
the gap where every clean snapshot/restore round trip emitted six
bogus `metadata_override` "failure:" lines.

Root cause:
- Sources/WorkspaceLayoutExecutor.swift has dedicated setters for the
  canonical `title` (workspace.setPanelCustomTitle) and `description`
  (a separate write) fields. After they fire, it iterates
  spec.metadata and re-writes everything via SurfaceMetadataStore in
  merge mode. When metadata["title"] is also set and spec.title is
  non-nil, the executor flags this as an override even though the
  values usually agree — the dup is a round-trip artifact, not a real
  conflict.
- WorkspacePlanCapture reads `surfaceMetadata` (which contains the
  canonical "title" key, written through SurfaceMetadataStore by
  setPanelCustomTitle) AND sets SurfaceSpec.title from
  workspace.panelCustomTitles. Both make it onto the wire; the
  executor sees both and warns on every restore.

Fix:
- Add a private strippingRedundantCanonicalFields helper to
  WorkspacePlanCapture.Walker. It drops metadata["title"] (and
  metadata["description"]) when the metadata value matches the
  canonical SurfaceSpec field exactly — preserving the divergent case
  (operator wrote `set-metadata --key title --value "Foo"` separate
  from `set-title "Bar"`) where the override warning is a real signal.
- Call the helper from walkPane before constructing SurfaceSpec.
  Description is unconditionally nil from capture today, so the
  description branch is a no-op for now; the symmetry is intentional
  for when capture starts promoting description into spec.description.

Build: c11 scheme compiles clean against my WS3a commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* CMUX-37 Impl A: snapshot manifest on --all; restore polymorphic on manifest id

`c11 snapshot --all` keeps writing N independent per-workspace files
and now ALSO writes a manifest at
`~/.c11-snapshots/sets/<set_id>.json` listing the inner ids with
set-level metadata (set id, capture time, c11 version, which workspace
was selected). The new `WorkspaceSnapshotSetFile` envelope is a pure
pointer file — each inner snapshot stays independently restorable
through the existing `c11 restore <inner-id>` path; nothing is
duplicated.

`c11 restore <id>` is now polymorphic. The CLI probes
`~/.c11-snapshots/sets/<id>.json` first; when that exists the request
goes to the new `snapshot.restore_set` v2 method, which reads the
manifest and runs each inner snapshot through
`WorkspaceLayoutExecutor` in capture order, re-establishing the
selected workspace at the end (subject to the socket focus policy).
`--in-place` is rejected for set restores: a set creates several
fresh workspaces and there is no single target to replace.
Path-on-disk targets are classified by content (`set_id` +
`snapshots` → manifest, `snapshot_id` + `plan` → single
snapshot) and staged into the matching default location before the v2
call.

`c11 list-snapshots` grows two flags: `--sets` switches to the
manifest table (SET_ID, CREATED_AT, COUNT, C11_VERSION); `--all`
prints both tables back-to-back. The default output is unchanged.

New v2 methods: `snapshot.restore_set`, `snapshot.list_sets`. The
`sets/` subdirectory is naturally excluded from per-snapshot
enumeration (the existing `enumerate(directory:source:)` is
non-recursive). New unit test rounds the manifest envelope through
Codable, asserts snake_case wire keys, and confirms `selected: false`
is wire-omitted on the common case.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

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