Skip to content

feat: V0.1.47 improves#857

Merged
Henry-811 merged 4 commits intomainfrom
v0.1.47-improves
Feb 6, 2026
Merged

feat: V0.1.47 improves#857
Henry-811 merged 4 commits intomainfrom
v0.1.47-improves

Conversation

@ncrispino
Copy link
Collaborator

@ncrispino ncrispino commented Feb 6, 2026

PR Draft: v0.1.47 Release Fixes

Summary

This PR addresses multiple issues scheduled for the v0.1.47 release.

Closes MAS-264


MAS-272: Unify write_mode and two-tier workspace with in-worktree scratch

Closes MAS-272

Summary: Unified write_mode (worktree isolation) and use_two_tier_workspace (scratch/deliverable dirs) into a single system where every agent works inside a per-round git worktree with .massgen_scratch/ for experiments.

Key Changes

IsolationContextManager (massgen/filesystem_manager/_isolation_context_manager.py):

  • Added .massgen_scratch/ creation inside worktrees (git-excluded via info/exclude)
  • Added move_scratch_to_workspace() to archive scratch to .scratch_archive/ before teardown
  • Added cleanup_round() (remove worktree, keep branch) vs cleanup_session() (remove everything)
  • Added previous_branch support for one-branch-per-agent invariant
  • Branch names now use random suffixes (no agent IDs or round numbers)
  • Added setup_workspace_scratch() for no-context-paths case: git-inits workspace, creates branch + scratch
  • Fixed --git-common-dir relative path resolution for non-worktree repos

ChangeApplier (massgen/filesystem_manager/_change_applier.py):

  • Skip .massgen_scratch files in fallback _apply_file_changes()

Orchestrator (massgen/orchestrator.py):

  • Per-round worktree creation in _stream_agent_execution() with _agent_current_branches tracking
  • Fallback to workspace scratch when no context_paths are configured
  • Worktree paths passed to system message builder for agent prompt
  • Scratch archive + cleanup in finally block
  • Final presentation: scratch/branch info in presentation prompt, cleanup_session() at end

System Prompts (massgen/system_prompt_sections.py, massgen/system_message_builder.py):

  • WorkspaceStructureSection accepts worktree_paths param
  • When set, renders project checkout + scratch space docs instead of two-tier workspace

Deprecation (massgen/config_validator.py, massgen/agent_config.py, massgen/config_builder.py, 5 YAML configs):

  • use_two_tier_workspace deprecated with config validator warnings
  • Quickstart templates now use write_mode: auto

CLAUDE.md: Documented agent statelessness and anonymity principle

Test Plan

  • uv run pytest massgen/tests/test_write_mode_scratch.py -v (20 tests)
  • uv run pytest massgen/tests/test_isolation_context.py -v (39 tests)
  • uv run pytest massgen/tests/test_two_tier_workspace.py -v (15 tests)
  • uv run python scripts/validate_all_configs.py
  • Integration: uv run massgen --config @basic/multi/three_agents_default "Create hello.py with tests"

Documentation

  • New: docs/source/user_guide/agent_workspaces.rst
  • Updated: CLAUDE.md (agent statelessness principle)

MAS-269: Fix subagent timeout

Issue: Subagent spawning tools were timing out with the MCP client timeout (400s), even though subagents have their own internal timeout handling and can legitimately run longer.

Error observed:

20:59:50 | ERROR | Tool call timed out for spawn_subagents on subagent_agent_a after 400s

Fix: Added timeout exemption for subagent-related tools in the MCP client.

Changes

  • massgen/mcp_tools/client.py:
    • Added TIMEOUT_EXEMPT_TOOLS frozenset containing tools that manage their own timeouts:
      • spawn_subagents - Subagent spawning has its own timeout configuration
      • get_subagent_status - May block waiting for subagent completion
      • cancel_subagents - May need to wait for graceful shutdown
    • Modified call_tool() method to skip asyncio.wait_for() wrapper for exempt tools

Test Plan

  • Run MassGen with subagent spawning that takes >400s
  • Verify subagents complete successfully without MCP timeout errors
  • Verify non-exempt tools still respect the timeout

Closes MAS-269
Closes #852


Light Theme Visibility Fixes

Issue: Multiple UI elements were invisible or had poor contrast in light theme mode:

  • Mode bar toggle buttons (Normal, Multi-Agent, Refine) had invisible underlines
  • Separator lines above mode bar and below agent tabs were invisible
  • Toast notifications blended into the light background
  • Theme switching via /theme command didn't update all styled elements

Root Cause: The CSS used Textual's built-in $surface-lighten-1 and $surface-lighten-2 variables which compute to near-white colors in light mode, providing no contrast against the white background.

Changes

Palette files (_light.tcss, _dark.tcss):

  • Added semantic CSS variables for mode toggles:
    • $mode-toggle-border - underline color for inactive toggles
    • $mode-toggle-active-color - color for active toggle states
    • $mode-toggle-inactive-color - color for inactive toggle states
  • Added separator variables:
    • $separator-border - for horizontal divider lines
    • $tool-card-border - for tool card left borders
  • Added toast variables:
    • $toast-bg, $toast-border - theme-appropriate toast styling

Base CSS (base.tcss):

  • Updated ModeToggle styles to use semantic variables instead of $surface-lighten-2
  • Updated #input_header and AgentStatusRibbon to use $separator-border
  • Updated ToolCallCard.terminal-tool to use $tool-card-border
  • Added .theme-light and .theme-dark CSS class rules for dynamic theme switching
  • Fixed toast styling to use border-left: tall instead of Textual's default outer border

Python (textual_terminal_display.py):

  • Added theme class toggling (.theme-light/.theme-dark) on mount and theme switch
  • Enables CSS rules to update dynamically when user toggles theme via /theme

Test Plan

  • Launch TUI in light mode, verify mode bar underlines are visible
  • Verify separator lines above mode bar and below agent tabs are visible but subtle
  • Verify toast notifications have visible borders in light mode
  • Toggle theme with /theme command, verify all elements update correctly
  • Verify dark mode still looks correct (no regressions)

MAS-267: Quickstart Wizard Docker Setup

Issue: The TUI quickstart wizard lets users select "Docker Mode" but doesn't check if Docker is available or offer to pull the required images. Users end up with a config that references Docker but fails at runtime.

Fix: Integrated the existing DockerSetupStep from the setup wizard into the quickstart wizard flow, shown conditionally when Docker mode is selected. Added an in-step "Pull Selected Images" button with animated progress so users can pull images directly within the wizard.

Changes

massgen/frontend/displays/textual_widgets/quickstart_wizard.py:

  • Added docker_setup wizard step after execution_mode, using existing DockerSetupStep
  • Step is skipped when local mode is selected (skip_condition on execution_mode state)
  • Updated wizard flow docstring

massgen/frontend/displays/textual_widgets/setup_wizard.py (DockerSetupStep):

  • Added "Pull Selected Images" button with compact styling below image checkboxes
  • Added animated braille spinner (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏) on status label during pull
  • Streams docker pull stdout line-by-line so users see layer download progress in real-time
  • Uses asyncio.create_subprocess_exec to avoid blocking the TUI event loop
  • Added validate() that blocks advancing to next step while pull is in progress
  • Button shows "Retry Failed" on partial failure, "All images pulled" on success
  • Tightened CSS: compact button sizing, reduced whitespace between sections

Test Plan

  • Run uv run massgen (launches TUI with quickstart wizard)
  • Select Docker Mode in execution step → verify Docker Setup step appears
  • Select Local Mode → verify Docker Setup step is skipped
  • On Docker Setup step, click "Pull Selected Images" → verify animated spinner and stdout progress
  • Verify "Next" button is blocked while pull is in progress
  • Complete wizard with Docker mode → verify config is correct

Closes MAS-267


MAS-272: Worktree Isolation for File Writes

Issue: During final presentation, agents write directly to context paths, making changes immediately and irreversibly. Users have no opportunity to review or approve file changes before they're applied.

Fix: Added a full worktree isolation pipeline: agents write to a git worktree inside their workspace, a review modal shows the diff, and only approved changes are copied to the original context path.

Architecture

