Skip to content

feat: New answer fairness, log analysis#868

Closed
ncrispino wants to merge 6 commits intomainfrom
even_execute_time
Closed

feat: New answer fairness, log analysis#868
ncrispino wants to merge 6 commits intomainfrom
even_execute_time

Conversation

@ncrispino
Copy link
Collaborator

@ncrispino ncrispino commented Feb 9, 2026

PR Title Format

Your PR title must follow the format: <type>: <brief description>

Valid types:

  • fix: - Bug fixes
  • feat: - New features
  • breaking: - Breaking changes
  • docs: - Documentation updates
  • refactor: - Code refactoring
  • test: - Test additions/modifications
  • chore: - Maintenance tasks
  • perf: - Performance improvements
  • style: - Code style changes
  • ci: - CI/CD configuration changes

Examples:

  • fix: resolve memory leak in data processing
  • feat: add export to CSV functionality
  • breaking: change API response format
  • docs: update installation guide

Description

Brief description of the changes in this PR

Type of change

  • Bug fix (fix:) - Non-breaking change which fixes an issue
  • New feature (feat:) - Non-breaking change which adds functionality
  • Breaking change (breaking:) - Fix or feature that would cause existing functionality to not work as expected
  • Documentation (docs:) - Documentation updates
  • Code refactoring (refactor:) - Code changes that neither fix a bug nor add a feature
  • Tests (test:) - Adding missing tests or correcting existing tests
  • Chore (chore:) - Maintenance tasks, dependency updates, etc.
  • Performance improvement (perf:) - Code changes that improve performance
  • Code style (style:) - Changes that do not affect the meaning of the code (formatting, missing semi-colons, etc.)
  • CI/CD (ci:) - Changes to CI/CD configuration files and scripts

Checklist

  • I have run pre-commit on my changed files and all checks pass
  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Pre-commit status

# Paste the output of running pre-commit on your changed files:
# uv run pre-commit install
# git diff --name-only HEAD~1 | xargs uv run pre-commit run --files # for last commit
# git diff --name-only origin/<base branch>...HEAD | xargs uv run pre-commit run --files # for all commits in PR
# git add <your file> # if any fixes were applied
# git commit -m "chore: apply pre-commit fixes"
# git push origin <branch-name>

How to Test

Add test method for this PR.

Test CLI Command

Write down the test bash command. If there is pre-requests, please emphasize.

Expected Results

Description/screenshots of expected results.

Additional context

Add any other context about the PR here.

Summary by CodeRabbit

  • New Features

    • Added analysis mode with dev/user profiles for examining agent interactions
    • Introduced parallel personas generation for agent coordination
    • Expanded skills installation to include Anthropic/OpenAI/Vercel collections, Agent Browser, and Crawl4AI
    • Added in-app skills management modal UI
    • Implemented fairness coordination controls to balance agent pacing
  • Documentation

    • Updated quickstart guides with expanded skills installation options
    • Added Docker overlay workflow for official MCP runtime images
    • Documented new fairness and coordination configuration parameters
  • Bug Fixes

    • Improved context path handling to preserve non-git paths during synchronization
    • Enhanced workspace cleanup robustness with retry logic

@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

📝 Walkthrough

Walkthrough

This PR introduces fairness-based pacing controls for multi-agent coordination, expands MCP/skills support across SDK and Codex backends with skill discovery and synchronization, adds an analysis mode and persona-aware coordination to the UI, and extends Docker deployment with overlay images and agent-browser integration.

Changes

Cohort / File(s) Summary
Documentation & Schema
CLAUDE.md, README.md, README_PYPI.md, docs/source/quickstart/..., docs/source/reference/yaml_schema.rst, massgen/configs/README.md, massgen/docker/README.md
Expanded documentation for fairness defaults, MCP integration paths, skills installation (Anthropic/OpenAI/Vercel collections, Agent Browser, Crawl4AI), analysis mode, and Docker overlay image guidance. Added YAML schema documentation for fairness_enabled, fairness_lead_cap_answers, and max_midstream_injections_per_round parameters.
Core Configuration Models
massgen/agent_config.py
Added public config fields: voting_threshold, fairness_enabled (default True), fairness_lead_cap_answers (default 2), max_midstream_injections_per_round (default 2) with serialization/deserialization in to_dict/from_dict.
Backend Parameter Exclusion
massgen/api_params_handler/_api_params_handler_base.py, massgen/backend/base.py
Extended exclusion sets to include voting_threshold, fairness_enabled, fairness_lead_cap_answers, and max_midstream_injections_per_round, preventing these orchestrator-only parameters from being passed to APIs.
Backend MCP & Skills Integration
massgen/backend/claude_code.py
Enabled SDK MCP support, introduced skill discovery (_resolve_skill_source) and synchronization (_sync_skills_into_claude_home), integrated skill discovery into options building, added workflow/MCP tool capture and logging, enhanced state reset/disconnect handling for anyio cancellation, reverted tool filter to return Skill instead of Task.
Codex Skills Synchronization
massgen/backend/codex.py
Added skill discovery (_resolve_codex_skills_source) and synchronization (_sync_skills_into_codex_home) with workspace-wide .codex directory setup, extended manifest wiring for checklist_specs.json and AGENTS.md, hardened cleanup to remove new files and handle legacy paths.
CLI & Configuration Builder
massgen/cli.py, massgen/config_builder.py
Added analysis mode support with get_log_analysis_prompt_prefix, wired new orchestrator flags (voting_threshold, fairness controls, checklist options, injection/voting behaviors), introduced install_skills_now flow with skill installation hooks, extended planning/multi-turn workflows with analysis context and persona generation.
Configuration Validation
massgen/config_validator.py
Expanded VALID_VOTING_SENSITIVITY to include "checklist", "checklist_scored", "checklist_gated"; added orchestrator-level validations for fairness_enabled (boolean), fairness_lead_cap_answers (non-negative integer), and max_midstream_injections_per_round (positive integer).
Frontend Core & Modes
massgen/frontend/displays/tui_modes.py, massgen/frontend/displays/textual_terminal_display.py
Added AnalysisProfile type alias, AnalysisConfig dataclass with profile/log_dir/turn/enabled_skills, extended TuiModeState with analysis_config and parallel_personas_enabled flag. Major expansion of textual_terminal_display with analysis-mode flows, persona generation support, skill creation workflows from analysis results, session state persistence, and new public method set_agent_personas.
UI Widgets & Modals
massgen/frontend/displays/textual_widgets/mode_bar.py, massgen/frontend/displays/textual_widgets/plan_options.py, massgen/frontend/displays/textual_widgets/tab_bar.py, massgen/frontend/displays/textual_widgets/modals/skills_modals.py
Extended plan mode cycle to include "analysis" phase, introduced personas toggle. Added analysis-related state to PlanOptionsPopover (profile, log options, turn options, preview). Generalized assignment system in AgentTabBar and SessionInfoWidget to support "Persona" labels alongside "Subtask". Introduced SkillsModal and SkillConfirmModal for skill selection and creation workflows.
UI Setup & Quickstart
massgen/frontend/displays/textual_widgets/quickstart_wizard.py, massgen/frontend/displays/textual_widgets/setup_wizard.py
Added SkillsInstallStep to wizard flow for in-place skill package selection/installation with live status. Generalized setup_wizard installer mapping for anthropic, openai, vercel, agent_browser, and crawl4ai packages with conditional openskills CLI installation.
UI Theme & Styling
massgen/frontend/displays/textual_themes/base.tcss
Added ModeToggle.state-analysis styling with warning color and bold text. Introduced comprehensive Skills Modal styling (container, header, summary, scroll, actions, confirm sections). Reworked PlanOptionsPopover layout (docking to right, increased width/height) and added analysis-specific content sections (plan_details, analysis_target_meta, analysis_preview).
UI Exports & Helpers
massgen/frontend/displays/textual_widgets/__init__.py, massgen/frontend/displays/textual_widgets/modals/__init__.py, massgen/frontend/displays/textual/__init__.py, massgen/frontend/displays/textual_widgets/shortcuts_modal.py, massgen/frontend/displays/task_plan_support.py, massgen/frontend/interactive_controller.py
Exposed SkillsModal, SkillConfirmModal, and new events (AnalysisProfileChanged, AnalysisTargetChanged, OpenSkillsRequested, ViewAnalysisRequested). Updated shortcuts to include k for Skills manager and /skills command. Enhanced task_plan_support with _safe_parse_mapping and _extract_structured_result for robust tool result parsing. Added /skills command handler in SlashCommandDispatcher.
Web API
massgen/frontend/web/server.py
Extended list_skills endpoint to return additional "packages" field with installed package status. Expanded installSkillPackage endpoint to accept openai, vercel, agent_browser in addition to anthropic and crawl4ai with dynamic openskills_installers mapping.
Docker & Deployment
massgen/docker/Dockerfile, massgen/docker/Dockerfile.sudo, massgen/docker/Dockerfile.overlay, massgen/docker/Dockerfile.overlay.sudo, massgen/docker/build_overlay_from_official.sh
Added agent-browser to global npm installs, introduced PLAYWRIGHT_BROWSERS_PATH environment variable for browser cache. Created overlay Dockerfile variants (standard and sudo) for building on official MCP runtime images. Added build_overlay_from_official.sh script for building overlays with customizable tags, names, and base images.
Filesystem & Utilities
massgen/filesystem_manager/_filesystem_manager.py, massgen/filesystem_manager/_docker_manager.py
Modified setup_orchestration_paths to preserve non-git context_paths (e.g., log/session dirs) while collecting git-based suppressed repos. Simplified clear_temp_workspace with single shutil.rmtree call and recovery logic. Added PLAYWRIGHT_BROWSERS_PATH environment variable to Docker container creation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • PR #820: Overlapping modifications to TUI/plan-execution and skills integration code including textual widgets, plan_options, textual_terminal_display, and backend skill sync logic affecting the same files and integration points.
  • PR #787: Both PRs modify get_base_excluded_config_params in massgen/backend/base.py to add coordinator/backend-only configuration parameters, creating direct code-level dependency.
  • PR #783: Both PRs modify ClaudeCodeBackend and backend streaming/tool-handling code in massgen/backend/claude_code.py affecting MCP integration and execution flows.

