Skip to content

feat: pre-launch hooks and interactive runtime env vars#1

Merged
donbeave merged 21 commits into
mainfrom
feature/pre-launch-hooks-env-vars
Apr 6, 2026
Merged

feat: pre-launch hooks and interactive runtime env vars#1
donbeave merged 21 commits into
mainfrom
feature/pre-launch-hooks-env-vars

Conversation

@donbeave

@donbeave donbeave commented Apr 4, 2026

Copy link
Copy Markdown
Member

Summary

  • Add [hooks] section to agent manifest with pre_launch script support — runs custom bash scripts before Claude starts (e.g., ctx7 MCP setup, specstory login)
  • Add [env.*] section with interactive prompting — static defaults, free-text input, select-from-list, skippable prompts, and dependency chains with skip cascading
  • Enforce strict manifest parsing (deny_unknown_fields) across all manifest structs to catch typos
  • New env_resolver module with topological sort for dependency ordering and cycle detection
  • Pre-launch hooks are copied into the derived Docker image and executed by the entrypoint

Unblocks ChainArgos migration from custom Docker setup to jackin (addresses 2 of 5 migration gaps).

Design spec: docs/superpowers/specs/2026-04-04-pre-launch-hooks-and-env-vars-design.md

Test plan

  • 36 new unit tests covering manifest parsing, validation rules, env resolution, skip cascading, dependency ordering, hook path validation
  • All 173 tests pass, zero clippy warnings
  • Manual test with a sample agent manifest declaring hooks and interactive env vars
  • Verify docs build (cd docs && bun run build)

🤖 Generated with Claude Code

donbeave and others added 21 commits April 4, 2026 07:55
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracts validate_relative_path() helper, uses it to validate pre-launch
hook paths (relative, inside repo, non-empty file), and calls manifest.validate()
for env var declaration checks during agent repo validation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… hooks

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

Amp-Thread-ID: https://ampcode.com/threads/T-019d6365-46bb-77ef-a9ac-4438ec6c8710
Co-authored-by: Amp <amp@ampcode.com>

# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/derived_image.rs
…-findings

fix: address pre-launch env review findings
@donbeave donbeave merged commit fde8258 into main Apr 6, 2026
1 check passed
@donbeave donbeave deleted the feature/pre-launch-hooks-env-vars branch April 6, 2026 18:10
donbeave added a commit that referenced this pull request Apr 19, 2026
Section 03 — "The false choice" reframed as red pill / blue pill:
- Two-tone CSS pill capsules (colored + white halves with gloss highlight)
- Blue pill = "Babysit every prompt" (gated, productivity destroyed)
- Red pill = "Full YOLO on host" (unfiltered, risk maximum)
- Transition: "Refuse the pill. You're the Operator — define the
  construct instead." — operator = Morpheus, above the choice

Section 04 — Kitchen-Sink vs Role-Specific callout:
- Two-card insight block below the composition machine's axis note
- Left: neutral "Kitchen-Sink Agent" (Every toolchain/plugin/convention)
- Right: green-accented "Role-Specific Agent" (Only relevant tools/plugins)
- Argues the *why* of narrow agent classes without adding a new section

Section 06 — New: "Parallel work" (clone the agent, split the work):
- Three clone cards: agent-smith #1/#2/#3 on different branches
  (feature/auth-redesign, feature/payment-v2, fix/api-timeout)
- Each card: pulsing live dot, branch name, container/DinD/history/network
- Four scenarios: parallel features, service fan-out, divide and conquer,
  experiment safely
- Old Section 06 (Security) renumbered to 07; old 07 (Ecosystem) to 08

Section 08 — Ecosystem rewritten with 5-agent Matrix cast:
- agent-smith (built-in, default)
- the-architect (built-in, Rust dev)
- agent-jones (archetype: company specialist — scentbird/jackin-agent-jones)
- agent-brown (archetype: role-specific frontend)
- your-own (build-your-own template)
- Section title: "Every org builds a cast"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 20, 2026
* design: add landing page mockup for jackin.tailrocks.com

Full-page mockup with editorial split hero and seven sections:
Vocabulary, Problem, Mental Model (org × agent-class × workspace
composition machine with mounts), How It Works, Security,
Ecosystem + Install.

Hero rain is a faithful port of src/tui.rs digital_rain — ASCII
char pool, age-based color gradient, 35ms frame rate. Standalone
HTML with inlined CSS/JS, no build step required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: landing page iteration — full-screen rain hero + scroll-driven glossary

Hero:
- Full-screen (min-height: 100vh) with flex-centered content
- Nav moved inside hero-stage so rain falls from Y=0 through the nav
- Nav dropped its border + nav-links; Star button now CTA-only (no count)
- Trim pass: removed meta-row, pill band, sub-stats, footer, and
  duplicate brew-install buttons; tightened hero to headline + deck + CTA

Rain:
- Faithful port of src/tui.rs digital_rain — ASCII char pool, age-based
  color gradient (WHITE/pale/MATRIX_GREEN/mid/MATRIX_DIM/MATRIX_DARK),
  per-column speed/fade, mutation probabilities, 35ms frame rate
- 32% canvas opacity + radial vignette + bottom-fade so text stays legible

