feat: integrate Docker Sandbox placement and Crabbox execution - #1
Open
chsong1 wants to merge 7 commits into
Open
feat: integrate Docker Sandbox placement and Crabbox execution#1chsong1 wants to merge 7 commits into
chsong1 wants to merge 7 commits into
Conversation
chsong1
force-pushed
the
fm/docker-sandbox-crabbox
branch
from
August 12, 2026 09:46
60a8c9c to
2dbafcc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Integrate Docker Sandboxes and Crabbox into the Firstmate harness as two independent, opt-in modules while preserving current host/direct/local defaults and all existing worker-runtime backends. Docker Sandbox owns workspace placement for ordinary workers and persistent secondmates, supports direct or clone workspace modes plus optional Docker Kits and official agent presets, keeps canonical task metadata and private bridge cursors on the host, synchronizes only bounded status/turn-end deltas, reuses exact recorded placements on relaunch, and releases only exact recorded placements on cleanup. Crabbox owns task-scoped command execution only: explicit configured profile or recorded lease routing, argv-preserving execution without eval, provider snapshot validation, run/lease/cost/expiry persistence, bounded logs, explicit release, and no role as a worker or secondmate carrier. Configuration is a strict optional inherited JSON file; missing optional tools stay silent unless selected, then bootstrap emits one actionable manual-install diagnostic. Preserve existing firstmate lifecycle, no-mistakes, trace-context, remote-secondmate, and host behavior. Add observable fake-tool behavioral coverage for placement, bridge safety, command execution, changed-test selection, and secondmate persistence/recovery. Do not install Docker Sandbox or Crabbox; their binaries are absent, so live provider smoke is not part of this change.
What Changed
Risk Assessment
✅ Low: The reviewed delta contains no additional substantiated source risk beyond prior-round decisions.
Testing
Focused fake-tool, lifecycle, backend-preservation, changed-test-selection, bridge-safety, placement, and command-execution checks passed after serial confirmation; reviewer-visible CLI evidence was captured, the worktree remained clean, and live Docker Sandbox/Crabbox smoke was intentionally not attempted because their binaries are absent by design.
Evidence: Sandbox secondmate lifecycle transcript
Evidence: Crabbox command execution transcript
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 8 issues found → auto-fixed (30) ✅
bin/fm-workspace-execution-config.sh:92- The intent requires “supports direct or clone workspace modes,” but configuration accepts onlyworkspaceMode=directfor workers and secondmates, while fresh Docker placements hard-codedirect. The adapter’s clone implementation is unreachable through the supported lifecycle.bin/fm-teardown.sh:2317- The intent requires “releases only exact recorded placements on cleanup,” but forced parent cleanup deletes child metadata and homes without releasing recorded Docker placements or bridges. A Docker-placed child can therefore leak its sandbox.bin/executors/crabbox.sh:43- Profile execution invokescrabbox run --job, but the documented Crabbox interface uses--profile <name>. Selected profile execution will fail before running the command. See the Crabbox run command.bin/fm-exec.sh:334- The intent requires “provider snapshot validation, run/lease/cost/expiry persistence,” but the adapter discards provider lifecycle output andfm-execrecords only local run metadata; snapshots are validated only as arbitrary JSON objects. Provider runs therefore cannot be durably reconciled or released.bin/executors/crabbox.sh:60- The intent requires “bounded logs,” but Crabbox logs are streamed without any byte or line cap. A noisy provider can makefm-exec logsemit unbounded output.bin/fm-spawn.sh:2318- The intent requires preserving the existing firstmate lifecycle, but ordinary Docker workers skip busy-state arming. Their bridge turn-end hooks do not provide a busy verdict, so stable active turns can be classified as missing/unknown and enter the watcher’s stale path.bin/executors/crabbox.sh:42- Crabbox is passed the task worktree but ignores it, invoking the provider from the caller’s ambient directory. Since Crabbox syncs the current checkout before execution, a task can run against the wrong repository. See the Crabbox run command.bin/fm-sandbox-bridge-lib.sh:307- Bridge status append and cursor advancement are separate operations. If append succeeds but cursor persistence fails, retrying re-appends the same delta and can duplicate wakes; use a durable transactional or idempotent boundary.🔧 Fix: Correct Crabbox profile routing and make bridge sync retry-idempotent
7 issues (4 errors, 3 warnings) still open:
bin/fm-spawn.sh:321- The required criteria preservetrace-contextandremote-secondmate, but the parser no longer consumes--traceparent; remote and local launches treat it as a positional argument and reject the spawn. Restore explicit option handling before positional validation.bin/fm-spawn.sh:1147- Selecting Crabbox makes every fresh worker/secondmate spawn fail: this path unconditionally callsfm_command_execution_prepare, while Crabbox's prepare hook deliberately returns 1 because it has no prewarm operation. Crabbox is required to be task-scoped command execution only, so preparation must not block carrier creation.bin/fm-workspace-execution-config.sh:94- The strict JSON validator acceptskits: nulland the loader silently converts it to[], despite kits being required to be an array. Reject explicit null values instead of silently discarding malformed configuration.bin/fm-workspace-execution-config.sh:64-jq -e .accepts multiple top-level JSON values, and the subsequent schema filter validates them independently. The “strict optional inherited JSON file” can therefore pass validation and fail later ambiguously; require exactly one JSON document.bin/fm-spawn.sh:2711- Profile values are checked only for nonempty strings. A configured or explicit profile containing newline, carriage return, or tab is written directly into canonical task metadata at this path, allowing metadata-field injection or corruption. Reject control whitespace before publication.bin/fm-sandbox-bridge-lib.sh:112- Bridge validation canonicalizes the parent roots and exact lexical path but never verifies the task bridge directory itself is a real, non-symlink directory. Replacing<state>/sandbox-bridge/<task-id>with a symlink can redirect binding/status/runtime reads and cleanup outside the canonical bridge root, violating private host cursors and canonical host binding.tests/fm-test-run.test.sh:96- The changed-test fixture no longer createstests/fm-backend.test.shortests/fm-ask-user-authority.test.sh, but later assertions still require both paths in event-wait, non-bin-source, and changed-source selection cases. The behavioral test cannot pass with the fixture as written; restore the fixtures or update the expected selection contract.🔧 Fix: Restore missing changed-selection fixture scripts
7 issues (4 errors, 3 warnings) still open:
bin/fm-spawn.sh:2286- The required behavior is “synchronizes only bounded status/turn-end deltas.” Fresh Docker spawns switch to the bridge copy of the brief at this line, but the generated brief still tells agents to append to the absolute host path$STATE/$ID.status(bin/fm-brief.sh:179,434). Only the worktree and bridge are mounted, so Docker workers and secondmates cannot publish status throughbridge/status; watcher synchronization has nothing to consume.bin/fm-spawn.sh:2282- The intent requires “canonical task metadata and private bridge cursors on the host” while crossing only bounded deltas. The bridge is mounted as an ordinary extra workspace here; Docker mounts extra workspaces read-write unless:rois used (workspace mounting docs). A sandbox can therefore rewriteruntime-brief.mdorfm-operational-input.sh, and relaunch trusts the modified brief while validation checks only regular-file safety and binding. Keep immutable bridge inputs read-only or outside the writable mount.bin/placements/docker-sandbox.sh:235- The required criterion is “releases only exact recorded placements on cleanup,” but normal teardown invokes this release path without force. It unconditionally stops the verified sandbox and then removes it without--force; Docker documents thatsbx rmrequires confirmation and--forcefor non-interactive scripts or in-use sandboxes (sbx rm docs), whilesbx stoponly stops running sandboxes (sbx stop docs). A stopped or attached placement can therefore make teardown prompt or fail and leave resources behind; even forced teardown can fail before reaching forced removal if stop errors.bin/fm-teardown.sh:2628- Crabbox is required to support “explicit release” and “run/lease/cost/expiry persistence.”fm-execrecords explicit leases instate/<id>.execution, but teardown neither checks for nor releases an unreleased lease; this cleanup removes the task metadata while leaving the execution journal and snapshot. Afterrun --lease lease-afollowed by teardown,fm-exec releasecan no longer run because metadata is gone, leaking the provider lease; reusing the task ID can also make the stale journal’s lease current for the new task. Refuse teardown while a lease is active or release that exact lease before erasing the task identity.bin/fm-spawn.sh:2581- Docker OpenCode spawns create.opencode/plugins/fm-sandbox-turnend.jshere, but ordinary teardown removes onlyfm-turn-end.jsatbin/fm-teardown.sh:2514. The new ignored hook can survive treehouse return and be loaded by a later task using the pooled worktree, producing stale turn-ended notifications. Remove this exact generated file in the corresponding teardown and abort-cleanup paths.bin/fm-watch.sh:317-watch_meta_valuerejects duplicate or malformed placement fields, but the watcher silently continues at this line. A corrupted Docker task metadata file is skipped beforewatch_sync_sandbox_bridgecan emit its existing actionable check wake, so bridge status and turn-end deltas stop syncing without diagnosis. Route malformed Docker candidates through the failure wake instead of dropping them.bin/placements/docker-sandbox.sh:27- The existing task-ID contract accepts underscores (bin/fm-pr-lib.sh:93-109), but the Docker placement adapter rejects them when deriving the sandbox name. A valid task such asfoo_bartherefore cannot opt into Docker Sandbox placement for a worker or secondmate. Use a collision-safe name encoding or explicitly narrow the task-ID contract before accepting this placement.🔧 Fix: Clean Docker OpenCode hooks and surface malformed metadata
3 issues (2 errors, 1 warning) still open:
bin/placements/docker-sandbox.sh:205- The required criteria are “reuses exact recorded placements on relaunch” and “releases only exact recorded placements on cleanup,” but inspect/release identify a sandbox only by deterministic name. If the original sandbox is deleted and another with the same name is created, relaunch adopts the replacement and teardown can remove it even if its workspace or bridge differs. Persist and verify an immutable provider identity, or fail closed when it cannot be proven.bin/fm-spawn.sh:2284- Fresh Docker spawns create the backend endpoint/worktree before bridge and placement setup. If bridge creation fails, abort cleanup is not armed; if placement setup fails, cleanup still never removes the backend endpoint, and a partial provider placement can remain unrecorded because its handle is assigned only after preparation succeeds. Register resources at creation and unwind all prepublication failures.bin/fm-spawn.sh:1131- Explicit per-axis overrides retain incompatible dependent configuration. With a Crabbox default/profile,--executor localleaves the profile populated and is rejected; with Docker kits,--placement hostleaves the kits populated and is rejected. An operator cannot opt one task back to host/local defaults without editing config. Clear dependent values on override or explicitly define and document this rejection contract.🔧 Fix: Make fresh Docker spawn cleanup transactional
6 issues (4 errors, 2 warnings) still open:
bin/placements/docker-sandbox.sh:159-sbx createfailure cleanup uses only the deterministic name before ownership is proven, so a race can remove another sandbox; if cleanup fails, no acquired handle is exported. The clone path also returns without cleanup. This contradicts “releases only exact recorded placements on cleanup.”bin/fm-spawn.sh:739- Abort cleanup clears its bookkeeping before resource release and only warns on failure. A prepublication abort can leave an endpoint, worktree, bridge, or placement live with no metadata or retry record, violating “Preserve existing firstmate lifecycle.”bin/fm-spawn.sh:2092- Endpoint cleanup is registered only after backend creation fully returns. Zellij, cmux, and Herdr can allocate an endpoint before pane/surface parsing or verification fails, leaving partial endpoints outside the new transaction.bin/fm-spawn.sh:2297-treehouse getcan acquire a worktree before settling or validation completes, but registration occurs afterward. Timeout or validation failure therefore leaves no exact handle fortreehouse return, violating “Preserve existing firstmate lifecycle.”bin/fm-spawn.sh:2337- The bridge path is recorded only after bridge creation succeeds. If creation fails after partial bridge/cursor creation and its best-effort cleanup also fails, the abort trap has no path to retry cleanup, conflicting with host-private bridge ownership.bin/fm-watch.sh:321- Any nonempty placement value other thandocker-sandboxis silently skipped as if it were host.placement=bogustherefore stops bridge synchronization without an actionable wake, violating “synchronizes only bounded status/turn-end deltas.”🔧 Fix: Harden transactional cleanup and exact resource ownership
3 errors still open:
bin/placements/docker-sandbox.sh:108- The required criterion is “releases only exact recorded placements on cleanup.” Inventory JSON with an unsupported or scalar wrapper is normalized to an empty array at this line instead of rejected.inspectthen treats an existing sandbox as absent, allowing teardown to delete task metadata without releasing the provider placement; fresh creation can likewise leave an unrecorded sandbox.bin/fm-spawn.sh:2083- The required criterion is “Preserve existing firstmate lifecycle” with exact resources registered at acquisition. Backend creation can succeed and produce an exact endpoint, but if acquisition-record persistence fails, the caller ignores the failure (spawn_register_endpoint_from_file || true) and the durable record remains unusable. EXIT cleanup then has no endpoint handle and cannot retry cleanup, affecting tmux, zellij, cmux, and Herdr paths.bin/fm-spawn.sh:2486- The required criterion is “Preserve existing firstmate lifecycle.” Aftertreehouse getacquires a worktree, registration occurs only after two consecutive matching path observations. If settlement times out or keeps changing, the worktree remains acquired but no cleanup record is written, so the abort path never returns it and no durable retry record exists.🔧 Fix: Harden inventory and preserve exact acquisition cleanup
7 issues (5 errors, 2 warnings) still open:
bin/fm-spawn.sh:3079- After fresh metadata is written, this marks publication complete, but the EXIT trap still runs prepublication cleanup until later flags are cleared. An interruption in that window can release the published task’s endpoint, worktree, bridge, or placement while its metadata remains live, violating “Preserve existing firstmate lifecycle.”bin/fm-spawn.sh:2552- Every tentative pane cwd is registered as the cleanup handle. If treehouse acquires worktree B but a stale/foreign pane observation A arrives before settlement, abort cleanup can return A and leak B, violating “Preserve existing firstmate lifecycle.” Keep settlement observations separate from the exact acquired worktree handle.bin/backends/cmux.sh:379-new-workspacecan succeed while the follow-up workspace-list lookup fails. This returns without writing an acquisition record or emitting an exact ID, so spawn cleanup cannot remove the created cmux workspace, violating lifecycle preservation and exact endpoint ownership.bin/fm-sandbox-bridge-lib.sh:448- Teardown retains metadata when this removal fails, but the sequential deletion can partially succeed. If the bridge is removed and cursor deletion fails, the next retry rejects the missing bridge during validation and can never finish cleanup, violating “releases only exact recorded placements on cleanup.”bin/placements/docker-sandbox.sh:130- The inventory wrapper is validated, but malformed members are silently reduced to empty fields and skipped. A live recorded sandbox with a malformed entry can therefore appear absent to inspect, allowing teardown to erase metadata without releasing the provider placement.bin/fm-spawn.sh:887- Endpoint acquisition creates an empty file before backend creation. If creation fails before an endpoint is recorded, this condition still writes a durable cleanup record withendpoint_cleanup=0, and the empty file remains; the watcher then reports a permanent unpublished-cleanup wake for a resource that was never acquired.bin/fm-spawn.sh:2935- The Docker OpenCode hook interpolatesTURNENDdirectly into a JavaScript string. Valid paths containing quotes, backslashes, or tabs can produce invalid or altered hook code; escape the path as a JSON/JavaScript string before writing the plugin.🔧 Fix: Harden spawn publication and exact cleanup ownership
5 errors still open:
bin/fm-teardown.sh:500- Teardown validates a complete bridge and existing worktree at this line. If bridge removal partially succeeds or later worktree/home cleanup fails, the retained task cannot be retried because the next run rejects the now-missing bridge/worktree before reaching idempotent cleanup. Make teardown validate the remaining cleanup phase or remove bridge/cursor before returning the worktree.bin/fm-spawn.sh:2611- The non-lease path sendstreehouse getat line 2571 but arms worktree cleanup only after two settled cwd observations here. If settling or path reads time out, an acquired worktree has no exact cleanup handle or durable retry record and leaks. Register the exact provider result before observation, or retain an exact fallback handle through timeout.bin/backends/cmux.sh:386-cmux new-workspacehas already created a workspace before this pending-record write or the subsequent title lookup can fail. The resultingcmux-pendingrecord is not consumed by endpoint cleanup, and the failure path may emit no exact workspace ID, leaving the workspace live with only a non-actionable title record. Emit an exact ID or make pending records consumable by exact cleanup.bin/fm-spawn.sh:2566- Cleanup is armed beforevalidate_spawn_worktreeproves isolation. If treehouse returns the primary checkout, a foreign worktree, or a non-git path, abort cleanup can still runtreehouse return --forceon that unverified path, potentially returning another task's worktree. Keep candidate observations separate and only arm destructive cleanup after ownership validation; retain a non-destructive retry record otherwise.bin/fm-spawn.sh:1275- Remote secondmate handling begins here before the new placement/executor/profile axes are resolved or forwarded; the launch arguments later contain only legacy harness/model/effort/backend values. A request such as--secondmate --placement docker-sandboxor--executor crabbox --execution-profile pis silently accepted but launches with remote SSH/Herdr and remote defaults, contradicting the required Docker placement for persistent secondmates and the documented per-spawn overrides. Reject these flags explicitly on remote routes or propagate them through the remote control contract.🔧 Fix: Harden phased cleanup and exact worktree recovery
1 error still open:
bin/backends/cmux.sh:386- The required contract says “Preserve existing firstmate lifecycle,” but aftercmux new-workspacesucceeds, a failure writing the pending acquisition record at this line returns without emitting the exact workspace ID.fm_spawn.shthen cannot register or durably clean up the created workspace, leaving it orphaned. Resolve and retain an exact ID before this write or make every post-create failure produce a consumable cleanup record.🔧 Fix: Make cmux acquisition ownership transactional
2 issues (1 error, 1 warning) still open:
bin/backends/cmux.sh:388- Aftercmux new-workspacesucceeds but ID resolution fails, this persists only the workspace title;fm-spawn.sh:764then resolves the first matching workspace and treats it as exact cleanup authority. A duplicate/title race can close an unrelated workspace while leaking the created one, violating “Preserve existing firstmate lifecycle.” Capture the exact ID before fallible publication or retain a non-destructive unresolved record; never clean up by title.tests/fm-backend-cmux.test.sh:522- The new failure-path test only rejects writes taggedcmux-pending, but its response sequence resolves a workspace ID and writes a normalcmux-workspacerecord. It therefore does not exercise the pending-record write failure it claims to cover.🔧 Fix: Enforce exact cmux identity during cleanup
5 errors still open:
bin/backends/cmux.sh:765- Exact cleanup scans only the current cmux window. If the task workspace was created in window A and the current window is B, the missing ID is treated as already gone, so abort clears its cleanup record while the workspace remains live. Search all windows before declaring the exact workspace absent.bin/backends/zellij.sh:380-fm_backend_zellij_session_existsreturns failure both when the session is absent and whenzellij list-sessionsfails. This helper treats either case as successful cleanup, allowing a transient CLI/server error during abort to forget the exact tab and leak it. Distinguish verified absence from inspection failure and retain cleanup for retry.bin/fm-sandbox-bridge-lib.sh:280- The runtime brief copy is unchecked. If the source brief disappears or cannot be read after validation, an empty or truncatedruntime-brief.mdcan still pass metadata/mode validation and be published to a Docker task. Make the copy checked and atomic, failing and cleaning up on error.bin/fm-spawn.sh:2742- Bridge creation acquires the bridge and cursor before the caller copies their exact IDs intoPLACEMENT_*and enables bridge cleanup. An interruption between these assignments can leave the bridge unregistered; abort cleanup has a provider fallback but no equivalent bridge fallback. Publish the acquired bridge cleanup handle before the next fallible boundary.bin/fm-bootstrap.sh:1107- The required criterion says “missing optional tools stay silent unless selected, then bootstrap emits one actionable manual-install diagnostic.” Selected Docker configuration checks onlysbxhere, although the Docker placement adapter also requiresjq; withjqabsent, config loading can fail before the manual-install diagnostic or proceed without reporting the missing dependency. Include all selected adapter dependencies in the diagnostic path.🔧 Fix: Hardened exact cleanup and atomic bridge ownership
6 issues (5 errors, 1 warning) still open:
bin/backends/tmux.sh:93- The new transactional writers use GNU-only--separators (mv -f --/rm -f --here and in zellij:347, cmux:402, herdr:2000, spawn-cleanup:46; bridgecp/mv/rmat fm-sandbox-bridge-lib.sh:289-340). macOS BSD tools reject these options, so fresh backend acquisition and Docker bridge publication fail, while cleanup journals cannot be persisted.bin/fm-sandbox-bridge-lib.sh:257- The required criteria are “Preserve existing firstmate lifecycle” and “releases only exact recorded placements on cleanup.” After creating the bridge, identity lookup can fail at this line or during cursor initialization at line 267, leaving an acquired bridge/cursor with empty IDs; removal requires nonempty matching identities at lines 327-334, so the resource has no exact retryable cleanup handle.bin/fm-spawn-cleanup-lib.sh:32- The cleanup-record validator sends lines containing newline, carriage-return, or tab into key parsing instead of rejecting them. A control-character value can therefore inject extra journal fields and corrupt durable cleanup state; reject control characters before parsingkey=value.bin/fm-spawn.sh:912- The required criterion is “Preserve existing firstmate lifecycle.” The shared gate treats an empty acquisition journal/result as proof that no endpoint exists at this line, but backend creation can fail or return no usable ID after creating a window, tab, or workspace. Abort then removes the empty journal and loses cleanup authority, leaking the endpoint; retain an unresolved create-attempt record until absence is proven.bin/fm-spawn.sh:2625- The required criterion is “Preserve existing firstmate lifecycle.” Lease-modetreehouse getis invoked without arming cleanup or a durable unresolved record first. If the provider acquires a lease/worktree and then exits nonzero or returns no path, abort cannot return the worktree or retry cleanup.bin/backends/zellij.sh:596- The required criterion is “Preserve existing firstmate lifecycle.” The new tri-state probe returns an inspection-error state, but the generic kill path still collapses that state to successful absence here.fm-teardown.sh:2607then removes task metadata, so a transientzellij list-sessionsfailure can leave the exact tab live with no retry record.🔧 Fix: Restore BSD-safe writers and reject unsafe duplicate cleanup fields
2 errors still open:
bin/fm-exec.sh:362- The required criterion is “Crabbox owns task-scoped command execution only,” butlogsvalidates only an opaque string before invokingcrabbox logs; it never proves that the ID belongs to a run recorded for this task. This permits retrieving another provider run’s logs. Reject unbound IDs or persist a verified task-to-run binding.bin/fm-control-lib.sh:201- The new Docker OpenCode hook cleanup invokesrm -f --, which is not portable to the supported macOS BSD tools. Docker OpenCode teardown and abort paths can therefore fail while leaving the hook and durable cleanup state behind. Use a BSD-safe removal form.🔧 Fix: Make Docker OpenCode hook cleanup BSD-safe and option-safe
4 issues (3 errors, 1 warning) still open:
bin/fm-teardown.sh:485- The intent requires “releases only exact recorded placements on cleanup” and “reuses exact recorded placements on relaunch.” This validates the handle/provider identity but never checks its embedded task ID against the current task; task A metadata containing task B’s valid handle can release or relaunch B’s sandbox. Bind inspect/release to the caller task ID and fail closed.bin/placements/docker-sandbox.sh:338- The handle contains an immutable provider ID, but launch invokessbx execby deterministic name only. If that sandbox is replaced after inspection, relaunch can execute in the replacement; clone cwd discovery at line 312 has the same gap. Use the recorded provider identity for every provider execution.bin/fm-spawn.sh:1036- “Preserve existing firstmate lifecycle” requires a durable retry record when cleanup fails. Failure to write the cleanup record is only warned about, after which abort cleanup proceeds; if resource cleanup also fails, later journal-write failures are ignored and the exact live resource has no standard retry record. Fail closed before destructive cleanup or retain an alternative durable journal.bin/fm-test-run.sh:200- The newtests/fm-transactional-writers.test.shis absent from the backend-dispatch family. A changed-only run triggered solely bybin/backends/{tmux,zellij,cmux,herdr}.shtherefore omits this fake-tool regression; add the test to that family or map the source paths directly.🔧 Fix: Map transactional writer coverage to backend dispatch
2 issues (1 error, 1 warning) still open:
bin/backends/tmux.sh:122- Exact cleanup treats an empty provider label/name as verified absence. If a live endpoint with the recorded ID is listed with an empty label, tmux (line 122) and the analogous zellij/herdr paths return success, so abort or teardown clears the durable cleanup record while the endpoint remains live. Only zero matching IDs should prove absence; an empty or malformed matching entry must retain cleanup for retry.bin/fm-exec.sh:283- The required intent includes “provider snapshot validation,” butjq -e 'type == "object"'accepts multiple top-level JSON documents (including a scalar followed by an object) and persists them as a validated snapshot. Require exactly one JSON document whose value is an object before committing the snapshot.🔧 Fix: Harden exact endpoint and snapshot validation
3 issues (2 errors, 1 warning) still open:
bin/fm-spawn.sh:799- The required criterion is “Preserve existing firstmate lifecycle.” When Herdr creates a tab and pane but its full acquisition-record write fails, it emits the exact two-token fallback<tab_id> <pane_id>atbin/backends/herdr.sh:2040-2042. The guard at this line rejects any result containing a space before the backend-specific parser runs, so exact endpoint cleanup is never armed and the created tab can leak.bin/backends/zellij.sh:426- The required criterion is “Preserve existing firstmate lifecycle.” The post-close absence checks here and atbin/backends/herdr.sh:3138runjq -eover raw provider output without requiring one JSON document. A stale document containing the recorded tab followed by an empty document makes the final predicate false, so cleanup reports success while the tab remains live and its durable endpoint record may be cleared. Require exactly one complete response and zero matching IDs before declaring absence.bin/placements/docker-sandbox.sh:32- The required criteria are “reuses exact recorded placements on relaunch” and “releases only exact recorded placements on cleanup.” The Docker inventory normalizer usesjq -e -cwithout enforcing a single complete JSON document, so concatenated valid arrays are accepted and consumed as authoritative identity/absence data instead of failing closed. Reject multi-document provider listings before normalizing or selecting IDs.🔧 Fix: Harden provider inventory and partial endpoint cleanup
1 error still open:
bin/placements/docker-sandbox.sh:289- The required criterion says “releases only exact recorded placements on cleanup,” butsbx createcan create a sandbox and return nonzero at this hunk. Both create branches return without reconcilingbefore_idsagainst a post-create inventory;fm-spawn.shpersists onlyplacement_pending_nameand the watcher merely reports it, leaving the live provider resource without an exact handle or supported retry release. Reconcile observable post-create identities and retain an exact or durable unresolved ownership record across failures.🔧 Fix: Reconcile partial Docker Sandbox creation failures
2 errors still open:
bin/placements/docker-sandbox.sh:252- The required criterion is “releases only exact recorded placements on cleanup.” If another actor creates the same deterministic sandbox name after the pre-create snapshot, a failedsbx createleaves exactly one new ID; lines 252-277 adopt it solely because its ID is new and its name matches, so cleanup can release the other actor’s sandbox. Require provider-level ownership proof or retain unresolved state.bin/fm-sandbox-bridge-lib.sh:521- The required criteria are “releases only exact recorded placements on cleanup” and “Preserve existing firstmate lifecycle.” If bridge removal partially deletes files beforerm -rffails at line 531, the retry re-enters full validation at line 521, which requires all bridge inputs and can never remove the remaining bridge/cursor. Make bridge cleanup phase-aware and retryable using the exact remaining identity.🔧 Fix: Fail closed Docker ownership and retry bridge cleanup
1 error still open:
bin/backends/cmux.sh:432-fm_backend_cmux_create_tasktreats a one-ID before/after title diff as the exact workspace created by this call. If cmux creates A, A disappears before the post-list, and another actor creates same-title B, this code records B and later exact cleanup can close the foreign workspace. This violates the required “Preserve existing firstmate lifecycle” contract; use the provider’s creation ID or retain an unresolved record whenever ownership is not provable.🔧 Fix: Require provider IDs for cmux workspace ownership
1 warning still open:
bin/backends/cmux.sh:440- The required criterion is “Preserve existing firstmate lifecycle,” but this unresolved cmux acquisition record is written directly to the live handoff path. An interrupted write or path replacement can leave a truncated or redirected record before abort reconciliation consumes it; publish it via the existing private temp-file, chmod, and atomic-rename path.🔧 Fix: Publish unresolved cmux records atomically
1 error still open:
bin/backends/cmux.sh:487- The atomic writer still serializes an unvalidatedsurface_id. A malformed or multi-documentlist-panesresponse can inject newlines/duplicate fields, replace the valid workspace-only record, and prevent endpoint registration, leaving the created cmux workspace without an exact cleanup handle. Validate one complete response and one control-free provider ID before publication.🔧 Fix: Validate cmux surface identities before record publication
2 errors still open:
bin/fm-config-inherit-lib.sh:69- The new inherited allowlist includesworkspace-execution.json, but fresh local secondmate spawning explicitly propagates onlytrace-context(bin/fm-spawn.sh:2521-2522). Because seeded homes start with an emptyconfig/, a secondmate never receives the primary’s placement and command-execution defaults at launch; its future workers fall back to host/direct/local. This contradicts the required persistent-secondmate Docker placement and inherited configuration behavior.bin/backends/cmux.sh:371- When a recorded cmux workspace ID is stale, recovery selects the first workspace with the expected title viahead -1without requiring a unique match or validating the provider ID. Since cmux permits duplicate titles, a foreign same-title workspace can receive sends/captures and normal teardown can close it, violating lifecycle preservation. Require exactly one validated match or fail closed.🔧 Fix: Harden secondmate inheritance and cmux stale recovery
2 errors still open:
bin/backends/cmux.sh:1036- Published teardown dispatches cmux to this best-effort kill path, which swallows close failures and never verifies post-close absence. If the task is the only workspace and sibling creation fails (or close no-ops), teardown removes task metadata while the cmux workspace remains live with no retryable identity. Use the exact post-close-verifying cleanup boundary.bin/fm-spawn.sh:2522- Secondmate inheritance failures are reduced to warnings and launch continues. If the selected primary workspace-execution config cannot be copied or stale destination config cannot be removed, future workers silently fall back to host/direct/local instead of inheriting the primary defaults. Fail before publication or retain a retryable pending state.🔧 Fix: Harden cmux teardown and secondmate config publication
1 error still open:
bin/backends/cmux.sh:1068- Published teardown parses the recorded cmux target and treats a missing workspace ID as successful absence. cmux workspace IDs do not survive app relaunch, so a restored task with the same scoped title is left live whilefm-teardowndeletes its metadata, contradicting “Preserve existing firstmate lifecycle.” Recover the unique scoped workspace across all windows before accepting absence.🔧 Fix: Recover cmux stale IDs through exact all-window cleanup
2 errors still open:
bin/fm-sandbox-bridge-lib.sh:420- The intent requires “Preserve existing firstmate lifecycle” and bridge cleanup to remain retryable after partial deletion. Prepublication abort callsfm_sandbox_bridge_remove_acquireddirectly without first creating the phase-aware removal journal. If deletion partially succeeds and then fails, only the generic spawn-cleanup record remains; later retry revalidates already-consumed bridge inputs and cannot finish cleanup.bin/backends/cmux.sh:791- The intent requires preserving “all existing worker-runtime backends” and reliable stale-ID recovery.fm_backend_cmux_target_readyuses the current-window workspace inventory, so after restart or window changes it refuses sends/captures for a uniquely titled task located in another cmux window, even though the all-window inventory path can prove its identity. Use the same unique all-window recovery boundary or fail with a durable retry state.🔧 Fix: Make bridge aborts retryable and cmux recovery all-window
2 errors still open:
bin/fm-spawn.sh:2073- The secondmate inheritance call still converts configuration or shared-preference copy failures into a warning and continues. The later check only retries workspace config after endpoint acquisition and never rechecks shared preferences, so a failed inheritance can still publish a secondmate with stale or missing inherited state. Make the complete inheritance transaction a pre-publication gate or retain durable pending state.bin/backends/herdr.sh:1998- Herdr acquisition records serialize provider-derived tab, pane, session, and workspace values directly into a line-oriented cleanup handoff without requiring single control-free fields. A malformed provider response can corrupt the record; endpoint registration then rejects both the record and fallback result, leaving an already-created tab without exact abort cleanup. Validate provider identities before publishing and bind the consumed record to the requested backend/task.🔧 Fix: Validate Herdr acquisition identities and task binding
3 issues (2 errors, 1 warning) still open:
bin/backends/cmux.sh:719- The required criterion is “Preserve existing firstmate lifecycle.” Ifcmux new-workspacecreates a workspace but then returns nonzero, this branch discards the response and emits no exact or unresolved acquisition record. Spawn cleanup then has no handle to return or retry, leaking the workspace.bin/backends/herdr.sh:1859- The required criterion is “Preserve existing firstmate lifecycle.” Herdr endpoint disappearance checks parse rawpane getoutput without requiring exactly one complete response. A live response followed bypane_not_foundcan be classified as absent, allowing teardown to remove metadata while the pane remains live.bin/backends/cmux.sh:779- The required criterion is “reuses exact recorded placements on relaunch.”cmuxsurface readiness still trusts rawlist-panesJSON without single-document or structural validation. A malformed or multi-document response can make a stale/ambiguous surface appear ready and receive sends or captures.🔧 Fix: Harden cmux and Herdr provider lifecycle validation
3 issues (2 errors, 1 warning) still open:
bin/backends/zellij.sh:404- Exact Zellij cleanup validates only the top-level array. A malformed tab member or wrong-typed ID is filtered out, yielding zero matches and falsely proving the tab absent; abort cleanup can then forget a live tab. Validate all members and fail closed.bin/backends/herdr.sh:3257- Exact Herdr cleanup checks only thatresult.tabsis an array and does not validate each member. A malformed member lackingtab_idyields zero matches, so abort cleanup can forget a live tab despite an incomplete inventory. Retain cleanup on malformed or ambiguous output.bin/backends/cmux.sh:850- Stale-ID recovery updates only process-local cmux globals; task metadata remains stale. The next invocation must rediscover the target and loses durable recovery. Persist recovered IDs before sending or capturing, or fail closed.🔧 Fix: Harden provider inventories and persist cmux recovery
1 error still open:
bin/backends/cmux.sh:810- The intent requires “preserving current host/direct/local defaults and all existing worker-runtime backends.”fm_backend_cmux_surface_id_for_workspaceaccepts a pane response containing onlyselected_surface_id, butfm_backend_cmux_surface_existsrejects that same response because it requires a nonemptysurface_idsarray. Creation or recovery can record the selected ID, then every later target-ready, send, or capture check fails. Normalize both paths consistently or reject the shape during acquisition.🔧 Fix: Unify cmux pane normalization across lifecycle paths
1 warning still open:
bin/fm-bootstrap.sh:1095- The required criterion says “missing optional tools stay silent unless selected.” The new bootstrap path uses GNU-stylerm -f --here and again at line 1100; stock macOS BSDrmrejects that separator, so config detection emits a cleanup error and leaves its temporary file even when no optional tool is selected. Use a BSD-safe exact-path removal form.🔧 Fix: Make bootstrap temp cleanup BSD-safe
✅ Re-checked - no issues remain.
tests/fm-spawn-worktree-settle.test.sh:175- The settle timing assertion exceeded its threshold under batch load twice but passed two serial reruns; this appears scheduling-sensitive rather than a product failure.tests/fm-bootstrap.test.sh:1008- Bootstrap clone-timing attribution failed once in the parallel batch but passed serially; no product failure reproduced.bash tests/fm-workspace-placement.test.shbash tests/fm-workspace-execution-config.test.shbash tests/fm-command-execution.test.shbash tests/fm-sandbox-bridge-lib.test.shbash tests/fm-watch-sandbox-bridge.test.shbash tests/fm-spawn-docker-transaction.test.shbash tests/fm-secondmate-lifecycle-e2e.test.shbash tests/fm-secondmate-harness.test.shbash tests/fm-test-run.test.shbash tests/fm-transactional-writers.test.shbash tests/fm-backend.test.shbash tests/fm-backend-cmux.test.shbash tests/fm-backend-herdr.test.shbash tests/fm-backend-zellij.test.shbash tests/fm-backend-tmux-smoke.test.shbash tests/fm-spawn-worktree-settle.test.sh(serial confirmation)bash tests/fm-control-relaunch.test.sh(serial confirmation)bash tests/fm-gate-refuse.test.shbash tests/fm-bootstrap.test.sh(serial confirmation)Captured sandbox lifecycle and Crabbox transcripts withtee.git status --short --untracked-files=allbin/fm-workspace-execution-config.sh:92- Integrated config and spawn reject every workspace mode except direct, so required Docker Sandbox clone support remains unimplemented outside this document-only phase.bin/fm-spawn.sh:1589- Crabbox selection reaches unconditional preparation, which always fails; normal spawns therefore cannot use the documented opt-in executor.🔧 Fix: Updated authoritative inheritance and metadata documentation
2 errors still open:
bin/fm-workspace-execution-config.sh:92- Integrated configuration and spawn validation still reject every workspace mode except direct, so required Docker Sandbox clone-mode support for ordinary and secondmate spawns remains unimplemented outside this document-only phase.bin/fm-spawn.sh:1589- Every fresh spawn unconditionally calls Crabbox preparation, which intentionally fails because no recordable prewarm lease exists, so opt-in Crabbox spawns cannot publish a worker or secondmate.🔧 **Lint** - 1 issue found → auto-fixed ✅
🔧 Fix: Fix ShellCheck findings across backend and test scripts
✅ Re-checked - no issues remain.
✅ **Push** - passed
✅ No issues found.