ci(docs): verify docs links in PRs and on the deployed site#173
Merged
Conversation
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>
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>
4 tasks
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>
This was referenced Apr 25, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
bun run check:links/bun run check:links:freshdocs commands.docs/**, so code renames can break docs links before merge.docs/distwith lychee--remap, including same-origin canonical/OG/page links, GitHub edit links, and GitHubblob/mainfile links.workflow_dispatchof the deployed-site check from non-main branches, so a maintainer can manually verify the live docs without redeploying.<RepoFile path="..." />MDX component for repository file links (opens in a new tab to match other external links).tree/mainlinks, and plain inline-code references to existing repo files (coverssrc/,docs/,docker/,.github/, plus top-level repo files likeCargo.toml,Justfile,docker-bake.hcl,mise.toml,release.toml).<RepoFile>so lychee verifies them against the PR checkout.main.Root Cause
Starlight prepends
editLink.baseUrlto the current content source path. The previous base URL already includeddocs/src/content/docs/, so generated edit links duplicated that segment and pointed at paths likedocs/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 / builda required status check onmain. Apply that ruleset after this PR merges so the link gate becomes enforced at the protection layer (currently the docs check is advisory — a redDocs / builddoes not block merge).Validation
PATH="/opt/homebrew/bin:$PATH" bun run check:links:freshbun run check:repo-linksbun testindocs/ruby -e 'require "yaml"; YAML.load_file(".github/workflows/docs.yml"); puts "ok"'git diff --checkbun install --frozen-lockfile,cargo fmt -- --check,cargo clippy -- -D warnings,cargo nextest runNote:
bunx tsc --noEmitstill 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.