Section 2 (Vocabulary) — full redesign:
- Replaced Matrix-mapping table with scroll-driven dictionary
- Sticky rail (300px) + Fraunces-serif detail panel
- Nine entries now include Agent class + Workspace with explicit relationships
  (Agent class "built on top of the Construct"; Workspace is "a named list
  of mounts and access rules"; Jacking in is "Loading an agent into a workspace")
- Section is 500vh tall; scroll progress drives which entry is active;
  rail items are also click-to-jump with smooth scrollTo
- Introduces Fraunces serif alongside Inter + JetBrains Mono

Section 4 (Composition Machine):
- Extended to three dimensions: organization × agent-class × workspace
- Three orgs preloaded (jackin-project, chainargos, acme) with realistic
  agent/workspace configs; workspaces show per-mount rw/ro tags
- Cross-path mounts render src → dst; same-path mounts render once
- Allowed-agents rejection renders a red "not loaded" state

Section 7 (Ecosystem):
- Repo names follow the jackin- prefix convention
  (jackin-project/jackin-agent-smith, your-org/jackin-your-agent)
- Install block shows the cd step before jackin load; copy button removed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: red pill/blue pill, cloning section, 5-agent cast

Section 03 — "The false choice" reframed as red pill / blue pill:
- Two-tone CSS pill capsules (colored + white halves with gloss highlight)
- Blue pill = "Babysit every prompt" (gated, productivity destroyed)
- Red pill = "Full YOLO on host" (unfiltered, risk maximum)
- Transition: "Refuse the pill. You're the Operator — define the
  construct instead." — operator = Morpheus, above the choice

Section 04 — Kitchen-Sink vs Role-Specific callout:
- Two-card insight block below the composition machine's axis note
- Left: neutral "Kitchen-Sink Agent" (Every toolchain/plugin/convention)
- Right: green-accented "Role-Specific Agent" (Only relevant tools/plugins)
- Argues the *why* of narrow agent classes without adding a new section

Section 06 — New: "Parallel work" (clone the agent, split the work):
- Three clone cards: agent-smith #1/#2/#3 on different branches
  (feature/auth-redesign, feature/payment-v2, fix/api-timeout)
- Each card: pulsing live dot, branch name, container/DinD/history/network
- Four scenarios: parallel features, service fan-out, divide and conquer,
  experiment safely
- Old Section 06 (Security) renumbered to 07; old 07 (Ecosystem) to 08

Section 08 — Ecosystem rewritten with 5-agent Matrix cast:
- agent-smith (built-in, default)
- the-architect (built-in, Rust dev)
- agent-jones (archetype: company specialist — scentbird/jackin-agent-jones)
- agent-brown (archetype: role-specific frontend)
- your-own (build-your-own template)
- Section title: "Every org builds a cast"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: real agent-class structure + redesigned Cast section

Section 04 (The Approach):
- Route 01 is now prose + toolset chips (no Dockerfile), describing
  the-architect concretely: what it adds on top of the construct
- Route 02 uses tabbed builder (manifest / Dockerfile) showing both
  files that make up a real jackin agent class
- Dockerfile patterns now match jackin conventions: FROM projectjackin
  (not jackin-project), mise install … && mise use --global, and the
  USER root / apt-get / USER claude pattern for system packages
- Dropped fictional features (password-manager ENV, EXPOSE port) since
  they're not part of jackin today
- Each route now shows CLI + REPO mapping (e.g. jackin load the-architect
  vs github.com/jackin-project/jackin-the-architect)

Section 05 (Cast) — full redesign:
- Reduced from 6 cards to 3 character cards + 1 full-width invite strip
- Dropped the Architect (covered in Section 04 Route 01) and DB
  maintainer (no Matrix moniker) — zero duplication with Section 04
- Cards lead with the Fraunces-serif character name, not the role
  identifier, making Smith/Jones/Brown the visual anchors
- Removed placeholder repo paths — they were fake noise
- "Create your own role" is now a distinctly-styled full-width strip
  below the character trio, with a real link to the
  developing/creating-agents docs
- Intro updated: "Smith, Jones, Brown — archetypes to adopt.
  Every other role, yours to cast."

Section 02 (Vocabulary):
- Construct image reference fixed to projectjackin/construct:trixie
  (matches docs/developing/construct-image.mdx)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: tighten Section 06 Mental Model copy

- Intro shortened from ~60 words to 15: "Same agent in different
  workspaces. Same workspace with different agents. Pick both —
  see what runs."
- Dropped the axis-note block entirely (it was re-teaching Section
  02's dictionary definitions)
- Simplified machine sublabels: "the tool profile" / "workdir + mounts"
  / "the resulting container" (down from 2-3 words each)
- Kitchen-Sink/Role-Specific callout notes compressed to single
  lines: "Too much context — worse decisions." and "Focused context
  — better results, faster."
- Section is now ~60% shorter on copy with the interactive machine
  unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: daily-loop redesign, drop Security, simplify Install, add wordmark footer

Section 06 (Mental Model):
- acme → your-org across composition machine data and tabs

Section 07 (How it Works):
- Redesigned from vertical filmstrip into "The daily loop" with five
  vertical frames, each using a 2-column layout (info left, full terminal
  right with chrome dots)
- Added new "clone" frame between load and hardline that demonstrates
  parallel agents with cd setup and two load cycles on different paths
- Each frame: Fraunces-serif command name, italic mythos line
  (Jacking in, More of me, The hardline, Pulling out, Casting out),
  description paragraph, full terminal with colored output
- Frame 01 load now shows the cd ~/Projects/my-app setup step

Section 08 (Parallel work — clone cards + scenarios):
- Deleted entirely. The clone story is now Entry 02 of the daily loop.
  Old CSS (.clones, .clone-card, .scenarios, .scenario) also removed.

Section 08 (Security Model — honest version):
- Deleted. The section is documented elsewhere; on the landing it was
  slowing down the path to install.

Section 08 (Install — was 09/10):
- Simplified: dropped the rehash intro "Load an agent. Give it full
  autonomy inside..." that restated Section 07's loop
- Dropped the cd step from the install block (shown in Section 07)
- Dropped comment lines inside the code block
- Dropped "Your host stays untouched" closing h2 (repeats the hero)
- Title is now one word: "Install."
- Sec-label changed to "08 · Jack in" to avoid duplicating the title
  and to bookend the hero's Operator narrative
- CTA links point to real URLs (tailrocks.com/ and GitHub)

Terminal chrome:
- Removed "operator@construct" label from all six terminal bars
  (one in hero code panel, five in Section 07 loop)

Footer (new):
- Added big Inter 900 "jackin'" wordmark at clamp(120px, 24vw, 300px)
  with green accent apostrophe — modern full-width branding end-mark
- Small mono meta row above: GitHub · Docs · Apache 2.0

Page total: 8 sections (was 10). ~250 lines lighter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: fix Install copy — accurate count + bookend the hero

The previous intro "Three lines, then load" was misleading: load IS the
third line, not a follow-up. Swapped to "Tap, install, load — you're
in." — three verbs that map 1:1 to the three install block lines, with
"you're in" echoing the hero tagline's "inside."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: add spec for landing page implementation

Captures the design decisions from the landing-v2.html mockup and
specifies the Vocs/React integration plan. Option 3 (landing at /,
docs URLs unchanged) — replaces the current HomePage.Root in
docs/pages/index.mdx with a set of React components under
layout: landing frontmatter.

Spec covers palette, typography trio (Inter + JetBrains Mono +
Fraunces), section-by-section structure, interaction patterns
(tabs, scroll-driven rail+detail, composition machine), a
component inventory mapping the mockup to ~14 React components,
and technical constraints (Vocs 1.4+, React 19, Tailwind v4, Bun).

The mockup at docs/superpowers/mockups/landing-v2.html remains
the source of truth for pixel-level details; this spec is the
design intent + integration plan for implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan: landing page implementation plan — 18 tasks across 5 phases

Task-by-task plan for converting the landing-v2.html mockup into Vocs
React components under docs/components/landing/. Covers:

- Scaffold + design tokens (tasks 1-2)
- Simple static sections first to establish patterns (tasks 3-5):
  InstallBlock, WordmarkFooter, PillCards
- Shared TabbedBuilder + ApproachCards + CastRoster (tasks 6-8)
- CompositionMachine with state + FocusCallout (tasks 9-10)
- Scroll-driven VocabularyDictionary (task 11)
- DailyLoop with 5 frames (task 12)
- Hero stack: rainEngine (TDD with unit tests), DigitalRain, CodePanel,
  HeroStage + HeroContent (tasks 13-16)
- Responsive + accessibility pass (task 17)
- Build verification + visual regression (task 18)

Each task: exact file paths, step-by-step instructions with code, TDD
for pure logic (rain engine), dev-server visual verification for React
components, commit at end.

Mockup at docs/superpowers/mockups/landing-v2.html remains the source
of truth for pixel-level details; plan references it for CSS copying.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* landing: scaffold Landing component and mount under layout: landing

* landing: add design tokens, base CSS, font imports

* landing: add InstallBlock (Section 08 · Jack in)

* landing: add WordmarkFooter

* landing: add PillCards (Section 03)

* landing: add TabbedBuilder component

* landing: add ApproachCards (Section 04) with TabbedBuilder

* landing: add CastRoster (Section 05)

* landing: add CompositionMachine (Section 06)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* landing: add FocusCallout inside Section 06

* landing: add VocabularyDictionary (Section 02) scroll-driven

* landing: add DailyLoop (Section 07)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* landing: add rainEngine (ported from src/tui.rs) with unit tests

* landing: add DigitalRain React component wrapping rainEngine

* landing: add CodePanel with tabbed typing animations

* landing: add HeroStage + HeroContent, assemble full page

* landing: remove duplicate .landing-btn-primary rule from hero block

* landing: a11y polish (buttons + focus styles), rAF cleanup, reduced-motion CSS

* landing: inset focus outline on voc items (avoid overflow-hidden clip)

* landing: final a11y polish — unnest hero section, guard window in jumpTo

* landing: fix CSS delivery + layout chrome

- Move global CSS from docs/pages/_root.css (Vocs ignores) to
  docs/styles.css (the file Vocs actually loads via virtual:styles).
  Old _root.css deleted.
- Reorder @import before non-@import rules so they aren't invalidated.
- Set showTopNav/showLogo/showSidebar/showOutline/showAiCta to false
  in index.mdx frontmatter — Vocs defaults these to true regardless
  of layout.
- Override max-width on .vocs_DocsLayout_content when data-layout is
  landing so the page renders full-bleed.

* landing: also zero .vocs_Content max-width + padding for full-bleed

The Vocs <article class="vocs_Content"> wrapper sits inside
.vocs_DocsLayout_content and has its own max-width cap plus
horizontal padding, so overriding DocsLayout_content alone still
left the landing boxed. Extend the override to vocs_Content too.

* landing: load Google Fonts at runtime + fix Docs CTA href

- Runtime font injection in Landing.tsx. The CSS @import url() for
  Google Fonts was stripped by Tailwind v4's bundler, so neither
  Inter (beyond fallback) nor Fraunces was actually reaching the
  browser. Injecting the link tags from useEffect guarantees the
  fonts actually load (Inter 400-900, JetBrains Mono 400-600,
  Fraunces 400-700).
- Dead @import url() removed from styles.css (it was silently being
  dropped by the bundler anyway).
- Read the Docs CTA in InstallBlock now uses /getting-started/why
  relative link instead of pointing to its own hostname.

* landing: hide Vocs footer on landing + WordmarkFooter flex layout

* landing: fix Vocs cascade + shell width + relative links

* landing: align OK columns in terminal output blocks

* landing: denied-state code inherits font + exile column alignment

* landing: exile frame — one more space before OK for agent lines

* docs: apply dark Matrix theme + shared font loader

- Add theme block in vocs.config.ts with dark colorScheme,
  #00ff41 accentColor, and Inter / JetBrains Mono font families.
  Overrides vocs color tokens (background, text, border, heading)
  to match the landing palette (--landing-bg = #0a0b0a, etc).
- Move runtime font-link injection from Landing.tsx to a shared
  docs/layout.tsx consumer component, so fonts load on every page
  rather than only the landing.
- Landing.tsx no longer needs to inject fonts (picked up by layout).

* docs: deeper chrome theming — mono nav + green hairline sidebar headers

* docs: neutral inline-code chip + terminal-like code block bg

* docs: bump Vocs fontWeight tokens (300->400 regular, 500->600 semibold)

* docs: code blocks as framed terminal cards

* docs: scope inline-code bg + reset Ask-in-ChatGPT button styling

* docs: switch to Tempo-inspired neutral look for docs chrome

* docs: adopt Tempo's Radix color ramps + semantic tokens

* docs: Tempo 1:1 — single light-dark() rule, no theme config

Matches tempoxyz/docs approach as closely as Vocs 1.4.1 allows:

- Remove the 'theme' key from vocs.config.ts entirely (Tempo has
  none). All Vocs chrome colors are mapped to Radix gray tokens
  via CSS in docs-theme.css using the light-dark() function, so a
  single declaration covers both modes.
- Mirror Tempo's src/pages/_root.css structure in docs/styles.css:
  @import 'tailwindcss' important, @source './', the @Utility
  scrollbar-* blocks, [data-v-logo] img sizing, and the
  @custom-variant dark selector (broadened to match both Vocs's
  .dark class AND the inline color-scheme style Tempo uses).
- Bridge Vocs's .dark class toggle to CSS color-scheme so the
  light-dark() tokens resolve correctly without injecting a script.
- Use :root, :root.dark selectors for the Vocs overrides so they
  match Vocs's internal specificity (Vocs uses :root.dark in dark
  mode — plain :root loses the cascade battle).

Monochrome accent (black-on-light, white-on-dark) matches Tempo's
branding choice. Landing page is untouched — scoped by .landing-root
and data-layout='landing'.

* docs: default-to-dark init script + more visible theme toggle

* docs: Tempo-style outline indicator + Ask AI label

* docs: fix outline double-border + match Tempo indicator behavior 1:1

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 20, 2026
…v-vars

feat: pre-launch hooks and interactive runtime env vars
donbeave added a commit that referenced this pull request Apr 20, 2026
* design: add landing page mockup for jackin.tailrocks.com

Full-page mockup with editorial split hero and seven sections:
Vocabulary, Problem, Mental Model (org × agent-class × workspace
composition machine with mounts), How It Works, Security,
Ecosystem + Install.

Hero rain is a faithful port of src/tui.rs digital_rain — ASCII
char pool, age-based color gradient, 35ms frame rate. Standalone
HTML with inlined CSS/JS, no build step required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: landing page iteration — full-screen rain hero + scroll-driven glossary

Hero:
- Full-screen (min-height: 100vh) with flex-centered content
- Nav moved inside hero-stage so rain falls from Y=0 through the nav
- Nav dropped its border + nav-links; Star button now CTA-only (no count)
- Trim pass: removed meta-row, pill band, sub-stats, footer, and
  duplicate brew-install buttons; tightened hero to headline + deck + CTA

Rain:
- Faithful port of src/tui.rs digital_rain — ASCII char pool, age-based
  color gradient (WHITE/pale/PHOSPHOR_GREEN/mid/PHOSPHOR_DIM/PHOSPHOR_DARK),
  per-column speed/fade, mutation probabilities, 35ms frame rate
- 32% canvas opacity + radial vignette + bottom-fade so text stays legible

Section 2 (Vocabulary) — full redesign:
- Replaced franchise-mapping table with scroll-driven dictionary
- Sticky rail (300px) + Fraunces-serif detail panel
- Nine entries now include Agent class + Workspace with explicit relationships
  (Agent class "built on top of the Construct"; Workspace is "a named list
  of mounts and access rules"; Jacking in is "Loading an agent into a workspace")
- Section is 500vh tall; scroll progress drives which entry is active;
  rail items are also click-to-jump with smooth scrollTo
- Introduces Fraunces serif alongside Inter + JetBrains Mono

Section 4 (Composition Machine):
- Extended to three dimensions: organization × agent-class × workspace
- Three orgs preloaded (jackin-project, chainargos, acme) with realistic
  agent/workspace configs; workspaces show per-mount rw/ro tags
- Cross-path mounts render src → dst; same-path mounts render once
- Allowed-agents rejection renders a red "not loaded" state

Section 7 (Ecosystem):
- Repo names follow the jackin- prefix convention
  (jackin-project/jackin-agent-smith, your-org/jackin-your-agent)
- Install block shows the cd step before jackin load; copy button removed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: red pill/blue pill, cloning section, 5-agent cast

Section 03 — "The false choice" reframed as red pill / blue pill:
- Two-tone CSS pill capsules (colored + white halves with gloss highlight)
- Blue pill = "Babysit every prompt" (gated, productivity destroyed)
- Red pill = "Full YOLO on host" (unfiltered, risk maximum)
- Transition: "Refuse the pill. You're the Operator — define the
  construct instead." — operator = Morpheus, above the choice

Section 04 — Kitchen-Sink vs Role-Specific callout:
- Two-card insight block below the composition machine's axis note
- Left: neutral "Kitchen-Sink Agent" (Every toolchain/plugin/convention)
- Right: green-accented "Role-Specific Agent" (Only relevant tools/plugins)
- Argues the *why* of narrow agent classes without adding a new section

Section 06 — New: "Parallel work" (clone the agent, split the work):
- Three clone cards: agent-smith #1/#2/#3 on different branches
  (feature/auth-redesign, feature/payment-v2, fix/api-timeout)
- Each card: pulsing live dot, branch name, container/DinD/history/network
- Four scenarios: parallel features, service fan-out, divide and conquer,
  experiment safely
- Old Section 06 (Security) renumbered to 07; old 07 (Ecosystem) to 08

Section 08 — Ecosystem rewritten with 5-agent franchise cast:
- agent-smith (built-in, default)
- the-architect (built-in, Rust dev)
- agent-jones (archetype: company specialist — scentbird/jackin-agent-jones)
- agent-brown (archetype: role-specific frontend)
- your-own (build-your-own template)
- Section title: "Every org builds a cast"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: real agent-class structure + redesigned Cast section

Section 04 (The Approach):
- Route 01 is now prose + toolset chips (no Dockerfile), describing
  the-architect concretely: what it adds on top of the construct
- Route 02 uses tabbed builder (manifest / Dockerfile) showing both
  files that make up a real jackin agent class
- Dockerfile patterns now match jackin conventions: FROM projectjackin
  (not jackin-project), mise install … && mise use --global, and the
  USER root / apt-get / USER claude pattern for system packages
- Dropped fictional features (password-manager ENV, EXPOSE port) since
  they're not part of jackin today
- Each route now shows CLI + REPO mapping (e.g. jackin load the-architect
  vs github.com/jackin-project/jackin-the-architect)

Section 05 (Cast) — full redesign:
- Reduced from 6 cards to 3 character cards + 1 full-width invite strip
- Dropped the Architect (covered in Section 04 Route 01) and DB
  maintainer (no franchise moniker) — zero duplication with Section 04
- Cards lead with the Fraunces-serif character name, not the role
  identifier, making Smith/Jones/Brown the visual anchors
- Removed placeholder repo paths — they were fake noise
- "Create your own role" is now a distinctly-styled full-width strip
  below the character trio, with a real link to the
  developing/creating-agents docs
- Intro updated: "Smith, Jones, Brown — archetypes to adopt.
  Every other role, yours to cast."

Section 02 (Vocabulary):
- Construct image reference fixed to projectjackin/construct:trixie
  (matches docs/developing/construct-image.mdx)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: tighten Section 06 Mental Model copy

- Intro shortened from ~60 words to 15: "Same agent in different
  workspaces. Same workspace with different agents. Pick both —
  see what runs."
- Dropped the axis-note block entirely (it was re-teaching Section
  02's dictionary definitions)
- Simplified machine sublabels: "the tool profile" / "workdir + mounts"
  / "the resulting container" (down from 2-3 words each)
- Kitchen-Sink/Role-Specific callout notes compressed to single
  lines: "Too much context — worse decisions." and "Focused context
  — better results, faster."
- Section is now ~60% shorter on copy with the interactive machine
  unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: daily-loop redesign, drop Security, simplify Install, add wordmark footer

Section 06 (Mental Model):
- acme → your-org across composition machine data and tabs

Section 07 (How it Works):
- Redesigned from vertical filmstrip into "The daily loop" with five
  vertical frames, each using a 2-column layout (info left, full terminal
  right with chrome dots)
- Added new "clone" frame between load and hardline that demonstrates
  parallel agents with cd setup and two load cycles on different paths
- Each frame: Fraunces-serif command name, italic mythos line
  (Jacking in, More of me, The hardline, Pulling out, Casting out),
  description paragraph, full terminal with colored output
- Frame 01 load now shows the cd ~/Projects/my-app setup step

Section 08 (Parallel work — clone cards + scenarios):
- Deleted entirely. The clone story is now Entry 02 of the daily loop.
  Old CSS (.clones, .clone-card, .scenarios, .scenario) also removed.

Section 08 (Security Model — honest version):
- Deleted. The section is documented elsewhere; on the landing it was
  slowing down the path to install.

Section 08 (Install — was 09/10):
- Simplified: dropped the rehash intro "Load an agent. Give it full
  autonomy inside..." that restated Section 07's loop
- Dropped the cd step from the install block (shown in Section 07)
- Dropped comment lines inside the code block
- Dropped "Your host stays untouched" closing h2 (repeats the hero)
- Title is now one word: "Install."
- Sec-label changed to "08 · Jack in" to avoid duplicating the title
  and to bookend the hero's Operator narrative
- CTA links point to real URLs (tailrocks.com/ and GitHub)

Terminal chrome:
- Removed "operator@construct" label from all six terminal bars
  (one in hero code panel, five in Section 07 loop)

Footer (new):
- Added big Inter 900 "jackin'" wordmark at clamp(120px, 24vw, 300px)
  with green accent apostrophe — modern full-width branding end-mark
- Small mono meta row above: GitHub · Docs · Apache 2.0

Page total: 8 sections (was 10). ~250 lines lighter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: fix Install copy — accurate count + bookend the hero

The previous intro "Three lines, then load" was misleading: load IS the
third line, not a follow-up. Swapped to "Tap, install, load — you're
in." — three verbs that map 1:1 to the three install block lines, with
"you're in" echoing the hero tagline's "inside."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: add spec for landing page implementation

Captures the design decisions from the landing-v2.html mockup and
specifies the Vocs/React integration plan. Option 3 (landing at /,
docs URLs unchanged) — replaces the current HomePage.Root in
docs/pages/index.mdx with a set of React components under
layout: landing frontmatter.

Spec covers palette, typography trio (Inter + JetBrains Mono +
Fraunces), section-by-section structure, interaction patterns
(tabs, scroll-driven rail+detail, composition machine), a
component inventory mapping the mockup to ~14 React components,
and technical constraints (Vocs 1.4+, React 19, Tailwind v4, Bun).

The mockup at docs/superpowers/mockups/landing-v2.html remains
the source of truth for pixel-level details; this spec is the
design intent + integration plan for implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan: landing page implementation plan — 18 tasks across 5 phases

Task-by-task plan for converting the landing-v2.html mockup into Vocs
React components under docs/components/landing/. Covers:

- Scaffold + design tokens (tasks 1-2)
- Simple static sections first to establish patterns (tasks 3-5):
  InstallBlock, WordmarkFooter, PillCards
- Shared TabbedBuilder + ApproachCards + CastRoster (tasks 6-8)
- CompositionMachine with state + FocusCallout (tasks 9-10)
- Scroll-driven VocabularyDictionary (task 11)
- DailyLoop with 5 frames (task 12)
- Hero stack: rainEngine (TDD with unit tests), DigitalRain, CodePanel,
  HeroStage + HeroContent (tasks 13-16)
- Responsive + accessibility pass (task 17)
- Build verification + visual regression (task 18)

Each task: exact file paths, step-by-step instructions with code, TDD
for pure logic (rain engine), dev-server visual verification for React
components, commit at end.

Mockup at docs/superpowers/mockups/landing-v2.html remains the source
of truth for pixel-level details; plan references it for CSS copying.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* landing: scaffold Landing component and mount under layout: landing

* landing: add design tokens, base CSS, font imports

* landing: add InstallBlock (Section 08 · Jack in)

* landing: add WordmarkFooter

* landing: add PillCards (Section 03)

* landing: add TabbedBuilder component

* landing: add ApproachCards (Section 04) with TabbedBuilder

* landing: add CastRoster (Section 05)

* landing: add CompositionMachine (Section 06)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* landing: add FocusCallout inside Section 06

* landing: add VocabularyDictionary (Section 02) scroll-driven

* landing: add DailyLoop (Section 07)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* landing: add rainEngine (ported from src/tui.rs) with unit tests

* landing: add DigitalRain React component wrapping rainEngine

* landing: add CodePanel with tabbed typing animations

* landing: add HeroStage + HeroContent, assemble full page

* landing: remove duplicate .landing-btn-primary rule from hero block

* landing: a11y polish (buttons + focus styles), rAF cleanup, reduced-motion CSS

* landing: inset focus outline on voc items (avoid overflow-hidden clip)

* landing: final a11y polish — unnest hero section, guard window in jumpTo

* landing: fix CSS delivery + layout chrome

- Move global CSS from docs/pages/_root.css (Vocs ignores) to
  docs/styles.css (the file Vocs actually loads via virtual:styles).
  Old _root.css deleted.
- Reorder @import before non-@import rules so they aren't invalidated.
- Set showTopNav/showLogo/showSidebar/showOutline/showAiCta to false
  in index.mdx frontmatter — Vocs defaults these to true regardless
  of layout.
- Override max-width on .vocs_DocsLayout_content when data-layout is
  landing so the page renders full-bleed.

* landing: also zero .vocs_Content max-width + padding for full-bleed

The Vocs <article class="vocs_Content"> wrapper sits inside
.vocs_DocsLayout_content and has its own max-width cap plus
horizontal padding, so overriding DocsLayout_content alone still
left the landing boxed. Extend the override to vocs_Content too.

* landing: load Google Fonts at runtime + fix Docs CTA href

- Runtime font injection in Landing.tsx. The CSS @import url() for
  Google Fonts was stripped by Tailwind v4's bundler, so neither
  Inter (beyond fallback) nor Fraunces was actually reaching the
  browser. Injecting the link tags from useEffect guarantees the
  fonts actually load (Inter 400-900, JetBrains Mono 400-600,
  Fraunces 400-700).
- Dead @import url() removed from styles.css (it was silently being
  dropped by the bundler anyway).
- Read the Docs CTA in InstallBlock now uses /getting-started/why
  relative link instead of pointing to its own hostname.

* landing: hide Vocs footer on landing + WordmarkFooter flex layout

* landing: fix Vocs cascade + shell width + relative links

* landing: align OK columns in terminal output blocks

* landing: denied-state code inherits font + exile column alignment

* landing: exile frame — one more space before OK for agent lines

* docs: apply dark franchise theme + shared font loader

- Add theme block in vocs.config.ts with dark colorScheme,
  #00ff41 accentColor, and Inter / JetBrains Mono font families.
  Overrides vocs color tokens (background, text, border, heading)
  to match the landing palette (--landing-bg = #0a0b0a, etc).
- Move runtime font-link injection from Landing.tsx to a shared
  docs/layout.tsx consumer component, so fonts load on every page
  rather than only the landing.
- Landing.tsx no longer needs to inject fonts (picked up by layout).

* docs: deeper chrome theming — mono nav + green hairline sidebar headers

* docs: neutral inline-code chip + terminal-like code block bg

* docs: bump Vocs fontWeight tokens (300->400 regular, 500->600 semibold)

* docs: code blocks as framed terminal cards

* docs: scope inline-code bg + reset Ask-in-ChatGPT button styling

* docs: switch to Tempo-inspired neutral look for docs chrome

* docs: adopt Tempo's Radix color ramps + semantic tokens

* docs: Tempo 1:1 — single light-dark() rule, no theme config

Matches tempoxyz/docs approach as closely as Vocs 1.4.1 allows:

- Remove the 'theme' key from vocs.config.ts entirely (Tempo has
  none). All Vocs chrome colors are mapped to Radix gray tokens
  via CSS in docs-theme.css using the light-dark() function, so a
  single declaration covers both modes.
- Mirror Tempo's src/pages/_root.css structure in docs/styles.css:
  @import 'tailwindcss' important, @source './', the @Utility
  scrollbar-* blocks, [data-v-logo] img sizing, and the
  @custom-variant dark selector (broadened to match both Vocs's
  .dark class AND the inline color-scheme style Tempo uses).
- Bridge Vocs's .dark class toggle to CSS color-scheme so the
  light-dark() tokens resolve correctly without injecting a script.
- Use :root, :root.dark selectors for the Vocs overrides so they
  match Vocs's internal specificity (Vocs uses :root.dark in dark
  mode — plain :root loses the cascade battle).

Monochrome accent (black-on-light, white-on-dark) matches Tempo's
branding choice. Landing page is untouched — scoped by .landing-root
and data-layout='landing'.

* docs: default-to-dark init script + more visible theme toggle

* docs: Tempo-style outline indicator + Ask AI label

* docs: fix outline double-border + match Tempo indicator behavior 1:1

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 21, 2026
…v-vars

feat: pre-launch hooks and interactive runtime env vars
donbeave added a commit that referenced this pull request Apr 21, 2026
* design: add landing page mockup for jackin.tailrocks.com

Full-page mockup with editorial split hero and seven sections:
Vocabulary, Problem, Mental Model (org × agent-class × workspace
composition machine with mounts), How It Works, Security,
Ecosystem + Install.

Hero rain is a faithful port of src/tui.rs digital_rain — ASCII
char pool, age-based color gradient, 35ms frame rate. Standalone
HTML with inlined CSS/JS, no build step required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: landing page iteration — full-screen rain hero + scroll-driven glossary

Hero:
- Full-screen (min-height: 100vh) with flex-centered content
- Nav moved inside hero-stage so rain falls from Y=0 through the nav
- Nav dropped its border + nav-links; Star button now CTA-only (no count)
- Trim pass: removed meta-row, pill band, sub-stats, footer, and
  duplicate brew-install buttons; tightened hero to headline + deck + CTA

Rain:
- Faithful port of src/tui.rs digital_rain — ASCII char pool, age-based
  color gradient (WHITE/pale/PHOSPHOR_GREEN/mid/PHOSPHOR_DIM/PHOSPHOR_DARK),
  per-column speed/fade, mutation probabilities, 35ms frame rate
- 32% canvas opacity + radial vignette + bottom-fade so text stays legible

Section 2 (Vocabulary) — full redesign:
- Replaced franchise-mapping table with scroll-driven dictionary
- Sticky rail (300px) + Fraunces-serif detail panel
- Nine entries now include Agent class + Workspace with explicit relationships
  (Agent class "built on top of the Construct"; Workspace is "a named list
  of mounts and access rules"; Jacking in is "Loading an agent into a workspace")
- Section is 500vh tall; scroll progress drives which entry is active;
  rail items are also click-to-jump with smooth scrollTo
- Introduces Fraunces serif alongside Inter + JetBrains Mono

Section 4 (Composition Machine):
- Extended to three dimensions: organization × agent-class × workspace
- Three orgs preloaded (jackin-project, chainargos, acme) with realistic
  agent/workspace configs; workspaces show per-mount rw/ro tags
- Cross-path mounts render src → dst; same-path mounts render once
- Allowed-agents rejection renders a red "not loaded" state

Section 7 (Ecosystem):
- Repo names follow the jackin- prefix convention
  (jackin-project/jackin-agent-smith, your-org/jackin-your-agent)
- Install block shows the cd step before jackin load; copy button removed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: red pill/blue pill, cloning section, 5-agent cast

Section 03 — "The false choice" reframed as red pill / blue pill:
- Two-tone CSS pill capsules (colored + white halves with gloss highlight)
- Blue pill = "Babysit every prompt" (gated, productivity destroyed)
- Red pill = "Full YOLO on host" (unfiltered, risk maximum)
- Transition: "Refuse the pill. You're the Operator — define the
  construct instead." — operator = Morpheus, above the choice

Section 04 — Kitchen-Sink vs Role-Specific callout:
- Two-card insight block below the composition machine's axis note
- Left: neutral "Kitchen-Sink Agent" (Every toolchain/plugin/convention)
- Right: green-accented "Role-Specific Agent" (Only relevant tools/plugins)
- Argues the *why* of narrow agent classes without adding a new section

Section 06 — New: "Parallel work" (clone the agent, split the work):
- Three clone cards: agent-smith #1/#2/#3 on different branches
  (feature/auth-redesign, feature/payment-v2, fix/api-timeout)
- Each card: pulsing live dot, branch name, container/DinD/history/network
- Four scenarios: parallel features, service fan-out, divide and conquer,
  experiment safely
- Old Section 06 (Security) renumbered to 07; old 07 (Ecosystem) to 08

Section 08 — Ecosystem rewritten with 5-agent franchise cast:
- agent-smith (built-in, default)
- the-architect (built-in, Rust dev)
- agent-jones (archetype: company specialist — scentbird/jackin-agent-jones)
- agent-brown (archetype: role-specific frontend)
- your-own (build-your-own template)
- Section title: "Every org builds a cast"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: real agent-class structure + redesigned Cast section

Section 04 (The Approach):
- Route 01 is now prose + toolset chips (no Dockerfile), describing
  the-architect concretely: what it adds on top of the construct
- Route 02 uses tabbed builder (manifest / Dockerfile) showing both
  files that make up a real jackin agent class
- Dockerfile patterns now match jackin conventions: FROM projectjackin
  (not jackin-project), mise install … && mise use --global, and the
  USER root / apt-get / USER claude pattern for system packages
- Dropped fictional features (password-manager ENV, EXPOSE port) since
  they're not part of jackin today
- Each route now shows CLI + REPO mapping (e.g. jackin load the-architect
  vs github.com/jackin-project/jackin-the-architect)

Section 05 (Cast) — full redesign:
- Reduced from 6 cards to 3 character cards + 1 full-width invite strip
- Dropped the Architect (covered in Section 04 Route 01) and DB
  maintainer (no franchise moniker) — zero duplication with Section 04
- Cards lead with the Fraunces-serif character name, not the role
  identifier, making Smith/Jones/Brown the visual anchors
- Removed placeholder repo paths — they were fake noise
- "Create your own role" is now a distinctly-styled full-width strip
  below the character trio, with a real link to the
  developing/creating-agents docs
- Intro updated: "Smith, Jones, Brown — archetypes to adopt.
  Every other role, yours to cast."

Section 02 (Vocabulary):
- Construct image reference fixed to projectjackin/construct:trixie
  (matches docs/developing/construct-image.mdx)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: tighten Section 06 Mental Model copy

- Intro shortened from ~60 words to 15: "Same agent in different
  workspaces. Same workspace with different agents. Pick both —
  see what runs."
- Dropped the axis-note block entirely (it was re-teaching Section
  02's dictionary definitions)
- Simplified machine sublabels: "the tool profile" / "workdir + mounts"
  / "the resulting container" (down from 2-3 words each)
- Kitchen-Sink/Role-Specific callout notes compressed to single
  lines: "Too much context — worse decisions." and "Focused context
  — better results, faster."
- Section is now ~60% shorter on copy with the interactive machine
  unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: daily-loop redesign, drop Security, simplify Install, add wordmark footer

Section 06 (Mental Model):
- acme → your-org across composition machine data and tabs

Section 07 (How it Works):
- Redesigned from vertical filmstrip into "The daily loop" with five
  vertical frames, each using a 2-column layout (info left, full terminal
  right with chrome dots)
- Added new "clone" frame between load and hardline that demonstrates
  parallel agents with cd setup and two load cycles on different paths
- Each frame: Fraunces-serif command name, italic mythos line
  (Jacking in, More of me, The hardline, Pulling out, Casting out),
  description paragraph, full terminal with colored output
- Frame 01 load now shows the cd ~/Projects/my-app setup step

Section 08 (Parallel work — clone cards + scenarios):
- Deleted entirely. The clone story is now Entry 02 of the daily loop.
  Old CSS (.clones, .clone-card, .scenarios, .scenario) also removed.

Section 08 (Security Model — honest version):
- Deleted. The section is documented elsewhere; on the landing it was
  slowing down the path to install.

Section 08 (Install — was 09/10):
- Simplified: dropped the rehash intro "Load an agent. Give it full
  autonomy inside..." that restated Section 07's loop
- Dropped the cd step from the install block (shown in Section 07)
- Dropped comment lines inside the code block
- Dropped "Your host stays untouched" closing h2 (repeats the hero)
- Title is now one word: "Install."
- Sec-label changed to "08 · Jack in" to avoid duplicating the title
  and to bookend the hero's Operator narrative
- CTA links point to real URLs (tailrocks.com/ and GitHub)

Terminal chrome:
- Removed "operator@construct" label from all six terminal bars
  (one in hero code panel, five in Section 07 loop)

Footer (new):
- Added big Inter 900 "jackin'" wordmark at clamp(120px, 24vw, 300px)
  with green accent apostrophe — modern full-width branding end-mark
- Small mono meta row above: GitHub · Docs · Apache 2.0

Page total: 8 sections (was 10). ~250 lines lighter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: fix Install copy — accurate count + bookend the hero

The previous intro "Three lines, then load" was misleading: load IS the
third line, not a follow-up. Swapped to "Tap, install, load — you're
in." — three verbs that map 1:1 to the three install block lines, with
"you're in" echoing the hero tagline's "inside."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: add spec for landing page implementation

Captures the design decisions from the landing-v2.html mockup and
specifies the Vocs/React integration plan. Option 3 (landing at /,
docs URLs unchanged) — replaces the current HomePage.Root in
docs/pages/index.mdx with a set of React components under
layout: landing frontmatter.

Spec covers palette, typography trio (Inter + JetBrains Mono +
Fraunces), section-by-section structure, interaction patterns
(tabs, scroll-driven rail+detail, composition machine), a
component inventory mapping the mockup to ~14 React components,
and technical constraints (Vocs 1.4+, React 19, Tailwind v4, Bun).

The mockup at docs/superpowers/mockups/landing-v2.html remains
the source of truth for pixel-level details; this spec is the
design intent + integration plan for implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan: landing page implementation plan — 18 tasks across 5 phases

Task-by-task plan for converting the landing-v2.html mockup into Vocs
React components under docs/components/landing/. Covers:

- Scaffold + design tokens (tasks 1-2)
- Simple static sections first to establish patterns (tasks 3-5):
  InstallBlock, WordmarkFooter, PillCards
- Shared TabbedBuilder + ApproachCards + CastRoster (tasks 6-8)
- CompositionMachine with state + FocusCallout (tasks 9-10)
- Scroll-driven VocabularyDictionary (task 11)
- DailyLoop with 5 frames (task 12)
- Hero stack: rainEngine (TDD with unit tests), DigitalRain, CodePanel,
  HeroStage + HeroContent (tasks 13-16)
- Responsive + accessibility pass (task 17)
- Build verification + visual regression (task 18)

Each task: exact file paths, step-by-step instructions with code, TDD
for pure logic (rain engine), dev-server visual verification for React
components, commit at end.

Mockup at docs/superpowers/mockups/landing-v2.html remains the source
of truth for pixel-level details; plan references it for CSS copying.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* landing: scaffold Landing component and mount under layout: landing

* landing: add design tokens, base CSS, font imports

* landing: add InstallBlock (Section 08 · Jack in)

* landing: add WordmarkFooter

* landing: add PillCards (Section 03)

* landing: add TabbedBuilder component

* landing: add ApproachCards (Section 04) with TabbedBuilder

* landing: add CastRoster (Section 05)

* landing: add CompositionMachine (Section 06)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* landing: add FocusCallout inside Section 06

* landing: add VocabularyDictionary (Section 02) scroll-driven

* landing: add DailyLoop (Section 07)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* landing: add rainEngine (ported from src/tui.rs) with unit tests

* landing: add DigitalRain React component wrapping rainEngine

* landing: add CodePanel with tabbed typing animations

* landing: add HeroStage + HeroContent, assemble full page

* landing: remove duplicate .landing-btn-primary rule from hero block

* landing: a11y polish (buttons + focus styles), rAF cleanup, reduced-motion CSS

* landing: inset focus outline on voc items (avoid overflow-hidden clip)

* landing: final a11y polish — unnest hero section, guard window in jumpTo

* landing: fix CSS delivery + layout chrome

- Move global CSS from docs/pages/_root.css (Vocs ignores) to
  docs/styles.css (the file Vocs actually loads via virtual:styles).
  Old _root.css deleted.
- Reorder @import before non-@import rules so they aren't invalidated.
- Set showTopNav/showLogo/showSidebar/showOutline/showAiCta to false
  in index.mdx frontmatter — Vocs defaults these to true regardless
  of layout.
- Override max-width on .vocs_DocsLayout_content when data-layout is
  landing so the page renders full-bleed.

* landing: also zero .vocs_Content max-width + padding for full-bleed

The Vocs <article class="vocs_Content"> wrapper sits inside
.vocs_DocsLayout_content and has its own max-width cap plus
horizontal padding, so overriding DocsLayout_content alone still
left the landing boxed. Extend the override to vocs_Content too.

* landing: load Google Fonts at runtime + fix Docs CTA href

- Runtime font injection in Landing.tsx. The CSS @import url() for
  Google Fonts was stripped by Tailwind v4's bundler, so neither
  Inter (beyond fallback) nor Fraunces was actually reaching the
  browser. Injecting the link tags from useEffect guarantees the
  fonts actually load (Inter 400-900, JetBrains Mono 400-600,
  Fraunces 400-700).
- Dead @import url() removed from styles.css (it was silently being
  dropped by the bundler anyway).
- Read the Docs CTA in InstallBlock now uses /getting-started/why
  relative link instead of pointing to its own hostname.

* landing: hide Vocs footer on landing + WordmarkFooter flex layout

* landing: fix Vocs cascade + shell width + relative links

* landing: align OK columns in terminal output blocks

* landing: denied-state code inherits font + exile column alignment

* landing: exile frame — one more space before OK for agent lines

* docs: apply dark franchise theme + shared font loader

- Add theme block in vocs.config.ts with dark colorScheme,
  #00ff41 accentColor, and Inter / JetBrains Mono font families.
  Overrides vocs color tokens (background, text, border, heading)
  to match the landing palette (--landing-bg = #0a0b0a, etc).
- Move runtime font-link injection from Landing.tsx to a shared
  docs/layout.tsx consumer component, so fonts load on every page
  rather than only the landing.
- Landing.tsx no longer needs to inject fonts (picked up by layout).

* docs: deeper chrome theming — mono nav + green hairline sidebar headers

* docs: neutral inline-code chip + terminal-like code block bg

* docs: bump Vocs fontWeight tokens (300->400 regular, 500->600 semibold)

* docs: code blocks as framed terminal cards

* docs: scope inline-code bg + reset Ask-in-ChatGPT button styling

* docs: switch to Tempo-inspired neutral look for docs chrome

* docs: adopt Tempo's Radix color ramps + semantic tokens

* docs: Tempo 1:1 — single light-dark() rule, no theme config

Matches tempoxyz/docs approach as closely as Vocs 1.4.1 allows:

- Remove the 'theme' key from vocs.config.ts entirely (Tempo has
  none). All Vocs chrome colors are mapped to Radix gray tokens
  via CSS in docs-theme.css using the light-dark() function, so a
  single declaration covers both modes.
- Mirror Tempo's src/pages/_root.css structure in docs/styles.css:
  @import 'tailwindcss' important, @source './', the @Utility
  scrollbar-* blocks, [data-v-logo] img sizing, and the
  @custom-variant dark selector (broadened to match both Vocs's
  .dark class AND the inline color-scheme style Tempo uses).
- Bridge Vocs's .dark class toggle to CSS color-scheme so the
  light-dark() tokens resolve correctly without injecting a script.
- Use :root, :root.dark selectors for the Vocs overrides so they
  match Vocs's internal specificity (Vocs uses :root.dark in dark
  mode — plain :root loses the cascade battle).

Monochrome accent (black-on-light, white-on-dark) matches Tempo's
branding choice. Landing page is untouched — scoped by .landing-root
and data-layout='landing'.

* docs: default-to-dark init script + more visible theme toggle

* docs: Tempo-style outline indicator + Ask AI label

* docs: fix outline double-border + match Tempo indicator behavior 1:1

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 21, 2026
feat: pre-launch hooks and interactive runtime env vars
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
* 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
feat: pre-launch hooks and interactive runtime env vars
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
* design: add landing page mockup for jackin.tailrocks.com

Full-page mockup with editorial split hero and seven sections:
Vocabulary, Problem, Mental Model (org × agent-class × workspace
composition machine with mounts), How It Works, Security,
Ecosystem + Install.

Hero rain is a faithful port of src/tui.rs digital_rain — ASCII
char pool, age-based color gradient, 35ms frame rate. Standalone
HTML with inlined CSS/JS, no build step required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: landing page iteration — full-screen rain hero + scroll-driven glossary

Hero:
- Full-screen (min-height: 100vh) with flex-centered content
- Nav moved inside hero-stage so rain falls from Y=0 through the nav
- Nav dropped its border + nav-links; Star button now CTA-only (no count)
- Trim pass: removed meta-row, pill band, sub-stats, footer, and
  duplicate brew-install buttons; tightened hero to headline + deck + CTA

Rain:
- Faithful port of src/tui.rs digital_rain — ASCII char pool, age-based
  color gradient (WHITE/pale/PHOSPHOR_GREEN/mid/PHOSPHOR_DIM/PHOSPHOR_DARK),
  per-column speed/fade, mutation probabilities, 35ms frame rate
- 32% canvas opacity + radial vignette + bottom-fade so text stays legible

Section 2 (Vocabulary) — full redesign:
- Replaced product-mapping table with scroll-driven dictionary
- Sticky rail (300px) + Fraunces-serif detail panel
- Nine entries now include Agent class + Workspace with explicit relationships
  (Agent class "built on top of the Construct"; Workspace is "a named list
  of mounts and access rules"; Jacking in is "Loading an agent into a workspace")
- Section is 500vh tall; scroll progress drives which entry is active;
  rail items are also click-to-jump with smooth scrollTo
- Introduces Fraunces serif alongside Inter + JetBrains Mono

Section 4 (Composition Machine):
- Extended to three dimensions: organization × agent-class × workspace
- Three orgs preloaded (jackin-project, chainargos, acme) with realistic
  agent/workspace configs; workspaces show per-mount rw/ro tags
- Cross-path mounts render src → dst; same-path mounts render once
- Allowed-agents rejection renders a red "not loaded" state

Section 7 (Ecosystem):
- Repo names follow the jackin- prefix convention
  (jackin-project/jackin-agent-smith, your-org/jackin-your-agent)
- Install block shows the cd step before jackin load; copy button removed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: red pill/blue pill, cloning section, 5-agent cast

Section 03 — "The false choice" reframed as red pill / blue pill:
- Two-tone CSS pill capsules (colored + white halves with gloss highlight)
- Blue pill = "Babysit every prompt" (gated, productivity destroyed)
- Red pill = "Full YOLO on host" (unfiltered, risk maximum)
- Transition: "Refuse the pill. You're the Operator — define the
  construct instead." — operator = Morpheus, above the choice

Section 04 — Kitchen-Sink vs Role-Specific callout:
- Two-card insight block below the composition machine's axis note
- Left: neutral "Kitchen-Sink Agent" (Every toolchain/plugin/convention)
- Right: green-accented "Role-Specific Agent" (Only relevant tools/plugins)
- Argues the *why* of narrow agent classes without adding a new section

Section 06 — New: "Parallel work" (clone the agent, split the work):
- Three clone cards: agent-smith #1/#2/#3 on different branches
  (feature/auth-redesign, feature/payment-v2, fix/api-timeout)
- Each card: pulsing live dot, branch name, container/DinD/history/network
- Four scenarios: parallel features, service fan-out, divide and conquer,
  experiment safely
- Old Section 06 (Security) renumbered to 07; old 07 (Ecosystem) to 08

Section 08 — Ecosystem rewritten with 5-agent product cast:
- agent-smith (built-in, default)
- the-architect (built-in, Rust dev)
- agent-jones (archetype: company specialist — scentbird/jackin-agent-jones)
- agent-brown (archetype: role-specific frontend)
- your-own (build-your-own template)
- Section title: "Every org builds a cast"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: real agent-class structure + redesigned Cast section

Section 04 (The Approach):
- Route 01 is now prose + toolset chips (no Dockerfile), describing
  the-architect concretely: what it adds on top of the construct
- Route 02 uses tabbed builder (manifest / Dockerfile) showing both
  files that make up a real jackin agent class
- Dockerfile patterns now match jackin conventions: FROM projectjackin
  (not jackin-project), mise install … && mise use --global, and the
  USER root / apt-get / USER claude pattern for system packages
- Dropped fictional features (password-manager ENV, EXPOSE port) since
  they're not part of jackin today
- Each route now shows CLI + REPO mapping (e.g. jackin load the-architect
  vs github.com/jackin-project/jackin-the-architect)

Section 05 (Cast) — full redesign:
- Reduced from 6 cards to 3 character cards + 1 full-width invite strip
- Dropped the Architect (covered in Section 04 Route 01) and DB
  maintainer (no product moniker) — zero duplication with Section 04
- Cards lead with the Fraunces-serif character name, not the role
  identifier, making Smith/Jones/Brown the visual anchors
- Removed placeholder repo paths — they were fake noise
- "Create your own role" is now a distinctly-styled full-width strip
  below the character trio, with a real link to the
  developing/creating-agents docs
- Intro updated: "Smith, Jones, Brown — archetypes to adopt.
  Every other role, yours to cast."

Section 02 (Vocabulary):
- Construct image reference fixed to projectjackin/construct:trixie
  (matches docs/developing/construct-image.mdx)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: tighten Section 06 Mental Model copy

- Intro shortened from ~60 words to 15: "Same agent in different
  workspaces. Same workspace with different agents. Pick both —
  see what runs."
- Dropped the axis-note block entirely (it was re-teaching Section
  02's dictionary definitions)
- Simplified machine sublabels: "the tool profile" / "workdir + mounts"
  / "the resulting container" (down from 2-3 words each)
- Kitchen-Sink/Role-Specific callout notes compressed to single
  lines: "Too much context — worse decisions." and "Focused context
  — better results, faster."
- Section is now ~60% shorter on copy with the interactive machine
  unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: daily-loop redesign, drop Security, simplify Install, add wordmark footer

Section 06 (Mental Model):
- acme → your-org across composition machine data and tabs

Section 07 (How it Works):
- Redesigned from vertical filmstrip into "The daily loop" with five
  vertical frames, each using a 2-column layout (info left, full terminal
  right with chrome dots)
- Added new "clone" frame between load and hardline that demonstrates
  parallel agents with cd setup and two load cycles on different paths
- Each frame: Fraunces-serif command name, italic mythos line
  (Jacking in, More of me, The hardline, Pulling out, Casting out),
  description paragraph, full terminal with colored output
- Frame 01 load now shows the cd ~/Projects/my-app setup step

Section 08 (Parallel work — clone cards + scenarios):
- Deleted entirely. The clone story is now Entry 02 of the daily loop.
  Old CSS (.clones, .clone-card, .scenarios, .scenario) also removed.

Section 08 (Security Model — honest version):
- Deleted. The section is documented elsewhere; on the landing it was
  slowing down the path to install.

Section 08 (Install — was 09/10):
- Simplified: dropped the rehash intro "Load an agent. Give it full
  autonomy inside..." that restated Section 07's loop
- Dropped the cd step from the install block (shown in Section 07)
- Dropped comment lines inside the code block
- Dropped "Your host stays untouched" closing h2 (repeats the hero)
- Title is now one word: "Install."
- Sec-label changed to "08 · Jack in" to avoid duplicating the title
  and to bookend the hero's Operator narrative
- CTA links point to real URLs (tailrocks.com/ and GitHub)

Terminal chrome:
- Removed "operator@construct" label from all six terminal bars
  (one in hero code panel, five in Section 07 loop)

Footer (new):
- Added big Inter 900 "jackin'" wordmark at clamp(120px, 24vw, 300px)
  with green accent apostrophe — modern full-width branding end-mark
- Small mono meta row above: GitHub · Docs · Apache 2.0

Page total: 8 sections (was 10). ~250 lines lighter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: fix Install copy — accurate count + bookend the hero

The previous intro "Three lines, then load" was misleading: load IS the
third line, not a follow-up. Swapped to "Tap, install, load — you're
in." — three verbs that map 1:1 to the three install block lines, with
"you're in" echoing the hero tagline's "inside."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: add spec for landing page implementation

Captures the design decisions from the landing-v2.html mockup and
specifies the Vocs/React integration plan. Option 3 (landing at /,
docs URLs unchanged) — replaces the current HomePage.Root in
docs/pages/index.mdx with a set of React components under
layout: landing frontmatter.

Spec covers palette, typography trio (Inter + JetBrains Mono +
Fraunces), section-by-section structure, interaction patterns
(tabs, scroll-driven rail+detail, composition machine), a
component inventory mapping the mockup to ~14 React components,
and technical constraints (Vocs 1.4+, React 19, Tailwind v4, Bun).

The mockup at docs/superpowers/mockups/landing-v2.html remains
the source of truth for pixel-level details; this spec is the
design intent + integration plan for implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan: landing page implementation plan — 18 tasks across 5 phases

Task-by-task plan for converting the landing-v2.html mockup into Vocs
React components under docs/components/landing/. Covers:

- Scaffold + design tokens (tasks 1-2)
- Simple static sections first to establish patterns (tasks 3-5):
  InstallBlock, WordmarkFooter, PillCards
- Shared TabbedBuilder + ApproachCards + CastRoster (tasks 6-8)
- CompositionMachine with state + FocusCallout (tasks 9-10)
- Scroll-driven VocabularyDictionary (task 11)
- DailyLoop with 5 frames (task 12)
- Hero stack: rainEngine (TDD with unit tests), DigitalRain, CodePanel,
  HeroStage + HeroContent (tasks 13-16)
- Responsive + accessibility pass (task 17)
- Build verification + visual regression (task 18)

Each task: exact file paths, step-by-step instructions with code, TDD
for pure logic (rain engine), dev-server visual verification for React
components, commit at end.

Mockup at docs/superpowers/mockups/landing-v2.html remains the source
of truth for pixel-level details; plan references it for CSS copying.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* landing: scaffold Landing component and mount under layout: landing

* landing: add design tokens, base CSS, font imports

* landing: add InstallBlock (Section 08 · Jack in)

* landing: add WordmarkFooter

* landing: add PillCards (Section 03)

* landing: add TabbedBuilder component

* landing: add ApproachCards (Section 04) with TabbedBuilder

* landing: add CastRoster (Section 05)

* landing: add CompositionMachine (Section 06)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* landing: add FocusCallout inside Section 06

* landing: add VocabularyDictionary (Section 02) scroll-driven

* landing: add DailyLoop (Section 07)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* landing: add rainEngine (ported from src/tui.rs) with unit tests

* landing: add DigitalRain React component wrapping rainEngine

* landing: add CodePanel with tabbed typing animations

* landing: add HeroStage + HeroContent, assemble full page

* landing: remove duplicate .landing-btn-primary rule from hero block

* landing: a11y polish (buttons + focus styles), rAF cleanup, reduced-motion CSS

* landing: inset focus outline on voc items (avoid overflow-hidden clip)

* landing: final a11y polish — unnest hero section, guard window in jumpTo

* landing: fix CSS delivery + layout chrome

- Move global CSS from docs/pages/_root.css (Vocs ignores) to
  docs/styles.css (the file Vocs actually loads via virtual:styles).
  Old _root.css deleted.
- Reorder @import before non-@import rules so they aren't invalidated.
- Set showTopNav/showLogo/showSidebar/showOutline/showAiCta to false
  in index.mdx frontmatter — Vocs defaults these to true regardless
  of layout.
- Override max-width on .vocs_DocsLayout_content when data-layout is
  landing so the page renders full-bleed.

* landing: also zero .vocs_Content max-width + padding for full-bleed

The Vocs <article class="vocs_Content"> wrapper sits inside
.vocs_DocsLayout_content and has its own max-width cap plus
horizontal padding, so overriding DocsLayout_content alone still
left the landing boxed. Extend the override to vocs_Content too.

* landing: load Google Fonts at runtime + fix Docs CTA href

- Runtime font injection in Landing.tsx. The CSS @import url() for
  Google Fonts was stripped by Tailwind v4's bundler, so neither
  Inter (beyond fallback) nor Fraunces was actually reaching the
  browser. Injecting the link tags from useEffect guarantees the
  fonts actually load (Inter 400-900, JetBrains Mono 400-600,
  Fraunces 400-700).
- Dead @import url() removed from styles.css (it was silently being
  dropped by the bundler anyway).
- Read the Docs CTA in InstallBlock now uses /getting-started/why
  relative link instead of pointing to its own hostname.

* landing: hide Vocs footer on landing + WordmarkFooter flex layout

* landing: fix Vocs cascade + shell width + relative links

* landing: align OK columns in terminal output blocks

* landing: denied-state code inherits font + exile column alignment

* landing: exile frame — one more space before OK for agent lines

* docs: apply dark product theme + shared font loader

- Add theme block in vocs.config.ts with dark colorScheme,
  #00ff41 accentColor, and Inter / JetBrains Mono font families.
  Overrides vocs color tokens (background, text, border, heading)
  to match the landing palette (--landing-bg = #0a0b0a, etc).
- Move runtime font-link injection from Landing.tsx to a shared
  docs/layout.tsx consumer component, so fonts load on every page
  rather than only the landing.
- Landing.tsx no longer needs to inject fonts (picked up by layout).

* docs: deeper chrome theming — mono nav + green hairline sidebar headers

* docs: neutral inline-code chip + terminal-like code block bg

* docs: bump Vocs fontWeight tokens (300->400 regular, 500->600 semibold)

* docs: code blocks as framed terminal cards

* docs: scope inline-code bg + reset Ask-in-ChatGPT button styling

* docs: switch to Tempo-inspired neutral look for docs chrome

* docs: adopt Tempo's Radix color ramps + semantic tokens

* docs: Tempo 1:1 — single light-dark() rule, no theme config

Matches tempoxyz/docs approach as closely as Vocs 1.4.1 allows:

- Remove the 'theme' key from vocs.config.ts entirely (Tempo has
  none). All Vocs chrome colors are mapped to Radix gray tokens
  via CSS in docs-theme.css using the light-dark() function, so a
  single declaration covers both modes.
- Mirror Tempo's src/pages/_root.css structure in docs/styles.css:
  @import 'tailwindcss' important, @source './', the @Utility
  scrollbar-* blocks, [data-v-logo] img sizing, and the
  @custom-variant dark selector (broadened to match both Vocs's
  .dark class AND the inline color-scheme style Tempo uses).
- Bridge Vocs's .dark class toggle to CSS color-scheme so the
  light-dark() tokens resolve correctly without injecting a script.
- Use :root, :root.dark selectors for the Vocs overrides so they
  match Vocs's internal specificity (Vocs uses :root.dark in dark
  mode — plain :root loses the cascade battle).

Monochrome accent (black-on-light, white-on-dark) matches Tempo's
branding choice. Landing page is untouched — scoped by .landing-root
and data-layout='landing'.

* docs: default-to-dark init script + more visible theme toggle

* docs: Tempo-style outline indicator + Ask AI label

* docs: fix outline double-border + match Tempo indicator behavior 1:1

---------

Co-authored-by: Claude Opus 4.7 (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
* 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
feat: pre-launch hooks and interactive runtime env vars

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
* design: add landing page mockup for jackin.tailrocks.com

Full-page mockup with editorial split hero and seven sections:
Vocabulary, Problem, Mental Model (org × agent-class × workspace
composition machine with mounts), How It Works, Security,
Ecosystem + Install.

Hero rain is a faithful port of src/tui.rs digital_rain — ASCII
char pool, age-based color gradient, 35ms frame rate. Standalone
HTML with inlined CSS/JS, no build step required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: landing page iteration — full-screen rain hero + scroll-driven glossary

Hero:
- Full-screen (min-height: 100vh) with flex-centered content
- Nav moved inside hero-stage so rain falls from Y=0 through the nav
- Nav dropped its border + nav-links; Star button now CTA-only (no count)
- Trim pass: removed meta-row, pill band, sub-stats, footer, and
  duplicate brew-install buttons; tightened hero to headline + deck + CTA

Rain:
- Faithful port of src/tui.rs digital_rain — ASCII char pool, age-based
  color gradient (WHITE/pale/PHOSPHOR_GREEN/mid/PHOSPHOR_DIM/PHOSPHOR_DARK),
  per-column speed/fade, mutation probabilities, 35ms frame rate
- 32% canvas opacity + radial vignette + bottom-fade so text stays legible

Section 2 (Vocabulary) — full redesign:
- Replaced product-mapping table with scroll-driven dictionary
- Sticky rail (300px) + Fraunces-serif detail panel
- Nine entries now include Agent class + Workspace with explicit relationships
  (Agent class "built on top of the Construct"; Workspace is "a named list
  of mounts and access rules"; Jacking in is "Loading an agent into a workspace")
- Section is 500vh tall; scroll progress drives which entry is active;
  rail items are also click-to-jump with smooth scrollTo
- Introduces Fraunces serif alongside Inter + JetBrains Mono

Section 4 (Composition Machine):
- Extended to three dimensions: organization × agent-class × workspace
- Three orgs preloaded (jackin-project, chainargos, acme) with realistic
  agent/workspace configs; workspaces show per-mount rw/ro tags
- Cross-path mounts render src → dst; same-path mounts render once
- Allowed-agents rejection renders a red "not loaded" state

Section 7 (Ecosystem):
- Repo names follow the jackin- prefix convention
  (jackin-project/jackin-agent-smith, your-org/jackin-your-agent)
- Install block shows the cd step before jackin load; copy button removed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: red pill/blue pill, cloning section, 5-agent cast

Section 03 — "The false choice" reframed as red pill / blue pill:
- Two-tone CSS pill capsules (colored + white halves with gloss highlight)
- Blue pill = "Babysit every prompt" (gated, productivity destroyed)
- Red pill = "Full YOLO on host" (unfiltered, risk maximum)
- Transition: "Refuse the pill. You're the Operator — define the
  construct instead." — operator = Morpheus, above the choice

Section 04 — Kitchen-Sink vs Role-Specific callout:
- Two-card insight block below the composition machine's axis note
- Left: neutral "Kitchen-Sink Agent" (Every toolchain/plugin/convention)
- Right: green-accented "Role-Specific Agent" (Only relevant tools/plugins)
- Argues the *why* of narrow agent classes without adding a new section

Section 06 — New: "Parallel work" (clone the agent, split the work):
- Three clone cards: agent-smith #1/#2/#3 on different branches
  (feature/auth-redesign, feature/payment-v2, fix/api-timeout)
- Each card: pulsing live dot, branch name, container/DinD/history/network
- Four scenarios: parallel features, service fan-out, divide and conquer,
  experiment safely
- Old Section 06 (Security) renumbered to 07; old 07 (Ecosystem) to 08

Section 08 — Ecosystem rewritten with 5-agent product cast:
- agent-smith (built-in, default)
- the-architect (built-in, Rust dev)
- agent-jones (archetype: company specialist — scentbird/jackin-agent-jones)
- agent-brown (archetype: role-specific frontend)
- your-own (build-your-own template)
- Section title: "Every org builds a cast"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: real agent-class structure + redesigned Cast section

Section 04 (The Approach):
- Route 01 is now prose + toolset chips (no Dockerfile), describing
  the-architect concretely: what it adds on top of the construct
- Route 02 uses tabbed builder (manifest / Dockerfile) showing both
  files that make up a real jackin agent class
- Dockerfile patterns now match jackin conventions: FROM projectjackin
  (not jackin-project), mise install … && mise use --global, and the
  USER root / apt-get / USER claude pattern for system packages
- Dropped fictional features (password-manager ENV, EXPOSE port) since
  they're not part of jackin today
- Each route now shows CLI + REPO mapping (e.g. jackin load the-architect
  vs github.com/jackin-project/jackin-the-architect)

Section 05 (Cast) — full redesign:
- Reduced from 6 cards to 3 character cards + 1 full-width invite strip
- Dropped the Architect (covered in Section 04 Route 01) and DB
  maintainer (no product moniker) — zero duplication with Section 04
- Cards lead with the Fraunces-serif character name, not the role
  identifier, making Smith/Jones/Brown the visual anchors
- Removed placeholder repo paths — they were fake noise
- "Create your own role" is now a distinctly-styled full-width strip
  below the character trio, with a real link to the
  developing/creating-agents docs
- Intro updated: "Smith, Jones, Brown — archetypes to adopt.
  Every other role, yours to cast."

Section 02 (Vocabulary):
- Construct image reference fixed to projectjackin/construct:trixie
  (matches docs/developing/construct-image.mdx)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: tighten Section 06 Mental Model copy

- Intro shortened from ~60 words to 15: "Same agent in different
  workspaces. Same workspace with different agents. Pick both —
  see what runs."
- Dropped the axis-note block entirely (it was re-teaching Section
  02's dictionary definitions)
- Simplified machine sublabels: "the tool profile" / "workdir + mounts"
  / "the resulting container" (down from 2-3 words each)
- Kitchen-Sink/Role-Specific callout notes compressed to single
  lines: "Too much context — worse decisions." and "Focused context
  — better results, faster."
- Section is now ~60% shorter on copy with the interactive machine
  unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: daily-loop redesign, drop Security, simplify Install, add wordmark footer

Section 06 (Mental Model):
- acme → your-org across composition machine data and tabs

Section 07 (How it Works):
- Redesigned from vertical filmstrip into "The daily loop" with five
  vertical frames, each using a 2-column layout (info left, full terminal
  right with chrome dots)
- Added new "clone" frame between load and hardline that demonstrates
  parallel agents with cd setup and two load cycles on different paths
- Each frame: Fraunces-serif command name, italic mythos line
  (Jacking in, More of me, The hardline, Pulling out, Casting out),
  description paragraph, full terminal with colored output
- Frame 01 load now shows the cd ~/Projects/my-app setup step

Section 08 (Parallel work — clone cards + scenarios):
- Deleted entirely. The clone story is now Entry 02 of the daily loop.
  Old CSS (.clones, .clone-card, .scenarios, .scenario) also removed.

Section 08 (Security Model — honest version):
- Deleted. The section is documented elsewhere; on the landing it was
  slowing down the path to install.

Section 08 (Install — was 09/10):
- Simplified: dropped the rehash intro "Load an agent. Give it full
  autonomy inside..." that restated Section 07's loop
- Dropped the cd step from the install block (shown in Section 07)
- Dropped comment lines inside the code block
- Dropped "Your host stays untouched" closing h2 (repeats the hero)
- Title is now one word: "Install."
- Sec-label changed to "08 · Jack in" to avoid duplicating the title
  and to bookend the hero's Operator narrative
- CTA links point to real URLs (tailrocks.com/ and GitHub)

Terminal chrome:
- Removed "operator@construct" label from all six terminal bars
  (one in hero code panel, five in Section 07 loop)

Footer (new):
- Added big Inter 900 "jackin'" wordmark at clamp(120px, 24vw, 300px)
  with green accent apostrophe — modern full-width branding end-mark
- Small mono meta row above: GitHub · Docs · Apache 2.0

Page total: 8 sections (was 10). ~250 lines lighter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: fix Install copy — accurate count + bookend the hero

The previous intro "Three lines, then load" was misleading: load IS the
third line, not a follow-up. Swapped to "Tap, install, load — you're
in." — three verbs that map 1:1 to the three install block lines, with
"you're in" echoing the hero tagline's "inside."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* design: add spec for landing page implementation

Captures the design decisions from the landing-v2.html mockup and
specifies the Vocs/React integration plan. Option 3 (landing at /,
docs URLs unchanged) — replaces the current HomePage.Root in
docs/pages/index.mdx with a set of React components under
layout: landing frontmatter.

Spec covers palette, typography trio (Inter + JetBrains Mono +
Fraunces), section-by-section structure, interaction patterns
(tabs, scroll-driven rail+detail, composition machine), a
component inventory mapping the mockup to ~14 React components,
and technical constraints (Vocs 1.4+, React 19, Tailwind v4, Bun).

The mockup at docs/superpowers/mockups/landing-v2.html remains
the source of truth for pixel-level details; this spec is the
design intent + integration plan for implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan: landing page implementation plan — 18 tasks across 5 phases

Task-by-task plan for converting the landing-v2.html mockup into Vocs
React components under docs/components/landing/. Covers:

- Scaffold + design tokens (tasks 1-2)
- Simple static sections first to establish patterns (tasks 3-5):
  InstallBlock, WordmarkFooter, PillCards
- Shared TabbedBuilder + ApproachCards + CastRoster (tasks 6-8)
- CompositionMachine with state + FocusCallout (tasks 9-10)
- Scroll-driven VocabularyDictionary (task 11)
- DailyLoop with 5 frames (task 12)
- Hero stack: rainEngine (TDD with unit tests), DigitalRain, CodePanel,
  HeroStage + HeroContent (tasks 13-16)
- Responsive + accessibility pass (task 17)
- Build verification + visual regression (task 18)

Each task: exact file paths, step-by-step instructions with code, TDD
for pure logic (rain engine), dev-server visual verification for React
components, commit at end.

Mockup at docs/superpowers/mockups/landing-v2.html remains the source
of truth for pixel-level details; plan references it for CSS copying.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* landing: scaffold Landing component and mount under layout: landing

* landing: add design tokens, base CSS, font imports

* landing: add InstallBlock (Section 08 · Jack in)

* landing: add WordmarkFooter

* landing: add PillCards (Section 03)

* landing: add TabbedBuilder component

* landing: add ApproachCards (Section 04) with TabbedBuilder

* landing: add CastRoster (Section 05)

* landing: add CompositionMachine (Section 06)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* landing: add FocusCallout inside Section 06

* landing: add VocabularyDictionary (Section 02) scroll-driven

* landing: add DailyLoop (Section 07)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* landing: add rainEngine (ported from src/tui.rs) with unit tests

* landing: add DigitalRain React component wrapping rainEngine

* landing: add CodePanel with tabbed typing animations

* landing: add HeroStage + HeroContent, assemble full page

* landing: remove duplicate .landing-btn-primary rule from hero block

* landing: a11y polish (buttons + focus styles), rAF cleanup, reduced-motion CSS

* landing: inset focus outline on voc items (avoid overflow-hidden clip)

* landing: final a11y polish — unnest hero section, guard window in jumpTo

* landing: fix CSS delivery + layout chrome

- Move global CSS from docs/pages/_root.css (Vocs ignores) to
  docs/styles.css (the file Vocs actually loads via virtual:styles).
  Old _root.css deleted.
- Reorder @import before non-@import rules so they aren't invalidated.
- Set showTopNav/showLogo/showSidebar/showOutline/showAiCta to false
  in index.mdx frontmatter — Vocs defaults these to true regardless
  of layout.
- Override max-width on .vocs_DocsLayout_content when data-layout is
  landing so the page renders full-bleed.

* landing: also zero .vocs_Content max-width + padding for full-bleed

The Vocs <article class="vocs_Content"> wrapper sits inside
.vocs_DocsLayout_content and has its own max-width cap plus
horizontal padding, so overriding DocsLayout_content alone still
left the landing boxed. Extend the override to vocs_Content too.

* landing: load Google Fonts at runtime + fix Docs CTA href

- Runtime font injection in Landing.tsx. The CSS @import url() for
  Google Fonts was stripped by Tailwind v4's bundler, so neither
  Inter (beyond fallback) nor Fraunces was actually reaching the
  browser. Injecting the link tags from useEffect guarantees the
  fonts actually load (Inter 400-900, JetBrains Mono 400-600,
  Fraunces 400-700).
- Dead @import url() removed from styles.css (it was silently being
  dropped by the bundler anyway).
- Read the Docs CTA in InstallBlock now uses /getting-started/why
  relative link instead of pointing to its own hostname.

* landing: hide Vocs footer on landing + WordmarkFooter flex layout

* landing: fix Vocs cascade + shell width + relative links

* landing: align OK columns in terminal output blocks

* landing: denied-state code inherits font + exile column alignment

* landing: exile frame — one more space before OK for agent lines

* docs: apply dark product theme + shared font loader

- Add theme block in vocs.config.ts with dark colorScheme,
  #00ff41 accentColor, and Inter / JetBrains Mono font families.
  Overrides vocs color tokens (background, text, border, heading)
  to match the landing palette (--landing-bg = #0a0b0a, etc).
- Move runtime font-link injection from Landing.tsx to a shared
  docs/layout.tsx consumer component, so fonts load on every page
  rather than only the landing.
- Landing.tsx no longer needs to inject fonts (picked up by layout).

* docs: deeper chrome theming — mono nav + green hairline sidebar headers

* docs: neutral inline-code chip + terminal-like code block bg

* docs: bump Vocs fontWeight tokens (300->400 regular, 500->600 semibold)

* docs: code blocks as framed terminal cards

* docs: scope inline-code bg + reset Ask-in-ChatGPT button styling

* docs: switch to Tempo-inspired neutral look for docs chrome

* docs: adopt Tempo's Radix color ramps + semantic tokens

* docs: Tempo 1:1 — single light-dark() rule, no theme config

Matches tempoxyz/docs approach as closely as Vocs 1.4.1 allows:

- Remove the 'theme' key from vocs.config.ts entirely (Tempo has
  none). All Vocs chrome colors are mapped to Radix gray tokens
  via CSS in docs-theme.css using the light-dark() function, so a
  single declaration covers both modes.
- Mirror Tempo's src/pages/_root.css structure in docs/styles.css:
  @import 'tailwindcss' important, @source './', the @Utility
  scrollbar-* blocks, [data-v-logo] img sizing, and the
  @custom-variant dark selector (broadened to match both Vocs's
  .dark class AND the inline color-scheme style Tempo uses).
- Bridge Vocs's .dark class toggle to CSS color-scheme so the
  light-dark() tokens resolve correctly without injecting a script.
- Use :root, :root.dark selectors for the Vocs overrides so they
  match Vocs's internal specificity (Vocs uses :root.dark in dark
  mode — plain :root loses the cascade battle).

Monochrome accent (black-on-light, white-on-dark) matches Tempo's
branding choice. Landing page is untouched — scoped by .landing-root
and data-layout='landing'.

* docs: default-to-dark init script + more visible theme toggle

* docs: Tempo-style outline indicator + Ask AI label

* docs: fix outline double-border + match Tempo indicator behavior 1:1

---------

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.


* 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
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.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>
Co-authored-by: Amp <amp@ampcode.com>
donbeave added a commit that referenced this pull request May 7, 2026
…ort (#1)

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.com>
donbeave added a commit that referenced this pull request May 7, 2026
…ion (#1)

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.com>
donbeave added a commit that referenced this pull request May 7, 2026
…epo (#1)

Extracts validate_relative_path() helper, uses it to validate pre-launch
hook paths (relative, inside repo, non-empty file), and calls manifest.validate()
for env var declaration checks during agent repo validation.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.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>
Co-authored-by: Amp <amp@ampcode.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>
Co-authored-by: Amp <amp@ampcode.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>
Co-authored-by: Amp <amp@ampcode.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>
Co-authored-by: Amp <amp@ampcode.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>
Co-authored-by: Amp <amp@ampcode.com>
donbeave added a commit that referenced this pull request May 7, 2026
…#1)

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.com>
donbeave added a commit that referenced this pull request May 7, 2026
Amp-Thread-ID: https://ampcode.com/threads/T-019d6365-46bb-77ef-a9ac-4438ec6c8710

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.com>
donbeave added a commit that referenced this pull request May 7, 2026
Amp-Thread-ID: https://ampcode.com/threads/T-019d6365-46bb-77ef-a9ac-4438ec6c8710

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.com>
donbeave added a commit that referenced this pull request May 7, 2026
Amp-Thread-ID: https://ampcode.com/threads/T-019d6365-46bb-77ef-a9ac-4438ec6c8710

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.com>
donbeave added a commit that referenced this pull request May 7, 2026
Amp-Thread-ID: https://ampcode.com/threads/T-019d63f5-9ee7-752d-96ae-5f6ec3cacab0

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.com>
donbeave added a commit that referenced this pull request May 7, 2026
feat: pre-launch hooks and interactive runtime env vars

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
* design: add landing page mockup for jackin.tailrocks.com

Full-page mockup with editorial split hero and seven sections:
Vocabulary, Problem, Mental Model (org × agent-class × workspace
composition machine with mounts), How It Works, Security,
Ecosystem + Install.

Hero rain is a faithful port of src/tui.rs digital_rain — ASCII
char pool, age-based color gradient, 35ms frame rate. Standalone
HTML with inlined CSS/JS, no build step required.


* design: landing page iteration — full-screen rain hero + scroll-driven glossary

Hero:
- Full-screen (min-height: 100vh) with flex-centered content
- Nav moved inside hero-stage so rain falls from Y=0 through the nav
- Nav dropped its border + nav-links; Star button now CTA-only (no count)
- Trim pass: removed meta-row, pill band, sub-stats, footer, and
  duplicate brew-install buttons; tightened hero to headline + deck + CTA

Rain:
- Faithful port of src/tui.rs digital_rain — ASCII char pool, age-based
  color gradient (WHITE/pale/PHOSPHOR_GREEN/mid/PHOSPHOR_DIM/PHOSPHOR_DARK),
  per-column speed/fade, mutation probabilities, 35ms frame rate
- 32% canvas opacity + radial vignette + bottom-fade so text stays legible

Section 2 (Vocabulary) — full redesign:
- Replaced product-mapping table with scroll-driven dictionary
- Sticky rail (300px) + Fraunces-serif detail panel
- Nine entries now include Agent class + Workspace with explicit relationships
  (Agent class "built on top of the Construct"; Workspace is "a named list
  of mounts and access rules"; Jacking in is "Loading an agent into a workspace")
- Section is 500vh tall; scroll progress drives which entry is active;
  rail items are also click-to-jump with smooth scrollTo
- Introduces Fraunces serif alongside Inter + JetBrains Mono

Section 4 (Composition Machine):
- Extended to three dimensions: organization × agent-class × workspace
- Three orgs preloaded (jackin-project, chainargos, acme) with realistic
  agent/workspace configs; workspaces show per-mount rw/ro tags
- Cross-path mounts render src → dst; same-path mounts render once
- Allowed-agents rejection renders a red "not loaded" state

Section 7 (Ecosystem):
- Repo names follow the jackin- prefix convention
  (jackin-project/jackin-agent-smith, your-org/jackin-your-agent)
- Install block shows the cd step before jackin load; copy button removed


* design: red pill/blue pill, cloning section, 5-agent cast

Section 03 — "The false choice" reframed as red pill / blue pill:
- Two-tone CSS pill capsules (colored + white halves with gloss highlight)
- Blue pill = "Babysit every prompt" (gated, productivity destroyed)
- Red pill = "Full YOLO on host" (unfiltered, risk maximum)
- Transition: "Refuse the pill. You're the Operator — define the
  construct instead." — operator = Morpheus, above the choice

Section 04 — Kitchen-Sink vs Role-Specific callout:
- Two-card insight block below the composition machine's axis note
- Left: neutral "Kitchen-Sink Agent" (Every toolchain/plugin/convention)
- Right: green-accented "Role-Specific Agent" (Only relevant tools/plugins)
- Argues the *why* of narrow agent classes without adding a new section

Section 06 — New: "Parallel work" (clone the agent, split the work):
- Three clone cards: agent-smith #1/#2/#3 on different branches
  (feature/auth-redesign, feature/payment-v2, fix/api-timeout)
- Each card: pulsing live dot, branch name, container/DinD/history/network
- Four scenarios: parallel features, service fan-out, divide and conquer,
  experiment safely
- Old Section 06 (Security) renumbered to 07; old 07 (Ecosystem) to 08

Section 08 — Ecosystem rewritten with 5-agent product cast:
- agent-smith (built-in, default)
- the-architect (built-in, Rust dev)
- agent-jones (archetype: company specialist — scentbird/jackin-agent-jones)
- agent-brown (archetype: role-specific frontend)
- your-own (build-your-own template)
- Section title: "Every org builds a cast"


* design: real agent-class structure + redesigned Cast section

Section 04 (The Approach):
- Route 01 is now prose + toolset chips (no Dockerfile), describing
  the-architect concretely: what it adds on top of the construct
- Route 02 uses tabbed builder (manifest / Dockerfile) showing both
  files that make up a real jackin agent class
- Dockerfile patterns now match jackin conventions: FROM projectjackin
  (not jackin-project), mise install … && mise use --global, and the
  USER root / apt-get / USER claude pattern for system packages
- Dropped fictional features (password-manager ENV, EXPOSE port) since
  they're not part of jackin today
- Each route now shows CLI + REPO mapping (e.g. jackin load the-architect
  vs github.com/jackin-project/jackin-the-architect)

Section 05 (Cast) — full redesign:
- Reduced from 6 cards to 3 character cards + 1 full-width invite strip
- Dropped the Architect (covered in Section 04 Route 01) and DB
  maintainer (no product moniker) — zero duplication with Section 04
- Cards lead with the Fraunces-serif character name, not the role
  identifier, making Smith/Jones/Brown the visual anchors
- Removed placeholder repo paths — they were fake noise
- "Create your own role" is now a distinctly-styled full-width strip
  below the character trio, with a real link to the
  developing/creating-agents docs
- Intro updated: "Smith, Jones, Brown — archetypes to adopt.
  Every other role, yours to cast."

Section 02 (Vocabulary):
- Construct image reference fixed to projectjackin/construct:trixie
  (matches docs/developing/construct-image.mdx)


* design: tighten Section 06 Mental Model copy

- Intro shortened from ~60 words to 15: "Same agent in different
  workspaces. Same workspace with different agents. Pick both —
  see what runs."
- Dropped the axis-note block entirely (it was re-teaching Section
  02's dictionary definitions)
- Simplified machine sublabels: "the tool profile" / "workdir + mounts"
  / "the resulting container" (down from 2-3 words each)
- Kitchen-Sink/Role-Specific callout notes compressed to single
  lines: "Too much context — worse decisions." and "Focused context
  — better results, faster."
- Section is now ~60% shorter on copy with the interactive machine
  unchanged


* design: daily-loop redesign, drop Security, simplify Install, add wordmark footer

Section 06 (Mental Model):
- acme → your-org across composition machine data and tabs

Section 07 (How it Works):
- Redesigned from vertical filmstrip into "The daily loop" with five
  vertical frames, each using a 2-column layout (info left, full terminal
  right with chrome dots)
- Added new "clone" frame between load and hardline that demonstrates
  parallel agents with cd setup and two load cycles on different paths
- Each frame: Fraunces-serif command name, italic mythos line
  (Jacking in, More of me, The hardline, Pulling out, Casting out),
  description paragraph, full terminal with colored output
- Frame 01 load now shows the cd ~/Projects/my-app setup step

Section 08 (Parallel work — clone cards + scenarios):
- Deleted entirely. The clone story is now Entry 02 of the daily loop.
  Old CSS (.clones, .clone-card, .scenarios, .scenario) also removed.

Section 08 (Security Model — honest version):
- Deleted. The section is documented elsewhere; on the landing it was
  slowing down the path to install.

Section 08 (Install — was 09/10):
- Simplified: dropped the rehash intro "Load an agent. Give it full
  autonomy inside..." that restated Section 07's loop
- Dropped the cd step from the install block (shown in Section 07)
- Dropped comment lines inside the code block
- Dropped "Your host stays untouched" closing h2 (repeats the hero)
- Title is now one word: "Install."
- Sec-label changed to "08 · Jack in" to avoid duplicating the title
  and to bookend the hero's Operator narrative
- CTA links point to real URLs (tailrocks.com/ and GitHub)

Terminal chrome:
- Removed "operator@construct" label from all six terminal bars
  (one in hero code panel, five in Section 07 loop)

Footer (new):
- Added big Inter 900 "jackin'" wordmark at clamp(120px, 24vw, 300px)
  with green accent apostrophe — modern full-width branding end-mark
- Small mono meta row above: GitHub · Docs · Apache 2.0

Page total: 8 sections (was 10). ~250 lines lighter.


* design: fix Install copy — accurate count + bookend the hero

The previous intro "Three lines, then load" was misleading: load IS the
third line, not a follow-up. Swapped to "Tap, install, load — you're
in." — three verbs that map 1:1 to the three install block lines, with
"you're in" echoing the hero tagline's "inside."


* design: add spec for landing page implementation

Captures the design decisions from the landing-v2.html mockup and
specifies the Vocs/React integration plan. Option 3 (landing at /,
docs URLs unchanged) — replaces the current HomePage.Root in
docs/pages/index.mdx with a set of React components under
layout: landing frontmatter.

Spec covers palette, typography trio (Inter + JetBrains Mono +
Fraunces), section-by-section structure, interaction patterns
(tabs, scroll-driven rail+detail, composition machine), a
component inventory mapping the mockup to ~14 React components,
and technical constraints (Vocs 1.4+, React 19, Tailwind v4, Bun).

The mockup at docs/superpowers/mockups/landing-v2.html remains
the source of truth for pixel-level details; this spec is the
design intent + integration plan for implementation.


* plan: landing page implementation plan — 18 tasks across 5 phases

Task-by-task plan for converting the landing-v2.html mockup into Vocs
React components under docs/components/landing/. Covers:

- Scaffold + design tokens (tasks 1-2)
- Simple static sections first to establish patterns (tasks 3-5):
  InstallBlock, WordmarkFooter, PillCards
- Shared TabbedBuilder + ApproachCards + CastRoster (tasks 6-8)
- CompositionMachine with state + FocusCallout (tasks 9-10)
- Scroll-driven VocabularyDictionary (task 11)
- DailyLoop with 5 frames (task 12)
- Hero stack: rainEngine (TDD with unit tests), DigitalRain, CodePanel,
  HeroStage + HeroContent (tasks 13-16)
- Responsive + accessibility pass (task 17)
- Build verification + visual regression (task 18)

Each task: exact file paths, step-by-step instructions with code, TDD
for pure logic (rain engine), dev-server visual verification for React
components, commit at end.

Mockup at docs/superpowers/mockups/landing-v2.html remains the source
of truth for pixel-level details; plan references it for CSS copying.


* landing: scaffold Landing component and mount under layout: landing

* landing: add design tokens, base CSS, font imports

* landing: add InstallBlock (Section 08 · Jack in)

* landing: add WordmarkFooter

* landing: add PillCards (Section 03)

* landing: add TabbedBuilder component

* landing: add ApproachCards (Section 04) with TabbedBuilder

* landing: add CastRoster (Section 05)

* landing: add CompositionMachine (Section 06)


* landing: add FocusCallout inside Section 06

* landing: add VocabularyDictionary (Section 02) scroll-driven

* landing: add DailyLoop (Section 07)


* landing: add rainEngine (ported from src/tui.rs) with unit tests

* landing: add DigitalRain React component wrapping rainEngine

* landing: add CodePanel with tabbed typing animations

* landing: add HeroStage + HeroContent, assemble full page

* landing: remove duplicate .landing-btn-primary rule from hero block

* landing: a11y polish (buttons + focus styles), rAF cleanup, reduced-motion CSS

* landing: inset focus outline on voc items (avoid overflow-hidden clip)

* landing: final a11y polish — unnest hero section, guard window in jumpTo

* landing: fix CSS delivery + layout chrome

- Move global CSS from docs/pages/_root.css (Vocs ignores) to
  docs/styles.css (the file Vocs actually loads via virtual:styles).
  Old _root.css deleted.
- Reorder @import before non-@import rules so they aren't invalidated.
- Set showTopNav/showLogo/showSidebar/showOutline/showAiCta to false
  in index.mdx frontmatter — Vocs defaults these to true regardless
  of layout.
- Override max-width on .vocs_DocsLayout_content when data-layout is
  landing so the page renders full-bleed.

* landing: also zero .vocs_Content max-width + padding for full-bleed

The Vocs <article class="vocs_Content"> wrapper sits inside
.vocs_DocsLayout_content and has its own max-width cap plus
horizontal padding, so overriding DocsLayout_content alone still
left the landing boxed. Extend the override to vocs_Content too.

* landing: load Google Fonts at runtime + fix Docs CTA href

- Runtime font injection in Landing.tsx. The CSS @import url() for
  Google Fonts was stripped by Tailwind v4's bundler, so neither
  Inter (beyond fallback) nor Fraunces was actually reaching the
  browser. Injecting the link tags from useEffect guarantees the
  fonts actually load (Inter 400-900, JetBrains Mono 400-600,
  Fraunces 400-700).
- Dead @import url() removed from styles.css (it was silently being
  dropped by the bundler anyway).
- Read the Docs CTA in InstallBlock now uses /getting-started/why
  relative link instead of pointing to its own hostname.

* landing: hide Vocs footer on landing + WordmarkFooter flex layout

* landing: fix Vocs cascade + shell width + relative links

* landing: align OK columns in terminal output blocks

* landing: denied-state code inherits font + exile column alignment

* landing: exile frame — one more space before OK for agent lines

* docs: apply dark product theme + shared font loader

- Add theme block in vocs.config.ts with dark colorScheme,
  #00ff41 accentColor, and Inter / JetBrains Mono font families.
  Overrides vocs color tokens (background, text, border, heading)
  to match the landing palette (--landing-bg = #0a0b0a, etc).
- Move runtime font-link injection from Landing.tsx to a shared
  docs/layout.tsx consumer component, so fonts load on every page
  rather than only the landing.
- Landing.tsx no longer needs to inject fonts (picked up by layout).

* docs: deeper chrome theming — mono nav + green hairline sidebar headers

* docs: neutral inline-code chip + terminal-like code block bg

* docs: bump Vocs fontWeight tokens (300->400 regular, 500->600 semibold)

* docs: code blocks as framed terminal cards

* docs: scope inline-code bg + reset Ask-in-ChatGPT button styling

* docs: switch to Tempo-inspired neutral look for docs chrome

* docs: adopt Tempo's Radix color ramps + semantic tokens

* docs: Tempo 1:1 — single light-dark() rule, no theme config

Matches tempoxyz/docs approach as closely as Vocs 1.4.1 allows:

- Remove the 'theme' key from vocs.config.ts entirely (Tempo has
  none). All Vocs chrome colors are mapped to Radix gray tokens
  via CSS in docs-theme.css using the light-dark() function, so a
  single declaration covers both modes.
- Mirror Tempo's src/pages/_root.css structure in docs/styles.css:
  @import 'tailwindcss' important, @source './', the @Utility
  scrollbar-* blocks, [data-v-logo] img sizing, and the
  @custom-variant dark selector (broadened to match both Vocs's
  .dark class AND the inline color-scheme style Tempo uses).
- Bridge Vocs's .dark class toggle to CSS color-scheme so the
  light-dark() tokens resolve correctly without injecting a script.
- Use :root, :root.dark selectors for the Vocs overrides so they
  match Vocs's internal specificity (Vocs uses :root.dark in dark
  mode — plain :root loses the cascade battle).

Monochrome accent (black-on-light, white-on-dark) matches Tempo's
branding choice. Landing page is untouched — scoped by .landing-root
and data-layout='landing'.

* docs: default-to-dark init script + more visible theme toggle

* docs: Tempo-style outline indicator + Ask AI label

* docs: fix outline double-border + match Tempo indicator behavior 1:1

---------

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 8, 2026
Add `docs/.../roadmap/workspace-claude-token-setup.mdx` as the design
record for `jackin workspace claude-token setup|rotate|revoke|doctor`
shipped in the parent commit. The doc captures:

- Problem statement (manual five-step copy-paste flow vs. token-mode
  recommendation).
- Background reversing the original "non-goal: wrap setup-token"
  stance from `2026-04-23-claude-token-auth-mode-design.md` once the
  three follow-on requirements (workspace-scoped tokens, multi-1P
  accounts, lifecycle / rotation) were in scope.
- Four design options + recommendation (Option 3: full guided
  pipeline; Option 4: keychain backend as a follow-up gated on the
  Credential Source Pattern roadmap).
- CLI surface, sample interactive transcript, config shape, launch
  diagnostic with expiry suffix.
- Security notes — PTY isolation, `secrecy::SecretString`, no argv
  for secret values, no temp files, SHA-256-prefix-only provenance
  metadata stamp.
- Implementation phases A-E with concrete touched files.
- Open questions (token-line parsing stability, concurrent setup
  flock, default `op_account` discovery, doctor verbosity, keychain
  follow-up timing, bulk migration).

Cross-link from parent `claude-auth-strategy.mdx` (Open Question #1
on token storage marked as addressed by this proposal). Sidebar entry
added under *Open items → Agent runtimes & authentication* per
docs/AGENTS.md → "Roadmap sidebar discipline".

Status block lists shipped vs. pending so reviewers can match the
doc against the code in the parent commit.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request May 8, 2026
Add `docs/.../roadmap/workspace-claude-token-setup.mdx` as the design
record for `jackin workspace claude-token setup|rotate|revoke|doctor`
shipped in the parent commit. The doc captures:

- Problem statement (manual five-step copy-paste flow vs. token-mode
  recommendation).
- Background reversing the original "non-goal: wrap setup-token"
  stance from `2026-04-23-claude-token-auth-mode-design.md` once the
  three follow-on requirements (workspace-scoped tokens, multi-1P
  accounts, lifecycle / rotation) were in scope.
- Four design options + recommendation (Option 3: full guided
  pipeline; Option 4: keychain backend as a follow-up gated on the
  Credential Source Pattern roadmap).
- CLI surface, sample interactive transcript, config shape, launch
  diagnostic with expiry suffix.
- Security notes — PTY isolation, `secrecy::SecretString`, no argv
  for secret values, no temp files, SHA-256-prefix-only provenance
  metadata stamp.
- Implementation phases A-E with concrete touched files.
- Open questions (token-line parsing stability, concurrent setup
  flock, default `op_account` discovery, doctor verbosity, keychain
  follow-up timing, bulk migration).

Cross-link from parent `claude-auth-strategy.mdx` (Open Question #1
on token storage marked as addressed by this proposal). Sidebar entry
added under *Open items → Agent runtimes & authentication* per
docs/AGENTS.md → "Roadmap sidebar discipline".

Status block lists shipped vs. pending so reviewers can match the
doc against the code in the parent commit.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request May 9, 2026
Add `docs/.../roadmap/workspace-claude-token-setup.mdx` as the design
record for `jackin workspace claude-token setup|rotate|revoke|doctor`
shipped in the parent commit. The doc captures:

- Problem statement (manual five-step copy-paste flow vs. token-mode
  recommendation).
- Background reversing the original "non-goal: wrap setup-token"
  stance from `2026-04-23-claude-token-auth-mode-design.md` once the
  three follow-on requirements (workspace-scoped tokens, multi-1P
  accounts, lifecycle / rotation) were in scope.
- Four design options + recommendation (Option 3: full guided
  pipeline; Option 4: keychain backend as a follow-up gated on the
  Credential Source Pattern roadmap).
- CLI surface, sample interactive transcript, config shape, launch
  diagnostic with expiry suffix.
- Security notes — PTY isolation, `secrecy::SecretString`, no argv
  for secret values, no temp files, SHA-256-prefix-only provenance
  metadata stamp.
- Implementation phases A-E with concrete touched files.
- Open questions (token-line parsing stability, concurrent setup
  flock, default `op_account` discovery, doctor verbosity, keychain
  follow-up timing, bulk migration).

Cross-link from parent `claude-auth-strategy.mdx` (Open Question #1
on token storage marked as addressed by this proposal). Sidebar entry
added under *Open items → Agent runtimes & authentication* per
docs/AGENTS.md → "Roadmap sidebar discipline".

Status block lists shipped vs. pending so reviewers can match the
doc against the code in the parent commit.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.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