1. Orchestrator creates worktree at {workspace}/.worktree/ctx_N
2. Original context path removed from PPM (agent can't see it)
3. Presentation prompt tells agent to write to the worktree
4. After presentation: git diff worktree → review modal → ChangeApplier copies approved files back

Changes

New files:

  • massgen/infrastructure/worktree_manager.py - Git worktree create/remove/list/prune via GitPython
  • massgen/infrastructure/shadow_repo.py - Shadow repo for non-git directories
  • massgen/infrastructure/__init__.py - Exports WorktreeManager, ShadowRepo, is_git_repo
  • massgen/filesystem_manager/_isolation_context_manager.py - Manages isolated write contexts (worktree or shadow mode), handles diff/change detection
  • massgen/filesystem_manager/_change_applier.py - Applies approved changes from worktree back to original paths, with ReviewResult dataclass
  • massgen/frontend/displays/textual/widgets/modals/review_modal.py - Two-panel TUI modal: file list with click-to-toggle approval (✓/○) + syntax-highlighted diff viewer with green/red background tinting
  • massgen/utils/git_utils.py - Shared git utilities (get_git_root, get_changes, is_git_repo)
  • massgen/tests/test_isolation_context.py - 39 unit tests covering worktree, shadow, change detection, PPM integration
  • massgen/configs/debug/test_write_mode_isolation.yaml - Debug config for testing the isolation flow
  • scripts/test_review_modal.py - Standalone test harness for iterating on modal design with theme toggle

Modified files:

  • massgen/agent_config.py - Added write_mode field to CoordinationConfig
  • massgen/config_validator.py - Added VALID_WRITE_MODES validation
  • massgen/api_params_handler/_api_params_handler_base.py - Excluded write_mode from API passthrough
  • massgen/backend/base.py - Excluded write_mode from API passthrough
  • massgen/cli.py - Parse write_mode from coordination config in all code paths
  • massgen/orchestrator.py:
    • Worktree setup in get_final_presentation(): creates IsolationContextManager, removes original paths from PPM, adds worktree info to presentation prompt
    • Docker extra mounts for worktree paths
    • Review phase in _present_final_answer(): re-adds paths, calls _review_isolated_changes()
    • _review_isolated_changes(): collects changes/diff, shows TUI modal, applies via ChangeApplier
    • Swapped post-eval and review order so post-eval runs first, review modal is last step
    • Uses repo_root for correct path resolution when context is a git subdirectory
  • massgen/filesystem_manager/__init__.py - Exports IsolationContextManager, ChangeApplier, ReviewResult
  • massgen/filesystem_manager/_filesystem_manager.py - Added extra_mount_paths to Docker container recreation
  • massgen/filesystem_manager/_docker_manager.py - Mount extra paths in container
  • massgen/filesystem_manager/_path_permission_manager.py - Added remove_context_path() and re_add_context_path() for isolation
  • massgen/frontend/displays/textual_terminal_display.py:
    • Added show_change_review_modal() with thread-safe Future-based cross-thread communication
    • Fixed CSS combination to include MODAL_BASE_CSS (was missing, causing no borders in real app)
    • Added modal_base.py to CSS cache invalidation
  • massgen/frontend/displays/textual_themes/base.tcss - Full review modal CSS section with toggle indicators, diff panel, file list, instructions
  • massgen/frontend/displays/textual_themes/palettes/_dark.tcss - Added $diff-add-bg, $diff-remove-bg, $modal-* variables
  • massgen/frontend/displays/textual_themes/palettes/_light.tcss - Added matching light theme variables
  • pyproject.toml - Added gitpython dependency

YAML Config

orchestrator:
  context_paths:
    - path: "my/project"
      permission: "write"
  coordination:
    write_mode: auto  # auto | worktree | isolated | legacy

Test Plan

  • uv run pytest massgen/tests/test_isolation_context.py -v (39 tests)
  • uv run python scripts/test_review_modal.py - standalone modal test with theme toggle
  • uv run massgen --config @debug/test_write_mode_isolation "Create hello.py" - full end-to-end
  • Verify review modal shows with borders, diff colors, ✓/○ toggles
  • Approve → file copied to original path; Reject → no changes applied
  • Test with subdirectory context paths (not just git root)

Closes MAS-272


MAS-268: Disable post-evaluation restarts in quickstart defaults

Issue: TUI crashes on orchestration restart (shows attempt 2 banner then crashes).

Fix: Set max_orchestration_restarts: 0 in both Docker and local quickstart config templates in config_builder.py. Users who manually set max_orchestration_restarts in their YAML are unaffected.

Changes

  • massgen/config_builder.py - Changed max_orchestration_restarts from 2 to 0 in both Docker and local quickstart templates, with comment referencing MAS-268

Test Plan

  • Run quickstart wizard → verify generated config has max_orchestration_restarts: 0
  • Verify existing configs with explicit max_orchestration_restarts: 2 still work

Relates to MAS-268


Summary by CodeRabbit

Release Notes

  • New Features

    • Added write_mode configuration for workspace isolation with git worktree and shadow repository support
    • Introduced interactive review modal for approving or rejecting isolated workspace changes
    • Enhanced context path management with runtime permission controls and per-path operations
  • Configuration Changes

    • Deprecated use_two_tier_workspace in favor of write_mode setting
    • Extended voting sensitivity options for coordination strategies
  • Documentation

    • Added comprehensive Agent Workspaces and Code Isolation guide
    • Added Worktrees Module documentation with integration examples

@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive git worktree and shadow-repository isolation infrastructure for agent workspaces during multi-agent coordination. It adds a new write_mode configuration parameter, manages per-round isolated contexts with git worktrees or shadow repositories, provides an interactive change review modal UI, orchestrates cleanup and archiving, and integrates isolation-aware system messages and prompts throughout the stack.

Changes

Cohort / File(s) Summary
Git Utilities and Infrastructure
massgen/utils/git_utils.py, massgen/infrastructure/worktree_manager.py, massgen/infrastructure/shadow_repo.py, massgen/infrastructure/__init__.py
New git repository utilities (root, branch, status discovery) and git worktree/shadow repository management classes for isolated agent workspaces. Provides public exports via infrastructure module.
Isolation Context Management
massgen/filesystem_manager/_isolation_context_manager.py, massgen/filesystem_manager/_change_applier.py
Core IsolationContextManager orchestrating per-context isolation (worktree, shadow, or legacy modes) with scratch handling, branch lifecycle, and session cleanup. ChangeApplier applies approved changes from isolated contexts to original paths using git-based or file-diff fallback.
Filesystem Manager Integration
massgen/filesystem_manager/_filesystem_manager.py, massgen/filesystem_manager/_docker_manager.py, massgen/filesystem_manager/_path_permission_manager.py, massgen/filesystem_manager/__init__.py
Enhanced FilesystemManager to initialize and manage IsolationContextManager instances, extra docker mount paths, and cleanup. PathPermissionManager gains runtime context path management (remove, update, re-add). New public exports for ChangeApplier, IsolationContextManager, ReviewResult.
Configuration and Validation
massgen/agent_config.py, massgen/config_validator.py, massgen/config_builder.py, massgen/api_params_handler/_api_params_handler_base.py, massgen/backend/base.py, massgen/backend/codex.py
Added write_mode field to CoordinationConfig; deprecated use_two_tier_workspace with migration guidance. Config validator adds VALID_WRITE_MODES and validates against it. API params and backend exclude write_mode from external APIs. Removes Codex config mounting in docker mode.
CLI and Orchestration Wiring
massgen/cli.py, massgen/orchestrator.py
CLI propagates write_mode through coordination and backend config paths. Orchestrator introduces per-round isolation managers, workspace symlink creation, per-agent branch tracking, and an async _review_isolated_changes flow for presenting and applying approved changes with anonymized branch info in messages.
System Messages and Prompts
massgen/system_message_builder.py, massgen/system_prompt_sections.py, massgen/message_templates.py, massgen/utils/__init__.py, massgen/mcp_tools/client.py
System message builder and prompt sections gain worktree_paths and branch_name parameters for coordination messages. EvaluationSection adds round_number for sequential voting. Message templates include branch_info in restart context. New REVIEWING stage added to CoordinationStage enum. Subagent timeout tools exempted from MCP client timeout.
TUI Review Modal and Widgets
massgen/frontend/displays/textual/widgets/modals/review_modal.py, massgen/frontend/displays/textual/widgets/modals/content_modals.py, massgen/frontend/displays/textual/widgets/modals/__init__.py, massgen/frontend/displays/textual/__init__.py, massgen/frontend/displays/textual_widgets/__init__.py, massgen/frontend/displays/textual_widgets/agent_status_ribbon.py
New GitDiffReviewModal implementing two-panel git-style diff UI with file list and approval toggles. ContextModal enhanced with interactive path permission management, worktree mapping display, and per-agent synchronization. Agent status ribbon adds context paths indicator and TasksClicked signature update.
Terminal Display and Content
massgen/frontend/displays/textual_terminal_display.py, massgen/frontend/displays/textual_widgets/content_sections.py, massgen/frontend/displays/textual_widgets/file_explorer_panel.py, massgen/frontend/displays/textual_widgets/task_plan_host.py, massgen/frontend/displays/textual_widgets/multi_line_input.py, massgen/frontend/displays/content_processor.py
TextualTerminalDisplay gains final-presentation flag, theme loading from user settings, context-paths signal/modal integration, and async show_change_review_modal method. TimelineSection and FinalPresentationCard add final-lock mechanism, ID prefix namespacing, and streaming-to-final markdown rendering pipeline. FileExplorerPanel adds bounded preview logic for binary/text files. Various minor input validation and scroll handling improvements.
TUI Theming and Debug
massgen/frontend/displays/textual_themes/base.tcss, massgen/frontend/displays/textual_themes/dark.tcss, massgen/frontend/displays/textual_themes/light.tcss, massgen/frontend/displays/textual_themes/palettes/_dark.tcss, massgen/frontend/displays/textual_themes/palettes/_light.tcss, massgen/frontend/displays/shared/tui_debug.py, massgen/frontend/displays/shared/__init__.py
New review modal styling (two-panel layout with file list and diff viewer), toast and final presentation card styling. Light/dark theme variants with new semantic variables (mode-toggle, separator-border, tool-card-border, toast colors). Context paths indicator styling. Environment-based TUI debug toggle with conditional logging guards.
Coordination UI
massgen/frontend/coordination_ui.py, massgen/frontend/displays/textual_widgets/quickstart_wizard.py, massgen/frontend/displays/textual_widgets/setup_wizard.py, massgen/frontend/displays/tui_event_pipeline.py
Coordination UI flags final presentation and calls ensure_workspace_symlinks. QuickstartWizard adds Docker Setup step. SetupWizard adds dynamic per-provider API key steps and docker image pull with spinner animation. TUI event pipeline normalizes round numbers and captures final answer during final presentation.
Configuration Files
massgen/configs/basic/multi/three_agents_default.yaml, massgen/configs/features/test_subagent_orchestrator_code_mode.yaml, massgen/configs/debug/test_write_mode_isolation.yaml, massgen/configs/providers/openai/codex/codex_docker.yaml, massgen/configs/providers/openai/codex/codex_local.yaml
Replace use_two_tier_workspace: true with write_mode: auto across configs. Add new test_write_mode_isolation.yaml debug config demonstrating write_mode with auto setting.
Documentation
CLAUDE.md, docs/modules/worktrees.md, docs/source/user_guide/agent_workspaces.rst
New implementation guidelines recommending end-to-end wiring verification and integration tests. Comprehensive worktrees module docs describing workflow, components, parameters, and system prompt behavior. User guide section on agent workspaces covering write_mode configs, per-round worktrees, scratch/archive semantics, agent statelessness, and migration guidance.
Tests
massgen/tests/test_isolation_context.py, massgen/tests/test_write_mode_scratch.py
Comprehensive integration tests covering git utilities, worktree/shadow repo lifecycle, isolation context manager modes, change detection/application, and ChangeApplier. Extensive test suite for write_mode scratch handling including branch lifecycle, archive behavior, docker mounts, prompt integration, and config validation.
Utilities and Settings
massgen/user_settings.py
New persistent user settings manager for TUI (theme, vim_mode) with ~/.config/massgen/settings.json storage and fallback handling.
Test Harness
scripts/test_review_modal.py
Standalone Textual app for testing GitDiffReviewModal with mock diff datasets and theme toggling.

Sequence Diagram(s)

sequenceDiagram
    participant Orch as Orchestrator
    participant ICM as IsolationContextManager
    participant WM as WorktreeManager
    participant FM as FilesystemManager
    participant Modal as ReviewModal
    participant CA as ChangeApplier
    participant Orig as Original Repo

    Orch->>ICM: initialize_context(path)
    ICM->>WM: create_worktree(target, branch)
    WM-->>ICM: worktree path
    ICM-->>FM: isolated path & context info
    FM-->>Orch: ready for execution

    Orch->>Orch: execute agent in isolated context

    Orch->>ICM: get_changes(context_path)
    ICM-->>Orch: list of changes

    alt TUI Mode
        Orch->>Modal: show_change_review_modal(changes)
        Modal->>Modal: parse diffs & render
        Modal-->>Orch: ReviewResult (approved files)
    else Non-TUI
        Orch->>Orch: auto-approve all changes
    end

    Orch->>CA: apply_changes(isolated, original, approved_files)
    CA->>Orig: copy/delete approved files
    CA-->>Orch: applied files list

    Orch->>ICM: move_scratch_to_workspace()
    Orch->>ICM: cleanup_round()
    ICM->>WM: remove_worktree()
    WM-->>ICM: worktree removed
    ICM-->>Orch: cleanup complete
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

The changes are substantial and intricate, spanning foundational infrastructure (git utilities, isolation management, change application), mid-layer plumbing (configuration, backend wiring, filesystem integration), and extensive UI/frontend reimplementation. The diff introduces multiple new classes and modules (~2700+ lines of substantive new code), complex orchestration logic with per-round isolation workflows, interactive modal UI with approval flows, and extensive theming/styling updates. While many changes follow consistent patterns (e.g., write_mode propagation, YAML config updates), the heterogeneity across layers—from git operations to TUI modals to system prompts—and the density of new logic (isolation manager with worktree/shadow/legacy modes, change diffing and application, modal interactions) demand careful, multi-faceted review.

Possibly related issues

  • Linked issue #852 is directly addressed—this PR implements the subagent timeout exemption and the complete git worktree/change review infrastructure requested in that feature description.

Possibly related PRs

  • PR #835 overlaps in TUI event pipeline modifications and textual widget changes.
  • PR #787 evolved the use_two_tier_workspace concept that this PR now refactors into write_mode/worktree isolation.
  • PR #789 touched the same workspace/coordination config surfaces; this PR supersedes and evolves that work with the new write_mode model.

Suggested reviewers

  • a5507203
🚥 Pre-merge checks | ✅ 7 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The PR title "feat: V0.1.47 improves" is too vague and generic, lacking specific detail about the main changes. Use a more descriptive title such as "feat: Add worktree isolation, fix subagent timeout, and improve light theme visibility" to clearly indicate the primary features.
✅ Passed checks (7 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses the linked issue #852 (MAS-269) by exempting subagent-related MCP tools from timeout, meeting the requirement to prevent timeout aborts of long-running subagent operations.
Out of Scope Changes check ✅ Passed All code changes are scoped to the documented objectives: worktree isolation (MAS-272), subagent timeout fix (MAS-269), light theme fixes, quickstart wizard integration (MAS-267), and restart defaults (MAS-268).
Docstring Coverage ✅ Passed Docstring coverage is 90.16% which is sufficient. The required threshold is 80.00%.
Documentation Updated ✅ Passed PR includes comprehensive documentation: user-facing docs in agent_workspaces.rst and worktrees.md, configuration validation with VALID_WRITE_MODES, example YAML configs, Python docstrings for major classes, extensive test documentation covering scenarios, and CLAUDE.md guidance for AI assistants.
Capabilities Registry Check ✅ Passed This PR does not contain backend model or capability changes requiring updates to capabilities.py or token_manager.py. The modifications focus on write-mode isolation infrastructure, TUI/frontend improvements, and configuration validation.
Config Parameter Sync ✅ Passed Both files properly include write_mode parameter in exclusion sets with consistent comments and correct propagation to FilesystemManager.
Description check ✅ Passed The PR description is comprehensive, well-structured, and covers multiple major features and fixes. It includes clear summaries of changes, technical details, test plans, and issue references for each item.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch v0.1.47-improves

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
massgen/frontend/displays/textual_widgets/setup_wizard.py (1)

1055-1079: ⚠️ Potential issue | 🟠 Major

Redundant synchronous docker pull blocks the event loop.

on_wizard_complete performs a synchronous subprocess.run("docker", "pull", ...) (line 1067) for images remaining in images_to_pull. This blocks the Textual event loop and freezes the TUI. Moreover, with the new in-step pull button in DockerSetupStep, successfully-pulled images are already removed from the list—but if the user skips the pull button, this code path runs synchronously.

Consider either removing this redundant pull (letting the in-step button be the sole mechanism), or switching to asyncio.create_subprocess_exec to match the in-step approach.

massgen/filesystem_manager/_docker_manager.py (1)

490-526: ⚠️ Potential issue | 🟡 Minor

Document extra_mount_paths in the Google‑style docstring.

create_container gained a new parameter but the Args section wasn’t updated, so the docstring is now incomplete.

As per coding guidelines **/*.py: “For new or changed functions, include Google-style docstrings”.

massgen/backend/base.py (1)

97-104: ⚠️ Potential issue | 🟡 Minor

Add a Google‑style docstring to LLMBackend.__init__.

This function changed (new write_mode wiring) but still lacks a docstring. Please add a Google‑style docstring describing key args/behavior.

As per coding guidelines **/*.py: “For new or changed functions, include Google-style docstrings”.

massgen/frontend/displays/tui_event_pipeline.py (1)

96-100: ⚠️ Potential issue | 🟡 Minor

Add a Google‑style docstring to _apply_output.

The function was modified but still lacks a docstring. Please document the method and its Args.

As per coding guidelines **/*.py: “For new or changed functions, include Google-style docstrings”.

✍️ Example docstring
 def _apply_output(self, output: ContentOutput) -> None:
+        """Apply a parsed ContentOutput to the timeline.
+
+        Args:
+            output: Parsed output payload to render.
+        """
         timeline = self._get_timeline()
massgen/orchestrator.py (1)

7562-7614: ⚠️ Potential issue | 🟠 Major

Restart handling is now gated by isolation review.

The restart check is currently nested under the isolation review block, so when write_mode is off (no isolation manager), restarts will be skipped. That changes existing behavior and can silently disable restarts. Please dedent the restart block to run regardless of isolation.

🐛 Suggested fix
         if hasattr(self, "_isolation_manager") and self._isolation_manager:
             selected_agent = self.agents.get(self._selected_agent)
             if selected_agent:
                 # Re-add removed context paths so ChangeApplier can write back
                 ppm = selected_agent.backend.filesystem_manager.path_permission_manager
                 for orig_path, removed_mp in getattr(self, "_isolation_removed_paths", {}).items():
                     ppm.re_add_context_path(removed_mp)

                 async for chunk in self._review_isolated_changes(
                     agent=selected_agent,
                     isolation_manager=self._isolation_manager,
                     selected_agent_id=self._selected_agent,
                 ):
                     yield chunk
             # Clear the references after review
             self._isolation_manager = None
             self._isolation_worktree_paths = {}
             self._isolation_removed_paths = {}

-            # Check if restart was requested
-            if self.restart_pending and self.current_attempt < (self.max_attempts - 1):
-                ...
-                return
+        # Check if restart was requested
+        if self.restart_pending and self.current_attempt < (self.max_attempts - 1):
+            ...
+            return
massgen/frontend/displays/textual/widgets/modals/content_modals.py (1)

108-117: ⚠️ Potential issue | 🟡 Minor

subprocess.run calls lack check and timeout parameters.

These calls could silently fail (non-zero exit) or block the event loop indefinitely if the file manager doesn't detach. Since this is a Textual app (single-threaded event loop), a blocking subprocess would freeze the TUI.

Proposed fix: use `Popen` for fire-and-forget, or at least add a timeout
         try:
             system = platform.system()
             if system == "Darwin":  # macOS
-                subprocess.run(["open", str(workspace_path)])
+                subprocess.Popen(["open", str(workspace_path)])
             elif system == "Windows":
-                subprocess.run(["explorer", str(workspace_path)])
+                subprocess.Popen(["explorer", str(workspace_path)])
             else:  # Linux
-                subprocess.run(["xdg-open", str(workspace_path)])
+                subprocess.Popen(["xdg-open", str(workspace_path)])
         except Exception as e:
🤖 Fix all issues with AI agents
In `@massgen/filesystem_manager/_change_applier.py`:
- Around line 98-109: The change detection currently only checks unstaged
working-tree vs index (repo.index.diff(None)) and untracked files, missing
staged-but-uncommitted changes; update _apply_git_changes (and similarly
get_changes_summary) to also include index-vs-HEAD diffs via
repo.index.diff("HEAD") and merge those results with the existing
repo.index.diff(None) and repo.untracked_files output so staged changes are
captured; for each diff from repo.index.diff("HEAD") use diff.a_path or
diff.b_path and diff.change_type[0].upper(), and ensure when merging you
deduplicate paths preferring the staged (HEAD vs index) change_type when present
so staged changes are not overwritten by working-tree diffs.

In `@massgen/filesystem_manager/_docker_manager.py`:
- Around line 657-663: The extra mount handling currently resolves container
paths on the host and doesn't validate host paths; update the loop that
processes extra_mount_paths (the block building volumes and mount_info) to (1)
stop calling Path(container_path).resolve() — treat container_path as a
container-side string (e.g., ensure it remains unmodified and normalized only as
a string) and (2) validate host_path before adding to volumes by resolving
host_path to an absolute path with Path(host_path).resolve() and checking
existence/type (raise or skip with a clear error if missing or not allowed) so
Docker doesn't auto-create unintended directories; update mount_info
construction to use the validated host_path_resolved and the original
container_path string.

In `@massgen/filesystem_manager/_path_permission_manager.py`:
- Around line 217-241: The update_context_path_permission method currently sets
mp.permission to Permission.WRITE without checking the coordination-phase flag;
enforce the context_write_access_enabled guard so WRITE is not applied
immediately when self.context_write_access_enabled is False (preserve
will_be_writable but keep mp.permission read-only), i.e. in
update_context_path_permission validate Permission.WRITE against
self.context_write_access_enabled and only set mp.permission to WRITE when the
flag is True (otherwise leave mp.permission unchanged and only set
mp.will_be_writable), mirroring the coordination behavior used in
add_context_paths and ensuring managed_paths, Permission, mp.permission and
mp.will_be_writable semantics are preserved.

In `@massgen/frontend/displays/textual_terminal_display.py`:
- Around line 2572-2577: The combined CSS is built from user_settings.theme
which can diverge from the resolved display.theme passed by callers; update
TextualApp.__init__ to determine the effective theme by using display.theme if
set (fallback to get_user_settings().theme), validate against self.PALETTE_MAP,
and then call self._get_combined_css_path(effective_theme) instead of using
user_settings.theme so the CSS palette matches the runtime theme applied in
on_mount; adjust references to theme variable accordingly (look for
TextualApp.__init__, get_user_settings, self.PALETTE_MAP,
_get_combined_css_path, and on_mount).

In `@massgen/infrastructure/worktree_manager.py`:
- Around line 43-52: The exception handlers in create_worktree and
remove_worktree currently catch GitCommandError and re-raise a RuntimeError
without preserving the original traceback; update both exception raises to chain
the original exception using "raise RuntimeError(f'Failed to create/remove
worktree: {e.stderr}') from e" (keep the existing logger.error calls that
reference e.stderr) so the original GitCommandError is preserved for debugging
while still normalizing the error type.
🟡 Minor comments (27)
massgen/frontend/displays/textual_widgets/setup_wizard.py-469-494 (1)

469-494: ⚠️ Potential issue | 🟡 Minor

Potential pipe deadlock: read stderr only after stdout is fully consumed.

Both stdout and stderr use PIPE. The code reads stdout line-by-line to completion before calling proc.stderr.read(). If docker pull writes enough to stderr to fill the OS pipe buffer (~64 KB) before closing stdout, the subprocess will block on the stderr write and never close stdout, creating a deadlock.

For docker pull this is unlikely in practice (stderr is small), but consider using proc.communicate() or reading both streams concurrently for robustness.

Suggested safer approach
-                proc = await asyncio.create_subprocess_exec(
-                    "docker",
-                    "pull",
-                    image,
-                    stdout=asyncio.subprocess.PIPE,
-                    stderr=asyncio.subprocess.PIPE,
-                )
-                # Stream stdout line-by-line to show layer progress
-                if proc.stdout:
-                    async for raw_line in proc.stdout:
-                        line = raw_line.decode(errors="replace").strip()
-                        if line:
-                            self._spinner_message = line
-                stderr = await proc.stderr.read() if proc.stderr else b""
-                await proc.wait()
+                proc = await asyncio.create_subprocess_exec(
+                    "docker",
+                    "pull",
+                    image,
+                    stdout=asyncio.subprocess.PIPE,
+                    stderr=asyncio.subprocess.PIPE,
+                )
+
+                stderr_chunks: list[bytes] = []
+
+                async def _drain_stderr():
+                    if proc.stderr:
+                        async for chunk in proc.stderr:
+                            stderr_chunks.append(chunk)
+
+                stderr_task = asyncio.create_task(_drain_stderr())
+
+                if proc.stdout:
+                    async for raw_line in proc.stdout:
+                        line = raw_line.decode(errors="replace").strip()
+                        if line:
+                            self._spinner_message = line
+
+                await stderr_task
+                stderr = b"".join(stderr_chunks)
+                await proc.wait()
massgen/frontend/displays/textual_widgets/file_explorer_panel.py-44-48 (1)

44-48: ⚠️ Potential issue | 🟡 Minor

Placeholder entries are unintentionally clickable — clicking shows "file not found".

absolute_path="" is falsy, so _add_path (line 57: absolute_path or display_path) stores the display text "... (N more files)" as the tree-node data. That string is truthy, so on_tree_node_selected calls _show_preview, which fails to find the "file" and shows a confusing "file not found" message. The same issue applies to line 141.

Fix _add_path to distinguish an explicit empty string from None:

Proposed fix
 def _add_path(self, display_path: str, status: str, absolute_path: Optional[str] = None) -> None:
     """Track a path for display and lookup."""
     self._all_paths[display_path] = status
-    self._path_lookup[display_path] = absolute_path or display_path
+    self._path_lookup[display_path] = absolute_path if absolute_path is not None else display_path
massgen/configs/debug/test_write_mode_isolation.yaml-1-38 (1)

1-38: ⚠️ Potential issue | 🟡 Minor

Config placement doesn’t follow required configs/ subdirectory structure.

Please move this file under one of the approved subdirectories (basic/, tools/, providers/, teams/) to keep the configs tree consistent.

Based on learnings “Configuration YAML files in massgen/configs/ should be organized into subdirectories: basic/ for simple single/multi-agent configs, tools/ for MCP/filesystem/code execution configs, providers/ for provider-specific examples, and teams/ for pre-configured specialized teams”.

massgen/infrastructure/shadow_repo.py-69-72 (1)

69-72: ⚠️ Potential issue | 🟡 Minor

Chain the original exception for debuggability.

raise RuntimeError(...) discards the original GitCommandError traceback. Use from e to preserve the exception chain. Also, logging.exception would auto-include the traceback.

Proposed fix
         except GitCommandError as e:
-            logger.error(f"Failed to initialize shadow repo: {e.stderr}")
+            logger.exception(f"Failed to initialize shadow repo: {e.stderr}")
             self.cleanup()
-            raise RuntimeError(f"Failed to initialize shadow repo: {e.stderr}")
+            raise RuntimeError(f"Failed to initialize shadow repo: {e.stderr}") from e
massgen/infrastructure/shadow_repo.py-127-139 (1)

127-139: ⚠️ Potential issue | 🟡 Minor

Docstring claims .gitignore is respected, but only .git dirs are skipped.

The docstring says "respecting .gitignore if present" but the implementation only filters out .git directories via shutil.ignore_patterns(".git"). Files that would be git-ignored (e.g., __pycache__, node_modules) are still copied. Either update the docstring to reflect actual behavior, or add .gitignore parsing.

Proposed docstring fix
     def _copy_source_files(self) -> None:
-        """Copy files from source to temp_dir, respecting .gitignore if present."""
+        """Copy files from source to temp_dir, excluding .git directories."""
massgen/utils/git_utils.py-109-134 (1)

109-134: ⚠️ Potential issue | 🟡 Minor

get_changes docstring claims staged changes are included, but they aren't.

The docstring says "Get list of all changes (staged, unstaged, untracked)" but the implementation only collects unstaged changes (repo.index.diff(None)) and untracked files. Staged changes (repo.index.diff("HEAD")) are not captured. Either update the docstring to say "unstaged and untracked" or add the staged diff.

Option A: Fix docstring to match implementation
 def get_changes(repo: Repo) -> List[Dict[str, str]]:
     """
-    Get list of all changes (staged, unstaged, untracked) in a repo.
+    Get list of unstaged and untracked changes in a repo.
Option B: Add staged changes to match docstring
     changes = []
 
+    # Staged changes (index vs HEAD)
+    try:
+        for diff in repo.index.diff("HEAD"):
+            changes.append(
+                {
+                    "status": diff.change_type[0].upper(),
+                    "path": diff.a_path or diff.b_path,
+                },
+            )
+    except Exception:
+        pass  # Empty repo with no HEAD commit
+
     # Unstaged changes (working tree vs index)
     for diff in repo.index.diff(None):
massgen/cli.py-1471-1474 (1)

1471-1474: ⚠️ Potential issue | 🟡 Minor

Preserve explicit falsy write_mode values.

if write_mode_setting: skips explicit falsy values (e.g., false in YAML). If False is a valid override, it won’t propagate to the backend config.

🔧 Proposed fix
-        write_mode_setting = coordination_settings_for_injection.get("write_mode")
-        if write_mode_setting:
+        write_mode_setting = coordination_settings_for_injection.get("write_mode")
+        if write_mode_setting is not None:
             backend_config["write_mode"] = write_mode_setting
massgen/tests/test_isolation_context.py-725-731 (1)

725-731: ⚠️ Potential issue | 🟡 Minor

Remove unused tmp_path fixture to satisfy lint.

Proposed fix
-    def test_remove_nonexistent_path_returns_none(self, tmp_path):
+    def test_remove_nonexistent_path_returns_none(self):
massgen/tests/test_isolation_context.py-17-30 (1)

17-30: ⚠️ Potential issue | 🟡 Minor

Add Google-style Args/Returns to init_test_repo docstring.

Proposed fix
 def init_test_repo(path: Path, with_commit: bool = True) -> Repo:
-    """Helper to initialize a test git repo with GitPython."""
+    """Helper to initialize a test git repo with GitPython.
+
+    Args:
+        path: Directory to initialize as a git repository.
+        with_commit: Whether to create an initial commit.
+
+    Returns:
+        Initialized GitPython Repo instance.
+    """
As per coding guidelines: `**/*.py`: For new or changed functions, include Google-style docstrings.
massgen/frontend/displays/textual/widgets/modals/review_modal.py-35-67 (1)

35-67: ⚠️ Potential issue | 🟡 Minor

Add Google-style docstrings to _ApprovalToggle / _FileEntry methods.

The new __init__ and on_click handlers are missing Google-style docstrings.

As per coding guidelines: **/*.py: For new or changed functions, include Google-style docstrings.

scripts/test_review_modal.py-183-223 (1)

183-223: ⚠️ Potential issue | 🟡 Minor

Add Google-style Args/Returns to _build_combined_css docstring.

Proposed fix
 def _build_combined_css(theme: str = "dark") -> Path:
-    """Build a combined CSS file from palette + modal_base + base, matching the real app.
-
-    Always writes to the same fixed path so the stylesheet can be re-read on theme toggle.
-    """
+    """Build a combined CSS file from palette + modal_base + base, matching the real app.
+
+    Always writes to the same fixed path so the stylesheet can be re-read on theme toggle.
+
+    Args:
+        theme: Theme name ("dark" or "light").
+
+    Returns:
+        Path to the combined CSS file on disk.
+    """
As per coding guidelines: `**/*.py`: For new or changed functions, include Google-style docstrings.
scripts/test_review_modal.py-25-31 (1)

25-31: ⚠️ Potential issue | 🟡 Minor

Remove unused noqa: E402 directives to avoid Ruff warnings.

Proposed fix
-from textual.app import App, ComposeResult  # noqa: E402
-from textual.containers import Center, Vertical  # noqa: E402
-from textual.widgets import Button, Footer, Header, Label, Static  # noqa: E402
+from textual.app import App, ComposeResult
+from textual.containers import Center, Vertical
+from textual.widgets import Button, Footer, Header, Label, Static
 
-from massgen.filesystem_manager import ReviewResult  # noqa: E402
+from massgen.filesystem_manager import ReviewResult
 from massgen.frontend.displays.textual.widgets.modals.review_modal import (  # noqa: E402
     GitDiffReviewModal,
 )
scripts/test_review_modal.py-231-333 (1)

231-333: ⚠️ Potential issue | 🟡 Minor

Add Google-style docstrings to ReviewModalTestApp public methods.

compose, on_button_pressed, action_*, _open_modal, and _handle_result are new and should include Google-style docstrings (summary + Args/Returns where relevant).

As per coding guidelines: **/*.py: For new or changed functions, include Google-style docstrings.

massgen/filesystem_manager/_change_applier.py-144-191 (1)

144-191: ⚠️ Potential issue | 🟡 Minor

_apply_file_changes does not handle file deletions.

The fallback method walks the source to find new/modified files, but never detects files that exist in the target but were deleted in the source. This means deletions would silently be ignored when the git-based path fails. Consider walking the target as well to detect removed files.

massgen/infrastructure/worktree_manager.py-25-29 (1)

25-29: ⚠️ Potential issue | 🟡 Minor

repo.working_tree_dir can be None for bare repositories.

If a bare repo path is passed, Repo() succeeds but working_tree_dir returns None, leaving self.repo_path = None silently. Subsequent operations would fail with confusing errors.

Proposed fix
         try:
             self.repo = Repo(repo_path, search_parent_directories=True)
             self.repo_path = self.repo.working_tree_dir
+            if self.repo_path is None:
+                raise ValueError(f"Path {repo_path} is in a bare git repository (no working tree)")
         except InvalidGitRepositoryError:
-            raise ValueError(f"Path {repo_path} is not in a git repository")
+            raise ValueError(f"Path {repo_path} is not in a git repository") from None
massgen/filesystem_manager/_filesystem_manager.py-411-429 (1)

411-429: ⚠️ Potential issue | 🟡 Minor

Non-git context paths are silently dropped when write_mode is active.

When write_mode is set, Line 426 clears context_paths = [] unconditionally. However, extra_mount_paths only includes entries for context paths that have a .git directory (Line 420). If a user configures a context path that is not a git repository, it will neither be mounted as a regular context path nor as an extra .git mount — effectively making it invisible to the agent with no warning.

Consider logging a warning (or raising a validation error upstream) for context paths that lack a .git directory when write_mode requires worktree-based isolation.

Proposed fix
                 if ctx_path and os.path.isdir(git_dir):
                     extra_mount_paths.append((git_dir, git_dir, "rw"))
                     logger.info(
                         f"[FilesystemManager] write_mode: mounting .git/ dir for worktree refs: {git_dir}",
                     )
+                elif ctx_path:
+                    logger.warning(
+                        f"[FilesystemManager] write_mode: context path {ctx_path} has no .git directory — "
+                        "it will not be mounted. Worktree isolation requires a git repository.",
+                    )
massgen/frontend/displays/textual_themes/base.tcss-4966-4976 (1)

4966-4976: ⚠️ Potential issue | 🟡 Minor

SubagentScreen AgentTabBar needs .theme-light/.theme-dark overrides for consistency.

AgentStatusRibbon and #input_header both pair $separator-border with explicit .theme-light/.theme-dark hex overrides (lines ~396–403, ~1093–1100). SubagentScreen AgentTabBar (line 4969) lacks equivalent overrides. While AgentStatusRibbon inherits theme rules via global descendant selectors, AgentTabBar has no corresponding theme-specific rules anywhere, risking incorrect border visibility in light mode.

massgen/frontend/displays/textual_terminal_display.py-2907-2977 (1)

2907-2977: ⚠️ Potential issue | 🟡 Minor

Avoid writing debug JSON to a fixed /tmp path.

Even behind a debug flag, fixed /tmp/textual_debug.json can be clobbered or redirected (symlink attacks). Use a secure temp file or the session log directory to avoid predictable paths.

🛠️ Suggested fix
-                with open("/tmp/textual_debug.json", "w") as f:
-                    json.dump(debug_info, f, indent=2, default=str)
-                self.log("DEBUG: Widget info written to /tmp/textual_debug.json")
+                import tempfile
+                with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as f:
+                    json.dump(debug_info, f, indent=2, default=str)
+                    debug_path = f.name
+                self.log(f"DEBUG: Widget info written to {debug_path}")
massgen/frontend/displays/textual_terminal_display.py-1687-1721 (1)

1687-1721: ⚠️ Potential issue | 🟡 Minor

Timeout handler can pop the wrong screen.

On timeout, pop_screen() is invoked without checking that the review modal is still the active screen. If another modal was opened (or the review modal already closed), this can dismiss the wrong UI. Track the modal instance and only pop when it’s still on top (similar to the broadcast modal flow).

🛠️ Suggested fix
-        loop = asyncio.get_running_loop()
-        result_future: asyncio.Future[ReviewResult] = loop.create_future()
+        loop = asyncio.get_running_loop()
+        result_future: asyncio.Future[ReviewResult] = loop.create_future()
+        modal_ref = {"modal": None}
@@
-                modal = GitDiffReviewModal(changes=changes)
+                modal = GitDiffReviewModal(changes=changes)
+                modal_ref["modal"] = modal
@@
-            # Try to dismiss the modal on timeout
-            try:
-                self._app.call_from_thread(self._app.pop_screen)
-            except Exception:
-                pass
+            # Try to dismiss the modal on timeout (only if it’s still on top)
+            def safe_pop():
+                if self._app and modal_ref["modal"] and self._app.screen_stack:
+                    if self._app.screen_stack[-1] is modal_ref["modal"]:
+                        self._app.pop_screen()
+            try:
+                self._app.call_from_thread(safe_pop)
+            except Exception:
+                pass
massgen/frontend/displays/textual_terminal_display.py-603-607 (1)

603-607: ⚠️ Potential issue | 🟡 Minor

Final-presentation gating still allows default “thinking” content to leak into the timeline.

The comment says only tools should pass, but the allowlist includes "thinking". Since content_type defaults to "thinking", final-presentation chunks can still append to the timeline and reintroduce duplicates. If the goal is to suppress all non-tool content in this phase, drop "thinking" from the allowlist or ensure final presentation uses content_type="presentation".

🛠️ Possible adjustment (if tools-only is intended)
-        if hasattr(self, "_in_final_presentation") and self._in_final_presentation:
-            if content_type not in ("tool", "thinking"):
-                return
+        if hasattr(self, "_in_final_presentation") and self._in_final_presentation:
+            if content_type != "tool":
+                return
massgen/tests/test_write_mode_scratch.py-142-142 (1)

142-142: ⚠️ Potential issue | 🟡 Minor

Typo in class names: "Scatch" → "Scratch".

TestMoveScatchToWorkspace (line 142) and TestShadowModeCreatesScatch (line 405) both misspell "Scratch."

Fix
-class TestMoveScatchToWorkspace:
+class TestMoveScratchToWorkspace:
-class TestShadowModeCreatesScatch:
+class TestShadowModeCreatesScratch:

Also applies to: 405-405

massgen/system_prompt_sections.py-949-971 (1)

949-971: ⚠️ Potential issue | 🟡 Minor

Branch info and scratch-archive note are duplicated once per worktree path.

The for wt_path in self.worktree_paths: loop (line 951) contains the "### Code Branches" header, branch_name, other_branches, and the .scratch_archive/ note. When multiple context paths produce multiple worktree entries, all of this gets repeated for every iteration. Only the "### Project Checkout" block is per-worktree; the branch/coordination info should be rendered once, outside the loop.

Proposed fix: move branch info outside the loop
         # Worktree-based workspace (new unified model) takes precedence
         if self.worktree_paths:
             for wt_path in self.worktree_paths:
                 content_parts.append("### Project Checkout\n")
                 content_parts.append(f"The project is checked out at `{wt_path}`. Full read/write access.\n")
                 content_parts.append("**Scratch Space**: `.massgen_scratch/` inside the checkout")
                 content_parts.append("- For experiments, eval scripts, notes")
                 content_parts.append("- Git-excluded, invisible to reviewers")
                 content_parts.append("- Can import from project naturally (e.g., `from src.foo import bar`)\n")
 
-                content_parts.append("### Code Branches\n")
-                if self.branch_name:
-                    content_parts.append(f"Your work is on branch `{self.branch_name}`. All changes are auto-committed when your turn ends.\n")
-                else:
-                    content_parts.append("All changes are auto-committed when your turn ends.\n")
-
-                if self.other_branches:
-                    content_parts.append("**Other agents' branches:**")
-                    for label, branch in self.other_branches.items():
-                        content_parts.append(f"- {label}: `{branch}`")
-                    content_parts.append("\nUse `git diff <branch>` to compare, `git merge <branch>` to incorporate.\n")
-
-                content_parts.append("**Previous scratch files**: Check `.scratch_archive/` in your workspace for experiments from prior rounds.\n")
+            content_parts.append("### Code Branches\n")
+            if self.branch_name:
+                content_parts.append(f"Your work is on branch `{self.branch_name}`. All changes are auto-committed when your turn ends.\n")
+            else:
+                content_parts.append("All changes are auto-committed when your turn ends.\n")
+
+            if self.other_branches:
+                content_parts.append("**Other agents' branches:**")
+                for label, branch in self.other_branches.items():
+                    content_parts.append(f"- {label}: `{branch}`")
+                content_parts.append("\nUse `git diff <branch>` to compare, `git merge <branch>` to incorporate.\n")
+
+            content_parts.append("**Previous scratch files**: Check `.scratch_archive/` in your workspace for experiments from prior rounds.\n")
massgen/orchestrator.py-8659-8675 (1)

8659-8675: ⚠️ Potential issue | 🟡 Minor

Unused parameter in _review_isolated_changes.

agent isn’t used; either remove it or prefix with _ to satisfy lint and clarify intent.

🔧 Suggested fix
-    async def _review_isolated_changes(
-        self,
-        agent: "ChatAgent",
+    async def _review_isolated_changes(
+        self,
+        _agent: "ChatAgent",
         isolation_manager: "IsolationContextManager",
         selected_agent_id: str,
     ) -> AsyncGenerator[StreamChunk, None]:
massgen/orchestrator.py-7893-7905 (1)

7893-7905: ⚠️ Potential issue | 🟡 Minor

Avoid silent isolation cleanup failures.

The try/except: pass masks cleanup issues and will make isolation failures hard to debug. Logging at debug is safer and satisfies lint.

🧹 Suggested fix
-                    if self._isolation_manager:
-                        try:
-                            self._isolation_manager.cleanup_all()
-                        except Exception:
-                            pass
+                    if self._isolation_manager:
+                        try:
+                            self._isolation_manager.cleanup_all()
+                        except Exception as cleanup_err:
+                            logger.debug(
+                                f"[Orchestrator] Isolation cleanup failed during fallback: {cleanup_err}",
+                            )
massgen/orchestrator.py-5828-5883 (1)

5828-5883: ⚠️ Potential issue | 🟡 Minor

Clean up partial worktrees on setup failure.

If setup fails mid‑way, lingering worktrees/branches can leak and affect later rounds. Consider cleaning up the partially created isolation session in the exception path.

🔧 Suggested fix
-        if write_mode and write_mode != "legacy" and agent.backend.filesystem_manager:
-            try:
+        if write_mode and write_mode != "legacy" and agent.backend.filesystem_manager:
+            round_isolation_mgr = None
+            try:
                 from .filesystem_manager import IsolationContextManager
@@
-                round_isolation_mgr = IsolationContextManager(
+                round_isolation_mgr = IsolationContextManager(
                     session_id=f"{self.session_id}-{round_suffix}",
                     write_mode=write_mode,
                     workspace_path=workspace_path,
                     previous_branch=previous_branch,
                 )
@@
-            except Exception as e:
-                logger.warning(f"[Orchestrator] Failed to create per-round worktree for {agent_id}: {e}")
-                round_worktree_paths = None
+            except Exception as e:
+                if round_isolation_mgr:
+                    try:
+                        round_isolation_mgr.cleanup_session()
+                    except Exception as cleanup_err:
+                        logger.debug(
+                            f"[Orchestrator] Failed to cleanup partial worktree for {agent_id}: {cleanup_err}",
+                        )
+                logger.warning(f"[Orchestrator] Failed to create per-round worktree for {agent_id}: {e}")
+                round_worktree_paths = None
massgen/filesystem_manager/_isolation_context_manager.py-506-510 (1)

506-510: ⚠️ Potential issue | 🟡 Minor

shutil.move behaves differently when destination already exists.

If move_scratch_to_workspace is called twice with the same archive_label, the second call will nest the scratch directory inside the existing archive directory (e.g., .scratch_archive/agent1/.massgen_scratch/) instead of replacing it. This could surprise consumers of the archive.

Proposed fix: remove existing archive dir before moving
         archive_dir = os.path.join(workspace, ".scratch_archive", archive_name)
         os.makedirs(os.path.dirname(archive_dir), exist_ok=True)
 
         try:
+            if os.path.exists(archive_dir):
+                shutil.rmtree(archive_dir)
             shutil.move(scratch_path, archive_dir)
massgen/filesystem_manager/_isolation_context_manager.py-214-220 (1)

214-220: ⚠️ Potential issue | 🟡 Minor

Branch-name collision is possible when branch_label is set and multiple context paths exist in the same repository, though this is an edge case.

When initialize_context is called multiple times with different context_path values that point to the same git repository (e.g., repo/src and repo/tests both within repo), and branch_label is provided (e.g., "presenter"), the first call succeeds but the second will fail. The initialize_context cache is keyed by individual context_path, not by repo_root, so it cannot prevent the collision. When git worktree add -b "presenter" is called the second time, it will raise GitCommandError because the branch already exists, which propagates as a RuntimeError.

While multiple writable context paths in the same repository during final presentation is not a typical usage pattern and lacks test coverage, it is technically possible through the orchestrator's managed paths system. Consider either: (1) detecting this scenario and reusing the existing branch/worktree for same-repo paths, (2) appending a suffix to branch_label per context path, or (3) documenting that branch_label assumes a single context path per repository during final presentation.

🧹 Nitpick comments (36)
massgen/frontend/displays/textual_widgets/multi_line_input.py (1)

601-608: Pre-existing: redundant identical branches in _delete_to_position.

Both branches of the ternary on Line 603 evaluate to the same expression (target_col + 1), and similarly Line 607 evaluates to target_col in both branches. The logic happens to be correct because _find_char_position already adjusts target_col for t/T vs f/F, but the dead conditionals are confusing and suggest the author may have intended different behavior per motion type.

Not introduced by this PR, so just flagging for awareness.

♻️ Suggested simplification
         if motion in ("f", "t"):
             # Delete forward (include target for 'f', exclude for 't' already handled)
-            end = target_col + 1 if motion == "f" else target_col + 1
+            end = target_col + 1
             lines[row] = line[:col] + line[end:]
         else:
             # Delete backward
-            start = target_col if motion == "F" else target_col
+            start = target_col
             lines[row] = line[:start] + line[col:]
             col = start  # Move cursor to deletion point
massgen/frontend/displays/textual_widgets/setup_wizard.py (3)

519-521: Successfully-pulled image checkboxes are re-enabled and remain interactive.

After a successful pull, _selected_images is trimmed (line 503) but the corresponding checkboxes are re-enabled on line 520–521. A user could re-check a successfully-pulled image, causing it to be re-added to _selected_images via on_checkbox_changed and re-pulled on retry.

Consider disabling or removing checkboxes for successfully-pulled images.

Suggested fix
         # Re-enable remaining checkboxes
-        for cb in self._checkboxes.values():
-            cb.disabled = False
+        for img_name, cb in self._checkboxes.items():
+            if img_name in pulled:
+                cb.value = False
+                cb.disabled = True
+            else:
+                cb.disabled = False

304-310: Instance attribute _SPINNER_FRAMES uses UPPER_CASE naming.

PEP 8 reserves UPPER_CASE for class-level constants. Since this is an instance attribute, either promote it to a class constant (like AVAILABLE_IMAGES) or rename it to _spinner_frames.

Suggested fix

Move to class level alongside AVAILABLE_IMAGES:

 class DockerSetupStep(StepComponent):
     ...
     AVAILABLE_IMAGES = [...]
+    _SPINNER_FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")

     def __init__(self, ...):
         ...
-        self._SPINNER_FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")

This aligns with the pattern in textual_terminal_display.py where SPINNER_FRAMES is a class-level attribute (see relevant code snippet at line 2339).


1016-1025: Fragile .env parsing doesn't handle quoted values or inline comments.

The manual .env parser (lines 1020–1025) splits on = and strips whitespace, but doesn't handle quoted values (KEY="value") or inline comments (KEY=value # comment). If any existing .env entry uses these patterns, the re-written file will corrupt them.

Consider using dotenv's own dotenv_values() to read the existing file, since python-dotenv is already a dependency.

Suggested fix
+        from dotenv import dotenv_values
+
         existing_content = {}
         if env_path.exists():
-            try:
-                with open(env_path, "r") as f:
-                    for line in f:
-                        line = line.strip()
-                        if line and not line.startswith("#") and "=" in line:
-                            key, value = line.split("=", 1)
-                            existing_content[key.strip()] = value.strip()
-            except Exception as e:
-                _setup_log(f"SetupWizard: Could not read existing .env: {e}")
+            existing_content = dotenv_values(env_path) or {}
massgen/frontend/displays/textual_widgets/content_sections.py (3)

36-38: _env_flag duplicates tui_debug_enabled logic — consider reusing.

This helper replicates the same truthy-env-var check that tui_debug_enabled() in tui_debug.py uses. If more env flags are needed, this generic helper is fine. But if it's only used for MASSGEN_TUI_SCROLL_DEBUG (Line 445), you could document that relationship or import a shared helper to keep the "truthy env var" definition in one place.

Not blocking — just a minor DRY note.


813-843: Consider logging timer stop failures instead of silencing them.

The enter_final_lock method thoroughly resets all scroll state and cancels pending timers — good. However, the try/except pass blocks at Lines 824-828 and 829-834 silently swallow exceptions, which was flagged by Ruff (S110). Elsewhere in this file (Line 752-754), timer stop failures are logged via tui_log. For consistency:

♻️ Suggested fix
         if self._scroll_timer is not None:
             try:
                 self._scroll_timer.stop()
-            except Exception:
-                pass
+            except Exception as e:
+                tui_log(f"[ContentSections] enter_final_lock scroll_timer stop: {e}")
             self._scroll_timer = None
         if self._debounce_timer is not None:
             try:
                 self._debounce_timer.stop()
-            except Exception:
-                pass
+            except Exception as e:
+                tui_log(f"[ContentSections] enter_final_lock debounce_timer stop: {e}")
             self._debounce_timer = None

2591-2591: Back-compat alias _vote_results should stay in sync with vote_results.

self._vote_results = self.vote_results is set once in __init__. If vote_results is later mutated (e.g., dict updated in place), both references point to the same object, so they stay in sync. However, if self.vote_results is ever reassigned to a new dict, _vote_results will go stale. If external code only reads _vote_results, this is fine for the current usage, but a @property would be safer long-term.

massgen/frontend/displays/textual_widgets/file_explorer_panel.py (1)

142-143: Silent except Exception: pass hides scanning failures.

If os.walk or the loop body raises an unexpected error, the panel silently shows nothing with no way to diagnose why. Consider logging at debug level so issues are traceable without disrupting the TUI.

Proposed fix
+import logging
+
+logger = logging.getLogger(__name__)
-        except Exception:
-            pass
+        except Exception:
+            logger.debug("Error scanning workspace %s", self.workspace_path, exc_info=True)
pyproject.toml (1)

72-72: Consider making gitpython an optional dependency.

If the worktree/shadow-repo isolation features aren't used by all users, moving gitpython into an optional dependency group (e.g., [project.optional-dependencies] isolation = ["gitpython>=3.1.46"]) would keep the default install lighter. This is similar to how docker is already handled as an optional extra on line 110-112.

massgen/configs/debug/test_write_mode_isolation.yaml (1)

16-19: Prefer a cost‑effective model for example/debug configs.

Unless this test specifically needs Claude, consider switching to one of the preferred budget models (e.g., gpt‑5‑mini or gemini‑2.5‑flash).

As per coding guidelines massgen/configs/**/*.yaml: “Prefer cost-effective models (gpt-5-nano, gpt-5-mini, gemini-2.5-flash)”.

massgen/agent_config.py (1)

118-163: Add write_mode validation for programmatic configs.

CoordinationConfig can be instantiated directly (bypassing YAML validation). Consider validating write_mode in __post_init__ to fail fast on invalid values.

♻️ Suggested patch
@@
     def __post_init__(self):
         """Validate configuration after initialization."""
         self._validate_broadcast_config()
         self._validate_timeout_config()
+        self._validate_write_mode()
+
+    def _validate_write_mode(self) -> None:
+        """Validate write_mode configuration."""
+        if self.write_mode is None:
+            return
+        valid = {"auto", "worktree", "isolated", "legacy"}
+        if self.write_mode not in valid:
+            raise ValueError(
+                f"Invalid write_mode: {self.write_mode}. Must be one of {sorted(valid)} or None."
+            )
massgen/user_settings.py (2)

19-22: Annotate DEFAULT_SETTINGS with ClassVar.

Mutable class attributes should be annotated with typing.ClassVar to signal they aren't instance attributes. Flagged by Ruff (RUF012).

Proposed fix
+from typing import Any, ClassVar, Dict, Optional
-from typing import Any, Dict, Optional
 ...
-    DEFAULT_SETTINGS = {
+    DEFAULT_SETTINGS: ClassVar[Dict[str, Any]] = {
         "theme": "dark",
         "vim_mode": False,
     }

24-28: Missing docstring on __init__.

As per coding guidelines, new or changed functions should include Google-style docstrings. __init__ should document its behavior (loading settings from disk).

Proposed fix
 def __init__(self):
+    """Initialize UserSettings, loading from ~/.config/massgen/settings.json."""
     self._settings: Dict[str, Any] = {}
massgen/message_templates.py (1)

510-522: Docstring for branch_info understates the accepted type of other_branches.

Line 521 documents other_branches as list[str], but the implementation (Lines 556–560) also handles dict (label→branch mapping). The docstring should reflect both accepted formats to avoid confusion for callers.

Proposed docstring fix
-            branch_info: Optional dict with 'own_branch' (str) and 'other_branches' (list[str])
-                for communicating branch names from the previous attempt
+            branch_info: Optional dict with 'own_branch' (str) and 'other_branches'
+                (dict[str, str] mapping label to branch, or list[str] for legacy format)
+                for communicating branch names from the previous attempt
massgen/infrastructure/shadow_repo.py (1)

74-99: get_changes() duplicates git_utils.get_changes().

The logic here is identical to massgen/utils/git_utils.py:get_changes(repo). Consider delegating to avoid maintaining the same diff-enumeration code in two places.

Proposed fix
+from massgen.utils.git_utils import get_changes as git_get_changes
+
     def get_changes(self) -> List[Dict[str, str]]:
-        """
-        Get list of changes in the shadow repo since initial commit.
-
-        Returns:
-            List of dicts with 'status' and 'path' keys
-        """
+        """Get list of changes in the shadow repo since initial commit."""
         if not self.is_initialized or not self.repo:
             return []
-
-        changes = []
-
-        # Unstaged changes (working tree vs index)
-        for diff in self.repo.index.diff(None):
-            changes.append(
-                {
-                    "status": diff.change_type[0].upper(),  # M, A, D, R
-                    "path": diff.a_path or diff.b_path,
-                },
-            )
-
-        # Untracked files
-        for path in self.repo.untracked_files:
-            changes.append({"status": "?", "path": path})
-
-        return changes
+        return git_get_changes(self.repo)
massgen/frontend/coordination_ui.py (1)

824-829: Duplicated ensure_workspace_symlinks block across coordinate() and coordinate_with_context().

Both methods contain an identical 6-line block for symlink creation. This is consistent with the existing parallelism between these two methods, but worth noting as a candidate for extraction into a shared helper if these methods are ever refactored.

The broad Exception catch (flagged by Ruff BLE001) is acceptable here since symlink creation is non-critical and the orchestrator method already has its own internal exception handling (see massgen/orchestrator.py, Lines 528-548).

Also applies to: 1458-1463

docs/source/user_guide/agent_workspaces.rst (1)

82-83: Minor: RST underline length mismatch.

The title is 37 characters but the underline is 39. RST requires the underline to be at least as long as the title (so this works), but convention is to match exactly. Very minor nit.

docs/modules/worktrees.md (1)

9-23: Add language identifiers to fenced code blocks.

Three code blocks lack language specifiers (flagged by markdownlint MD040). Since these are ASCII diagrams and text blocks, text is appropriate:

Suggested fix

Line 9:

-```
+```text

Line 41:

-```
+```text

Line 59:

-```
+```text

Also applies to: 41-45, 59-65

massgen/infrastructure/__init__.py (1)

14-21: __all__ is not sorted (Ruff RUF022).

The filesystem_manager/__init__.py in this same PR uses alphabetical ordering. Align for consistency:

Suggested fix
 __all__ = [
+    "ShadowRepo",
     "WorktreeManager",
-    "ShadowRepo",
-    "get_git_root",
     "get_git_branch",
+    "get_git_root",
     "get_git_status",
     "is_git_repo",
 ]
massgen/filesystem_manager/_change_applier.py (2)

93-96: Chain the original exception for debuggability.

Ruff B904 is valid here — re-raising without from loses the original traceback context.

Proposed fix
         try:
             repo = Repo(str(source))
         except InvalidGitRepositoryError:
-            raise ValueError(f"Source is not a git repository: {source}")
+            raise ValueError(f"Source is not a git repository: {source}") from None

139-140: Use log.exception() instead of log.error() in exception handlers.

Within except blocks, log.exception() automatically includes the traceback, which is valuable for debugging file-apply failures. This applies to Lines 140, 189, and 240.

Proposed fix (example for line 140)
             except Exception as e:
-                log.error(f"Failed to apply change for {rel_path}: {e}")
+                log.exception(f"Failed to apply change for {rel_path}: {e}")

Also applies to: 188-189, 239-240

massgen/infrastructure/worktree_manager.py (2)

127-135: Silent error swallowing in _delete_branch loses diagnostic information.

If branch deletion fails (e.g., branch is checked out elsewhere), the bare pass makes it invisible. A debug-level log would aid troubleshooting without being noisy.

Proposed fix
         except GitCommandError:
-            pass
+            logger.debug(f"Could not delete branch {branch_name} (may already be deleted or in use)")

15-16: Missing Google-style class docstring.

The class has a one-liner docstring but no Attributes: section documenting repo and repo_path. As per coding guidelines, new classes in massgen/**/*.py should include Google-style docstrings.

massgen/frontend/displays/textual_themes/base.tcss (2)

4819-4857: Toast theming approach is sound; hardcoded hex duplicates the pattern from AgentStatusRibbon.

The override strategy (base → theme-class → severity) is correct and source-order ensures severity border-left wins over the theme default. Minor observation: the hardcoded hex values (#ffffff, #d0d7de, #0969da, etc.) for light/dark toasts duplicate the same GitHub-style palette values used in the AgentStatusRibbon and #input_header overrides. If these are already defined as variables in your palette files, referencing the variables would reduce the duplication and make future palette changes easier.


392-403: Hardcoded separator hex values duplicated across selectors.

The same hex pairs (#eaeef2 for light, #30363d for dark) appear in both AgentStatusRibbon (lines 398, 402) and #input_header (lines 1095, 1099). If $separator-border is already theme-aware in the palette, the .theme-light/.theme-dark overrides may be redundant. If not, consider extracting these into dedicated palette variables to avoid the duplication and make future color changes a single-point edit.

Also applies to: 1088-1100

massgen/frontend/displays/textual_terminal_display.py (1)

3861-3866: Don’t silently swallow preference persistence failures.

Failures to persist vim mode/theme are currently swallowed, which makes preference bugs very hard to diagnose. Consider logging at least a warning so users/devs can see why preferences didn’t save.

🛠️ Suggested fix (apply similarly to theme persistence)
             try:
                 user_settings = get_user_settings()
                 user_settings.vim_mode = self.question_input.vim_mode
-            except Exception:
-                pass
+            except Exception as e:
+                tui_log(f"[TextualDisplay] Failed to persist vim_mode: {e}")

Also applies to: 6261-6265

massgen/system_prompt_sections.py (1)

918-937: Update class docstring with the new constructor parameters.

The class-level docstring (lines 905–916) still lists only workspace_path, context_paths, and use_two_tier_workspace. The three new parameters—worktree_paths, branch_name, and other_branches—should be documented there as well to keep the Args block accurate.

As per coding guidelines, **/*.py: "For new or changed functions, include Google-style docstrings."

massgen/tests/test_write_mode_scratch.py (1)

381-402: Testing a private method (_apply_file_changes) directly.

This is acceptable for unit tests of internal logic, but note that if the method signature changes, this test will break without a public API change. Consider whether a thin public wrapper or integration test would be more maintainable long-term.

massgen/filesystem_manager/_isolation_context_manager.py (5)

265-267: Chain exceptions with raise ... from e to preserve traceback context.

Both _create_worktree_context and _create_shadow_context re-raise as RuntimeError but discard the original exception chain. Also, log.error won't include the traceback; prefer log.exception.

Proposed fix (showing worktree variant; apply same pattern to shadow)
         except Exception as e:
-            log.error(f"Failed to create worktree: {e}")
-            raise RuntimeError(f"Failed to create worktree context: {e}")
+            log.exception(f"Failed to create worktree: {e}")
+            raise RuntimeError(f"Failed to create worktree context: {e}") from e
         except Exception as e:
-            log.error(f"Failed to create shadow repo: {e}")
-            raise RuntimeError(f"Failed to create shadow context: {e}")
+            log.exception(f"Failed to create shadow repo: {e}")
+            raise RuntimeError(f"Failed to create shadow context: {e}") from e

Also applies to: 288-290


645-660: Silent try/except/pass blocks swallow errors during session cleanup.

These blocks (lines 648–652 and 657–660) silently discard exceptions, making it difficult to diagnose cleanup failures. Even at session end, a log.debug would aid troubleshooting stale branches or worktree metadata.

Proposed fix
         for branch_name in self._session_branches:
             for wm in self._worktree_managers.values():
                 try:
                     wm._delete_branch(branch_name, force=True)
                     log.info(f"Cleaned up session branch: {branch_name}")
-                except Exception:
-                    pass
+                except Exception as e:
+                    log.debug(f"Could not delete branch {branch_name}: {e}")
         self._session_branches.clear()
 
         # Prune any stale worktree metadata
         for wm in self._worktree_managers.values():
             try:
                 wm.prune()
-            except Exception:
-                pass
+            except Exception as e:
+                log.debug(f"Failed to prune worktree metadata: {e}")

744-761: get_diff for worktree mode stages and resets — non-atomic and leaks staged state on failure.

The add -Adiff --stagedreset HEAD sequence at lines 756–758 is a known trick, but if diff or reset throws (e.g., corrupted index), the worktree is left in a fully-staged state. Since this is an isolated worktree and the window is small, it's low risk, but worth noting.

Consider wrapping the reset in a finally block:

Proposed fix
                 # Stage everything so untracked (new) files appear in the diff,
                 # then unstage so ChangeApplier can still detect changes via
                 # repo.index.diff(None) and repo.untracked_files.
                 repo.git.add("-A")
-                diff_output = repo.git.diff("--staged")
-                repo.git.reset("HEAD")
-                return diff_output
+                try:
+                    diff_output = repo.git.diff("--staged")
+                finally:
+                    repo.git.reset("HEAD")
+                return diff_output

811-816: Another try/except/pass in cleanup_all — same silent swallowing concern as cleanup_session.

Same pattern as flagged in cleanup_session. Add log.debug for consistency.

Proposed fix
         for wm in self._worktree_managers.values():
             try:
                 wm.prune()
-            except Exception:
-                pass
+            except Exception as e:
+                log.debug(f"Failed to prune worktree metadata: {e}")

292-293: Method name _setup_scratch_in_worktree is misleading — it is also called for shadow repos.

Line 284 calls this from _create_shadow_context. Consider renaming to _setup_scratch_dir or _setup_scratch_in_isolation to reflect actual usage.

massgen/frontend/displays/textual/widgets/modals/content_modals.py (3)

390-403: Index-based button dispatch has a TOCTOU window if the path list changes between render and click.

on_button_pressed parses an integer index from the button ID and uses it to index into a fresh call to _get_current_paths() (inside _toggle_permission / _remove_path). If another agent or process modifies the paths between the render and the click, the index could refer to a different path—or be out of bounds (though the bounds check at lines 408/422 prevents a crash).

The risk is low in practice since changes require user action in the same modal, but worth noting.


289-301: _get_current_paths reads only from the first agent's PPM.

If agents' path lists ever diverge (e.g., a propagation partially fails), the modal will only reflect the first agent's state. Consider adding a defensive log if agents have inconsistent path sets, so divergence is surfaced early.


500-525: _update_agent_config_context_paths silently drops through when no keyword flags match.

If none of remove, add, or new_permission is truthy, the method exits without doing anything. This is technically correct since callers always set exactly one, but adding an else log or assertion would catch misuse.

Comment on lines +98 to +109
# Get all changed files
changed_files: Dict[str, str] = {} # path -> change_type (M, A, D, ?)

# Unstaged changes (working tree vs index)
for diff in repo.index.diff(None):
rel_path = diff.a_path or diff.b_path
if rel_path:
changed_files[rel_path] = diff.change_type[0].upper()

# Untracked files (new files)
for rel_path in repo.untracked_files:
changed_files[rel_path] = "A" # Treat untracked as added
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

_apply_git_changes only detects unstaged changes — staged changes are missed.

repo.index.diff(None) (Line 102) detects working-tree-vs-index changes, and repo.untracked_files (Line 108) catches new files. However, if the isolation context stages changes via git add (without committing), those would only appear in repo.index.diff("HEAD"), not in repo.index.diff(None). The same gap exists in get_changes_summary (Line 222).

Proposed fix
         # Unstaged changes (working tree vs index)
         for diff in repo.index.diff(None):
             rel_path = diff.a_path or diff.b_path
             if rel_path:
                 changed_files[rel_path] = diff.change_type[0].upper()

+        # Staged changes (index vs HEAD)
+        for diff in repo.index.diff("HEAD"):
+            rel_path = diff.a_path or diff.b_path
+            if rel_path and rel_path not in changed_files:
+                changed_files[rel_path] = diff.change_type[0].upper()
+
         # Untracked files (new files)
         for rel_path in repo.untracked_files:
             changed_files[rel_path] = "A"  # Treat untracked as added
🤖 Prompt for AI Agents
In `@massgen/filesystem_manager/_change_applier.py` around lines 98 - 109, The
change detection currently only checks unstaged working-tree vs index
(repo.index.diff(None)) and untracked files, missing staged-but-uncommitted
changes; update _apply_git_changes (and similarly get_changes_summary) to also
include index-vs-HEAD diffs via repo.index.diff("HEAD") and merge those results
with the existing repo.index.diff(None) and repo.untracked_files output so
staged changes are captured; for each diff from repo.index.diff("HEAD") use
diff.a_path or diff.b_path and diff.change_type[0].upper(), and ensure when
merging you deduplicate paths preferring the staged (HEAD vs index) change_type
when present so staged changes are not overwritten by working-tree diffs.

Comment on lines +657 to +663
# Mount extra paths (e.g., worktree isolation paths)
if extra_mount_paths:
for host_path, container_path, mode in extra_mount_paths:
host_path_resolved = str(Path(host_path).resolve())
container_path_str = str(Path(container_path).resolve())
volumes[host_path_resolved] = {"bind": container_path_str, "mode": mode}
mount_info.append(f" {host_path_resolved} ← {container_path_str} ({mode}, extra)")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid resolving container paths on the host and validate extra mounts.

Path(container_path).resolve() converts relative container paths into host paths, which can mount to an unintended location inside the container. Also, missing host-path checks can cause Docker to create directories unexpectedly. Consider treating container_path as a container-side string and validating host_path existence before mounting.

🛠️ Suggested fix
-        if extra_mount_paths:
-            for host_path, container_path, mode in extra_mount_paths:
-                host_path_resolved = str(Path(host_path).resolve())
-                container_path_str = str(Path(container_path).resolve())
-                volumes[host_path_resolved] = {"bind": container_path_str, "mode": mode}
-                mount_info.append(f"      {host_path_resolved} ← {container_path_str} ({mode}, extra)")
+        if extra_mount_paths:
+            for host_path, container_path, mode in extra_mount_paths:
+                host_path_resolved = Path(host_path).expanduser().resolve()
+                if not host_path_resolved.exists():
+                    logger.warning(f"⚠️ [Docker] Extra mount path not found: {host_path}")
+                    continue
+                # Keep container_path as a container-side path (don’t resolve on host).
+                container_path_str = str(container_path)
+                volumes[str(host_path_resolved)] = {"bind": container_path_str, "mode": mode}
+                mount_info.append(f"      {host_path_resolved} ← {container_path_str} ({mode}, extra)")
🤖 Prompt for AI Agents
In `@massgen/filesystem_manager/_docker_manager.py` around lines 657 - 663, The
extra mount handling currently resolves container paths on the host and doesn't
validate host paths; update the loop that processes extra_mount_paths (the block
building volumes and mount_info) to (1) stop calling
Path(container_path).resolve() — treat container_path as a container-side string
(e.g., ensure it remains unmodified and normalized only as a string) and (2)
validate host_path before adding to volumes by resolving host_path to an
absolute path with Path(host_path).resolve() and checking existence/type (raise
or skip with a clear error if missing or not allowed) so Docker doesn't
auto-create unintended directories; update mount_info construction to use the
validated host_path_resolved and the original container_path string.

Comment on lines +779 to +801
def _cleanup_single_context(self, context_path: str) -> None:
"""Cleanup a single isolated context."""
if context_path not in self._contexts:
return

ctx = self._contexts[context_path]
mode = ctx.get("mode")

if mode == "shadow":
manager = ctx.get("manager")
if isinstance(manager, ShadowRepo):
manager.cleanup()

elif mode == "worktree":
manager = ctx.get("manager")
isolated_path = ctx.get("isolated_path")
if isinstance(manager, WorktreeManager) and isolated_path:
try:
manager.remove_worktree(isolated_path, force=True, delete_branch=True)
except Exception as e:
log.warning(f"Failed to cleanup worktree {isolated_path}: {e}")

del self._contexts[context_path]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

_cleanup_single_context does not handle "workspace" mode — branches may leak.

cleanup / cleanup_all delegates to _cleanup_single_context, which only handles "shadow" and "worktree" modes. For "workspace" contexts (created by setup_workspace_scratch), the branch is not switched back to the default, nor is it deleted. Compare with cleanup_round (line 576–596) and cleanup_session (line 625–637), which both handle workspace mode explicitly.

Since __exit__ calls cleanup_all, using the context manager will leak workspace branches.

Proposed fix: add workspace handling to `_cleanup_single_context`
     def _cleanup_single_context(self, context_path: str) -> None:
         """Cleanup a single isolated context."""
         if context_path not in self._contexts:
             return
 
         ctx = self._contexts[context_path]
         mode = ctx.get("mode")
 
         if mode == "shadow":
             manager = ctx.get("manager")
             if isinstance(manager, ShadowRepo):
                 manager.cleanup()
 
         elif mode == "worktree":
             manager = ctx.get("manager")
             isolated_path = ctx.get("isolated_path")
             if isinstance(manager, WorktreeManager) and isolated_path:
                 try:
                     manager.remove_worktree(isolated_path, force=True, delete_branch=True)
                 except Exception as e:
                     log.warning(f"Failed to cleanup worktree {isolated_path}: {e}")
 
+        elif mode == "workspace":
+            branch_name = ctx.get("branch_name")
+            try:
+                from git import Repo
+
+                repo = Repo(context_path)
+                for name in ("main", "master"):
+                    if name in [b.name for b in repo.branches]:
+                        repo.git.checkout(name)
+                        break
+                if branch_name:
+                    repo.git.branch("-D", branch_name)
+            except Exception as e:
+                log.warning(f"Failed to cleanup workspace {context_path}: {e}")
+
         del self._contexts[context_path]
         log.info(f"Cleaned up isolated context: {context_path}")
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 798-798: Do not catch blind exception: Exception

(BLE001)

Comment on lines +217 to +241
def update_context_path_permission(self, path_str: str, new_permission: str) -> bool:
"""Update the permission of a context path.

Args:
path_str: The context path to update
new_permission: New permission value ("read" or "write")

Returns:
True if the path was found and updated, False otherwise
"""
resolved = Path(path_str).resolve()
try:
perm = Permission(new_permission.lower())
except ValueError:
logger.warning(f"[PathPermissionManager] Invalid permission '{new_permission}', ignoring update")
return False

for mp in self.managed_paths:
if mp.path.resolve() == resolved and mp.path_type == "context":
mp.permission = perm
mp.will_be_writable = perm == Permission.WRITE
self._permission_cache.clear()
logger.info(f"[PathPermissionManager] Updated context path permission: {path_str} -> {new_permission}")
return True
return False
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "update_context_path_permission" -S

Repository: massgen/MassGen

Length of output: 355


🏁 Script executed:

# First, let's examine the file structure and find the class definition
head -250 massgen/filesystem_manager/_path_permission_manager.py | tail -100

Repository: massgen/MassGen

Length of output: 4462


🏁 Script executed:

# Search for context_write_access_enabled usage
rg -n "context_write_access_enabled" -A 2 -B 2

Repository: massgen/MassGen

Length of output: 24215


🏁 Script executed:

# Look for the full PathPermissionManager class to understand context_write_access_enabled
fd "_path_permission_manager.py" -x wc -l {}

Repository: massgen/MassGen

Length of output: 173


🏁 Script executed:

# Look at the call site in content_modals.py
sed -n '450,490p' massgen/frontend/displays/textual/widgets/modals/content_modals.py

Repository: massgen/MassGen

Length of output: 2134


🏁 Script executed:

# Also check the get_permission method to see if it enforces context_write_access_enabled
grep -n "def get_permission" massgen/filesystem_manager/_path_permission_manager.py

Repository: massgen/MassGen

Length of output: 178


🏁 Script executed:

# Read the get_permission implementation
sed -n '330,420p' massgen/filesystem_manager/_path_permission_manager.py

Repository: massgen/MassGen

Length of output: 4123


🏁 Script executed:

# Get the full get_permission method implementation
sed -n '541,600p' massgen/filesystem_manager/_path_permission_manager.py

Repository: massgen/MassGen

Length of output: 3377


🏁 Script executed:

# Let's see more of the get_permission method
sed -n '541,650p' massgen/filesystem_manager/_path_permission_manager.py

Repository: massgen/MassGen

Length of output: 5802


🏁 Script executed:

# Look for _validate_write_tool to see if it has a secondary guard
grep -n "_validate_write_tool" massgen/filesystem_manager/_path_permission_manager.py

Repository: massgen/MassGen

Length of output: 325


🏁 Script executed:

# Check the actual implementation of _validate_write_tool
sed -n '1140,1210p' massgen/filesystem_manager/_path_permission_manager.py

Repository: massgen/MassGen

Length of output: 4145


Enforce context_write_access_enabled guard in update_context_path_permission.

This method can grant WRITE access even when context_write_access_enabled is False, bypassing the coordination-phase isolation enforced by add_context_paths(). The design intent is that during coordination (when the flag is False), all context paths remain read-only. While will_be_writable is correctly preserved, the actual mp.permission is set without checking the flag, allowing tool validation to grant immediate write access.

Recommended: Prevent setting Permission.WRITE when context_write_access_enabled is False, or verify that the UI caller enforces write-access ordering before invoking this method.

Suggested approach
-                mp.permission = perm
-                mp.will_be_writable = perm == Permission.WRITE
+                effective_perm = perm
+                if perm == Permission.WRITE and not self.context_write_access_enabled:
+                    effective_perm = Permission.READ
+                mp.permission = effective_perm
+                mp.will_be_writable = perm == Permission.WRITE
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def update_context_path_permission(self, path_str: str, new_permission: str) -> bool:
"""Update the permission of a context path.
Args:
path_str: The context path to update
new_permission: New permission value ("read" or "write")
Returns:
True if the path was found and updated, False otherwise
"""
resolved = Path(path_str).resolve()
try:
perm = Permission(new_permission.lower())
except ValueError:
logger.warning(f"[PathPermissionManager] Invalid permission '{new_permission}', ignoring update")
return False
for mp in self.managed_paths:
if mp.path.resolve() == resolved and mp.path_type == "context":
mp.permission = perm
mp.will_be_writable = perm == Permission.WRITE
self._permission_cache.clear()
logger.info(f"[PathPermissionManager] Updated context path permission: {path_str} -> {new_permission}")
return True
return False
def update_context_path_permission(self, path_str: str, new_permission: str) -> bool:
"""Update the permission of a context path.
Args:
path_str: The context path to update
new_permission: New permission value ("read" or "write")
Returns:
True if the path was found and updated, False otherwise
"""
resolved = Path(path_str).resolve()
try:
perm = Permission(new_permission.lower())
except ValueError:
logger.warning(f"[PathPermissionManager] Invalid permission '{new_permission}', ignoring update")
return False
for mp in self.managed_paths:
if mp.path.resolve() == resolved and mp.path_type == "context":
effective_perm = perm
if perm == Permission.WRITE and not self.context_write_access_enabled:
effective_perm = Permission.READ
mp.permission = effective_perm
mp.will_be_writable = perm == Permission.WRITE
self._permission_cache.clear()
logger.info(f"[PathPermissionManager] Updated context path permission: {path_str} -> {new_permission}")
return True
return False
🤖 Prompt for AI Agents
In `@massgen/filesystem_manager/_path_permission_manager.py` around lines 217 -
241, The update_context_path_permission method currently sets mp.permission to
Permission.WRITE without checking the coordination-phase flag; enforce the
context_write_access_enabled guard so WRITE is not applied immediately when
self.context_write_access_enabled is False (preserve will_be_writable but keep
mp.permission read-only), i.e. in update_context_path_permission validate
Permission.WRITE against self.context_write_access_enabled and only set
mp.permission to WRITE when the flag is True (otherwise leave mp.permission
unchanged and only set mp.will_be_writable), mirroring the coordination behavior
used in add_context_paths and ensuring managed_paths, Permission, mp.permission
and mp.will_be_writable semantics are preserved.

Comment on lines +2572 to 2577
# Load user settings for theme preference
user_settings = get_user_settings()
theme = user_settings.theme
if theme not in self.PALETTE_MAP:
theme = "dark"
combined_css_path = self._get_combined_css_path(theme)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Align combined CSS with the effective theme (explicit overrides are ignored today).

TextualApp.__init__ builds the combined CSS using user_settings.theme, which can diverge from display.theme when a caller explicitly passes a theme. That creates a mismatch between the palette in CSS and the theme/classes later applied in on_mount, producing inconsistent colors. Use the already-resolved display.theme (fallback to user settings if needed) so CSS and runtime theme stay aligned.

🛠️ Suggested fix
-            # Load user settings for theme preference
-            user_settings = get_user_settings()
-            theme = user_settings.theme
+            # Use the effective theme (explicit override wins)
+            theme = display.theme or get_user_settings().theme
             if theme not in self.PALETTE_MAP:
                 theme = "dark"
🤖 Prompt for AI Agents
In `@massgen/frontend/displays/textual_terminal_display.py` around lines 2572 -
2577, The combined CSS is built from user_settings.theme which can diverge from
the resolved display.theme passed by callers; update TextualApp.__init__ to
determine the effective theme by using display.theme if set (fallback to
get_user_settings().theme), validate against self.PALETTE_MAP, and then call
self._get_combined_css_path(effective_theme) instead of using
user_settings.theme so the CSS palette matches the runtime theme applied in
on_mount; adjust references to theme variable accordingly (look for
TextualApp.__init__, get_user_settings, self.PALETTE_MAP,
_get_combined_css_path, and on_mount).

Comment on lines +43 to +52
target_path = os.path.abspath(target_path)

try:
# git worktree add -b <branch> <path> <base_commit>
self.repo.git.worktree("add", "-b", branch_name, target_path, base_commit)
logger.info(f"Created worktree at {target_path} on branch {branch_name}")
return target_path
except GitCommandError as e:
logger.error(f"Failed to create worktree: {e.stderr}")
raise RuntimeError(f"Failed to create worktree: {e.stderr}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Chain the original exception for debuggability.

Both create_worktree and remove_worktree catch GitCommandError and re-raise as RuntimeError, but without from e the original traceback is lost. This makes debugging harder when worktree operations fail.

Proposed fix
         except GitCommandError as e:
             logger.error(f"Failed to create worktree: {e.stderr}")
-            raise RuntimeError(f"Failed to create worktree: {e.stderr}")
+            raise RuntimeError(f"Failed to create worktree: {e.stderr}") from e
         except GitCommandError as e:
             logger.error(f"Failed to remove worktree: {e.stderr}")
-            raise RuntimeError(f"Failed to remove worktree: {e.stderr}")
+            raise RuntimeError(f"Failed to remove worktree: {e.stderr}") from e

Also applies to: 66-78

🧰 Tools
🪛 Ruff (0.14.14)

[warning] 49-49: Consider moving this statement to an else block

(TRY300)


[warning] 51-51: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


[warning] 52-52: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


[warning] 52-52: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In `@massgen/infrastructure/worktree_manager.py` around lines 43 - 52, The
exception handlers in create_worktree and remove_worktree currently catch
GitCommandError and re-raise a RuntimeError without preserving the original
traceback; update both exception raises to chain the original exception using
"raise RuntimeError(f'Failed to create/remove worktree: {e.stderr}') from e"
(keep the existing logger.error calls that reference e.stderr) so the original
GitCommandError is preserved for debugging while still normalizing the error
type.

@Henry-811 Henry-811 merged commit 7c7d748 into main Feb 6, 2026
24 checks passed
This was referenced Feb 11, 2026
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.

[FEATURE] Fix subagent timeout

2 participants