Suggested reviewers

  • Henry-811
  • a5507203
  • sonichi
🚥 Pre-merge checks | ✅ 3 | ❌ 3
❌ Failed checks (3 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description contains only the template boilerplate with unchecked checkboxes and placeholder sections. No actual description of changes, testing approach, or context is provided. Fill in the Description section with concrete details about the changes, mark the appropriate Type of change checkbox, complete the Checklist items, provide pre-commit status output, and add testing information and context.
Documentation Updated ⚠️ Warning PR introduces substantial new features but has multiple documentation gaps including inconsistent capability naming, missing docstrings, and no example configs with fairness parameters. Update capabilities.py to use 'Skill' instead of 'Task', add Google-style docstrings to cli.py functions, add Args sections to plan_options.py message classes, add docstring to SkillsInstallStep.compose(), and update example YAML configs with fairness parameters.
Capabilities Registry Check ⚠️ Warning Backend changes in claude_code.py return ["Skill"] but capabilities.py still contains ["Task"], creating a mismatch between implementation and registry. Update massgen/backend/capabilities.py lines 220-235 to replace "Task" with "Skill" in builtin_tools for claude_code backend.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: New answer fairness, log analysis' accurately describes the main changes—it introduces fairness controls for coordinating multiple agents and log analysis features.
Docstring Coverage ✅ Passed Docstring coverage is 86.80% which is sufficient. The required threshold is 80.00%.
Config Parameter Sync ✅ Passed Both required files have been updated with all four new YAML parameters in their exclusion sets.

✏️ 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 even_execute_time

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: 20

Caution

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

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

1088-1133: ⚠️ Potential issue | 🟡 Minor

Add Google-style docstring for on_wizard_complete.

This method now produces a structured result payload (skills + Docker artifacts). Please update its docstring to Google style so the API docs stay accurate.

Proposed docstring update
-    """Save the API keys to the selected location."""
+    """Save API keys and finalize setup.
+
+    Returns:
+        Dict[str, Any]: Result payload containing setup status and artifacts.
+    """

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

massgen/config_builder.py (1)

5072-5161: ⚠️ Potential issue | 🟡 Minor

Surface new fairness_ parameters in the interactive wizard/validation.*

You now inject fairness_enabled, fairness_lead_cap_answers, and max_midstream_injections_per_round into quickstart configs, but the full interactive wizard path doesn’t expose or document them. Please add prompts/defaults and validation/help text so non‑quickstart configs remain consistent.

Based on learnings: Update massgen/config_builder.py to add new YAML parameters to interactive wizard, parameter validation, and help text when new configuration options are added.

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

375-478: ⚠️ Potential issue | 🟡 Minor

Add Google-style docstrings for new persona helpers.

set_parallel_personas_enabled, _update_coordination_aux_controls, and get_parallel_personas_enabled need Args/Returns sections.

Proposed docstring updates
     def set_parallel_personas_enabled(self, enabled: bool) -> None:
-        """Set the parallel persona toggle state."""
+        """Set the parallel persona toggle state.
+
+        Args:
+            enabled: True to enable personas, False to disable.
+        """
         if self._persona_toggle:
             self._persona_toggle.set_state("on" if enabled else "off")

     def _update_coordination_aux_controls(self, mode: str) -> None:
-        """Update controls that depend on coordination mode."""
+        """Update controls that depend on coordination mode.
+
+        Args:
+            mode: The current coordination mode ("parallel" or "decomposition").
+        """
         if not self._subtasks_btn:
             pass
         elif mode == "decomposition":
             self._subtasks_btn.remove_class("hidden")
         else:
             self._subtasks_btn.add_class("hidden")

     def get_parallel_personas_enabled(self) -> bool:
-        """Get current parallel persona toggle state."""
+        """Get current parallel persona toggle state.
+
+        Returns:
+            True if parallel personas are enabled.
+        """
         return self._persona_toggle.get_state() == "on" if self._persona_toggle else False

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

massgen/agent_config.py (1)

226-240: ⚠️ Potential issue | 🟡 Minor

Document voting_threshold in AgentConfig args.

voting_threshold is now a public field but it isn’t listed in the Args section. Please add it so API docs stay complete.

💡 Suggested docstring update
@@
-        voting_sensitivity: Controls how critical agents are when voting ("lenient", "balanced", "strict")
+        voting_sensitivity: Controls how critical agents are when voting ("lenient", "balanced", "strict")
+        voting_threshold: Optional numeric threshold for ROI-style voting (e.g., 15 = 15% improvement required)

As per coding guidelines: **/*.py: Use Google-style docstrings for all Python modules and functions to enable auto-generation of API documentation.

massgen/cli.py (1)

5369-5473: ⚠️ Potential issue | 🟡 Minor

Add Google‑style docstrings for the new nested helpers.

_merge_readonly_context_path and _agents_have_context_path (Lines 5371 and 5407) should include Google‑style Args/Returns sections.

Proposed fix
             def _merge_readonly_context_path(config_dict: Dict[str, Any], path_str: str, description: str) -> bool:
-                """Add a read-only orchestrator context path if missing."""
+                """Add a read-only orchestrator context path if missing.
+
+                Args:
+                    config_dict: Config dict to update in-place.
+                    path_str: Path to add as a context path.
+                    description: Human-readable description for the path.
+
+                Returns:
+                    True if the path was added; otherwise False.
+                """
                 if not path_str:
                     return False
@@
             def _agents_have_context_path(current_agents: Optional[Dict[str, Any]], path_str: str) -> bool:
-                """Check whether all active agents already have a specific context path."""
+                """Check whether all active agents already have a specific context path.
+
+                Args:
+                    current_agents: Active agents (or None).
+                    path_str: Path to verify.
+
+                Returns:
+                    True if all agents already have the path; otherwise False.
+                """
                 if not current_agents:
                     return False

As per coding guidelines: **/*.py: Use Google-style docstrings for all Python modules and functions to enable auto-generation of API documentation

🤖 Fix all issues with AI agents
In `@docs/source/quickstart/installation.rst`:
- Around line 187-192: Add a minimal runnable example and expected output for
the new Skills step in the quickstart note: show the exact command "uv run
massgen --quickstart" (and an example using "--setup-skills" for
retry/pre-install), include a short terminal transcript demonstrating the Skills
step prompts and on-page status updates (e.g., selection prompt, "Installing
Anthropic...", "Installed: Anthropic", success summary), and append a brief
bullet in docs/source/index.rst under "Recent Releases" announcing the
Skills-step feature and linking to the quickstart page; update the note text in
installation.rst (the uv run massgen --quickstart block) to include this
runnable snippet and the expected output example.

In `@massgen/backend/claude_code.py`:
- Around line 710-716: The same-path guard currently swallows OSError and
proceeds to copy, risking self-overwrite; in the try/except around
source.resolve() == dest.resolve() update the except OSError branch to log the
caught exception (including which paths failed) and return early instead of
continuing, so the subsequent copytree/copy2 logic is not executed when
resolve() fails for either source or dest; reference the existing
source.resolve(), dest.resolve(), and the copytree/copy2 call sites when making
this change.
- Around line 564-570: The current loop that calls task.uncancel() over
asyncio.all_tasks() is too broad; instead, capture the specific coordination
task (e.g., the current_task via asyncio.current_task() or the known
coordination Task object used by the coordination loop) before invoking
disconnect(), and after disconnect only check/uncancel that single task (check
its cancelling() > 0 then call uncancel()) rather than iterating all tasks;
apply this same targeted-uncancel pattern in the disconnect() code path
referenced (the block around the disconnect() implementation where all_tasks()
is used).
- Around line 749-751: The claude_code backend now advertises
get_supported_builtin_tools() returning ["Skill"], but the capabilities registry
still lists "Task"; update the claude_code entry in
massgen/backend/capabilities.py to change its builtin_tools list to include
"Skill" (replace "Task" with "Skill" or add "Skill" if preserving others) so the
registry matches the get_supported_builtin_tools implementation and the
backend's supported tools.

In `@massgen/backend/codex.py`:
- Around line 649-659: The cleanup currently unconditionally removes a
workspace-root AGENTS.md (agents_md) which can delete user files; update the
removal logic so you only unlink the workspace-root AGENTS.md if it contains a
MassGen generation marker (or other provenance check) — e.g., open
Path(self.cwd)/"AGENTS.md" and scan for a specific marker string or timestamp
signature you add when generating files; if the marker is present, unlink
agents_md, otherwise leave it and optionally log/emit a warning. Ensure this
check is done where agents_md is defined and keep the existing config_dir
cleanup unchanged.

In `@massgen/cli.py`:
- Around line 115-139: Update the docstring for _quickstart_config_uses_skills
to use Google-style format: include an "Args" section documenting the
config_path: Optional[str] parameter and its meaning, and a "Returns" section
describing the boolean result (True when the config enables coordination skills,
False otherwise). Keep the existing one-line summary, add the Args and Returns
blocks, and ensure typing and brief descriptions match the function behavior.
- Around line 141-159: Update the _ensure_quickstart_skills_ready function
docstring to Google style: add an "Args:" section describing config_path
(Optional[str]) and install_requested (bool, default True) and a "Returns:"
section stating it returns bool indicating whether skill installation succeeded
or was skipped; keep the short summary line ("Install quickstart skill packages
when generated config enables skills.") and ensure types and default behavior
are documented for clarity in tooling and code reviews.
- Around line 5547-5587: The cleanup call for agent backends is not best-effort:
wrap the await ag.backend.__aexit__(None, None, None) in a try/except to catch
any Exception and log it (similar to the existing filesystem_manager.cleanup()
handling) so a failing backend __aexit__ doesn’t abort the whole recreate flow;
keep using logger.warning (include agent id aid and the exception) and continue
the loop so agents are still recreated and context.agents is replaced with the
new_agents.

In `@massgen/docker/build_overlay_from_official.sh`:
- Around line 95-99: The script calls docker build using DOCKERFILE and assumes
the current working directory is the repo root; add a preflight check to verify
the Dockerfile exists (test -f "${DOCKERFILE}") and fail with a clear error if
not found, or resolve DOCKERFILE relative to the script directory (using the
script's dirname) before invoking docker build so that the build context and
Dockerfile path are valid; update the block that uses DOCKERFILE, BASE_IMAGE,
IMAGE_NAME, and TAG to perform this validation and exit early on failure.
- Around line 20-66: The option handlers in the while/case loop (for -t|--tag,
-n|--name, --base-image, --base-image-sudo) currently assign "$2" blindly which
allows missing-argument cases to set empty TAG, IMAGE_NAME, BASE_IMAGE_STANDARD
or BASE_IMAGE_SUDO; update each handler in the while/case block to validate that
a non-empty next argument exists and does not begin with '-' before assigning,
and if the check fails print a clear error (or usage via the existing help text)
and exit nonzero so missing values are caught immediately.

In `@massgen/filesystem_manager/_docker_manager.py`:
- Around line 604-606: The code unconditionally overwrites
PLAYWRIGHT_BROWSERS_PATH in env_vars; change the assignment in
_docker_manager.py so you only set a default when the user hasn't provided one
(e.g., use env_vars.setdefault("PLAYWRIGHT_BROWSERS_PATH",
"/home/massgen/.cache/ms-playwright")) to avoid clobbering values loaded by
_build_environment() including hermetic values like "0".

In `@massgen/filesystem_manager/_filesystem_manager.py`:
- Around line 433-438: The log call in FilesystemManager uses "{}" placeholders
which Python's logging won't interpolate; replace the logger.info invocation so
it formats the message (preferably using an f-string to match the rest of the
file) and include the actual lengths of suppressed_repo_paths, context_paths,
and extra_mount_paths (refer to the logger.info call and the variables
suppressed_repo_paths, context_paths, extra_mount_paths) so the message outputs
the real counts.
- Around line 2056-2060: The try/except around
self.agent_temporary_workspace_parent.mkdir(...) currently swallows errors;
change it to catch Exception as e and log a warning including the exception
details so failures to recreate the directory are visible. Use the class logger
(self.logger.warn/ warning) if available, otherwise get a module logger
(logging.getLogger(__name__)) and log a message like "Failed to recreate
agent_temporary_workspace_parent" with the exception info; keep the existing
behavior of not raising after logging. Ensure you reference
self.agent_temporary_workspace_parent.mkdir and the class logger (self.logger)
in the change.

In `@massgen/frontend/displays/textual_terminal_display.py`:
- Around line 4620-4775: The docstrings for the new persona helper methods
(_get_persona_runtime_subagent, _open_persona_runtime_subagent_screen,
_persona_subagent_events_ready, _auto_open_persona_runtime_subagent_screen) are
missing Google-style "Args:" and "Returns:" sections; update each function's
docstring to include an "Args:" entry describing parameters (e.g., subagent_id:
str, auto_return_on_completion: bool, attempt: int) and a "Returns:" entry
describing the return type/meaning (e.g., Optional[Any], bool, None) following
Google-style formatting so the docstrings comply with the project's Python
docstring guideline.
- Around line 9062-9204: Add Google-style docstrings with Args: and Returns: to
the listed helper methods (_normalize_skill_name_list, _collect_skill_inventory,
_slugify_skill_name, _extract_skill_candidate_from_answer,
_offer_skill_creation_from_analysis). For each function include a one-line
summary, an Args: section documenting parameter names and types (e.g.,
skill_names: List[str], answer_text: str), and a Returns: section describing the
return type and meaning (e.g., List[str], Tuple[List[Dict[str, Any]],
List[Dict[str, Any]]], Optional[Tuple[str,str]], or None). Ensure the docstrings
match existing project style (Google style), are placed immediately under each
def, and keep descriptions concise for the private methods named above.
- Around line 6878-7181: Add Google-style docstrings (including Args: and
Returns: blocks) for each new/changed helper method so they meet the project's
docstring standard; update the docstrings for _get_available_log_sessions,
_get_turn_numbers, _ensure_analysis_defaults, _build_analysis_log_options,
_build_analysis_turn_options, _get_analysis_report_path,
_extract_user_query_fragment, _condense_preview, _read_yaml_file,
_get_latest_attempt_dir, _extract_query_preview_from_attempt,
_extract_winner_from_status, _extract_final_answer_preview_from_attempt, and
_build_analysis_preview_text to describe parameters and return types (and
briefly note side effects where applicable) using the Google-style Args/Returns
format.
- Around line 9195-9232: In _offer_skill_creation_from_analysis: prevent silent
overwrites by checking if target_path.exists() before writing; if SKILL.md
already exists, either (a) change the flow to present a confirmation dialog that
explicitly warns about overwriting (reuse or extend SkillConfirmModal to include
an "overwrite" prompt) or (b) abort and notify the user that a skill with that
slug already exists and suggest a different name; implement this check inside
the _on_confirm handler (or before pushing the modal) and only perform
target_dir.mkdir(...) and target_path.write_text(...) after explicit overwrite
confirmation, ensuring notify(...) is used to report the outcome or
cancellation.

In `@massgen/frontend/displays/textual_widgets/plan_options.py`:
- Around line 84-112: Add Google-style docstrings with an "Args:" section to the
__init__ methods of AnalysisProfileChanged, AnalysisTargetChanged, and
ViewAnalysisRequested so they match the project's docs style (like
PlanSelected/PlanDepthChanged); describe each parameter and its type (e.g.,
profile: str; log_dir: Optional[str]; turn: Optional[int] or int) and keep the
brief one-line summary above the Args section, leaving the existing
super().__init__ calls intact.

In `@massgen/frontend/displays/textual_widgets/quickstart_wizard.py`:
- Around line 990-999: The on_checkbox_changed handler must reset the install
state when selections change: inside QuickstartWizard.on_checkbox_changed (which
iterates self._checkboxes and updates self._selected_packages) set
self._install_attempted = False whenever a package is added or removed so
validate() cannot bypass re-install; ensure the flag is cleared before breaking
out of the loop so any change (event.value true or false) resets the install
attempt state.
- Around line 929-1152: Add Google-style docstrings to all newly added/changed
methods in SkillsInstallStep: compose, on_checkbox_changed,
_run_installer_quiet, on_button_pressed, validate, get_value, and set_value. For
each method include a short summary line, Args with parameter types and
descriptions (e.g., event: Checkbox.Changed for on_checkbox_changed, event:
Button.Pressed for on_button_pressed, installer for _run_installer_quiet),
Returns with the return type and meaning (or None), and Raises if applicable;
keep formatting consistent with other project docstrings and place the docstring
immediately below each def declaration (triple-quoted Google-style).
🧹 Nitpick comments (27)
massgen/frontend/displays/textual/widgets/modals/shortcuts_modal.py (1)

20-20: Consider adding Google-style docstrings to compose and on_button_pressed.

Per project guidelines, functions should have Google-style docstrings. These methods currently lack them. As per coding guidelines, "Use Google-style docstrings for all Python modules and functions to enable auto-generation of API documentation."

Also applies to: 83-83

massgen/frontend/displays/task_plan_support.py (2)

50-57: Consider narrowing the exception types in the parse fallback chain.

Ruff flags the bare Exception catches (BLE001). Since json.loads raises json.JSONDecodeError (a ValueError subclass) and ast.literal_eval raises ValueError or SyntaxError, you can tighten these without changing behavior.

♻️ Suggested narrower exceptions
     try:
         parsed = json.loads(text)
-    except Exception:
+    except (ValueError, TypeError):
         try:
             parsed = ast.literal_eval(text)
-        except Exception:
+        except (ValueError, SyntaxError):
             return None

134-141: Redundant isinstance check — _extract_structured_result already guarantees a Dict or None.

_extract_structured_result returns Optional[Dict[str, Any]]. After the if not result_data guard at line 135, result_data is always a dict, making the check on line 140 dead code.

♻️ Remove dead branch
     result_data = _extract_structured_result(result)
     if not result_data:
         result_len = len(result) if isinstance(result, str) else "n/a"
         _log(f"_task_plan: parse error (result length={result_len})")
         return True
 
-    if not isinstance(result_data, dict):
-        return True
-
     tasks: List[Dict[str, Any]] = []
massgen/docker/Dockerfile.overlay (1)

8-25: Pin the overlay base image and npm tools to avoid drift.

Using latest plus unpinned npm installs makes the overlay non-reproducible; consider pinning the base image tag/digest and reusing the same tool versions as the runtime Dockerfile.

📌 Example pinning pattern
-ARG BASE_IMAGE=ghcr.io/massgen/mcp-runtime:latest
+ARG BASE_IMAGE=ghcr.io/massgen/mcp-runtime:<version-or-digest>
...
-RUN npm install -g agent-browser && \
-    (command -v openskills >/dev/null 2>&1 || npm install -g openskills)
+ARG AGENT_BROWSER_VERSION=...
+ARG OPENSKILLS_VERSION=...
+RUN npm install -g agent-browser@${AGENT_BROWSER_VERSION} && \
+    (command -v openskills >/dev/null 2>&1 || npm install -g openskills@${OPENSKILLS_VERSION})
What is the latest stable tag (or digest) for the MassGen MCP runtime image, and the current versions for agent-browser and openskills?
massgen/docker/Dockerfile (1)

67-67: Pin global npm tool versions for reproducible image builds.

Unpinned global installs can change behavior between builds. Pin to specific versions like [email protected], @openai/[email protected], and [email protected] (or use build args for flexibility):

📌 Example pinning pattern
-RUN npm install -g openskills `@openai/codex` agent-browser
+RUN npm install -g \
+    [email protected] \
+    `@openai/codex`@0.28.0 \
+    [email protected]
massgen/frontend/interactive_controller.py (2)

508-514: Missing return after analysis-mode skill creation may cause unintended notifications.

The plan-mode block at line 507 returns early to prevent fallthrough into the was_cancelled/error notification logic below. This analysis-mode block doesn't return, so execution continues to lines 516–519. Currently this is safe because the guard ensures was_cancelled=False and error=None, but it's inconsistent and fragile if downstream logic changes.

Proposed fix
             if mode_state.plan_mode == "analysis" and result.answer_text and not result.was_cancelled and not result.error:
                 analysis_profile = getattr(mode_state.analysis_config, "profile", "dev")
                 if analysis_profile == "user":
                     self._display._call_app_method(
                         "_offer_skill_creation_from_analysis",
                         result.answer_text,
                     )
+                    return

1245-1251: Consider accessing _display via the existing adapter pattern rather than getattr.

The _display attribute is always set in TextualInteractiveAdapter.__init__, so using getattr with no default is equivalent to direct access. This works, but other handlers (e.g., show_help at line 633) call the adapter method directly rather than reaching into the adapter's internals.

For consistency with other non-modal UI actions in this method, consider adding a show_skills method on TextualInteractiveAdapter (like show_help, show_vote, etc.) and calling it from here.

massgen/backend/codex.py (2)

683-710: Skills source resolution looks correct but consider adding Google-style docstrings.

The priority chain (Docker temp → local manager → project .agent/skills → home .agent/skills) is sensible. Per coding guidelines, new functions should include Google-style docstrings with Args and Returns sections.

Suggested docstring
     def _resolve_codex_skills_source(self) -> Optional[Path]:
-        """Resolve the best available skills source directory, if any."""
+        """Resolve the best available skills source directory, if any.
+
+        Checks multiple locations in priority order: Docker temp skills dirs,
+        local skills directory from filesystem manager, project-level
+        ``.agent/skills``, and user-level ``~/.agent/skills``.
+
+        Returns:
+            Path to the skills source directory, or None if no source found.
+        """

As per coding guidelines, **/*.py: Use Google-style docstrings for all Python modules and functions.


728-743: A single file-copy failure aborts the entire sync.

The try/except OSError wraps the entire for entry in source.iterdir() loop, so one bad entry (e.g., a broken symlink or permission error on a single file) halts all remaining copies. If best-effort sync is the intent, move the error handling inside the loop.

Proposed fix
         copied_entries = 0
-        try:
-            for entry in source.iterdir():
+        for entry in source.iterdir():
+            try:
                 target = dest / entry.name
                 if entry.is_dir():
                     shutil.copytree(entry, target, dirs_exist_ok=True)
                     copied_entries += 1
                 elif entry.is_file():
                     shutil.copy2(entry, target)
                     copied_entries += 1
-        except OSError as e:
-            logger.warning(f"Codex skills sync failed from {source} to {dest}: {e}")
-            return
+            except OSError as e:
+                logger.warning(f"Codex skills sync: failed to copy {entry.name}: {e}")
 
         if copied_entries:
             logger.info(f"Codex skills sync: copied {copied_entries} entries from {source} to {dest}")
massgen/backend/claude_code.py (3)

673-699: _resolve_skill_source — docstring is minimal; consider documenting the resolution order.

The method silently walks four distinct locations (SDK-dict temp paths → local skills dir → project .agent/skills → home .agent/skills). A brief Google-style docstring listing the priority order would help future maintainers. As per coding guidelines, Google-style docstrings are required for new functions in **/*.py.


53-53: Redundant import shutil — already imported at module top level (line 53).

Line 155 does import shutil locally inside __init__, but shutil is now also imported at line 53. The local import is dead code.

Also applies to: 155-155


1894-1918: MCP-style workflow tool capture looks correct; minor: import inside hot loop.

The from ..tool.workflow_toolkits.base import WORKFLOW_TOOL_NAMES import is executed on every ToolUseBlock iteration. Since Python caches module imports this is functionally fine, but moving it to module level (or at least outside the for block in message.content loop) would be cleaner.

massgen/docker/Dockerfile.overlay.sudo (2)

13-14: Pin npm package versions for reproducible builds.

agent-browser and openskills are installed without version pins, meaning builds on different days may produce different images. Consider pinning to specific versions (e.g., npm install -g [email protected]).


28-28: tail -f /dev/null keeps the container alive as a workspace — consider adding a HEALTHCHECK.

Without a HEALTHCHECK, orchestrators (Docker Compose, Kubernetes) cannot distinguish a healthy idle container from a hung one. A simple HEALTHCHECK CMD true would suffice if the container is expected to be a long-running workspace.

massgen/agent_config.py (1)

249-257: Consider validating the new numeric fairness/voting fields.

Adding sanity checks (non-negative/positive) for voting_threshold, fairness_lead_cap_answers, and max_midstream_injections_per_round prevents invalid configs from propagating.

♻️ Suggested validation
@@
 class AgentConfig:
@@
     max_midstream_injections_per_round: int = 2
+
+    def __post_init__(self) -> None:
+        self._validate_fairness_config()
+
+    def _validate_fairness_config(self) -> None:
+        if self.voting_threshold is not None and self.voting_threshold <= 0:
+            raise ValueError("voting_threshold must be positive")
+        if self.fairness_lead_cap_answers < 0:
+            raise ValueError("fairness_lead_cap_answers must be >= 0")
+        if self.max_midstream_injections_per_round < 0:
+            raise ValueError("max_midstream_injections_per_round must be >= 0")
massgen/frontend/web/server.py (2)

900-908: Synchronous check_skill_packages_installed() call in async endpoint.

check_skill_packages_installed() performs filesystem I/O (scanning skill directories, reading manifests). Calling it synchronously here blocks the event loop. This is consistent with the existing pattern in this file (e.g., _scan_workspace_files in other endpoints), but for a generally heavier operation involving multiple directory scans it could cause noticeable latency.

Consider wrapping in run_in_executor:

♻️ Suggested improvement
-        from massgen.utils.skills_installer import check_skill_packages_installed
+        from massgen.utils.skills_installer import check_skill_packages_installed
+        import asyncio
+        loop = asyncio.get_running_loop()
+        packages = await loop.run_in_executor(None, check_skill_packages_installed)

         return {
             "skills": skills,
             "builtin_count": len([s for s in skills if s["location"] == "builtin"]),
             "user_count": len([s for s in skills if s["location"] == "user"]),
             "project_count": len([s for s in skills if s["location"] == "project"]),
-            "packages": check_skill_packages_installed(),
+            "packages": packages,
         }

948-1007: Refactored skill installation logic looks good — one edge case to note.

The openskills_installers lookup table and the separated crawl4ai path cleanly extend the endpoint. The docstring accurately reflects the expanded accepted inputs.

Minor note: if package_id is None (i.e., the "package" key is absent from the request), it falls through to line 1004 and returns "Unknown package: None". Consider adding an early validation:

🛡️ Optional: validate package_id early
         package_id = request_data.get("package")
+        if not package_id:
+            return JSONResponse(
+                {"error": "Missing required field: 'package'"},
+                status_code=400,
+            )

Additionally, the installer functions (install_anthropic_skills, install_openai_skills, etc.) are synchronous and perform subprocess calls (e.g., npm install -g openskills). Calling them directly from this async endpoint blocks the event loop. Consider wrapping the installation in loop.run_in_executor(None, installer) — the same pattern already used elsewhere in this file for create_agents_from_config.

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

90-109: Bare Exception catches silently swallow errors — use the specific Textual exception.

Ruff flags S112 and BLE001 here. query_one raises textual.css.query.NoMatches when the selector doesn't match. Catching that specifically is both safer and self-documenting. At minimum, log unexpected errors to avoid masking real bugs.

♻️ Proposed fix
+    from textual.css.query import NoMatches
+
     def _set_all_checkboxes(self, value: bool) -> None:
         """Set all skill checkboxes to the given value."""
         for checkbox_id in self._checkbox_to_name:
             try:
                 cb = self.query_one(f"#{checkbox_id}", Checkbox)
                 cb.value = value
-            except Exception:
+            except NoMatches:
                 continue
 
     def _collect_enabled_names(self) -> List[str]:
         """Collect selected skill names from checkbox state."""
         selected: List[str] = []
         for checkbox_id, name in self._checkbox_to_name.items():
             try:
                 cb = self.query_one(f"#{checkbox_id}", Checkbox)
                 if cb.value:
                     selected.append(name)
-            except Exception:
+            except NoMatches:
                 continue
         return selected

111-125: on_button_pressed overrides BaseModal.on_button_pressed without calling super().

BaseModal.on_button_pressed handles buttons whose IDs start with "close" or equal "cancel_button". Since SkillsModal defines its own on_button_pressed, the parent handler is never reached. This works today because no button IDs here match the parent's patterns, but it's fragile — if a future change adds a close button or renames an ID, the parent logic silently stops working.

Consider calling super() as a fallback:

♻️ Suggested improvement
     def on_button_pressed(self, event: Button.Pressed) -> None:
         if event.button.id == "enable_all_skills_btn":
             self._set_all_checkboxes(True)
             return
         if event.button.id == "disable_all_skills_btn":
             self._set_all_checkboxes(False)
             return
         if event.button.id == "apply_skills_btn":
             self.dismiss(self._collect_enabled_names())
             event.stop()
             return
         if event.button.id == "skills_cancel_button":
             self.dismiss(None)
             event.stop()
             return
+        super().on_button_pressed(event)

20-34: Missing Google-style docstrings on __init__ methods.

Both SkillsModal.__init__ and SkillConfirmModal.__init__ lack docstrings describing their parameters. As per coding guidelines, new or changed functions in **/*.py should include Google-style docstrings.

📝 Example for SkillsModal
     def __init__(
         self,
         *,
         default_skills: List[Dict[str, Any]],
         created_skills: List[Dict[str, Any]],
         enabled_skill_names: Optional[List[str]],
     ) -> None:
+        """Initialize the skills manager modal.
+
+        Args:
+            default_skills: Built-in skills to display.
+            created_skills: User-created or project skills to display.
+            enabled_skill_names: Names of currently enabled skills,
+                or None to treat all as enabled.
+        """
         super().__init__()

As per coding guidelines, **/*.py: "Use Google-style docstrings for all Python modules and functions to enable auto-generation of API documentation."

Also applies to: 128-141

massgen/cli.py (1)

5865-5897: Prefer direct assignment over setattr for enabled_skill_names.

Using setattr with a constant attribute is non‑idiomatic here; direct assignment is clearer.

Proposed fix
-                    setattr(
-                        orchestrator_config.coordination_config,
-                        "enabled_skill_names",
-                        enabled_skill_names,
-                    )
+                    orchestrator_config.coordination_config.enabled_skill_names = enabled_skill_names
massgen/frontend/displays/textual_widgets/plan_options.py (4)

256-266: Incomplete __init__ docstring — new analysis_* parameters are undocumented.

The six new constructor parameters (analysis_profile, analysis_log_options, analysis_selected_log_dir, analysis_turn_options, analysis_selected_turn, analysis_preview_text) are missing from the Args: section.

Proposed addition
         Args:
             plan_mode: Current mode ("normal", "plan", "execute", or "analysis").
             available_plans: List of available plan sessions.
             current_plan_id: Currently selected plan ID.
             current_depth: Current plan depth setting.
             current_broadcast: Current broadcast setting.
+            analysis_profile: Analysis profile to use ("dev" or "user").
+            analysis_log_options: List of (label, value) tuples for log session selection.
+            analysis_selected_log_dir: Currently selected log directory path.
+            analysis_turn_options: List of (label, value) tuples for turn selection.
+            analysis_selected_turn: Currently selected turn number.
+            analysis_preview_text: Preview text for the selected analysis target.
             id: Optional DOM ID.
             classes: Optional CSS classes.

As per coding guidelines, **/*.py: "Use Google-style docstrings for all Python modules and functions to enable auto-generation of API documentation."


370-382: Synchronous filesystem I/O inside compose() may block the UI thread.

Line 377 performs Path(...).exists() synchronously during widget composition. While fast on local filesystems, this couples I/O to the render path. Consider precomputing report_exists when the analysis target changes (e.g., in a handler or show()) and storing it as instance state, so compose() remains pure.


282-290: Add Args and Returns sections to _safe_select_value docstring.

This helper has a one-line summary but no Args: / Returns: sections, which are needed for Google-style docstrings.

Proposed fix
     `@staticmethod`
     def _safe_select_value(options: List[tuple[str, str]], preferred: Optional[str]) -> Optional[str]:
-        """Return a safe select value from options, preferring the given value."""
+        """Return a safe select value from options, preferring the given value.
+
+        Args:
+            options: List of (label, value) tuples to select from.
+            preferred: Preferred value to select if available.
+
+        Returns:
+            The preferred value if present in options, otherwise the first
+            option's value, or None if options is empty.
+        """
         if not options:
             return None

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


2-10: Module docstring is outdated — missing analysis mode.

The module-level docstring lists only plan/execute mode features. Now that the popover also serves as the analysis mode configuration surface, update the docstring to reflect this.

massgen/frontend/displays/textual_widgets/tab_bar.py (2)

124-132: Add Args section to update_assignment docstring.

update_assignment is a new public method but lacks an Args: section. For consistency with update_info and update_subtask, please add one.

Proposed fix
     def update_assignment(self, assignment: Optional[str], kind: str = "Subtask") -> None:
-        """Update the displayed agent assignment label."""
+        """Update the displayed agent assignment label.
+
+        Args:
+            assignment: The assignment text to display, or None to clear.
+            kind: Label for the assignment type (e.g., "Subtask", "Persona").
+        """
         self._assignment = assignment
         self._assignment_kind = kind
         self.refresh()

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


466-479: Add Args sections to set_agent_personas and _set_agent_assignments for consistency.

set_agent_subtasks (line 458) has a full Args: section, but the two new methods lack one. This inconsistency will affect auto-generated API docs.

Proposed fix
     def set_agent_personas(self, personas: Dict[str, str]) -> None:
-        """Set per-agent persona assignments for parallel mode."""
+        """Set per-agent persona assignments for parallel mode.
+
+        Args:
+            personas: Mapping of agent_id to persona summary/description.
+        """
         self._set_agent_assignments(personas, kind="Persona")

     def _set_agent_assignments(self, assignments: Dict[str, str], kind: str) -> None:
-        """Set and render per-agent assignment labels."""
+        """Set and render per-agent assignment labels.
+
+        Args:
+            assignments: Mapping of agent_id to assignment text.
+            kind: Label for the assignment type (e.g., "Subtask", "Persona").
+        """
         self._agent_assignments = assignments
         self._assignment_kind = kind

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

Comment on lines +187 to +192
.. note::

In ``uv run massgen --quickstart``, when Docker mode is selected the wizard includes
a Skills step where you can select package(s) and install them immediately with
on-page status updates (Anthropic/OpenAI/Vercel collections, Agent Browser skill,
and Crawl4AI). Use ``--setup-skills`` to retry or pre-install manually.
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a runnable example + expected output for the new Skills step and surface it in Recent Releases.

This note introduces new quickstart behavior; please add a minimal command + sample output snippet for the Skills step, and update docs/source/index.rst “Recent Releases” if this is a new feature.

✏️ Suggested RST snippet
 .. note::

    In ``uv run massgen --quickstart``, when Docker mode is selected the wizard includes
    a Skills step where you can select package(s) and install them immediately with
    on-page status updates (Anthropic/OpenAI/Vercel collections, Agent Browser skill,
    and Crawl4AI). Use ``--setup-skills`` to retry or pre-install manually.
+
+   Example:
+
+   .. code-block:: bash
+
+      uv run massgen --quickstart
+
+   .. code-block:: text
+
+      [Skills] Installing vercel-labs/agent-browser ... done

Based on learnings and coding guidelines: Documentation for new features must include runnable commands with expected output, and docs/source/index.rst "Recent Releases" should be updated.

📝 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
.. note::
In ``uv run massgen --quickstart``, when Docker mode is selected the wizard includes
a Skills step where you can select package(s) and install them immediately with
on-page status updates (Anthropic/OpenAI/Vercel collections, Agent Browser skill,
and Crawl4AI). Use ``--setup-skills`` to retry or pre-install manually.
.. note::
In ``uv run massgen --quickstart``, when Docker mode is selected the wizard includes
a Skills step where you can select package(s) and install them immediately with
on-page status updates (Anthropic/OpenAI/Vercel collections, Agent Browser skill,
and Crawl4AI). Use ``--setup-skills`` to retry or pre-install manually.
Example:
.. code-block:: bash
uv run massgen --quickstart
.. code-block:: text
[Skills] Installing vercel-labs/agent-browser ... done
🤖 Prompt for AI Agents
In `@docs/source/quickstart/installation.rst` around lines 187 - 192, Add a
minimal runnable example and expected output for the new Skills step in the
quickstart note: show the exact command "uv run massgen --quickstart" (and an
example using "--setup-skills" for retry/pre-install), include a short terminal
transcript demonstrating the Skills step prompts and on-page status updates
(e.g., selection prompt, "Installing Anthropic...", "Installed: Anthropic",
success summary), and append a brief bullet in docs/source/index.rst under
"Recent Releases" announcing the Skills-step feature and linking to the
quickstart page; update the note text in installation.rst (the uv run massgen
--quickstart block) to include this runnable snippet and the expected output
example.

Comment on lines +564 to +570

# anyio's cancel scope propagation may have called task.cancel() on
# parent tasks during disconnect(). Reverse any pending cancellations
# to prevent killing the coordination loop.
for task in asyncio.all_tasks():
if not task.done() and task.cancelling() > 0:
task.uncancel()
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

uncancel() on all event-loop tasks is overly broad and may mask legitimate cancellations.

asyncio.all_tasks() returns every task in the loop — including timeouts, background health checks, and other coordinated cancellations. Blindly uncancelling them all can silently swallow a CancelledError that was raised for a valid reason (e.g., an orchestrator timeout, a user-initiated Ctrl+C propagating through task groups).

A safer approach is to record the current task (or the known coordination task) before calling disconnect() and only uncancel that specific task.

Proposed fix
     async def reset_state(self) -> None:
         ...
         if self._client is not None:
+            current = asyncio.current_task()
             try:
                 await self._client.disconnect()
             except asyncio.CancelledError:
                 pass  # anyio cancel scope may raise CancelledError
             except Exception:
                 pass  # Ignore cleanup errors
 
-            # anyio's cancel scope propagation may have called task.cancel() on
-            # parent tasks during disconnect(). Reverse any pending cancellations
-            # to prevent killing the coordination loop.
-            for task in asyncio.all_tasks():
-                if not task.done() and task.cancelling() > 0:
-                    task.uncancel()
+            # Reverse cancellation only on the task that called disconnect(),
+            # which is the one anyio's cancel scope targeted.
+            if current and not current.done() and current.cancelling() > 0:
+                current.uncancel()

Apply the same pattern in disconnect() (lines 2224-2227).

🤖 Prompt for AI Agents
In `@massgen/backend/claude_code.py` around lines 564 - 570, The current loop that
calls task.uncancel() over asyncio.all_tasks() is too broad; instead, capture
the specific coordination task (e.g., the current_task via
asyncio.current_task() or the known coordination Task object used by the
coordination loop) before invoking disconnect(), and after disconnect only
check/uncancel that single task (check its cancelling() > 0 then call
uncancel()) rather than iterating all tasks; apply this same targeted-uncancel
pattern in the disconnect() code path referenced (the block around the
disconnect() implementation where all_tasks() is used).

Comment on lines +710 to +716

try:
if source.resolve() == dest.resolve():
return
except OSError:
# Continue best-effort copy if either path cannot be resolved.
pass
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same-path check swallows OSError and continues with the copy — could lead to self-overwrite.

If source.resolve() or dest.resolve() raises OSError, the guard is skipped and the loop proceeds to copy source into dest. When source and dest are actually the same directory on a filesystem where resolve() fails (e.g., broken symlink chain), the copytree / copy2 calls may corrupt the files.

Consider logging the exception and returning early instead of continuing best-effort:

Proposed fix
         try:
             if source.resolve() == dest.resolve():
                 return
         except OSError:
-            # Continue best-effort copy if either path cannot be resolved.
-            pass
+            logger.warning(
+                f"Cannot resolve skill paths ({source}, {dest}); skipping sync to avoid self-overwrite"
+            )
+            return
🤖 Prompt for AI Agents
In `@massgen/backend/claude_code.py` around lines 710 - 716, The same-path guard
currently swallows OSError and proceeds to copy, risking self-overwrite; in the
try/except around source.resolve() == dest.resolve() update the except OSError
branch to log the caught exception (including which paths failed) and return
early instead of continuing, so the subsequent copytree/copy2 logic is not
executed when resolve() fails for either source or dest; reference the existing
source.resolve(), dest.resolve(), and the copytree/copy2 call sites when making
this change.

Comment on lines 749 to 751
tools = [
"Task", # Subagent spawning - unique to Claude Code
"Skill",
]
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:

#!/bin/bash
# Check if capabilities.py reflects the Skill builtin tool for claude_code
rg -n -i 'skill' massgen/backend/capabilities.py

Repository: massgen/MassGen

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Check the structure of capabilities.py to see how tools are registered
head -100 massgen/backend/capabilities.py

Repository: massgen/MassGen

Length of output: 3934


🏁 Script executed:

#!/bin/bash
# Look at the claude_code.py change to understand the context
sed -n '740,760p' massgen/backend/claude_code.py

Repository: massgen/MassGen

Length of output: 779


🏁 Script executed:

#!/bin/bash
# Find the claude_code entry in capabilities.py
rg -n 'claude_code' massgen/backend/capabilities.py -A 15

Repository: massgen/MassGen

Length of output: 633


🏁 Script executed:

#!/bin/bash
# Get all builtin_tools mentions in capabilities.py to understand the pattern
rg -n 'builtin_tools' massgen/backend/capabilities.py -B 2 -A 2

Repository: massgen/MassGen

Length of output: 4911


🏁 Script executed:

#!/bin/bash
# Get the complete builtin_tools list for claude_code in capabilities.py
sed -n '220,235p' massgen/backend/capabilities.py

Repository: massgen/MassGen

Length of output: 407


Add "Skill" to capabilities.py builtin_tools for claude_code backend.

The get_supported_builtin_tools method in claude_code.py now returns ["Skill"], but the capabilities registry still lists "Task" instead. Update the claude_code entry in massgen/backend/capabilities.py (line 220-235) to replace or update the builtin_tools list to reflect this change.

🤖 Prompt for AI Agents
In `@massgen/backend/claude_code.py` around lines 749 - 751, The claude_code
backend now advertises get_supported_builtin_tools() returning ["Skill"], but
the capabilities registry still lists "Task"; update the claude_code entry in
massgen/backend/capabilities.py to change its builtin_tools list to include
"Skill" (replace "Task" with "Skill" or add "Skill" if preserving others) so the
registry matches the get_supported_builtin_tools implementation and the
backend's supported tools.

Comment on lines +649 to 659
for filename in ("config.toml", "custom_tool_specs.json", "workflow_tool_specs.json", "checklist_specs.json", "AGENTS.md"):
filepath = config_dir / filename
if filepath.exists():
filepath.unlink()
# Remove dir if empty
if config_dir.exists() and not any(config_dir.iterdir()):
config_dir.rmdir()
# Also remove AGENTS.md we wrote in workspace root
# Cleanup legacy workspace-root AGENTS.md (older backend behavior).
agents_md = Path(self.cwd) / "AGENTS.md"
if agents_md.exists():
agents_md.unlink()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Cleanup correctly handles both new and legacy file locations.

Adding checklist_specs.json and AGENTS.md to the cleanup list is correct. The legacy AGENTS.md cleanup at the workspace root (lines 656–659) is a nice migration touch, though it will silently delete any user-created AGENTS.md in the workspace root.

Consider checking whether the workspace-root AGENTS.md was created by MassGen (e.g., via a marker comment or timestamp check) before removing it, to avoid deleting user-created files. Alternatively, document this cleanup behavior.

🤖 Prompt for AI Agents
In `@massgen/backend/codex.py` around lines 649 - 659, The cleanup currently
unconditionally removes a workspace-root AGENTS.md (agents_md) which can delete
user files; update the removal logic so you only unlink the workspace-root
AGENTS.md if it contains a MassGen generation marker (or other provenance check)
— e.g., open Path(self.cwd)/"AGENTS.md" and scan for a specific marker string or
timestamp signature you add when generating files; if the marker is present,
unlink agents_md, otherwise leave it and optionally log/emit a warning. Ensure
this check is done where agents_md is defined and keep the existing config_dir
cleanup unchanged.

Comment on lines +9062 to +9204
@staticmethod
def _normalize_skill_name_list(skill_names: List[str]) -> List[str]:
"""Normalize and deduplicate skill names while preserving order."""
seen: Set[str] = set()
normalized: List[str] = []
for name in skill_names:
cleaned = (name or "").strip()
if not cleaned:
continue
key = cleaned.lower()
if key in seen:
continue
seen.add(key)
normalized.append(cleaned)
return normalized

def _collect_skill_inventory(self) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
"""Collect available skills grouped into default vs created lists."""
from massgen.filesystem_manager.skills_manager import scan_skills
from massgen.logs_analyzer import get_logs_dir

skills_dir = Path(".agent/skills")
logs_dir = get_logs_dir()
all_skills = scan_skills(skills_dir, logs_dir=logs_dir if logs_dir.exists() else None)

# Keep first instance per name to avoid duplicates across sources.
deduped: List[Dict[str, Any]] = []
seen: Set[str] = set()
for skill in all_skills:
name = str(skill.get("name", "")).strip()
if not name:
continue
key = name.lower()
if key in seen:
continue
seen.add(key)
deduped.append(skill)

default_skills = sorted(
[s for s in deduped if s.get("location") == "builtin"],
key=lambda s: str(s.get("name", "")).lower(),
)
created_skills = sorted(
[s for s in deduped if s.get("location") != "builtin"],
key=lambda s: str(s.get("name", "")).lower(),
)
return default_skills, created_skills

def _show_skills_modal(self) -> None:
"""Show session skill manager modal."""
default_skills, created_skills = self._collect_skill_inventory()
current_enabled = self._mode_state.analysis_config.get_enabled_skill_names()

modal = SkillsModal(
default_skills=default_skills,
created_skills=created_skills,
enabled_skill_names=current_enabled,
)

def _on_skills_dismiss(result: Optional[List[str]]) -> None:
if result is None:
return

selected = self._normalize_skill_name_list(result)
discovered = self._normalize_skill_name_list(
[str(s.get("name", "")) for s in default_skills + created_skills],
)

selected_set = {name.lower() for name in selected}
discovered_set = {name.lower() for name in discovered}

if discovered and selected_set == discovered_set:
# None = unfiltered; automatically includes newly discovered skills.
self._mode_state.analysis_config.enabled_skill_names = None
enabled_count = len(discovered)
self.notify(f"Skills enabled: all {enabled_count} discovered", severity="information", timeout=3)
else:
self._mode_state.analysis_config.enabled_skill_names = selected
self.notify(f"Skills enabled: {len(selected)}", severity="information", timeout=3)

self.push_screen(modal, _on_skills_dismiss)

@staticmethod
def _slugify_skill_name(name: str) -> str:
"""Create a filesystem-safe skill directory name."""
slug = re.sub(r"[^a-z0-9]+", "-", (name or "").lower()).strip("-")
return slug or "skill-from-analysis"

@staticmethod
def _extract_skill_candidate_from_answer(answer_text: str) -> Optional[Tuple[str, str]]:
"""Extract a SKILL.md candidate from an answer if present.

Returns:
Tuple of (skill_name, skill_markdown) when detected, otherwise None.
"""
if not answer_text:
return None

def parse_skill_markdown(candidate: str) -> Optional[str]:
import yaml

cleaned = candidate.strip()
if not cleaned.startswith("---"):
return None

match = re.match(r"^---\s*\n(.*?)\n---\s*(?:\n|$)", cleaned, re.DOTALL)
if not match:
return None

try:
metadata = yaml.safe_load(match.group(1)) or {}
except Exception:
return None
if not isinstance(metadata, dict):
return None

skill_name = str(metadata.get("name", "")).strip()
return skill_name or None

# Prefer fenced blocks so explanatory prose outside the block is ignored.
for fence in re.finditer(r"```(?:[a-zA-Z0-9_-]+)?\n(.*?)```", answer_text, re.DOTALL):
block = fence.group(1).strip()
skill_name = parse_skill_markdown(block)
if skill_name:
return skill_name, block + "\n"

# Fallback: treat the entire answer as a candidate.
skill_name = parse_skill_markdown(answer_text)
if skill_name:
return skill_name, answer_text.strip() + "\n"

return None

def _offer_skill_creation_from_analysis(self, answer_text: str) -> None:
"""Show a confirmation modal when a user-mode analysis returns SKILL.md content."""
if self._mode_state.plan_mode != "analysis":
return
if self._mode_state.analysis_config.profile != "user":
return

candidate = self._extract_skill_candidate_from_answer(answer_text)
if not candidate:
return
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add Google-style docstrings for new skills helpers.

Functions like _normalize_skill_name_list, _collect_skill_inventory, _slugify_skill_name, _extract_skill_candidate_from_answer, and _offer_skill_creation_from_analysis need Args:/Returns: sections.
As per coding guidelines, "**/*.py: For new or changed functions, include Google-style docstrings".

🧰 Tools
🪛 Ruff (0.14.14)

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

(BLE001)

🤖 Prompt for AI Agents
In `@massgen/frontend/displays/textual_terminal_display.py` around lines 9062 -
9204, Add Google-style docstrings with Args: and Returns: to the listed helper
methods (_normalize_skill_name_list, _collect_skill_inventory,
_slugify_skill_name, _extract_skill_candidate_from_answer,
_offer_skill_creation_from_analysis). For each function include a one-line
summary, an Args: section documenting parameter names and types (e.g.,
skill_names: List[str], answer_text: str), and a Returns: section describing the
return type and meaning (e.g., List[str], Tuple[List[Dict[str, Any]],
List[Dict[str, Any]]], Optional[Tuple[str,str]], or None). Ensure the docstrings
match existing project style (Google style), are placed immediately under each
def, and keep descriptions concise for the private methods named above.

Comment on lines +9195 to +9232
def _offer_skill_creation_from_analysis(self, answer_text: str) -> None:
"""Show a confirmation modal when a user-mode analysis returns SKILL.md content."""
if self._mode_state.plan_mode != "analysis":
return
if self._mode_state.analysis_config.profile != "user":
return

candidate = self._extract_skill_candidate_from_answer(answer_text)
if not candidate:
return

skill_name, skill_markdown = candidate
target_dir = Path(".agent") / "skills" / self._slugify_skill_name(skill_name)
target_path = target_dir / "SKILL.md"

modal = SkillConfirmModal(
skill_name=skill_name,
skill_markdown=skill_markdown,
target_path=str(target_path),
)

def _on_confirm(result: Optional[bool]) -> None:
if not result:
return
try:
target_dir.mkdir(parents=True, exist_ok=True)
target_path.write_text(skill_markdown, encoding="utf-8")
self.notify(f"Created skill: {target_path}", severity="information", timeout=4)

# If a filter is active, include the newly created skill automatically.
enabled = self._mode_state.analysis_config.get_enabled_skill_names()
if enabled is not None:
self._mode_state.analysis_config.enabled_skill_names = self._normalize_skill_name_list(enabled + [skill_name])
except Exception as e:
self.notify(f"Failed to create skill: {e}", severity="error", timeout=4)

self.push_screen(modal, _on_confirm)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Prevent accidental overwrite of existing skills.

The current write path always overwrites an existing SKILL.md with the same slug. Consider guarding or prompting before overwrite to avoid losing prior content.

🔧 Suggested guard
         def _on_confirm(result: Optional[bool]) -> None:
             if not result:
                 return
             try:
+                if target_path.exists():
+                    self.notify(f"Skill already exists: {target_path}", severity="warning", timeout=4)
+                    return
                 target_dir.mkdir(parents=True, exist_ok=True)
                 target_path.write_text(skill_markdown, encoding="utf-8")
                 self.notify(f"Created skill: {target_path}", severity="information", timeout=4)
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 9227-9227: Consider [*enabled, skill_name] instead of concatenation

Replace with [*enabled, skill_name]

(RUF005)


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

(BLE001)

🤖 Prompt for AI Agents
In `@massgen/frontend/displays/textual_terminal_display.py` around lines 9195 -
9232, In _offer_skill_creation_from_analysis: prevent silent overwrites by
checking if target_path.exists() before writing; if SKILL.md already exists,
either (a) change the flow to present a confirmation dialog that explicitly
warns about overwriting (reuse or extend SkillConfirmModal to include an
"overwrite" prompt) or (b) abort and notify the user that a skill with that slug
already exists and suggest a different name; implement this check inside the
_on_confirm handler (or before pushing the modal) and only perform
target_dir.mkdir(...) and target_path.write_text(...) after explicit overwrite
confirmation, ensuring notify(...) is used to report the outcome or
cancellation.

Comment on lines +84 to +112
class AnalysisProfileChanged(Message):
"""Message emitted when analysis profile is changed."""

def __init__(self, profile: str) -> None:
self.profile = profile
super().__init__()


class AnalysisTargetChanged(Message):
"""Message emitted when analysis log/turn target is changed."""

def __init__(self, log_dir: Optional[str], turn: Optional[int]) -> None:
self.log_dir = log_dir
self.turn = turn
super().__init__()


class ViewAnalysisRequested(Message):
"""Message emitted when user wants to view an analysis report."""

def __init__(self, log_dir: str, turn: int) -> None:
self.log_dir = log_dir
self.turn = turn
super().__init__()


class OpenSkillsRequested(Message):
"""Message emitted when user wants to open the skills manager."""

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

Missing Google-style docstrings on new Message __init__ methods.

AnalysisProfileChanged.__init__, AnalysisTargetChanged.__init__, and ViewAnalysisRequested.__init__ lack Google-style docstrings with Args: sections, unlike the existing messages in this file (e.g., PlanSelected, PlanDepthChanged). Please add them for consistency and to satisfy the project's documentation guidelines.

Proposed fix
 class AnalysisProfileChanged(Message):
     """Message emitted when analysis profile is changed."""

     def __init__(self, profile: str) -> None:
+        """Initialize the message.
+
+        Args:
+            profile: The new analysis profile (e.g., "dev", "user").
+        """
         self.profile = profile
         super().__init__()


 class AnalysisTargetChanged(Message):
     """Message emitted when analysis log/turn target is changed."""

     def __init__(self, log_dir: Optional[str], turn: Optional[int]) -> None:
+        """Initialize the message.
+
+        Args:
+            log_dir: Path to the selected log directory, or None.
+            turn: The selected turn number, or None for latest.
+        """
         self.log_dir = log_dir
         self.turn = turn
         super().__init__()


 class ViewAnalysisRequested(Message):
     """Message emitted when user wants to view an analysis report."""

     def __init__(self, log_dir: str, turn: int) -> None:
+        """Initialize the message.
+
+        Args:
+            log_dir: Path to the log directory to analyze.
+            turn: The turn number to view the report for.
+        """
         self.log_dir = log_dir
         self.turn = turn
         super().__init__()

As per coding guidelines, **/*.py: "Use Google-style docstrings for all Python modules and functions to enable auto-generation of API documentation."

🤖 Prompt for AI Agents
In `@massgen/frontend/displays/textual_widgets/plan_options.py` around lines 84 -
112, Add Google-style docstrings with an "Args:" section to the __init__ methods
of AnalysisProfileChanged, AnalysisTargetChanged, and ViewAnalysisRequested so
they match the project's docs style (like PlanSelected/PlanDepthChanged);
describe each parameter and its type (e.g., profile: str; log_dir:
Optional[str]; turn: Optional[int] or int) and keep the brief one-line summary
above the Args section, leaving the existing super().__init__ calls intact.

Comment on lines +929 to +1152
def compose(self) -> ComposeResult:
self._load_packages_status()
self._checkboxes = {}
self._available_package_ids = []

yield Label("Skill Packages", classes="text-input-label")
yield Label(
"Select which quickstart packages to install now, then press Install Selected Packages.",
classes="password-hint",
)

if self._packages_status is None:
yield Label("Could not check skill package status.", classes="password-hint")
return

missing_packages: List[tuple[str, Dict[str, Any]]] = []
for pkg_id, pkg in self._packages_status.items():
installed = bool(pkg.get("installed"))
status_text = "installed" if installed else "not installed"
yield Label(f"{pkg.get('name', pkg_id)} [{status_text}]", classes="password-hint")
yield Label(f" {pkg.get('description', '')}", classes="password-hint")
if not installed:
missing_packages.append((pkg_id, pkg))

if not missing_packages:
yield Label("All quickstart skill packages are already installed.", classes="password-hint")
self._install_attempted = True
return

self._available_package_ids = [pkg_id for pkg_id, _ in missing_packages]

if not self._selected_packages:
self._selected_packages = list(self._available_package_ids)
else:
self._selected_packages = [pkg_id for pkg_id in self._selected_packages if pkg_id in self._available_package_ids]

yield Label("Select packages to install:", classes="text-input-label")
for pkg_id, pkg in missing_packages:
cb = Checkbox(
f"{pkg.get('name', pkg_id)}",
value=pkg_id in self._selected_packages,
id=f"skills_pkg_{pkg_id}",
)
cb.disabled = self._installing
self._checkboxes[pkg_id] = cb
yield cb

self._install_button = Button(
"Install Selected Packages",
id="install_selected_skill_packages",
variant="default",
)
if self._installing:
self._install_button.disabled = True
self._install_button.label = "Installing..."
yield self._install_button

self._status_label = Label(self._status_message, classes="password-hint")
self._status_label.display = bool(self._status_message)
yield self._status_label

def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
"""Handle package selection toggles."""
for pkg_id, cb in self._checkboxes.items():
if cb.id != event.checkbox.id:
continue
if event.value and pkg_id not in self._selected_packages:
self._selected_packages.append(pkg_id)
elif not event.value and pkg_id in self._selected_packages:
self._selected_packages.remove(pkg_id)
break

@staticmethod
def _run_installer_quiet(installer) -> tuple[bool, str]:
"""Run installer while capturing stdout/stderr to avoid TUI corruption."""
output_buffer = io.StringIO()
error_buffer = io.StringIO()
with contextlib.redirect_stdout(output_buffer), contextlib.redirect_stderr(error_buffer):
ok = bool(installer())
combined = "\n".join([output_buffer.getvalue(), error_buffer.getvalue()]).strip()
return ok, combined

async def on_button_pressed(self, event: Button.Pressed) -> None:
"""Install selected skill packages with progress updates."""
if event.button.id != "install_selected_skill_packages" or self._installing:
return

if not self._selected_packages:
self._install_attempted = True
self._status_message = "No packages selected. You can continue."
if self._status_label:
self._status_label.update(self._status_message)
self._status_label.display = True
return

self._installing = True
if self._install_button:
self._install_button.disabled = True
self._install_button.label = "Installing..."
for cb in self._checkboxes.values():
cb.disabled = True

def _status(message: str) -> None:
self._status_message = message
if self._status_label:
self._status_label.update(message)
self._status_label.display = True

_status("Preparing skill package installation...")
self._installed_packages = []
self._failed_packages = []

try:
from massgen.utils.skills_installer import (
install_agent_browser_skill,
install_anthropic_skills,
install_crawl4ai_skill,
install_openai_skills,
install_openskills_cli,
install_vercel_skills,
)

openskills_installers = {
"anthropic": install_anthropic_skills,
"openai": install_openai_skills,
"vercel": install_vercel_skills,
"agent_browser": install_agent_browser_skill,
}

selected = list(self._selected_packages)
needs_openskills = any(pkg_id in openskills_installers for pkg_id in selected)
openskills_ready = True

if needs_openskills:
_status("Installing openskills CLI...")
openskills_ready, cli_logs = await asyncio.to_thread(
self._run_installer_quiet,
install_openskills_cli,
)
if cli_logs:
_quickstart_log(f"SkillsInstallStep: openskills logs:\n{cli_logs}")
if not openskills_ready:
self._failed_packages.extend([pkg_id for pkg_id in selected if pkg_id in openskills_installers])

for pkg_id in selected:
if pkg_id in openskills_installers:
if not openskills_ready:
continue
_status(f"Installing {pkg_id}...")
ok, pkg_logs = await asyncio.to_thread(
self._run_installer_quiet,
openskills_installers[pkg_id],
)
elif pkg_id == "crawl4ai":
_status("Installing crawl4ai...")
ok, pkg_logs = await asyncio.to_thread(
self._run_installer_quiet,
install_crawl4ai_skill,
)
else:
_quickstart_log(f"SkillsInstallStep: Unknown package id '{pkg_id}'")
pkg_logs = ""
ok = False

if pkg_logs:
_quickstart_log(f"SkillsInstallStep: {pkg_id} logs:\n{pkg_logs}")

if ok:
self._installed_packages.append(pkg_id)
else:
self._failed_packages.append(pkg_id)

self._install_attempted = True
if self._failed_packages:
_status(
f"Installed {len(self._installed_packages)} package(s), " f"{len(self._failed_packages)} failed. Retry or adjust selection.",
)
else:
_status(f"Installed {len(self._installed_packages)} package(s) successfully.")

except Exception as e:
self._install_attempted = True
self._failed_packages = list(self._selected_packages)
_quickstart_log(f"SkillsInstallStep: Installation error: {e}")
_status(f"Skills installation failed: {e}")

self._installing = False
if self._install_button:
self._install_button.disabled = False
self._install_button.label = "Install Selected Packages"
for cb in self._checkboxes.values():
cb.disabled = False

# Recompose so package status reflects newly installed items.
self.refresh(recompose=True)

def validate(self) -> Optional[str]:
if self._installing:
return "Please wait for skill installation to finish"

if not self._available_package_ids:
return None

if self._selected_packages and not self._install_attempted:
return "Click 'Install Selected Packages' before continuing, or deselect all packages"

return None

def get_value(self) -> Dict[str, Any]:
return {
"packages_to_install": list(self._selected_packages),
"install_attempted": self._install_attempted,
"installed_packages": list(self._installed_packages),
"failed_packages": list(self._failed_packages),
}

def set_value(self, value: Any) -> None:
if not isinstance(value, dict):
return
self._selected_packages = list(value.get("packages_to_install", []))
self._install_attempted = bool(value.get("install_attempted", False))
self._installed_packages = list(value.get("installed_packages", []))
self._failed_packages = list(value.get("failed_packages", []))

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add Google-style docstrings for the new SkillsInstallStep methods.

compose, validate, get_value, and set_value (and any other newly added methods) need Google-style docstrings.

💡 Example docstring (apply similarly to other methods)
-    def compose(self) -> ComposeResult:
+    def compose(self) -> ComposeResult:
+        """Compose the skills selection UI.
+
+        Returns:
+            ComposeResult: UI nodes for the skills install step.
+        """

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

🧰 Tools
🪛 Ruff (0.14.14)

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

(BLE001)

🤖 Prompt for AI Agents
In `@massgen/frontend/displays/textual_widgets/quickstart_wizard.py` around lines
929 - 1152, Add Google-style docstrings to all newly added/changed methods in
SkillsInstallStep: compose, on_checkbox_changed, _run_installer_quiet,
on_button_pressed, validate, get_value, and set_value. For each method include a
short summary line, Args with parameter types and descriptions (e.g., event:
Checkbox.Changed for on_checkbox_changed, event: Button.Pressed for
on_button_pressed, installer for _run_installer_quiet), Returns with the return
type and meaning (or None), and Raises if applicable; keep formatting consistent
with other project docstrings and place the docstring immediately below each def
declaration (triple-quoted Google-style).

Comment on lines +990 to +999
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
"""Handle package selection toggles."""
for pkg_id, cb in self._checkboxes.items():
if cb.id != event.checkbox.id:
continue
if event.value and pkg_id not in self._selected_packages:
self._selected_packages.append(pkg_id)
elif not event.value and pkg_id in self._selected_packages:
self._selected_packages.remove(pkg_id)
break
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reset install state when package selection changes.

If a user changes selection after an install attempt, validate() still allows proceeding without re-installing newly selected packages. Consider resetting _install_attempted on selection changes.

🛠️ Suggested fix
     def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
         """Handle package selection toggles."""
         for pkg_id, cb in self._checkboxes.items():
             if cb.id != event.checkbox.id:
                 continue
             if event.value and pkg_id not in self._selected_packages:
                 self._selected_packages.append(pkg_id)
             elif not event.value and pkg_id in self._selected_packages:
                 self._selected_packages.remove(pkg_id)
-            break
+            if self._install_attempted:
+                self._install_attempted = False
+                self._status_message = ""
+            break
🤖 Prompt for AI Agents
In `@massgen/frontend/displays/textual_widgets/quickstart_wizard.py` around lines
990 - 999, The on_checkbox_changed handler must reset the install state when
selections change: inside QuickstartWizard.on_checkbox_changed (which iterates
self._checkboxes and updates self._selected_packages) set
self._install_attempted = False whenever a package is added or removed so
validate() cannot bypass re-install; ensure the flag is cleared before breaking
out of the loop so any change (event.value true or false) resets the install
attempt state.

@ncrispino
Copy link
Collaborator Author

prefer #869 , has these merged

@ncrispino ncrispino closed this Feb 9, 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.

1 participant