Skip to content

chore: upgrade crossterm 0.29, ratatui 0.30, and toml 1.1#4

Merged
donbeave merged 1 commit into
mainfrom
chore/upgrade-dependencies
Apr 5, 2026
Merged

chore: upgrade crossterm 0.29, ratatui 0.30, and toml 1.1#4
donbeave merged 1 commit into
mainfrom
chore/upgrade-dependencies

Conversation

@donbeave

@donbeave donbeave commented Apr 5, 2026

Copy link
Copy Markdown
Member

Summary

  • Upgrade crossterm from 0.28 to 0.29
  • Upgrade ratatui from 0.29 to 0.30
  • Upgrade toml from 0.8 to 1.1

All 142 tests pass and clippy is clean.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@donbeave donbeave merged commit 1a749b0 into main Apr 5, 2026
1 check passed
@donbeave donbeave deleted the chore/upgrade-dependencies branch April 5, 2026 18:35
donbeave pushed a commit that referenced this pull request Apr 11, 2026
- Fix cross-reference link in load.mdx: use #4-agent-source-trust
  (matching the numbered heading in security-model.mdx)
- Rename test to `load_trusted_namespaced_agent_builds_and_runs` since
  it no longer exercises auto-registration (that's covered by the
  config test `resolve_agent_source_adds_owner_repo_on_first_use`)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT
donbeave added a commit that referenced this pull request Apr 11, 2026
* feat: implement trust-on-first-use model for third-party agent sources

Add a trust gate that prompts operators before building untrusted
third-party agents. Built-in agents are always trusted. New namespaced
agents default to untrusted and require explicit confirmation on first
use. The trusted flag is persisted in config.toml so subsequent runs
proceed without prompts. Non-interactive sessions bail with a clear
error for untrusted sources.

Also syncs the roadmap with TODO items: adds missing completed item
(JACKIN_DIND_HOSTNAME), adds missing planned items (Bollard migration),
and marks the agent source trust security finding as resolved.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* improve trust prompt with clear risk details and mark finding #9 resolved

Rewrite the trust confirmation message to clearly explain what trusting
an agent means: Dockerfile execution, arbitrary commands on the host,
and workspace file access. The non-interactive error now tells the user
how to pre-trust via config.toml. The decline message explains how to
retry.

Also mark security finding #9 (mount policy guardrails) as resolved
since sensitive mount warnings were already implemented.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: address PR review — docs, stale comment, save clarity, test, serde

Review fixes for the agent source trust model:

- Fix duplicate doc comment on trust_agent() (copy-paste from
  sync_builtin_agents)
- Replace confusing `!source.trusted` save condition with explicit
  `newly_trusted` flag for readability
- Add unit test for non-interactive trust gate (verifies the error
  includes agent name and git URL)
- Skip serializing `trusted = false` to keep config.toml clean
  (only `trusted = true` appears)
- Document trust in load.mdx (step 3 in "What happens", warning
  callout for CI) and security-model.mdx (new section 4 with
  config.toml example, threat model row, best practices update)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: broken docs anchor link and misleading test name

- Fix cross-reference link in load.mdx: use #4-agent-source-trust
  (matching the numbered heading in security-model.mdx)
- Rename test to `load_trusted_namespaced_agent_builds_and_runs` since
  it no longer exercises auto-registration (that's covered by the
  config test `resolve_agent_source_adds_owner_repo_on_first_use`)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* feat: add jackin trust/untrust CLI, testable trust gate, revocation

Address all follow-up items from the maintainer review:

- Extract load_agent_with() with injected trust callback, following the
  resolve_agent_repo_with() pattern. Tests can now exercise accept and
  decline paths without mocking dialoguer.
- Restore auto-registration test coverage: the accept test uses
  load_agent_with(auto_trust) so resolve_agent_source's is_new path is
  exercised again. Add decline test verifying no config persistence and
  no Docker commands when trust is refused.
- Add `jackin trust <selector>` and `jackin untrust <selector>` CLI
  commands so operators can manage trust without editing config.toml.
- Add untrust_agent() to AppConfig with tests.
- Document trust/untrust CLI in security-model.mdx and load.mdx.
- Document that trust is keyed by selector (not URL) with a note about
  remote-mismatch protection.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: consolidate trust/untrust into single CLI command

Follow mise's pattern: `jackin trust` is one command with flags
instead of separate trust/untrust subcommands.

  jackin trust <selector>              # grant trust
  jackin trust <selector> --untrust    # revoke trust
  jackin trust <selector> --show       # check status

The --show and --untrust flags conflict with each other (enforced
by clap).

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: move trust CLI under config with grant/revoke/list subcommands

Follow the same pattern as `jackin config mount {add,remove,list}`:

  jackin config trust grant chainargos/the-architect
  jackin config trust revoke chainargos/the-architect
  jackin config trust list

This keeps all config mutations under `jackin config` and uses the
subcommand pattern consistently throughout the CLI. The `list`
subcommand shows all currently trusted agents.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: refuse to revoke trust on builtins, add CLI parsing tests

- is_builtin_agent() check prevents revoking trust on built-in agents
  (agent-smith, the-architect) which would be silently re-granted by
  sync_builtin_agents on next load
- Add 3 CLI parsing tests for config trust {grant, revoke, list},
  matching the existing coverage for config mount {add, remove, list}
- Tighten TrustCommand::List doc comment to "List all currently
  trusted agent sources"

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

---------

Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 20, 2026
- Move 9 TODO items from monolithic TODO.md into separate files in todo/
- Each file is a self-contained design doc with problem, options, and
  related source files for easy agent handoff
