Skip to content

ci(docs): verify docs links in PRs and on the deployed site#173

Merged
donbeave merged 11 commits into
mainfrom
codex/docs-link-checks
Apr 25, 2026
Merged

ci(docs): verify docs links in PRs and on the deployed site#173
donbeave merged 11 commits into
mainfrom
codex/docs-link-checks

Conversation

@donbeave

@donbeave donbeave commented Apr 25, 2026

Copy link
Copy Markdown
Member

Summary

  • Fix the Starlight edit-link base URL so generated Edit page links map to real source files.
  • Add shared lychee configuration and local bun run check:links / bun run check:links:fresh docs commands.
  • Run docs link checks on every PR, not only PRs touching docs/**, so code renames can break docs links before merge.
  • Validate generated site URLs against docs/dist with lychee --remap, including same-origin canonical/OG/page links, GitHub edit links, and GitHub blob/main file links.
  • Add lychee caching for local and CI runs, with an Actions cache for broad PR coverage.
  • Run post-deploy and scheduled deployed-site lychee checks against the live sitemap.
  • Allow workflow_dispatch of the deployed-site check from non-main branches, so a maintainer can manually verify the live docs without redeploying.
  • Add a reusable <RepoFile path="..." /> MDX component for repository file links (opens in a new tab to match other external links).
  • Add a source lint that rejects raw internal GitHub blob URLs, internal tree/main links, and plain inline-code references to existing repo files (covers src/, docs/, docker/, .github/, plus top-level repo files like Cargo.toml, Justfile, docker-bake.hcl, mise.toml, release.toml).
  • Convert roadmap and construct docs file references to <RepoFile> so lychee verifies them against the PR checkout.
  • Repair existing broken docs links caught by generated/deployed output, including the new multi-runtime roadmap proposal merged from main.
  • Split the workflow concurrency group by event so a scheduled or manual run cannot cancel an in-flight push deploy.

Root Cause

Starlight prepends editLink.baseUrl to the current content source path. The previous base URL already included docs/src/content/docs/, so generated edit links duplicated that segment and pointed at paths like docs/src/content/docs/src/content/docs/....

The broader link-checking gap was that PR checks only ran for docs changes and several generated URLs were checked against current production or current main, not the PR checkout. That could miss a link that would break immediately after merge.

Follow-up

The companion ruleset change in jackin-project/jackin-github-terraform#10 makes Docs / build a required status check on main. Apply that ruleset after this PR merges so the link gate becomes enforced at the protection layer (currently the docs check is advisory — a red Docs / build does not block merge).

Validation

  • PATH="/opt/homebrew/bin:$PATH" bun run check:links:fresh
  • bun run check:repo-links
  • bun test in docs/
  • ruby -e 'require "yaml"; YAML.load_file(".github/workflows/docs.yml"); puts "ok"'
  • git diff --check
  • Manual lychee negative checks for missing same-origin and GitHub blob targets.
  • Earlier in this PR: bun install --frozen-lockfile, cargo fmt -- --check, cargo clippy -- -D warnings, cargo nextest run

Note: bunx tsc --noEmit still fails on existing Astro/Starlight virtual-module type resolution (astro:content, Starlight JSONC theme imports), so it was not used as a passing gate for this patch.

donbeave and others added 6 commits April 25, 2026 21:05
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
@donbeave donbeave changed the title ci(docs): add link checking ci(docs): verify docs links in PRs Apr 25, 2026
donbeave and others added 2 commits April 25, 2026 18:45
Two refinements from PR review:

- The check-deployed job now triggers on workflow_dispatch in addition
  to schedule, so maintainers can manually verify the live deployed
  docs without waiting for the daily cron or pushing to main. This
  closes the gap against goal "manual workflow to verify both built
  site and deployed documentation".

- Drop github.sha from the deploy job's lychee cache primary key so it
  matches across runs (the SHA-keyed primary was guaranteed to miss,
  forcing fallback to restore-keys). Now mirrors the cache key shape
  used by check-deployed.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Address review feedback on PR #173.

Workflow (.github/workflows/docs.yml):
- Split concurrency group by event_name so a scheduled or
  workflow_dispatch run cannot cancel an in-flight push deploy. Cancel
  in-progress is now scoped to pull_request only.
- Exclude main from workflow_dispatch on check-deployed. The deploy job
  already verifies the just-deployed site, so running both in parallel
  would race against the publish window. Manual verification of the live
  site from main flows through deploy; from feature branches it flows
  through check-deployed.
- Add the build cache as a fallback restore-key for check-deployed so
  the daily cron and manual runs warm-start from the last build cache
  when the lychee.toml fingerprint changes.

RepoFile component (docs/src/components/RepoFile.astro):
- Add target=_blank and rel=noopener noreferrer so GitHub source links
  open in a new tab, matching the behavior rehype-external-links applies
  to plain markdown external links in MDX.

Source lint (docs/scripts/check-repo-links.ts):
- Normalize leading slashes when checking RepoFile path existence so the
  validator agrees with the component's own normalization.
- Cover top-level repo-specific files (Cargo.toml, Cargo.lock, Justfile,
  build.rs, docker-bake.hcl, mise.toml, release.toml, renovate.json) so
  a rename of any of those also breaks the docs CI gate, not only paths
  under src/, docs/, docker/, .github/.

Content (docs/src/content/docs/developing/construct-image.mdx):
- Convert the remaining inline-code references to docker-bake.hcl and
  Justfile to <RepoFile />. These were the references that motivated
  extending the lint to top-level files.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave and others added 3 commits April 25, 2026 19:49
After merging main into codex/docs-link-checks, check-repo-links flagged
37 plain inline-code references to existing repo files in the new
multi-runtime-support.mdx (added in #174 before this PR's lint existed
on main). Convert each one to <RepoFile /> so renames or deletions of
those source files break the docs gate before merge, the same way the
rest of the roadmap is now protected.

No prose changes — every conversion is one-for-one (`src/foo.rs` →
<RepoFile path="src/foo.rs" />). The trailing-slash directory reference
to `docker/runtime/` is left as a code span, since the lint correctly
skips it (it's a directory, not a file).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
CI failed on this PR's last build because lychee found a 404 on
https://github.com/sourcegraph/amp in multi-runtime-support.mdx (added
in #174). That repo does not exist publicly — Amp's source is not on
GitHub.

Point the link at https://ampcode.com instead, which is already the
canonical Amp URL used elsewhere in the docs (getting-started/why.mdx).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
@donbeave donbeave marked this pull request as ready for review April 25, 2026 20:04
@donbeave donbeave changed the title ci(docs): verify docs links in PRs ci(docs): verify docs links in PRs and on the deployed site Apr 25, 2026
@donbeave donbeave merged commit f3f3e5e into main Apr 25, 2026
6 checks passed
@donbeave donbeave deleted the codex/docs-link-checks branch April 25, 2026 20:12
donbeave added a commit that referenced this pull request Apr 25, 2026
The first Docs workflow run on main after #173 (commit f3f3e5e) failed
in deploy → "Check deployed docs links" with "No files found for this
input source". Root cause: the previous step ran lychee --dump on the
deployed sitemap URL, but lychee 0.23.0 (the lycheeverse/lychee-action
v2 default) only extracts <a href> from HTML and matching patterns from
markdown — it does not parse <loc> entries from XML sitemaps. The dump
produced an empty list and the follow-up --files-from step had nothing
to read.

Upstream already fixed this. lycheeverse/lychee#2071 (merged
2026-03-13, tagged in v0.24.0 on 2026-04-24) adds <loc> extraction
from sitemap.xml, closing lycheeverse/lychee#2062 and #1819. Verified
locally on 0.24.0:

  $ lychee --version
  lychee 0.24.0
  $ lychee --dump https://jackin.tailrocks.com/sitemap-0.xml | wc -l
  45

Pin LYCHEE_VERSION at the workflow env level and reference it from
every lychee-action call so future bumps are one-line. v0.24.0's
breaking changes are in lychee-lib (the Rust API consumers); the CLI
surface we use is unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 25, 2026
)

* ci(docs): bump lychee to v0.24.0 to fix sitemap URL extraction

The first Docs workflow run on main after #173 (commit f3f3e5e) failed
in deploy → "Check deployed docs links" with "No files found for this
input source". Root cause: the previous step ran lychee --dump on the
deployed sitemap URL, but lychee 0.23.0 (the lycheeverse/lychee-action
v2 default) only extracts <a href> from HTML and matching patterns from
markdown — it does not parse <loc> entries from XML sitemaps. The dump
produced an empty list and the follow-up --files-from step had nothing
to read.

Upstream already fixed this. lycheeverse/lychee#2071 (merged
2026-03-13, tagged in v0.24.0 on 2026-04-24) adds <loc> extraction
from sitemap.xml, closing lycheeverse/lychee#2062 and #1819. Verified
locally on 0.24.0:

  $ lychee --version
  lychee 0.24.0
  $ lychee --dump https://jackin.tailrocks.com/sitemap-0.xml | wc -l
  45

Pin LYCHEE_VERSION at the workflow env level and reference it from
every lychee-action call so future bumps are one-line. v0.24.0's
breaking changes are in lychee-lib (the Rust API consumers); the CLI
surface we use is unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): bump lychee-action and lychee for sitemap URL extraction

Replace the previous v0.24.0 bump with the only combination that
actually works against the current lychee release pipeline:

  - lycheeverse/lychee-action SHA 8646ba3 (tagged v2.8.0) → faea714
    (post-v2.8.0 master). Adds subfolder-aware install needed for any
    lychee 0.24.x tarball.
  - LYCHEE_VERSION 'v0.24.0' → 'v0.24.1'.

Why both moves:

* lychee 0.24.0 added <loc> extraction from XML sitemaps
  (lycheeverse/lychee#2071), which is what the deploy and check-deployed
  jobs need to feed --files-from. lychee 0.23.0 dumps zero links from a
  sitemap, which is what produced the "No files found for this input
  source" failure on f3f3e5e.
* lychee 0.24.0's release tarball was repackaged with a top-level
  subfolder AND the asset filename was renamed to
  lychee-lychee-v0.24.0-{arch}-... — both incompatible with
  lychee-action v2.8.0's hardcoded download URL and flat-extract logic.
* lychee 0.24.1 (released the same day) reverted to the original asset
  filename but kept the subfolder layout AND kept the sitemap fix.
* lychee-action faea714 (unreleased; current HEAD of master) bumps the
  default to 0.24.1 and adds subfolder-aware install. Pinning the SHA
  is the same security model we already use for v2.8.0.

The combination 8646ba3 + 'latest' or 8646ba3 + 'v0.24.x' both fail.
The combination faea714 + 'v0.24.1' works.

Verified locally:

  $ lychee-v0.24.1/lychee --version
  lychee 0.24.1
  $ lychee-v0.24.1/lychee --dump https://jackin.tailrocks.com/sitemap-0.xml | wc -l
  45

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): add TODO(lychee-action-sha-pin) marker

Companion to #179, which establishes the convention. Mark the spot
where the SHA pin needs to be reverted once lycheeverse/lychee-action
cuts a tagged release at or after faea714, with a back-link to the
tracked entry in TODO.md so a single grep finds both ends.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
* ci(docs): add link checking

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): stabilize lychee checks

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): validate edit links with lychee

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): close link check gaps

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs: add repo file link component

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs: explain repo link source check

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): allow manual dispatch of deployed link check

Two refinements from PR review:

- The check-deployed job now triggers on workflow_dispatch in addition
  to schedule, so maintainers can manually verify the live deployed
  docs without waiting for the daily cron or pushing to main. This
  closes the gap against goal "manual workflow to verify both built
  site and deployed documentation".

- Drop github.sha from the deploy job's lychee cache primary key so it
  matches across runs (the SHA-keyed primary was guaranteed to miss,
  forcing fallback to restore-keys). Now mirrors the cache key shape
  used by check-deployed.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): harden link-check workflow and broaden source lint

Address review feedback on PR #173.

Workflow (.github/workflows/docs.yml):
- Split concurrency group by event_name so a scheduled or
  workflow_dispatch run cannot cancel an in-flight push deploy. Cancel
  in-progress is now scoped to pull_request only.
- Exclude main from workflow_dispatch on check-deployed. The deploy job
  already verifies the just-deployed site, so running both in parallel
  would race against the publish window. Manual verification of the live
  site from main flows through deploy; from feature branches it flows
  through check-deployed.
- Add the build cache as a fallback restore-key for check-deployed so
  the daily cron and manual runs warm-start from the last build cache
  when the lychee.toml fingerprint changes.

RepoFile component (docs/src/components/RepoFile.astro):
- Add target=_blank and rel=noopener noreferrer so GitHub source links
  open in a new tab, matching the behavior rehype-external-links applies
  to plain markdown external links in MDX.

Source lint (docs/scripts/check-repo-links.ts):
- Normalize leading slashes when checking RepoFile path existence so the
  validator agrees with the component's own normalization.
- Cover top-level repo-specific files (Cargo.toml, Cargo.lock, Justfile,
  build.rs, docker-bake.hcl, mise.toml, release.toml, renovate.json) so
  a rename of any of those also breaks the docs CI gate, not only paths
  under src/, docs/, docker/, .github/.

Content (docs/src/content/docs/developing/construct-image.mdx):
- Convert the remaining inline-code references to docker-bake.hcl and
  Justfile to <RepoFile />. These were the references that motivated
  extending the lint to top-level files.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): apply RepoFile lint to multi-runtime proposal

After merging main into codex/docs-link-checks, check-repo-links flagged
37 plain inline-code references to existing repo files in the new
multi-runtime-support.mdx (added in #174 before this PR's lint existed
on main). Convert each one to <RepoFile /> so renames or deletions of
those source files break the docs gate before merge, the same way the
rest of the roadmap is now protected.

No prose changes — every conversion is one-for-one (`src/foo.rs` →
<RepoFile path="src/foo.rs" />). The trailing-slash directory reference
to `docker/runtime/` is left as a code span, since the lint correctly
skips it (it's a directory, not a file).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(roadmap): repair broken Amp CLI link in multi-runtime proposal

CI failed on this PR's last build because lychee found a 404 on
https://github.com/sourcegraph/amp in multi-runtime-support.mdx (added
in #174). That repo does not exist publicly — Amp's source is not on
GitHub.

Point the link at https://ampcode.com instead, which is already the
canonical Amp URL used elsewhere in the docs (getting-started/why.mdx).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
(cherry picked from commit f3f3e5e)
donbeave added a commit that referenced this pull request Apr 26, 2026
…n page

The check-repo-links script (added in #173) flags any inline-code
reference to a real repo file that isn't wrapped in <RepoFile />.
src/cli/cd.rs was created on this branch, so once #173's lint reaches
this branch via the previous cherry-pick, the bare reference fails
the Docs CI check.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
Resolves 9 conflicts to integrate PR #173 (link-check infrastructure),
PR #171 (workspace manager TUI rewrite), and minor doc updates with the
per-mount-isolation work.

Conflict resolutions of note:
- src/console/state.rs, src/console/preview.rs: take main's slimmer
  versions — main's #171 moved the agent-preview rendering into the new
  manager TUI components, where the iso badge work from this branch
  already lives (render/list.rs, render/editor.rs).
- src/console/manager/state.rs: combine main's `DeleteEnvVar` +
  `SecretsScopeTag` with this branch's `DeleteIsolatedAndSave` /
  source-drift Confirm modal extension.
- src/console/manager/input/editor.rs: route through main's
  `apply_editor_confirm` helper for simple variants while handling
  `DeleteIsolatedAndSave` inline (consumes `plan` and re-stashes as
  `EditorSaveFlow::PendingCommit`).
- docs/.github/workflows/docs.yml: take main's verbatim — main has
  post-#173 refinements (lychee-action SHA pin, cache@v5 bump).
- docs/.../per-mount-isolation.mdx: keep this branch's `<RepoFile>`
  conversion of `src/cli/cd.rs`.

Drops one orphan test (`preselects_saved_workspace_for_nested_directory_under_mount_root`)
that #171 removed from console/state.rs — the same coverage exists in
workspace/resolve.rs:215 (`saved_workspace_match_depth_still_matches_nested_path_under_mount_root`).

Adds `isolation: MountIsolation::Shared` to MountConfig literals that
arrived via main's tests (state.rs, render/list.rs, manager_flow.rs ×6).

All checks pass locally:
- cargo fmt --check
- cargo clippy -- -D warnings
- cargo nextest run (1153/1153)
- bun run check:repo-links (in docs/)
- bun run build (in docs/)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
The link-check job's lychee step (added on main in #173, hardened in
#176) verifies built-site links against the on-disk dist tree. Relative
`.mdx`-suffixed links break that check because lychee resolves them as
literal file paths under the rendered URL's directory — e.g.
`./workspaces.mdx` rendered from `/guides/mounts/` resolves to
`/guides/mounts/workspaces.mdx`, not `/guides/workspaces/`.

Switch the four cross-doc links added by the per-mount-isolation work
to the rendered-URL form (`/guides/workspaces/#per-mount-isolation`
etc.) — same convention as the existing `[mount collapse](/commands/workspace/#mount-collapse)`
link in the same file.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
* docs(spec): per-mount isolation V1 implementation spec

Captures roadmap → executable design: module layout under
src/isolation/, MountIsolation enum + MountConfig.isolation field,
materialization runtime hook, foreground finalizer with
safe/preserved/force cleanup, source-drift detection, jackin cd
command, TUI integration. Test list and docs touchpoints enumerated
for the implementation plan.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): introduce MountIsolation enum

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(workspace): add isolation field to MountConfig

Defaults to Shared; serde skips emitting the field when Shared so
existing TOMLs round-trip unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(workspace): reject nested isolated mounts

Two worktree-isolated mounts whose dsts nest have no safe on-disk
layout. Sibling isolated mounts and isolated-parent-with-shared-child
remain allowed.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(config): reject isolation field on global mounts

Adds a strict GlobalMountConfig wire-format struct that mirrors
MountConfig minus the isolation field, with deny_unknown_fields
so operators get a clear parse error if they try to set isolation
on a global mount. Isolation remains a workspace-mount concept.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(config): drop dead code and tighten global-mount API

- Delete unused From<MountConfig> for GlobalMountConfig (silently
  dropped isolation; no callers).
- Delete unused get_mut and remove on DockerMounts along with their
  #[allow(dead_code)] annotations.
- Tighten AppConfig::add_mount: debug_assert that incoming
  MountConfig has Shared isolation, and construct GlobalMountConfig
  explicitly from src/dst/readonly rather than via the (now-deleted)
  From impl. Keeps the public signature stable so callers in CLI,
  resolve.rs, and preview.rs don't need to change (Issue 2 option B).
- Add wire-path rejection test that goes through MountEntry's
  untagged enum and asserts on the actual serde error
  ("data did not match any variant of untagged enum MountEntry").
- Soften GlobalMountConfig's doc comment to reflect the actual
  serde error shape at the wire path.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): IsolationRecord + isolation.json IO

Atomic write via tmp+rename. Version-1 envelope leaves room for schema
evolution. Read/upsert/remove keyed by mount destination.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* test(isolation): drop redundant clones in state tests

Bring the warning baseline back to 87 after Task 2.1 introduced
two clippy::style hits (cloned_ref_to_slice_refs, redundant_clone).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): list_records_for_workspace walks data dir

Used by workspace-edit drift detection to find which containers have
preserved isolated state for a given workspace.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): branch_name renderer with namespace + suffix support

Suffix is appended to the final selector segment so namespaced agents
keep their selector shape and the disambiguator goes on the leaf name.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): MaterializedWorkspace types

Third workspace shape (Config -> Resolved -> Materialized) used as the
runtime handoff into Docker launch.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): worktree_path_for derives on-disk path from mount dst

Uses dst verbatim (leading/trailing slashes stripped) under
isolated/, so the layout mirrors the container path.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): ensure_worktree_config_enabled

One-shot enabler for extensions.worktreeConfig on the host repo.
Bumps core.repositoryformatversion to 1 when needed.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): preflight checks for worktree materialization

Sensitive-mount, readonly, repo-root, and mid-operation guards.
Errors cite the mount destination and the worktree mode.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): dirty-host preflight gate with --force opt-out

Non-interactive load without --force rejects a dirty host tree.
Interactive contexts are expected to obtain ack upstream.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): materialize_workspace orchestrator

Per-mount worktree materialization with idempotent reuse, source-drift
guard, and branch-name disambiguation when multiple isolated mounts
target the same host repo.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* test(isolation): cover branch disambiguation for same-repo mounts

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): order Docker mounts parent-before-child

Length-ascending sort so shared cache children overlay isolated
worktree parents at container start.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(runtime): hook materialize_workspace between AgentState and Docker

Workspace mounts now flow Config -> Resolved -> Materialized before
reaching the docker run command, with parent-before-child ordering.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): force_cleanup_isolated removes worktree + branch + record

Best-effort git invocations that tolerate missing host repo and
already-removed worktree. Used by purge and the finalizer's force-delete
branch.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): finalizer skeleton + AttachOutcome shape

Decides Preserved when container still running, OOMed, or exited
non-zero. Clean-exit path stubbed - implemented in follow-ups.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): safe-cleanup deletes branches with no commits

When the worktree is clean and HEAD equals the recorded base, the
scratch branch is removed automatically.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): consult upstream when deciding safe cleanup

Pushed commits (reachable from upstream) are safe to delete; local-only
commits or no-upstream divergence preserve the worktree.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* test(isolation): cover interactive unsafe-cleanup prompt branches

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(runtime): finalize foreground session after attach in load + hardline

Both load and hardline now consult inspect_attach_outcome and dispatch
the shared finalizer. Return-to-agent retries safe cleanup once after
the operator returns.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(purge): refuse to run on a live container

Closes a pre-existing gap where purge could delete state out from
under a running agent. Operator must eject first.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(purge): remove isolated worktrees and scratch branches

Reads isolation.json and runs force_cleanup_isolated for each record
before deleting the per-container state directory.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(cli): --mount-isolation DST=TYPE on workspace create/edit

Repeatable. Rejects clone before persistence with the canonical
"reserved but not implemented yet" message.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(workspace): add Isolation column to workspace show

Renders canonical lowercase name for every mount so CLI output matches
TOML/CLI input verbatim.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(load): add --force to acknowledge dirty host tree

Required for non-interactive isolated-mount materialization when the
host working tree is dirty.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(workspace): detect source drift on edit affecting isolated mounts

Edits that change src for a mount with preserved isolated state are
rejected unless --delete-isolated-state is passed and no related
container is running.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(cli): jackin cd opens a child shell in an isolated worktree

Single-mount-no-dst → uses it. Dst-provided → exact match. Multi-mount
no-dst → interactive picker on TTY, error on non-TTY. Sets JACKIN_*
env vars. Does not modify the parent shell.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(console): show isolation badge per mount in editor + preview

Adds an Iso column to the workspace-manager mount table (editor and
list-pane sub-panel) and a `[shared|worktree|clone]` tag to the agent
preview's resolved-mounts lines. Per the per-mount-isolation spec the
badge renders the canonical spelling for every mount, including
`shared`, so operators always see which strategy applies.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(console): I hotkey cycles isolation on the selected mount

Mirrors the existing R (readonly) toggle. Cycles Shared -> Worktree
-> Shared; Clone is reserved-but-rejected in V1 and is not entered
through this hotkey, but a saved Clone mount snaps back to Shared on
the first I press rather than getting stuck. The cycling rule lives in
EditorState::cycle_isolation_for_selected_mount so the input dispatch
arm stays trivial.

Also surfaces the new key in the Mounts-tab footer hint alongside the
existing R toggle so the affordance is discoverable.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(console): source-drift confirm modal in workspace editor

Save flow runs the same drift detection as `jackin workspace edit`:
detect_workspace_edit_drift evaluates the prospective mount list
(post-collapse, post-upsert) against IsolationRecords on disk before
the on-disk write.

Running container drift -> ErrorPopup ("eject first"); save aborted.
Stopped container drift -> Confirm modal listing the affected
container names with a Yes/No prompt. On Yes the modal handler
re-stashes the plan with delete_isolated_acknowledged = true and the
second commit pass calls force_cleanup_isolated for each affected
record before writing.

Reduced scope vs the original three-button "Delete preserved state and
save / Cancel / Open mount details" dialog: the modal is the existing
two-button Confirm widget (Yes/No). The third "open mount details"
affordance is omitted — operators dismiss with N/Esc, find the
offending mount in the editor, and revert the src by hand. Adding it
would require either a custom widget or repurposing an existing
multi-choice one and threading mount-row focus through the modal
plumbing; the safety value is in the block-and-ack semantics, which
the two-button form covers.

Adds a commit_editor_save_with_runner test seam so the FakeRunner can
drive the drift branch without a real Docker daemon.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(workspaces): add per-mount isolation section

Document the per-mount isolation feature in the workspaces guide:
the three modes (shared default, worktree, clone reserved-but-rejected),
validation preconditions, the isolated-source + shared-cache child
pattern with TOML, and pointers to --mount-isolation and jackin cd.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(mounts): document mount isolation field

Add an "Mount isolation" section to the mounts guide covering the
shared/worktree values, the global-mount rejection at parse time,
and the isolated-source + shared-cache child composition pattern.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(configuration): add MountConfig.isolation field

Document the new mounts[].isolation field in the configuration
reference: shared default, worktree opt-in, clone reserved-but-rejected,
and the global-mount parse-time rejection.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(architecture): document materialization flow + isolation.json

Add a "Workspace materialization" section to the architecture reference
covering the WorkspaceConfig -> ResolvedWorkspace -> MaterializedWorkspace
shapes, the per-container isolation.json layout, and the post-attach
foreground finalizer's Preserved/Cleaned/ReturnToAgent decision matrix.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(workspace): document --mount-isolation and Isolation column

Add --mount-isolation to workspace create/edit option tables (with the
clone "planned but not implemented" note), document the new
--delete-isolated-state flag for non-interactive source-drift edits,
and note the Isolation column on workspace show.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(load): document --force dirty-host acknowledgement

Add --force to the option table and a dedicated section explaining
when it's required (non-interactive load with a worktree-isolated
mount + dirty host tree) and what it does NOT do (no stash, no
discard, no relaxation of other validation).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(purge): document running-agent guard and isolated cleanup

Document purge's new behavior: refuses to run on a running container
(eject first), force-removes isolated worktrees + scratch branches
recorded in isolation.json, and tolerates a missing host repo on
best-effort cleanup.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(cd): add jackin cd command reference

Create a reference page for jackin cd <container> [dst] covering
arguments, the mount-selection behavior matrix (zero/one/many isolated
records, with and without dst), the JACKIN_* env vars set in the
child shell, exit-code passthrough, and the no-parent-mutation
guarantee. Wire it into the Commands sidebar between console and
launch.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): mark per-mount isolation V1 implemented

Flip the per-mount-isolation roadmap status to "Implemented in V1"
and replace the duplicate-mounts-allowed line with the actual V1
rule: multiple isolated mounts are allowed (with branch-name
disambiguation), but nested isolated dst paths are rejected at
validation because the inner worktree's .git would land inside the
outer worktree's tree.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(structure): add isolation module tree and cd command

Update PROJECT_STRUCTURE.md to document the new per-mount-isolation
work: isolation/ module row (mod/branch/materialize/state/finalize/
cleanup), cli/cd.rs entry on the cli/ row, --mount-isolation /
--delete-isolated-state / --force notes on the relevant CLI rows,
foreground-finalizer mention on the runtime row, the new
commands/cd.mdx in the docs map, and a code->docs cross-reference
row mapping src/isolation/** to all the doc pages it touches.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* test(isolation): end-to-end materialize -> clean-exit -> cleanup

Exercises the full lifecycle through public APIs with a small inline
scripted runner. No real git or docker.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(isolation): note finalizer is local-only for hardline lockdown

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* style(test): apply rustfmt to per-mount isolation e2e

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): verify docs links in PRs and on the deployed site (#173)

* ci(docs): add link checking

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): stabilize lychee checks

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): validate edit links with lychee

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): close link check gaps

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs: add repo file link component

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs: explain repo link source check

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): allow manual dispatch of deployed link check

Two refinements from PR review:

- The check-deployed job now triggers on workflow_dispatch in addition
  to schedule, so maintainers can manually verify the live deployed
  docs without waiting for the daily cron or pushing to main. This
  closes the gap against goal "manual workflow to verify both built
  site and deployed documentation".

- Drop github.sha from the deploy job's lychee cache primary key so it
  matches across runs (the SHA-keyed primary was guaranteed to miss,
  forcing fallback to restore-keys). Now mirrors the cache key shape
  used by check-deployed.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): harden link-check workflow and broaden source lint

Address review feedback on PR #173.

Workflow (.github/workflows/docs.yml):
- Split concurrency group by event_name so a scheduled or
  workflow_dispatch run cannot cancel an in-flight push deploy. Cancel
  in-progress is now scoped to pull_request only.
- Exclude main from workflow_dispatch on check-deployed. The deploy job
  already verifies the just-deployed site, so running both in parallel
  would race against the publish window. Manual verification of the live
  site from main flows through deploy; from feature branches it flows
  through check-deployed.
- Add the build cache as a fallback restore-key for check-deployed so
  the daily cron and manual runs warm-start from the last build cache
  when the lychee.toml fingerprint changes.

RepoFile component (docs/src/components/RepoFile.astro):
- Add target=_blank and rel=noopener noreferrer so GitHub source links
  open in a new tab, matching the behavior rehype-external-links applies
  to plain markdown external links in MDX.

Source lint (docs/scripts/check-repo-links.ts):
- Normalize leading slashes when checking RepoFile path existence so the
  validator agrees with the component's own normalization.
- Cover top-level repo-specific files (Cargo.toml, Cargo.lock, Justfile,
  build.rs, docker-bake.hcl, mise.toml, release.toml, renovate.json) so
  a rename of any of those also breaks the docs CI gate, not only paths
  under src/, docs/, docker/, .github/.

Content (docs/src/content/docs/developing/construct-image.mdx):
- Convert the remaining inline-code references to docker-bake.hcl and
  Justfile to <RepoFile />. These were the references that motivated
  extending the lint to top-level files.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): apply RepoFile lint to multi-runtime proposal

After merging main into codex/docs-link-checks, check-repo-links flagged
37 plain inline-code references to existing repo files in the new
multi-runtime-support.mdx (added in #174 before this PR's lint existed
on main). Convert each one to <RepoFile /> so renames or deletions of
those source files break the docs gate before merge, the same way the
rest of the roadmap is now protected.

No prose changes — every conversion is one-for-one (`src/foo.rs` →
<RepoFile path="src/foo.rs" />). The trailing-slash directory reference
to `docker/runtime/` is left as a code span, since the lint correctly
skips it (it's a directory, not a file).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(roadmap): repair broken Amp CLI link in multi-runtime proposal

CI failed on this PR's last build because lychee found a 404 on
https://github.com/sourcegraph/amp in multi-runtime-support.mdx (added
in #174). That repo does not exist publicly — Amp's source is not on
GitHub.

Point the link at https://ampcode.com instead, which is already the
canonical Amp URL used elsewhere in the docs (getting-started/why.mdx).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
(cherry picked from commit f3f3e5e2d386a9cb3c2537be1e51b60f3fc09e6e)

* docs(roadmap): wrap src/cli/cd.rs in <RepoFile> on per-mount-isolation page

The check-repo-links script (added in #173) flags any inline-code
reference to a real repo file that isn't wrapped in <RepoFile />.
src/cli/cd.rs was created on this branch, so once #173's lint reaches
this branch via the previous cherry-pick, the bare reference fails
the Docs CI check.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs: switch cross-doc links to absolute URL form

The link-check job's lychee step (added on main in #173, hardened in
#176) verifies built-site links against the on-disk dist tree. Relative
`.mdx`-suffixed links break that check because lychee resolves them as
literal file paths under the rendered URL's directory — e.g.
`./workspaces.mdx` rendered from `/guides/mounts/` resolves to
`/guides/mounts/workspaces.mdx`, not `/guides/workspaces/`.

Switch the four cross-doc links added by the per-mount-isolation work
to the rendered-URL form (`/guides/workspaces/#per-mount-isolation`
etc.) — same convention as the existing `[mount collapse](/commands/workspace/#mount-collapse)`
link in the same file.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): emit verbose debug-mode trace for worktree lifecycle

Operators sharing logs to debug worktree behavior had no visibility into
the lifecycle — only three error/warning sites fired output, and none of
them ran on the happy path. `--debug` toggled a display mode (preserve
scrollback, clear spinner) but was not a verbose-trace facility.

This adds a `debug_log!(category, fmt, ...)` macro in `src/tui/mod.rs`
that gates on the existing `DEBUG_MODE` atomic so disabled call sites
cost only an atomic load (formatting is deferred behind the gate).
Output uses a `[jackin debug <category>]` prefix so shared logs are
greppable.

Instrumented sites (all under `category = "isolation"`):

- `materialize_workspace`: per-call summary (workspace, container,
  selector, mount counts, force/interactive flags).
- `materialize_one`: per-mount decision trail — drift detection,
  worktree reuse, preflight, base-commit lookup, branch derivation
  with selected suffix, and the `git worktree add` invocation itself.
- `ensure_worktree_config_enabled`: every state transition (already
  enabled vs. bumping repositoryformatversion vs. flipping the flag)
  with the host repo path.
- `state.rs`: write_records (count + path), upsert_record (insert vs.
  replace), remove_record (drop vs. no-op).
- `cleanup.rs`: force_cleanup_isolated entry, the two git invocations,
  the rm -rf fallback, and the host-repo-missing skip path.
  purge_isolated_for_container per-container summary.
- `finalize.rs`: foreground-session entry with exit code/oom/interactive
  flags, the early-return path for non-clean exits, per-record cleanup
  assessment.
- `runtime/launch.rs`: load_agent's call into materialize_workspace.

Read paths (`read_records`, `read_record`) are intentionally NOT logged
— they fire on every invocation and would drown the log.

Manual verification (since binary-level stderr capture would need a new
test dependency):

    cargo run --release --bin jackin -- --debug load <agent>
    cargo run --release --bin jackin -- --debug workspace edit <ws> \
        --mount-isolation /workspace/proj=worktree
    cargo run --release --bin jackin -- --debug purge <container>

Each emits a chronologically ordered `[jackin debug isolation] ...`
trace covering every git invocation, isolation.json mutation, and
finalize decision — suitable for sharing in bug reports.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): rename Iso column to Isolation, count isolation flips as one change

Two related TUI papercuts surfaced together when an operator flipped a
mount's isolation from `shared` to `worktree` via the `I` hotkey:

1. The mount-table header read `Iso` — opaque on first sight. Replaced
   with `Isolation` (the full word). Bumped the column-width constant
   from 8 → 9 so the header label fits without disturbing data-row
   alignment, and renamed `MOUNT_ISO_COL_WIDTH` →
   `MOUNT_ISOLATION_COL_WIDTH` for consistency. Updated the
   alignment-regression test that asserted on the old label.

2. Cycling isolation on an existing mount (same `dst`, same `src`)
   reported "2 changes" in the save-row footer and rendered the
   Confirm Save dialog with a `+`/`-` pair for the same path. Both
   sites used `MountConfig::contains()` — full-struct equality — so
   any isolation/readonly drift made the row appear as remove + add.

   Extracted a `MountDiff` classifier in `console/manager/state.rs`
   that keys on `dst` (the identity used by upsert/remove everywhere
   else). Same-`dst` matches with structural drift are now reported
   as a single `Modified`, counted as one change in `change_count`
   and rendered as a `~ <new>` line with a dimmed `was: <old>` follow-up
   in the Confirm Save summary so the operator sees exactly what
   changed without parsing a remove + add pair.

   Extended `mount_summary` to include the isolation tag so the
   delta is visible in both the new and old lines:
   `~/foo  (rw, worktree, github · main)`.

Records a new shared rule in `RULES.md` ("TUI Labels") to prevent
future short-form labels in user-facing TUI surfaces — operators
read the TUI in passing and cannot afford to decode `Iso`/`Cfg`/`Env`/
`WD`-style abbreviations. Lists the established short forms that are
NOT considered abbreviations (`dst`, `src`, `git`, `op`).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(isolation): make worktree mode actually work inside the container

V1's worktree mode shipped with a gap: the materialized worktree was
bind-mounted into the container at <dst>, but the worktree's `.git`
text file (a pointer back to <host_repo>/.git/worktrees/<n>/) referenced
an absolute host path that didn't exist inside the container. Every git
command — `git status`, `git log`, `git commit`, `git push` — failed
with "fatal: not a git repository". The agent could read source files
but could not commit work, defeating worktree mode's whole purpose.

Fix: wire up three additional bind mounts at docker-run time, plus two
jackin-owned override files written at materialization, so git's gitdir
relationship resolves consistently inside the container without
modifying any host-side files.

For each isolated worktree, the container now sees:

1. The worktree at <dst> (existing).
2. The host repo's `.git/` at /jackin-isolation/<container>-git/ rw,
   so git can find objects, refs, and the per-worktree admin dir.
3. A jackin-owned `.git` text file at <dst>/.git overriding the
   worktree's host-side pointer with one targeting the container path.
4. A jackin-owned back-pointer at /jackin-isolation/<container>-git/
   worktrees/<n>/gitdir overriding git's verification check (host's
   absolute path doesn't match <dst> inside the container).

Override files live under <data_dir>/jackin-<container>/.git-overrides/
and are written once at materialize time. Host files (worktree's `.git`
and the admin dir's `gitdir`) are NEVER modified — host-side
`git worktree list` continues to work identically.

Three layouts were considered (Docker Sandboxes-style `.jackin/` in the
host repo, indirect mount with override files, jackin-owned bare repo).
The chosen approach preserves jackin's dst-based mount model (operator
configures dst=/workspace/jackin → agent works at that exact path), keeps
the host repo clean (no `.jackin/` directory), and exposes only the
worktree to the agent (not the entire host main tree). Full design
rationale and the comparison with Docker Sandboxes, Conductor, and
clone mode (planned for V1.1) are in
docs/src/content/docs/reference/roadmap/per-mount-isolation.mdx
under "Design Decision: Worktree Materialization Layout" and
"Comparison with Other Tools".

Trust trade-off: the agent has rw access to the host repo's `.git/`
since refs/objects are inherently shared in git worktrees. Worktree
mode is appropriate for trusted agents on personal projects where
immediate ref visibility on the host is valuable. Operators who want
ref isolation should use clone mode (planned).

Tests:
- write_git_overrides_writes_both_files_with_correct_content asserts
  override file content matches the design doc verbatim.
- write_git_overrides_is_idempotent confirms re-running on a reused
  worktree (load → eject → load) doesn't drift.
- override_id_strips_slashes_and_trims pins the file-naming scheme.
- container_git_dir_path_namespaces_by_container_name pins the
  hardcoded container-side path so two parallel agents don't collide.
- Extended per_mount_isolation_e2e to assert MaterializedMount carries
  WorktreeAuxMounts on the worktree path and that override files land
  on disk at the documented locations.

Manual verification recipe (add after running once):
  cargo run --release --bin jackin -- --debug load <agent>
  docker exec -ti <container> git status     # was failing, now works
  docker exec -ti <container> git log        # works
  docker exec -ti <container> git commit -m test --allow-empty
  git -C <host-repo> branch -a               # shows new branch

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(isolation): /jackin/{host,admin}/<dst> mounts, container-name basename, :ro hardening

Three related changes that finalize the worktree-mode mount layout:

1. Container-side path scheme renamed and reorganized.
   /jackin-isolation/<container>-git/...  →  /jackin/host/<dst-stripped>/.git
                                             /jackin/admin/<dst-stripped>/{commondir,gitdir}

   - Single top-level /jackin/ namespace for everything jackin contributes
     to the agent's filesystem (room to grow with /jackin/cache/, etc.)
   - host/ category mirrors host topology so docker inspect shows symmetric
     Source/Destination paths both ending in `.git`
   - admin/ category lives at a separate top level so the override files
     (which sit on top of files inside the admin dir) do NOT visually nest
     inside /jackin/host/.../.git/. Two top-level concerns, no overlap.

2. Host-side storage groups all git artifacts for one mount under
   <state>/git/<dst-stripped>/, with override-file names matching their
   docker mount destinations:

       <state>/git/<dst-stripped>/
       ├── <container>/    (the worktree; basename = container name)
       └── overrides/
           ├── .git
           ├── commondir
           └── gitdir

   Replaces the prior <state>/.git-overrides/ flat layout with underscored
   slug filenames. New layout uses dst as a real directory tree (no slug)
   and source filenames identical to destination filenames — the source/
   destination relationship is obvious in `docker inspect`.

3. Worktree subdir basename = container name. `git worktree add` derives
   the host-side admin entry name from the worktree path's basename (no
   --name flag exists upstream). Using the container name (which jackin
   guarantees is globally unique) makes admin entries in
   <host_repo>/.git/worktrees/ globally unique per (host_repo, container)
   — `git worktree list` on the host immediately shows which container
   owns each worktree.

   This required a new validation rule:
   `workspace::validate_isolation_layout` now rejects two isolated mounts
   that resolve to the same host repository within one workspace.
   Allowing them would force the same container-name basename twice in
   one host repo's .git/worktrees/ namespace; no real operator workflow
   has surfaced for this case. Revisit if one does.

   Removes the now-dead suffix logic from materialize.rs:
   - `count_isolated_per_repo` (helper)
   - `canonicalize_or_clone` (helper)
   - `dst_to_branch_suffix` (in src/isolation/branch.rs — no callers left)
   The `branch_name` function keeps its optional suffix parameter for
   future clone-mode use; V1 worktree always passes None.

4. The three override files (replacement `.git` pointer, `commondir`,
   `gitdir` back-pointer) are mounted `:ro` as defensive hardening. Git
   only reads them during normal agent work, and a misbehaving agent
   could otherwise rewrite the gitdir pointer to redirect operations at
   a different repo entirely. The host `.git/` and admin mounts stay rw
   because git writes refs/objects/HEAD/index/logs there.

Tests:
- workspace::tests::isolation_layout_rejects_two_worktree_mounts_on_same_repo
- workspace::tests::isolation_layout_allows_different_host_repos_in_one_workspace
- materialize::tests::worktree_path_uses_container_name_as_basename
- materialize::tests::container_host_git_path_mirrors_dst_under_jackin_host
- materialize::tests::container_admin_path_lives_under_jackin_admin
- materialize::tests::host_and_admin_paths_disambiguate_per_mount_in_one_container
- materialize::tests::write_git_overrides_writes_three_files_with_correct_content
- materialize::tests::write_git_overrides_is_idempotent
- launch::tests::build_workspace_mount_strings_marks_overrides_readonly
  (asserts all 6 mounts in correct order with correct :ro placement)
- per_mount_isolation_e2e: updated for new path scheme + admin name

Removed:
- materialize::tests::two_isolated_mounts_same_repo_get_dst_suffixed_branches
  (case is now rejected at the workspace-validation level)

Roadmap MDX (per-mount-isolation.mdx):
- Container-side mount layout section: 4 mounts → 6, new path scheme,
  override-file storage layout
- Composition Rules: documents the new same-host-repo rejection
- Comparison table: bind mount count for jackin worktree updated 4 → 6
- V1 Scope: ship list updated with new layout and the new validation rule

Manual verification (after merge):
  cargo run --release --bin jackin -- --debug load <agent> <workspace>
  docker inspect <container> | jq '.[0].Mounts'   # see /jackin/{host,admin}/...
  docker exec -w <dst> <container> git status     # works
  git -C <host_repo> worktree list                # admin name = <container>

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(isolation): single /jackin/host/ root, no commondir override, Model B branch naming

Final V1 design after extended brainstorming with the operator. Drops
the `/jackin/admin/<dst>` namespace and the `commondir` override file:
the per-worktree admin entry now lives natively at
`worktrees/<container>/` inside the host `.git/` mount, so git's
on-disk default `commondir = ../..` resolves correctly without an
override.

Container-side topology, per isolated mount (4 binds total, down from 6):
- `<dst>` (rw) — the materialized worktree
- `/jackin/host/<dst-tree>/.git` (rw) — host repo's `.git/`
- `<dst>/.git` (`:ro`) — replacement gitdir pointer
- `/jackin/host/<dst-tree>/.git/worktrees/<container>/gitdir` (`:ro`)
  — replacement back-pointer

Host-side layout under each per-container state dir:
- `git/worktree/repo/<dst-tree>/<container>/` — git's territory
- `git/overrides/<dst-tree>/{.git,gitdir}` — jackin-owned overrides

Branch naming follows Model B: `jackin/scratch/<container_name>`
verbatim. Admin entry name = container name (deterministic, globally
unique because container names are workspace-unique and
`validate_isolation_layout` rejects two isolated mounts on the same
host repo within one workspace — no auto-suffix or read-back needed).

Roadmap doc updated to reflect the final design.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* style(isolation): rustfmt assert_eq! width

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(isolation): align stale references with shipped V1 design

Sweep stale references across roadmap, guides, command and architecture
docs into alignment with what the code actually does. No content added —
the doc just told two contradictory stories before (Model B branch
naming alongside the old selector-key derivation; the new
git/worktree/repo/<dst>/<container>/ on-disk layout alongside the
proposed-but-never-implemented isolated/<slug>/ layout). Now there is
one consistent story end-to-end.

Touches:
- docs/src/content/docs/reference/roadmap/per-mount-isolation.mdx
- docs/src/content/docs/guides/workspaces.mdx
- docs/src/content/docs/commands/purge.mdx
- docs/src/content/docs/reference/architecture.mdx

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* chore(cli): remove jackin cd command from V1

Operationally redundant with `git worktree list` + native shell `cd`.
For worktree mode, the host's `git worktree list` already enumerates
every isolated worktree by branch and absolute path, so a plain
`cd $(...)` reaches the same destination. The remaining edge cases
(preserved-dirty inspection, multi-mount picker) are rare enough that a
dedicated subcommand is net cost rather than net benefit.

Removes:
- src/cli/cd.rs (CdArgs + select_record + tests)
- Command::Cd enum variant in src/cli/mod.rs
- handle_cd dispatch in src/app/mod.rs
- docs/src/content/docs/commands/cd.mdx and its sidebar entry

Updates:
- src/isolation/finalize.rs preserved-state warning: drop "jackin cd ..."
  hint, point operator to the printed worktree path instead
- src/isolation/materialize.rs source-drift error: same treatment
- guides/workspaces.mdx + reference/architecture.mdx: drop cd references
- roadmap entry: replace "Convenience navigation" paragraph with a
  removed-from-V1 note explaining the rationale; add cd to the Defer
  list so it's recoverable if a real workflow surfaces

isolation.json schema, the preserved-state machinery, and `hardline` /
`purge` flows are unaffected — only the inspection convenience layer
is gone.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* chore(isolation): drop clone from V1 enum/parser/CLI

Per principle: don't pre-add API for unimplemented features. The `clone`
keyword was previously parsed by TOML/CLI and then rejected at validation
with a "planned but not implemented yet" error. Operators got false
positives in linting tools and confusing late failures with no benefit —
nothing in V1 actually does anything useful with the value.

Removes from the runtime:
- MountIsolation::Clone enum variant (src/isolation/mod.rs)
- explicit Clone-rejection in parse_mount_isolation (src/cli/workspace.rs):
  FromStr now produces "invalid isolation `clone`" naturally
- MountIsolation::Clone match arm in materialize_workspace
  (src/isolation/materialize.rs)
- console comments referencing the reserved-but-rejected wording

Tests:
- New `rejects_clone_until_implemented` test on FromStr asserts the
  standard "invalid isolation `clone`; expected one of: shared, worktree"
  error so this stays locked down
- parse_mount_isolation_rejects_clone updated to assert the new error
  shape

Docs:
- guides/workspaces.mdx, commands/workspace.mdx,
  reference/configuration.mdx, reference/roadmap.mdx: drop the
  "reserved keyword" wording, point at the V1.1 roadmap entry instead
- roadmap/per-mount-isolation.mdx: keep the `clone` design discussion,
  rephrase the V1 vocabulary section to make it explicit that the keyword
  is added back when clone mode ships, not pre-shipped now

apply_isolation_overrides already enforces "--mount-isolation must
reference an existing mount destination" (planner.rs); no change needed
for that requirement, just clarified in the roadmap CLI-behavior bullet.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(workspace): always write mount isolation field explicitly on save

Old configs without the `isolation` field still deserialize to Shared
(the enum default). On save, drop the `skip_serializing_if = is_shared`
guard so every mount writes its isolation level explicitly — including
`shared`. Old TOMLs migrate to the new shape on first save instead of
silently retaining their pre-isolation form.

Rationale: when the operator opens the saved config, every mount should
name its isolation level. No "field is missing therefore implicitly
shared" — the value is always present and the source of truth in the
file matches the source of truth at runtime.

Touches:
- src/workspace/mod.rs MountConfig.isolation: drop skip_serializing_if
- mount_config_omits_isolation_field_when_shared_on_serialize → renamed
  to mount_config_writes_isolation_field_even_when_shared_on_serialize,
  asserts the field IS written
- guides/mounts.mdx + reference/configuration.mdx: update wording

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs: remove stale per-mount-isolation design spec

The brainstorming spec at docs/superpowers/specs/ predated the V1 design
iterations and no longer reflects what shipped (it still describes the
old `/jackin-isolation/` mount layout, the selector-key-based branch
naming, the reserved `clone` enum value, and `jackin cd`). The roadmap
entry at docs/src/content/docs/reference/roadmap/per-mount-isolation.mdx
is now the single source of truth.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): improve per-mount isolation entry — accurate Sandboxes
comparison, V1 overview, concrete clone-mode personas

Four targeted improvements to the per-mount-isolation roadmap doc:

1. Add a "How V1 worktree mode works (TL;DR)" overview at the top of
   Host-Side Materialization. 5-step walkthrough of the materialize +
   mount + commit flow before the deep-dive subsections, so readers
   land on the entry can understand the shipped V1 in 90 seconds
   without scrolling through the layout/lifecycle/etc.

2. Rewrite the Docker Sandboxes comparison for accuracy. The old
   description got it wrong on multiple points:
   - Sandboxes use a microVM with hypervisor isolation, not Docker
     containers (Linux namespaces) like jackin.
   - The host filesystem is exposed via filesystem passthrough at the
     SAME absolute path as the host, not a bind mount of the entire
     repo at "the same relative paths".
   - Worktree path is `<host_repo>/.sbx/<sandbox-name>-worktrees/<branch>/`
     (sandbox name is in the path), not `<host_repo>/.sbx/<branch>/`.
   - Sandboxes do NOT expose the host main working tree to the agent
     — only the worktree subdir and the parent `.git/`. Our table
     previously said "✓ (entire host repo mounted)".
   The architectural insight is now explicit: Sandboxes' absolute-path
   equivalence makes git's on-disk absolute pointers resolve natively,
   which is why they don't need override files. We pay that cost
   because Docker containers translate host paths to operator-chosen
   `dst` values, breaking absolute-path equivalence.

3. Concrete clone-mode operator personas. The previous description
   was abstract ("complementary mode for ref isolation"). Replaced
   with four named situations clone mode targets: untrusted/experimental
   agents, parallel-fan-out scratch-branch noise, editor watcher
   churn, and teams whose workflow is push-to-share anyway.

4. New "Sandbox runtime" and "Host file exposure" rows in the
   comparison table to make the underlying architectural choice
   immediately visible. Cross-referenced from the Sandboxes prose.

Net: ~80 lines changed/added, primarily replacement of the wrong
Docker Sandboxes facts and addition of the V1-overview block.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(isolation): close four data-loss windows in finalize/cleanup paths

Four merge-blocking issues surfaced by deep code review of PR #177.
Each was a silent-failure window that could destroy operator data on
the unhappy path while the happy-path manual smoke test stayed green.

1. assess_cleanup now treats every git capture failure as
   PreservedUnpushed (was: unwrap_or_default → empty string → could
   land in SafeToDelete). Without this, a transient `git rev-list`
   failure (corrupted pack mid-traversal, broken pipe under load,
   index.lock from a backgrounded git GC) would auto-delete the
   worktree and scratch branch, garbage-collecting unpushed commits.
   Each capture site now uses an explicit `match` that returns
   PreservedUnpushed with debug_log of the underlying error, plus a
   defense-in-depth empty-HEAD guard.

2. finalize_clean_exit now collects ALL preserved records and prompts
   per-record (was: needs_prompt.get_or_insert reached only the first
   one). On a multi-mount workspace where the operator chooses
   force-delete on the first prompt, the second preserved worktree was
   silently orphaned and the container torn down anyway — the only
   reconnection path (jackin hardline) was lost. Now each preserved
   record gets its own prompt; "return to agent" short-circuits the
   loop; "preserve" propagates as Preserved and skips container
   teardown.

3. inspect_attach_outcome now returns still_running() on docker capture
   failure (was: stopped(0) → entered finalize_clean_exit → could
   compound with #1 to delete worktrees of containers that may still
   be alive). The conservative direction is "preserve when we don't
   know" — `jackin hardline` recovers from there.

4. force_cleanup_isolated now verifies cleanup actually completed
   before removing the isolation.json record (was: let _ on git ops +
   unconditional remove_record → orphan worktree admin entries on the
   host repo and orphan branches with no jackin reference). Tolerates
   the idempotent paths (already-removed worktree, already-deleted
   branch verified absent via `git branch --list`); bails on real
   failures with a clear "record retained, re-run jackin purge after
   resolving the issue" message.

Test coverage:
- 5 new tests in isolation/finalize.rs pinning the assess_cleanup
  capture-failure → PreservedUnpushed contract for each git command
  in the assessment chain, plus the empty-HEAD guard.
- 3 new tests pinning the multi-record finalize path (force-delete-all,
  mixed force/preserve, non-interactive multi-mount warning).
- 1 new test for inspect_attach_outcome capture-failure fallback.
- 3 new cleanup tests (branch-already-deleted tolerance, real-failure
  retention, error-message contract).
- 1 new test for validate_workspace_config integration (catches the
  validate_isolation_layout call site if anyone refactors it away).
- 1 new test for build_workspace_mount_strings on a multi-mount
  isolated workspace (8 distinct binds, no path collisions, :ro
  hardening on every override file).
- 2 new drift-detection tests (dst-removed flagged; isolation-mode
  flips not-flagged with explanatory note for future improvement).

Net: 1170 → 1186 tests, 16 additions, all passing.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(isolation): close P1/P2/P3 — follow-on bugs from second review

Second deep code review of PR #177 surfaced three more issues after
the first round of merge-blocker fixes shipped:

P1. `force_cleanup_isolated` failure mid-loop in finalize_clean_exit
    propagated as Err via `?`, leaving the operator with a raw cleanup
    error from deep in finalize, no Preserved signal to the caller, the
    container left running without explicit teardown decision, and
    subsequent records in the loop never prompted. This regression was
    introduced by the round-1 multi-record loop fix (the single-record
    path always succeeded). Now caught per-record, eprintln'd as a
    warning, and treated as `any_preserved_after_prompt = true` so the
    loop continues and the caller gets `Preserved`.

P2. `inspect_attach_outcome` only treated `status == "running"` as
    still-alive. `paused | restarting | removing | created | dead` all
    fell through to `stopped(0)` → entered `finalize_clean_exit` →
    could auto-delete worktrees of containers that may resume any
    moment. Concrete: `docker pause jackin-x` while jackin re-attaches
    → status="paused" → SafeToDelete on a clean tree → operator
    unpauses to find the worktree gone. Replaced if-cascade with an
    explicit `match status` that only routes `exited` through stopped()
    and treats unknown status strings conservatively as still_running.

P3. `purge_isolated_for_container` swallowed per-record errors with
    eprintln warnings and returned `Ok(())`. Exacerbated by the
    round-1 fix #4 (force_cleanup_isolated now bails more often on
    real failures). Operator runs `jackin purge`, sees a warning
    scroll past, gets exit-code-0 prompt back, may believe purge
    completed. Now collects failures and surfaces an aggregate Err
    with the failed mount list so the exit code reflects reality.

Test coverage for these fixes:
- 2 new tests in finalize.rs: ReturnToAgent on the 2nd-of-3 prompt
  (early-return short-circuits), and force_cleanup_isolated failing
  mid-loop (loop continues, returns Preserved).
- 8 new tests in launch.rs covering every status code path:
  exited(0/non-zero/oom), running, paused, transient (restarting/
  removing/created), dead, unknown.
- 2 new tests in cleanup.rs: purge bails on partial failure,
  branch_still_present returning None proceeds (pins the doc-comment
  contract against future refactors to `unwrap_or(true)`).

Net: 1186 → 1198 tests, +12 additions, all passing. fmt/clippy clean.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
donbeave added a commit that referenced this pull request May 6, 2026
* ci(docs): add link checking

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): stabilize lychee checks

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): validate edit links with lychee

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): close link check gaps

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs: add repo file link component

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs: explain repo link source check

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): allow manual dispatch of deployed link check

Two refinements from PR review:

- The check-deployed job now triggers on workflow_dispatch in addition
  to schedule, so maintainers can manually verify the live deployed
  docs without waiting for the daily cron or pushing to main. This
  closes the gap against goal "manual workflow to verify both built
  site and deployed documentation".

- Drop github.sha from the deploy job's lychee cache primary key so it
  matches across runs (the SHA-keyed primary was guaranteed to miss,
  forcing fallback to restore-keys). Now mirrors the cache key shape
  used by check-deployed.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): harden link-check workflow and broaden source lint

Address review feedback on PR #173.

Workflow (.github/workflows/docs.yml):
- Split concurrency group by event_name so a scheduled or
  workflow_dispatch run cannot cancel an in-flight push deploy. Cancel
  in-progress is now scoped to pull_request only.
- Exclude main from workflow_dispatch on check-deployed. The deploy job
  already verifies the just-deployed site, so running both in parallel
  would race against the publish window. Manual verification of the live
  site from main flows through deploy; from feature branches it flows
  through check-deployed.
- Add the build cache as a fallback restore-key for check-deployed so
  the daily cron and manual runs warm-start from the last build cache
  when the lychee.toml fingerprint changes.

RepoFile component (docs/src/components/RepoFile.astro):
- Add target=_blank and rel=noopener noreferrer so GitHub source links
  open in a new tab, matching the behavior rehype-external-links applies
  to plain markdown external links in MDX.

Source lint (docs/scripts/check-repo-links.ts):
- Normalize leading slashes when checking RepoFile path existence so the
  validator agrees with the component's own normalization.
- Cover top-level repo-specific files (Cargo.toml, Cargo.lock, Justfile,
  build.rs, docker-bake.hcl, mise.toml, release.toml, renovate.json) so
  a rename of any of those also breaks the docs CI gate, not only paths
  under src/, docs/, docker/, .github/.

Content (docs/src/content/docs/developing/construct-image.mdx):
- Convert the remaining inline-code references to docker-bake.hcl and
  Justfile to <RepoFile />. These were the references that motivated
  extending the lint to top-level files.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): apply RepoFile lint to multi-runtime proposal

After merging main into codex/docs-link-checks, check-repo-links flagged
37 plain inline-code references to existing repo files in the new
multi-runtime-support.mdx (added in #174 before this PR's lint existed
on main). Convert each one to <RepoFile /> so renames or deletions of
those source files break the docs gate before merge, the same way the
rest of the roadmap is now protected.

No prose changes — every conversion is one-for-one (`src/foo.rs` →
<RepoFile path="src/foo.rs" />). The trailing-slash directory reference
to `docker/runtime/` is left as a code span, since the lint correctly
skips it (it's a directory, not a file).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(roadmap): repair broken Amp CLI link in multi-runtime proposal

CI failed on this PR's last build because lychee found a 404 on
https://github.com/sourcegraph/amp in multi-runtime-support.mdx (added
in #174). That repo does not exist publicly — Amp's source is not on
GitHub.

Point the link at https://ampcode.com instead, which is already the
canonical Amp URL used elsewhere in the docs (getting-started/why.mdx).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request May 6, 2026
)

* ci(docs): bump lychee to v0.24.0 to fix sitemap URL extraction

The first Docs workflow run on main after #173 (commit f3f3e5e) failed
in deploy → "Check deployed docs links" with "No files found for this
input source". Root cause: the previous step ran lychee --dump on the
deployed sitemap URL, but lychee 0.23.0 (the lycheeverse/lychee-action
v2 default) only extracts <a href> from HTML and matching patterns from
markdown — it does not parse <loc> entries from XML sitemaps. The dump
produced an empty list and the follow-up --files-from step had nothing
to read.

Upstream already fixed this. lycheeverse/lychee#2071 (merged
2026-03-13, tagged in v0.24.0 on 2026-04-24) adds <loc> extraction
from sitemap.xml, closing lycheeverse/lychee#2062 and #1819. Verified
locally on 0.24.0:

  $ lychee --version
  lychee 0.24.0
  $ lychee --dump https://jackin.tailrocks.com/sitemap-0.xml | wc -l
  45

Pin LYCHEE_VERSION at the workflow env level and reference it from
every lychee-action call so future bumps are one-line. v0.24.0's
breaking changes are in lychee-lib (the Rust API consumers); the CLI
surface we use is unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): bump lychee-action and lychee for sitemap URL extraction

Replace the previous v0.24.0 bump with the only combination that
actually works against the current lychee release pipeline:

  - lycheeverse/lychee-action SHA 8646ba3 (tagged v2.8.0) → faea714
    (post-v2.8.0 master). Adds subfolder-aware install needed for any
    lychee 0.24.x tarball.
  - LYCHEE_VERSION 'v0.24.0' → 'v0.24.1'.

Why both moves:

* lychee 0.24.0 added <loc> extraction from XML sitemaps
  (lycheeverse/lychee#2071), which is what the deploy and check-deployed
  jobs need to feed --files-from. lychee 0.23.0 dumps zero links from a
  sitemap, which is what produced the "No files found for this input
  source" failure on f3f3e5e.
* lychee 0.24.0's release tarball was repackaged with a top-level
  subfolder AND the asset filename was renamed to
  lychee-lychee-v0.24.0-{arch}-... — both incompatible with
  lychee-action v2.8.0's hardcoded download URL and flat-extract logic.
* lychee 0.24.1 (released the same day) reverted to the original asset
  filename but kept the subfolder layout AND kept the sitemap fix.
* lychee-action faea714 (unreleased; current HEAD of master) bumps the
  default to 0.24.1 and adds subfolder-aware install. Pinning the SHA
  is the same security model we already use for v2.8.0.

The combination 8646ba3 + 'latest' or 8646ba3 + 'v0.24.x' both fail.
The combination faea714 + 'v0.24.1' works.

Verified locally:

  $ lychee-v0.24.1/lychee --version
  lychee 0.24.1
  $ lychee-v0.24.1/lychee --dump https://jackin.tailrocks.com/sitemap-0.xml | wc -l
  45

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): add TODO(lychee-action-sha-pin) marker

Companion to #179, which establishes the convention. Mark the spot
where the SHA pin needs to be reverted once lycheeverse/lychee-action
cuts a tagged release at or after faea714, with a back-link to the
tracked entry in TODO.md so a single grep finds both ends.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request May 6, 2026
* docs(spec): per-mount isolation V1 implementation spec

Captures roadmap → executable design: module layout under
src/isolation/, MountIsolation enum + MountConfig.isolation field,
materialization runtime hook, foreground finalizer with
safe/preserved/force cleanup, source-drift detection, jackin cd
command, TUI integration. Test list and docs touchpoints enumerated
for the implementation plan.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): introduce MountIsolation enum

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(workspace): add isolation field to MountConfig

Defaults to Shared; serde skips emitting the field when Shared so
existing TOMLs round-trip unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(workspace): reject nested isolated mounts

Two worktree-isolated mounts whose dsts nest have no safe on-disk
layout. Sibling isolated mounts and isolated-parent-with-shared-child
remain allowed.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(config): reject isolation field on global mounts

Adds a strict GlobalMountConfig wire-format struct that mirrors
MountConfig minus the isolation field, with deny_unknown_fields
so operators get a clear parse error if they try to set isolation
on a global mount. Isolation remains a workspace-mount concept.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(config): drop dead code and tighten global-mount API

- Delete unused From<MountConfig> for GlobalMountConfig (silently
  dropped isolation; no callers).
- Delete unused get_mut and remove on DockerMounts along with their
  #[allow(dead_code)] annotations.
- Tighten AppConfig::add_mount: debug_assert that incoming
  MountConfig has Shared isolation, and construct GlobalMountConfig
  explicitly from src/dst/readonly rather than via the (now-deleted)
  From impl. Keeps the public signature stable so callers in CLI,
  resolve.rs, and preview.rs don't need to change (Issue 2 option B).
- Add wire-path rejection test that goes through MountEntry's
  untagged enum and asserts on the actual serde error
  ("data did not match any variant of untagged enum MountEntry").
- Soften GlobalMountConfig's doc comment to reflect the actual
  serde error shape at the wire path.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): IsolationRecord + isolation.json IO

Atomic write via tmp+rename. Version-1 envelope leaves room for schema
evolution. Read/upsert/remove keyed by mount destination.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* test(isolation): drop redundant clones in state tests

Bring the warning baseline back to 87 after Task 2.1 introduced
two clippy::style hits (cloned_ref_to_slice_refs, redundant_clone).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): list_records_for_workspace walks data dir

Used by workspace-edit drift detection to find which containers have
preserved isolated state for a given workspace.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): branch_name renderer with namespace + suffix support

Suffix is appended to the final selector segment so namespaced agents
keep their selector shape and the disambiguator goes on the leaf name.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): MaterializedWorkspace types

Third workspace shape (Config -> Resolved -> Materialized) used as the
runtime handoff into Docker launch.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): worktree_path_for derives on-disk path from mount dst

Uses dst verbatim (leading/trailing slashes stripped) under
isolated/, so the layout mirrors the container path.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): ensure_worktree_config_enabled

One-shot enabler for extensions.worktreeConfig on the host repo.
Bumps core.repositoryformatversion to 1 when needed.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): preflight checks for worktree materialization

Sensitive-mount, readonly, repo-root, and mid-operation guards.
Errors cite the mount destination and the worktree mode.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): dirty-host preflight gate with --force opt-out

Non-interactive load without --force rejects a dirty host tree.
Interactive contexts are expected to obtain ack upstream.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): materialize_workspace orchestrator

Per-mount worktree materialization with idempotent reuse, source-drift
guard, and branch-name disambiguation when multiple isolated mounts
target the same host repo.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* test(isolation): cover branch disambiguation for same-repo mounts

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): order Docker mounts parent-before-child

Length-ascending sort so shared cache children overlay isolated
worktree parents at container start.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(runtime): hook materialize_workspace between AgentState and Docker

Workspace mounts now flow Config -> Resolved -> Materialized before
reaching the docker run command, with parent-before-child ordering.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): force_cleanup_isolated removes worktree + branch + record

Best-effort git invocations that tolerate missing host repo and
already-removed worktree. Used by purge and the finalizer's force-delete
branch.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): finalizer skeleton + AttachOutcome shape

Decides Preserved when container still running, OOMed, or exited
non-zero. Clean-exit path stubbed - implemented in follow-ups.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): safe-cleanup deletes branches with no commits

When the worktree is clean and HEAD equals the recorded base, the
scratch branch is removed automatically.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): consult upstream when deciding safe cleanup

Pushed commits (reachable from upstream) are safe to delete; local-only
commits or no-upstream divergence preserve the worktree.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* test(isolation): cover interactive unsafe-cleanup prompt branches

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(runtime): finalize foreground session after attach in load + hardline

Both load and hardline now consult inspect_attach_outcome and dispatch
the shared finalizer. Return-to-agent retries safe cleanup once after
the operator returns.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(purge): refuse to run on a live container

Closes a pre-existing gap where purge could delete state out from
under a running agent. Operator must eject first.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(purge): remove isolated worktrees and scratch branches

Reads isolation.json and runs force_cleanup_isolated for each record
before deleting the per-container state directory.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(cli): --mount-isolation DST=TYPE on workspace create/edit

Repeatable. Rejects clone before persistence with the canonical
"reserved but not implemented yet" message.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(workspace): add Isolation column to workspace show

Renders canonical lowercase name for every mount so CLI output matches
TOML/CLI input verbatim.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(load): add --force to acknowledge dirty host tree

Required for non-interactive isolated-mount materialization when the
host working tree is dirty.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(workspace): detect source drift on edit affecting isolated mounts

Edits that change src for a mount with preserved isolated state are
rejected unless --delete-isolated-state is passed and no related
container is running.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(cli): jackin cd opens a child shell in an isolated worktree

Single-mount-no-dst → uses it. Dst-provided → exact match. Multi-mount
no-dst → interactive picker on TTY, error on non-TTY. Sets JACKIN_*
env vars. Does not modify the parent shell.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(console): show isolation badge per mount in editor + preview

Adds an Iso column to the workspace-manager mount table (editor and
list-pane sub-panel) and a `[shared|worktree|clone]` tag to the agent
preview's resolved-mounts lines. Per the per-mount-isolation spec the
badge renders the canonical spelling for every mount, including
`shared`, so operators always see which strategy applies.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(console): I hotkey cycles isolation on the selected mount

Mirrors the existing R (readonly) toggle. Cycles Shared -> Worktree
-> Shared; Clone is reserved-but-rejected in V1 and is not entered
through this hotkey, but a saved Clone mount snaps back to Shared on
the first I press rather than getting stuck. The cycling rule lives in
EditorState::cycle_isolation_for_selected_mount so the input dispatch
arm stays trivial.

Also surfaces the new key in the Mounts-tab footer hint alongside the
existing R toggle so the affordance is discoverable.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(console): source-drift confirm modal in workspace editor

Save flow runs the same drift detection as `jackin workspace edit`:
detect_workspace_edit_drift evaluates the prospective mount list
(post-collapse, post-upsert) against IsolationRecords on disk before
the on-disk write.

Running container drift -> ErrorPopup ("eject first"); save aborted.
Stopped container drift -> Confirm modal listing the affected
container names with a Yes/No prompt. On Yes the modal handler
re-stashes the plan with delete_isolated_acknowledged = true and the
second commit pass calls force_cleanup_isolated for each affected
record before writing.

Reduced scope vs the original three-button "Delete preserved state and
save / Cancel / Open mount details" dialog: the modal is the existing
two-button Confirm widget (Yes/No). The third "open mount details"
affordance is omitted — operators dismiss with N/Esc, find the
offending mount in the editor, and revert the src by hand. Adding it
would require either a custom widget or repurposing an existing
multi-choice one and threading mount-row focus through the modal
plumbing; the safety value is in the block-and-ack semantics, which
the two-button form covers.

Adds a commit_editor_save_with_runner test seam so the FakeRunner can
drive the drift branch without a real Docker daemon.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(workspaces): add per-mount isolation section

Document the per-mount isolation feature in the workspaces guide:
the three modes (shared default, worktree, clone reserved-but-rejected),
validation preconditions, the isolated-source + shared-cache child
pattern with TOML, and pointers to --mount-isolation and jackin cd.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(mounts): document mount isolation field

Add an "Mount isolation" section to the mounts guide covering the
shared/worktree values, the global-mount rejection at parse time,
and the isolated-source + shared-cache child composition pattern.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(configuration): add MountConfig.isolation field

Document the new mounts[].isolation field in the configuration
reference: shared default, worktree opt-in, clone reserved-but-rejected,
and the global-mount parse-time rejection.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(architecture): document materialization flow + isolation.json

Add a "Workspace materialization" section to the architecture reference
covering the WorkspaceConfig -> ResolvedWorkspace -> MaterializedWorkspace
shapes, the per-container isolation.json layout, and the post-attach
foreground finalizer's Preserved/Cleaned/ReturnToAgent decision matrix.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(workspace): document --mount-isolation and Isolation column

Add --mount-isolation to workspace create/edit option tables (with the
clone "planned but not implemented" note), document the new
--delete-isolated-state flag for non-interactive source-drift edits,
and note the Isolation column on workspace show.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(load): document --force dirty-host acknowledgement

Add --force to the option table and a dedicated section explaining
when it's required (non-interactive load with a worktree-isolated
mount + dirty host tree) and what it does NOT do (no stash, no
discard, no relaxation of other validation).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(purge): document running-agent guard and isolated cleanup

Document purge's new behavior: refuses to run on a running container
(eject first), force-removes isolated worktrees + scratch branches
recorded in isolation.json, and tolerates a missing host repo on
best-effort cleanup.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(cd): add jackin cd command reference

Create a reference page for jackin cd <container> [dst] covering
arguments, the mount-selection behavior matrix (zero/one/many isolated
records, with and without dst), the JACKIN_* env vars set in the
child shell, exit-code passthrough, and the no-parent-mutation
guarantee. Wire it into the Commands sidebar between console and
launch.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): mark per-mount isolation V1 implemented

Flip the per-mount-isolation roadmap status to "Implemented in V1"
and replace the duplicate-mounts-allowed line with the actual V1
rule: multiple isolated mounts are allowed (with branch-name
disambiguation), but nested isolated dst paths are rejected at
validation because the inner worktree's .git would land inside the
outer worktree's tree.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(structure): add isolation module tree and cd command

Update PROJECT_STRUCTURE.md to document the new per-mount-isolation
work: isolation/ module row (mod/branch/materialize/state/finalize/
cleanup), cli/cd.rs entry on the cli/ row, --mount-isolation /
--delete-isolated-state / --force notes on the relevant CLI rows,
foreground-finalizer mention on the runtime row, the new
commands/cd.mdx in the docs map, and a code->docs cross-reference
row mapping src/isolation/** to all the doc pages it touches.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* test(isolation): end-to-end materialize -> clean-exit -> cleanup

Exercises the full lifecycle through public APIs with a small inline
scripted runner. No real git or docker.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(isolation): note finalizer is local-only for hardline lockdown

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* style(test): apply rustfmt to per-mount isolation e2e

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): verify docs links in PRs and on the deployed site (#173)

* ci(docs): add link checking

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): stabilize lychee checks

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): validate edit links with lychee

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): close link check gaps

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs: add repo file link component

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs: explain repo link source check

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): allow manual dispatch of deployed link check

Two refinements from PR review:

- The check-deployed job now triggers on workflow_dispatch in addition
  to schedule, so maintainers can manually verify the live deployed
  docs without waiting for the daily cron or pushing to main. This
  closes the gap against goal "manual workflow to verify both built
  site and deployed documentation".

- Drop github.sha from the deploy job's lychee cache primary key so it
  matches across runs (the SHA-keyed primary was guaranteed to miss,
  forcing fallback to restore-keys). Now mirrors the cache key shape
  used by check-deployed.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* ci(docs): harden link-check workflow and broaden source lint

Address review feedback on PR #173.

Workflow (.github/workflows/docs.yml):
- Split concurrency group by event_name so a scheduled or
  workflow_dispatch run cannot cancel an in-flight push deploy. Cancel
  in-progress is now scoped to pull_request only.
- Exclude main from workflow_dispatch on check-deployed. The deploy job
  already verifies the just-deployed site, so running both in parallel
  would race against the publish window. Manual verification of the live
  site from main flows through deploy; from feature branches it flows
  through check-deployed.
- Add the build cache as a fallback restore-key for check-deployed so
  the daily cron and manual runs warm-start from the last build cache
  when the lychee.toml fingerprint changes.

RepoFile component (docs/src/components/RepoFile.astro):
- Add target=_blank and rel=noopener noreferrer so GitHub source links
  open in a new tab, matching the behavior rehype-external-links applies
  to plain markdown external links in MDX.

Source lint (docs/scripts/check-repo-links.ts):
- Normalize leading slashes when checking RepoFile path existence so the
  validator agrees with the component's own normalization.
- Cover top-level repo-specific files (Cargo.toml, Cargo.lock, Justfile,
  build.rs, docker-bake.hcl, mise.toml, release.toml, renovate.json) so
  a rename of any of those also breaks the docs CI gate, not only paths
  under src/, docs/, docker/, .github/.

Content (docs/src/content/docs/developing/construct-image.mdx):
- Convert the remaining inline-code references to docker-bake.hcl and
  Justfile to <RepoFile />. These were the references that motivated
  extending the lint to top-level files.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): apply RepoFile lint to multi-runtime proposal

After merging main into codex/docs-link-checks, check-repo-links flagged
37 plain inline-code references to existing repo files in the new
multi-runtime-support.mdx (added in #174 before this PR's lint existed
on main). Convert each one to <RepoFile /> so renames or deletions of
those source files break the docs gate before merge, the same way the
rest of the roadmap is now protected.

No prose changes — every conversion is one-for-one (`src/foo.rs` →
<RepoFile path="src/foo.rs" />). The trailing-slash directory reference
to `docker/runtime/` is left as a code span, since the lint correctly
skips it (it's a directory, not a file).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(roadmap): repair broken Amp CLI link in multi-runtime proposal

CI failed on this PR's last build because lychee found a 404 on
https://github.com/sourcegraph/amp in multi-runtime-support.mdx (added
in #174). That repo does not exist publicly — Amp's source is not on
GitHub.

Point the link at https://ampcode.com instead, which is already the
canonical Amp URL used elsewhere in the docs (getting-started/why.mdx).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
(cherry picked from commit f3f3e5e2d386a9cb3c2537be1e51b60f3fc09e6e)

* docs(roadmap): wrap src/cli/cd.rs in <RepoFile> on per-mount-isolation page

The check-repo-links script (added in #173) flags any inline-code
reference to a real repo file that isn't wrapped in <RepoFile />.
src/cli/cd.rs was created on this branch, so once #173's lint reaches
this branch via the previous cherry-pick, the bare reference fails
the Docs CI check.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs: switch cross-doc links to absolute URL form

The link-check job's lychee step (added on main in #173, hardened in
#176) verifies built-site links against the on-disk dist tree. Relative
`.mdx`-suffixed links break that check because lychee resolves them as
literal file paths under the rendered URL's directory — e.g.
`./workspaces.mdx` rendered from `/guides/mounts/` resolves to
`/guides/mounts/workspaces.mdx`, not `/guides/workspaces/`.

Switch the four cross-doc links added by the per-mount-isolation work
to the rendered-URL form (`/guides/workspaces/#per-mount-isolation`
etc.) — same convention as the existing `[mount collapse](/commands/workspace/#mount-collapse)`
link in the same file.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(isolation): emit verbose debug-mode trace for worktree lifecycle

Operators sharing logs to debug worktree behavior had no visibility into
the lifecycle — only three error/warning sites fired output, and none of
them ran on the happy path. `--debug` toggled a display mode (preserve
scrollback, clear spinner) but was not a verbose-trace facility.

This adds a `debug_log!(category, fmt, ...)` macro in `src/tui/mod.rs`
that gates on the existing `DEBUG_MODE` atomic so disabled call sites
cost only an atomic load (formatting is deferred behind the gate).
Output uses a `[jackin debug <category>]` prefix so shared logs are
greppable.

Instrumented sites (all under `category = "isolation"`):

- `materialize_workspace`: per-call summary (workspace, container,
  selector, mount counts, force/interactive flags).
- `materialize_one`: per-mount decision trail — drift detection,
  worktree reuse, preflight, base-commit lookup, branch derivation
  with selected suffix, and the `git worktree add` invocation itself.
- `ensure_worktree_config_enabled`: every state transition (already
  enabled vs. bumping repositoryformatversion vs. flipping the flag)
  with the host repo path.
- `state.rs`: write_records (count + path), upsert_record (insert vs.
  replace), remove_record (drop vs. no-op).
- `cleanup.rs`: force_cleanup_isolated entry, the two git invocations,
  the rm -rf fallback, and the host-repo-missing skip path.
  purge_isolated_for_container per-container summary.
- `finalize.rs`: foreground-session entry with exit code/oom/interactive
  flags, the early-return path for non-clean exits, per-record cleanup
  assessment.
- `runtime/launch.rs`: load_agent's call into materialize_workspace.

Read paths (`read_records`, `read_record`) are intentionally NOT logged
— they fire on every invocation and would drown the log.

Manual verification (since binary-level stderr capture would need a new
test dependency):

    cargo run --release --bin jackin -- --debug load <agent>
    cargo run --release --bin jackin -- --debug workspace edit <ws> \
        --mount-isolation /workspace/proj=worktree
    cargo run --release --bin jackin -- --debug purge <container>

Each emits a chronologically ordered `[jackin debug isolation] ...`
trace covering every git invocation, isolation.json mutation, and
finalize decision — suitable for sharing in bug reports.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): rename Iso column to Isolation, count isolation flips as one change

Two related TUI papercuts surfaced together when an operator flipped a
mount's isolation from `shared` to `worktree` via the `I` hotkey:

1. The mount-table header read `Iso` — opaque on first sight. Replaced
   with `Isolation` (the full word). Bumped the column-width constant
   from 8 → 9 so the header label fits without disturbing data-row
   alignment, and renamed `MOUNT_ISO_COL_WIDTH` →
   `MOUNT_ISOLATION_COL_WIDTH` for consistency. Updated the
   alignment-regression test that asserted on the old label.

2. Cycling isolation on an existing mount (same `dst`, same `src`)
   reported "2 changes" in the save-row footer and rendered the
   Confirm Save dialog with a `+`/`-` pair for the same path. Both
   sites used `MountConfig::contains()` — full-struct equality — so
   any isolation/readonly drift made the row appear as remove + add.

   Extracted a `MountDiff` classifier in `console/manager/state.rs`
   that keys on `dst` (the identity used by upsert/remove everywhere
   else). Same-`dst` matches with structural drift are now reported
   as a single `Modified`, counted as one change in `change_count`
   and rendered as a `~ <new>` line with a dimmed `was: <old>` follow-up
   in the Confirm Save summary so the operator sees exactly what
   changed without parsing a remove + add pair.

   Extended `mount_summary` to include the isolation tag so the
   delta is visible in both the new and old lines:
   `~/foo  (rw, worktree, github · main)`.

Records a new shared rule in `RULES.md` ("TUI Labels") to prevent
future short-form labels in user-facing TUI surfaces — operators
read the TUI in passing and cannot afford to decode `Iso`/`Cfg`/`Env`/
`WD`-style abbreviations. Lists the established short forms that are
NOT considered abbreviations (`dst`, `src`, `git`, `op`).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(isolation): make worktree mode actually work inside the container

V1's worktree mode shipped with a gap: the materialized worktree was
bind-mounted into the container at <dst>, but the worktree's `.git`
text file (a pointer back to <host_repo>/.git/worktrees/<n>/) referenced
an absolute host path that didn't exist inside the container. Every git
command — `git status`, `git log`, `git commit`, `git push` — failed
with "fatal: not a git repository". The agent could read source files
but could not commit work, defeating worktree mode's whole purpose.

Fix: wire up three additional bind mounts at docker-run time, plus two
jackin-owned override files written at materialization, so git's gitdir
relationship resolves consistently inside the container without
modifying any host-side files.

For each isolated worktree, the container now sees:

1. The worktree at <dst> (existing).
2. The host repo's `.git/` at /jackin-isolation/<container>-git/ rw,
   so git can find objects, refs, and the per-worktree admin dir.
3. A jackin-owned `.git` text file at <dst>/.git overriding the
   worktree's host-side pointer with one targeting the container path.
4. A jackin-owned back-pointer at /jackin-isolation/<container>-git/
   worktrees/<n>/gitdir overriding git's verification check (host's
   absolute path doesn't match <dst> inside the container).

Override files live under <data_dir>/jackin-<container>/.git-overrides/
and are written once at materialize time. Host files (worktree's `.git`
and the admin dir's `gitdir`) are NEVER modified — host-side
`git worktree list` continues to work identically.

Three layouts were considered (Docker Sandboxes-style `.jackin/` in the
host repo, indirect mount with override files, jackin-owned bare repo).
The chosen approach preserves jackin's dst-based mount model (operator
configures dst=/workspace/jackin → agent works at that exact path), keeps
the host repo clean (no `.jackin/` directory), and exposes only the
worktree to the agent (not the entire host main tree). Full design
rationale and the comparison with Docker Sandboxes, Conductor, and
clone mode (planned for V1.1) are in
docs/src/content/docs/reference/roadmap/per-mount-isolation.mdx
under "Design Decision: Worktree Materialization Layout" and
"Comparison with Other Tools".

Trust trade-off: the agent has rw access to the host repo's `.git/`
since refs/objects are inherently shared in git worktrees. Worktree
mode is appropriate for trusted agents on personal projects where
immediate ref visibility on the host is valuable. Operators who want
ref isolation should use clone mode (planned).

Tests:
- write_git_overrides_writes_both_files_with_correct_content asserts
  override file content matches the design doc verbatim.
- write_git_overrides_is_idempotent confirms re-running on a reused
  worktree (load → eject → load) doesn't drift.
- override_id_strips_slashes_and_trims pins the file-naming scheme.
- container_git_dir_path_namespaces_by_container_name pins the
  hardcoded container-side path so two parallel agents don't collide.
- Extended per_mount_isolation_e2e to assert MaterializedMount carries
  WorktreeAuxMounts on the worktree path and that override files land
  on disk at the documented locations.

Manual verification recipe (add after running once):
  cargo run --release --bin jackin -- --debug load <agent>
  docker exec -ti <container> git status     # was failing, now works
  docker exec -ti <container> git log        # works
  docker exec -ti <container> git commit -m test --allow-empty
  git -C <host-repo> branch -a               # shows new branch

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(isolation): /jackin/{host,admin}/<dst> mounts, container-name basename, :ro hardening

Three related changes that finalize the worktree-mode mount layout:

1. Container-side path scheme renamed and reorganized.
   /jackin-isolation/<container>-git/...  →  /jackin/host/<dst-stripped>/.git
                                             /jackin/admin/<dst-stripped>/{commondir,gitdir}

   - Single top-level /jackin/ namespace for everything jackin contributes
     to the agent's filesystem (room to grow with /jackin/cache/, etc.)
   - host/ category mirrors host topology so docker inspect shows symmetric
     Source/Destination paths both ending in `.git`
   - admin/ category lives at a separate top level so the override files
     (which sit on top of files inside the admin dir) do NOT visually nest
     inside /jackin/host/.../.git/. Two top-level concerns, no overlap.

2. Host-side storage groups all git artifacts for one mount under
   <state>/git/<dst-stripped>/, with override-file names matching their
   docker mount destinations:

       <state>/git/<dst-stripped>/
       ├── <container>/    (the worktree; basename = container name)
       └── overrides/
           ├── .git
           ├── commondir
           └── gitdir

   Replaces the prior <state>/.git-overrides/ flat layout with underscored
   slug filenames. New layout uses dst as a real directory tree (no slug)
   and source filenames identical to destination filenames — the source/
   destination relationship is obvious in `docker inspect`.

3. Worktree subdir basename = container name. `git worktree add` derives
   the host-side admin entry name from the worktree path's basename (no
   --name flag exists upstream). Using the container name (which jackin
   guarantees is globally unique) makes admin entries in
   <host_repo>/.git/worktrees/ globally unique per (host_repo, container)
   — `git worktree list` on the host immediately shows which container
   owns each worktree.

   This required a new validation rule:
   `workspace::validate_isolation_layout` now rejects two isolated mounts
   that resolve to the same host repository within one workspace.
   Allowing them would force the same container-name basename twice in
   one host repo's .git/worktrees/ namespace; no real operator workflow
   has surfaced for this case. Revisit if one does.

   Removes the now-dead suffix logic from materialize.rs:
   - `count_isolated_per_repo` (helper)
   - `canonicalize_or_clone` (helper)
   - `dst_to_branch_suffix` (in src/isolation/branch.rs — no callers left)
   The `branch_name` function keeps its optional suffix parameter for
   future clone-mode use; V1 worktree always passes None.

4. The three override files (replacement `.git` pointer, `commondir`,
   `gitdir` back-pointer) are mounted `:ro` as defensive hardening. Git
   only reads them during normal agent work, and a misbehaving agent
   could otherwise rewrite the gitdir pointer to redirect operations at
   a different repo entirely. The host `.git/` and admin mounts stay rw
   because git writes refs/objects/HEAD/index/logs there.

Tests:
- workspace::tests::isolation_layout_rejects_two_worktree_mounts_on_same_repo
- workspace::tests::isolation_layout_allows_different_host_repos_in_one_workspace
- materialize::tests::worktree_path_uses_container_name_as_basename
- materialize::tests::container_host_git_path_mirrors_dst_under_jackin_host
- materialize::tests::container_admin_path_lives_under_jackin_admin
- materialize::tests::host_and_admin_paths_disambiguate_per_mount_in_one_container
- materialize::tests::write_git_overrides_writes_three_files_with_correct_content
- materialize::tests::write_git_overrides_is_idempotent
- launch::tests::build_workspace_mount_strings_marks_overrides_readonly
  (asserts all 6 mounts in correct order with correct :ro placement)
- per_mount_isolation_e2e: updated for new path scheme + admin name

Removed:
- materialize::tests::two_isolated_mounts_same_repo_get_dst_suffixed_branches
  (case is now rejected at the workspace-validation level)

Roadmap MDX (per-mount-isolation.mdx):
- Container-side mount layout section: 4 mounts → 6, new path scheme,
  override-file storage layout
- Composition Rules: documents the new same-host-repo rejection
- Comparison table: bind mount count for jackin worktree updated 4 → 6
- V1 Scope: ship list updated with new layout and the new validation rule

Manual verification (after merge):
  cargo run --release --bin jackin -- --debug load <agent> <workspace>
  docker inspect <container> | jq '.[0].Mounts'   # see /jackin/{host,admin}/...
  docker exec -w <dst> <container> git status     # works
  git -C <host_repo> worktree list                # admin name = <container>

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(isolation): single /jackin/host/ root, no commondir override, Model B branch naming

Final V1 design after extended brainstorming with the operator. Drops
the `/jackin/admin/<dst>` namespace and the `commondir` override file:
the per-worktree admin entry now lives natively at
`worktrees/<container>/` inside the host `.git/` mount, so git's
on-disk default `commondir = ../..` resolves correctly without an
override.

Container-side topology, per isolated mount (4 binds total, down from 6):
- `<dst>` (rw) — the materialized worktree
- `/jackin/host/<dst-tree>/.git` (rw) — host repo's `.git/`
- `<dst>/.git` (`:ro`) — replacement gitdir pointer
- `/jackin/host/<dst-tree>/.git/worktrees/<container>/gitdir` (`:ro`)
  — replacement back-pointer

Host-side layout under each per-container state dir:
- `git/worktree/repo/<dst-tree>/<container>/` — git's territory
- `git/overrides/<dst-tree>/{.git,gitdir}` — jackin-owned overrides

Branch naming follows Model B: `jackin/scratch/<container_name>`
verbatim. Admin entry name = container name (deterministic, globally
unique because container names are workspace-unique and
`validate_isolation_layout` rejects two isolated mounts on the same
host repo within one workspace — no auto-suffix or read-back needed).

Roadmap doc updated to reflect the final design.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* style(isolation): rustfmt assert_eq! width

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(isolation): align stale references with shipped V1 design

Sweep stale references across roadmap, guides, command and architecture
docs into alignment with what the code actually does. No content added —
the doc just told two contradictory stories before (Model B branch
naming alongside the old selector-key derivation; the new
git/worktree/repo/<dst>/<container>/ on-disk layout alongside the
proposed-but-never-implemented isolated/<slug>/ layout). Now there is
one consistent story end-to-end.

Touches:
- docs/src/content/docs/reference/roadmap/per-mount-isolation.mdx
- docs/src/content/docs/guides/workspaces.mdx
- docs/src/content/docs/commands/purge.mdx
- docs/src/content/docs/reference/architecture.mdx

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* chore(cli): remove jackin cd command from V1

Operationally redundant with `git worktree list` + native shell `cd`.
For worktree mode, the host's `git worktree list` already enumerates
every isolated worktree by branch and absolute path, so a plain
`cd $(...)` reaches the same destination. The remaining edge cases
(preserved-dirty inspection, multi-mount picker) are rare enough that a
dedicated subcommand is net cost rather than net benefit.

Removes:
- src/cli/cd.rs (CdArgs + select_record + tests)
- Command::Cd enum variant in src/cli/mod.rs
- handle_cd dispatch in src/app/mod.rs
- docs/src/content/docs/commands/cd.mdx and its sidebar entry

Updates:
- src/isolation/finalize.rs preserved-state warning: drop "jackin cd ..."
  hint, point operator to the printed worktree path instead
- src/isolation/materialize.rs source-drift error: same treatment
- guides/workspaces.mdx + reference/architecture.mdx: drop cd references
- roadmap entry: replace "Convenience navigation" paragraph with a
  removed-from-V1 note explaining the rationale; add cd to the Defer
  list so it's recoverable if a real workflow surfaces

isolation.json schema, the preserved-state machinery, and `hardline` /
`purge` flows are unaffected — only the inspection convenience layer
is gone.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* chore(isolation): drop clone from V1 enum/parser/CLI

Per principle: don't pre-add API for unimplemented features. The `clone`
keyword was previously parsed by TOML/CLI and then rejected at validation
with a "planned but not implemented yet" error. Operators got false
positives in linting tools and confusing late failures with no benefit —
nothing in V1 actually does anything useful with the value.

Removes from the runtime:
- MountIsolation::Clone enum variant (src/isolation/mod.rs)
- explicit Clone-rejection in parse_mount_isolation (src/cli/workspace.rs):
  FromStr now produces "invalid isolation `clone`" naturally
- MountIsolation::Clone match arm in materialize_workspace
  (src/isolation/materialize.rs)
- console comments referencing the reserved-but-rejected wording

Tests:
- New `rejects_clone_until_implemented` test on FromStr asserts the
  standard "invalid isolation `clone`; expected one of: shared, worktree"
  error so this stays locked down
- parse_mount_isolation_rejects_clone updated to assert the new error
  shape

Docs:
- guides/workspaces.mdx, commands/workspace.mdx,
  reference/configuration.mdx, reference/roadmap.mdx: drop the
  "reserved keyword" wording, point at the V1.1 roadmap entry instead
- roadmap/per-mount-isolation.mdx: keep the `clone` design discussion,
  rephrase the V1 vocabulary section to make it explicit that the keyword
  is added back when clone mode ships, not pre-shipped now

apply_isolation_overrides already enforces "--mount-isolation must
reference an existing mount destination" (planner.rs); no change needed
for that requirement, just clarified in the roadmap CLI-behavior bullet.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(workspace): always write mount isolation field explicitly on save

Old configs without the `isolation` field still deserialize to Shared
(the enum default). On save, drop the `skip_serializing_if = is_shared`
guard so every mount writes its isolation level explicitly — including
`shared`. Old TOMLs migrate to the new shape on first save instead of
silently retaining their pre-isolation form.

Rationale: when the operator opens the saved config, every mount should
name its isolation level. No "field is missing therefore implicitly
shared" — the value is always present and the source of truth in the
file matches the source of truth at runtime.

Touches:
- src/workspace/mod.rs MountConfig.isolation: drop skip_serializing_if
- mount_config_omits_isolation_field_when_shared_on_serialize → renamed
  to mount_config_writes_isolation_field_even_when_shared_on_serialize,
  asserts the field IS written
- guides/mounts.mdx + reference/configuration.mdx: update wording

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs: remove stale per-mount-isolation design spec

The brainstorming spec at docs/superpowers/specs/ predated the V1 design
iterations and no longer reflects what shipped (it still describes the
old `/jackin-isolation/` mount layout, the selector-key-based branch
naming, the reserved `clone` enum value, and `jackin cd`). The roadmap
entry at docs/src/content/docs/reference/roadmap/per-mount-isolation.mdx
is now the single source of truth.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): improve per-mount isolation entry — accurate Sandboxes
comparison, V1 overview, concrete clone-mode personas

Four targeted improvements to the per-mount-isolation roadmap doc:

1. Add a "How V1 worktree mode works (TL;DR)" overview at the top of
   Host-Side Materialization. 5-step walkthrough of the materialize +
   mount + commit flow before the deep-dive subsections, so readers
   land on the entry can understand the shipped V1 in 90 seconds
   without scrolling through the layout/lifecycle/etc.

2. Rewrite the Docker Sandboxes comparison for accuracy. The old
   description got it wrong on multiple points:
   - Sandboxes use a microVM with hypervisor isolation, not Docker
     containers (Linux namespaces) like jackin.
   - The host filesystem is exposed via filesystem passthrough at the
     SAME absolute path as the host, not a bind mount of the entire
     repo at "the same relative paths".
   - Worktree path is `<host_repo>/.sbx/<sandbox-name>-worktrees/<branch>/`
     (sandbox name is in the path), not `<host_repo>/.sbx/<branch>/`.
   - Sandboxes do NOT expose the host main working tree to the agent
     — only the worktree subdir and the parent `.git/`. Our table
     previously said "✓ (entire host repo mounted)".
   The architectural insight is now explicit: Sandboxes' absolute-path
   equivalence makes git's on-disk absolute pointers resolve natively,
   which is why they don't need override files. We pay that cost
   because Docker containers translate host paths to operator-chosen
   `dst` values, breaking absolute-path equivalence.

3. Concrete clone-mode operator personas. The previous description
   was abstract ("complementary mode for ref isolation"). Replaced
   with four named situations clone mode targets: untrusted/experimental
   agents, parallel-fan-out scratch-branch noise, editor watcher
   churn, and teams whose workflow is push-to-share anyway.

4. New "Sandbox runtime" and "Host file exposure" rows in the
   comparison table to make the underlying architectural choice
   immediately visible. Cross-referenced from the Sandboxes prose.

Net: ~80 lines changed/added, primarily replacement of the wrong
Docker Sandboxes facts and addition of the V1-overview block.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(isolation): close four data-loss windows in finalize/cleanup paths

Four merge-blocking issues surfaced by deep code review of PR #177.
Each was a silent-failure window that could destroy operator data on
the unhappy path while the happy-path manual smoke test stayed green.

1. assess_cleanup now treats every git capture failure as
   PreservedUnpushed (was: unwrap_or_default → empty string → could
   land in SafeToDelete). Without this, a transient `git rev-list`
   failure (corrupted pack mid-traversal, broken pipe under load,
   index.lock from a backgrounded git GC) would auto-delete the
   worktree and scratch branch, garbage-collecting unpushed commits.
   Each capture site now uses an explicit `match` that returns
   PreservedUnpushed with debug_log of the underlying error, plus a
   defense-in-depth empty-HEAD guard.

2. finalize_clean_exit now collects ALL preserved records and prompts
   per-record (was: needs_prompt.get_or_insert reached only the first
   one). On a multi-mount workspace where the operator chooses
   force-delete on the first prompt, the second preserved worktree was
   silently orphaned and the container torn down anyway — the only
   reconnection path (jackin hardline) was lost. Now each preserved
   record gets its own prompt; "return to agent" short-circuits the
   loop; "preserve" propagates as Preserved and skips container
   teardown.

3. inspect_attach_outcome now returns still_running() on docker capture
   failure (was: stopped(0) → entered finalize_clean_exit → could
   compound with #1 to delete worktrees of containers that may still
   be alive). The conservative direction is "preserve when we don't
   know" — `jackin hardline` recovers from there.

4. force_cleanup_isolated now verifies cleanup actually completed
   before removing the isolation.json record (was: let _ on git ops +
   unconditional remove_record → orphan worktree admin entries on the
   host repo and orphan branches with no jackin reference). Tolerates
   the idempotent paths (already-removed worktree, already-deleted
   branch verified absent via `git branch --list`); bails on real
   failures with a clear "record retained, re-run jackin purge after
   resolving the issue" message.

Test coverage:
- 5 new tests in isolation/finalize.rs pinning the assess_cleanup
  capture-failure → PreservedUnpushed contract for each git command
  in the assessment chain, plus the empty-HEAD guard.
- 3 new tests pinning the multi-record finalize path (force-delete-all,
  mixed force/preserve, non-interactive multi-mount warning).
- 1 new test for inspect_attach_outcome capture-failure fallback.
- 3 new cleanup tests (branch-already-deleted tolerance, real-failure
  retention, error-message contract).
- 1 new test for validate_workspace_config integration (catches the
  validate_isolation_layout call site if anyone refactors it away).
- 1 new test for build_workspace_mount_strings on a multi-mount
  isolated workspace (8 distinct binds, no path collisions, :ro
  hardening on every override file).
- 2 new drift-detection tests (dst-removed flagged; isolation-mode
  flips not-flagged with explanatory note for future improvement).

Net: 1170 → 1186 tests, 16 additions, all passing.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(isolation): close P1/P2/P3 — follow-on bugs from second review

Second deep code review of PR #177 surfaced three more issues after
the first round of merge-blocker fixes shipped:

P1. `force_cleanup_isolated` failure mid-loop in finalize_clean_exit
    propagated as Err via `?`, leaving the operator with a raw cleanup
    error from deep in finalize, no Preserved signal to the caller, the
    container left running without explicit teardown decision, and
    subsequent records in the loop never prompted. This regression was
    introduced by the round-1 multi-record loop fix (the single-record
    path always succeeded). Now caught per-record, eprintln'd as a
    warning, and treated as `any_preserved_after_prompt = true` so the
    loop continues and the caller gets `Preserved`.

P2. `inspect_attach_outcome` only treated `status == "running"` as
    still-alive. `paused | restarting | removing | created | dead` all
    fell through to `stopped(0)` → entered `finalize_clean_exit` →
    could auto-delete worktrees of containers that may resume any
    moment. Concrete: `docker pause jackin-x` while jackin re-attaches
    → status="paused" → SafeToDelete on a clean tree → operator
    unpauses to find the worktree gone. Replaced if-cascade with an
    explicit `match status` that only routes `exited` through stopped()
    and treats unknown status strings conservatively as still_running.

P3. `purge_isolated_for_container` swallowed per-record errors with
    eprintln warnings and returned `Ok(())`. Exacerbated by the
    round-1 fix #4 (force_cleanup_isolated now bails more often on
    real failures). Operator runs `jackin purge`, sees a warning
    scroll past, gets exit-code-0 prompt back, may believe purge
    completed. Now collects failures and surfaces an aggregate Err
    with the failed mount list so the exit code reflects reality.

Test coverage for these fixes:
- 2 new tests in finalize.rs: ReturnToAgent on the 2nd-of-3 prompt
  (early-return short-circuits), and force_cleanup_isolated failing
  mid-loop (loop continues, returns Preserved).
- 8 new tests in launch.rs covering every status code path:
  exited(0/non-zero/oom), running, paused, transient (restarting/
  removing/created), dead, unknown.
- 2 new tests in cleanup.rs: purge bails on partial failure,
  branch_still_present returning None proceeds (pins the doc-comment
  contract against future refactors to `unwrap_or(true)`).

Net: 1186 → 1198 tests, +12 additions, all passing. fmt/clippy clean.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
donbeave added a commit that referenced this pull request May 7, 2026
* ci(docs): add link checking


* ci(docs): stabilize lychee checks


* ci(docs): validate edit links with lychee


* ci(docs): close link check gaps


* docs: add repo file link component


* docs: explain repo link source check


* ci(docs): allow manual dispatch of deployed link check

Two refinements from PR review:

- The check-deployed job now triggers on workflow_dispatch in addition
  to schedule, so maintainers can manually verify the live deployed
  docs without waiting for the daily cron or pushing to main. This
  closes the gap against goal "manual workflow to verify both built
  site and deployed documentation".

- Drop github.sha from the deploy job's lychee cache primary key so it
  matches across runs (the SHA-keyed primary was guaranteed to miss,
  forcing fallback to restore-keys). Now mirrors the cache key shape
  used by check-deployed.

Co-authored-by: Claude <noreply@anthropic.com>

* ci(docs): harden link-check workflow and broaden source lint

Address review feedback on PR #173.

Workflow (.github/workflows/docs.yml):
- Split concurrency group by event_name so a scheduled or
  workflow_dispatch run cannot cancel an in-flight push deploy. Cancel
  in-progress is now scoped to pull_request only.
- Exclude main from workflow_dispatch on check-deployed. The deploy job
  already verifies the just-deployed site, so running both in parallel
  would race against the publish window. Manual verification of the live
  site from main flows through deploy; from feature branches it flows
  through check-deployed.
- Add the build cache as a fallback restore-key for check-deployed so
  the daily cron and manual runs warm-start from the last build cache
  when the lychee.toml fingerprint changes.

RepoFile component (docs/src/components/RepoFile.astro):
- Add target=_blank and rel=noopener noreferrer so GitHub source links
  open in a new tab, matching the behavior rehype-external-links applies
  to plain markdown external links in MDX.

Source lint (docs/scripts/check-repo-links.ts):
- Normalize leading slashes when checking RepoFile path existence so the
  validator agrees with the component's own normalization.
- Cover top-level repo-specific files (Cargo.toml, Cargo.lock, Justfile,
  build.rs, docker-bake.hcl, mise.toml, release.toml, renovate.json) so
  a rename of any of those also breaks the docs CI gate, not only paths
  under src/, docs/, docker/, .github/.

Content (docs/src/content/docs/developing/construct-image.mdx):
- Convert the remaining inline-code references to docker-bake.hcl and
  Justfile to <RepoFile />. These were the references that motivated
  extending the lint to top-level files.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): apply RepoFile lint to multi-runtime proposal

After merging main into codex/docs-link-checks, check-repo-links flagged
37 plain inline-code references to existing repo files in the new
multi-runtime-support.mdx (added in #174 before this PR's lint existed
on main). Convert each one to <RepoFile /> so renames or deletions of
those source files break the docs gate before merge, the same way the
rest of the roadmap is now protected.

No prose changes — every conversion is one-for-one (`src/foo.rs` →
<RepoFile path="src/foo.rs" />). The trailing-slash directory reference
to `docker/runtime/` is left as a code span, since the lint correctly
skips it (it's a directory, not a file).

Co-authored-by: Claude <noreply@anthropic.com>

* fix(roadmap): repair broken Amp CLI link in multi-runtime proposal

CI failed on this PR's last build because lychee found a 404 on
https://github.com/sourcegraph/amp in multi-runtime-support.mdx (added
in #174). That repo does not exist publicly — Amp's source is not on
GitHub.

Point the link at https://ampcode.com instead, which is already the
canonical Amp URL used elsewhere in the docs (getting-started/why.mdx).

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Codex <codex@openai.com>
donbeave added a commit that referenced this pull request May 7, 2026
)

* ci(docs): bump lychee to v0.24.0 to fix sitemap URL extraction

The first Docs workflow run on main after #173 (commit f3f3e5e) failed
in deploy → "Check deployed docs links" with "No files found for this
input source". Root cause: the previous step ran lychee --dump on the
deployed sitemap URL, but lychee 0.23.0 (the lycheeverse/lychee-action
v2 default) only extracts <a href> from HTML and matching patterns from
markdown — it does not parse <loc> entries from XML sitemaps. The dump
produced an empty list and the follow-up --files-from step had nothing
to read.

Upstream already fixed this. lycheeverse/lychee#2071 (merged
2026-03-13, tagged in v0.24.0 on 2026-04-24) adds <loc> extraction
from sitemap.xml, closing lycheeverse/lychee#2062 and #1819. Verified
locally on 0.24.0:

  $ lychee --version
  lychee 0.24.0
  $ lychee --dump https://jackin.tailrocks.com/sitemap-0.xml | wc -l
  45

Pin LYCHEE_VERSION at the workflow env level and reference it from
every lychee-action call so future bumps are one-line. v0.24.0's
breaking changes are in lychee-lib (the Rust API consumers); the CLI
surface we use is unchanged.

Co-authored-by: Claude <noreply@anthropic.com>

* ci(docs): bump lychee-action and lychee for sitemap URL extraction

Replace the previous v0.24.0 bump with the only combination that
actually works against the current lychee release pipeline:

  - lycheeverse/lychee-action SHA 8646ba3 (tagged v2.8.0) → faea714
    (post-v2.8.0 master). Adds subfolder-aware install needed for any
    lychee 0.24.x tarball.
  - LYCHEE_VERSION 'v0.24.0' → 'v0.24.1'.

Why both moves:

* lychee 0.24.0 added <loc> extraction from XML sitemaps
  (lycheeverse/lychee#2071), which is what the deploy and check-deployed
  jobs need to feed --files-from. lychee 0.23.0 dumps zero links from a
  sitemap, which is what produced the "No files found for this input
  source" failure on f3f3e5e.
* lychee 0.24.0's release tarball was repackaged with a top-level
  subfolder AND the asset filename was renamed to
  lychee-lychee-v0.24.0-{arch}-... — both incompatible with
  lychee-action v2.8.0's hardcoded download URL and flat-extract logic.
* lychee 0.24.1 (released the same day) reverted to the original asset
  filename but kept the subfolder layout AND kept the sitemap fix.
* lychee-action faea714 (unreleased; current HEAD of master) bumps the
  default to 0.24.1 and adds subfolder-aware install. Pinning the SHA
  is the same security model we already use for v2.8.0.

The combination 8646ba3 + 'latest' or 8646ba3 + 'v0.24.x' both fail.
The combination faea714 + 'v0.24.1' works.

Verified locally:

  $ lychee-v0.24.1/lychee --version
  lychee 0.24.1
  $ lychee-v0.24.1/lychee --dump https://jackin.tailrocks.com/sitemap-0.xml | wc -l
  45

Co-authored-by: Claude <noreply@anthropic.com>

* ci(docs): add TODO(lychee-action-sha-pin) marker

Companion to #179, which establishes the convention. Mark the spot
where the SHA pin needs to be reverted once lycheeverse/lychee-action
cuts a tagged release at or after faea714, with a back-link to the
tracked entry in TODO.md so a single grep finds both ends.

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Codex <codex@openai.com>
donbeave added a commit that referenced this pull request May 7, 2026
* docs(spec): per-mount isolation V1 implementation spec

Captures roadmap → executable design: module layout under
src/isolation/, MountIsolation enum + MountConfig.isolation field,
materialization runtime hook, foreground finalizer with
safe/preserved/force cleanup, source-drift detection, jackin cd
command, TUI integration. Test list and docs touchpoints enumerated
for the implementation plan.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(isolation): introduce MountIsolation enum

Co-authored-by: Claude <noreply@anthropic.com>

* feat(workspace): add isolation field to MountConfig

Defaults to Shared; serde skips emitting the field when Shared so
existing TOMLs round-trip unchanged.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(workspace): reject nested isolated mounts

Two worktree-isolated mounts whose dsts nest have no safe on-disk
layout. Sibling isolated mounts and isolated-parent-with-shared-child
remain allowed.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(config): reject isolation field on global mounts

Adds a strict GlobalMountConfig wire-format struct that mirrors
MountConfig minus the isolation field, with deny_unknown_fields
so operators get a clear parse error if they try to set isolation
on a global mount. Isolation remains a workspace-mount concept.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(config): drop dead code and tighten global-mount API

- Delete unused From<MountConfig> for GlobalMountConfig (silently
  dropped isolation; no callers).
- Delete unused get_mut and remove on DockerMounts along with their
  #[allow(dead_code)] annotations.
- Tighten AppConfig::add_mount: debug_assert that incoming
  MountConfig has Shared isolation, and construct GlobalMountConfig
  explicitly from src/dst/readonly rather than via the (now-deleted)
  From impl. Keeps the public signature stable so callers in CLI,
  resolve.rs, and preview.rs don't need to change (Issue 2 option B).
- Add wire-path rejection test that goes through MountEntry's
  untagged enum and asserts on the actual serde error
  ("data did not match any variant of untagged enum MountEntry").
- Soften GlobalMountConfig's doc comment to reflect the actual
  serde error shape at the wire path.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(isolation): IsolationRecord + isolation.json IO

Atomic write via tmp+rename. Version-1 envelope leaves room for schema
evolution. Read/upsert/remove keyed by mount destination.

Co-authored-by: Claude <noreply@anthropic.com>

* test(isolation): drop redundant clones in state tests

Bring the warning baseline back to 87 after Task 2.1 introduced
two clippy::style hits (cloned_ref_to_slice_refs, redundant_clone).

Co-authored-by: Claude <noreply@anthropic.com>

* feat(isolation): list_records_for_workspace walks data dir

Used by workspace-edit drift detection to find which containers have
preserved isolated state for a given workspace.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(isolation): branch_name renderer with namespace + suffix support

Suffix is appended to the final selector segment so namespaced agents
keep their selector shape and the disambiguator goes on the leaf name.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(isolation): MaterializedWorkspace types

Third workspace shape (Config -> Resolved -> Materialized) used as the
runtime handoff into Docker launch.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(isolation): worktree_path_for derives on-disk path from mount dst

Uses dst verbatim (leading/trailing slashes stripped) under
isolated/, so the layout mirrors the container path.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(isolation): ensure_worktree_config_enabled

One-shot enabler for extensions.worktreeConfig on the host repo.
Bumps core.repositoryformatversion to 1 when needed.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(isolation): preflight checks for worktree materialization

Sensitive-mount, readonly, repo-root, and mid-operation guards.
Errors cite the mount destination and the worktree mode.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(isolation): dirty-host preflight gate with --force opt-out

Non-interactive load without --force rejects a dirty host tree.
Interactive contexts are expected to obtain ack upstream.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(isolation): materialize_workspace orchestrator

Per-mount worktree materialization with idempotent reuse, source-drift
guard, and branch-name disambiguation when multiple isolated mounts
target the same host repo.

Co-authored-by: Claude <noreply@anthropic.com>

* test(isolation): cover branch disambiguation for same-repo mounts

Co-authored-by: Claude <noreply@anthropic.com>

* feat(isolation): order Docker mounts parent-before-child

Length-ascending sort so shared cache children overlay isolated
worktree parents at container start.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(runtime): hook materialize_workspace between AgentState and Docker

Workspace mounts now flow Config -> Resolved -> Materialized before
reaching the docker run command, with parent-before-child ordering.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(isolation): force_cleanup_isolated removes worktree + branch + record

Best-effort git invocations that tolerate missing host repo and
already-removed worktree. Used by purge and the finalizer's force-delete
branch.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(isolation): finalizer skeleton + AttachOutcome shape

Decides Preserved when container still running, OOMed, or exited
non-zero. Clean-exit path stubbed - implemented in follow-ups.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(isolation): safe-cleanup deletes branches with no commits

When the worktree is clean and HEAD equals the recorded base, the
scratch branch is removed automatically.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(isolation): consult upstream when deciding safe cleanup

Pushed commits (reachable from upstream) are safe to delete; local-only
commits or no-upstream divergence preserve the worktree.

Co-authored-by: Claude <noreply@anthropic.com>

* test(isolation): cover interactive unsafe-cleanup prompt branches

Co-authored-by: Claude <noreply@anthropic.com>

* feat(runtime): finalize foreground session after attach in load + hardline

Both load and hardline now consult inspect_attach_outcome and dispatch
the shared finalizer. Return-to-agent retries safe cleanup once after
the operator returns.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(purge): refuse to run on a live container

Closes a pre-existing gap where purge could delete state out from
under a running agent. Operator must eject first.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(purge): remove isolated worktrees and scratch branches

Reads isolation.json and runs force_cleanup_isolated for each record
before deleting the per-container state directory.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(cli): --mount-isolation DST=TYPE on workspace create/edit

Repeatable. Rejects clone before persistence with the canonical
"reserved but not implemented yet" message.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(workspace): add Isolation column to workspace show

Renders canonical lowercase name for every mount so CLI output matches
TOML/CLI input verbatim.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(load): add --force to acknowledge dirty host tree

Required for non-interactive isolated-mount materialization when the
host working tree is dirty.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(workspace): detect source drift on edit affecting isolated mounts

Edits that change src for a mount with preserved isolated state are
rejected unless --delete-isolated-state is passed and no related
container is running.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(cli): jackin cd opens a child shell in an isolated worktree

Single-mount-no-dst → uses it. Dst-provided → exact match. Multi-mount
no-dst → interactive picker on TTY, error on non-TTY. Sets JACKIN_*
env vars. Does not modify the parent shell.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(console): show isolation badge per mount in editor + preview

Adds an Iso column to the workspace-manager mount table (editor and
list-pane sub-panel) and a `[shared|worktree|clone]` tag to the agent
preview's resolved-mounts lines. Per the per-mount-isolation spec the
badge renders the canonical spelling for every mount, including
`shared`, so operators always see which strategy applies.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(console): I hotkey cycles isolation on the selected mount

Mirrors the existing R (readonly) toggle. Cycles Shared -> Worktree
-> Shared; Clone is reserved-but-rejected in V1 and is not entered
through this hotkey, but a saved Clone mount snaps back to Shared on
the first I press rather than getting stuck. The cycling rule lives in
EditorState::cycle_isolation_for_selected_mount so the input dispatch
arm stays trivial.

Also surfaces the new key in the Mounts-tab footer hint alongside the
existing R toggle so the affordance is discoverable.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(console): source-drift confirm modal in workspace editor

Save flow runs the same drift detection as `jackin workspace edit`:
detect_workspace_edit_drift evaluates the prospective mount list
(post-collapse, post-upsert) against IsolationRecords on disk before
the on-disk write.

Running container drift -> ErrorPopup ("eject first"); save aborted.
Stopped container drift -> Confirm modal listing the affected
container names with a Yes/No prompt. On Yes the modal handler
re-stashes the plan with delete_isolated_acknowledged = true and the
second commit pass calls force_cleanup_isolated for each affected
record before writing.

Reduced scope vs the original three-button "Delete preserved state and
save / Cancel / Open mount details" dialog: the modal is the existing
two-button Confirm widget (Yes/No). The third "open mount details"
affordance is omitted — operators dismiss with N/Esc, find the
offending mount in the editor, and revert the src by hand. Adding it
would require either a custom widget or repurposing an existing
multi-choice one and threading mount-row focus through the modal
plumbing; the safety value is in the block-and-ack semantics, which
the two-button form covers.

Adds a commit_editor_save_with_runner test seam so the FakeRunner can
drive the drift branch without a real Docker daemon.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(workspaces): add per-mount isolation section

Document the per-mount isolation feature in the workspaces guide:
the three modes (shared default, worktree, clone reserved-but-rejected),
validation preconditions, the isolated-source + shared-cache child
pattern with TOML, and pointers to --mount-isolation and jackin cd.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(mounts): document mount isolation field

Add an "Mount isolation" section to the mounts guide covering the
shared/worktree values, the global-mount rejection at parse time,
and the isolated-source + shared-cache child composition pattern.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(configuration): add MountConfig.isolation field

Document the new mounts[].isolation field in the configuration
reference: shared default, worktree opt-in, clone reserved-but-rejected,
and the global-mount parse-time rejection.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(architecture): document materialization flow + isolation.json

Add a "Workspace materialization" section to the architecture reference
covering the WorkspaceConfig -> ResolvedWorkspace -> MaterializedWorkspace
shapes, the per-container isolation.json layout, and the post-attach
foreground finalizer's Preserved/Cleaned/ReturnToAgent decision matrix.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(workspace): document --mount-isolation and Isolation column

Add --mount-isolation to workspace create/edit option tables (with the
clone "planned but not implemented" note), document the new
--delete-isolated-state flag for non-interactive source-drift edits,
and note the Isolation column on workspace show.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(load): document --force dirty-host acknowledgement

Add --force to the option table and a dedicated section explaining
when it's required (non-interactive load with a worktree-isolated
mount + dirty host tree) and what it does NOT do (no stash, no
discard, no relaxation of other validation).

Co-authored-by: Claude <noreply@anthropic.com>

* docs(purge): document running-agent guard and isolated cleanup

Document purge's new behavior: refuses to run on a running container
(eject first), force-removes isolated worktrees + scratch branches
recorded in isolation.json, and tolerates a missing host repo on
best-effort cleanup.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(cd): add jackin cd command reference

Create a reference page for jackin cd <container> [dst] covering
arguments, the mount-selection behavior matrix (zero/one/many isolated
records, with and without dst), the JACKIN_* env vars set in the
child shell, exit-code passthrough, and the no-parent-mutation
guarantee. Wire it into the Commands sidebar between console and
launch.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): mark per-mount isolation V1 implemented

Flip the per-mount-isolation roadmap status to "Implemented in V1"
and replace the duplicate-mounts-allowed line with the actual V1
rule: multiple isolated mounts are allowed (with branch-name
disambiguation), but nested isolated dst paths are rejected at
validation because the inner worktree's .git would land inside the
outer worktree's tree.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(structure): add isolation module tree and cd command

Update PROJECT_STRUCTURE.md to document the new per-mount-isolation
work: isolation/ module row (mod/branch/materialize/state/finalize/
cleanup), cli/cd.rs entry on the cli/ row, --mount-isolation /
--delete-isolated-state / --force notes on the relevant CLI rows,
foreground-finalizer mention on the runtime row, the new
commands/cd.mdx in the docs map, and a code->docs cross-reference
row mapping src/isolation/** to all the doc pages it touches.

Co-authored-by: Claude <noreply@anthropic.com>

* test(isolation): end-to-end materialize -> clean-exit -> cleanup

Exercises the full lifecycle through public APIs with a small inline
scripted runner. No real git or docker.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(isolation): note finalizer is local-only for hardline lockdown

Co-authored-by: Claude <noreply@anthropic.com>

* style(test): apply rustfmt to per-mount isolation e2e

Co-authored-by: Claude <noreply@anthropic.com>

* ci(docs): verify docs links in PRs and on the deployed site (#173)

* ci(docs): add link checking


* ci(docs): stabilize lychee checks


* ci(docs): validate edit links with lychee


* ci(docs): close link check gaps


* docs: add repo file link component


* docs: explain repo link source check


* ci(docs): allow manual dispatch of deployed link check

Two refinements from PR review:

- The check-deployed job now triggers on workflow_dispatch in addition
  to schedule, so maintainers can manually verify the live deployed
  docs without waiting for the daily cron or pushing to main. This
  closes the gap against goal "manual workflow to verify both built
  site and deployed documentation".

- Drop github.sha from the deploy job's lychee cache primary key so it
  matches across runs (the SHA-keyed primary was guaranteed to miss,
  forcing fallback to restore-keys). Now mirrors the cache key shape
  used by check-deployed.

Co-authored-by: Claude <noreply@anthropic.com>

* ci(docs): harden link-check workflow and broaden source lint

Address review feedback on PR #173.

Workflow (.github/workflows/docs.yml):
- Split concurrency group by event_name so a scheduled or
  workflow_dispatch run cannot cancel an in-flight push deploy. Cancel
  in-progress is now scoped to pull_request only.
- Exclude main from workflow_dispatch on check-deployed. The deploy job
  already verifies the just-deployed site, so running both in parallel
  would race against the publish window. Manual verification of the live
  site from main flows through deploy; from feature branches it flows
  through check-deployed.
- Add the build cache as a fallback restore-key for check-deployed so
  the daily cron and manual runs warm-start from the last build cache
  when the lychee.toml fingerprint changes.

RepoFile component (docs/src/components/RepoFile.astro):
- Add target=_blank and rel=noopener noreferrer so GitHub source links
  open in a new tab, matching the behavior rehype-external-links applies
  to plain markdown external links in MDX.

Source lint (docs/scripts/check-repo-links.ts):
- Normalize leading slashes when checking RepoFile path existence so the
  validator agrees with the component's own normalization.
- Cover top-level repo-specific files (Cargo.toml, Cargo.lock, Justfile,
  build.rs, docker-bake.hcl, mise.toml, release.toml, renovate.json) so
  a rename of any of those also breaks the docs CI gate, not only paths
  under src/, docs/, docker/, .github/.

Content (docs/src/content/docs/developing/construct-image.mdx):
- Convert the remaining inline-code references to docker-bake.hcl and
  Justfile to <RepoFile />. These were the references that motivated
  extending the lint to top-level files.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): apply RepoFile lint to multi-runtime proposal

After merging main into codex/docs-link-checks, check-repo-links flagged
37 plain inline-code references to existing repo files in the new
multi-runtime-support.mdx (added in #174 before this PR's lint existed
on main). Convert each one to <RepoFile /> so renames or deletions of
those source files break the docs gate before merge, the same way the
rest of the roadmap is now protected.

No prose changes — every conversion is one-for-one (`src/foo.rs` →
<RepoFile path="src/foo.rs" />). The trailing-slash directory reference
to `docker/runtime/` is left as a code span, since the lint correctly
skips it (it's a directory, not a file).

Co-authored-by: Claude <noreply@anthropic.com>

* fix(roadmap): repair broken Amp CLI link in multi-runtime proposal

CI failed on this PR's last build because lychee found a 404 on
https://github.com/sourcegraph/amp in multi-runtime-support.mdx (added
in #174). That repo does not exist publicly — Amp's source is not on
GitHub.

Point the link at https://ampcode.com instead, which is already the
canonical Amp URL used elsewhere in the docs (getting-started/why.mdx).

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
(cherry picked from commit f3f3e5e)

* docs(roadmap): wrap src/cli/cd.rs in <RepoFile> on per-mount-isolation page

The check-repo-links script (added in #173) flags any inline-code
reference to a real repo file that isn't wrapped in <RepoFile />.
src/cli/cd.rs was created on this branch, so once #173's lint reaches
this branch via the previous cherry-pick, the bare reference fails
the Docs CI check.

Co-authored-by: Claude <noreply@anthropic.com>

* docs: switch cross-doc links to absolute URL form

The link-check job's lychee step (added on main in #173, hardened in
#176) verifies built-site links against the on-disk dist tree. Relative
`.mdx`-suffixed links break that check because lychee resolves them as
literal file paths under the rendered URL's directory — e.g.
`./workspaces.mdx` rendered from `/guides/mounts/` resolves to
`/guides/mounts/workspaces.mdx`, not `/guides/workspaces/`.

Switch the four cross-doc links added by the per-mount-isolation work
to the rendered-URL form (`/guides/workspaces/#per-mount-isolation`
etc.) — same convention as the existing `[mount collapse](/commands/workspace/#mount-collapse)`
link in the same file.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(isolation): emit verbose debug-mode trace for worktree lifecycle

Operators sharing logs to debug worktree behavior had no visibility into
the lifecycle — only three error/warning sites fired output, and none of
them ran on the happy path. `--debug` toggled a display mode (preserve
scrollback, clear spinner) but was not a verbose-trace facility.

This adds a `debug_log!(category, fmt, ...)` macro in `src/tui/mod.rs`
that gates on the existing `DEBUG_MODE` atomic so disabled call sites
cost only an atomic load (formatting is deferred behind the gate).
Output uses a `[jackin debug <category>]` prefix so shared logs are
greppable.

Instrumented sites (all under `category = "isolation"`):

- `materialize_workspace`: per-call summary (workspace, container,
  selector, mount counts, force/interactive flags).
- `materialize_one`: per-mount decision trail — drift detection,
  worktree reuse, preflight, base-commit lookup, branch derivation
  with selected suffix, and the `git worktree add` invocation itself.
- `ensure_worktree_config_enabled`: every state transition (already
  enabled vs. bumping repositoryformatversion vs. flipping the flag)
  with the host repo path.
- `state.rs`: write_records (count + path), upsert_record (insert vs.
  replace), remove_record (drop vs. no-op).
- `cleanup.rs`: force_cleanup_isolated entry, the two git invocations,
  the rm -rf fallback, and the host-repo-missing skip path.
  purge_isolated_for_container per-container summary.
- `finalize.rs`: foreground-session entry with exit code/oom/interactive
  flags, the early-return path for non-clean exits, per-record cleanup
  assessment.
- `runtime/launch.rs`: load_agent's call into materialize_workspace.

Read paths (`read_records`, `read_record`) are intentionally NOT logged
— they fire on every invocation and would drown the log.

Manual verification (since binary-level stderr capture would need a new
test dependency):

    cargo run --release --bin jackin -- --debug load <agent>
    cargo run --release --bin jackin -- --debug workspace edit <ws> \
        --mount-isolation /workspace/proj=worktree
    cargo run --release --bin jackin -- --debug purge <container>

Each emits a chronologically ordered `[jackin debug isolation] ...`
trace covering every git invocation, isolation.json mutation, and
finalize decision — suitable for sharing in bug reports.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): rename Iso column to Isolation, count isolation flips as one change

Two related TUI papercuts surfaced together when an operator flipped a
mount's isolation from `shared` to `worktree` via the `I` hotkey:

1. The mount-table header read `Iso` — opaque on first sight. Replaced
   with `Isolation` (the full word). Bumped the column-width constant
   from 8 → 9 so the header label fits without disturbing data-row
   alignment, and renamed `MOUNT_ISO_COL_WIDTH` →
   `MOUNT_ISOLATION_COL_WIDTH` for consistency. Updated the
   alignment-regression test that asserted on the old label.

2. Cycling isolation on an existing mount (same `dst`, same `src`)
   reported "2 changes" in the save-row footer and rendered the
   Confirm Save dialog with a `+`/`-` pair for the same path. Both
   sites used `MountConfig::contains()` — full-struct equality — so
   any isolation/readonly drift made the row appear as remove + add.

   Extracted a `MountDiff` classifier in `console/manager/state.rs`
   that keys on `dst` (the identity used by upsert/remove everywhere
   else). Same-`dst` matches with structural drift are now reported
   as a single `Modified`, counted as one change in `change_count`
   and rendered as a `~ <new>` line with a dimmed `was: <old>` follow-up
   in the Confirm Save summary so the operator sees exactly what
   changed without parsing a remove + add pair.

   Extended `mount_summary` to include the isolation tag so the
   delta is visible in both the new and old lines:
   `~/foo  (rw, worktree, github · main)`.

Records a new shared rule in `RULES.md` ("TUI Labels") to prevent
future short-form labels in user-facing TUI surfaces — operators
read the TUI in passing and cannot afford to decode `Iso`/`Cfg`/`Env`/
`WD`-style abbreviations. Lists the established short forms that are
NOT considered abbreviations (`dst`, `src`, `git`, `op`).

Co-authored-by: Claude <noreply@anthropic.com>

* fix(isolation): make worktree mode actually work inside the container

V1's worktree mode shipped with a gap: the materialized worktree was
bind-mounted into the container at <dst>, but the worktree's `.git`
text file (a pointer back to <host_repo>/.git/worktrees/<n>/) referenced
an absolute host path that didn't exist inside the container. Every git
command — `git status`, `git log`, `git commit`, `git push` — failed
with "fatal: not a git repository". The agent could read source files
but could not commit work, defeating worktree mode's whole purpose.

Fix: wire up three additional bind mounts at docker-run time, plus two
jackin-owned override files written at materialization, so git's gitdir
relationship resolves consistently inside the container without
modifying any host-side files.

For each isolated worktree, the container now sees:

1. The worktree at <dst> (existing).
2. The host repo's `.git/` at /jackin-isolation/<container>-git/ rw,
   so git can find objects, refs, and the per-worktree admin dir.
3. A jackin-owned `.git` text file at <dst>/.git overriding the
   worktree's host-side pointer with one targeting the container path.
4. A jackin-owned back-pointer at /jackin-isolation/<container>-git/
   worktrees/<n>/gitdir overriding git's verification check (host's
   absolute path doesn't match <dst> inside the container).

Override files live under <data_dir>/jackin-<container>/.git-overrides/
and are written once at materialize time. Host files (worktree's `.git`
and the admin dir's `gitdir`) are NEVER modified — host-side
`git worktree list` continues to work identically.

Three layouts were considered (Docker Sandboxes-style `.jackin/` in the
host repo, indirect mount with override files, jackin-owned bare repo).
The chosen approach preserves jackin's dst-based mount model (operator
configures dst=/workspace/jackin → agent works at that exact path), keeps
the host repo clean (no `.jackin/` directory), and exposes only the
worktree to the agent (not the entire host main tree). Full design
rationale and the comparison with Docker Sandboxes, Conductor, and
clone mode (planned for V1.1) are in
docs/src/content/docs/reference/roadmap/per-mount-isolation.mdx
under "Design Decision: Worktree Materialization Layout" and
"Comparison with Other Tools".

Trust trade-off: the agent has rw access to the host repo's `.git/`
since refs/objects are inherently shared in git worktrees. Worktree
mode is appropriate for trusted agents on personal projects where
immediate ref visibility on the host is valuable. Operators who want
ref isolation should use clone mode (planned).

Tests:
- write_git_overrides_writes_both_files_with_correct_content asserts
  override file content matches the design doc verbatim.
- write_git_overrides_is_idempotent confirms re-running on a reused
  worktree (load → eject → load) doesn't drift.
- override_id_strips_slashes_and_trims pins the file-naming scheme.
- container_git_dir_path_namespaces_by_container_name pins the
  hardcoded container-side path so two parallel agents don't collide.
- Extended per_mount_isolation_e2e to assert MaterializedMount carries
  WorktreeAuxMounts on the worktree path and that override files land
  on disk at the documented locations.

Manual verification recipe (add after running once):
  cargo run --release --bin jackin -- --debug load <agent>
  docker exec -ti <container> git status     # was failing, now works
  docker exec -ti <container> git log        # works
  docker exec -ti <container> git commit -m test --allow-empty
  git -C <host-repo> branch -a               # shows new branch

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(isolation): /jackin/{host,admin}/<dst> mounts, container-name basename, :ro hardening

Three related changes that finalize the worktree-mode mount layout:

1. Container-side path scheme renamed and reorganized.
   /jackin-isolation/<container>-git/...  →  /jackin/host/<dst-stripped>/.git
                                             /jackin/admin/<dst-stripped>/{commondir,gitdir}

   - Single top-level /jackin/ namespace for everything jackin contributes
     to the agent's filesystem (room to grow with /jackin/cache/, etc.)
   - host/ category mirrors host topology so docker inspect shows symmetric
     Source/Destination paths both ending in `.git`
   - admin/ category lives at a separate top level so the override files
     (which sit on top of files inside the admin dir) do NOT visually nest
     inside /jackin/host/.../.git/. Two top-level concerns, no overlap.

2. Host-side storage groups all git artifacts for one mount under
   <state>/git/<dst-stripped>/, with override-file names matching their
   docker mount destinations:

       <state>/git/<dst-stripped>/
       ├── <container>/    (the worktree; basename = container name)
       └── overrides/
           ├── .git
           ├── commondir
           └── gitdir

   Replaces the prior <state>/.git-overrides/ flat layout with underscored
   slug filenames. New layout uses dst as a real directory tree (no slug)
   and source filenames identical to destination filenames — the source/
   destination relationship is obvious in `docker inspect`.

3. Worktree subdir basename = container name. `git worktree add` derives
   the host-side admin entry name from the worktree path's basename (no
   --name flag exists upstream). Using the container name (which jackin
   guarantees is globally unique) makes admin entries in
   <host_repo>/.git/worktrees/ globally unique per (host_repo, container)
   — `git worktree list` on the host immediately shows which container
   owns each worktree.

   This required a new validation rule:
   `workspace::validate_isolation_layout` now rejects two isolated mounts
   that resolve to the same host repository within one workspace.
   Allowing them would force the same container-name basename twice in
   one host repo's .git/worktrees/ namespace; no real operator workflow
   has surfaced for this case. Revisit if one does.

   Removes the now-dead suffix logic from materialize.rs:
   - `count_isolated_per_repo` (helper)
   - `canonicalize_or_clone` (helper)
   - `dst_to_branch_suffix` (in src/isolation/branch.rs — no callers left)
   The `branch_name` function keeps its optional suffix parameter for
   future clone-mode use; V1 worktree always passes None.

4. The three override files (replacement `.git` pointer, `commondir`,
   `gitdir` back-pointer) are mounted `:ro` as defensive hardening. Git
   only reads them during normal agent work, and a misbehaving agent
   could otherwise rewrite the gitdir pointer to redirect operations at
   a different repo entirely. The host `.git/` and admin mounts stay rw
   because git writes refs/objects/HEAD/index/logs there.

Tests:
- workspace::tests::isolation_layout_rejects_two_worktree_mounts_on_same_repo
- workspace::tests::isolation_layout_allows_different_host_repos_in_one_workspace
- materialize::tests::worktree_path_uses_container_name_as_basename
- materialize::tests::container_host_git_path_mirrors_dst_under_jackin_host
- materialize::tests::container_admin_path_lives_under_jackin_admin
- materialize::tests::host_and_admin_paths_disambiguate_per_mount_in_one_container
- materialize::tests::write_git_overrides_writes_three_files_with_correct_content
- materialize::tests::write_git_overrides_is_idempotent
- launch::tests::build_workspace_mount_strings_marks_overrides_readonly
  (asserts all 6 mounts in correct order with correct :ro placement)
- per_mount_isolation_e2e: updated for new path scheme + admin name

Removed:
- materialize::tests::two_isolated_mounts_same_repo_get_dst_suffixed_branches
  (case is now rejected at the workspace-validation level)

Roadmap MDX (per-mount-isolation.mdx):
- Container-side mount layout section: 4 mounts → 6, new path scheme,
  override-file storage layout
- Composition Rules: documents the new same-host-repo rejection
- Comparison table: bind mount count for jackin worktree updated 4 → 6
- V1 Scope: ship list updated with new layout and the new validation rule

Manual verification (after merge):
  cargo run --release --bin jackin -- --debug load <agent> <workspace>
  docker inspect <container> | jq '.[0].Mounts'   # see /jackin/{host,admin}/...
  docker exec -w <dst> <container> git status     # works
  git -C <host_repo> worktree list                # admin name = <container>

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(isolation): single /jackin/host/ root, no commondir override, Model B branch naming

Final V1 design after extended brainstorming with the operator. Drops
the `/jackin/admin/<dst>` namespace and the `commondir` override file:
the per-worktree admin entry now lives natively at
`worktrees/<container>/` inside the host `.git/` mount, so git's
on-disk default `commondir = ../..` resolves correctly without an
override.

Container-side topology, per isolated mount (4 binds total, down from 6):
- `<dst>` (rw) — the materialized worktree
- `/jackin/host/<dst-tree>/.git` (rw) — host repo's `.git/`
- `<dst>/.git` (`:ro`) — replacement gitdir pointer
- `/jackin/host/<dst-tree>/.git/worktrees/<container>/gitdir` (`:ro`)
  — replacement back-pointer

Host-side layout under each per-container state dir:
- `git/worktree/repo/<dst-tree>/<container>/` — git's territory
- `git/overrides/<dst-tree>/{.git,gitdir}` — jackin-owned overrides

Branch naming follows Model B: `jackin/scratch/<container_name>`
verbatim. Admin entry name = container name (deterministic, globally
unique because container names are workspace-unique and
`validate_isolation_layout` rejects two isolated mounts on the same
host repo within one workspace — no auto-suffix or read-back needed).

Roadmap doc updated to reflect the final design.

Co-authored-by: Claude <noreply@anthropic.com>

* style(isolation): rustfmt assert_eq! width

Co-authored-by: Claude <noreply@anthropic.com>

* docs(isolation): align stale references with shipped V1 design

Sweep stale references across roadmap, guides, command and architecture
docs into alignment with what the code actually does. No content added —
the doc just told two contradictory stories before (Model B branch
naming alongside the old selector-key derivation; the new
git/worktree/repo/<dst>/<container>/ on-disk layout alongside the
proposed-but-never-implemented isolated/<slug>/ layout). Now there is
one consistent story end-to-end.

Touches:
- docs/src/content/docs/reference/roadmap/per-mount-isolation.mdx
- docs/src/content/docs/guides/workspaces.mdx
- docs/src/content/docs/commands/purge.mdx
- docs/src/content/docs/reference/architecture.mdx

Co-authored-by: Claude <noreply@anthropic.com>

* chore(cli): remove jackin cd command from V1

Operationally redundant with `git worktree list` + native shell `cd`.
For worktree mode, the host's `git worktree list` already enumerates
every isolated worktree by branch and absolute path, so a plain
`cd $(...)` reaches the same destination. The remaining edge cases
(preserved-dirty inspection, multi-mount picker) are rare enough that a
dedicated subcommand is net cost rather than net benefit.

Removes:
- src/cli/cd.rs (CdArgs + select_record + tests)
- Command::Cd enum variant in src/cli/mod.rs
- handle_cd dispatch in src/app/mod.rs
- docs/src/content/docs/commands/cd.mdx and its sidebar entry

Updates:
- src/isolation/finalize.rs preserved-state warning: drop "jackin cd ..."
  hint, point operator to the printed worktree path instead
- src/isolation/materialize.rs source-drift error: same treatment
- guides/workspaces.mdx + reference/architecture.mdx: drop cd references
- roadmap entry: replace "Convenience navigation" paragraph with a
  removed-from-V1 note explaining the rationale; add cd to the Defer
  list so it's recoverable if a real workflow surfaces

isolation.json schema, the preserved-state machinery, and `hardline` /
`purge` flows are unaffected — only the inspection convenience layer
is gone.

Co-authored-by: Claude <noreply@anthropic.com>

* chore(isolation): drop clone from V1 enum/parser/CLI

Per principle: don't pre-add API for unimplemented features. The `clone`
keyword was previously parsed by TOML/CLI and then rejected at validation
with a "planned but not implemented yet" error. Operators got false
positives in linting tools and confusing late failures with no benefit —
nothing in V1 actually does anything useful with the value.

Removes from the runtime:
- MountIsolation::Clone enum variant (src/isolation/mod.rs)
- explicit Clone-rejection in parse_mount_isolation (src/cli/workspace.rs):
  FromStr now produces "invalid isolation `clone`" naturally
- MountIsolation::Clone match arm in materialize_workspace
  (src/isolation/materialize.rs)
- console comments referencing the reserved-but-rejected wording

Tests:
- New `rejects_clone_until_implemented` test on FromStr asserts the
  standard "invalid isolation `clone`; expected one of: shared, worktree"
  error so this stays locked down
- parse_mount_isolation_rejects_clone updated to assert the new error
  shape

Docs:
- guides/workspaces.mdx, commands/workspace.mdx,
  reference/configuration.mdx, reference/roadmap.mdx: drop the
  "reserved keyword" wording, point at the V1.1 roadmap entry instead
- roadmap/per-mount-isolation.mdx: keep the `clone` design discussion,
  rephrase the V1 vocabulary section to make it explicit that the keyword
  is added back when clone mode ships, not pre-shipped now

apply_isolation_overrides already enforces "--mount-isolation must
reference an existing mount destination" (planner.rs); no change needed
for that requirement, just clarified in the roadmap CLI-behavior bullet.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(workspace): always write mount isolation field explicitly on save

Old configs without the `isolation` field still deserialize to Shared
(the enum default). On save, drop the `skip_serializing_if = is_shared`
guard so every mount writes its isolation level explicitly — including
`shared`. Old TOMLs migrate to the new shape on first save instead of
silently retaining their pre-isolation form.

Rationale: when the operator opens the saved config, every mount should
name its isolation level. No "field is missing therefore implicitly
shared" — the value is always present and the source of truth in the
file matches the source of truth at runtime.

Touches:
- src/workspace/mod.rs MountConfig.isolation: drop skip_serializing_if
- mount_config_omits_isolation_field_when_shared_on_serialize → renamed
  to mount_config_writes_isolation_field_even_when_shared_on_serialize,
  asserts the field IS written
- guides/mounts.mdx + reference/configuration.mdx: update wording

Co-authored-by: Claude <noreply@anthropic.com>

* docs: remove stale per-mount-isolation design spec

The brainstorming spec at docs/superpowers/specs/ predated the V1 design
iterations and no longer reflects what shipped (it still describes the
old `/jackin-isolation/` mount layout, the selector-key-based branch
naming, the reserved `clone` enum value, and `jackin cd`). The roadmap
entry at docs/src/content/docs/reference/roadmap/per-mount-isolation.mdx
is now the single source of truth.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): improve per-mount isolation entry — accurate Sandboxes
comparison, V1 overview, concrete clone-mode personas

Four targeted improvements to the per-mount-isolation roadmap doc:

1. Add a "How V1 worktree mode works (TL;DR)" overview at the top of
   Host-Side Materialization. 5-step walkthrough of the materialize +
   mount + commit flow before the deep-dive subsections, so readers
   land on the entry can understand the shipped V1 in 90 seconds
   without scrolling through the layout/lifecycle/etc.

2. Rewrite the Docker Sandboxes comparison for accuracy. The old
   description got it wrong on multiple points:
   - Sandboxes use a microVM with hypervisor isolation, not Docker
     containers (Linux namespaces) like jackin.
   - The host filesystem is exposed via filesystem passthrough at the
     SAME absolute path as the host, not a bind mount of the entire
     repo at "the same relative paths".
   - Worktree path is `<host_repo>/.sbx/<sandbox-name>-worktrees/<branch>/`
     (sandbox name is in the path), not `<host_repo>/.sbx/<branch>/`.
   - Sandboxes do NOT expose the host main working tree to the agent
     — only the worktree subdir and the parent `.git/`. Our table
     previously said "✓ (entire host repo mounted)".
   The architectural insight is now explicit: Sandboxes' absolute-path
   equivalence makes git's on-disk absolute pointers resolve natively,
   which is why they don't need override files. We pay that cost
   because Docker containers translate host paths to operator-chosen
   `dst` values, breaking absolute-path equivalence.

3. Concrete clone-mode operator personas. The previous description
   was abstract ("complementary mode for ref isolation"). Replaced
   with four named situations clone mode targets: untrusted/experimental
   agents, parallel-fan-out scratch-branch noise, editor watcher
   churn, and teams whose workflow is push-to-share anyway.

4. New "Sandbox runtime" and "Host file exposure" rows in the
   comparison table to make the underlying architectural choice
   immediately visible. Cross-referenced from the Sandboxes prose.

Net: ~80 lines changed/added, primarily replacement of the wrong
Docker Sandboxes facts and addition of the V1-overview block.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(isolation): close four data-loss windows in finalize/cleanup paths

Four merge-blocking issues surfaced by deep code review of PR #177.
Each was a silent-failure window that could destroy operator data on
the unhappy path while the happy-path manual smoke test stayed green.

1. assess_cleanup now treats every git capture failure as
   PreservedUnpushed (was: unwrap_or_default → empty string → could
   land in SafeToDelete). Without this, a transient `git rev-list`
   failure (corrupted pack mid-traversal, broken pipe under load,
   index.lock from a backgrounded git GC) would auto-delete the
   worktree and scratch branch, garbage-collecting unpushed commits.
   Each capture site now uses an explicit `match` that returns
   PreservedUnpushed with debug_log of the underlying error, plus a
   defense-in-depth empty-HEAD guard.

2. finalize_clean_exit now collects ALL preserved records and prompts
   per-record (was: needs_prompt.get_or_insert reached only the first
   one). On a multi-mount workspace where the operator chooses
   force-delete on the first prompt, the second preserved worktree was
   silently orphaned and the container torn down anyway — the only
   reconnection path (jackin hardline) was lost. Now each preserved
   record gets its own prompt; "return to agent" short-circuits the
   loop; "preserve" propagates as Preserved and skips container
   teardown.

3. inspect_attach_outcome now returns still_running() on docker capture
   failure (was: stopped(0) → entered finalize_clean_exit → could
   compound with #1 to delete worktrees of containers that may still
   be alive). The conservative direction is "preserve when we don't
   know" — `jackin hardline` recovers from there.

4. force_cleanup_isolated now verifies cleanup actually completed
   before removing the isolation.json record (was: let _ on git ops +
   unconditional remove_record → orphan worktree admin entries on the
   host repo and orphan branches with no jackin reference). Tolerates
   the idempotent paths (already-removed worktree, already-deleted
   branch verified absent via `git branch --list`); bails on real
   failures with a clear "record retained, re-run jackin purge after
   resolving the issue" message.

Test coverage:
- 5 new tests in isolation/finalize.rs pinning the assess_cleanup
  capture-failure → PreservedUnpushed contract for each git command
  in the assessment chain, plus the empty-HEAD guard.
- 3 new tests pinning the multi-record finalize path (force-delete-all,
  mixed force/preserve, non-interactive multi-mount warning).
- 1 new test for inspect_attach_outcome capture-failure fallback.
- 3 new cleanup tests (branch-already-deleted tolerance, real-failure
  retention, error-message contract).
- 1 new test for validate_workspace_config integration (catches the
  validate_isolation_layout call site if anyone refactors it away).
- 1 new test for build_workspace_mount_strings on a multi-mount
  isolated workspace (8 distinct binds, no path collisions, :ro
  hardening on every override file).
- 2 new drift-detection tests (dst-removed flagged; isolation-mode
  flips not-flagged with explanatory note for future improvement).

Net: 1170 → 1186 tests, 16 additions, all passing.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(isolation): close P1/P2/P3 — follow-on bugs from second review

Second deep code review of PR #177 surfaced three more issues after
the first round of merge-blocker fixes shipped:

P1. `force_cleanup_isolated` failure mid-loop in finalize_clean_exit
    propagated as Err via `?`, leaving the operator with a raw cleanup
    error from deep in finalize, no Preserved signal to the caller, the
    container left running without explicit teardown decision, and
    subsequent records in the loop never prompted. This regression was
    introduced by the round-1 multi-record loop fix (the single-record
    path always succeeded). Now caught per-record, eprintln'd as a
    warning, and treated as `any_preserved_after_prompt = true` so the
    loop continues and the caller gets `Preserved`.

P2. `inspect_attach_outcome` only treated `status == "running"` as
    still-alive. `paused | restarting | removing | created | dead` all
    fell through to `stopped(0)` → entered `finalize_clean_exit` →
    could auto-delete worktrees of containers that may resume any
    moment. Concrete: `docker pause jackin-x` while jackin re-attaches
    → status="paused" → SafeToDelete on a clean tree → operator
    unpauses to find the worktree gone. Replaced if-cascade with an
    explicit `match status` that only routes `exited` through stopped()
    and treats unknown status strings conservatively as still_running.

P3. `purge_isolated_for_container` swallowed per-record errors with
    eprintln warnings and returned `Ok(())`. Exacerbated by the
    round-1 fix #4 (force_cleanup_isolated now bails more often on
    real failures). Operator runs `jackin purge`, sees a warning
    scroll past, gets exit-code-0 prompt back, may believe purge
    completed. Now collects failures and surfaces an aggregate Err
    with the failed mount list so the exit code reflects reality.

Test coverage for these fixes:
- 2 new tests in finalize.rs: ReturnToAgent on the 2nd-of-3 prompt
  (early-return short-circuits), and force_cleanup_isolated failing
  mid-loop (loop continues, returns Preserved).
- 8 new tests in launch.rs covering every status code path:
  exited(0/non-zero/oom), running, paused, transient (restarting/
  removing/created), dead, unknown.
- 2 new tests in cleanup.rs: purge bails on partial failure,
  branch_still_present returning None proceeds (pins the doc-comment
  contract against future refactors to `unwrap_or(true)`).

Net: 1186 → 1198 tests, +12 additions, all passing. fmt/clippy clean.

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Codex <codex@openai.com>
donbeave added a commit that referenced this pull request May 7, 2026
* ci(docs): add link checking


* ci(docs): stabilize lychee checks


* ci(docs): validate edit links with lychee


* ci(docs): close link check gaps


* docs: add repo file link component


* docs: explain repo link source check


* ci(docs): allow manual dispatch of deployed link check

Two refinements from PR review:

- The check-deployed job now triggers on workflow_dispatch in addition
  to schedule, so maintainers can manually verify the live deployed
  docs without waiting for the daily cron or pushing to main. This
  closes the gap against goal "manual workflow to verify both built
  site and deployed documentation".

- Drop github.sha from the deploy job's lychee cache primary key so it
  matches across runs (the SHA-keyed primary was guaranteed to miss,
  forcing fallback to restore-keys). Now mirrors the cache key shape
  used by check-deployed.


* ci(docs): harden link-check workflow and broaden source lint

Address review feedback on PR #173.

Workflow (.github/workflows/docs.yml):
- Split concurrency group by event_name so a scheduled or
  workflow_dispatch run cannot cancel an in-flight push deploy. Cancel
  in-progress is now scoped to pull_request only.
- Exclude main from workflow_dispatch on check-deployed. The deploy job
  already verifies the just-deployed site, so running both in parallel
  would race against the publish window. Manual verification of the live
  site from main flows through deploy; from feature branches it flows
  through check-deployed.
- Add the build cache as a fallback restore-key for check-deployed so
  the daily cron and manual runs warm-start from the last build cache
  when the lychee.toml fingerprint changes.

RepoFile component (docs/src/components/RepoFile.astro):
- Add target=_blank and rel=noopener noreferrer so GitHub source links
  open in a new tab, matching the behavior rehype-external-links applies
  to plain markdown external links in MDX.

Source lint (docs/scripts/check-repo-links.ts):
- Normalize leading slashes when checking RepoFile path existence so the
  validator agrees with the component's own normalization.
- Cover top-level repo-specific files (Cargo.toml, Cargo.lock, Justfile,
  build.rs, docker-bake.hcl, mise.toml, release.toml, renovate.json) so
  a rename of any of those also breaks the docs CI gate, not only paths
  under src/, docs/, docker/, .github/.

Content (docs/src/content/docs/developing/construct-image.mdx):
- Convert the remaining inline-code references to docker-bake.hcl and
  Justfile to <RepoFile />. These were the references that motivated
  extending the lint to top-level files.


* docs(roadmap): apply RepoFile lint to multi-runtime proposal

After merging main into codex/docs-link-checks, check-repo-links flagged
37 plain inline-code references to existing repo files in the new
multi-runtime-support.mdx (added in #174 before this PR's lint existed
on main). Convert each one to <RepoFile /> so renames or deletions of
those source files break the docs gate before merge, the same way the
rest of the roadmap is now protected.

No prose changes — every conversion is one-for-one (`src/foo.rs` →
<RepoFile path="src/foo.rs" />). The trailing-slash directory reference
to `docker/runtime/` is left as a code span, since the lint correctly
skips it (it's a directory, not a file).


* fix(roadmap): repair broken Amp CLI link in multi-runtime proposal

CI failed on this PR's last build because lychee found a 404 on
https://github.com/sourcegraph/amp in multi-runtime-support.mdx (added
in #174). That repo does not exist publicly — Amp's source is not on
GitHub.

Point the link at https://ampcode.com instead, which is already the
canonical Amp URL used elsewhere in the docs (getting-started/why.mdx).


---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request May 7, 2026
)

* ci(docs): bump lychee to v0.24.0 to fix sitemap URL extraction

The first Docs workflow run on main after #173 (commit f3f3e5e) failed
in deploy → "Check deployed docs links" with "No files found for this
input source". Root cause: the previous step ran lychee --dump on the
deployed sitemap URL, but lychee 0.23.0 (the lycheeverse/lychee-action
v2 default) only extracts <a href> from HTML and matching patterns from
markdown — it does not parse <loc> entries from XML sitemaps. The dump
produced an empty list and the follow-up --files-from step had nothing
to read.

Upstream already fixed this. lycheeverse/lychee#2071 (merged
2026-03-13, tagged in v0.24.0 on 2026-04-24) adds <loc> extraction
from sitemap.xml, closing lycheeverse/lychee#2062 and #1819. Verified
locally on 0.24.0:

  $ lychee --version
  lychee 0.24.0
  $ lychee --dump https://jackin.tailrocks.com/sitemap-0.xml | wc -l
  45

Pin LYCHEE_VERSION at the workflow env level and reference it from
every lychee-action call so future bumps are one-line. v0.24.0's
breaking changes are in lychee-lib (the Rust API consumers); the CLI
surface we use is unchanged.


* ci(docs): bump lychee-action and lychee for sitemap URL extraction

Replace the previous v0.24.0 bump with the only combination that
actually works against the current lychee release pipeline:

  - lycheeverse/lychee-action SHA 8646ba3 (tagged v2.8.0) → faea714
    (post-v2.8.0 master). Adds subfolder-aware install needed for any
    lychee 0.24.x tarball.
  - LYCHEE_VERSION 'v0.24.0' → 'v0.24.1'.

Why both moves:

* lychee 0.24.0 added <loc> extraction from XML sitemaps
  (lycheeverse/lychee#2071), which is what the deploy and check-deployed
  jobs need to feed --files-from. lychee 0.23.0 dumps zero links from a
  sitemap, which is what produced the "No files found for this input
  source" failure on f3f3e5e.
* lychee 0.24.0's release tarball was repackaged with a top-level
  subfolder AND the asset filename was renamed to
  lychee-lychee-v0.24.0-{arch}-... — both incompatible with
  lychee-action v2.8.0's hardcoded download URL and flat-extract logic.
* lychee 0.24.1 (released the same day) reverted to the original asset
  filename but kept the subfolder layout AND kept the sitemap fix.
* lychee-action faea714 (unreleased; current HEAD of master) bumps the
  default to 0.24.1 and adds subfolder-aware install. Pinning the SHA
  is the same security model we already use for v2.8.0.

The combination 8646ba3 + 'latest' or 8646ba3 + 'v0.24.x' both fail.
The combination faea714 + 'v0.24.1' works.

Verified locally:

  $ lychee-v0.24.1/lychee --version
  lychee 0.24.1
  $ lychee-v0.24.1/lychee --dump https://jackin.tailrocks.com/sitemap-0.xml | wc -l
  45


* ci(docs): add TODO(lychee-action-sha-pin) marker

Companion to #179, which establishes the convention. Mark the spot
where the SHA pin needs to be reverted once lycheeverse/lychee-action
cuts a tagged release at or after faea714, with a back-link to the
tracked entry in TODO.md so a single grep finds both ends.


---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request May 7, 2026
* docs(spec): per-mount isolation V1 implementation spec

Captures roadmap → executable design: module layout under
src/isolation/, MountIsolation enum + MountConfig.isolation field,
materialization runtime hook, foreground finalizer with
safe/preserved/force cleanup, source-drift detection, jackin cd
command, TUI integration. Test list and docs touchpoints enumerated
for the implementation plan.


* feat(isolation): introduce MountIsolation enum


* feat(workspace): add isolation field to MountConfig

Defaults to Shared; serde skips emitting the field when Shared so
existing TOMLs round-trip unchanged.


* feat(workspace): reject nested isolated mounts

Two worktree-isolated mounts whose dsts nest have no safe on-disk
layout. Sibling isolated mounts and isolated-parent-with-shared-child
remain allowed.


* feat(config): reject isolation field on global mounts

Adds a strict GlobalMountConfig wire-format struct that mirrors
MountConfig minus the isolation field, with deny_unknown_fields
so operators get a clear parse error if they try to set isolation
on a global mount. Isolation remains a workspace-mount concept.


* refactor(config): drop dead code and tighten global-mount API

- Delete unused From<MountConfig> for GlobalMountConfig (silently
  dropped isolation; no callers).
- Delete unused get_mut and remove on DockerMounts along with their
  #[allow(dead_code)] annotations.
- Tighten AppConfig::add_mount: debug_assert that incoming
  MountConfig has Shared isolation, and construct GlobalMountConfig
  explicitly from src/dst/readonly rather than via the (now-deleted)
  From impl. Keeps the public signature stable so callers in CLI,
  resolve.rs, and preview.rs don't need to change (Issue 2 option B).
- Add wire-path rejection test that goes through MountEntry's
  untagged enum and asserts on the actual serde error
  ("data did not match any variant of untagged enum MountEntry").
- Soften GlobalMountConfig's doc comment to reflect the actual
  serde error shape at the wire path.


* feat(isolation): IsolationRecord + isolation.json IO

Atomic write via tmp+rename. Version-1 envelope leaves room for schema
evolution. Read/upsert/remove keyed by mount destination.


* test(isolation): drop redundant clones in state tests

Bring the warning baseline back to 87 after Task 2.1 introduced
two clippy::style hits (cloned_ref_to_slice_refs, redundant_clone).


* feat(isolation): list_records_for_workspace walks data dir

Used by workspace-edit drift detection to find which containers have
preserved isolated state for a given workspace.


* feat(isolation): branch_name renderer with namespace + suffix support

Suffix is appended to the final selector segment so namespaced agents
keep their selector shape and the disambiguator goes on the leaf name.


* feat(isolation): MaterializedWorkspace types

Third workspace shape (Config -> Resolved -> Materialized) used as the
runtime handoff into Docker launch.


* feat(isolation): worktree_path_for derives on-disk path from mount dst

Uses dst verbatim (leading/trailing slashes stripped) under
isolated/, so the layout mirrors the container path.


* feat(isolation): ensure_worktree_config_enabled

One-shot enabler for extensions.worktreeConfig on the host repo.
Bumps core.repositoryformatversion to 1 when needed.


* feat(isolation): preflight checks for worktree materialization

Sensitive-mount, readonly, repo-root, and mid-operation guards.
Errors cite the mount destination and the worktree mode.


* feat(isolation): dirty-host preflight gate with --force opt-out

Non-interactive load without --force rejects a dirty host tree.
Interactive contexts are expected to obtain ack upstream.


* feat(isolation): materialize_workspace orchestrator

Per-mount worktree materialization with idempotent reuse, source-drift
guard, and branch-name disambiguation when multiple isolated mounts
target the same host repo.


* test(isolation): cover branch disambiguation for same-repo mounts


* feat(isolation): order Docker mounts parent-before-child

Length-ascending sort so shared cache children overlay isolated
worktree parents at container start.


* feat(runtime): hook materialize_workspace between AgentState and Docker

Workspace mounts now flow Config -> Resolved -> Materialized before
reaching the docker run command, with parent-before-child ordering.


* feat(isolation): force_cleanup_isolated removes worktree + branch + record

Best-effort git invocations that tolerate missing host repo and
already-removed worktree. Used by purge and the finalizer's force-delete
branch.


* feat(isolation): finalizer skeleton + AttachOutcome shape

Decides Preserved when container still running, OOMed, or exited
non-zero. Clean-exit path stubbed - implemented in follow-ups.


* feat(isolation): safe-cleanup deletes branches with no commits

When the worktree is clean and HEAD equals the recorded base, the
scratch branch is removed automatically.


* feat(isolation): consult upstream when deciding safe cleanup

Pushed commits (reachable from upstream) are safe to delete; local-only
commits or no-upstream divergence preserve the worktree.


* test(isolation): cover interactive unsafe-cleanup prompt branches


* feat(runtime): finalize foreground session after attach in load + hardline

Both load and hardline now consult inspect_attach_outcome and dispatch
the shared finalizer. Return-to-agent retries safe cleanup once after
the operator returns.


* fix(purge): refuse to run on a live container

Closes a pre-existing gap where purge could delete state out from
under a running agent. Operator must eject first.


* feat(purge): remove isolated worktrees and scratch branches

Reads isolation.json and runs force_cleanup_isolated for each record
before deleting the per-container state directory.


* feat(cli): --mount-isolation DST=TYPE on workspace create/edit

Repeatable. Rejects clone before persistence with the canonical
"reserved but not implemented yet" message.


* feat(workspace): add Isolation column to workspace show

Renders canonical lowercase name for every mount so CLI output matches
TOML/CLI input verbatim.


* feat(load): add --force to acknowledge dirty host tree

Required for non-interactive isolated-mount materialization when the
host working tree is dirty.


* feat(workspace): detect source drift on edit affecting isolated mounts

Edits that change src for a mount with preserved isolated state are
rejected unless --delete-isolated-state is passed and no related
container is running.


* feat(cli): jackin cd opens a child shell in an isolated worktree

Single-mount-no-dst → uses it. Dst-provided → exact match. Multi-mount
no-dst → interactive picker on TTY, error on non-TTY. Sets JACKIN_*
env vars. Does not modify the parent shell.


* feat(console): show isolation badge per mount in editor + preview

Adds an Iso column to the workspace-manager mount table (editor and
list-pane sub-panel) and a `[shared|worktree|clone]` tag to the agent
preview's resolved-mounts lines. Per the per-mount-isolation spec the
badge renders the canonical spelling for every mount, including
`shared`, so operators always see which strategy applies.


* feat(console): I hotkey cycles isolation on the selected mount

Mirrors the existing R (readonly) toggle. Cycles Shared -> Worktree
-> Shared; Clone is reserved-but-rejected in V1 and is not entered
through this hotkey, but a saved Clone mount snaps back to Shared on
the first I press rather than getting stuck. The cycling rule lives in
EditorState::cycle_isolation_for_selected_mount so the input dispatch
arm stays trivial.

Also surfaces the new key in the Mounts-tab footer hint alongside the
existing R toggle so the affordance is discoverable.


* feat(console): source-drift confirm modal in workspace editor

Save flow runs the same drift detection as `jackin workspace edit`:
detect_workspace_edit_drift evaluates the prospective mount list
(post-collapse, post-upsert) against IsolationRecords on disk before
the on-disk write.

Running container drift -> ErrorPopup ("eject first"); save aborted.
Stopped container drift -> Confirm modal listing the affected
container names with a Yes/No prompt. On Yes the modal handler
re-stashes the plan with delete_isolated_acknowledged = true and the
second commit pass calls force_cleanup_isolated for each affected
record before writing.

Reduced scope vs the original three-button "Delete preserved state and
save / Cancel / Open mount details" dialog: the modal is the existing
two-button Confirm widget (Yes/No). The third "open mount details"
affordance is omitted — operators dismiss with N/Esc, find the
offending mount in the editor, and revert the src by hand. Adding it
would require either a custom widget or repurposing an existing
multi-choice one and threading mount-row focus through the modal
plumbing; the safety value is in the block-and-ack semantics, which
the two-button form covers.

Adds a commit_editor_save_with_runner test seam so the FakeRunner can
drive the drift branch without a real Docker daemon.


* docs(workspaces): add per-mount isolation section

Document the per-mount isolation feature in the workspaces guide:
the three modes (shared default, worktree, clone reserved-but-rejected),
validation preconditions, the isolated-source + shared-cache child
pattern with TOML, and pointers to --mount-isolation and jackin cd.


* docs(mounts): document mount isolation field

Add an "Mount isolation" section to the mounts guide covering the
shared/worktree values, the global-mount rejection at parse time,
and the isolated-source + shared-cache child composition pattern.


* docs(configuration): add MountConfig.isolation field

Document the new mounts[].isolation field in the configuration
reference: shared default, worktree opt-in, clone reserved-but-rejected,
and the global-mount parse-time rejection.


* docs(architecture): document materialization flow + isolation.json

Add a "Workspace materialization" section to the architecture reference
covering the WorkspaceConfig -> ResolvedWorkspace -> MaterializedWorkspace
shapes, the per-container isolation.json layout, and the post-attach
foreground finalizer's Preserved/Cleaned/ReturnToAgent decision matrix.


* docs(workspace): document --mount-isolation and Isolation column

Add --mount-isolation to workspace create/edit option tables (with the
clone "planned but not implemented" note), document the new
--delete-isolated-state flag for non-interactive source-drift edits,
and note the Isolation column on workspace show.


* docs(load): document --force dirty-host acknowledgement

Add --force to the option table and a dedicated section explaining
when it's required (non-interactive load with a worktree-isolated
mount + dirty host tree) and what it does NOT do (no stash, no
discard, no relaxation of other validation).


* docs(purge): document running-agent guard and isolated cleanup

Document purge's new behavior: refuses to run on a running container
(eject first), force-removes isolated worktrees + scratch branches
recorded in isolation.json, and tolerates a missing host repo on
best-effort cleanup.


* docs(cd): add jackin cd command reference

Create a reference page for jackin cd <container> [dst] covering
arguments, the mount-selection behavior matrix (zero/one/many isolated
records, with and without dst), the JACKIN_* env vars set in the
child shell, exit-code passthrough, and the no-parent-mutation
guarantee. Wire it into the Commands sidebar between console and
launch.


* docs(roadmap): mark per-mount isolation V1 implemented

Flip the per-mount-isolation roadmap status to "Implemented in V1"
and replace the duplicate-mounts-allowed line with the actual V1
rule: multiple isolated mounts are allowed (with branch-name
disambiguation), but nested isolated dst paths are rejected at
validation because the inner worktree's .git would land inside the
outer worktree's tree.


* docs(structure): add isolation module tree and cd command

Update PROJECT_STRUCTURE.md to document the new per-mount-isolation
work: isolation/ module row (mod/branch/materialize/state/finalize/
cleanup), cli/cd.rs entry on the cli/ row, --mount-isolation /
--delete-isolated-state / --force notes on the relevant CLI rows,
foreground-finalizer mention on the runtime row, the new
commands/cd.mdx in the docs map, and a code->docs cross-reference
row mapping src/isolation/** to all the doc pages it touches.


* test(isolation): end-to-end materialize -> clean-exit -> cleanup

Exercises the full lifecycle through public APIs with a small inline
scripted runner. No real git or docker.


* docs(isolation): note finalizer is local-only for hardline lockdown


* style(test): apply rustfmt to per-mount isolation e2e


* ci(docs): verify docs links in PRs and on the deployed site (#173)

* ci(docs): add link checking


* ci(docs): stabilize lychee checks


* ci(docs): validate edit links with lychee


* ci(docs): close link check gaps


* docs: add repo file link component


* docs: explain repo link source check


* ci(docs): allow manual dispatch of deployed link check

Two refinements from PR review:

- The check-deployed job now triggers on workflow_dispatch in addition
  to schedule, so maintainers can manually verify the live deployed
  docs without waiting for the daily cron or pushing to main. This
  closes the gap against goal "manual workflow to verify both built
  site and deployed documentation".

- Drop github.sha from the deploy job's lychee cache primary key so it
  matches across runs (the SHA-keyed primary was guaranteed to miss,
  forcing fallback to restore-keys). Now mirrors the cache key shape
  used by check-deployed.


* ci(docs): harden link-check workflow and broaden source lint

Address review feedback on PR #173.

Workflow (.github/workflows/docs.yml):
- Split concurrency group by event_name so a scheduled or
  workflow_dispatch run cannot cancel an in-flight push deploy. Cancel
  in-progress is now scoped to pull_request only.
- Exclude main from workflow_dispatch on check-deployed. The deploy job
  already verifies the just-deployed site, so running both in parallel
  would race against the publish window. Manual verification of the live
  site from main flows through deploy; from feature branches it flows
  through check-deployed.
- Add the build cache as a fallback restore-key for check-deployed so
  the daily cron and manual runs warm-start from the last build cache
  when the lychee.toml fingerprint changes.

RepoFile component (docs/src/components/RepoFile.astro):
- Add target=_blank and rel=noopener noreferrer so GitHub source links
  open in a new tab, matching the behavior rehype-external-links applies
  to plain markdown external links in MDX.

Source lint (docs/scripts/check-repo-links.ts):
- Normalize leading slashes when checking RepoFile path existence so the
  validator agrees with the component's own normalization.
- Cover top-level repo-specific files (Cargo.toml, Cargo.lock, Justfile,
  build.rs, docker-bake.hcl, mise.toml, release.toml, renovate.json) so
  a rename of any of those also breaks the docs CI gate, not only paths
  under src/, docs/, docker/, .github/.

Content (docs/src/content/docs/developing/construct-image.mdx):
- Convert the remaining inline-code references to docker-bake.hcl and
  Justfile to <RepoFile />. These were the references that motivated
  extending the lint to top-level files.


* docs(roadmap): apply RepoFile lint to multi-runtime proposal

After merging main into codex/docs-link-checks, check-repo-links flagged
37 plain inline-code references to existing repo files in the new
multi-runtime-support.mdx (added in #174 before this PR's lint existed
on main). Convert each one to <RepoFile /> so renames or deletions of
those source files break the docs gate before merge, the same way the
rest of the roadmap is now protected.

No prose changes — every conversion is one-for-one (`src/foo.rs` →
<RepoFile path="src/foo.rs" />). The trailing-slash directory reference
to `docker/runtime/` is left as a code span, since the lint correctly
skips it (it's a directory, not a file).


* fix(roadmap): repair broken Amp CLI link in multi-runtime proposal

CI failed on this PR's last build because lychee found a 404 on
https://github.com/sourcegraph/amp in multi-runtime-support.mdx (added
in #174). That repo does not exist publicly — Amp's source is not on
GitHub.

Point the link at https://ampcode.com instead, which is already the
canonical Amp URL used elsewhere in the docs (getting-started/why.mdx).


---------

(cherry picked from commit f3f3e5e)

* docs(roadmap): wrap src/cli/cd.rs in <RepoFile> on per-mount-isolation page

The check-repo-links script (added in #173) flags any inline-code
reference to a real repo file that isn't wrapped in <RepoFile />.
src/cli/cd.rs was created on this branch, so once #173's lint reaches
this branch via the previous cherry-pick, the bare reference fails
the Docs CI check.


* docs: switch cross-doc links to absolute URL form

The link-check job's lychee step (added on main in #173, hardened in
#176) verifies built-site links against the on-disk dist tree. Relative
`.mdx`-suffixed links break that check because lychee resolves them as
literal file paths under the rendered URL's directory — e.g.
`./workspaces.mdx` rendered from `/guides/mounts/` resolves to
`/guides/mounts/workspaces.mdx`, not `/guides/workspaces/`.

Switch the four cross-doc links added by the per-mount-isolation work
to the rendered-URL form (`/guides/workspaces/#per-mount-isolation`
etc.) — same convention as the existing `[mount collapse](/commands/workspace/#mount-collapse)`
link in the same file.


* feat(isolation): emit verbose debug-mode trace for worktree lifecycle

Operators sharing logs to debug worktree behavior had no visibility into
the lifecycle — only three error/warning sites fired output, and none of
them ran on the happy path. `--debug` toggled a display mode (preserve
scrollback, clear spinner) but was not a verbose-trace facility.

This adds a `debug_log!(category, fmt, ...)` macro in `src/tui/mod.rs`
that gates on the existing `DEBUG_MODE` atomic so disabled call sites
cost only an atomic load (formatting is deferred behind the gate).
Output uses a `[jackin debug <category>]` prefix so shared logs are
greppable.

Instrumented sites (all under `category = "isolation"`):

- `materialize_workspace`: per-call summary (workspace, container,
  selector, mount counts, force/interactive flags).
- `materialize_one`: per-mount decision trail — drift detection,
  worktree reuse, preflight, base-commit lookup, branch derivation
  with selected suffix, and the `git worktree add` invocation itself.
- `ensure_worktree_config_enabled`: every state transition (already
  enabled vs. bumping repositoryformatversion vs. flipping the flag)
  with the host repo path.
- `state.rs`: write_records (count + path), upsert_record (insert vs.
  replace), remove_record (drop vs. no-op).
- `cleanup.rs`: force_cleanup_isolated entry, the two git invocations,
  the rm -rf fallback, and the host-repo-missing skip path.
  purge_isolated_for_container per-container summary.
- `finalize.rs`: foreground-session entry with exit code/oom/interactive
  flags, the early-return path for non-clean exits, per-record cleanup
  assessment.
- `runtime/launch.rs`: load_agent's call into materialize_workspace.

Read paths (`read_records`, `read_record`) are intentionally NOT logged
— they fire on every invocation and would drown the log.

Manual verification (since binary-level stderr capture would need a new
test dependency):

    cargo run --release --bin jackin -- --debug load <agent>
    cargo run --release --bin jackin -- --debug workspace edit <ws> \
        --mount-isolation /workspace/proj=worktree
    cargo run --release --bin jackin -- --debug purge <container>

Each emits a chronologically ordered `[jackin debug isolation] ...`
trace covering every git invocation, isolation.json mutation, and
finalize decision — suitable for sharing in bug reports.


* fix(tui): rename Iso column to Isolation, count isolation flips as one change

Two related TUI papercuts surfaced together when an operator flipped a
mount's isolation from `shared` to `worktree` via the `I` hotkey:

1. The mount-table header read `Iso` — opaque on first sight. Replaced
   with `Isolation` (the full word). Bumped the column-width constant
   from 8 → 9 so the header label fits without disturbing data-row
   alignment, and renamed `MOUNT_ISO_COL_WIDTH` →
   `MOUNT_ISOLATION_COL_WIDTH` for consistency. Updated the
   alignment-regression test that asserted on the old label.

2. Cycling isolation on an existing mount (same `dst`, same `src`)
   reported "2 changes" in the save-row footer and rendered the
   Confirm Save dialog with a `+`/`-` pair for the same path. Both
   sites used `MountConfig::contains()` — full-struct equality — so
   any isolation/readonly drift made the row appear as remove + add.

   Extracted a `MountDiff` classifier in `console/manager/state.rs`
   that keys on `dst` (the identity used by upsert/remove everywhere
   else). Same-`dst` matches with structural drift are now reported
   as a single `Modified`, counted as one change in `change_count`
   and rendered as a `~ <new>` line with a dimmed `was: <old>` follow-up
   in the Confirm Save summary so the operator sees exactly what
   changed without parsing a remove + add pair.

   Extended `mount_summary` to include the isolation tag so the
   delta is visible in both the new and old lines:
   `~/foo  (rw, worktree, github · main)`.

Records a new shared rule in `RULES.md` ("TUI Labels") to prevent
future short-form labels in user-facing TUI surfaces — operators
read the TUI in passing and cannot afford to decode `Iso`/`Cfg`/`Env`/
`WD`-style abbreviations. Lists the established short forms that are
NOT considered abbreviations (`dst`, `src`, `git`, `op`).


* fix(isolation): make worktree mode actually work inside the container

V1's worktree mode shipped with a gap: the materialized worktree was
bind-mounted into the container at <dst>, but the worktree's `.git`
text file (a pointer back to <host_repo>/.git/worktrees/<n>/) referenced
an absolute host path that didn't exist inside the container. Every git
command — `git status`, `git log`, `git commit`, `git push` — failed
with "fatal: not a git repository". The agent could read source files
but could not commit work, defeating worktree mode's whole purpose.

Fix: wire up three additional bind mounts at docker-run time, plus two
jackin-owned override files written at materialization, so git's gitdir
relationship resolves consistently inside the container without
modifying any host-side files.

For each isolated worktree, the container now sees:

1. The worktree at <dst> (existing).
2. The host repo's `.git/` at /jackin-isolation/<container>-git/ rw,
   so git can find objects, refs, and the per-worktree admin dir.
3. A jackin-owned `.git` text file at <dst>/.git overriding the
   worktree's host-side pointer with one targeting the container path.
4. A jackin-owned back-pointer at /jackin-isolation/<container>-git/
   worktrees/<n>/gitdir overriding git's verification check (host's
   absolute path doesn't match <dst> inside the container).

Override files live under <data_dir>/jackin-<container>/.git-overrides/
and are written once at materialize time. Host files (worktree's `.git`
and the admin dir's `gitdir`) are NEVER modified — host-side
`git worktree list` continues to work identically.

Three layouts were considered (Docker Sandboxes-style `.jackin/` in the
host repo, indirect mount with override files, jackin-owned bare repo).
The chosen approach preserves jackin's dst-based mount model (operator
configures dst=/workspace/jackin → agent works at that exact path), keeps
the host repo clean (no `.jackin/` directory), and exposes only the
worktree to the agent (not the entire host main tree). Full design
rationale and the comparison with Docker Sandboxes, Conductor, and
clone mode (planned for V1.1) are in
docs/src/content/docs/reference/roadmap/per-mount-isolation.mdx
under "Design Decision: Worktree Materialization Layout" and
"Comparison with Other Tools".

Trust trade-off: the agent has rw access to the host repo's `.git/`
since refs/objects are inherently shared in git worktrees. Worktree
mode is appropriate for trusted agents on personal projects where
immediate ref visibility on the host is valuable. Operators who want
ref isolation should use clone mode (planned).

Tests:
- write_git_overrides_writes_both_files_with_correct_content asserts
  override file content matches the design doc verbatim.
- write_git_overrides_is_idempotent confirms re-running on a reused
  worktree (load → eject → load) doesn't drift.
- override_id_strips_slashes_and_trims pins the file-naming scheme.
- container_git_dir_path_namespaces_by_container_name pins the
  hardcoded container-side path so two parallel agents don't collide.
- Extended per_mount_isolation_e2e to assert MaterializedMount carries
  WorktreeAuxMounts on the worktree path and that override files land
  on disk at the documented locations.

Manual verification recipe (add after running once):
  cargo run --release --bin jackin -- --debug load <agent>
  docker exec -ti <container> git status     # was failing, now works
  docker exec -ti <container> git log        # works
  docker exec -ti <container> git commit -m test --allow-empty
  git -C <host-repo> branch -a               # shows new branch


* refactor(isolation): /jackin/{host,admin}/<dst> mounts, container-name basename, :ro hardening

Three related changes that finalize the worktree-mode mount layout:

1. Container-side path scheme renamed and reorganized.
   /jackin-isolation/<container>-git/...  →  /jackin/host/<dst-stripped>/.git
                                             /jackin/admin/<dst-stripped>/{commondir,gitdir}

   - Single top-level /jackin/ namespace for everything jackin contributes
     to the agent's filesystem (room to grow with /jackin/cache/, etc.)
   - host/ category mirrors host topology so docker inspect shows symmetric
     Source/Destination paths both ending in `.git`
   - admin/ category lives at a separate top level so the override files
     (which sit on top of files inside the admin dir) do NOT visually nest
     inside /jackin/host/.../.git/. Two top-level concerns, no overlap.

2. Host-side storage groups all git artifacts for one mount under
   <state>/git/<dst-stripped>/, with override-file names matching their
   docker mount destinations:

       <state>/git/<dst-stripped>/
       ├── <container>/    (the worktree; basename = container name)
       └── overrides/
           ├── .git
           ├── commondir
           └── gitdir

   Replaces the prior <state>/.git-overrides/ flat layout with underscored
   slug filenames. New layout uses dst as a real directory tree (no slug)
   and source filenames identical to destination filenames — the source/
   destination relationship is obvious in `docker inspect`.

3. Worktree subdir basename = container name. `git worktree add` derives
   the host-side admin entry name from the worktree path's basename (no
   --name flag exists upstream). Using the container name (which jackin
   guarantees is globally unique) makes admin entries in
   <host_repo>/.git/worktrees/ globally unique per (host_repo, container)
   — `git worktree list` on the host immediately shows which container
   owns each worktree.

   This required a new validation rule:
   `workspace::validate_isolation_layout` now rejects two isolated mounts
   that resolve to the same host repository within one workspace.
   Allowing them would force the same container-name basename twice in
   one host repo's .git/worktrees/ namespace; no real operator workflow
   has surfaced for this case. Revisit if one does.

   Removes the now-dead suffix logic from materialize.rs:
   - `count_isolated_per_repo` (helper)
   - `canonicalize_or_clone` (helper)
   - `dst_to_branch_suffix` (in src/isolation/branch.rs — no callers left)
   The `branch_name` function keeps its optional suffix parameter for
   future clone-mode use; V1 worktree always passes None.

4. The three override files (replacement `.git` pointer, `commondir`,
   `gitdir` back-pointer) are mounted `:ro` as defensive hardening. Git
   only reads them during normal agent work, and a misbehaving agent
   could otherwise rewrite the gitdir pointer to redirect operations at
   a different repo entirely. The host `.git/` and admin mounts stay rw
   because git writes refs/objects/HEAD/index/logs there.

Tests:
- workspace::tests::isolation_layout_rejects_two_worktree_mounts_on_same_repo
- workspace::tests::isolation_layout_allows_different_host_repos_in_one_workspace
- materialize::tests::worktree_path_uses_container_name_as_basename
- materialize::tests::container_host_git_path_mirrors_dst_under_jackin_host
- materialize::tests::container_admin_path_lives_under_jackin_admin
- materialize::tests::host_and_admin_paths_disambiguate_per_mount_in_one_container
- materialize::tests::write_git_overrides_writes_three_files_with_correct_content
- materialize::tests::write_git_overrides_is_idempotent
- launch::tests::build_workspace_mount_strings_marks_overrides_readonly
  (asserts all 6 mounts in correct order with correct :ro placement)
- per_mount_isolation_e2e: updated for new path scheme + admin name

Removed:
- materialize::tests::two_isolated_mounts_same_repo_get_dst_suffixed_branches
  (case is now rejected at the workspace-validation level)

Roadmap MDX (per-mount-isolation.mdx):
- Container-side mount layout section: 4 mounts → 6, new path scheme,
  override-file storage layout
- Composition Rules: documents the new same-host-repo rejection
- Comparison table: bind mount count for jackin worktree updated 4 → 6
- V1 Scope: ship list updated with new layout and the new validation rule

Manual verification (after merge):
  cargo run --release --bin jackin -- --debug load <agent> <workspace>
  docker inspect <container> | jq '.[0].Mounts'   # see /jackin/{host,admin}/...
  docker exec -w <dst> <container> git status     # works
  git -C <host_repo> worktree list                # admin name = <container>


* refactor(isolation): single /jackin/host/ root, no commondir override, Model B branch naming

Final V1 design after extended brainstorming with the operator. Drops
the `/jackin/admin/<dst>` namespace and the `commondir` override file:
the per-worktree admin entry now lives natively at
`worktrees/<container>/` inside the host `.git/` mount, so git's
on-disk default `commondir = ../..` resolves correctly without an
override.

Container-side topology, per isolated mount (4 binds total, down from 6):
- `<dst>` (rw) — the materialized worktree
- `/jackin/host/<dst-tree>/.git` (rw) — host repo's `.git/`
- `<dst>/.git` (`:ro`) — replacement gitdir pointer
- `/jackin/host/<dst-tree>/.git/worktrees/<container>/gitdir` (`:ro`)
  — replacement back-pointer

Host-side layout under each per-container state dir:
- `git/worktree/repo/<dst-tree>/<container>/` — git's territory
- `git/overrides/<dst-tree>/{.git,gitdir}` — jackin-owned overrides

Branch naming follows Model B: `jackin/scratch/<container_name>`
verbatim. Admin entry name = container name (deterministic, globally
unique because container names are workspace-unique and
`validate_isolation_layout` rejects two isolated mounts on the same
host repo within one workspace — no auto-suffix or read-back needed).

Roadmap doc updated to reflect the final design.


* style(isolation): rustfmt assert_eq! width


* docs(isolation): align stale references with shipped V1 design

Sweep stale references across roadmap, guides, command and architecture
docs into alignment with what the code actually does. No content added —
the doc just told two contradictory stories before (Model B branch
naming alongside the old selector-key derivation; the new
git/worktree/repo/<dst>/<container>/ on-disk layout alongside the
proposed-but-never-implemented isolated/<slug>/ layout). Now there is
one consistent story end-to-end.

Touches:
- docs/src/content/docs/reference/roadmap/per-mount-isolation.mdx
- docs/src/content/docs/guides/workspaces.mdx
- docs/src/content/docs/commands/purge.mdx
- docs/src/content/docs/reference/architecture.mdx


* chore(cli): remove jackin cd command from V1

Operationally redundant with `git worktree list` + native shell `cd`.
For worktree mode, the host's `git worktree list` already enumerates
every isolated worktree by branch and absolute path, so a plain
`cd $(...)` reaches the same destination. The remaining edge cases
(preserved-dirty inspection, multi-mount picker) are rare enough that a
dedicated subcommand is net cost rather than net benefit.

Removes:
- src/cli/cd.rs (CdArgs + select_record + tests)
- Command::Cd enum variant in src/cli/mod.rs
- handle_cd dispatch in src/app/mod.rs
- docs/src/content/docs/commands/cd.mdx and its sidebar entry

Updates:
- src/isolation/finalize.rs preserved-state warning: drop "jackin cd ..."
  hint, point operator to the printed worktree path instead
- src/isolation/materialize.rs source-drift error: same treatment
- guides/workspaces.mdx + reference/architecture.mdx: drop cd references
- roadmap entry: replace "Convenience navigation" paragraph with a
  removed-from-V1 note explaining the rationale; add cd to the Defer
  list so it's recoverable if a real workflow surfaces

isolation.json schema, the preserved-state machinery, and `hardline` /
`purge` flows are unaffected — only the inspection convenience layer
is gone.


* chore(isolation): drop clone from V1 enum/parser/CLI

Per principle: don't pre-add API for unimplemented features. The `clone`
keyword was previously parsed by TOML/CLI and then rejected at validation
with a "planned but not implemented yet" error. Operators got false
positives in linting tools and confusing late failures with no benefit —
nothing in V1 actually does anything useful with the value.

Removes from the runtime:
- MountIsolation::Clone enum variant (src/isolation/mod.rs)
- explicit Clone-rejection in parse_mount_isolation (src/cli/workspace.rs):
  FromStr now produces "invalid isolation `clone`" naturally
- MountIsolation::Clone match arm in materialize_workspace
  (src/isolation/materialize.rs)
- console comments referencing the reserved-but-rejected wording

Tests:
- New `rejects_clone_until_implemented` test on FromStr asserts the
  standard "invalid isolation `clone`; expected one of: shared, worktree"
  error so this stays locked down
- parse_mount_isolation_rejects_clone updated to assert the new error
  shape

Docs:
- guides/workspaces.mdx, commands/workspace.mdx,
  reference/configuration.mdx, reference/roadmap.mdx: drop the
  "reserved keyword" wording, point at the V1.1 roadmap entry instead
- roadmap/per-mount-isolation.mdx: keep the `clone` design discussion,
  rephrase the V1 vocabulary section to make it explicit that the keyword
  is added back when clone mode ships, not pre-shipped now

apply_isolation_overrides already enforces "--mount-isolation must
reference an existing mount destination" (planner.rs); no change needed
for that requirement, just clarified in the roadmap CLI-behavior bullet.


* fix(workspace): always write mount isolation field explicitly on save

Old configs without the `isolation` field still deserialize to Shared
(the enum default). On save, drop the `skip_serializing_if = is_shared`
guard so every mount writes its isolation level explicitly — including
`shared`. Old TOMLs migrate to the new shape on first save instead of
silently retaining their pre-isolation form.

Rationale: when the operator opens the saved config, every mount should
name its isolation level. No "field is missing therefore implicitly
shared" — the value is always present and the source of truth in the
file matches the source of truth at runtime.

Touches:
- src/workspace/mod.rs MountConfig.isolation: drop skip_serializing_if
- mount_config_omits_isolation_field_when_shared_on_serialize → renamed
  to mount_config_writes_isolation_field_even_when_shared_on_serialize,
  asserts the field IS written
- guides/mounts.mdx + reference/configuration.mdx: update wording


* docs: remove stale per-mount-isolation design spec

The brainstorming spec at docs/superpowers/specs/ predated the V1 design
iterations and no longer reflects what shipped (it still describes the
old `/jackin-isolation/` mount layout, the selector-key-based branch
naming, the reserved `clone` enum value, and `jackin cd`). The roadmap
entry at docs/src/content/docs/reference/roadmap/per-mount-isolation.mdx
is now the single source of truth.


* docs(roadmap): improve per-mount isolation entry — accurate Sandboxes
comparison, V1 overview, concrete clone-mode personas

Four targeted improvements to the per-mount-isolation roadmap doc:

1. Add a "How V1 worktree mode works (TL;DR)" overview at the top of
   Host-Side Materialization. 5-step walkthrough of the materialize +
   mount + commit flow before the deep-dive subsections, so readers
   land on the entry can understand the shipped V1 in 90 seconds
   without scrolling through the layout/lifecycle/etc.

2. Rewrite the Docker Sandboxes comparison for accuracy. The old
   description got it wrong on multiple points:
   - Sandboxes use a microVM with hypervisor isolation, not Docker
     containers (Linux namespaces) like jackin.
   - The host filesystem is exposed via filesystem passthrough at the
     SAME absolute path as the host, not a bind mount of the entire
     repo at "the same relative paths".
   - Worktree path is `<host_repo>/.sbx/<sandbox-name>-worktrees/<branch>/`
     (sandbox name is in the path), not `<host_repo>/.sbx/<branch>/`.
   - Sandboxes do NOT expose the host main working tree to the agent
     — only the worktree subdir and the parent `.git/`. Our table
     previously said "✓ (entire host repo mounted)".
   The architectural insight is now explicit: Sandboxes' absolute-path
   equivalence makes git's on-disk absolute pointers resolve natively,
   which is why they don't need override files. We pay that cost
   because Docker containers translate host paths to operator-chosen
   `dst` values, breaking absolute-path equivalence.

3. Concrete clone-mode operator personas. The previous description
   was abstract ("complementary mode for ref isolation"). Replaced
   with four named situations clone mode targets: untrusted/experimental
   agents, parallel-fan-out scratch-branch noise, editor watcher
   churn, and teams whose workflow is push-to-share anyway.

4. New "Sandbox runtime" and "Host file exposure" rows in the
   comparison table to make the underlying architectural choice
   immediately visible. Cross-referenced from the Sandboxes prose.

Net: ~80 lines changed/added, primarily replacement of the wrong
Docker Sandboxes facts and addition of the V1-overview block.


* fix(isolation): close four data-loss windows in finalize/cleanup paths

Four merge-blocking issues surfaced by deep code review of PR #177.
Each was a silent-failure window that could destroy operator data on
the unhappy path while the happy-path manual smoke test stayed green.

1. assess_cleanup now treats every git capture failure as
   PreservedUnpushed (was: unwrap_or_default → empty string → could
   land in SafeToDelete). Without this, a transient `git rev-list`
   failure (corrupted pack mid-traversal, broken pipe under load,
   index.lock from a backgrounded git GC) would auto-delete the
   worktree and scratch branch, garbage-collecting unpushed commits.
   Each capture site now uses an explicit `match` that returns
   PreservedUnpushed with debug_log of the underlying error, plus a
   defense-in-depth empty-HEAD guard.

2. finalize_clean_exit now collects ALL preserved records and prompts
   per-record (was: needs_prompt.get_or_insert reached only the first
   one). On a multi-mount workspace where the operator chooses
   force-delete on the first prompt, the second preserved worktree was
   silently orphaned and the container torn down anyway — the only
   reconnection path (jackin hardline) was lost. Now each preserved
   record gets its own prompt; "return to agent" short-circuits the
   loop; "preserve" propagates as Preserved and skips container
   teardown.

3. inspect_attach_outcome now returns still_running() on docker capture
   failure (was: stopped(0) → entered finalize_clean_exit → could
   compound with #1 to delete worktrees of containers that may still
   be alive). The conservative direction is "preserve when we don't
   know" — `jackin hardline` recovers from there.

4. force_cleanup_isolated now verifies cleanup actually completed
   before removing the isolation.json record (was: let _ on git ops +
   unconditional remove_record → orphan worktree admin entries on the
   host repo and orphan branches with no jackin reference). Tolerates
   the idempotent paths (already-removed worktree, already-deleted
   branch verified absent via `git branch --list`); bails on real
   failures with a clear "record retained, re-run jackin purge after
   resolving the issue" message.

Test coverage:
- 5 new tests in isolation/finalize.rs pinning the assess_cleanup
  capture-failure → PreservedUnpushed contract for each git command
  in the assessment chain, plus the empty-HEAD guard.
- 3 new tests pinning the multi-record finalize path (force-delete-all,
  mixed force/preserve, non-interactive multi-mount warning).
- 1 new test for inspect_attach_outcome capture-failure fallback.
- 3 new cleanup tests (branch-already-deleted tolerance, real-failure
  retention, error-message contract).
- 1 new test for validate_workspace_config integration (catches the
  validate_isolation_layout call site if anyone refactors it away).
- 1 new test for build_workspace_mount_strings on a multi-mount
  isolated workspace (8 distinct binds, no path collisions, :ro
  hardening on every override file).
- 2 new drift-detection tests (dst-removed flagged; isolation-mode
  flips not-flagged with explanatory note for future improvement).

Net: 1170 → 1186 tests, 16 additions, all passing.


* fix(isolation): close P1/P2/P3 — follow-on bugs from second review

Second deep code review of PR #177 surfaced three more issues after
the first round of merge-blocker fixes shipped:

P1. `force_cleanup_isolated` failure mid-loop in finalize_clean_exit
    propagated as Err via `?`, leaving the operator with a raw cleanup
    error from deep in finalize, no Preserved signal to the caller, the
    container left running without explicit teardown decision, and
    subsequent records in the loop never prompted. This regression was
    introduced by the round-1 multi-record loop fix (the single-record
    path always succeeded). Now caught per-record, eprintln'd as a
    warning, and treated as `any_preserved_after_prompt = true` so the
    loop continues and the caller gets `Preserved`.

P2. `inspect_attach_outcome` only treated `status == "running"` as
    still-alive. `paused | restarting | removing | created | dead` all
    fell through to `stopped(0)` → entered `finalize_clean_exit` →
    could auto-delete worktrees of containers that may resume any
    moment. Concrete: `docker pause jackin-x` while jackin re-attaches
    → status="paused" → SafeToDelete on a clean tree → operator
    unpauses to find the worktree gone. Replaced if-cascade with an
    explicit `match status` that only routes `exited` through stopped()
    and treats unknown status strings conservatively as still_running.

P3. `purge_isolated_for_container` swallowed per-record errors with
    eprintln warnings and returned `Ok(())`. Exacerbated by the
    round-1 fix #4 (force_cleanup_isolated now bails more often on
    real failures). Operator runs `jackin purge`, sees a warning
    scroll past, gets exit-code-0 prompt back, may believe purge
    completed. Now collects failures and surfaces an aggregate Err
    with the failed mount list so the exit code reflects reality.

Test coverage for these fixes:
- 2 new tests in finalize.rs: ReturnToAgent on the 2nd-of-3 prompt
  (early-return short-circuits), and force_cleanup_isolated failing
  mid-loop (loop continues, returns Preserved).
- 8 new tests in launch.rs covering every status code path:
  exited(0/non-zero/oom), running, paused, transient (restarting/
  removing/created), dead, unknown.
- 2 new tests in cleanup.rs: purge bails on partial failure,
  branch_still_present returning None proceeds (pins the doc-comment
  contract against future refactors to `unwrap_or(true)`).

Net: 1186 → 1198 tests, +12 additions, all passing. fmt/clippy clean.


---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
donbeave added a commit that referenced this pull request May 7, 2026
* ci(docs): add link checking


* ci(docs): stabilize lychee checks


* ci(docs): validate edit links with lychee


* ci(docs): close link check gaps


* docs: add repo file link component


* docs: explain repo link source check


* ci(docs): allow manual dispatch of deployed link check

Two refinements from PR review:

- The check-deployed job now triggers on workflow_dispatch in addition
  to schedule, so maintainers can manually verify the live deployed
  docs without waiting for the daily cron or pushing to main. This
  closes the gap against goal "manual workflow to verify both built
  site and deployed documentation".

- Drop github.sha from the deploy job's lychee cache primary key so it
  matches across runs (the SHA-keyed primary was guaranteed to miss,
  forcing fallback to restore-keys). Now mirrors the cache key shape
  used by check-deployed.


* ci(docs): harden link-check workflow and broaden source lint

Address review feedback on PR #173.

Workflow (.github/workflows/docs.yml):
- Split concurrency group by event_name so a scheduled or
  workflow_dispatch run cannot cancel an in-flight push deploy. Cancel
  in-progress is now scoped to pull_request only.
- Exclude main from workflow_dispatch on check-deployed. The deploy job
  already verifies the just-deployed site, so running both in parallel
  would race against the publish window. Manual verification of the live
  site from main flows through deploy; from feature branches it flows
  through check-deployed.
- Add the build cache as a fallback restore-key for check-deployed so
  the daily cron and manual runs warm-start from the last build cache
  when the lychee.toml fingerprint changes.

RepoFile component (docs/src/components/RepoFile.astro):
- Add target=_blank and rel=noopener noreferrer so GitHub source links
  open in a new tab, matching the behavior rehype-external-links applies
  to plain markdown external links in MDX.

Source lint (docs/scripts/check-repo-links.ts):
- Normalize leading slashes when checking RepoFile path existence so the
  validator agrees with the component's own normalization.
- Cover top-level repo-specific files (Cargo.toml, Cargo.lock, Justfile,
  build.rs, docker-bake.hcl, mise.toml, release.toml, renovate.json) so
  a rename of any of those also breaks the docs CI gate, not only paths
  under src/, docs/, docker/, .github/.

Content (docs/src/content/docs/developing/construct-image.mdx):
- Convert the remaining inline-code references to docker-bake.hcl and
  Justfile to <RepoFile />. These were the references that motivated
  extending the lint to top-level files.


* docs(roadmap): apply RepoFile lint to multi-runtime proposal

After merging main into codex/docs-link-checks, check-repo-links flagged
37 plain inline-code references to existing repo files in the new
multi-runtime-support.mdx (added in #174 before this PR's lint existed
on main). Convert each one to <RepoFile /> so renames or deletions of
those source files break the docs gate before merge, the same way the
rest of the roadmap is now protected.

No prose changes — every conversion is one-for-one (`src/foo.rs` →
<RepoFile path="src/foo.rs" />). The trailing-slash directory reference
to `docker/runtime/` is left as a code span, since the lint correctly
skips it (it's a directory, not a file).


* fix(roadmap): repair broken Amp CLI link in multi-runtime proposal

CI failed on this PR's last build because lychee found a 404 on
https://github.com/sourcegraph/amp in multi-runtime-support.mdx (added
in #174). That repo does not exist publicly — Amp's source is not on
GitHub.

Point the link at https://ampcode.com instead, which is already the
canonical Amp URL used elsewhere in the docs (getting-started/why.mdx).


---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request May 7, 2026
)

* ci(docs): bump lychee to v0.24.0 to fix sitemap URL extraction

The first Docs workflow run on main after #173 (commit f3f3e5e) failed
in deploy → "Check deployed docs links" with "No files found for this
input source". Root cause: the previous step ran lychee --dump on the
deployed sitemap URL, but lychee 0.23.0 (the lycheeverse/lychee-action
v2 default) only extracts <a href> from HTML and matching patterns from
markdown — it does not parse <loc> entries from XML sitemaps. The dump
produced an empty list and the follow-up --files-from step had nothing
to read.

Upstream already fixed this. lycheeverse/lychee#2071 (merged
2026-03-13, tagged in v0.24.0 on 2026-04-24) adds <loc> extraction
from sitemap.xml, closing lycheeverse/lychee#2062 and #1819. Verified
locally on 0.24.0:

  $ lychee --version
  lychee 0.24.0
  $ lychee --dump https://jackin.tailrocks.com/sitemap-0.xml | wc -l
  45

Pin LYCHEE_VERSION at the workflow env level and reference it from
every lychee-action call so future bumps are one-line. v0.24.0's
breaking changes are in lychee-lib (the Rust API consumers); the CLI
surface we use is unchanged.


* ci(docs): bump lychee-action and lychee for sitemap URL extraction

Replace the previous v0.24.0 bump with the only combination that
actually works against the current lychee release pipeline:

  - lycheeverse/lychee-action SHA 8646ba3 (tagged v2.8.0) → faea714
    (post-v2.8.0 master). Adds subfolder-aware install needed for any
    lychee 0.24.x tarball.
  - LYCHEE_VERSION 'v0.24.0' → 'v0.24.1'.

Why both moves:

* lychee 0.24.0 added <loc> extraction from XML sitemaps
  (lycheeverse/lychee#2071), which is what the deploy and check-deployed
  jobs need to feed --files-from. lychee 0.23.0 dumps zero links from a
  sitemap, which is what produced the "No files found for this input
  source" failure on f3f3e5e.
* lychee 0.24.0's release tarball was repackaged with a top-level
  subfolder AND the asset filename was renamed to
  lychee-lychee-v0.24.0-{arch}-... — both incompatible with
  lychee-action v2.8.0's hardcoded download URL and flat-extract logic.
* lychee 0.24.1 (released the same day) reverted to the original asset
  filename but kept the subfolder layout AND kept the sitemap fix.
* lychee-action faea714 (unreleased; current HEAD of master) bumps the
  default to 0.24.1 and adds subfolder-aware install. Pinning the SHA
  is the same security model we already use for v2.8.0.

The combination 8646ba3 + 'latest' or 8646ba3 + 'v0.24.x' both fail.
The combination faea714 + 'v0.24.1' works.

Verified locally:

  $ lychee-v0.24.1/lychee --version
  lychee 0.24.1
  $ lychee-v0.24.1/lychee --dump https://jackin.tailrocks.com/sitemap-0.xml | wc -l
  45


* ci(docs): add TODO(lychee-action-sha-pin) marker

Companion to #179, which establishes the convention. Mark the spot
where the SHA pin needs to be reverted once lycheeverse/lychee-action
cuts a tagged release at or after faea714, with a back-link to the
tracked entry in TODO.md so a single grep finds both ends.


---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request May 7, 2026
* docs(spec): per-mount isolation V1 implementation spec

Captures roadmap → executable design: module layout under
src/isolation/, MountIsolation enum + MountConfig.isolation field,
materialization runtime hook, foreground finalizer with
safe/preserved/force cleanup, source-drift detection, jackin cd
command, TUI integration. Test list and docs touchpoints enumerated
for the implementation plan.


* feat(isolation): introduce MountIsolation enum


* feat(workspace): add isolation field to MountConfig

Defaults to Shared; serde skips emitting the field when Shared so
existing TOMLs round-trip unchanged.


* feat(workspace): reject nested isolated mounts

Two worktree-isolated mounts whose dsts nest have no safe on-disk
layout. Sibling isolated mounts and isolated-parent-with-shared-child
remain allowed.


* feat(config): reject isolation field on global mounts

Adds a strict GlobalMountConfig wire-format struct that mirrors
MountConfig minus the isolation field, with deny_unknown_fields
so operators get a clear parse error if they try to set isolation
on a global mount. Isolation remains a workspace-mount concept.


* refactor(config): drop dead code and tighten global-mount API

- Delete unused From<MountConfig> for GlobalMountConfig (silently
  dropped isolation; no callers).
- Delete unused get_mut and remove on DockerMounts along with their
  #[allow(dead_code)] annotations.
- Tighten AppConfig::add_mount: debug_assert that incoming
  MountConfig has Shared isolation, and construct GlobalMountConfig
  explicitly from src/dst/readonly rather than via the (now-deleted)
  From impl. Keeps the public signature stable so callers in CLI,
  resolve.rs, and preview.rs don't need to change (Issue 2 option B).
- Add wire-path rejection test that goes through MountEntry's
  untagged enum and asserts on the actual serde error
  ("data did not match any variant of untagged enum MountEntry").
- Soften GlobalMountConfig's doc comment to reflect the actual
  serde error shape at the wire path.


* feat(isolation): IsolationRecord + isolation.json IO

Atomic write via tmp+rename. Version-1 envelope leaves room for schema
evolution. Read/upsert/remove keyed by mount destination.


* test(isolation): drop redundant clones in state tests

Bring the warning baseline back to 87 after Task 2.1 introduced
two clippy::style hits (cloned_ref_to_slice_refs, redundant_clone).


* feat(isolation): list_records_for_workspace walks data dir

Used by workspace-edit drift detection to find which containers have
preserved isolated state for a given workspace.


* feat(isolation): branch_name renderer with namespace + suffix support

Suffix is appended to the final selector segment so namespaced agents
keep their selector shape and the disambiguator goes on the leaf name.


* feat(isolation): MaterializedWorkspace types

Third workspace shape (Config -> Resolved -> Materialized) used as the
runtime handoff into Docker launch.


* feat(isolation): worktree_path_for derives on-disk path from mount dst

Uses dst verbatim (leading/trailing slashes stripped) under
isolated/, so the layout mirrors the container path.


* feat(isolation): ensure_worktree_config_enabled

One-shot enabler for extensions.worktreeConfig on the host repo.
Bumps core.repositoryformatversion to 1 when needed.


* feat(isolation): preflight checks for worktree materialization

Sensitive-mount, readonly, repo-root, and mid-operation guards.
Errors cite the mount destination and the worktree mode.


* feat(isolation): dirty-host preflight gate with --force opt-out

Non-interactive load without --force rejects a dirty host tree.
Interactive contexts are expected to obtain ack upstream.


* feat(isolation): materialize_workspace orchestrator

Per-mount worktree materialization with idempotent reuse, source-drift
guard, and branch-name disambiguation when multiple isolated mounts
target the same host repo.


* test(isolation): cover branch disambiguation for same-repo mounts


* feat(isolation): order Docker mounts parent-before-child

Length-ascending sort so shared cache children overlay isolated
worktree parents at container start.


* feat(runtime): hook materialize_workspace between AgentState and Docker

Workspace mounts now flow Config -> Resolved -> Materialized before
reaching the docker run command, with parent-before-child ordering.


* feat(isolation): force_cleanup_isolated removes worktree + branch + record

Best-effort git invocations that tolerate missing host repo and
already-removed worktree. Used by purge and the finalizer's force-delete
branch.


* feat(isolation): finalizer skeleton + AttachOutcome shape

Decides Preserved when container still running, OOMed, or exited
non-zero. Clean-exit path stubbed - implemented in follow-ups.


* feat(isolation): safe-cleanup deletes branches with no commits

When the worktree is clean and HEAD equals the recorded base, the
scratch branch is removed automatically.


* feat(isolation): consult upstream when deciding safe cleanup

Pushed commits (reachable from upstream) are safe to delete; local-only
commits or no-upstream divergence preserve the worktree.


* test(isolation): cover interactive unsafe-cleanup prompt branches


* feat(runtime): finalize foreground session after attach in load + hardline

Both load and hardline now consult inspect_attach_outcome and dispatch
the shared finalizer. Return-to-agent retries safe cleanup once after
the operator returns.


* fix(purge): refuse to run on a live container

Closes a pre-existing gap where purge could delete state out from
under a running agent. Operator must eject first.


* feat(purge): remove isolated worktrees and scratch branches

Reads isolation.json and runs force_cleanup_isolated for each record
before deleting the per-container state directory.


* feat(cli): --mount-isolation DST=TYPE on workspace create/edit

Repeatable. Rejects clone before persistence with the canonical
"reserved but not implemented yet" message.


* feat(workspace): add Isolation column to workspace show

Renders canonical lowercase name for every mount so CLI output matches
TOML/CLI input verbatim.


* feat(load): add --force to acknowledge dirty host tree

Required for non-interactive isolated-mount materialization when the
host working tree is dirty.


* feat(workspace): detect source drift on edit affecting isolated mounts

Edits that change src for a mount with preserved isolated state are
rejected unless --delete-isolated-state is passed and no related
container is running.


* feat(cli): jackin cd opens a child shell in an isolated worktree

Single-mount-no-dst → uses it. Dst-provided → exact match. Multi-mount
no-dst → interactive picker on TTY, error on non-TTY. Sets JACKIN_*
env vars. Does not modify the parent shell.


* feat(console): show isolation badge per mount in editor + preview

Adds an Iso column to the workspace-manager mount table (editor and
list-pane sub-panel) and a `[shared|worktree|clone]` tag to the agent
preview's resolved-mounts lines. Per the per-mount-isolation spec the
badge renders the canonical spelling for every mount, including
`shared`, so operators always see which strategy applies.


* feat(console): I hotkey cycles isolation on the selected mount

Mirrors the existing R (readonly) toggle. Cycles Shared -> Worktree
-> Shared; Clone is reserved-but-rejected in V1 and is not entered
through this hotkey, but a saved Clone mount snaps back to Shared on
the first I press rather than getting stuck. The cycling rule lives in
EditorState::cycle_isolation_for_selected_mount so the input dispatch
arm stays trivial.

Also surfaces the new key in the Mounts-tab footer hint alongside the
existing R toggle so the affordance is discoverable.


* feat(console): source-drift confirm modal in workspace editor

Save flow runs the same drift detection as `jackin workspace edit`:
detect_workspace_edit_drift evaluates the prospective mount list
(post-collapse, post-upsert) against IsolationRecords on disk before
the on-disk write.

Running container drift -> ErrorPopup ("eject first"); save aborted.
Stopped container drift -> Confirm modal listing the affected
container names with a Yes/No prompt. On Yes the modal handler
re-stashes the plan with delete_isolated_acknowledged = true and the
second commit pass calls force_cleanup_isolated for each affected
record before writing.

Reduced scope vs the original three-button "Delete preserved state and
save / Cancel / Open mount details" dialog: the modal is the existing
two-button Confirm widget (Yes/No). The third "open mount details"
affordance is omitted — operators dismiss with N/Esc, find the
offending mount in the editor, and revert the src by hand. Adding it
would require either a custom widget or repurposing an existing
multi-choice one and threading mount-row focus through the modal
plumbing; the safety value is in the block-and-ack semantics, which
the two-button form covers.

Adds a commit_editor_save_with_runner test seam so the FakeRunner can
drive the drift branch without a real Docker daemon.


* docs(workspaces): add per-mount isolation section

Document the per-mount isolation feature in the workspaces guide:
the three modes (shared default, worktree, clone reserved-but-rejected),
validation preconditions, the isolated-source + shared-cache child
pattern with TOML, and pointers to --mount-isolation and jackin cd.


* docs(mounts): document mount isolation field

Add an "Mount isolation" section to the mounts guide covering the
shared/worktree values, the global-mount rejection at parse time,
and the isolated-source + shared-cache child composition pattern.


* docs(configuration): add MountConfig.isolation field

Document the new mounts[].isolation field in the configuration
reference: shared default, worktree opt-in, clone reserved-but-rejected,
and the global-mount parse-time rejection.


* docs(architecture): document materialization flow + isolation.json

Add a "Workspace materialization" section to the architecture reference
covering the WorkspaceConfig -> ResolvedWorkspace -> MaterializedWorkspace
shapes, the per-container isolation.json layout, and the post-attach
foreground finalizer's Preserved/Cleaned/ReturnToAgent decision matrix.


* docs(workspace): document --mount-isolation and Isolation column

Add --mount-isolation to workspace create/edit option tables (with the
clone "planned but not implemented" note), document the new
--delete-isolated-state flag for non-interactive source-drift edits,
and note the Isolation column on workspace show.


* docs(load): document --force dirty-host acknowledgement

Add --force to the option table and a dedicated section explaining
when it's required (non-interactive load with a worktree-isolated
mount + dirty host tree) and what it does NOT do (no stash, no
discard, no relaxation of other validation).


* docs(purge): document running-agent guard and isolated cleanup

Document purge's new behavior: refuses to run on a running container
(eject first), force-removes isolated worktrees + scratch branches
recorded in isolation.json, and tolerates a missing host repo on
best-effort cleanup.


* docs(cd): add jackin cd command reference

Create a reference page for jackin cd <container> [dst] covering
arguments, the mount-selection behavior matrix (zero/one/many isolated
records, with and without dst), the JACKIN_* env vars set in the
child shell, exit-code passthrough, and the no-parent-mutation
guarantee. Wire it into the Commands sidebar between console and
launch.


* docs(roadmap): mark per-mount isolation V1 implemented

Flip the per-mount-isolation roadmap status to "Implemented in V1"
and replace the duplicate-mounts-allowed line with the actual V1
rule: multiple isolated mounts are allowed (with branch-name
disambiguation), but nested isolated dst paths are rejected at
validation because the inner worktree's .git would land inside the
outer worktree's tree.


* docs(structure): add isolation module tree and cd command

Update PROJECT_STRUCTURE.md to document the new per-mount-isolation
work: isolation/ module row (mod/branch/materialize/state/finalize/
cleanup), cli/cd.rs entry on the cli/ row, --mount-isolation /
--delete-isolated-state / --force notes on the relevant CLI rows,
foreground-finalizer mention on the runtime row, the new
commands/cd.mdx in the docs map, and a code->docs cross-reference
row mapping src/isolation/** to all the doc pages it touches.


* test(isolation): end-to-end materialize -> clean-exit -> cleanup

Exercises the full lifecycle through public APIs with a small inline
scripted runner. No real git or docker.


* docs(isolation): note finalizer is local-only for hardline lockdown


* style(test): apply rustfmt to per-mount isolation e2e


* ci(docs): verify docs links in PRs and on the deployed site (#173)

* ci(docs): add link checking


* ci(docs): stabilize lychee checks


* ci(docs): validate edit links with lychee


* ci(docs): close link check gaps


* docs: add repo file link component


* docs: explain repo link source check


* ci(docs): allow manual dispatch of deployed link check

Two refinements from PR review:

- The check-deployed job now triggers on workflow_dispatch in addition
  to schedule, so maintainers can manually verify the live deployed
  docs without waiting for the daily cron or pushing to main. This
  closes the gap against goal "manual workflow to verify both built
  site and deployed documentation".

- Drop github.sha from the deploy job's lychee cache primary key so it
  matches across runs (the SHA-keyed primary was guaranteed to miss,
  forcing fallback to restore-keys). Now mirrors the cache key shape
  used by check-deployed.


* ci(docs): harden link-check workflow and broaden source lint

Address review feedback on PR #173.

Workflow (.github/workflows/docs.yml):
- Split concurrency group by event_name so a scheduled or
  workflow_dispatch run cannot cancel an in-flight push deploy. Cancel
  in-progress is now scoped to pull_request only.
- Exclude main from workflow_dispatch on check-deployed. The deploy job
  already verifies the just-deployed site, so running both in parallel
  would race against the publish window. Manual verification of the live
  site from main flows through deploy; from feature branches it flows
  through check-deployed.
- Add the build cache as a fallback restore-key for check-deployed so
  the daily cron and manual runs warm-start from the last build cache
  when the lychee.toml fingerprint changes.

RepoFile component (docs/src/components/RepoFile.astro):
- Add target=_blank and rel=noopener noreferrer so GitHub source links
  open in a new tab, matching the behavior rehype-external-links applies
  to plain markdown external links in MDX.

Source lint (docs/scripts/check-repo-links.ts):
- Normalize leading slashes when checking RepoFile path existence so the
  validator agrees with the component's own normalization.
- Cover top-level repo-specific files (Cargo.toml, Cargo.lock, Justfile,
  build.rs, docker-bake.hcl, mise.toml, release.toml, renovate.json) so
  a rename of any of those also breaks the docs CI gate, not only paths
  under src/, docs/, docker/, .github/.

Content (docs/src/content/docs/developing/construct-image.mdx):
- Convert the remaining inline-code references to docker-bake.hcl and
  Justfile to <RepoFile />. These were the references that motivated
  extending the lint to top-level files.


* docs(roadmap): apply RepoFile lint to multi-runtime proposal

After merging main into codex/docs-link-checks, check-repo-links flagged
37 plain inline-code references to existing repo files in the new
multi-runtime-support.mdx (added in #174 before this PR's lint existed
on main). Convert each one to <RepoFile /> so renames or deletions of
those source files break the docs gate before merge, the same way the
rest of the roadmap is now protected.

No prose changes — every conversion is one-for-one (`src/foo.rs` →
<RepoFile path="src/foo.rs" />). The trailing-slash directory reference
to `docker/runtime/` is left as a code span, since the lint correctly
skips it (it's a directory, not a file).


* fix(roadmap): repair broken Amp CLI link in multi-runtime proposal

CI failed on this PR's last build because lychee found a 404 on
https://github.com/sourcegraph/amp in multi-runtime-support.mdx (added
in #174). That repo does not exist publicly — Amp's source is not on
GitHub.

Point the link at https://ampcode.com instead, which is already the
canonical Amp URL used elsewhere in the docs (getting-started/why.mdx).


---------

(cherry picked from commit f3f3e5e)

* docs(roadmap): wrap src/cli/cd.rs in <RepoFile> on per-mount-isolation page

The check-repo-links script (added in #173) flags any inline-code
reference to a real repo file that isn't wrapped in <RepoFile />.
src/cli/cd.rs was created on this branch, so once #173's lint reaches
this branch via the previous cherry-pick, the bare reference fails
the Docs CI check.


* docs: switch cross-doc links to absolute URL form

The link-check job's lychee step (added on main in #173, hardened in
#176) verifies built-site links against the on-disk dist tree. Relative
`.mdx`-suffixed links break that check because lychee resolves them as
literal file paths under the rendered URL's directory — e.g.
`./workspaces.mdx` rendered from `/guides/mounts/` resolves to
`/guides/mounts/workspaces.mdx`, not `/guides/workspaces/`.

Switch the four cross-doc links added by the per-mount-isolation work
to the rendered-URL form (`/guides/workspaces/#per-mount-isolation`
etc.) — same convention as the existing `[mount collapse](/commands/workspace/#mount-collapse)`
link in the same file.


* feat(isolation): emit verbose debug-mode trace for worktree lifecycle

Operators sharing logs to debug worktree behavior had no visibility into
the lifecycle — only three error/warning sites fired output, and none of
them ran on the happy path. `--debug` toggled a display mode (preserve
scrollback, clear spinner) but was not a verbose-trace facility.

This adds a `debug_log!(category, fmt, ...)` macro in `src/tui/mod.rs`
that gates on the existing `DEBUG_MODE` atomic so disabled call sites
cost only an atomic load (formatting is deferred behind the gate).
Output uses a `[jackin debug <category>]` prefix so shared logs are
greppable.

Instrumented sites (all under `category = "isolation"`):

- `materialize_workspace`: per-call summary (workspace, container,
  selector, mount counts, force/interactive flags).
- `materialize_one`: per-mount decision trail — drift detection,
  worktree reuse, preflight, base-commit lookup, branch derivation
  with selected suffix, and the `git worktree add` invocation itself.
- `ensure_worktree_config_enabled`: every state transition (already
  enabled vs. bumping repositoryformatversion vs. flipping the flag)
  with the host repo path.
- `state.rs`: write_records (count + path), upsert_record (insert vs.
  replace), remove_record (drop vs. no-op).
- `cleanup.rs`: force_cleanup_isolated entry, the two git invocations,
  the rm -rf fallback, and the host-repo-missing skip path.
  purge_isolated_for_container per-container summary.
- `finalize.rs`: foreground-session entry with exit code/oom/interactive
  flags, the early-return path for non-clean exits, per-record cleanup
  assessment.
- `runtime/launch.rs`: load_agent's call into materialize_workspace.

Read paths (`read_records`, `read_record`) are intentionally NOT logged
— they fire on every invocation and would drown the log.

Manual verification (since binary-level stderr capture would need a new
test dependency):

    cargo run --release --bin jackin -- --debug load <agent>
    cargo run --release --bin jackin -- --debug workspace edit <ws> \
        --mount-isolation /workspace/proj=worktree
    cargo run --release --bin jackin -- --debug purge <container>

Each emits a chronologically ordered `[jackin debug isolation] ...`
trace covering every git invocation, isolation.json mutation, and
finalize decision — suitable for sharing in bug reports.


* fix(tui): rename Iso column to Isolation, count isolation flips as one change

Two related TUI papercuts surfaced together when an operator flipped a
mount's isolation from `shared` to `worktree` via the `I` hotkey:

1. The mount-table header read `Iso` — opaque on first sight. Replaced
   with `Isolation` (the full word). Bumped the column-width constant
   from 8 → 9 so the header label fits without disturbing data-row
   alignment, and renamed `MOUNT_ISO_COL_WIDTH` →
   `MOUNT_ISOLATION_COL_WIDTH` for consistency. Updated the
   alignment-regression test that asserted on the old label.

2. Cycling isolation on an existing mount (same `dst`, same `src`)
   reported "2 changes" in the save-row footer and rendered the
   Confirm Save dialog with a `+`/`-` pair for the same path. Both
   sites used `MountConfig::contains()` — full-struct equality — so
   any isolation/readonly drift made the row appear as remove + add.

   Extracted a `MountDiff` classifier in `console/manager/state.rs`
   that keys on `dst` (the identity used by upsert/remove everywhere
   else). Same-`dst` matches with structural drift are now reported
   as a single `Modified`, counted as one change in `change_count`
   and rendered as a `~ <new>` line with a dimmed `was: <old>` follow-up
   in the Confirm Save summary so the operator sees exactly what
   changed without parsing a remove + add pair.

   Extended `mount_summary` to include the isolation tag so the
   delta is visible in both the new and old lines:
   `~/foo  (rw, worktree, github · main)`.

Records a new shared rule in `RULES.md` ("TUI Labels") to prevent
future short-form labels in user-facing TUI surfaces — operators
read the TUI in passing and cannot afford to decode `Iso`/`Cfg`/`Env`/
`WD`-style abbreviations. Lists the established short forms that are
NOT considered abbreviations (`dst`, `src`, `git`, `op`).


* fix(isolation): make worktree mode actually work inside the container

V1's worktree mode shipped with a gap: the materialized worktree was
bind-mounted into the container at <dst>, but the worktree's `.git`
text file (a pointer back to <host_repo>/.git/worktrees/<n>/) referenced
an absolute host path that didn't exist inside the container. Every git
command — `git status`, `git log`, `git commit`, `git push` — failed
with "fatal: not a git repository". The agent could read source files
but could not commit work, defeating worktree mode's whole purpose.

Fix: wire up three additional bind mounts at docker-run time, plus two
jackin-owned override files written at materialization, so git's gitdir
relationship resolves consistently inside the container without
modifying any host-side files.

For each isolated worktree, the container now sees:

1. The worktree at <dst> (existing).
2. The host repo's `.git/` at /jackin-isolation/<container>-git/ rw,
   so git can find objects, refs, and the per-worktree admin dir.
3. A jackin-owned `.git` text file at <dst>/.git overriding the
   worktree's host-side pointer with one targeting the container path.
4. A jackin-owned back-pointer at /jackin-isolation/<container>-git/
   worktrees/<n>/gitdir overriding git's verification check (host's
   absolute path doesn't match <dst> inside the container).

Override files live under <data_dir>/jackin-<container>/.git-overrides/
and are written once at materialize time. Host files (worktree's `.git`
and the admin dir's `gitdir`) are NEVER modified — host-side
`git worktree list` continues to work identically.

Three layouts were considered (Docker Sandboxes-style `.jackin/` in the
host repo, indirect mount with override files, jackin-owned bare repo).
The chosen approach preserves jackin's dst-based mount model (operator
configures dst=/workspace/jackin → agent works at that exact path), keeps
the host repo clean (no `.jackin/` directory), and exposes only the
worktree to the agent (not the entire host main tree). Full design
rationale and the comparison with Docker Sandboxes, Conductor, and
clone mode (planned for V1.1) are in
docs/src/content/docs/reference/roadmap/per-mount-isolation.mdx
under "Design Decision: Worktree Materialization Layout" and
"Comparison with Other Tools".

Trust trade-off: the agent has rw access to the host repo's `.git/`
since refs/objects are inherently shared in git worktrees. Worktree
mode is appropriate for trusted agents on personal projects where
immediate ref visibility on the host is valuable. Operators who want
ref isolation should use clone mode (planned).

Tests:
- write_git_overrides_writes_both_files_with_correct_content asserts
  override file content matches the design doc verbatim.
- write_git_overrides_is_idempotent confirms re-running on a reused
  worktree (load → eject → load) doesn't drift.
- override_id_strips_slashes_and_trims pins the file-naming scheme.
- container_git_dir_path_namespaces_by_container_name pins the
  hardcoded container-side path so two parallel agents don't collide.
- Extended per_mount_isolation_e2e to assert MaterializedMount carries
  WorktreeAuxMounts on the worktree path and that override files land
  on disk at the documented locations.

Manual verification recipe (add after running once):
  cargo run --release --bin jackin -- --debug load <agent>
  docker exec -ti <container> git status     # was failing, now works
  docker exec -ti <container> git log        # works
  docker exec -ti <container> git commit -m test --allow-empty
  git -C <host-repo> branch -a               # shows new branch


* refactor(isolation): /jackin/{host,admin}/<dst> mounts, container-name basename, :ro hardening

Three related changes that finalize the worktree-mode mount layout:

1. Container-side path scheme renamed and reorganized.
   /jackin-isolation/<container>-git/...  →  /jackin/host/<dst-stripped>/.git
                                             /jackin/admin/<dst-stripped>/{commondir,gitdir}

   - Single top-level /jackin/ namespace for everything jackin contributes
     to the agent's filesystem (room to grow with /jackin/cache/, etc.)
   - host/ category mirrors host topology so docker inspect shows symmetric
     Source/Destination paths both ending in `.git`
   - admin/ category lives at a separate top level so the override files
     (which sit on top of files inside the admin dir) do NOT visually nest
     inside /jackin/host/.../.git/. Two top-level concerns, no overlap.

2. Host-side storage groups all git artifacts for one mount under
   <state>/git/<dst-stripped>/, with override-file names matching their
   docker mount destinations:

       <state>/git/<dst-stripped>/
       ├── <container>/    (the worktree; basename = container name)
       └── overrides/
           ├── .git
           ├── commondir
           └── gitdir

   Replaces the prior <state>/.git-overrides/ flat layout with underscored
   slug filenames. New layout uses dst as a real directory tree (no slug)
   and source filenames identical to destination filenames — the source/
   destination relationship is obvious in `docker inspect`.

3. Worktree subdir basename = container name. `git worktree add` derives
   the host-side admin entry name from the worktree path's basename (no
   --name flag exists upstream). Using the container name (which jackin
   guarantees is globally unique) makes admin entries in
   <host_repo>/.git/worktrees/ globally unique per (host_repo, container)
   — `git worktree list` on the host immediately shows which container
   owns each worktree.

   This required a new validation rule:
   `workspace::validate_isolation_layout` now rejects two isolated mounts
   that resolve to the same host repository within one workspace.
   Allowing them would force the same container-name basename twice in
   one host repo's .git/worktrees/ namespace; no real operator workflow
   has surfaced for this case. Revisit if one does.

   Removes the now-dead suffix logic from materialize.rs:
   - `count_isolated_per_repo` (helper)
   - `canonicalize_or_clone` (helper)
   - `dst_to_branch_suffix` (in src/isolation/branch.rs — no callers left)
   The `branch_name` function keeps its optional suffix parameter for
   future clone-mode use; V1 worktree always passes None.

4. The three override files (replacement `.git` pointer, `commondir`,
   `gitdir` back-pointer) are mounted `:ro` as defensive hardening. Git
   only reads them during normal agent work, and a misbehaving agent
   could otherwise rewrite the gitdir pointer to redirect operations at
   a different repo entirely. The host `.git/` and admin mounts stay rw
   because git writes refs/objects/HEAD/index/logs there.

Tests:
- workspace::tests::isolation_layout_rejects_two_worktree_mounts_on_same_repo
- workspace::tests::isolation_layout_allows_different_host_repos_in_one_workspace
- materialize::tests::worktree_path_uses_container_name_as_basename
- materialize::tests::container_host_git_path_mirrors_dst_under_jackin_host
- materialize::tests::container_admin_path_lives_under_jackin_admin
- materialize::tests::host_and_admin_paths_disambiguate_per_mount_in_one_container
- materialize::tests::write_git_overrides_writes_three_files_with_correct_content
- materialize::tests::write_git_overrides_is_idempotent
- launch::tests::build_workspace_mount_strings_marks_overrides_readonly
  (asserts all 6 mounts in correct order with correct :ro placement)
- per_mount_isolation_e2e: updated for new path scheme + admin name

Removed:
- materialize::tests::two_isolated_mounts_same_repo_get_dst_suffixed_branches
  (case is now rejected at the workspace-validation level)

Roadmap MDX (per-mount-isolation.mdx):
- Container-side mount layout section: 4 mounts → 6, new path scheme,
  override-file storage layout
- Composition Rules: documents the new same-host-repo rejection
- Comparison table: bind mount count for jackin worktree updated 4 → 6
- V1 Scope: ship list updated with new layout and the new validation rule

Manual verification (after merge):
  cargo run --release --bin jackin -- --debug load <agent> <workspace>
  docker inspect <container> | jq '.[0].Mounts'   # see /jackin/{host,admin}/...
  docker exec -w <dst> <container> git status     # works
  git -C <host_repo> worktree list                # admin name = <container>


* refactor(isolation): single /jackin/host/ root, no commondir override, Model B branch naming

Final V1 design after extended brainstorming with the operator. Drops
the `/jackin/admin/<dst>` namespace and the `commondir` override file:
the per-worktree admin entry now lives natively at
`worktrees/<container>/` inside the host `.git/` mount, so git's
on-disk default `commondir = ../..` resolves correctly without an
override.

Container-side topology, per isolated mount (4 binds total, down from 6):
- `<dst>` (rw) — the materialized worktree
- `/jackin/host/<dst-tree>/.git` (rw) — host repo's `.git/`
- `<dst>/.git` (`:ro`) — replacement gitdir pointer
- `/jackin/host/<dst-tree>/.git/worktrees/<container>/gitdir` (`:ro`)
  — replacement back-pointer

Host-side layout under each per-container state dir:
- `git/worktree/repo/<dst-tree>/<container>/` — git's territory
- `git/overrides/<dst-tree>/{.git,gitdir}` — jackin-owned overrides

Branch naming follows Model B: `jackin/scratch/<container_name>`
verbatim. Admin entry name = container name (deterministic, globally
unique because container names are workspace-unique and
`validate_isolation_layout` rejects two isolated mounts on the same
host repo within one workspace — no auto-suffix or read-back needed).

Roadmap doc updated to reflect the final design.


* style(isolation): rustfmt assert_eq! width


* docs(isolation): align stale references with shipped V1 design

Sweep stale references across roadmap, guides, command and architecture
docs into alignment with what the code actually does. No content added —
the doc just told two contradictory stories before (Model B branch
naming alongside the old selector-key derivation; the new
git/worktree/repo/<dst>/<container>/ on-disk layout alongside the
proposed-but-never-implemented isolated/<slug>/ layout). Now there is
one consistent story end-to-end.

Touches:
- docs/src/content/docs/reference/roadmap/per-mount-isolation.mdx
- docs/src/content/docs/guides/workspaces.mdx
- docs/src/content/docs/commands/purge.mdx
- docs/src/content/docs/reference/architecture.mdx


* chore(cli): remove jackin cd command from V1

Operationally redundant with `git worktree list` + native shell `cd`.
For worktree mode, the host's `git worktree list` already enumerates
every isolated worktree by branch and absolute path, so a plain
`cd $(...)` reaches the same destination. The remaining edge cases
(preserved-dirty inspection, multi-mount picker) are rare enough that a
dedicated subcommand is net cost rather than net benefit.

Removes:
- src/cli/cd.rs (CdArgs + select_record + tests)
- Command::Cd enum variant in src/cli/mod.rs
- handle_cd dispatch in src/app/mod.rs
- docs/src/content/docs/commands/cd.mdx and its sidebar entry

Updates:
- src/isolation/finalize.rs preserved-state warning: drop "jackin cd ..."
  hint, point operator to the printed worktree path instead
- src/isolation/materialize.rs source-drift error: same treatment
- guides/workspaces.mdx + reference/architecture.mdx: drop cd references
- roadmap entry: replace "Convenience navigation" paragraph with a
  removed-from-V1 note explaining the rationale; add cd to the Defer
  list so it's recoverable if a real workflow surfaces

isolation.json schema, the preserved-state machinery, and `hardline` /
`purge` flows are unaffected — only the inspection convenience layer
is gone.


* chore(isolation): drop clone from V1 enum/parser/CLI

Per principle: don't pre-add API for unimplemented features. The `clone`
keyword was previously parsed by TOML/CLI and then rejected at validation
with a "planned but not implemented yet" error. Operators got false
positives in linting tools and confusing late failures with no benefit —
nothing in V1 actually does anything useful with the value.

Removes from the runtime:
- MountIsolation::Clone enum variant (src/isolation/mod.rs)
- explicit Clone-rejection in parse_mount_isolation (src/cli/workspace.rs):
  FromStr now produces "invalid isolation `clone`" naturally
- MountIsolation::Clone match arm in materialize_workspace
  (src/isolation/materialize.rs)
- console comments referencing the reserved-but-rejected wording

Tests:
- New `rejects_clone_until_implemented` test on FromStr asserts the
  standard "invalid isolation `clone`; expected one of: shared, worktree"
  error so this stays locked down
- parse_mount_isolation_rejects_clone updated to assert the new error
  shape

Docs:
- guides/workspaces.mdx, commands/workspace.mdx,
  reference/configuration.mdx, reference/roadmap.mdx: drop the
  "reserved keyword" wording, point at the V1.1 roadmap entry instead
- roadmap/per-mount-isolation.mdx: keep the `clone` design discussion,
  rephrase the V1 vocabulary section to make it explicit that the keyword
  is added back when clone mode ships, not pre-shipped now

apply_isolation_overrides already enforces "--mount-isolation must
reference an existing mount destination" (planner.rs); no change needed
for that requirement, just clarified in the roadmap CLI-behavior bullet.


* fix(workspace): always write mount isolation field explicitly on save

Old configs without the `isolation` field still deserialize to Shared
(the enum default). On save, drop the `skip_serializing_if = is_shared`
guard so every mount writes its isolation level explicitly — including
`shared`. Old TOMLs migrate to the new shape on first save instead of
silently retaining their pre-isolation form.

Rationale: when the operator opens the saved config, every mount should
name its isolation level. No "field is missing therefore implicitly
shared" — the value is always present and the source of truth in the
file matches the source of truth at runtime.

Touches:
- src/workspace/mod.rs MountConfig.isolation: drop skip_serializing_if
- mount_config_omits_isolation_field_when_shared_on_serialize → renamed
  to mount_config_writes_isolation_field_even_when_shared_on_serialize,
  asserts the field IS written
- guides/mounts.mdx + reference/configuration.mdx: update wording


* docs: remove stale per-mount-isolation design spec

The brainstorming spec at docs/superpowers/specs/ predated the V1 design
iterations and no longer reflects what shipped (it still describes the
old `/jackin-isolation/` mount layout, the selector-key-based branch
naming, the reserved `clone` enum value, and `jackin cd`). The roadmap
entry at docs/src/content/docs/reference/roadmap/per-mount-isolation.mdx
is now the single source of truth.


* docs(roadmap): improve per-mount isolation entry — accurate Sandboxes
comparison, V1 overview, concrete clone-mode personas

Four targeted improvements to the per-mount-isolation roadmap doc:

1. Add a "How V1 worktree mode works (TL;DR)" overview at the top of
   Host-Side Materialization. 5-step walkthrough of the materialize +
   mount + commit flow before the deep-dive subsections, so readers
   land on the entry can understand the shipped V1 in 90 seconds
   without scrolling through the layout/lifecycle/etc.

2. Rewrite the Docker Sandboxes comparison for accuracy. The old
   description got it wrong on multiple points:
   - Sandboxes use a microVM with hypervisor isolation, not Docker
     containers (Linux namespaces) like jackin.
   - The host filesystem is exposed via filesystem passthrough at the
     SAME absolute path as the host, not a bind mount of the entire
     repo at "the same relative paths".
   - Worktree path is `<host_repo>/.sbx/<sandbox-name>-worktrees/<branch>/`
     (sandbox name is in the path), not `<host_repo>/.sbx/<branch>/`.
   - Sandboxes do NOT expose the host main working tree to the agent
     — only the worktree subdir and the parent `.git/`. Our table
     previously said "✓ (entire host repo mounted)".
   The architectural insight is now explicit: Sandboxes' absolute-path
   equivalence makes git's on-disk absolute pointers resolve natively,
   which is why they don't need override files. We pay that cost
   because Docker containers translate host paths to operator-chosen
   `dst` values, breaking absolute-path equivalence.

3. Concrete clone-mode operator personas. The previous description
   was abstract ("complementary mode for ref isolation"). Replaced
   with four named situations clone mode targets: untrusted/experimental
   agents, parallel-fan-out scratch-branch noise, editor watcher
   churn, and teams whose workflow is push-to-share anyway.

4. New "Sandbox runtime" and "Host file exposure" rows in the
   comparison table to make the underlying architectural choice
   immediately visible. Cross-referenced from the Sandboxes prose.

Net: ~80 lines changed/added, primarily replacement of the wrong
Docker Sandboxes facts and addition of the V1-overview block.


* fix(isolation): close four data-loss windows in finalize/cleanup paths

Four merge-blocking issues surfaced by deep code review of PR #177.
Each was a silent-failure window that could destroy operator data on
the unhappy path while the happy-path manual smoke test stayed green.

1. assess_cleanup now treats every git capture failure as
   PreservedUnpushed (was: unwrap_or_default → empty string → could
   land in SafeToDelete). Without this, a transient `git rev-list`
   failure (corrupted pack mid-traversal, broken pipe under load,
   index.lock from a backgrounded git GC) would auto-delete the
   worktree and scratch branch, garbage-collecting unpushed commits.
   Each capture site now uses an explicit `match` that returns
   PreservedUnpushed with debug_log of the underlying error, plus a
   defense-in-depth empty-HEAD guard.

2. finalize_clean_exit now collects ALL preserved records and prompts
   per-record (was: needs_prompt.get_or_insert reached only the first
   one). On a multi-mount workspace where the operator chooses
   force-delete on the first prompt, the second preserved worktree was
   silently orphaned and the container torn down anyway — the only
   reconnection path (jackin hardline) was lost. Now each preserved
   record gets its own prompt; "return to agent" short-circuits the
   loop; "preserve" propagates as Preserved and skips container
   teardown.

3. inspect_attach_outcome now returns still_running() on docker capture
   failure (was: stopped(0) → entered finalize_clean_exit → could
   compound with #1 to delete worktrees of containers that may still
   be alive). The conservative direction is "preserve when we don't
   know" — `jackin hardline` recovers from there.

4. force_cleanup_isolated now verifies cleanup actually completed
   before removing the isolation.json record (was: let _ on git ops +
   unconditional remove_record → orphan worktree admin entries on the
   host repo and orphan branches with no jackin reference). Tolerates
   the idempotent paths (already-removed worktree, already-deleted
   branch verified absent via `git branch --list`); bails on real
   failures with a clear "record retained, re-run jackin purge after
   resolving the issue" message.

Test coverage:
- 5 new tests in isolation/finalize.rs pinning the assess_cleanup
  capture-failure → PreservedUnpushed contract for each git command
  in the assessment chain, plus the empty-HEAD guard.
- 3 new tests pinning the multi-record finalize path (force-delete-all,
  mixed force/preserve, non-interactive multi-mount warning).
- 1 new test for inspect_attach_outcome capture-failure fallback.
- 3 new cleanup tests (branch-already-deleted tolerance, real-failure
  retention, error-message contract).
- 1 new test for validate_workspace_config integration (catches the
  validate_isolation_layout call site if anyone refactors it away).
- 1 new test for build_workspace_mount_strings on a multi-mount
  isolated workspace (8 distinct binds, no path collisions, :ro
  hardening on every override file).
- 2 new drift-detection tests (dst-removed flagged; isolation-mode
  flips not-flagged with explanatory note for future improvement).

Net: 1170 → 1186 tests, 16 additions, all passing.


* fix(isolation): close P1/P2/P3 — follow-on bugs from second review

Second deep code review of PR #177 surfaced three more issues after
the first round of merge-blocker fixes shipped:

P1. `force_cleanup_isolated` failure mid-loop in finalize_clean_exit
    propagated as Err via `?`, leaving the operator with a raw cleanup
    error from deep in finalize, no Preserved signal to the caller, the
    container left running without explicit teardown decision, and
    subsequent records in the loop never prompted. This regression was
    introduced by the round-1 multi-record loop fix (the single-record
    path always succeeded). Now caught per-record, eprintln'd as a
    warning, and treated as `any_preserved_after_prompt = true` so the
    loop continues and the caller gets `Preserved`.

P2. `inspect_attach_outcome` only treated `status == "running"` as
    still-alive. `paused | restarting | removing | created | dead` all
    fell through to `stopped(0)` → entered `finalize_clean_exit` →
    could auto-delete worktrees of containers that may resume any
    moment. Concrete: `docker pause jackin-x` while jackin re-attaches
    → status="paused" → SafeToDelete on a clean tree → operator
    unpauses to find the worktree gone. Replaced if-cascade with an
    explicit `match status` that only routes `exited` through stopped()
    and treats unknown status strings conservatively as still_running.

P3. `purge_isolated_for_container` swallowed per-record errors with
    eprintln warnings and returned `Ok(())`. Exacerbated by the
    round-1 fix #4 (force_cleanup_isolated now bails more often on
    real failures). Operator runs `jackin purge`, sees a warning
    scroll past, gets exit-code-0 prompt back, may believe purge
    completed. Now collects failures and surfaces an aggregate Err
    with the failed mount list so the exit code reflects reality.

Test coverage for these fixes:
- 2 new tests in finalize.rs: ReturnToAgent on the 2nd-of-3 prompt
  (early-return short-circuits), and force_cleanup_isolated failing
  mid-loop (loop continues, returns Preserved).
- 8 new tests in launch.rs covering every status code path:
  exited(0/non-zero/oom), running, paused, transient (restarting/
  removing/created), dead, unknown.
- 2 new tests in cleanup.rs: purge bails on partial failure,
  branch_still_present returning None proceeds (pins the doc-comment
  contract against future refactors to `unwrap_or(true)`).

Net: 1186 → 1198 tests, +12 additions, all passing. fmt/clippy clean.


---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.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