- Mark resolved security findings (#3, #4, #6, #7) in SECURITY_REVIEW_FINDINGS.md
- Update PROJECT_STRUCTURE.md with todo/ section and TESTING.md entry
- TODO.md becomes an index pointing to todo/ files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 20, 2026
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 20, 2026
* feat: implement trust-on-first-use model for third-party agent sources

Add a trust gate that prompts operators before building untrusted
third-party agents. Built-in agents are always trusted. New namespaced
agents default to untrusted and require explicit confirmation on first
use. The trusted flag is persisted in config.toml so subsequent runs
proceed without prompts. Non-interactive sessions bail with a clear
error for untrusted sources.

Also syncs the roadmap with TODO items: adds missing completed item
(JACKIN_DIND_HOSTNAME), adds missing planned items (Bollard migration),
and marks the agent source trust security finding as resolved.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* improve trust prompt with clear risk details and mark finding #9 resolved

Rewrite the trust confirmation message to clearly explain what trusting
an agent means: Dockerfile execution, arbitrary commands on the host,
and workspace file access. The non-interactive error now tells the user
how to pre-trust via config.toml. The decline message explains how to
retry.

Also mark security finding #9 (mount policy guardrails) as resolved
since sensitive mount warnings were already implemented.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: address PR review — docs, stale comment, save clarity, test, serde

Review fixes for the agent source trust model:

- Fix duplicate doc comment on trust_agent() (copy-paste from
  sync_builtin_agents)
- Replace confusing `!source.trusted` save condition with explicit
  `newly_trusted` flag for readability
- Add unit test for non-interactive trust gate (verifies the error
  includes agent name and git URL)
- Skip serializing `trusted = false` to keep config.toml clean
  (only `trusted = true` appears)
- Document trust in load.mdx (step 3 in "What happens", warning
  callout for CI) and security-model.mdx (new section 4 with
  config.toml example, threat model row, best practices update)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: broken docs anchor link and misleading test name

- Fix cross-reference link in load.mdx: use #4-agent-source-trust
  (matching the numbered heading in security-model.mdx)
- Rename test to `load_trusted_namespaced_agent_builds_and_runs` since
  it no longer exercises auto-registration (that's covered by the
  config test `resolve_agent_source_adds_owner_repo_on_first_use`)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* feat: add jackin trust/untrust CLI, testable trust gate, revocation

Address all follow-up items from the maintainer review:

- Extract load_agent_with() with injected trust callback, following the
  resolve_agent_repo_with() pattern. Tests can now exercise accept and
  decline paths without mocking dialoguer.
- Restore auto-registration test coverage: the accept test uses
  load_agent_with(auto_trust) so resolve_agent_source's is_new path is
  exercised again. Add decline test verifying no config persistence and
  no Docker commands when trust is refused.
- Add `jackin trust <selector>` and `jackin untrust <selector>` CLI
  commands so operators can manage trust without editing config.toml.
- Add untrust_agent() to AppConfig with tests.
- Document trust/untrust CLI in security-model.mdx and load.mdx.
- Document that trust is keyed by selector (not URL) with a note about
  remote-mismatch protection.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: consolidate trust/untrust into single CLI command

Follow mise's pattern: `jackin trust` is one command with flags
instead of separate trust/untrust subcommands.

  jackin trust <selector>              # grant trust
  jackin trust <selector> --untrust    # revoke trust
  jackin trust <selector> --show       # check status

The --show and --untrust flags conflict with each other (enforced
by clap).

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: move trust CLI under config with grant/revoke/list subcommands

Follow the same pattern as `jackin config mount {add,remove,list}`:

  jackin config trust grant chainargos/the-architect
  jackin config trust revoke chainargos/the-architect
  jackin config trust list

This keeps all config mutations under `jackin config` and uses the
subcommand pattern consistently throughout the CLI. The `list`
subcommand shows all currently trusted agents.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: refuse to revoke trust on builtins, add CLI parsing tests

- is_builtin_agent() check prevents revoking trust on built-in agents
  (agent-smith, the-architect) which would be silently re-granted by
  sync_builtin_agents on next load
- Add 3 CLI parsing tests for config trust {grant, revoke, list},
  matching the existing coverage for config mount {add, remove, list}
- Tighten TrustCommand::List doc comment to "List all currently
  trusted agent sources"

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

---------

Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 21, 2026
- Move 9 TODO items from monolithic TODO.md into separate files in todo/
- Each file is a self-contained design doc with problem, options, and
  related source files for easy agent handoff
- Mark resolved security findings (#3, #4, #6, #7) in SECURITY_REVIEW_FINDINGS.md
- Update PROJECT_STRUCTURE.md with todo/ section and TESTING.md entry
- TODO.md becomes an index pointing to todo/ files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 21, 2026
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 21, 2026
* feat: implement trust-on-first-use model for third-party agent sources

Add a trust gate that prompts operators before building untrusted
third-party agents. Built-in agents are always trusted. New namespaced
agents default to untrusted and require explicit confirmation on first
use. The trusted flag is persisted in config.toml so subsequent runs
proceed without prompts. Non-interactive sessions bail with a clear
error for untrusted sources.

Also syncs the roadmap with TODO items: adds missing completed item
(JACKIN_DIND_HOSTNAME), adds missing planned items (Bollard migration),
and marks the agent source trust security finding as resolved.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* improve trust prompt with clear risk details and mark finding #9 resolved

Rewrite the trust confirmation message to clearly explain what trusting
an agent means: Dockerfile execution, arbitrary commands on the host,
and workspace file access. The non-interactive error now tells the user
how to pre-trust via config.toml. The decline message explains how to
retry.

Also mark security finding #9 (mount policy guardrails) as resolved
since sensitive mount warnings were already implemented.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: address PR review — docs, stale comment, save clarity, test, serde

Review fixes for the agent source trust model:

- Fix duplicate doc comment on trust_agent() (copy-paste from
  sync_builtin_agents)
- Replace confusing `!source.trusted` save condition with explicit
  `newly_trusted` flag for readability
- Add unit test for non-interactive trust gate (verifies the error
  includes agent name and git URL)
- Skip serializing `trusted = false` to keep config.toml clean
  (only `trusted = true` appears)
- Document trust in load.mdx (step 3 in "What happens", warning
  callout for CI) and security-model.mdx (new section 4 with
  config.toml example, threat model row, best practices update)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: broken docs anchor link and misleading test name

- Fix cross-reference link in load.mdx: use #4-agent-source-trust
  (matching the numbered heading in security-model.mdx)
- Rename test to `load_trusted_namespaced_agent_builds_and_runs` since
  it no longer exercises auto-registration (that's covered by the
  config test `resolve_agent_source_adds_owner_repo_on_first_use`)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* feat: add jackin trust/untrust CLI, testable trust gate, revocation

Address all follow-up items from the maintainer review:

- Extract load_agent_with() with injected trust callback, following the
  resolve_agent_repo_with() pattern. Tests can now exercise accept and
  decline paths without mocking dialoguer.
- Restore auto-registration test coverage: the accept test uses
  load_agent_with(auto_trust) so resolve_agent_source's is_new path is
  exercised again. Add decline test verifying no config persistence and
  no Docker commands when trust is refused.
- Add `jackin trust <selector>` and `jackin untrust <selector>` CLI
  commands so operators can manage trust without editing config.toml.
- Add untrust_agent() to AppConfig with tests.
- Document trust/untrust CLI in security-model.mdx and load.mdx.
- Document that trust is keyed by selector (not URL) with a note about
  remote-mismatch protection.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: consolidate trust/untrust into single CLI command

Follow mise's pattern: `jackin trust` is one command with flags
instead of separate trust/untrust subcommands.

  jackin trust <selector>              # grant trust
  jackin trust <selector> --untrust    # revoke trust
  jackin trust <selector> --show       # check status

The --show and --untrust flags conflict with each other (enforced
by clap).

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: move trust CLI under config with grant/revoke/list subcommands

Follow the same pattern as `jackin config mount {add,remove,list}`:

  jackin config trust grant chainargos/the-architect
  jackin config trust revoke chainargos/the-architect
  jackin config trust list

This keeps all config mutations under `jackin config` and uses the
subcommand pattern consistently throughout the CLI. The `list`
subcommand shows all currently trusted agents.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: refuse to revoke trust on builtins, add CLI parsing tests

- is_builtin_agent() check prevents revoking trust on built-in agents
  (agent-smith, the-architect) which would be silently re-granted by
  sync_builtin_agents on next load
- Add 3 CLI parsing tests for config trust {grant, revoke, list},
  matching the existing coverage for config mount {add, remove, list}
- Tighten TrustCommand::List doc comment to "List all currently
  trusted agent sources"

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

---------

Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 21, 2026
- Move 9 TODO items from monolithic TODO.md into separate files in todo/
- Each file is a self-contained design doc with problem, options, and
  related source files for easy agent handoff
- Mark resolved security findings (#3, #4, #6, #7) in SECURITY_REVIEW_FINDINGS.md
- Update PROJECT_STRUCTURE.md with todo/ section and TESTING.md entry
- TODO.md becomes an index pointing to todo/ files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 21, 2026
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 21, 2026
* feat: implement trust-on-first-use model for third-party agent sources

Add a trust gate that prompts operators before building untrusted
third-party agents. Built-in agents are always trusted. New namespaced
agents default to untrusted and require explicit confirmation on first
use. The trusted flag is persisted in config.toml so subsequent runs
proceed without prompts. Non-interactive sessions bail with a clear
error for untrusted sources.

Also syncs the roadmap with TODO items: adds missing completed item
(JACKIN_DIND_HOSTNAME), adds missing planned items (Bollard migration),
and marks the agent source trust security finding as resolved.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* improve trust prompt with clear risk details and mark finding #9 resolved

Rewrite the trust confirmation message to clearly explain what trusting
an agent means: Dockerfile execution, arbitrary commands on the host,
and workspace file access. The non-interactive error now tells the user
how to pre-trust via config.toml. The decline message explains how to
retry.

Also mark security finding #9 (mount policy guardrails) as resolved
since sensitive mount warnings were already implemented.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: address PR review — docs, stale comment, save clarity, test, serde

Review fixes for the agent source trust model:

- Fix duplicate doc comment on trust_agent() (copy-paste from
  sync_builtin_agents)
- Replace confusing `!source.trusted` save condition with explicit
  `newly_trusted` flag for readability
- Add unit test for non-interactive trust gate (verifies the error
  includes agent name and git URL)
- Skip serializing `trusted = false` to keep config.toml clean
  (only `trusted = true` appears)
- Document trust in load.mdx (step 3 in "What happens", warning
  callout for CI) and security-model.mdx (new section 4 with
  config.toml example, threat model row, best practices update)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: broken docs anchor link and misleading test name

- Fix cross-reference link in load.mdx: use #4-agent-source-trust
  (matching the numbered heading in security-model.mdx)
- Rename test to `load_trusted_namespaced_agent_builds_and_runs` since
  it no longer exercises auto-registration (that's covered by the
  config test `resolve_agent_source_adds_owner_repo_on_first_use`)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* feat: add jackin trust/untrust CLI, testable trust gate, revocation

Address all follow-up items from the maintainer review:

- Extract load_agent_with() with injected trust callback, following the
  resolve_agent_repo_with() pattern. Tests can now exercise accept and
  decline paths without mocking dialoguer.
- Restore auto-registration test coverage: the accept test uses
  load_agent_with(auto_trust) so resolve_agent_source's is_new path is
  exercised again. Add decline test verifying no config persistence and
  no Docker commands when trust is refused.
- Add `jackin trust <selector>` and `jackin untrust <selector>` CLI
  commands so operators can manage trust without editing config.toml.
- Add untrust_agent() to AppConfig with tests.
- Document trust/untrust CLI in security-model.mdx and load.mdx.
- Document that trust is keyed by selector (not URL) with a note about
  remote-mismatch protection.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: consolidate trust/untrust into single CLI command

Follow mise's pattern: `jackin trust` is one command with flags
instead of separate trust/untrust subcommands.

  jackin trust <selector>              # grant trust
  jackin trust <selector> --untrust    # revoke trust
  jackin trust <selector> --show       # check status

The --show and --untrust flags conflict with each other (enforced
by clap).

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: move trust CLI under config with grant/revoke/list subcommands

Follow the same pattern as `jackin config mount {add,remove,list}`:

  jackin config trust grant chainargos/the-architect
  jackin config trust revoke chainargos/the-architect
  jackin config trust list

This keeps all config mutations under `jackin config` and uses the
subcommand pattern consistently throughout the CLI. The `list`
subcommand shows all currently trusted agents.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: refuse to revoke trust on builtins, add CLI parsing tests

- is_builtin_agent() check prevents revoking trust on built-in agents
  (agent-smith, the-architect) which would be silently re-granted by
  sync_builtin_agents on next load
- Add 3 CLI parsing tests for config trust {grant, revoke, list},
  matching the existing coverage for config mount {add, remove, list}
- Tighten TrustCommand::List doc comment to "List all currently
  trusted agent sources"

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

---------

Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 21, 2026
- Move 9 TODO items from monolithic TODO.md into separate files in todo/
- Each file is a self-contained design doc with problem, options, and
  related source files for easy agent handoff
- Mark resolved security findings (#3, #4, #6, #7) in SECURITY_REVIEW_FINDINGS.md
- Update PROJECT_STRUCTURE.md with todo/ section and TESTING.md entry
- TODO.md becomes an index pointing to todo/ files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 21, 2026
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 21, 2026
* feat: implement trust-on-first-use model for third-party agent sources

Add a trust gate that prompts operators before building untrusted
third-party agents. Built-in agents are always trusted. New namespaced
agents default to untrusted and require explicit confirmation on first
use. The trusted flag is persisted in config.toml so subsequent runs
proceed without prompts. Non-interactive sessions bail with a clear
error for untrusted sources.

Also syncs the roadmap with TODO items: adds missing completed item
(JACKIN_DIND_HOSTNAME), adds missing planned items (Bollard migration),
and marks the agent source trust security finding as resolved.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* improve trust prompt with clear risk details and mark finding #9 resolved

Rewrite the trust confirmation message to clearly explain what trusting
an agent means: Dockerfile execution, arbitrary commands on the host,
and workspace file access. The non-interactive error now tells the user
how to pre-trust via config.toml. The decline message explains how to
retry.

Also mark security finding #9 (mount policy guardrails) as resolved
since sensitive mount warnings were already implemented.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: address PR review — docs, stale comment, save clarity, test, serde

Review fixes for the agent source trust model:

- Fix duplicate doc comment on trust_agent() (copy-paste from
  sync_builtin_agents)
- Replace confusing `!source.trusted` save condition with explicit
  `newly_trusted` flag for readability
- Add unit test for non-interactive trust gate (verifies the error
  includes agent name and git URL)
- Skip serializing `trusted = false` to keep config.toml clean
  (only `trusted = true` appears)
- Document trust in load.mdx (step 3 in "What happens", warning
  callout for CI) and security-model.mdx (new section 4 with
  config.toml example, threat model row, best practices update)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: broken docs anchor link and misleading test name

- Fix cross-reference link in load.mdx: use #4-agent-source-trust
  (matching the numbered heading in security-model.mdx)
- Rename test to `load_trusted_namespaced_agent_builds_and_runs` since
  it no longer exercises auto-registration (that's covered by the
  config test `resolve_agent_source_adds_owner_repo_on_first_use`)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* feat: add jackin trust/untrust CLI, testable trust gate, revocation

Address all follow-up items from the maintainer review:

- Extract load_agent_with() with injected trust callback, following the
  resolve_agent_repo_with() pattern. Tests can now exercise accept and
  decline paths without mocking dialoguer.
- Restore auto-registration test coverage: the accept test uses
  load_agent_with(auto_trust) so resolve_agent_source's is_new path is
  exercised again. Add decline test verifying no config persistence and
  no Docker commands when trust is refused.
- Add `jackin trust <selector>` and `jackin untrust <selector>` CLI
  commands so operators can manage trust without editing config.toml.
- Add untrust_agent() to AppConfig with tests.
- Document trust/untrust CLI in security-model.mdx and load.mdx.
- Document that trust is keyed by selector (not URL) with a note about
  remote-mismatch protection.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: consolidate trust/untrust into single CLI command

Follow mise's pattern: `jackin trust` is one command with flags
instead of separate trust/untrust subcommands.

  jackin trust <selector>              # grant trust
  jackin trust <selector> --untrust    # revoke trust
  jackin trust <selector> --show       # check status

The --show and --untrust flags conflict with each other (enforced
by clap).

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: move trust CLI under config with grant/revoke/list subcommands

Follow the same pattern as `jackin config mount {add,remove,list}`:

  jackin config trust grant chainargos/the-architect
  jackin config trust revoke chainargos/the-architect
  jackin config trust list

This keeps all config mutations under `jackin config` and uses the
subcommand pattern consistently throughout the CLI. The `list`
subcommand shows all currently trusted agents.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: refuse to revoke trust on builtins, add CLI parsing tests

- is_builtin_agent() check prevents revoking trust on built-in agents
  (agent-smith, the-architect) which would be silently re-granted by
  sync_builtin_agents on next load
- Add 3 CLI parsing tests for config trust {grant, revoke, list},
  matching the existing coverage for config mount {add, remove, list}
- Tighten TrustCommand::List doc comment to "List all currently
  trusted agent sources"

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

---------

Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 21, 2026
- Move 9 TODO items from monolithic TODO.md into separate files in todo/
- Each file is a self-contained design doc with problem, options, and
  related source files for easy agent handoff
- Mark resolved security findings (#3, #4, #6, #7) in SECURITY_REVIEW_FINDINGS.md
- Update PROJECT_STRUCTURE.md with todo/ section and TESTING.md entry
- TODO.md becomes an index pointing to todo/ files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 21, 2026
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 21, 2026
* feat: implement trust-on-first-use model for third-party agent sources

Add a trust gate that prompts operators before building untrusted
third-party agents. Built-in agents are always trusted. New namespaced
agents default to untrusted and require explicit confirmation on first
use. The trusted flag is persisted in config.toml so subsequent runs
proceed without prompts. Non-interactive sessions bail with a clear
error for untrusted sources.

Also syncs the roadmap with TODO items: adds missing completed item
(JACKIN_DIND_HOSTNAME), adds missing planned items (Bollard migration),
and marks the agent source trust security finding as resolved.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* improve trust prompt with clear risk details and mark finding #9 resolved

Rewrite the trust confirmation message to clearly explain what trusting
an agent means: Dockerfile execution, arbitrary commands on the host,
and workspace file access. The non-interactive error now tells the user
how to pre-trust via config.toml. The decline message explains how to
retry.

Also mark security finding #9 (mount policy guardrails) as resolved
since sensitive mount warnings were already implemented.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: address PR review — docs, stale comment, save clarity, test, serde

Review fixes for the agent source trust model:

- Fix duplicate doc comment on trust_agent() (copy-paste from
  sync_builtin_agents)
- Replace confusing `!source.trusted` save condition with explicit
  `newly_trusted` flag for readability
- Add unit test for non-interactive trust gate (verifies the error
  includes agent name and git URL)
- Skip serializing `trusted = false` to keep config.toml clean
  (only `trusted = true` appears)
- Document trust in load.mdx (step 3 in "What happens", warning
  callout for CI) and security-model.mdx (new section 4 with
  config.toml example, threat model row, best practices update)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: broken docs anchor link and misleading test name

- Fix cross-reference link in load.mdx: use #4-agent-source-trust
  (matching the numbered heading in security-model.mdx)
- Rename test to `load_trusted_namespaced_agent_builds_and_runs` since
  it no longer exercises auto-registration (that's covered by the
  config test `resolve_agent_source_adds_owner_repo_on_first_use`)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* feat: add jackin trust/untrust CLI, testable trust gate, revocation

Address all follow-up items from the maintainer review:

- Extract load_agent_with() with injected trust callback, following the
  resolve_agent_repo_with() pattern. Tests can now exercise accept and
  decline paths without mocking dialoguer.
- Restore auto-registration test coverage: the accept test uses
  load_agent_with(auto_trust) so resolve_agent_source's is_new path is
  exercised again. Add decline test verifying no config persistence and
  no Docker commands when trust is refused.
- Add `jackin trust <selector>` and `jackin untrust <selector>` CLI
  commands so operators can manage trust without editing config.toml.
- Add untrust_agent() to AppConfig with tests.
- Document trust/untrust CLI in security-model.mdx and load.mdx.
- Document that trust is keyed by selector (not URL) with a note about
  remote-mismatch protection.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: consolidate trust/untrust into single CLI command

Follow mise's pattern: `jackin trust` is one command with flags
instead of separate trust/untrust subcommands.

  jackin trust <selector>              # grant trust
  jackin trust <selector> --untrust    # revoke trust
  jackin trust <selector> --show       # check status

The --show and --untrust flags conflict with each other (enforced
by clap).

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: move trust CLI under config with grant/revoke/list subcommands

Follow the same pattern as `jackin config mount {add,remove,list}`:

  jackin config trust grant chainargos/the-architect
  jackin config trust revoke chainargos/the-architect
  jackin config trust list

This keeps all config mutations under `jackin config` and uses the
subcommand pattern consistently throughout the CLI. The `list`
subcommand shows all currently trusted agents.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: refuse to revoke trust on builtins, add CLI parsing tests

- is_builtin_agent() check prevents revoking trust on built-in agents
  (agent-smith, the-architect) which would be silently re-granted by
  sync_builtin_agents on next load
- Add 3 CLI parsing tests for config trust {grant, revoke, list},
  matching the existing coverage for config mount {add, remove, list}
- Tighten TrustCommand::List doc comment to "List all currently
  trusted agent sources"

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

---------

Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 24, 2026
Previously, `commit_editor_save` mutated `editor.mode` to the new
name immediately after a successful `ce.rename_workspace` -- leaving
the editor UI reflecting the new name if a later step
(`ce.edit_workspace` / `ce.save`) failed, while disk/config still
held the old name.

Thread the pending-rename through the inner match as an
`Option<String>` return value, and apply it to `editor.mode` only
inside the `ce.save()` success arm after config has been reloaded.

Addresses finding #4 of the PR #166 current-branch review.

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 24, 2026
Mechanical cleanup of clippy warnings in files added or modified by
PR #166. Addresses finding #4 of the second-pass review:

tests/manager_flow.rs:
- missing_const_for_fn on the `key` helper -> mark `const fn`.
- redundant_clone on the third use of `host_path` -> drop the
  trailing `.clone()` so the value moves on its last use.
- default_trait_access on `Default::default()` for env/agents
  fields -> use the explicit `BTreeMap::new()` constructor.

src/launch/manager/input/save.rs (test module):
- used_underscore_binding on `_tmp` -> rename to `tmp`. The binding
  has to be held to keep the TempDir alive for the test, but it is
  also read via `.path()`, so the underscore was misleading; the
  rename makes the read explicit.
- used_underscore_binding on `_config0` -> swap for the `_`
  bit-bucket pattern; the loaded config is genuinely unused in
  those tests (they re-open the config via ConfigEditor).
- redundant_clone on `ws_b.clone()` at the last use site -> drop
  the clone and let the value move.

No #[allow(...)] suppressions -- every warning is addressed by code
change.

Warnings in files outside this PR remain as documented pre-existing
drift (finding #9 of the first-pass review).

Addresses finding #4 of the second-pass PR #166 review.

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
… line count corrections

- Add instance/auth.rs to //! priority queue at #4: four security
  invariants (0o600 perms, symlink rejection, TOCTOU-safe writes,
  macOS Keychain) documented in draft //! content
- Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types
  mixed with impl blocks; propose 5-file types/behavior split
- Correct stale line counts: render/list.rs 1122→1989 (PR #171 added
  render_environments_subpanel); state.rs 865→992; priorities upgraded
- Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332,
  mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944
- Renumber //! priority queue to 11 entries (was 10)

Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
… line count corrections

- Add instance/auth.rs to //! priority queue at #4: four security
  invariants (0o600 perms, symlink rejection, TOCTOU-safe writes,
  macOS Keychain) documented in draft //! content
- Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types
  mixed with impl blocks; propose 5-file types/behavior split
- Correct stale line counts: render/list.rs 1122→1989 (PR #171 added
  render_environments_subpanel); state.rs 865→992; priorities upgraded
- Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332,
  mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944
- Renumber //! priority queue to 11 entries (was 10)

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
… line count corrections

- Add instance/auth.rs to //! priority queue at #4: four security
  invariants (0o600 perms, symlink rejection, TOCTOU-safe writes,
  macOS Keychain) documented in draft //! content
- Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types
  mixed with impl blocks; propose 5-file types/behavior split
- Correct stale line counts: render/list.rs 1122→1989 (PR #171 added
  render_environments_subpanel); state.rs 865→992; priorities upgraded
- Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332,
  mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944
- Renumber //! priority queue to 11 entries (was 10)

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
… line count corrections

- Add instance/auth.rs to //! priority queue at #4: four security
  invariants (0o600 perms, symlink rejection, TOCTOU-safe writes,
  macOS Keychain) documented in draft //! content
- Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types
  mixed with impl blocks; propose 5-file types/behavior split
- Correct stale line counts: render/list.rs 1122→1989 (PR #171 added
  render_environments_subpanel); state.rs 865→992; priorities upgraded
- Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332,
  mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944
- Renumber //! priority queue to 11 entries (was 10)

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
… line count corrections

- Add instance/auth.rs to //! priority queue at #4: four security
  invariants (0o600 perms, symlink rejection, TOCTOU-safe writes,
  macOS Keychain) documented in draft //! content
- Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types
  mixed with impl blocks; propose 5-file types/behavior split
- Correct stale line counts: render/list.rs 1122→1989 (PR #171 added
  render_environments_subpanel); state.rs 865→992; priorities upgraded
- Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332,
  mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944
- Renumber //! priority queue to 11 entries (was 10)

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
…183)

* docs(roadmap): iteration 13 — AI code verifiability framing, config/types.rs full spec

Primary goal shift: codebase must be verifiable for AI-generated code.

- §0: replace generic description with explicit verifiability rationale
  (module contracts, localised concerns, types/behaviour separation)
- §4 intro: add "Why structure matters for AI-generated code" section with
  audit-units table mapping each post-split file to one reviewable question
- §4 4a: expand config/types.rs from description to full execution spec —
  exact type list, post-split mod.rs content, zero-change submodule
  guarantee (verified: agents.rs/persist.rs/workspaces.rs use super::T
  which resolves through mod.rs re-exports unchanged), impl-extension
  pattern already in use documented

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

* docs(roadmap): iteration 14 — editor method map, app helpers, //! queue

- §4 4c: config/editor.rs split is now execution-ready — complete
  6-file method-to-file table with private helper placement verified
  (validate_candidate→io.rs, table_path_mut→mod.rs pub(super),
  auth_forward_str→agent_ops.rs, create_workspace delegates to AppConfig)
- §4 4e: app/mod.rs split complete — all private helpers mapped
  (parse_auth_forward_mode_from_cli→config_cmd.rs,
  workspace_env_scope→workspace_cmd.rs, print_env_table note,
  remove_data_dir_if_exists→dispatch.rs)
- §10 step 5: add //! priority queue — 10 files with draft content,
  prioritised by cold-landing impact and AI audit risk; selector.rs
  and instance/mod.rs explicitly document the /→__ invariant

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

* docs(roadmap): iteration 15 — dep graph fix, trust.rs safety, OQ1 closed

- §4 4d: correct operator_env dependency graph — layers.rs imports
  both mod.rs (OpRunner) AND client.rs (OpCli for non-injectable
  resolve_operator_env wrapper at line 797); still a valid DAG
- §4 4f: verify trust.rs split safety — FnOnce injection pattern means
  launch_pipeline.rs has zero dependency on trust.rs; import chain
  documented; trust bypass audit now requires reading only ~60L
- §9 OQ1 closed: op_cache.rs read in full — 4-level structure,
  per-level invalidation, no TTL/expiry (expiry handled at OpCli
  subprocess level), DEFAULT_ACCOUNT_KEY sentinel documented

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

* docs(roadmap): iteration 16 — CommandRunner Rule 3, render/editor split, 4a/4c independence

- Fix duplicate Rule 3 section introduced by previous edit; add docker.rs
  co-location note as third edge case (three edge cases, not two)
- Add render/editor.rs as new Rule 5 violator: 1666L post-PR #171
  (was listed as 782L); propose 6-file tab-by-tab split with auditability
  note on the security-adjacent Secrets tab
- Add §10 execution-order note: 4a and 4c are independent — editor.rs
  imports AppConfig via crate::config re-exports regardless of 4a order
- Append iteration 16 log entry with confidence table and weakest sections

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

* docs(roadmap): iteration 17 — instance/auth.rs audit, state.rs split, line count corrections

- Add instance/auth.rs to //! priority queue at #4: four security
  invariants (0o600 perms, symlink rejection, TOCTOU-safe writes,
  macOS Keychain) documented in draft //! content
- Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types
  mixed with impl blocks; propose 5-file types/behavior split
- Correct stale line counts: render/list.rs 1122→1989 (PR #171 added
  render_environments_subpanel); state.rs 865→992; priorities upgraded
- Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332,
  mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944
- Renumber //! priority queue to 11 entries (was 10)

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

* docs(roadmap): iteration 18 — agent_allow OQ2 closed, render/list.rs split proposal

- Close OQ2: agent_allow.rs read in full — 55L, correct //! doc,
  design sound; serves as model for //! priority queue pattern
- Add render/list.rs as new Rule 5 violator: 668L production (PR #171
  added render_environments_subpanel); propose 3-file split (mod.rs,
  details.rs, subpanels.rs); note import-path change for agents_block_agent_count
- Update §1 module map: agent_allow.rs entry corrected with size/API

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

* docs(roadmap): iteration 19 — input/editor.rs critical correction, split proposal

- Correct input/editor.rs: 2349L total (was 1304L), 1141L production
  (was 547L) — PR #171 added Secrets-tab handlers; pub(super) fn
  handle_editor_modal at line 618 was invisible to previous grep pattern;
  now the largest production file in the codebase; priority → Critical
- Correct input/save.rs: 1472L total, 661L production (was 567L)
- Add 5-file split proposal for input/editor.rs: mod.rs (two dispatchers),
  secrets.rs (~500L AI-generated Secrets-tab), agents.rs, mounts.rs, general.rs
- Update key insight paragraph naming input/editor.rs as largest production file

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

* docs(roadmap): iteration 20 — console splits in §10, MSRV evidence, animation.rs verdict

- Add console/manager/ as §10 Step 4f group with 5 sub-steps in priority
  order; rename existing 4f (launch.rs) → 4g; add circular-import risk note
  for ManagerStage/EditorState split sequencing
- Analyze tui/animation.rs: 582L all-production, no split needed (banner_grid
  is a tightly-coupled rendering loop); section comments compensate for missing //!
- Partially close OQ3: u64::is_multiple_of (stabilized 1.86) found in animation.rs;
  within declared MSRV 1.94; full cargo +1.94.0 check deferred (toolchain unavailable)

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

* docs(roadmap): iteration 21 — input/save.rs split, //! queue fix, save.rs corrections

- Add input/save.rs split proposal: 4 pub(super) fns discovered; 3-file
  split (mod.rs + flow.rs + preview.rs); no cross-dependency between
  flow and preview groups; §10 4f-v updated from Optional to concrete plan
- Fix //! queue preamble: "first 10 files" → "first 11 files"
- Correct save.rs module map (1418→1472L, correct key exports) and
  hot-spot table note (begin_editor_save ~280L → ~118L; commit_editor_save
  is the Phase 2 partner at ~149L)

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

* docs(roadmap): iteration 22 — input/list.rs and mount_info.rs analysis

- Analyze input/list.rs: 214L production (tests at 215); has //! doc;
  two focused pub(super) fns; no split needed; Low priority; correct module map
- Add mount_info.rs to hot-spot table: 277L production; Low priority;
  has //! doc; correct module map with 3 public enums + inspect fn
- Fix stale §2 diagnosis note: docs/internal/roadmap/ now exists

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

* docs(roadmap): iteration 23 — audit units table +5 console rows, input/mod.rs corrected

- Expand audit units table from 8 to 13 entries: add state/types.rs,
  state/editor.rs, input/editor/secrets.rs, render/list/subpanels.rs,
  input/save/preview.rs — all targeting PR #171 AI-generated console code
- Add PR #171 context note linking 5 new entries to AI-generated code concern
- Correct input/mod.rs module map: 369L, add InputOutcome enum to exports
- Verify rust-toolchain.toml absence; §7.7 and §2 concept 25 already correct

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

* docs(roadmap): iteration 24 — render/mod.rs analysis, //! exemplars table, EditorTab confirmed

- Add §4 Rule 7 positive exemplars table: 7 files with //! docs graded
  1-element (render/mod.rs), 2-element (input/save.rs etc), 3-element
  (env_model.rs, agent_allow.rs); PR #171 docs-discipline pattern noted
- Correct render/mod.rs module map: 421L; FooterItem + palette constants
  + render_header + centered_rect_fixed added to key exports
- Confirm EditorTab variants: General, Mounts, Agents, Secrets (Rust enum)
  vs "Secrets / Environments" (UI label); /stub qualifier already removed

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

* docs(roadmap): iteration 25 — too_many_lines recount, FooterItem PR, MountConfig caveat

- Correct too_many_lines count: 13 across 8 → 16 across 11 files
  (PR #171 added 5 suppressions in console/manager); add full breakdown
  table; update all 3 occurrences in roadmap
- Fix FooterItem PR reference: #165#166 (confirmed by git log --follow)
- Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description

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

* docs(roadmap): iteration 26 — console/mod.rs and op_picker/render.rs analyzed

- Add console/mod.rs to hot-spot table: 406L/307L production (Low);
  correct module map from ~200 → 406L; note missing //! doc with
  ConsoleStage design block comment worth promoting
- Add op_picker/render.rs to hot-spot table: 865L/545L production
  (Medium); PR #171 AI-generated; 14 functions in two logical groups
  (entry/helpers vs level renderers); split into levels.rs proposed
- Correct 3 stale ~200L estimates for console/mod.rs across roadmap

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

* docs(roadmap): iteration 27 — op_picker/mod.rs discovery, render split, operator_env correction

- Add op_picker/mod.rs to hot-spot table: 1712L/775L production (High);
  PR #171 AI-generated; OpPickerState types+behavior split opportunity;
  has 7-line //! doc; module map split into two rows (mod.rs + render.rs)
- Add op_picker/render.rs 2-file split proposal: render.rs (coordinator)
  + render_pane.rs (pane/level renderers); no cross-dependency confirmed
- Correct operator_env.rs total: 1569→2130L (880L production); update
  4 occurrences across hot-spot table, ASCII tree, §4 analysis

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

* docs(roadmap): iteration 28 — op_picker/mod.rs 3-file split, count corrections

- Add op_picker/mod.rs formal 3-file split: loading.rs (async load family
  ~120L) + keys.rs (4 level key handlers ~315L) + mod.rs (types/constructors)
- Correct "24 files" → "28+" for 500L threshold count
- Update total LOC: ~40,664 → ~43,587 (2 occurrences, with provenance note)

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

* docs(roadmap): iteration 29 — op_picker execution order + file_browser analysis

- §10 Step 4f: expand from 5 to 7 sub-steps; add 4f-vi (op_picker/mod.rs
  → mod.rs + loading.rs + keys.rs) and 4f-vii (op_picker/render.rs →
  render.rs + pane.rs); document impl-extension and import-path caveats
- §4 //! exemplars: add file_browser/ subsystem analysis — all 5 files
  have //! docs, no file exceeds ~350L production; classified as exemplar
  (not a split candidate); document git_prompt.rs coupling-density
  justification and input.rs as 28-file false positive (144L production)
- §1 module map: expand single file_browser/ row to 5 individual rows
  with production LOC and dominant concern per file

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

* docs(roadmap): iteration 30 — challenge split-first thesis, fresh LOC corrections

- §4: Add "Alternative thesis: documentation-first verification" — challenges
  the two core assumptions behind file splitting (files-as-audit-unit and
  file-size-as-context-constraint); adds 7-criterion comparison table vs
  structure-first approach; introduces phased combined recommendation:
  Phase 1 = doc sprint (//! contracts + specs/ for 3 subsystems, 2-3 PRs,
  zero structural change); Phase 2 = splits only for >600L production files
  (reduces scope from 14+ to 4 files); Phase 3 = workspace if LOC > 150K
- Fix stale LOC: app/mod.rs 951→979, config/editor.rs 1467→1548 (7 and
  8 locations respectively; verified by fresh find|xargs wc -l scan)
- §1 module map: add agent_picker.rs (436L), scope_picker.rs (201L),
  source_picker.rs (244L) — all PR #171 additions with //! docs

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

* docs(roadmap): iteration 31 — fix 600L→800L threshold error, correct LOC

- §4 alternative thesis: correct ">600L production → 4 files" claim
  introduced in iteration 30; re-verified all 9 candidate files via
  #[cfg(test)] line position; threshold must be >800L to get exactly 4
  files (9 exceed 600L); add verification table with test-start lines
- Production LOC corrections (5+ locations each):
  launch.rs 1085→~1077, operator_env.rs 810→~880,
  app/mod.rs 928→~957, config/editor.rs 503→~584
- §2 OpPicker row: replace vague "no entry yet" with confirmed gap:
  PROJECT_STRUCTURE.md line 53 still lists pre-PR#171 widget set (10
  named); omits op_picker/, agent_picker.rs, scope_picker.rs,
  source_picker.rs and pre-dates the manager/ sub-structure split

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

* docs(roadmap): iteration 32 — two-tier spec arch, behavioral spec template

- §8.1: Add two-tier spec architecture table distinguishing feature specs
  (public Starlight MDX, user-facing) from behavioral specs (internal
  docs/internal/specs/, for AI code verification) — resolves contradiction
  between §4 (which said docs/internal/specs/) and §8.1 (which said
  "no longer needed; specs are public")
- §8.1: Add concrete behavioral spec template for op_picker/ with state
  machine table and 3 INV invariant entries each with a grep-executable
  "Verify by:" command; template directly usable for the 3 Phase 1 specs
- §8.1: Remove erroneous "docs/internal/specs/ no longer needed" claim
- Confirmed render/editor.rs ~736L and render/list.rs ~668L production
  (no interspersed production code — all test blocks follow consecutively)

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

* docs(roadmap): iteration 33 — executive summary, §0 correctness

- §0: Add executive summary (~300 words) with core problem, 3-phase
  recommendation, key counter-argument, and navigation table pointing
  to §2/§4/§7/§8/§10 by question — resolves the meta-irony of a
  readability roadmap with no entry-point orientation
- §0 item 2: "1569-line monolith" → "2130-line monolith" (operator_env.rs
  current verified size; stale reference was in the first section readers see)
- §0 item 3: Add "(selective)" qualifier and explicit note that standard
  Rust co-locates struct+impl — impl-extension pattern is justified only
  for files >800L production, not as a universal rule

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

* docs(roadmap): iteration 34 — spec priority reorder, §10 Phase 1 track

- §0 + §4 Phase 1: Prioritize runtime/launch.rs behavioral spec (no //!
  doc, ~1077L production, critical path — all jackin load failures trace
  here); drop config/editor.rs from Phase 1 (its 963L test suite already
  serves as behavioral spec — tests are behavioral examples); reduce Phase
  1 from 3 specs to 2 specs; add reasoning for the priority ordering
- §10 Step 2: Split into two parallel tracks — Track A (cc-sdd tooling
  setup) + Track B (Phase 1 behavioral spec authoring); Track B includes
  specific INV invariants to capture for runtime/launch.rs grounded in
  reading the actual function structure (step comment positions); adds
  sequencing rationale: spec must precede structural splits

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

* docs(roadmap): iteration 35 — verified INV entries for runtime/launch.rs

Read load_agent_with lines 553-892 in full. Replaced 3 draft INVs from
iteration 34 (inferred from step comment positions) with 5 verified INVs
citing exact line numbers:
- INV-1: trust gate (line 594) precedes image build (line 736)
- INV-2: container name claimed (line 754) between image build and network
- INV-3: token verified (line 763) before network creation (line 827)
- INV-4: render_exit called at lines 886 AND 890 (all exit paths)
- INV-5: cleanup disarm semantics — Running→disarm, clean exit→cleanup,
  crash→disarm (explains jackin hardline compatibility)
Corrected wrong line number: claim_container_name call is at 754, not 918
(918 is the function definition). Each INV has a grep-executable Verify by.

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

* docs(roadmap): iteration 36 — CI gate for PROJECT_STRUCTURE.md freshness

§3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with
three concrete options:
- Option A: CONTRIBUTING.md rule (necessary but insufficient)
- Option B: ci.yml git-diff-scoped shell check (recommended) — only checks
  files added in the current PR so it doesn't require fixing existing stale
  entries before merging; greps for module directory name in prose
- Option C: Structured TOML module registry (over-engineered for scale)
Includes concrete YAML snippet for Option B grounded in the check:repo-links.ts
pattern already established in docs/scripts/

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

* docs(roadmap): iterations 36-37 — CI gate + greenfield workspace architecture

Iteration 36:
- §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with
  3 options (CONTRIBUTING.md rule / ci.yml git-diff check / TOML registry);
  recommend Option B (git-diff-scoped YAML step) with concrete snippet grounded
  in existing check:repo-links.ts pattern from docs/scripts/

Iteration 37 (operator directive: greenfield Rust structure):
- §4: Add "Greenfield architecture — ideal structure for a growing project"
  section based on verified cross-module dependency graph (grep iteration 37)
- Confirms dependency tiers: workspace/manifest/docker/paths/selector = Tier 0;
  config/tui/instance = Tier 1; operator_env/runtime/repo = Tier 2; console = Tier 3
- Key finding: workspace/ is LOWER-level than config/ (config re-exports workspace
  types at lines 5-6); ideal naming inverted in greenfield (jackin-core > jackin-config)
- Documents ideal 6-crate workspace: jackin-core, jackin-config, jackin-tui,
  jackin-runtime, jackin-console, jackin-shell + thin binary
- Notes console/ has NO runtime/ import — cleanest pre-existing crate boundary
- Bridge: incremental splits (4a, 4d, 4g) are pre-work toward workspace migration

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

* docs(roadmap): iteration 38 — Rust workspace standards, community evidence

Ground workspace recommendation in real-world project research:
- ripgrep (9 crates), gitui (5 crates) went workspace due to library consumers
- starship and fd-find stay single-crate at 1M+ LOC — no library use case
- jackin (43K LOC, no external consumers) maps to starship/fd pattern
  → single-crate is community-standard; "stay single-crate" recommendation confirmed

Update greenfield workspace structure to follow matklad's pattern:
- Virtual manifest at root (no [package] in root Cargo.toml)
- Flat crates/ directory (not nested); crate names match folder names
- version = "0.0.0" for unpublished internal crates
- Add inline dep comments to each crate in the ASCII structure

Add research notes: ripgrep/starship/gitui/fd-find Cargo.toml findings +
Cargo workspaces reference + matklad "Large Rust Workspaces" (2021-08-22)

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

* docs(roadmap): revise §7.9 + §3 — adopt per-directory README.md

§7.9: Reverse previous "reject" recommendation to "adopt" per-directory
README.md for major src/ module directories. Rationale: README.md is
AI-native — Claude Code, Copilot, Cursor load it automatically on directory
entry, giving AI agents orientation before they decide which file to open.
PROJECT_STRUCTURE.md being confirmed stale removes the main argument for
the "single root file" approach.

Add three-layer documentation model table:
- README.md: directory orientation (AI + human, on entry)
- AGENTS.md: agent workflow rules (root, session start)
- CLAUDE.md: @AGENTS.md pointer only — NEVER add content here
- //! docs: file-level contracts (when reading/editing)

Add specific README.md content targets for 7 directories
(src/, src/runtime/, src/console/, src/console/manager/,
src/console/widgets/, docs/, docs/internal/).

§3 target document shape: Add per-directory README.md to proposed
hierarchy; add docs/internal/specs/ explicitly; note CLAUDE.md
design principle (single-line @AGENTS.md — never duplicate content).

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

* docs(roadmap): internal docs are browsable — unified Starlight site

Operator directive: internal docs (architecture, specs, ADRs, roadmap) should
be browsable, not hidden filesystem files. They are a different TYPE of docs
focused on implementation details and vision, published as a "Developer
Reference" section of the Starlight site.

§3 target document shape:
- docs/internal/ moves into docs/src/content/docs/internal/ (Starlight pages)
- Browsable at jackin.tailrocks.com/internal/
- Sidebar: "Developer Reference" group (collapsed by default) with sub-sections
  for architecture, code-tour, contributing, testing, decisions, specs, roadmap
- Include astro.config.ts sidebar config snippet

§8.1 two-tier spec distinction eliminated:
- Feature specs and behavioral specs both live at docs/src/content/docs/internal/specs/
- Type expressed via spec_type: behavioral | feature frontmatter, not filesystem location
- Both browsable and searchable via Starlight; AI agents can be pointed to URLs

§8.3 + §4:
- All docs/internal/specs/ paths → docs/src/content/docs/internal/specs/
- ADRs: docs/internal/decisions/ → docs/src/content/docs/internal/decisions/ (browsable)
- README.md pointer for src/runtime/ updated to URL reference

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

* docs(roadmap): §11 — modern Rust docs platform (future project)

Add §11 capturing the vision for a modern docs.rs alternative with:
- rustdoc JSON ingestion → Astro Starlight presentation
- MCP server for AI agent queries (Context7 alternative for Rust)
- Rust-specific query types: rust_get_context(), rust_find_impls(),
  rust_search_types() — things Context7 cannot provide
- Comparison table vs Context7
- Architecture diagram (ingestion → processing → Starlight + MCP)
- Name candidates: rustlight, ferrodoc, cargo-starlight / starlight.rs
- Note that jackin's §7.15 gen-rust-api.ts pipeline is the intentional
  prototype for the platform's processing and presentation layers

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

* docs(roadmap): iteration 39 — update §0, fix stale internal/ paths

§0 executive summary: rewrite to reflect decisions from iterations 30-38:
- browsable internal docs (jackin.tailrocks.com/internal/)
- per-directory README.md adoption (§7.9 reversed)
- CLAUDE.md = @AGENTS.md single-line pointer only
- greenfield workspace architecture (matklad's virtual manifest pattern)
- §11 future project: modern Rust docs platform / Context7-for-Rust
- document size 1800+ → 2200+

Fix stale docs/internal/ bare paths not caught by iteration 38 sweep:
- Mermaid diagram: INTERNAL_ROADMAP, INTERNAL_CODE_TOUR → Starlight paths
- §7.10 ADRs: docs/internal/decisions/NNN-title.md → .mdx Starlight path
- §10 Track B item 2: op-picker spec path → Starlight MDX

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

* docs(roadmap): iteration 40 — §7.15 pipeline + Rule 4 pub audit

§7.15 (new): rustdoc JSON → Astro Starlight API documentation pipeline
- Three options: rustdoc HTML publish / rustdoc JSON + bun script (recommended)
  / rustdoc-json crate as Rust binary
- Option B recommended: matches existing docs/scripts/ pattern, nightly
  isolated to separate CI step, zero effect on stable build
- Key design: URL at /internal/api/, cross-links to behavioral specs,
  Starlight unified search, prototype for §11 future project
- Pub(crate) note: gen-rust-api.ts can feed Rule 4 visibility audit
- Recommend: adopt after Phase 1 //! sprint (value ∝ coverage)

§4 Rule 4 pub discipline: replace estimated "50-100 items" guess with
verified numbers from iteration 40 grep:
- 257 bare pub items, 21 pub(crate), 61 pub(super) across 94 files
- 0 uses of unreachable_pub lint — no enforcement gate
- Top violators: operator_env.rs (17), tui/output.rs (13), planner.rs (8)
- Add concrete Cargo.toml [lints.rust] snippet: unreachable_pub = "warn"
- Revised scope: ~150-200 mechanical conversions (excludes entry points)

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

* docs(roadmap): split research into 19 actionable items

Delete _research_notes.md (no longer needed).

Replace 2343L READABILITY_AND_MODERNIZATION.md with:
- README.md: index of all 19 items with phase, ordering notes, links
- READABILITY_AND_MODERNIZATION.md: lightweight research summary (63L)
- items/ITEM-001 through ITEM-019: individual actionable items

Items by phase:
  Phase 1 (low risk, no confirmation): ITEM-001..004, 006..011
  Phase 1 (needs confirmation): ITEM-005, 016, 018
  Phase 2 (structural splits, confirmation required): ITEM-012..015
  Phase 3 (deferred): ITEM-017 (rustdoc pipeline), ITEM-019 (workspace)

Each item has: summary, key files with line numbers, steps, what
needs confirmation, and relevant research backing from the 40-iteration
analysis loop.

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

* docs(roadmap): migrate 19 items to Starlight reference/roadmap section

Move all codebase health roadmap items from docs/internal/roadmap/items/
(plain Markdown, not browsable) to docs/src/content/docs/reference/roadmap/
(MDX pages, browsable at jackin.tailrocks.com/reference/roadmap/).

Adds a new "Codebase health" sidebar group (Phase 1 → Phase 3) to
astro.config.ts. Deletes the old items/ directory. Updates the internal
README to redirect to the new location.

Also adds codebase-readability.mdx — a new overview item that captures
the overall readability/restructuring program with a recommended execution
order: file splits first, then greenfield workspace, then per-directory
README+AGENTS.md, then docs and specs.

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

* docs(roadmap): remove premature internal/roadmap/README.md

The internal/ structure doesn't exist yet — it will be created as part
of the roadmap items themselves. No need for a redirect stub now.

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

* docs(roadmap): remove READABILITY_AND_MODERNIZATION.md research archive

All content has been distilled into the individual Starlight roadmap pages.
The full 2343L research is preserved in git history at commit b7e9fc2.

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

* docs(roadmap): fix check:repo-links errors + remove iteration log

- Replace plain code spans with <RepoFile> for validate.rs, mise.toml,
  Cargo.toml, and op_picker/mod.rs
- Remove deleted READABILITY_AND_MODERNIZATION.md reference from
  codebase-readability.mdx
- Delete _iteration_log.md (git history is the archive)

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

* docs(roadmap): fix lychee false-positive link in move-contributing-testing

The example redirect text contained a markdown hyperlink to a proposed
future file path that doesn't exist yet. Changed to a code span.

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

---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>
Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
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>
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
…183)

* docs(roadmap): iteration 13 — AI code verifiability framing, config/types.rs full spec

Primary goal shift: codebase must be verifiable for AI-generated code.

- §0: replace generic description with explicit verifiability rationale
  (module contracts, localised concerns, types/behaviour separation)
- §4 intro: add "Why structure matters for AI-generated code" section with
  audit-units table mapping each post-split file to one reviewable question
- §4 4a: expand config/types.rs from description to full execution spec —
  exact type list, post-split mod.rs content, zero-change submodule
  guarantee (verified: agents.rs/persist.rs/workspaces.rs use super::T
  which resolves through mod.rs re-exports unchanged), impl-extension
  pattern already in use documented

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

* docs(roadmap): iteration 14 — editor method map, app helpers, //! queue

- §4 4c: config/editor.rs split is now execution-ready — complete
  6-file method-to-file table with private helper placement verified
  (validate_candidate→io.rs, table_path_mut→mod.rs pub(super),
  auth_forward_str→agent_ops.rs, create_workspace delegates to AppConfig)
- §4 4e: app/mod.rs split complete — all private helpers mapped
  (parse_auth_forward_mode_from_cli→config_cmd.rs,
  workspace_env_scope→workspace_cmd.rs, print_env_table note,
  remove_data_dir_if_exists→dispatch.rs)
- §10 step 5: add //! priority queue — 10 files with draft content,
  prioritised by cold-landing impact and AI audit risk; selector.rs
  and instance/mod.rs explicitly document the /→__ invariant

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

* docs(roadmap): iteration 15 — dep graph fix, trust.rs safety, OQ1 closed

- §4 4d: correct operator_env dependency graph — layers.rs imports
  both mod.rs (OpRunner) AND client.rs (OpCli for non-injectable
  resolve_operator_env wrapper at line 797); still a valid DAG
- §4 4f: verify trust.rs split safety — FnOnce injection pattern means
  launch_pipeline.rs has zero dependency on trust.rs; import chain
  documented; trust bypass audit now requires reading only ~60L
- §9 OQ1 closed: op_cache.rs read in full — 4-level structure,
  per-level invalidation, no TTL/expiry (expiry handled at OpCli
  subprocess level), DEFAULT_ACCOUNT_KEY sentinel documented

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

* docs(roadmap): iteration 16 — CommandRunner Rule 3, render/editor split, 4a/4c independence

- Fix duplicate Rule 3 section introduced by previous edit; add docker.rs
  co-location note as third edge case (three edge cases, not two)
- Add render/editor.rs as new Rule 5 violator: 1666L post-PR #171
  (was listed as 782L); propose 6-file tab-by-tab split with auditability
  note on the security-adjacent Secrets tab
- Add §10 execution-order note: 4a and 4c are independent — editor.rs
  imports AppConfig via crate::config re-exports regardless of 4a order
- Append iteration 16 log entry with confidence table and weakest sections

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

* docs(roadmap): iteration 17 — instance/auth.rs audit, state.rs split, line count corrections

- Add instance/auth.rs to //! priority queue at #4: four security
  invariants (0o600 perms, symlink rejection, TOCTOU-safe writes,
  macOS Keychain) documented in draft //! content
- Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types
  mixed with impl blocks; propose 5-file types/behavior split
- Correct stale line counts: render/list.rs 1122→1989 (PR #171 added
  render_environments_subpanel); state.rs 865→992; priorities upgraded
- Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332,
  mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944
- Renumber //! priority queue to 11 entries (was 10)

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

* docs(roadmap): iteration 18 — agent_allow OQ2 closed, render/list.rs split proposal

- Close OQ2: agent_allow.rs read in full — 55L, correct //! doc,
  design sound; serves as model for //! priority queue pattern
- Add render/list.rs as new Rule 5 violator: 668L production (PR #171
  added render_environments_subpanel); propose 3-file split (mod.rs,
  details.rs, subpanels.rs); note import-path change for agents_block_agent_count
- Update §1 module map: agent_allow.rs entry corrected with size/API

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

* docs(roadmap): iteration 19 — input/editor.rs critical correction, split proposal

- Correct input/editor.rs: 2349L total (was 1304L), 1141L production
  (was 547L) — PR #171 added Secrets-tab handlers; pub(super) fn
  handle_editor_modal at line 618 was invisible to previous grep pattern;
  now the largest production file in the codebase; priority → Critical
- Correct input/save.rs: 1472L total, 661L production (was 567L)
- Add 5-file split proposal for input/editor.rs: mod.rs (two dispatchers),
  secrets.rs (~500L AI-generated Secrets-tab), agents.rs, mounts.rs, general.rs
- Update key insight paragraph naming input/editor.rs as largest production file

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

* docs(roadmap): iteration 20 — console splits in §10, MSRV evidence, animation.rs verdict

- Add console/manager/ as §10 Step 4f group with 5 sub-steps in priority
  order; rename existing 4f (launch.rs) → 4g; add circular-import risk note
  for ManagerStage/EditorState split sequencing
- Analyze tui/animation.rs: 582L all-production, no split needed (banner_grid
  is a tightly-coupled rendering loop); section comments compensate for missing //!
- Partially close OQ3: u64::is_multiple_of (stabilized 1.86) found in animation.rs;
  within declared MSRV 1.94; full cargo +1.94.0 check deferred (toolchain unavailable)

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

* docs(roadmap): iteration 21 — input/save.rs split, //! queue fix, save.rs corrections

- Add input/save.rs split proposal: 4 pub(super) fns discovered; 3-file
  split (mod.rs + flow.rs + preview.rs); no cross-dependency between
  flow and preview groups; §10 4f-v updated from Optional to concrete plan
- Fix //! queue preamble: "first 10 files" → "first 11 files"
- Correct save.rs module map (1418→1472L, correct key exports) and
  hot-spot table note (begin_editor_save ~280L → ~118L; commit_editor_save
  is the Phase 2 partner at ~149L)

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

* docs(roadmap): iteration 22 — input/list.rs and mount_info.rs analysis

- Analyze input/list.rs: 214L production (tests at 215); has //! doc;
  two focused pub(super) fns; no split needed; Low priority; correct module map
- Add mount_info.rs to hot-spot table: 277L production; Low priority;
  has //! doc; correct module map with 3 public enums + inspect fn
- Fix stale §2 diagnosis note: docs/internal/roadmap/ now exists

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

* docs(roadmap): iteration 23 — audit units table +5 console rows, input/mod.rs corrected

- Expand audit units table from 8 to 13 entries: add state/types.rs,
  state/editor.rs, input/editor/secrets.rs, render/list/subpanels.rs,
  input/save/preview.rs — all targeting PR #171 AI-generated console code
- Add PR #171 context note linking 5 new entries to AI-generated code concern
- Correct input/mod.rs module map: 369L, add InputOutcome enum to exports
- Verify rust-toolchain.toml absence; §7.7 and §2 concept 25 already correct

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

* docs(roadmap): iteration 24 — render/mod.rs analysis, //! exemplars table, EditorTab confirmed

- Add §4 Rule 7 positive exemplars table: 7 files with //! docs graded
  1-element (render/mod.rs), 2-element (input/save.rs etc), 3-element
  (env_model.rs, agent_allow.rs); PR #171 docs-discipline pattern noted
- Correct render/mod.rs module map: 421L; FooterItem + palette constants
  + render_header + centered_rect_fixed added to key exports
- Confirm EditorTab variants: General, Mounts, Agents, Secrets (Rust enum)
  vs "Secrets / Environments" (UI label); /stub qualifier already removed

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

* docs(roadmap): iteration 25 — too_many_lines recount, FooterItem PR, MountConfig caveat

- Correct too_many_lines count: 13 across 8 → 16 across 11 files
  (PR #171 added 5 suppressions in console/manager); add full breakdown
  table; update all 3 occurrences in roadmap
- Fix FooterItem PR reference: #165#166 (confirmed by git log --follow)
- Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description

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

* docs(roadmap): iteration 26 — console/mod.rs and op_picker/render.rs analyzed

- Add console/mod.rs to hot-spot table: 406L/307L production (Low);
  correct module map from ~200 → 406L; note missing //! doc with
  ConsoleStage design block comment worth promoting
- Add op_picker/render.rs to hot-spot table: 865L/545L production
  (Medium); PR #171 AI-generated; 14 functions in two logical groups
  (entry/helpers vs level renderers); split into levels.rs proposed
- Correct 3 stale ~200L estimates for console/mod.rs across roadmap

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

* docs(roadmap): iteration 27 — op_picker/mod.rs discovery, render split, operator_env correction

- Add op_picker/mod.rs to hot-spot table: 1712L/775L production (High);
  PR #171 AI-generated; OpPickerState types+behavior split opportunity;
  has 7-line //! doc; module map split into two rows (mod.rs + render.rs)
- Add op_picker/render.rs 2-file split proposal: render.rs (coordinator)
  + render_pane.rs (pane/level renderers); no cross-dependency confirmed
- Correct operator_env.rs total: 1569→2130L (880L production); update
  4 occurrences across hot-spot table, ASCII tree, §4 analysis

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

* docs(roadmap): iteration 28 — op_picker/mod.rs 3-file split, count corrections

- Add op_picker/mod.rs formal 3-file split: loading.rs (async load family
  ~120L) + keys.rs (4 level key handlers ~315L) + mod.rs (types/constructors)
- Correct "24 files" → "28+" for 500L threshold count
- Update total LOC: ~40,664 → ~43,587 (2 occurrences, with provenance note)

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

* docs(roadmap): iteration 29 — op_picker execution order + file_browser analysis

- §10 Step 4f: expand from 5 to 7 sub-steps; add 4f-vi (op_picker/mod.rs
  → mod.rs + loading.rs + keys.rs) and 4f-vii (op_picker/render.rs →
  render.rs + pane.rs); document impl-extension and import-path caveats
- §4 //! exemplars: add file_browser/ subsystem analysis — all 5 files
  have //! docs, no file exceeds ~350L production; classified as exemplar
  (not a split candidate); document git_prompt.rs coupling-density
  justification and input.rs as 28-file false positive (144L production)
- §1 module map: expand single file_browser/ row to 5 individual rows
  with production LOC and dominant concern per file

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

* docs(roadmap): iteration 30 — challenge split-first thesis, fresh LOC corrections

- §4: Add "Alternative thesis: documentation-first verification" — challenges
  the two core assumptions behind file splitting (files-as-audit-unit and
  file-size-as-context-constraint); adds 7-criterion comparison table vs
  structure-first approach; introduces phased combined recommendation:
  Phase 1 = doc sprint (//! contracts + specs/ for 3 subsystems, 2-3 PRs,
  zero structural change); Phase 2 = splits only for >600L production files
  (reduces scope from 14+ to 4 files); Phase 3 = workspace if LOC > 150K
- Fix stale LOC: app/mod.rs 951→979, config/editor.rs 1467→1548 (7 and
  8 locations respectively; verified by fresh find|xargs wc -l scan)
- §1 module map: add agent_picker.rs (436L), scope_picker.rs (201L),
  source_picker.rs (244L) — all PR #171 additions with //! docs

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

* docs(roadmap): iteration 31 — fix 600L→800L threshold error, correct LOC

- §4 alternative thesis: correct ">600L production → 4 files" claim
  introduced in iteration 30; re-verified all 9 candidate files via
  #[cfg(test)] line position; threshold must be >800L to get exactly 4
  files (9 exceed 600L); add verification table with test-start lines
- Production LOC corrections (5+ locations each):
  launch.rs 1085→~1077, operator_env.rs 810→~880,
  app/mod.rs 928→~957, config/editor.rs 503→~584
- §2 OpPicker row: replace vague "no entry yet" with confirmed gap:
  PROJECT_STRUCTURE.md line 53 still lists pre-PR#171 widget set (10
  named); omits op_picker/, agent_picker.rs, scope_picker.rs,
  source_picker.rs and pre-dates the manager/ sub-structure split

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

* docs(roadmap): iteration 32 — two-tier spec arch, behavioral spec template

- §8.1: Add two-tier spec architecture table distinguishing feature specs
  (public Starlight MDX, user-facing) from behavioral specs (internal
  docs/internal/specs/, for AI code verification) — resolves contradiction
  between §4 (which said docs/internal/specs/) and §8.1 (which said
  "no longer needed; specs are public")
- §8.1: Add concrete behavioral spec template for op_picker/ with state
  machine table and 3 INV invariant entries each with a grep-executable
  "Verify by:" command; template directly usable for the 3 Phase 1 specs
- §8.1: Remove erroneous "docs/internal/specs/ no longer needed" claim
- Confirmed render/editor.rs ~736L and render/list.rs ~668L production
  (no interspersed production code — all test blocks follow consecutively)

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

* docs(roadmap): iteration 33 — executive summary, §0 correctness

- §0: Add executive summary (~300 words) with core problem, 3-phase
  recommendation, key counter-argument, and navigation table pointing
  to §2/§4/§7/§8/§10 by question — resolves the meta-irony of a
  readability roadmap with no entry-point orientation
- §0 item 2: "1569-line monolith" → "2130-line monolith" (operator_env.rs
  current verified size; stale reference was in the first section readers see)
- §0 item 3: Add "(selective)" qualifier and explicit note that standard
  Rust co-locates struct+impl — impl-extension pattern is justified only
  for files >800L production, not as a universal rule

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

* docs(roadmap): iteration 34 — spec priority reorder, §10 Phase 1 track

- §0 + §4 Phase 1: Prioritize runtime/launch.rs behavioral spec (no //!
  doc, ~1077L production, critical path — all jackin load failures trace
  here); drop config/editor.rs from Phase 1 (its 963L test suite already
  serves as behavioral spec — tests are behavioral examples); reduce Phase
  1 from 3 specs to 2 specs; add reasoning for the priority ordering
- §10 Step 2: Split into two parallel tracks — Track A (cc-sdd tooling
  setup) + Track B (Phase 1 behavioral spec authoring); Track B includes
  specific INV invariants to capture for runtime/launch.rs grounded in
  reading the actual function structure (step comment positions); adds
  sequencing rationale: spec must precede structural splits

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

* docs(roadmap): iteration 35 — verified INV entries for runtime/launch.rs

Read load_agent_with lines 553-892 in full. Replaced 3 draft INVs from
iteration 34 (inferred from step comment positions) with 5 verified INVs
citing exact line numbers:
- INV-1: trust gate (line 594) precedes image build (line 736)
- INV-2: container name claimed (line 754) between image build and network
- INV-3: token verified (line 763) before network creation (line 827)
- INV-4: render_exit called at lines 886 AND 890 (all exit paths)
- INV-5: cleanup disarm semantics — Running→disarm, clean exit→cleanup,
  crash→disarm (explains jackin hardline compatibility)
Corrected wrong line number: claim_container_name call is at 754, not 918
(918 is the function definition). Each INV has a grep-executable Verify by.

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

* docs(roadmap): iteration 36 — CI gate for PROJECT_STRUCTURE.md freshness

§3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with
three concrete options:
- Option A: CONTRIBUTING.md rule (necessary but insufficient)
- Option B: ci.yml git-diff-scoped shell check (recommended) — only checks
  files added in the current PR so it doesn't require fixing existing stale
  entries before merging; greps for module directory name in prose
- Option C: Structured TOML module registry (over-engineered for scale)
Includes concrete YAML snippet for Option B grounded in the check:repo-links.ts
pattern already established in docs/scripts/

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

* docs(roadmap): iterations 36-37 — CI gate + greenfield workspace architecture

Iteration 36:
- §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with
  3 options (CONTRIBUTING.md rule / ci.yml git-diff check / TOML registry);
  recommend Option B (git-diff-scoped YAML step) with concrete snippet grounded
  in existing check:repo-links.ts pattern from docs/scripts/

Iteration 37 (operator directive: greenfield Rust structure):
- §4: Add "Greenfield architecture — ideal structure for a growing project"
  section based on verified cross-module dependency graph (grep iteration 37)
- Confirms dependency tiers: workspace/manifest/docker/paths/selector = Tier 0;
  config/tui/instance = Tier 1; operator_env/runtime/repo = Tier 2; console = Tier 3
- Key finding: workspace/ is LOWER-level than config/ (config re-exports workspace
  types at lines 5-6); ideal naming inverted in greenfield (jackin-core > jackin-config)
- Documents ideal 6-crate workspace: jackin-core, jackin-config, jackin-tui,
  jackin-runtime, jackin-console, jackin-shell + thin binary
- Notes console/ has NO runtime/ import — cleanest pre-existing crate boundary
- Bridge: incremental splits (4a, 4d, 4g) are pre-work toward workspace migration

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

* docs(roadmap): iteration 38 — Rust workspace standards, community evidence

Ground workspace recommendation in real-world project research:
- ripgrep (9 crates), gitui (5 crates) went workspace due to library consumers
- starship and fd-find stay single-crate at 1M+ LOC — no library use case
- jackin (43K LOC, no external consumers) maps to starship/fd pattern
  → single-crate is community-standard; "stay single-crate" recommendation confirmed

Update greenfield workspace structure to follow matklad's pattern:
- Virtual manifest at root (no [package] in root Cargo.toml)
- Flat crates/ directory (not nested); crate names match folder names
- version = "0.0.0" for unpublished internal crates
- Add inline dep comments to each crate in the ASCII structure

Add research notes: ripgrep/starship/gitui/fd-find Cargo.toml findings +
Cargo workspaces reference + matklad "Large Rust Workspaces" (2021-08-22)

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

* docs(roadmap): revise §7.9 + §3 — adopt per-directory README.md

§7.9: Reverse previous "reject" recommendation to "adopt" per-directory
README.md for major src/ module directories. Rationale: README.md is
AI-native — Claude Code, Copilot, Cursor load it automatically on directory
entry, giving AI agents orientation before they decide which file to open.
PROJECT_STRUCTURE.md being confirmed stale removes the main argument for
the "single root file" approach.

Add three-layer documentation model table:
- README.md: directory orientation (AI + human, on entry)
- AGENTS.md: agent workflow rules (root, session start)
- CLAUDE.md: @AGENTS.md pointer only — NEVER add content here
- //! docs: file-level contracts (when reading/editing)

Add specific README.md content targets for 7 directories
(src/, src/runtime/, src/console/, src/console/manager/,
src/console/widgets/, docs/, docs/internal/).

§3 target document shape: Add per-directory README.md to proposed
hierarchy; add docs/internal/specs/ explicitly; note CLAUDE.md
design principle (single-line @AGENTS.md — never duplicate content).

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

* docs(roadmap): internal docs are browsable — unified Starlight site

Operator directive: internal docs (architecture, specs, ADRs, roadmap) should
be browsable, not hidden filesystem files. They are a different TYPE of docs
focused on implementation details and vision, published as a "Developer
Reference" section of the Starlight site.

§3 target document shape:
- docs/internal/ moves into docs/src/content/docs/internal/ (Starlight pages)
- Browsable at jackin.tailrocks.com/internal/
- Sidebar: "Developer Reference" group (collapsed by default) with sub-sections
  for architecture, code-tour, contributing, testing, decisions, specs, roadmap
- Include astro.config.ts sidebar config snippet

§8.1 two-tier spec distinction eliminated:
- Feature specs and behavioral specs both live at docs/src/content/docs/internal/specs/
- Type expressed via spec_type: behavioral | feature frontmatter, not filesystem location
- Both browsable and searchable via Starlight; AI agents can be pointed to URLs

§8.3 + §4:
- All docs/internal/specs/ paths → docs/src/content/docs/internal/specs/
- ADRs: docs/internal/decisions/ → docs/src/content/docs/internal/decisions/ (browsable)
- README.md pointer for src/runtime/ updated to URL reference

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

* docs(roadmap): §11 — modern Rust docs platform (future project)

Add §11 capturing the vision for a modern docs.rs alternative with:
- rustdoc JSON ingestion → Astro Starlight presentation
- MCP server for AI agent queries (Context7 alternative for Rust)
- Rust-specific query types: rust_get_context(), rust_find_impls(),
  rust_search_types() — things Context7 cannot provide
- Comparison table vs Context7
- Architecture diagram (ingestion → processing → Starlight + MCP)
- Name candidates: rustlight, ferrodoc, cargo-starlight / starlight.rs
- Note that jackin's §7.15 gen-rust-api.ts pipeline is the intentional
  prototype for the platform's processing and presentation layers

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

* docs(roadmap): iteration 39 — update §0, fix stale internal/ paths

§0 executive summary: rewrite to reflect decisions from iterations 30-38:
- browsable internal docs (jackin.tailrocks.com/internal/)
- per-directory README.md adoption (§7.9 reversed)
- CLAUDE.md = @AGENTS.md single-line pointer only
- greenfield workspace architecture (matklad's virtual manifest pattern)
- §11 future project: modern Rust docs platform / Context7-for-Rust
- document size 1800+ → 2200+

Fix stale docs/internal/ bare paths not caught by iteration 38 sweep:
- Mermaid diagram: INTERNAL_ROADMAP, INTERNAL_CODE_TOUR → Starlight paths
- §7.10 ADRs: docs/internal/decisions/NNN-title.md → .mdx Starlight path
- §10 Track B item 2: op-picker spec path → Starlight MDX

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

* docs(roadmap): iteration 40 — §7.15 pipeline + Rule 4 pub audit

§7.15 (new): rustdoc JSON → Astro Starlight API documentation pipeline
- Three options: rustdoc HTML publish / rustdoc JSON + bun script (recommended)
  / rustdoc-json crate as Rust binary
- Option B recommended: matches existing docs/scripts/ pattern, nightly
  isolated to separate CI step, zero effect on stable build
- Key design: URL at /internal/api/, cross-links to behavioral specs,
  Starlight unified search, prototype for §11 future project
- Pub(crate) note: gen-rust-api.ts can feed Rule 4 visibility audit
- Recommend: adopt after Phase 1 //! sprint (value ∝ coverage)

§4 Rule 4 pub discipline: replace estimated "50-100 items" guess with
verified numbers from iteration 40 grep:
- 257 bare pub items, 21 pub(crate), 61 pub(super) across 94 files
- 0 uses of unreachable_pub lint — no enforcement gate
- Top violators: operator_env.rs (17), tui/output.rs (13), planner.rs (8)
- Add concrete Cargo.toml [lints.rust] snippet: unreachable_pub = "warn"
- Revised scope: ~150-200 mechanical conversions (excludes entry points)

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

* docs(roadmap): split research into 19 actionable items

Delete _research_notes.md (no longer needed).

Replace 2343L READABILITY_AND_MODERNIZATION.md with:
- README.md: index of all 19 items with phase, ordering notes, links
- READABILITY_AND_MODERNIZATION.md: lightweight research summary (63L)
- items/ITEM-001 through ITEM-019: individual actionable items

Items by phase:
  Phase 1 (low risk, no confirmation): ITEM-001..004, 006..011
  Phase 1 (needs confirmation): ITEM-005, 016, 018
  Phase 2 (structural splits, confirmation required): ITEM-012..015
  Phase 3 (deferred): ITEM-017 (rustdoc pipeline), ITEM-019 (workspace)

Each item has: summary, key files with line numbers, steps, what
needs confirmation, and relevant research backing from the 40-iteration
analysis loop.

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

* docs(roadmap): migrate 19 items to Starlight reference/roadmap section

Move all codebase health roadmap items from docs/internal/roadmap/items/
(plain Markdown, not browsable) to docs/src/content/docs/reference/roadmap/
(MDX pages, browsable at jackin.tailrocks.com/reference/roadmap/).

Adds a new "Codebase health" sidebar group (Phase 1 → Phase 3) to
astro.config.ts. Deletes the old items/ directory. Updates the internal
README to redirect to the new location.

Also adds codebase-readability.mdx — a new overview item that captures
the overall readability/restructuring program with a recommended execution
order: file splits first, then greenfield workspace, then per-directory
README+AGENTS.md, then docs and specs.

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

* docs(roadmap): remove premature internal/roadmap/README.md

The internal/ structure doesn't exist yet — it will be created as part
of the roadmap items themselves. No need for a redirect stub now.

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

* docs(roadmap): remove READABILITY_AND_MODERNIZATION.md research archive

All content has been distilled into the individual Starlight roadmap pages.
The full 2343L research is preserved in git history at commit b7e9fc2.

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

* docs(roadmap): fix check:repo-links errors + remove iteration log

- Replace plain code spans with <RepoFile> for validate.rs, mise.toml,
  Cargo.toml, and op_picker/mod.rs
- Remove deleted READABILITY_AND_MODERNIZATION.md reference from
  codebase-readability.mdx
- Delete _iteration_log.md (git history is the archive)

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

* docs(roadmap): fix lychee false-positive link in move-contributing-testing

The example redirect text contained a markdown hyperlink to a proposed
future file path that doesn't exist yet. Changed to a code span.

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

---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.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
- Move 9 TODO items from monolithic TODO.md into separate files in todo/
- Each file is a self-contained design doc with problem, options, and
  related source files for easy agent handoff
- Mark resolved security findings (#3, #4, #6, #7) in SECURITY_REVIEW_FINDINGS.md
- Update PROJECT_STRUCTURE.md with todo/ section and TESTING.md entry
- TODO.md becomes an index pointing to todo/ files

Co-Authored-By: Claude Opus 4.6 (1M context) <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
Co-authored-by: Claude Opus 4.6 (1M context) <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
* feat: implement trust-on-first-use model for third-party agent sources

Add a trust gate that prompts operators before building untrusted
third-party agents. Built-in agents are always trusted. New namespaced
agents default to untrusted and require explicit confirmation on first
use. The trusted flag is persisted in config.toml so subsequent runs
proceed without prompts. Non-interactive sessions bail with a clear
error for untrusted sources.

Also syncs the roadmap with TODO items: adds missing completed item
(JACKIN_DIND_HOSTNAME), adds missing planned items (Bollard migration),
and marks the agent source trust security finding as resolved.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* improve trust prompt with clear risk details and mark finding #9 resolved

Rewrite the trust confirmation message to clearly explain what trusting
an agent means: Dockerfile execution, arbitrary commands on the host,
and workspace file access. The non-interactive error now tells the user
how to pre-trust via config.toml. The decline message explains how to
retry.

Also mark security finding #9 (mount policy guardrails) as resolved
since sensitive mount warnings were already implemented.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: address PR review — docs, stale comment, save clarity, test, serde

Review fixes for the agent source trust model:

- Fix duplicate doc comment on trust_agent() (copy-paste from
  sync_builtin_agents)
- Replace confusing `!source.trusted` save condition with explicit
  `newly_trusted` flag for readability
- Add unit test for non-interactive trust gate (verifies the error
  includes agent name and git URL)
- Skip serializing `trusted = false` to keep config.toml clean
  (only `trusted = true` appears)
- Document trust in load.mdx (step 3 in "What happens", warning
  callout for CI) and security-model.mdx (new section 4 with
  config.toml example, threat model row, best practices update)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: broken docs anchor link and misleading test name

- Fix cross-reference link in load.mdx: use #4-agent-source-trust
  (matching the numbered heading in security-model.mdx)
- Rename test to `load_trusted_namespaced_agent_builds_and_runs` since
  it no longer exercises auto-registration (that's covered by the
  config test `resolve_agent_source_adds_owner_repo_on_first_use`)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* feat: add jackin trust/untrust CLI, testable trust gate, revocation

Address all follow-up items from the maintainer review:

- Extract load_agent_with() with injected trust callback, following the
  resolve_agent_repo_with() pattern. Tests can now exercise accept and
  decline paths without mocking dialoguer.
- Restore auto-registration test coverage: the accept test uses
  load_agent_with(auto_trust) so resolve_agent_source's is_new path is
  exercised again. Add decline test verifying no config persistence and
  no Docker commands when trust is refused.
- Add `jackin trust <selector>` and `jackin untrust <selector>` CLI
  commands so operators can manage trust without editing config.toml.
- Add untrust_agent() to AppConfig with tests.
- Document trust/untrust CLI in security-model.mdx and load.mdx.
- Document that trust is keyed by selector (not URL) with a note about
  remote-mismatch protection.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: consolidate trust/untrust into single CLI command

Follow mise's pattern: `jackin trust` is one command with flags
instead of separate trust/untrust subcommands.

  jackin trust <selector>              # grant trust
  jackin trust <selector> --untrust    # revoke trust
  jackin trust <selector> --show       # check status

The --show and --untrust flags conflict with each other (enforced
by clap).

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: move trust CLI under config with grant/revoke/list subcommands

Follow the same pattern as `jackin config mount {add,remove,list}`:

  jackin config trust grant chainargos/the-architect
  jackin config trust revoke chainargos/the-architect
  jackin config trust list

This keeps all config mutations under `jackin config` and uses the
subcommand pattern consistently throughout the CLI. The `list`
subcommand shows all currently trusted agents.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: refuse to revoke trust on builtins, add CLI parsing tests

- is_builtin_agent() check prevents revoking trust on built-in agents
  (agent-smith, the-architect) which would be silently re-granted by
  sync_builtin_agents on next load
- Add 3 CLI parsing tests for config trust {grant, revoke, list},
  matching the existing coverage for config mount {add, remove, list}
- Tighten TrustCommand::List doc comment to "List all currently
  trusted agent sources"

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

---------

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
…183)

* docs(roadmap): iteration 13 — AI code verifiability framing, config/types.rs full spec

Primary goal shift: codebase must be verifiable for AI-generated code.

- §0: replace generic description with explicit verifiability rationale
  (module contracts, localised concerns, types/behaviour separation)
- §4 intro: add "Why structure matters for AI-generated code" section with
  audit-units table mapping each post-split file to one reviewable question
- §4 4a: expand config/types.rs from description to full execution spec —
  exact type list, post-split mod.rs content, zero-change submodule
  guarantee (verified: agents.rs/persist.rs/workspaces.rs use super::T
  which resolves through mod.rs re-exports unchanged), impl-extension
  pattern already in use documented

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

* docs(roadmap): iteration 14 — editor method map, app helpers, //! queue

- §4 4c: config/editor.rs split is now execution-ready — complete
  6-file method-to-file table with private helper placement verified
  (validate_candidate→io.rs, table_path_mut→mod.rs pub(super),
  auth_forward_str→agent_ops.rs, create_workspace delegates to AppConfig)
- §4 4e: app/mod.rs split complete — all private helpers mapped
  (parse_auth_forward_mode_from_cli→config_cmd.rs,
  workspace_env_scope→workspace_cmd.rs, print_env_table note,
  remove_data_dir_if_exists→dispatch.rs)
- §10 step 5: add //! priority queue — 10 files with draft content,
  prioritised by cold-landing impact and AI audit risk; selector.rs
  and instance/mod.rs explicitly document the /→__ invariant

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

* docs(roadmap): iteration 15 — dep graph fix, trust.rs safety, OQ1 closed

- §4 4d: correct operator_env dependency graph — layers.rs imports
  both mod.rs (OpRunner) AND client.rs (OpCli for non-injectable
  resolve_operator_env wrapper at line 797); still a valid DAG
- §4 4f: verify trust.rs split safety — FnOnce injection pattern means
  launch_pipeline.rs has zero dependency on trust.rs; import chain
  documented; trust bypass audit now requires reading only ~60L
- §9 OQ1 closed: op_cache.rs read in full — 4-level structure,
  per-level invalidation, no TTL/expiry (expiry handled at OpCli
  subprocess level), DEFAULT_ACCOUNT_KEY sentinel documented

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

* docs(roadmap): iteration 16 — CommandRunner Rule 3, render/editor split, 4a/4c independence

- Fix duplicate Rule 3 section introduced by previous edit; add docker.rs
  co-location note as third edge case (three edge cases, not two)
- Add render/editor.rs as new Rule 5 violator: 1666L post-PR #171
  (was listed as 782L); propose 6-file tab-by-tab split with auditability
  note on the security-adjacent Secrets tab
- Add §10 execution-order note: 4a and 4c are independent — editor.rs
  imports AppConfig via crate::config re-exports regardless of 4a order
- Append iteration 16 log entry with confidence table and weakest sections

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

* docs(roadmap): iteration 17 — instance/auth.rs audit, state.rs split, line count corrections

- Add instance/auth.rs to //! priority queue at #4: four security
  invariants (0o600 perms, symlink rejection, TOCTOU-safe writes,
  macOS Keychain) documented in draft //! content
- Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types
  mixed with impl blocks; propose 5-file types/behavior split
- Correct stale line counts: render/list.rs 1122→1989 (PR #171 added
  render_environments_subpanel); state.rs 865→992; priorities upgraded
- Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332,
  mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944
- Renumber //! priority queue to 11 entries (was 10)

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

* docs(roadmap): iteration 18 — agent_allow OQ2 closed, render/list.rs split proposal

- Close OQ2: agent_allow.rs read in full — 55L, correct //! doc,
  design sound; serves as model for //! priority queue pattern
- Add render/list.rs as new Rule 5 violator: 668L production (PR #171
  added render_environments_subpanel); propose 3-file split (mod.rs,
  details.rs, subpanels.rs); note import-path change for agents_block_agent_count
- Update §1 module map: agent_allow.rs entry corrected with size/API

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

* docs(roadmap): iteration 19 — input/editor.rs critical correction, split proposal

- Correct input/editor.rs: 2349L total (was 1304L), 1141L production
  (was 547L) — PR #171 added Secrets-tab handlers; pub(super) fn
  handle_editor_modal at line 618 was invisible to previous grep pattern;
  now the largest production file in the codebase; priority → Critical
- Correct input/save.rs: 1472L total, 661L production (was 567L)
- Add 5-file split proposal for input/editor.rs: mod.rs (two dispatchers),
  secrets.rs (~500L AI-generated Secrets-tab), agents.rs, mounts.rs, general.rs
- Update key insight paragraph naming input/editor.rs as largest production file

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

* docs(roadmap): iteration 20 — console splits in §10, MSRV evidence, animation.rs verdict

- Add console/manager/ as §10 Step 4f group with 5 sub-steps in priority
  order; rename existing 4f (launch.rs) → 4g; add circular-import risk note
  for ManagerStage/EditorState split sequencing
- Analyze tui/animation.rs: 582L all-production, no split needed (banner_grid
  is a tightly-coupled rendering loop); section comments compensate for missing //!
- Partially close OQ3: u64::is_multiple_of (stabilized 1.86) found in animation.rs;
  within declared MSRV 1.94; full cargo +1.94.0 check deferred (toolchain unavailable)

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

* docs(roadmap): iteration 21 — input/save.rs split, //! queue fix, save.rs corrections

- Add input/save.rs split proposal: 4 pub(super) fns discovered; 3-file
  split (mod.rs + flow.rs + preview.rs); no cross-dependency between
  flow and preview groups; §10 4f-v updated from Optional to concrete plan
- Fix //! queue preamble: "first 10 files" → "first 11 files"
- Correct save.rs module map (1418→1472L, correct key exports) and
  hot-spot table note (begin_editor_save ~280L → ~118L; commit_editor_save
  is the Phase 2 partner at ~149L)

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

* docs(roadmap): iteration 22 — input/list.rs and mount_info.rs analysis

- Analyze input/list.rs: 214L production (tests at 215); has //! doc;
  two focused pub(super) fns; no split needed; Low priority; correct module map
- Add mount_info.rs to hot-spot table: 277L production; Low priority;
  has //! doc; correct module map with 3 public enums + inspect fn
- Fix stale §2 diagnosis note: docs/internal/roadmap/ now exists

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

* docs(roadmap): iteration 23 — audit units table +5 console rows, input/mod.rs corrected

- Expand audit units table from 8 to 13 entries: add state/types.rs,
  state/editor.rs, input/editor/secrets.rs, render/list/subpanels.rs,
  input/save/preview.rs — all targeting PR #171 AI-generated console code
- Add PR #171 context note linking 5 new entries to AI-generated code concern
- Correct input/mod.rs module map: 369L, add InputOutcome enum to exports
- Verify rust-toolchain.toml absence; §7.7 and §2 concept 25 already correct

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

* docs(roadmap): iteration 24 — render/mod.rs analysis, //! exemplars table, EditorTab confirmed

- Add §4 Rule 7 positive exemplars table: 7 files with //! docs graded
  1-element (render/mod.rs), 2-element (input/save.rs etc), 3-element
  (env_model.rs, agent_allow.rs); PR #171 docs-discipline pattern noted
- Correct render/mod.rs module map: 421L; FooterItem + palette constants
  + render_header + centered_rect_fixed added to key exports
- Confirm EditorTab variants: General, Mounts, Agents, Secrets (Rust enum)
  vs "Secrets / Environments" (UI label); /stub qualifier already removed

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

* docs(roadmap): iteration 25 — too_many_lines recount, FooterItem PR, MountConfig caveat

- Correct too_many_lines count: 13 across 8 → 16 across 11 files
  (PR #171 added 5 suppressions in console/manager); add full breakdown
  table; update all 3 occurrences in roadmap
- Fix FooterItem PR reference: #165#166 (confirmed by git log --follow)
- Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description

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

* docs(roadmap): iteration 26 — console/mod.rs and op_picker/render.rs analyzed

- Add console/mod.rs to hot-spot table: 406L/307L production (Low);
  correct module map from ~200 → 406L; note missing //! doc with
  ConsoleStage design block comment worth promoting
- Add op_picker/render.rs to hot-spot table: 865L/545L production
  (Medium); PR #171 AI-generated; 14 functions in two logical groups
  (entry/helpers vs level renderers); split into levels.rs proposed
- Correct 3 stale ~200L estimates for console/mod.rs across roadmap

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

* docs(roadmap): iteration 27 — op_picker/mod.rs discovery, render split, operator_env correction

- Add op_picker/mod.rs to hot-spot table: 1712L/775L production (High);
  PR #171 AI-generated; OpPickerState types+behavior split opportunity;
  has 7-line //! doc; module map split into two rows (mod.rs + render.rs)
- Add op_picker/render.rs 2-file split proposal: render.rs (coordinator)
  + render_pane.rs (pane/level renderers); no cross-dependency confirmed
- Correct operator_env.rs total: 1569→2130L (880L production); update
  4 occurrences across hot-spot table, ASCII tree, §4 analysis

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

* docs(roadmap): iteration 28 — op_picker/mod.rs 3-file split, count corrections

- Add op_picker/mod.rs formal 3-file split: loading.rs (async load family
  ~120L) + keys.rs (4 level key handlers ~315L) + mod.rs (types/constructors)
- Correct "24 files" → "28+" for 500L threshold count
- Update total LOC: ~40,664 → ~43,587 (2 occurrences, with provenance note)

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

* docs(roadmap): iteration 29 — op_picker execution order + file_browser analysis

- §10 Step 4f: expand from 5 to 7 sub-steps; add 4f-vi (op_picker/mod.rs
  → mod.rs + loading.rs + keys.rs) and 4f-vii (op_picker/render.rs →
  render.rs + pane.rs); document impl-extension and import-path caveats
- §4 //! exemplars: add file_browser/ subsystem analysis — all 5 files
  have //! docs, no file exceeds ~350L production; classified as exemplar
  (not a split candidate); document git_prompt.rs coupling-density
  justification and input.rs as 28-file false positive (144L production)
- §1 module map: expand single file_browser/ row to 5 individual rows
  with production LOC and dominant concern per file

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

* docs(roadmap): iteration 30 — challenge split-first thesis, fresh LOC corrections

- §4: Add "Alternative thesis: documentation-first verification" — challenges
  the two core assumptions behind file splitting (files-as-audit-unit and
  file-size-as-context-constraint); adds 7-criterion comparison table vs
  structure-first approach; introduces phased combined recommendation:
  Phase 1 = doc sprint (//! contracts + specs/ for 3 subsystems, 2-3 PRs,
  zero structural change); Phase 2 = splits only for >600L production files
  (reduces scope from 14+ to 4 files); Phase 3 = workspace if LOC > 150K
- Fix stale LOC: app/mod.rs 951→979, config/editor.rs 1467→1548 (7 and
  8 locations respectively; verified by fresh find|xargs wc -l scan)
- §1 module map: add agent_picker.rs (436L), scope_picker.rs (201L),
  source_picker.rs (244L) — all PR #171 additions with //! docs

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

* docs(roadmap): iteration 31 — fix 600L→800L threshold error, correct LOC

- §4 alternative thesis: correct ">600L production → 4 files" claim
  introduced in iteration 30; re-verified all 9 candidate files via
  #[cfg(test)] line position; threshold must be >800L to get exactly 4
  files (9 exceed 600L); add verification table with test-start lines
- Production LOC corrections (5+ locations each):
  launch.rs 1085→~1077, operator_env.rs 810→~880,
  app/mod.rs 928→~957, config/editor.rs 503→~584
- §2 OpPicker row: replace vague "no entry yet" with confirmed gap:
  PROJECT_STRUCTURE.md line 53 still lists pre-PR#171 widget set (10
  named); omits op_picker/, agent_picker.rs, scope_picker.rs,
  source_picker.rs and pre-dates the manager/ sub-structure split

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

* docs(roadmap): iteration 32 — two-tier spec arch, behavioral spec template

- §8.1: Add two-tier spec architecture table distinguishing feature specs
  (public Starlight MDX, user-facing) from behavioral specs (internal
  docs/internal/specs/, for AI code verification) — resolves contradiction
  between §4 (which said docs/internal/specs/) and §8.1 (which said
  "no longer needed; specs are public")
- §8.1: Add concrete behavioral spec template for op_picker/ with state
  machine table and 3 INV invariant entries each with a grep-executable
  "Verify by:" command; template directly usable for the 3 Phase 1 specs
- §8.1: Remove erroneous "docs/internal/specs/ no longer needed" claim
- Confirmed render/editor.rs ~736L and render/list.rs ~668L production
  (no interspersed production code — all test blocks follow consecutively)

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

* docs(roadmap): iteration 33 — executive summary, §0 correctness

- §0: Add executive summary (~300 words) with core problem, 3-phase
  recommendation, key counter-argument, and navigation table pointing
  to §2/§4/§7/§8/§10 by question — resolves the meta-irony of a
  readability roadmap with no entry-point orientation
- §0 item 2: "1569-line monolith" → "2130-line monolith" (operator_env.rs
  current verified size; stale reference was in the first section readers see)
- §0 item 3: Add "(selective)" qualifier and explicit note that standard
  Rust co-locates struct+impl — impl-extension pattern is justified only
  for files >800L production, not as a universal rule

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

* docs(roadmap): iteration 34 — spec priority reorder, §10 Phase 1 track

- §0 + §4 Phase 1: Prioritize runtime/launch.rs behavioral spec (no //!
  doc, ~1077L production, critical path — all jackin load failures trace
  here); drop config/editor.rs from Phase 1 (its 963L test suite already
  serves as behavioral spec — tests are behavioral examples); reduce Phase
  1 from 3 specs to 2 specs; add reasoning for the priority ordering
- §10 Step 2: Split into two parallel tracks — Track A (cc-sdd tooling
  setup) + Track B (Phase 1 behavioral spec authoring); Track B includes
  specific INV invariants to capture for runtime/launch.rs grounded in
  reading the actual function structure (step comment positions); adds
  sequencing rationale: spec must precede structural splits

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

* docs(roadmap): iteration 35 — verified INV entries for runtime/launch.rs

Read load_agent_with lines 553-892 in full. Replaced 3 draft INVs from
iteration 34 (inferred from step comment positions) with 5 verified INVs
citing exact line numbers:
- INV-1: trust gate (line 594) precedes image build (line 736)
- INV-2: container name claimed (line 754) between image build and network
- INV-3: token verified (line 763) before network creation (line 827)
- INV-4: render_exit called at lines 886 AND 890 (all exit paths)
- INV-5: cleanup disarm semantics — Running→disarm, clean exit→cleanup,
  crash→disarm (explains jackin hardline compatibility)
Corrected wrong line number: claim_container_name call is at 754, not 918
(918 is the function definition). Each INV has a grep-executable Verify by.

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

* docs(roadmap): iteration 36 — CI gate for PROJECT_STRUCTURE.md freshness

§3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with
three concrete options:
- Option A: CONTRIBUTING.md rule (necessary but insufficient)
- Option B: ci.yml git-diff-scoped shell check (recommended) — only checks
  files added in the current PR so it doesn't require fixing existing stale
  entries before merging; greps for module directory name in prose
- Option C: Structured TOML module registry (over-engineered for scale)
Includes concrete YAML snippet for Option B grounded in the check:repo-links.ts
pattern already established in docs/scripts/

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

* docs(roadmap): iterations 36-37 — CI gate + greenfield workspace architecture

Iteration 36:
- §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with
  3 options (CONTRIBUTING.md rule / ci.yml git-diff check / TOML registry);
  recommend Option B (git-diff-scoped YAML step) with concrete snippet grounded
  in existing check:repo-links.ts pattern from docs/scripts/

Iteration 37 (operator directive: greenfield Rust structure):
- §4: Add "Greenfield architecture — ideal structure for a growing project"
  section based on verified cross-module dependency graph (grep iteration 37)
- Confirms dependency tiers: workspace/manifest/docker/paths/selector = Tier 0;
  config/tui/instance = Tier 1; operator_env/runtime/repo = Tier 2; console = Tier 3
- Key finding: workspace/ is LOWER-level than config/ (config re-exports workspace
  types at lines 5-6); ideal naming inverted in greenfield (jackin-core > jackin-config)
- Documents ideal 6-crate workspace: jackin-core, jackin-config, jackin-tui,
  jackin-runtime, jackin-console, jackin-shell + thin binary
- Notes console/ has NO runtime/ import — cleanest pre-existing crate boundary
- Bridge: incremental splits (4a, 4d, 4g) are pre-work toward workspace migration

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

* docs(roadmap): iteration 38 — Rust workspace standards, community evidence

Ground workspace recommendation in real-world project research:
- ripgrep (9 crates), gitui (5 crates) went workspace due to library consumers
- starship and fd-find stay single-crate at 1M+ LOC — no library use case
- jackin (43K LOC, no external consumers) maps to starship/fd pattern
  → single-crate is community-standard; "stay single-crate" recommendation confirmed

Update greenfield workspace structure to follow matklad's pattern:
- Virtual manifest at root (no [package] in root Cargo.toml)
- Flat crates/ directory (not nested); crate names match folder names
- version = "0.0.0" for unpublished internal crates
- Add inline dep comments to each crate in the ASCII structure

Add research notes: ripgrep/starship/gitui/fd-find Cargo.toml findings +
Cargo workspaces reference + matklad "Large Rust Workspaces" (2021-08-22)

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

* docs(roadmap): revise §7.9 + §3 — adopt per-directory README.md

§7.9: Reverse previous "reject" recommendation to "adopt" per-directory
README.md for major src/ module directories. Rationale: README.md is
AI-native — Claude Code, Copilot, Cursor load it automatically on directory
entry, giving AI agents orientation before they decide which file to open.
PROJECT_STRUCTURE.md being confirmed stale removes the main argument for
the "single root file" approach.

Add three-layer documentation model table:
- README.md: directory orientation (AI + human, on entry)
- AGENTS.md: agent workflow rules (root, session start)
- CLAUDE.md: @AGENTS.md pointer only — NEVER add content here
- //! docs: file-level contracts (when reading/editing)

Add specific README.md content targets for 7 directories
(src/, src/runtime/, src/console/, src/console/manager/,
src/console/widgets/, docs/, docs/internal/).

§3 target document shape: Add per-directory README.md to proposed
hierarchy; add docs/internal/specs/ explicitly; note CLAUDE.md
design principle (single-line @AGENTS.md — never duplicate content).

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

* docs(roadmap): internal docs are browsable — unified Starlight site

Operator directive: internal docs (architecture, specs, ADRs, roadmap) should
be browsable, not hidden filesystem files. They are a different TYPE of docs
focused on implementation details and vision, published as a "Developer
Reference" section of the Starlight site.

§3 target document shape:
- docs/internal/ moves into docs/src/content/docs/internal/ (Starlight pages)
- Browsable at jackin.tailrocks.com/internal/
- Sidebar: "Developer Reference" group (collapsed by default) with sub-sections
  for architecture, code-tour, contributing, testing, decisions, specs, roadmap
- Include astro.config.ts sidebar config snippet

§8.1 two-tier spec distinction eliminated:
- Feature specs and behavioral specs both live at docs/src/content/docs/internal/specs/
- Type expressed via spec_type: behavioral | feature frontmatter, not filesystem location
- Both browsable and searchable via Starlight; AI agents can be pointed to URLs

§8.3 + §4:
- All docs/internal/specs/ paths → docs/src/content/docs/internal/specs/
- ADRs: docs/internal/decisions/ → docs/src/content/docs/internal/decisions/ (browsable)
- README.md pointer for src/runtime/ updated to URL reference

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

* docs(roadmap): §11 — modern Rust docs platform (future project)

Add §11 capturing the vision for a modern docs.rs alternative with:
- rustdoc JSON ingestion → Astro Starlight presentation
- MCP server for AI agent queries (Context7 alternative for Rust)
- Rust-specific query types: rust_get_context(), rust_find_impls(),
  rust_search_types() — things Context7 cannot provide
- Comparison table vs Context7
- Architecture diagram (ingestion → processing → Starlight + MCP)
- Name candidates: rustlight, ferrodoc, cargo-starlight / starlight.rs
- Note that jackin's §7.15 gen-rust-api.ts pipeline is the intentional
  prototype for the platform's processing and presentation layers

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

* docs(roadmap): iteration 39 — update §0, fix stale internal/ paths

§0 executive summary: rewrite to reflect decisions from iterations 30-38:
- browsable internal docs (jackin.tailrocks.com/internal/)
- per-directory README.md adoption (§7.9 reversed)
- CLAUDE.md = @AGENTS.md single-line pointer only
- greenfield workspace architecture (matklad's virtual manifest pattern)
- §11 future project: modern Rust docs platform / Context7-for-Rust
- document size 1800+ → 2200+

Fix stale docs/internal/ bare paths not caught by iteration 38 sweep:
- Mermaid diagram: INTERNAL_ROADMAP, INTERNAL_CODE_TOUR → Starlight paths
- §7.10 ADRs: docs/internal/decisions/NNN-title.md → .mdx Starlight path
- §10 Track B item 2: op-picker spec path → Starlight MDX

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

* docs(roadmap): iteration 40 — §7.15 pipeline + Rule 4 pub audit

§7.15 (new): rustdoc JSON → Astro Starlight API documentation pipeline
- Three options: rustdoc HTML publish / rustdoc JSON + bun script (recommended)
  / rustdoc-json crate as Rust binary
- Option B recommended: matches existing docs/scripts/ pattern, nightly
  isolated to separate CI step, zero effect on stable build
- Key design: URL at /internal/api/, cross-links to behavioral specs,
  Starlight unified search, prototype for §11 future project
- Pub(crate) note: gen-rust-api.ts can feed Rule 4 visibility audit
- Recommend: adopt after Phase 1 //! sprint (value ∝ coverage)

§4 Rule 4 pub discipline: replace estimated "50-100 items" guess with
verified numbers from iteration 40 grep:
- 257 bare pub items, 21 pub(crate), 61 pub(super) across 94 files
- 0 uses of unreachable_pub lint — no enforcement gate
- Top violators: operator_env.rs (17), tui/output.rs (13), planner.rs (8)
- Add concrete Cargo.toml [lints.rust] snippet: unreachable_pub = "warn"
- Revised scope: ~150-200 mechanical conversions (excludes entry points)

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

* docs(roadmap): split research into 19 actionable items

Delete _research_notes.md (no longer needed).

Replace 2343L READABILITY_AND_MODERNIZATION.md with:
- README.md: index of all 19 items with phase, ordering notes, links
- READABILITY_AND_MODERNIZATION.md: lightweight research summary (63L)
- items/ITEM-001 through ITEM-019: individual actionable items

Items by phase:
  Phase 1 (low risk, no confirmation): ITEM-001..004, 006..011
  Phase 1 (needs confirmation): ITEM-005, 016, 018
  Phase 2 (structural splits, confirmation required): ITEM-012..015
  Phase 3 (deferred): ITEM-017 (rustdoc pipeline), ITEM-019 (workspace)

Each item has: summary, key files with line numbers, steps, what
needs confirmation, and relevant research backing from the 40-iteration
analysis loop.

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

* docs(roadmap): migrate 19 items to Starlight reference/roadmap section

Move all codebase health roadmap items from docs/internal/roadmap/items/
(plain Markdown, not browsable) to docs/src/content/docs/reference/roadmap/
(MDX pages, browsable at jackin.tailrocks.com/reference/roadmap/).

Adds a new "Codebase health" sidebar group (Phase 1 → Phase 3) to
astro.config.ts. Deletes the old items/ directory. Updates the internal
README to redirect to the new location.

Also adds codebase-readability.mdx — a new overview item that captures
the overall readability/restructuring program with a recommended execution
order: file splits first, then greenfield workspace, then per-directory
README+AGENTS.md, then docs and specs.

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

* docs(roadmap): remove premature internal/roadmap/README.md

The internal/ structure doesn't exist yet — it will be created as part
of the roadmap items themselves. No need for a redirect stub now.

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

* docs(roadmap): remove READABILITY_AND_MODERNIZATION.md research archive

All content has been distilled into the individual Starlight roadmap pages.
The full 2343L research is preserved in git history at commit b7e9fc2.

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

* docs(roadmap): fix check:repo-links errors + remove iteration log

- Replace plain code spans with <RepoFile> for validate.rs, mise.toml,
  Cargo.toml, and op_picker/mod.rs
- Remove deleted READABILITY_AND_MODERNIZATION.md reference from
  codebase-readability.mdx
- Delete _iteration_log.md (git history is the archive)

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

* docs(roadmap): fix lychee false-positive link in move-contributing-testing

The example redirect text contained a markdown hyperlink to a proposed
future file path that doesn't exist yet. Changed to a code span.

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

---------

Signed-off-by: Alexey Zhokhov <alexey@chainargos.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
- Move 9 TODO items from monolithic TODO.md into separate files in todo/
- Each file is a self-contained design doc with problem, options, and
  related source files for easy agent handoff
- Mark resolved security findings (#3, #4, #6, #7) in SECURITY_REVIEW_FINDINGS.md
- Update PROJECT_STRUCTURE.md with todo/ section and TESTING.md entry
- TODO.md becomes an index pointing to todo/ files

Co-Authored-By: Claude Opus 4.6 (1M context) <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
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
* feat: implement trust-on-first-use model for third-party agent sources

Add a trust gate that prompts operators before building untrusted
third-party agents. Built-in agents are always trusted. New namespaced
agents default to untrusted and require explicit confirmation on first
use. The trusted flag is persisted in config.toml so subsequent runs
proceed without prompts. Non-interactive sessions bail with a clear
error for untrusted sources.

Also syncs the roadmap with TODO items: adds missing completed item
(JACKIN_DIND_HOSTNAME), adds missing planned items (Bollard migration),
and marks the agent source trust security finding as resolved.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* improve trust prompt with clear risk details and mark finding #9 resolved

Rewrite the trust confirmation message to clearly explain what trusting
an agent means: Dockerfile execution, arbitrary commands on the host,
and workspace file access. The non-interactive error now tells the user
how to pre-trust via config.toml. The decline message explains how to
retry.

Also mark security finding #9 (mount policy guardrails) as resolved
since sensitive mount warnings were already implemented.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: address PR review — docs, stale comment, save clarity, test, serde

Review fixes for the agent source trust model:

- Fix duplicate doc comment on trust_agent() (copy-paste from
  sync_builtin_agents)
- Replace confusing `!source.trusted` save condition with explicit
  `newly_trusted` flag for readability
- Add unit test for non-interactive trust gate (verifies the error
  includes agent name and git URL)
- Skip serializing `trusted = false` to keep config.toml clean
  (only `trusted = true` appears)
- Document trust in load.mdx (step 3 in "What happens", warning
  callout for CI) and security-model.mdx (new section 4 with
  config.toml example, threat model row, best practices update)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: broken docs anchor link and misleading test name

- Fix cross-reference link in load.mdx: use #4-agent-source-trust
  (matching the numbered heading in security-model.mdx)
- Rename test to `load_trusted_namespaced_agent_builds_and_runs` since
  it no longer exercises auto-registration (that's covered by the
  config test `resolve_agent_source_adds_owner_repo_on_first_use`)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* feat: add jackin trust/untrust CLI, testable trust gate, revocation

Address all follow-up items from the maintainer review:

- Extract load_agent_with() with injected trust callback, following the
  resolve_agent_repo_with() pattern. Tests can now exercise accept and
  decline paths without mocking dialoguer.
- Restore auto-registration test coverage: the accept test uses
  load_agent_with(auto_trust) so resolve_agent_source's is_new path is
  exercised again. Add decline test verifying no config persistence and
  no Docker commands when trust is refused.
- Add `jackin trust <selector>` and `jackin untrust <selector>` CLI
  commands so operators can manage trust without editing config.toml.
- Add untrust_agent() to AppConfig with tests.
- Document trust/untrust CLI in security-model.mdx and load.mdx.
- Document that trust is keyed by selector (not URL) with a note about
  remote-mismatch protection.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: consolidate trust/untrust into single CLI command

Follow mise's pattern: `jackin trust` is one command with flags
instead of separate trust/untrust subcommands.

  jackin trust <selector>              # grant trust
  jackin trust <selector> --untrust    # revoke trust
  jackin trust <selector> --show       # check status

The --show and --untrust flags conflict with each other (enforced
by clap).

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: move trust CLI under config with grant/revoke/list subcommands

Follow the same pattern as `jackin config mount {add,remove,list}`:

  jackin config trust grant chainargos/the-architect
  jackin config trust revoke chainargos/the-architect
  jackin config trust list

This keeps all config mutations under `jackin config` and uses the
subcommand pattern consistently throughout the CLI. The `list`
subcommand shows all currently trusted agents.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: refuse to revoke trust on builtins, add CLI parsing tests

- is_builtin_agent() check prevents revoking trust on built-in agents
  (agent-smith, the-architect) which would be silently re-granted by
  sync_builtin_agents on next load
- Add 3 CLI parsing tests for config trust {grant, revoke, list},
  matching the existing coverage for config mount {add, remove, list}
- Tighten TrustCommand::List doc comment to "List all currently
  trusted agent sources"

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

---------

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
…183)

* docs(roadmap): iteration 13 — AI code verifiability framing, config/types.rs full spec

Primary goal shift: codebase must be verifiable for AI-generated code.

- §0: replace generic description with explicit verifiability rationale
  (module contracts, localised concerns, types/behaviour separation)
- §4 intro: add "Why structure matters for AI-generated code" section with
  audit-units table mapping each post-split file to one reviewable question
- §4 4a: expand config/types.rs from description to full execution spec —
  exact type list, post-split mod.rs content, zero-change submodule
  guarantee (verified: agents.rs/persist.rs/workspaces.rs use super::T
  which resolves through mod.rs re-exports unchanged), impl-extension
  pattern already in use documented


* docs(roadmap): iteration 14 — editor method map, app helpers, //! queue

- §4 4c: config/editor.rs split is now execution-ready — complete
  6-file method-to-file table with private helper placement verified
  (validate_candidate→io.rs, table_path_mut→mod.rs pub(super),
  auth_forward_str→agent_ops.rs, create_workspace delegates to AppConfig)
- §4 4e: app/mod.rs split complete — all private helpers mapped
  (parse_auth_forward_mode_from_cli→config_cmd.rs,
  workspace_env_scope→workspace_cmd.rs, print_env_table note,
  remove_data_dir_if_exists→dispatch.rs)
- §10 step 5: add //! priority queue — 10 files with draft content,
  prioritised by cold-landing impact and AI audit risk; selector.rs
  and instance/mod.rs explicitly document the /→__ invariant


* docs(roadmap): iteration 15 — dep graph fix, trust.rs safety, OQ1 closed

- §4 4d: correct operator_env dependency graph — layers.rs imports
  both mod.rs (OpRunner) AND client.rs (OpCli for non-injectable
  resolve_operator_env wrapper at line 797); still a valid DAG
- §4 4f: verify trust.rs split safety — FnOnce injection pattern means
  launch_pipeline.rs has zero dependency on trust.rs; import chain
  documented; trust bypass audit now requires reading only ~60L
- §9 OQ1 closed: op_cache.rs read in full — 4-level structure,
  per-level invalidation, no TTL/expiry (expiry handled at OpCli
  subprocess level), DEFAULT_ACCOUNT_KEY sentinel documented


* docs(roadmap): iteration 16 — CommandRunner Rule 3, render/editor split, 4a/4c independence

- Fix duplicate Rule 3 section introduced by previous edit; add docker.rs
  co-location note as third edge case (three edge cases, not two)
- Add render/editor.rs as new Rule 5 violator: 1666L post-PR #171
  (was listed as 782L); propose 6-file tab-by-tab split with auditability
  note on the security-adjacent Secrets tab
- Add §10 execution-order note: 4a and 4c are independent — editor.rs
  imports AppConfig via crate::config re-exports regardless of 4a order
- Append iteration 16 log entry with confidence table and weakest sections


* docs(roadmap): iteration 17 — instance/auth.rs audit, state.rs split, line count corrections

- Add instance/auth.rs to //! priority queue at #4: four security
  invariants (0o600 perms, symlink rejection, TOCTOU-safe writes,
  macOS Keychain) documented in draft //! content
- Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types
  mixed with impl blocks; propose 5-file types/behavior split
- Correct stale line counts: render/list.rs 1122→1989 (PR #171 added
  render_environments_subpanel); state.rs 865→992; priorities upgraded
- Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332,
  mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944
- Renumber //! priority queue to 11 entries (was 10)


* docs(roadmap): iteration 18 — agent_allow OQ2 closed, render/list.rs split proposal

- Close OQ2: agent_allow.rs read in full — 55L, correct //! doc,
  design sound; serves as model for //! priority queue pattern
- Add render/list.rs as new Rule 5 violator: 668L production (PR #171
  added render_environments_subpanel); propose 3-file split (mod.rs,
  details.rs, subpanels.rs); note import-path change for agents_block_agent_count
- Update §1 module map: agent_allow.rs entry corrected with size/API


* docs(roadmap): iteration 19 — input/editor.rs critical correction, split proposal

- Correct input/editor.rs: 2349L total (was 1304L), 1141L production
  (was 547L) — PR #171 added Secrets-tab handlers; pub(super) fn
  handle_editor_modal at line 618 was invisible to previous grep pattern;
  now the largest production file in the codebase; priority → Critical
- Correct input/save.rs: 1472L total, 661L production (was 567L)
- Add 5-file split proposal for input/editor.rs: mod.rs (two dispatchers),
  secrets.rs (~500L AI-generated Secrets-tab), agents.rs, mounts.rs, general.rs
- Update key insight paragraph naming input/editor.rs as largest production file


* docs(roadmap): iteration 20 — console splits in §10, MSRV evidence, animation.rs verdict

- Add console/manager/ as §10 Step 4f group with 5 sub-steps in priority
  order; rename existing 4f (launch.rs) → 4g; add circular-import risk note
  for ManagerStage/EditorState split sequencing
- Analyze tui/animation.rs: 582L all-production, no split needed (banner_grid
  is a tightly-coupled rendering loop); section comments compensate for missing //!
- Partially close OQ3: u64::is_multiple_of (stabilized 1.86) found in animation.rs;
  within declared MSRV 1.94; full cargo +1.94.0 check deferred (toolchain unavailable)


* docs(roadmap): iteration 21 — input/save.rs split, //! queue fix, save.rs corrections

- Add input/save.rs split proposal: 4 pub(super) fns discovered; 3-file
  split (mod.rs + flow.rs + preview.rs); no cross-dependency between
  flow and preview groups; §10 4f-v updated from Optional to concrete plan
- Fix //! queue preamble: "first 10 files" → "first 11 files"
- Correct save.rs module map (1418→1472L, correct key exports) and
  hot-spot table note (begin_editor_save ~280L → ~118L; commit_editor_save
  is the Phase 2 partner at ~149L)


* docs(roadmap): iteration 22 — input/list.rs and mount_info.rs analysis

- Analyze input/list.rs: 214L production (tests at 215); has //! doc;
  two focused pub(super) fns; no split needed; Low priority; correct module map
- Add mount_info.rs to hot-spot table: 277L production; Low priority;
  has //! doc; correct module map with 3 public enums + inspect fn
- Fix stale §2 diagnosis note: docs/internal/roadmap/ now exists


* docs(roadmap): iteration 23 — audit units table +5 console rows, input/mod.rs corrected

- Expand audit units table from 8 to 13 entries: add state/types.rs,
  state/editor.rs, input/editor/secrets.rs, render/list/subpanels.rs,
  input/save/preview.rs — all targeting PR #171 AI-generated console code
- Add PR #171 context note linking 5 new entries to AI-generated code concern
- Correct input/mod.rs module map: 369L, add InputOutcome enum to exports
- Verify rust-toolchain.toml absence; §7.7 and §2 concept 25 already correct


* docs(roadmap): iteration 24 — render/mod.rs analysis, //! exemplars table, EditorTab confirmed

- Add §4 Rule 7 positive exemplars table: 7 files with //! docs graded
  1-element (render/mod.rs), 2-element (input/save.rs etc), 3-element
  (env_model.rs, agent_allow.rs); PR #171 docs-discipline pattern noted
- Correct render/mod.rs module map: 421L; FooterItem + palette constants
  + render_header + centered_rect_fixed added to key exports
- Confirm EditorTab variants: General, Mounts, Agents, Secrets (Rust enum)
  vs "Secrets / Environments" (UI label); /stub qualifier already removed


* docs(roadmap): iteration 25 — too_many_lines recount, FooterItem PR, MountConfig caveat

- Correct too_many_lines count: 13 across 8 → 16 across 11 files
  (PR #171 added 5 suppressions in console/manager); add full breakdown
  table; update all 3 occurrences in roadmap
- Fix FooterItem PR reference: #165#166 (confirmed by git log --follow)
- Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description


* docs(roadmap): iteration 26 — console/mod.rs and op_picker/render.rs analyzed

- Add console/mod.rs to hot-spot table: 406L/307L production (Low);
  correct module map from ~200 → 406L; note missing //! doc with
  ConsoleStage design block comment worth promoting
- Add op_picker/render.rs to hot-spot table: 865L/545L production
  (Medium); PR #171 AI-generated; 14 functions in two logical groups
  (entry/helpers vs level renderers); split into levels.rs proposed
- Correct 3 stale ~200L estimates for console/mod.rs across roadmap


* docs(roadmap): iteration 27 — op_picker/mod.rs discovery, render split, operator_env correction

- Add op_picker/mod.rs to hot-spot table: 1712L/775L production (High);
  PR #171 AI-generated; OpPickerState types+behavior split opportunity;
  has 7-line //! doc; module map split into two rows (mod.rs + render.rs)
- Add op_picker/render.rs 2-file split proposal: render.rs (coordinator)
  + render_pane.rs (pane/level renderers); no cross-dependency confirmed
- Correct operator_env.rs total: 1569→2130L (880L production); update
  4 occurrences across hot-spot table, ASCII tree, §4 analysis


* docs(roadmap): iteration 28 — op_picker/mod.rs 3-file split, count corrections

- Add op_picker/mod.rs formal 3-file split: loading.rs (async load family
  ~120L) + keys.rs (4 level key handlers ~315L) + mod.rs (types/constructors)
- Correct "24 files" → "28+" for 500L threshold count
- Update total LOC: ~40,664 → ~43,587 (2 occurrences, with provenance note)


* docs(roadmap): iteration 29 — op_picker execution order + file_browser analysis

- §10 Step 4f: expand from 5 to 7 sub-steps; add 4f-vi (op_picker/mod.rs
  → mod.rs + loading.rs + keys.rs) and 4f-vii (op_picker/render.rs →
  render.rs + pane.rs); document impl-extension and import-path caveats
- §4 //! exemplars: add file_browser/ subsystem analysis — all 5 files
  have //! docs, no file exceeds ~350L production; classified as exemplar
  (not a split candidate); document git_prompt.rs coupling-density
  justification and input.rs as 28-file false positive (144L production)
- §1 module map: expand single file_browser/ row to 5 individual rows
  with production LOC and dominant concern per file


* docs(roadmap): iteration 30 — challenge split-first thesis, fresh LOC corrections

- §4: Add "Alternative thesis: documentation-first verification" — challenges
  the two core assumptions behind file splitting (files-as-audit-unit and
  file-size-as-context-constraint); adds 7-criterion comparison table vs
  structure-first approach; introduces phased combined recommendation:
  Phase 1 = doc sprint (//! contracts + specs/ for 3 subsystems, 2-3 PRs,
  zero structural change); Phase 2 = splits only for >600L production files
  (reduces scope from 14+ to 4 files); Phase 3 = workspace if LOC > 150K
- Fix stale LOC: app/mod.rs 951→979, config/editor.rs 1467→1548 (7 and
  8 locations respectively; verified by fresh find|xargs wc -l scan)
- §1 module map: add agent_picker.rs (436L), scope_picker.rs (201L),
  source_picker.rs (244L) — all PR #171 additions with //! docs


* docs(roadmap): iteration 31 — fix 600L→800L threshold error, correct LOC

- §4 alternative thesis: correct ">600L production → 4 files" claim
  introduced in iteration 30; re-verified all 9 candidate files via
  #[cfg(test)] line position; threshold must be >800L to get exactly 4
  files (9 exceed 600L); add verification table with test-start lines
- Production LOC corrections (5+ locations each):
  launch.rs 1085→~1077, operator_env.rs 810→~880,
  app/mod.rs 928→~957, config/editor.rs 503→~584
- §2 OpPicker row: replace vague "no entry yet" with confirmed gap:
  PROJECT_STRUCTURE.md line 53 still lists pre-PR#171 widget set (10
  named); omits op_picker/, agent_picker.rs, scope_picker.rs,
  source_picker.rs and pre-dates the manager/ sub-structure split


* docs(roadmap): iteration 32 — two-tier spec arch, behavioral spec template

- §8.1: Add two-tier spec architecture table distinguishing feature specs
  (public Starlight MDX, user-facing) from behavioral specs (internal
  docs/internal/specs/, for AI code verification) — resolves contradiction
  between §4 (which said docs/internal/specs/) and §8.1 (which said
  "no longer needed; specs are public")
- §8.1: Add concrete behavioral spec template for op_picker/ with state
  machine table and 3 INV invariant entries each with a grep-executable
  "Verify by:" command; template directly usable for the 3 Phase 1 specs
- §8.1: Remove erroneous "docs/internal/specs/ no longer needed" claim
- Confirmed render/editor.rs ~736L and render/list.rs ~668L production
  (no interspersed production code — all test blocks follow consecutively)


* docs(roadmap): iteration 33 — executive summary, §0 correctness

- §0: Add executive summary (~300 words) with core problem, 3-phase
  recommendation, key counter-argument, and navigation table pointing
  to §2/§4/§7/§8/§10 by question — resolves the meta-irony of a
  readability roadmap with no entry-point orientation
- §0 item 2: "1569-line monolith" → "2130-line monolith" (operator_env.rs
  current verified size; stale reference was in the first section readers see)
- §0 item 3: Add "(selective)" qualifier and explicit note that standard
  Rust co-locates struct+impl — impl-extension pattern is justified only
  for files >800L production, not as a universal rule


* docs(roadmap): iteration 34 — spec priority reorder, §10 Phase 1 track

- §0 + §4 Phase 1: Prioritize runtime/launch.rs behavioral spec (no //!
  doc, ~1077L production, critical path — all jackin load failures trace
  here); drop config/editor.rs from Phase 1 (its 963L test suite already
  serves as behavioral spec — tests are behavioral examples); reduce Phase
  1 from 3 specs to 2 specs; add reasoning for the priority ordering
- §10 Step 2: Split into two parallel tracks — Track A (cc-sdd tooling
  setup) + Track B (Phase 1 behavioral spec authoring); Track B includes
  specific INV invariants to capture for runtime/launch.rs grounded in
  reading the actual function structure (step comment positions); adds
  sequencing rationale: spec must precede structural splits


* docs(roadmap): iteration 35 — verified INV entries for runtime/launch.rs

Read load_agent_with lines 553-892 in full. Replaced 3 draft INVs from
iteration 34 (inferred from step comment positions) with 5 verified INVs
citing exact line numbers:
- INV-1: trust gate (line 594) precedes image build (line 736)
- INV-2: container name claimed (line 754) between image build and network
- INV-3: token verified (line 763) before network creation (line 827)
- INV-4: render_exit called at lines 886 AND 890 (all exit paths)
- INV-5: cleanup disarm semantics — Running→disarm, clean exit→cleanup,
  crash→disarm (explains jackin hardline compatibility)
Corrected wrong line number: claim_container_name call is at 754, not 918
(918 is the function definition). Each INV has a grep-executable Verify by.


* docs(roadmap): iteration 36 — CI gate for PROJECT_STRUCTURE.md freshness

§3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with
three concrete options:
- Option A: CONTRIBUTING.md rule (necessary but insufficient)
- Option B: ci.yml git-diff-scoped shell check (recommended) — only checks
  files added in the current PR so it doesn't require fixing existing stale
  entries before merging; greps for module directory name in prose
- Option C: Structured TOML module registry (over-engineered for scale)
Includes concrete YAML snippet for Option B grounded in the check:repo-links.ts
pattern already established in docs/scripts/


* docs(roadmap): iterations 36-37 — CI gate + greenfield workspace architecture

Iteration 36:
- §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with
  3 options (CONTRIBUTING.md rule / ci.yml git-diff check / TOML registry);
  recommend Option B (git-diff-scoped YAML step) with concrete snippet grounded
  in existing check:repo-links.ts pattern from docs/scripts/

Iteration 37 (operator directive: greenfield Rust structure):
- §4: Add "Greenfield architecture — ideal structure for a growing project"
  section based on verified cross-module dependency graph (grep iteration 37)
- Confirms dependency tiers: workspace/manifest/docker/paths/selector = Tier 0;
  config/tui/instance = Tier 1; operator_env/runtime/repo = Tier 2; console = Tier 3
- Key finding: workspace/ is LOWER-level than config/ (config re-exports workspace
  types at lines 5-6); ideal naming inverted in greenfield (jackin-core > jackin-config)
- Documents ideal 6-crate workspace: jackin-core, jackin-config, jackin-tui,
  jackin-runtime, jackin-console, jackin-shell + thin binary
- Notes console/ has NO runtime/ import — cleanest pre-existing crate boundary
- Bridge: incremental splits (4a, 4d, 4g) are pre-work toward workspace migration


* docs(roadmap): iteration 38 — Rust workspace standards, community evidence

Ground workspace recommendation in real-world project research:
- ripgrep (9 crates), gitui (5 crates) went workspace due to library consumers
- starship and fd-find stay single-crate at 1M+ LOC — no library use case
- jackin (43K LOC, no external consumers) maps to starship/fd pattern
  → single-crate is community-standard; "stay single-crate" recommendation confirmed

Update greenfield workspace structure to follow matklad's pattern:
- Virtual manifest at root (no [package] in root Cargo.toml)
- Flat crates/ directory (not nested); crate names match folder names
- version = "0.0.0" for unpublished internal crates
- Add inline dep comments to each crate in the ASCII structure

Add research notes: ripgrep/starship/gitui/fd-find Cargo.toml findings +
Cargo workspaces reference + matklad "Large Rust Workspaces" (2021-08-22)


* docs(roadmap): revise §7.9 + §3 — adopt per-directory README.md

§7.9: Reverse previous "reject" recommendation to "adopt" per-directory
README.md for major src/ module directories. Rationale: README.md is
AI-native — Claude Code, Copilot, Cursor load it automatically on directory
entry, giving AI agents orientation before they decide which file to open.
PROJECT_STRUCTURE.md being confirmed stale removes the main argument for
the "single root file" approach.

Add three-layer documentation model table:
- README.md: directory orientation (AI + human, on entry)
- AGENTS.md: agent workflow rules (root, session start)
- CLAUDE.md: @AGENTS.md pointer only — NEVER add content here
- //! docs: file-level contracts (when reading/editing)

Add specific README.md content targets for 7 directories
(src/, src/runtime/, src/console/, src/console/manager/,
src/console/widgets/, docs/, docs/internal/).

§3 target document shape: Add per-directory README.md to proposed
hierarchy; add docs/internal/specs/ explicitly; note CLAUDE.md
design principle (single-line @AGENTS.md — never duplicate content).


* docs(roadmap): internal docs are browsable — unified Starlight site

Operator directive: internal docs (architecture, specs, ADRs, roadmap) should
be browsable, not hidden filesystem files. They are a different TYPE of docs
focused on implementation details and vision, published as a "Developer
Reference" section of the Starlight site.

§3 target document shape:
- docs/internal/ moves into docs/src/content/docs/internal/ (Starlight pages)
- Browsable at jackin.tailrocks.com/internal/
- Sidebar: "Developer Reference" group (collapsed by default) with sub-sections
  for architecture, code-tour, contributing, testing, decisions, specs, roadmap
- Include astro.config.ts sidebar config snippet

§8.1 two-tier spec distinction eliminated:
- Feature specs and behavioral specs both live at docs/src/content/docs/internal/specs/
- Type expressed via spec_type: behavioral | feature frontmatter, not filesystem location
- Both browsable and searchable via Starlight; AI agents can be pointed to URLs

§8.3 + §4:
- All docs/internal/specs/ paths → docs/src/content/docs/internal/specs/
- ADRs: docs/internal/decisions/ → docs/src/content/docs/internal/decisions/ (browsable)
- README.md pointer for src/runtime/ updated to URL reference


* docs(roadmap): §11 — modern Rust docs platform (future project)

Add §11 capturing the vision for a modern docs.rs alternative with:
- rustdoc JSON ingestion → Astro Starlight presentation
- MCP server for AI agent queries (Context7 alternative for Rust)
- Rust-specific query types: rust_get_context(), rust_find_impls(),
  rust_search_types() — things Context7 cannot provide
- Comparison table vs Context7
- Architecture diagram (ingestion → processing → Starlight + MCP)
- Name candidates: rustlight, ferrodoc, cargo-starlight / starlight.rs
- Note that jackin's §7.15 gen-rust-api.ts pipeline is the intentional
  prototype for the platform's processing and presentation layers


* docs(roadmap): iteration 39 — update §0, fix stale internal/ paths

§0 executive summary: rewrite to reflect decisions from iterations 30-38:
- browsable internal docs (jackin.tailrocks.com/internal/)
- per-directory README.md adoption (§7.9 reversed)
- CLAUDE.md = @AGENTS.md single-line pointer only
- greenfield workspace architecture (matklad's virtual manifest pattern)
- §11 future project: modern Rust docs platform / Context7-for-Rust
- document size 1800+ → 2200+

Fix stale docs/internal/ bare paths not caught by iteration 38 sweep:
- Mermaid diagram: INTERNAL_ROADMAP, INTERNAL_CODE_TOUR → Starlight paths
- §7.10 ADRs: docs/internal/decisions/NNN-title.md → .mdx Starlight path
- §10 Track B item 2: op-picker spec path → Starlight MDX


* docs(roadmap): iteration 40 — §7.15 pipeline + Rule 4 pub audit

§7.15 (new): rustdoc JSON → Astro Starlight API documentation pipeline
- Three options: rustdoc HTML publish / rustdoc JSON + bun script (recommended)
  / rustdoc-json crate as Rust binary
- Option B recommended: matches existing docs/scripts/ pattern, nightly
  isolated to separate CI step, zero effect on stable build
- Key design: URL at /internal/api/, cross-links to behavioral specs,
  Starlight unified search, prototype for §11 future project
- Pub(crate) note: gen-rust-api.ts can feed Rule 4 visibility audit
- Recommend: adopt after Phase 1 //! sprint (value ∝ coverage)

§4 Rule 4 pub discipline: replace estimated "50-100 items" guess with
verified numbers from iteration 40 grep:
- 257 bare pub items, 21 pub(crate), 61 pub(super) across 94 files
- 0 uses of unreachable_pub lint — no enforcement gate
- Top violators: operator_env.rs (17), tui/output.rs (13), planner.rs (8)
- Add concrete Cargo.toml [lints.rust] snippet: unreachable_pub = "warn"
- Revised scope: ~150-200 mechanical conversions (excludes entry points)


* docs(roadmap): split research into 19 actionable items

Delete _research_notes.md (no longer needed).

Replace 2343L READABILITY_AND_MODERNIZATION.md with:
- README.md: index of all 19 items with phase, ordering notes, links
- READABILITY_AND_MODERNIZATION.md: lightweight research summary (63L)
- items/ITEM-001 through ITEM-019: individual actionable items

Items by phase:
  Phase 1 (low risk, no confirmation): ITEM-001..004, 006..011
  Phase 1 (needs confirmation): ITEM-005, 016, 018
  Phase 2 (structural splits, confirmation required): ITEM-012..015
  Phase 3 (deferred): ITEM-017 (rustdoc pipeline), ITEM-019 (workspace)

Each item has: summary, key files with line numbers, steps, what
needs confirmation, and relevant research backing from the 40-iteration
analysis loop.


* docs(roadmap): migrate 19 items to Starlight reference/roadmap section

Move all codebase health roadmap items from docs/internal/roadmap/items/
(plain Markdown, not browsable) to docs/src/content/docs/reference/roadmap/
(MDX pages, browsable at jackin.tailrocks.com/reference/roadmap/).

Adds a new "Codebase health" sidebar group (Phase 1 → Phase 3) to
astro.config.ts. Deletes the old items/ directory. Updates the internal
README to redirect to the new location.

Also adds codebase-readability.mdx — a new overview item that captures
the overall readability/restructuring program with a recommended execution
order: file splits first, then greenfield workspace, then per-directory
README+AGENTS.md, then docs and specs.


* docs(roadmap): remove premature internal/roadmap/README.md

The internal/ structure doesn't exist yet — it will be created as part
of the roadmap items themselves. No need for a redirect stub now.


* docs(roadmap): remove READABILITY_AND_MODERNIZATION.md research archive

All content has been distilled into the individual Starlight roadmap pages.
The full 2343L research is preserved in git history at commit b7e9fc2.


* docs(roadmap): fix check:repo-links errors + remove iteration log

- Replace plain code spans with <RepoFile> for validate.rs, mise.toml,
  Cargo.toml, and op_picker/mod.rs
- Remove deleted READABILITY_AND_MODERNIZATION.md reference from
  codebase-readability.mdx
- Delete _iteration_log.md (git history is the archive)


* docs(roadmap): fix lychee false-positive link in move-contributing-testing

The example redirect text contained a markdown hyperlink to a proposed
future file path that doesn't exist yet. Changed to a code span.


---------

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
- Move 9 TODO items from monolithic TODO.md into separate files in todo/
- Each file is a self-contained design doc with problem, options, and
  related source files for easy agent handoff
- Mark resolved security findings (#3, #4, #6, #7) in SECURITY_REVIEW_FINDINGS.md
- Update PROJECT_STRUCTURE.md with todo/ section and TESTING.md entry
- TODO.md becomes an index pointing to todo/ files

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
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
* feat: implement trust-on-first-use model for third-party agent sources

Add a trust gate that prompts operators before building untrusted
third-party agents. Built-in agents are always trusted. New namespaced
agents default to untrusted and require explicit confirmation on first
use. The trusted flag is persisted in config.toml so subsequent runs
proceed without prompts. Non-interactive sessions bail with a clear
error for untrusted sources.

Also syncs the roadmap with TODO items: adds missing completed item
(JACKIN_DIND_HOSTNAME), adds missing planned items (Bollard migration),
and marks the agent source trust security finding as resolved.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* improve trust prompt with clear risk details and mark finding #9 resolved

Rewrite the trust confirmation message to clearly explain what trusting
an agent means: Dockerfile execution, arbitrary commands on the host,
and workspace file access. The non-interactive error now tells the user
how to pre-trust via config.toml. The decline message explains how to
retry.

Also mark security finding #9 (mount policy guardrails) as resolved
since sensitive mount warnings were already implemented.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: address PR review — docs, stale comment, save clarity, test, serde

Review fixes for the agent source trust model:

- Fix duplicate doc comment on trust_agent() (copy-paste from
  sync_builtin_agents)
- Replace confusing `!source.trusted` save condition with explicit
  `newly_trusted` flag for readability
- Add unit test for non-interactive trust gate (verifies the error
  includes agent name and git URL)
- Skip serializing `trusted = false` to keep config.toml clean
  (only `trusted = true` appears)
- Document trust in load.mdx (step 3 in "What happens", warning
  callout for CI) and security-model.mdx (new section 4 with
  config.toml example, threat model row, best practices update)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: broken docs anchor link and misleading test name

- Fix cross-reference link in load.mdx: use #4-agent-source-trust
  (matching the numbered heading in security-model.mdx)
- Rename test to `load_trusted_namespaced_agent_builds_and_runs` since
  it no longer exercises auto-registration (that's covered by the
  config test `resolve_agent_source_adds_owner_repo_on_first_use`)

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* feat: add jackin trust/untrust CLI, testable trust gate, revocation

Address all follow-up items from the maintainer review:

- Extract load_agent_with() with injected trust callback, following the
  resolve_agent_repo_with() pattern. Tests can now exercise accept and
  decline paths without mocking dialoguer.
- Restore auto-registration test coverage: the accept test uses
  load_agent_with(auto_trust) so resolve_agent_source's is_new path is
  exercised again. Add decline test verifying no config persistence and
  no Docker commands when trust is refused.
- Add `jackin trust <selector>` and `jackin untrust <selector>` CLI
  commands so operators can manage trust without editing config.toml.
- Add untrust_agent() to AppConfig with tests.
- Document trust/untrust CLI in security-model.mdx and load.mdx.
- Document that trust is keyed by selector (not URL) with a note about
  remote-mismatch protection.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: consolidate trust/untrust into single CLI command

Follow mise's pattern: `jackin trust` is one command with flags
instead of separate trust/untrust subcommands.

  jackin trust <selector>              # grant trust
  jackin trust <selector> --untrust    # revoke trust
  jackin trust <selector> --show       # check status

The --show and --untrust flags conflict with each other (enforced
by clap).

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* refactor: move trust CLI under config with grant/revoke/list subcommands

Follow the same pattern as `jackin config mount {add,remove,list}`:

  jackin config trust grant chainargos/the-architect
  jackin config trust revoke chainargos/the-architect
  jackin config trust list

This keeps all config mutations under `jackin config` and uses the
subcommand pattern consistently throughout the CLI. The `list`
subcommand shows all currently trusted agents.

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

* fix: refuse to revoke trust on builtins, add CLI parsing tests

- is_builtin_agent() check prevents revoking trust on built-in agents
  (agent-smith, the-architect) which would be silently re-granted by
  sync_builtin_agents on next load
- Add 3 CLI parsing tests for config trust {grant, revoke, list},
  matching the existing coverage for config mount {add, remove, list}
- Tighten TrustCommand::List doc comment to "List all currently
  trusted agent sources"

https://claude.ai/code/session_01SkHoAne5Q5EpUybYk6ghUT

---------

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
…183)

* docs(roadmap): iteration 13 — AI code verifiability framing, config/types.rs full spec

Primary goal shift: codebase must be verifiable for AI-generated code.

- §0: replace generic description with explicit verifiability rationale
  (module contracts, localised concerns, types/behaviour separation)
- §4 intro: add "Why structure matters for AI-generated code" section with
  audit-units table mapping each post-split file to one reviewable question
- §4 4a: expand config/types.rs from description to full execution spec —
  exact type list, post-split mod.rs content, zero-change submodule
  guarantee (verified: agents.rs/persist.rs/workspaces.rs use super::T
  which resolves through mod.rs re-exports unchanged), impl-extension
  pattern already in use documented


* docs(roadmap): iteration 14 — editor method map, app helpers, //! queue

- §4 4c: config/editor.rs split is now execution-ready — complete
  6-file method-to-file table with private helper placement verified
  (validate_candidate→io.rs, table_path_mut→mod.rs pub(super),
  auth_forward_str→agent_ops.rs, create_workspace delegates to AppConfig)
- §4 4e: app/mod.rs split complete — all private helpers mapped
  (parse_auth_forward_mode_from_cli→config_cmd.rs,
  workspace_env_scope→workspace_cmd.rs, print_env_table note,
  remove_data_dir_if_exists→dispatch.rs)
- §10 step 5: add //! priority queue — 10 files with draft content,
  prioritised by cold-landing impact and AI audit risk; selector.rs
  and instance/mod.rs explicitly document the /→__ invariant


* docs(roadmap): iteration 15 — dep graph fix, trust.rs safety, OQ1 closed

- §4 4d: correct operator_env dependency graph — layers.rs imports
  both mod.rs (OpRunner) AND client.rs (OpCli for non-injectable
  resolve_operator_env wrapper at line 797); still a valid DAG
- §4 4f: verify trust.rs split safety — FnOnce injection pattern means
  launch_pipeline.rs has zero dependency on trust.rs; import chain
  documented; trust bypass audit now requires reading only ~60L
- §9 OQ1 closed: op_cache.rs read in full — 4-level structure,
  per-level invalidation, no TTL/expiry (expiry handled at OpCli
  subprocess level), DEFAULT_ACCOUNT_KEY sentinel documented


* docs(roadmap): iteration 16 — CommandRunner Rule 3, render/editor split, 4a/4c independence

- Fix duplicate Rule 3 section introduced by previous edit; add docker.rs
  co-location note as third edge case (three edge cases, not two)
- Add render/editor.rs as new Rule 5 violator: 1666L post-PR #171
  (was listed as 782L); propose 6-file tab-by-tab split with auditability
  note on the security-adjacent Secrets tab
- Add §10 execution-order note: 4a and 4c are independent — editor.rs
  imports AppConfig via crate::config re-exports regardless of 4a order
- Append iteration 16 log entry with confidence table and weakest sections


* docs(roadmap): iteration 17 — instance/auth.rs audit, state.rs split, line count corrections

- Add instance/auth.rs to //! priority queue at #4: four security
  invariants (0o600 perms, symlink rejection, TOCTOU-safe writes,
  macOS Keychain) documented in draft //! content
- Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types
  mixed with impl blocks; propose 5-file types/behavior split
- Correct stale line counts: render/list.rs 1122→1989 (PR #171 added
  render_environments_subpanel); state.rs 865→992; priorities upgraded
- Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332,
  mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944
- Renumber //! priority queue to 11 entries (was 10)


* docs(roadmap): iteration 18 — agent_allow OQ2 closed, render/list.rs split proposal

- Close OQ2: agent_allow.rs read in full — 55L, correct //! doc,
  design sound; serves as model for //! priority queue pattern
- Add render/list.rs as new Rule 5 violator: 668L production (PR #171
  added render_environments_subpanel); propose 3-file split (mod.rs,
  details.rs, subpanels.rs); note import-path change for agents_block_agent_count
- Update §1 module map: agent_allow.rs entry corrected with size/API


* docs(roadmap): iteration 19 — input/editor.rs critical correction, split proposal

- Correct input/editor.rs: 2349L total (was 1304L), 1141L production
  (was 547L) — PR #171 added Secrets-tab handlers; pub(super) fn
  handle_editor_modal at line 618 was invisible to previous grep pattern;
  now the largest production file in the codebase; priority → Critical
- Correct input/save.rs: 1472L total, 661L production (was 567L)
- Add 5-file split proposal for input/editor.rs: mod.rs (two dispatchers),
  secrets.rs (~500L AI-generated Secrets-tab), agents.rs, mounts.rs, general.rs
- Update key insight paragraph naming input/editor.rs as largest production file


* docs(roadmap): iteration 20 — console splits in §10, MSRV evidence, animation.rs verdict

- Add console/manager/ as §10 Step 4f group with 5 sub-steps in priority
  order; rename existing 4f (launch.rs) → 4g; add circular-import risk note
  for ManagerStage/EditorState split sequencing
- Analyze tui/animation.rs: 582L all-production, no split needed (banner_grid
  is a tightly-coupled rendering loop); section comments compensate for missing //!
- Partially close OQ3: u64::is_multiple_of (stabilized 1.86) found in animation.rs;
  within declared MSRV 1.94; full cargo +1.94.0 check deferred (toolchain unavailable)


* docs(roadmap): iteration 21 — input/save.rs split, //! queue fix, save.rs corrections

- Add input/save.rs split proposal: 4 pub(super) fns discovered; 3-file
  split (mod.rs + flow.rs + preview.rs); no cross-dependency between
  flow and preview groups; §10 4f-v updated from Optional to concrete plan
- Fix //! queue preamble: "first 10 files" → "first 11 files"
- Correct save.rs module map (1418→1472L, correct key exports) and
  hot-spot table note (begin_editor_save ~280L → ~118L; commit_editor_save
  is the Phase 2 partner at ~149L)


* docs(roadmap): iteration 22 — input/list.rs and mount_info.rs analysis

- Analyze input/list.rs: 214L production (tests at 215); has //! doc;
  two focused pub(super) fns; no split needed; Low priority; correct module map
- Add mount_info.rs to hot-spot table: 277L production; Low priority;
  has //! doc; correct module map with 3 public enums + inspect fn
- Fix stale §2 diagnosis note: docs/internal/roadmap/ now exists


* docs(roadmap): iteration 23 — audit units table +5 console rows, input/mod.rs corrected

- Expand audit units table from 8 to 13 entries: add state/types.rs,
  state/editor.rs, input/editor/secrets.rs, render/list/subpanels.rs,
  input/save/preview.rs — all targeting PR #171 AI-generated console code
- Add PR #171 context note linking 5 new entries to AI-generated code concern
- Correct input/mod.rs module map: 369L, add InputOutcome enum to exports
- Verify rust-toolchain.toml absence; §7.7 and §2 concept 25 already correct


* docs(roadmap): iteration 24 — render/mod.rs analysis, //! exemplars table, EditorTab confirmed

- Add §4 Rule 7 positive exemplars table: 7 files with //! docs graded
  1-element (render/mod.rs), 2-element (input/save.rs etc), 3-element
  (env_model.rs, agent_allow.rs); PR #171 docs-discipline pattern noted
- Correct render/mod.rs module map: 421L; FooterItem + palette constants
  + render_header + centered_rect_fixed added to key exports
- Confirm EditorTab variants: General, Mounts, Agents, Secrets (Rust enum)
  vs "Secrets / Environments" (UI label); /stub qualifier already removed


* docs(roadmap): iteration 25 — too_many_lines recount, FooterItem PR, MountConfig caveat

- Correct too_many_lines count: 13 across 8 → 16 across 11 files
  (PR #171 added 5 suppressions in console/manager); add full breakdown
  table; update all 3 occurrences in roadmap
- Fix FooterItem PR reference: #165#166 (confirmed by git log --follow)
- Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description


* docs(roadmap): iteration 26 — console/mod.rs and op_picker/render.rs analyzed

- Add console/mod.rs to hot-spot table: 406L/307L production (Low);
  correct module map from ~200 → 406L; note missing //! doc with
  ConsoleStage design block comment worth promoting
- Add op_picker/render.rs to hot-spot table: 865L/545L production
  (Medium); PR #171 AI-generated; 14 functions in two logical groups
  (entry/helpers vs level renderers); split into levels.rs proposed
- Correct 3 stale ~200L estimates for console/mod.rs across roadmap


* docs(roadmap): iteration 27 — op_picker/mod.rs discovery, render split, operator_env correction

- Add op_picker/mod.rs to hot-spot table: 1712L/775L production (High);
  PR #171 AI-generated; OpPickerState types+behavior split opportunity;
  has 7-line //! doc; module map split into two rows (mod.rs + render.rs)
- Add op_picker/render.rs 2-file split proposal: render.rs (coordinator)
  + render_pane.rs (pane/level renderers); no cross-dependency confirmed
- Correct operator_env.rs total: 1569→2130L (880L production); update
  4 occurrences across hot-spot table, ASCII tree, §4 analysis


* docs(roadmap): iteration 28 — op_picker/mod.rs 3-file split, count corrections

- Add op_picker/mod.rs formal 3-file split: loading.rs (async load family
  ~120L) + keys.rs (4 level key handlers ~315L) + mod.rs (types/constructors)
- Correct "24 files" → "28+" for 500L threshold count
- Update total LOC: ~40,664 → ~43,587 (2 occurrences, with provenance note)


* docs(roadmap): iteration 29 — op_picker execution order + file_browser analysis

- §10 Step 4f: expand from 5 to 7 sub-steps; add 4f-vi (op_picker/mod.rs
  → mod.rs + loading.rs + keys.rs) and 4f-vii (op_picker/render.rs →
  render.rs + pane.rs); document impl-extension and import-path caveats
- §4 //! exemplars: add file_browser/ subsystem analysis — all 5 files
  have //! docs, no file exceeds ~350L production; classified as exemplar
  (not a split candidate); document git_prompt.rs coupling-density
  justification and input.rs as 28-file false positive (144L production)
- §1 module map: expand single file_browser/ row to 5 individual rows
  with production LOC and dominant concern per file


* docs(roadmap): iteration 30 — challenge split-first thesis, fresh LOC corrections

- §4: Add "Alternative thesis: documentation-first verification" — challenges
  the two core assumptions behind file splitting (files-as-audit-unit and
  file-size-as-context-constraint); adds 7-criterion comparison table vs
  structure-first approach; introduces phased combined recommendation:
  Phase 1 = doc sprint (//! contracts + specs/ for 3 subsystems, 2-3 PRs,
  zero structural change); Phase 2 = splits only for >600L production files
  (reduces scope from 14+ to 4 files); Phase 3 = workspace if LOC > 150K
- Fix stale LOC: app/mod.rs 951→979, config/editor.rs 1467→1548 (7 and
  8 locations respectively; verified by fresh find|xargs wc -l scan)
- §1 module map: add agent_picker.rs (436L), scope_picker.rs (201L),
  source_picker.rs (244L) — all PR #171 additions with //! docs


* docs(roadmap): iteration 31 — fix 600L→800L threshold error, correct LOC

- §4 alternative thesis: correct ">600L production → 4 files" claim
  introduced in iteration 30; re-verified all 9 candidate files via
  #[cfg(test)] line position; threshold must be >800L to get exactly 4
  files (9 exceed 600L); add verification table with test-start lines
- Production LOC corrections (5+ locations each):
  launch.rs 1085→~1077, operator_env.rs 810→~880,
  app/mod.rs 928→~957, config/editor.rs 503→~584
- §2 OpPicker row: replace vague "no entry yet" with confirmed gap:
  PROJECT_STRUCTURE.md line 53 still lists pre-PR#171 widget set (10
  named); omits op_picker/, agent_picker.rs, scope_picker.rs,
  source_picker.rs and pre-dates the manager/ sub-structure split


* docs(roadmap): iteration 32 — two-tier spec arch, behavioral spec template

- §8.1: Add two-tier spec architecture table distinguishing feature specs
  (public Starlight MDX, user-facing) from behavioral specs (internal
  docs/internal/specs/, for AI code verification) — resolves contradiction
  between §4 (which said docs/internal/specs/) and §8.1 (which said
  "no longer needed; specs are public")
- §8.1: Add concrete behavioral spec template for op_picker/ with state
  machine table and 3 INV invariant entries each with a grep-executable
  "Verify by:" command; template directly usable for the 3 Phase 1 specs
- §8.1: Remove erroneous "docs/internal/specs/ no longer needed" claim
- Confirmed render/editor.rs ~736L and render/list.rs ~668L production
  (no interspersed production code — all test blocks follow consecutively)


* docs(roadmap): iteration 33 — executive summary, §0 correctness

- §0: Add executive summary (~300 words) with core problem, 3-phase
  recommendation, key counter-argument, and navigation table pointing
  to §2/§4/§7/§8/§10 by question — resolves the meta-irony of a
  readability roadmap with no entry-point orientation
- §0 item 2: "1569-line monolith" → "2130-line monolith" (operator_env.rs
  current verified size; stale reference was in the first section readers see)
- §0 item 3: Add "(selective)" qualifier and explicit note that standard
  Rust co-locates struct+impl — impl-extension pattern is justified only
  for files >800L production, not as a universal rule


* docs(roadmap): iteration 34 — spec priority reorder, §10 Phase 1 track

- §0 + §4 Phase 1: Prioritize runtime/launch.rs behavioral spec (no //!
  doc, ~1077L production, critical path — all jackin load failures trace
  here); drop config/editor.rs from Phase 1 (its 963L test suite already
  serves as behavioral spec — tests are behavioral examples); reduce Phase
  1 from 3 specs to 2 specs; add reasoning for the priority ordering
- §10 Step 2: Split into two parallel tracks — Track A (cc-sdd tooling
  setup) + Track B (Phase 1 behavioral spec authoring); Track B includes
  specific INV invariants to capture for runtime/launch.rs grounded in
  reading the actual function structure (step comment positions); adds
  sequencing rationale: spec must precede structural splits


* docs(roadmap): iteration 35 — verified INV entries for runtime/launch.rs

Read load_agent_with lines 553-892 in full. Replaced 3 draft INVs from
iteration 34 (inferred from step comment positions) with 5 verified INVs
citing exact line numbers:
- INV-1: trust gate (line 594) precedes image build (line 736)
- INV-2: container name claimed (line 754) between image build and network
- INV-3: token verified (line 763) before network creation (line 827)
- INV-4: render_exit called at lines 886 AND 890 (all exit paths)
- INV-5: cleanup disarm semantics — Running→disarm, clean exit→cleanup,
  crash→disarm (explains jackin hardline compatibility)
Corrected wrong line number: claim_container_name call is at 754, not 918
(918 is the function definition). Each INV has a grep-executable Verify by.


* docs(roadmap): iteration 36 — CI gate for PROJECT_STRUCTURE.md freshness

§3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with
three concrete options:
- Option A: CONTRIBUTING.md rule (necessary but insufficient)
- Option B: ci.yml git-diff-scoped shell check (recommended) — only checks
  files added in the current PR so it doesn't require fixing existing stale
  entries before merging; greps for module directory name in prose
- Option C: Structured TOML module registry (over-engineered for scale)
Includes concrete YAML snippet for Option B grounded in the check:repo-links.ts
pattern already established in docs/scripts/


* docs(roadmap): iterations 36-37 — CI gate + greenfield workspace architecture

Iteration 36:
- §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with
  3 options (CONTRIBUTING.md rule / ci.yml git-diff check / TOML registry);
  recommend Option B (git-diff-scoped YAML step) with concrete snippet grounded
  in existing check:repo-links.ts pattern from docs/scripts/

Iteration 37 (operator directive: greenfield Rust structure):
- §4: Add "Greenfield architecture — ideal structure for a growing project"
  section based on verified cross-module dependency graph (grep iteration 37)
- Confirms dependency tiers: workspace/manifest/docker/paths/selector = Tier 0;
  config/tui/instance = Tier 1; operator_env/runtime/repo = Tier 2; console = Tier 3
- Key finding: workspace/ is LOWER-level than config/ (config re-exports workspace
  types at lines 5-6); ideal naming inverted in greenfield (jackin-core > jackin-config)
- Documents ideal 6-crate workspace: jackin-core, jackin-config, jackin-tui,
  jackin-runtime, jackin-console, jackin-shell + thin binary
- Notes console/ has NO runtime/ import — cleanest pre-existing crate boundary
- Bridge: incremental splits (4a, 4d, 4g) are pre-work toward workspace migration


* docs(roadmap): iteration 38 — Rust workspace standards, community evidence

Ground workspace recommendation in real-world project research:
- ripgrep (9 crates), gitui (5 crates) went workspace due to library consumers
- starship and fd-find stay single-crate at 1M+ LOC — no library use case
- jackin (43K LOC, no external consumers) maps to starship/fd pattern
  → single-crate is community-standard; "stay single-crate" recommendation confirmed

Update greenfield workspace structure to follow matklad's pattern:
- Virtual manifest at root (no [package] in root Cargo.toml)
- Flat crates/ directory (not nested); crate names match folder names
- version = "0.0.0" for unpublished internal crates
- Add inline dep comments to each crate in the ASCII structure

Add research notes: ripgrep/starship/gitui/fd-find Cargo.toml findings +
Cargo workspaces reference + matklad "Large Rust Workspaces" (2021-08-22)


* docs(roadmap): revise §7.9 + §3 — adopt per-directory README.md

§7.9: Reverse previous "reject" recommendation to "adopt" per-directory
README.md for major src/ module directories. Rationale: README.md is
AI-native — Claude Code, Copilot, Cursor load it automatically on directory
entry, giving AI agents orientation before they decide which file to open.
PROJECT_STRUCTURE.md being confirmed stale removes the main argument for
the "single root file" approach.

Add three-layer documentation model table:
- README.md: directory orientation (AI + human, on entry)
- AGENTS.md: agent workflow rules (root, session start)
- CLAUDE.md: @AGENTS.md pointer only — NEVER add content here
- //! docs: file-level contracts (when reading/editing)

Add specific README.md content targets for 7 directories
(src/, src/runtime/, src/console/, src/console/manager/,
src/console/widgets/, docs/, docs/internal/).

§3 target document shape: Add per-directory README.md to proposed
hierarchy; add docs/internal/specs/ explicitly; note CLAUDE.md
design principle (single-line @AGENTS.md — never duplicate content).


* docs(roadmap): internal docs are browsable — unified Starlight site

Operator directive: internal docs (architecture, specs, ADRs, roadmap) should
be browsable, not hidden filesystem files. They are a different TYPE of docs
focused on implementation details and vision, published as a "Developer
Reference" section of the Starlight site.

§3 target document shape:
- docs/internal/ moves into docs/src/content/docs/internal/ (Starlight pages)
- Browsable at jackin.tailrocks.com/internal/
- Sidebar: "Developer Reference" group (collapsed by default) with sub-sections
  for architecture, code-tour, contributing, testing, decisions, specs, roadmap
- Include astro.config.ts sidebar config snippet

§8.1 two-tier spec distinction eliminated:
- Feature specs and behavioral specs both live at docs/src/content/docs/internal/specs/
- Type expressed via spec_type: behavioral | feature frontmatter, not filesystem location
- Both browsable and searchable via Starlight; AI agents can be pointed to URLs

§8.3 + §4:
- All docs/internal/specs/ paths → docs/src/content/docs/internal/specs/
- ADRs: docs/internal/decisions/ → docs/src/content/docs/internal/decisions/ (browsable)
- README.md pointer for src/runtime/ updated to URL reference


* docs(roadmap): §11 — modern Rust docs platform (future project)

Add §11 capturing the vision for a modern docs.rs alternative with:
- rustdoc JSON ingestion → Astro Starlight presentation
- MCP server for AI agent queries (Context7 alternative for Rust)
- Rust-specific query types: rust_get_context(), rust_find_impls(),
  rust_search_types() — things Context7 cannot provide
- Comparison table vs Context7
- Architecture diagram (ingestion → processing → Starlight + MCP)
- Name candidates: rustlight, ferrodoc, cargo-starlight / starlight.rs
- Note that jackin's §7.15 gen-rust-api.ts pipeline is the intentional
  prototype for the platform's processing and presentation layers


* docs(roadmap): iteration 39 — update §0, fix stale internal/ paths

§0 executive summary: rewrite to reflect decisions from iterations 30-38:
- browsable internal docs (jackin.tailrocks.com/internal/)
- per-directory README.md adoption (§7.9 reversed)
- CLAUDE.md = @AGENTS.md single-line pointer only
- greenfield workspace architecture (matklad's virtual manifest pattern)
- §11 future project: modern Rust docs platform / Context7-for-Rust
- document size 1800+ → 2200+

Fix stale docs/internal/ bare paths not caught by iteration 38 sweep:
- Mermaid diagram: INTERNAL_ROADMAP, INTERNAL_CODE_TOUR → Starlight paths
- §7.10 ADRs: docs/internal/decisions/NNN-title.md → .mdx Starlight path
- §10 Track B item 2: op-picker spec path → Starlight MDX


* docs(roadmap): iteration 40 — §7.15 pipeline + Rule 4 pub audit

§7.15 (new): rustdoc JSON → Astro Starlight API documentation pipeline
- Three options: rustdoc HTML publish / rustdoc JSON + bun script (recommended)
  / rustdoc-json crate as Rust binary
- Option B recommended: matches existing docs/scripts/ pattern, nightly
  isolated to separate CI step, zero effect on stable build
- Key design: URL at /internal/api/, cross-links to behavioral specs,
  Starlight unified search, prototype for §11 future project
- Pub(crate) note: gen-rust-api.ts can feed Rule 4 visibility audit
- Recommend: adopt after Phase 1 //! sprint (value ∝ coverage)

§4 Rule 4 pub discipline: replace estimated "50-100 items" guess with
verified numbers from iteration 40 grep:
- 257 bare pub items, 21 pub(crate), 61 pub(super) across 94 files
- 0 uses of unreachable_pub lint — no enforcement gate
- Top violators: operator_env.rs (17), tui/output.rs (13), planner.rs (8)
- Add concrete Cargo.toml [lints.rust] snippet: unreachable_pub = "warn"
- Revised scope: ~150-200 mechanical conversions (excludes entry points)


* docs(roadmap): split research into 19 actionable items

Delete _research_notes.md (no longer needed).

Replace 2343L READABILITY_AND_MODERNIZATION.md with:
- README.md: index of all 19 items with phase, ordering notes, links
- READABILITY_AND_MODERNIZATION.md: lightweight research summary (63L)
- items/ITEM-001 through ITEM-019: individual actionable items

Items by phase:
  Phase 1 (low risk, no confirmation): ITEM-001..004, 006..011
  Phase 1 (needs confirmation): ITEM-005, 016, 018
  Phase 2 (structural splits, confirmation required): ITEM-012..015
  Phase 3 (deferred): ITEM-017 (rustdoc pipeline), ITEM-019 (workspace)

Each item has: summary, key files with line numbers, steps, what
needs confirmation, and relevant research backing from the 40-iteration
analysis loop.


* docs(roadmap): migrate 19 items to Starlight reference/roadmap section

Move all codebase health roadmap items from docs/internal/roadmap/items/
(plain Markdown, not browsable) to docs/src/content/docs/reference/roadmap/
(MDX pages, browsable at jackin.tailrocks.com/reference/roadmap/).

Adds a new "Codebase health" sidebar group (Phase 1 → Phase 3) to
astro.config.ts. Deletes the old items/ directory. Updates the internal
README to redirect to the new location.

Also adds codebase-readability.mdx — a new overview item that captures
the overall readability/restructuring program with a recommended execution
order: file splits first, then greenfield workspace, then per-directory
README+AGENTS.md, then docs and specs.


* docs(roadmap): remove premature internal/roadmap/README.md

The internal/ structure doesn't exist yet — it will be created as part
of the roadmap items themselves. No need for a redirect stub now.


* docs(roadmap): remove READABILITY_AND_MODERNIZATION.md research archive

All content has been distilled into the individual Starlight roadmap pages.
The full 2343L research is preserved in git history at commit b7e9fc2.


* docs(roadmap): fix check:repo-links errors + remove iteration log

- Replace plain code spans with <RepoFile> for validate.rs, mise.toml,
  Cargo.toml, and op_picker/mod.rs
- Remove deleted READABILITY_AND_MODERNIZATION.md reference from
  codebase-readability.mdx
- Delete _iteration_log.md (git history is the archive)


* docs(roadmap): fix lychee false-positive link in move-contributing-testing

The example redirect text contained a markdown hyperlink to a proposed
future file path that doesn't exist yet. Changed to a code span.


---------

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

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


* feat(isolation): introduce MountIsolation enum


* feat(workspace): add isolation field to MountConfig

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


* feat(workspace): reject nested isolated mounts

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


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

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


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

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


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

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


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

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


* feat(isolation): list_records_for_workspace walks data dir

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


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

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


* feat(isolation): MaterializedWorkspace types

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


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

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


* feat(isolation): ensure_worktree_config_enabled

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


* feat(isolation): preflight checks for worktree materialization

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


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

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


* feat(isolation): materialize_workspace orchestrator

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


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


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

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


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

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


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

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


* feat(isolation): finalizer skeleton + AttachOutcome shape

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


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

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


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

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


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


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

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


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

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


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

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


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

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


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

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


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

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


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

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


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

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


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

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


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

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

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


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

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

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

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

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


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

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


* docs(mounts): document mount isolation field

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


* docs(configuration): add MountConfig.isolation field

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


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

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


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

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


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

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


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

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


* docs(cd): add jackin cd command reference

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


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

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


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

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


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

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


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


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


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

* ci(docs): add link checking


* ci(docs): stabilize lychee checks


* ci(docs): validate edit links with lychee


* ci(docs): close link check gaps


* docs: add repo file link component


* docs: explain repo link source check


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

Two refinements from PR review:

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

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


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

Address review feedback on PR #173.

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

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

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

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


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

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

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


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

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

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


---------

(cherry picked from commit f3f3e5e)

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

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


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

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

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


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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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


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

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

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

For each isolated worktree, the container now sees:

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

Roadmap doc updated to reflect the final design.


* style(isolation): rustfmt assert_eq! width


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

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

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


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

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

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

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

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


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

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

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

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

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

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


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

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

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

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


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

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


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

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

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

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

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

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

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


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

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

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

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

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

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

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

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


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

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

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

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

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

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

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


---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant