Skip to content

Fix clone_repo to Always Clone from Remote URL#368

Merged
Trecek merged 6 commits intointegrationfrom
clone-fix-367
Mar 13, 2026
Merged

Fix clone_repo to Always Clone from Remote URL#368
Trecek merged 6 commits intointegrationfrom
clone-fix-367

Conversation

@Trecek
Copy link
Collaborator

@Trecek Trecek commented Mar 13, 2026

Summary

clone_repo previously fell back to the local filesystem path as the clone source in two bug scenarios inside _resolve_clone_source: when a branch was not found on the remote (ls-remote exit code 2) and when a network timeout occurred during ls-remote. When a remote URL is configured, the clone source must always be the remote — never the local filesystem — to preserve the isolation guarantee.

The fix simplifies _resolve_clone_source to a single-expression function: remove the branch parameter and the ls-remote network check, and always return the remote URL when one is available. The no-remote fallback to local path is retained as the only legitimate use of local as a clone source.

Architecture Impact

Process Flow Diagram

%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;
    classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff;
    classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;

    %% TERMINALS %%
    START([clone_repo called])
    SUCCESS([return clone_path + remote_url])
    WARN_UNCOMMITTED([return uncommitted_changes dict])
    WARN_UNPUBLISHED([return unpublished_branch dict])
    WARN_REWRITE([return remote_url_rewrite_failed dict])
    ERROR_VAL([raise ValueError])
    ERROR_RT([raise RuntimeError])

    subgraph Resolution ["Source Resolution"]
        direction TB
        AutoDetect{"source_dir<br/>━━━━━━━━━━<br/>empty?"}
        CwdDetect["detect_source_dir(cwd)<br/>━━━━━━━━━━<br/>git rev-parse --show-toplevel"]
        ValidateDir{"Path.is_dir()?"}
        BranchDetect{"branch<br/>━━━━━━━━━━<br/>empty?"}
        DetectBranch["detect_branch(source)<br/>━━━━━━━━━━<br/>git rev-parse --abbrev-ref HEAD"]
    end

    subgraph Guards ["Strategy Guards"]
        direction TB
        StratCheck{"strategy<br/>━━━━━━━━━━<br/>== ''?"}
        UncommittedGuard["detect_uncommitted_changes<br/>━━━━━━━━━━<br/>git status --porcelain"]
        DirtyCheck{"changes<br/>━━━━━━━━━━<br/>found?"}
        UnpublishedGuard["detect_unpublished_branch<br/>━━━━━━━━━━<br/>git ls-remote origin"]
        UnpubCheck{"branch missing<br/>━━━━━━━━━━<br/>on remote?"}
    end

    subgraph CloneSetup ["Clone Setup"]
        direction TB
        TimestampPath["compute clone_path<br/>━━━━━━━━━━<br/>runs_parent / run_name-timestamp"]
        GetRemoteURL["git remote get-url origin<br/>━━━━━━━━━━<br/>detected_url from source<br/>timeout=30s"]
    end

    subgraph StrategyBranch ["Strategy Branch"]
        direction TB
        StrategyCheck{"● strategy<br/>━━━━━━━━━━<br/>== clone_local?"}
        LocalCopy["shutil.copytree<br/>━━━━━━━━━━<br/>copy working tree"]
        ResolveSource["● _resolve_clone_source<br/>━━━━━━━━━━<br/>detected_url truthy?"]
        UseRemote["Use remote URL<br/>━━━━━━━━━━<br/>always when URL known<br/>— no ls-remote check"]
        UseLocal["Use local path<br/>━━━━━━━━━━<br/>only when no remote<br/>URL configured"]
        GitClone["git clone [--branch B]<br/>━━━━━━━━━━<br/>clone_source → clone_path"]
        CloneOK{"returncode<br/>━━━━━━━━━━<br/>== 0?"}
    end

    subgraph PostClone ["Post-Clone"]
        direction TB
        EffectiveURL["effective_url<br/>━━━━━━━━━━<br/>remote_url arg or detected_url"]
        RewriteOrigin["git remote set-url origin<br/>━━━━━━━━━━<br/>enforce effective_url"]
        RewriteOK{"rewrite<br/>━━━━━━━━━━<br/>succeeded?"}
        Decontam["decontaminate<br/>━━━━━━━━━━<br/>untrack + delete<br/>generated files"]
    end

    %% FLOW: Source Resolution %%
    START --> AutoDetect
    AutoDetect -->|"yes"| CwdDetect
    AutoDetect -->|"no"| ValidateDir
    CwdDetect --> ValidateDir
    ValidateDir -->|"false"| ERROR_VAL
    ValidateDir -->|"true"| BranchDetect
    BranchDetect -->|"yes"| DetectBranch
    BranchDetect -->|"no"| StratCheck
    DetectBranch --> StratCheck

    %% FLOW: Guards %%
    StratCheck -->|"yes"| UncommittedGuard
    StratCheck -->|"no (proceed / clone_local)"| TimestampPath
    UncommittedGuard --> DirtyCheck
    DirtyCheck -->|"yes"| WARN_UNCOMMITTED
    DirtyCheck -->|"no"| UnpublishedGuard
    UnpublishedGuard --> UnpubCheck
    UnpubCheck -->|"yes"| WARN_UNPUBLISHED
    UnpubCheck -->|"no"| TimestampPath

    %% FLOW: Clone Setup %%
    TimestampPath --> GetRemoteURL
    GetRemoteURL --> StrategyCheck

    %% FLOW: Strategy Branch %%
    StrategyCheck -->|"yes"| LocalCopy
    StrategyCheck -->|"no"| ResolveSource
    ResolveSource -->|"URL configured"| UseRemote
    ResolveSource -->|"no URL"| UseLocal
    UseRemote --> GitClone
    UseLocal --> GitClone
    LocalCopy --> EffectiveURL
    GitClone --> CloneOK
    CloneOK -->|"no"| ERROR_RT
    CloneOK -->|"yes"| EffectiveURL

    %% FLOW: Post-Clone %%
    EffectiveURL --> RewriteOrigin
    RewriteOrigin --> RewriteOK
    RewriteOK -->|"failed + caller supplied remote_url"| WARN_REWRITE
    RewriteOK -->|"ok or auto-detected"| Decontam
    Decontam --> SUCCESS

    %% CLASS ASSIGNMENTS %%
    class START,SUCCESS,WARN_UNCOMMITTED,WARN_UNPUBLISHED,WARN_REWRITE,ERROR_VAL,ERROR_RT terminal;
    class AutoDetect,BranchDetect,StratCheck,DirtyCheck,UnpubCheck,StrategyCheck,CloneOK,RewriteOK stateNode;
    class CwdDetect,DetectBranch,TimestampPath,GetRemoteURL,GitClone,LocalCopy,EffectiveURL,RewriteOrigin,Decontam handler;
    class UncommittedGuard,UnpublishedGuard detector;
    class ResolveSource stateNode;
    class UseRemote,UseLocal phase;
Loading

Color Legend:

Color Category Description
Dark Blue Terminal Start, success, warning returns, and error states
Teal State Decision and routing nodes (● = modified)
Orange Handler Processing, git command execution, path computation
Red Detector Uncommitted/unpublished validation guards
Purple Phase Clone source options (remote URL vs local path)

Closes #367

Implementation Plan

Plan file: /home/talon/projects/autoskillit-runs/clone-fix-20260312-173207/temp/make-plan/clone_remote_fix_plan_2026-03-12_173652.md

Token Usage Summary

No token data accumulated for this pipeline run.

🤖 Generated with Claude Code via AutoSkillit

Trecek added 4 commits March 12, 2026 18:03
- Add TestResolveCloneSource with 3 unit tests for the simplified helper
  (2-param signature: no branch, always returns URL when configured)
- Add T1-D: test_clone_uses_remote_when_branch_not_on_remote verifies
  that clone_repo raises RuntimeError (not silently clones local) when
  branch is missing on remote
- Update test_strategy_proceed_bypasses_unpublished_guard → renamed to
  test_strategy_proceed_with_unpublished_branch_fails_from_remote to
  reflect correct post-fix behavior (RuntimeError expected)

Tests fail against current codebase — confirms they are properly anchored.
…figured

Remove the branch parameter and ls-remote check from _resolve_clone_source.
When a remote URL is configured, it is now always used as the clone source —
no fallback to local path based on branch availability or network reachability.

The only legitimate local-path fallback remains: when no remote origin is
configured at all. This eliminates two silent-failure bugs:
- Branch not on remote (ls-remote exit 2) → was silently cloning local
- Network timeout during ls-remote → was silently cloning local

Both scenarios now surface as RuntimeError from git clone, making the
isolation violation explicit rather than silent.

Also update docstrings: module note scoped to clone_local strategy only;
clone_repo docstring reflects the new deterministic remote-first behavior.
…ns_remote_url

After _resolve_clone_source simplification, clone always targets the remote
URL when configured. The test set up an empty bare remote and never pushed
the local commit, causing git clone to fail (no 'main' on remote). Push the
commit before cloning so the remote has the expected branch.
Copy link
Collaborator Author

@Trecek Trecek left a comment

Choose a reason for hiding this comment

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

AutoSkillit PR Review — Verdict: changes_requested (REQUEST_CHANGES unavailable on own PR; posting as COMMENT)

remote URL when one is configured, so git clone fails (correctly) instead
of silently cloning local state.
"""
with patch(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

[warning] tests: test_clone_uses_remote_when_branch_not_on_remote substantially overlaps with test_strategy_proceed_with_unpublished_branch_fails_from_remote (same fixture, branch, expected RuntimeError). The only unique value — asserting clone source — is undermined by the fragile index noted separately.

clone_calls = [
call
for call in spy.call_args_list
if call[0] and isinstance(call[0][0], list) and call[0][0][:2] == ["git", "clone"]
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

[warning] tests: Fragile positional index clone_calls[0][0][0][-2] extracts clone source without validating command list length; silently returns the wrong element if git clone gains/loses arguments (e.g. --no-hardlinks, --depth). Use a named extraction relative to known flags instead.

source = tmp_path / "repo"
assert _resolve_clone_source(source, "") == str(source)

def test_url_returned_regardless_of_branch_existence(self, tmp_path: Path) -> None:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

[info] tests: test_url_returned_regardless_of_branch_existence is redundant with test_returns_url_when_configured: both pass a non-empty URL and assert it is returned. _resolve_clone_source no longer accepts a branch parameter, so there is nothing to distinguish the two tests.

@@ -1272,3 +1286,62 @@ def test_clone_result_remote_url_correct_after_remote_clone(
assert result["remote_url"] == str(bare_remote)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

[info] cohesion: TestResolveCloneSource imports _resolve_clone_source inside each individual test method instead of at module scope, inconsistent with the import-locality pattern used elsewhere in the test suite.

from autoskillit.workspace.clone import _resolve_clone_source

source = tmp_path / "repo"
assert _resolve_clone_source(source, "") == str(source)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

[info] cohesion: test_url_returned_regardless_of_branch_existence name implies branch-parameter testing, but the function no longer accepts a branch parameter after simplification; the name is misleading.

network reachability — when a remote URL is known, it is always
used as the clone source. The clone_local strategy bypasses this
function entirely (shutil.copytree, always local).
"""
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

[info] slop: Docstring for _resolve_clone_source is over-verbose for a one-liner function; four sentences including a redundant note about clone_local that duplicates the module docstring.

"""strategy='proceed' with unpublished branch now fails — remote is always used.

Previously 'proceed' bypassed the guard by silently falling back to the
local path. After the fix the clone always targets the remote, so a branch
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

[info] slop: Docstring contains historical 'Previously... After the fix' narrative; tests should document the current invariant, not a changelog.

# T1-D
def test_clone_uses_remote_when_branch_not_on_remote(
self, local_with_remote: Path, bare_remote: Path
) -> None:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

[info] slop: Docstring contains historical 'Previously... After the fix' narrative; only the current invariant belongs in a test docstring.

Trecek and others added 2 commits March 12, 2026 19:23
…gile positional index

Replaces clone_calls[0][0][0][-2] with a flag-aware walk of the git
clone command arguments. The new extraction skips flag/value pairs
(--branch, --depth, --no-hardlinks, etc.) and collects positional args,
so the clone source is always positional[0] regardless of how many
optional flags are present.

Addresses reviewer findings:
- Fragile positional index silently breaks if git clone args change
- Makes the spy assertion robust, restoring the unique value of
  test_clone_uses_remote_when_branch_not_on_remote
…o_returns_remote_url

The bare repo was initialized without --initial-branch=main, leaving HEAD
pointing to master. The push used HEAD:main, so the bare repo had a main
branch but a master HEAD. git clone then failed with "Remote branch master
not found" because master didn't exist. Setting --initial-branch=main at
init time aligns HEAD with the branch that gets pushed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Trecek Trecek added this pull request to the merge queue Mar 13, 2026
Merged via the queue into integration with commit 9f9dda7 Mar 13, 2026
2 checks passed
@Trecek Trecek deleted the clone-fix-367 branch March 13, 2026 03:19
Trecek added a commit that referenced this pull request Mar 15, 2026
…, Headless Isolation (#404)

## Summary

Integration rollup of **43 PRs** (#293#406) consolidating **62
commits** across **291 files** (+27,909 / −6,040 lines). This release
advances AutoSkillit from v0.2.0 to v0.3.1 with GitHub merge queue
integration, sub-recipe composition, a PostToolUse output reformatter,
headless session isolation guards, and comprehensive pipeline
observability — plus 24 new bundled skills, 3 new MCP tools, and 47 new
test files.

---

## Major Features

### GitHub Merge Queue Integration (#370, #362, #390)
- New `wait_for_merge_queue` MCP tool — polls a PR through GitHub's
merge queue until merged, ejected, or timed out (default 600s). Uses
REST + GraphQL APIs with stuck-queue detection and auto-merge
re-enrollment
- New `DefaultMergeQueueWatcher` L1 service (`execution/merge_queue.py`)
— never raises; all outcomes are structured results
- `parse_merge_queue_response()` pure function for GraphQL queue entry
parsing
- New `auto_merge` ingredient in `implementation.yaml` and
`remediation.yaml` — enrolls PRs in the merge queue after CI passes
- Full queue-mode path added to `merge-prs.yaml`: detect queue → enqueue
→ wait → handle ejections → re-enter
- `analyze-prs` skill gains Step 0.5 (merge queue detection) and Step
1.5 (CI/review eligibility filtering)

### Sub-Recipe Composition (#380)
- Recipe steps can now reference sub-recipes via `sub_recipe` + `gate`
fields — lazy-loaded and merged at validation time
- Composition engine in `recipe/_api.py`: `_merge_sub_recipe()` inlines
sub-recipe steps with safe name-prefixing and route remapping (`done` →
parent's `on_success`, `escalate` → parent's `on_failure`)
- `_build_active_recipe()` evaluates gate ingredients against
overrides/defaults; dual validation runs on both active and combined
recipes
- First sub-recipe: `sprint-prefix.yaml` — triage → plan → confirm →
dispatch workflow, gated by `sprint_mode` ingredient (hidden, default
false)
- Both `implementation.yaml` and `remediation.yaml` gain `sprint_entry`
placeholder step
- New semantic rules: `unknown-sub-recipe` (ERROR),
`circular-sub-recipe` (ERROR) with DFS cycle detection

### PostToolUse Output Reformatter (#293, #405)
- `pretty_output.py` — new 671-line PostToolUse hook that rewrites raw
MCP JSON responses to Markdown-KV before Claude consumes them (30–77%
token overhead reduction)
- Dedicated formatters for 11 high-traffic tools (`run_skill`,
`run_cmd`, `test_check`, `merge_worktree`, `get_token_summary`, etc.)
plus a generic KV formatter for remaining tools
- Pipeline vs. interactive mode detection via hook config file
- Unwraps Claude Code's `{"result": "<json-string>"}` envelope before
dispatching
- 1,516-line test file with 40+ behavioral tests

### Headless Session Isolation (#359, #393, #397, #405, #406)
- **Env isolation**: `build_sanitized_env()` strips
`AUTOSKILLIT_PRIVATE_ENV_VARS` from subprocess environments, preventing
`AUTOSKILLIT_HEADLESS=1` from leaking into test runners
- **CWD path contamination defense**: `_inject_cwd_anchor()` anchors all
relative paths to session CWD; `_validate_output_paths()` checks
structured output tokens against CWD prefix; `_scan_jsonl_write_paths()`
post-session scanner catches actual Write/Edit/Bash tool calls outside
CWD
- **Headless orchestration guard**: new PreToolUse hook blocks
`run_skill`/`run_cmd`/`run_python` when `AUTOSKILLIT_HEADLESS=1`,
enforcing Tier 1/Tier 2 nesting invariant
- **`_require_not_headless()` server-side guard**: blocks 10
orchestration-only tools from headless sessions at the handler layer
- **Unified error response contract**: `headless_error_result()`
produces consistent 9-field responses;
`_build_headless_error_response()` canonical builder for all failure
paths in `tools_integrations.py`

### Cook UX Overhaul (#375, #363)
- `open_kitchen` now accepts optional `name` + `overrides` — opens
kitchen AND loads recipe in a single call
- Pre-launch terminal preview with ANSI-colored flow diagram and
ingredients table via new `cli/_ansi.py` module
- `--dangerously-skip-permissions` warning banner with interactive
confirmation prompt
- Randomized session greetings from themed pools
- Orchestrator prompt rewritten: recipe YAML no longer injected via
`--append-system-prompt`; session calls `open_kitchen('{recipe_name}')`
as first action
- Conversational ingredient collection replaces mechanical per-field
prompting

---

## New MCP Tools

| Tool | Gate | Description |
|------|------|-------------|
| `wait_for_merge_queue` | Kitchen | Polls PR through GitHub merge queue
(REST + GraphQL) |
| `set_commit_status` | Kitchen | Posts GitHub Commit Status to a SHA
for review-first gating |
| `get_quota_events` | Ungated | Surfaces quota guard decisions from
`quota_events.jsonl` |

---

## Pipeline Observability (#318, #341)

- **`TelemetryFormatter`** (`pipeline/telemetry_fmt.py`) — single source
of truth for all telemetry rendering; replaces dual-formatter
anti-pattern. Four rendering modes: Markdown table, terminal table,
compact KV (for PostToolUse hook)
- `get_token_summary` and `get_timing_summary` gain `format` parameter
(`"json"` | `"table"`)
- `wall_clock_seconds` merged into token summary output — see duration
alongside token counts in one call
- **Telemetry clear marker**: `write_telemetry_clear_marker()` /
`read_telemetry_clear_marker()` prevent token accounting drift on MCP
server restart after `clear=True`
- **Quota event logging**: `quota_check.py` hook now writes structured
JSONL events (`cache_miss`, `parse_error`, `blocked`, `approved`) to
`quota_events.jsonl`

---

## CI Watcher & Remote Resolution Fixes (#395, #406)

- **`CIRunScope` value object** — carries `workflow` + `head_sha` scope;
replaces bare `head_sha` parameter across all CI watcher signatures
- **Workflow filter**: `wait_for_ci` and `get_ci_status` accept
`workflow` parameter (falls back to project-level `config.ci.workflow`),
preventing unrelated workflows (version bumps, labelers) from satisfying
CI checks
- **`FAILED_CONCLUSIONS` expanded**: `failure` → `{failure, timed_out,
startup_failure, cancelled}`
- **Canonical remote resolver** (`execution/remote_resolver.py`):
`resolve_remote_repo()` with `REMOTE_PRECEDENCE = (upstream, origin)` —
correctly resolves `owner/repo` after `clone_repo` sets `origin` to
`file://` isolation URL
- **Clone isolation fix**: `clone_repo` now always clones from remote
URL (never local path); sets `origin=file:///<clone>` for isolation and
`upstream=<real_url>` for push/CI operations

---

## PR Pipeline Gates (#317, #343)

- **`pipeline/pr_gates.py`**: `is_ci_passing()`, `is_review_passing()`,
`partition_prs()` — partitions PRs into
eligible/CI-blocked/review-blocked with human-readable reasons
- **`pipeline/fidelity.py`**: `extract_linked_issues()`
(Closes/Fixes/Resolves patterns), `is_valid_fidelity_finding()` schema
validation
- **`check_pr_mergeable`** now returns `mergeable_status` field
alongside boolean
- **`release_issue`** gains `target_branch` + `staged_label` parameters
for staged issue lifecycle on non-default branches (#392)

---

## Recipe System Changes

### Structural
- `RecipeIngredient.hidden` field — excluded from ingredients table
(used for internal flags like `sprint_mode`)
- `Recipe.experimental` flag parsed from YAML
- `_TERMINAL_TARGETS` moved to `schema.py` as single source of truth
- `format_ingredients_table()` with sorted display order (required →
auto-detect → flags → optional → constants)
- Diagram rendering engine (~670 lines) removed from `diagrams.py` —
rendering now handled by `/render-recipe` skill; format version bumped
to v7

### Recipe YAML Changes
- **Deleted**: `audit-and-fix.yaml`, `batch-implementation.yaml`,
`bugfix-loop.yaml`
- **Renamed**: `pr-merge-pipeline.yaml` → `merge-prs.yaml`
- **`implementation.yaml`**: merge queue steps,
`auto_merge`/`sprint_mode` ingredients, `base_branch` default → `""`
(auto-detect), CI workflow filter, `extract_pr_number` step
- **`remediation.yaml`**: `topic` → `task` rename, merge queue steps,
`dry_walkthrough` retries:3 with forward-only routing, `verify` → `test`
rename
- **`merge-prs.yaml`**: full queue-mode path, `open-integration-pr` step
(replaces `create-review-pr`), post-PR mergeability polling, review
cycle with `resolve-review` retries

### New Semantic Rules
- `missing-output-patterns` (WARNING) — flags `run_skill` steps without
`expected_output_patterns`
- `unknown-sub-recipe` (ERROR) — validates sub-recipe references exist
- `circular-sub-recipe` (ERROR) — DFS cycle detection
- `unknown-skill-command` (ERROR) — validates skill names against
bundled set
- `telemetry-before-open-pr` (WARNING) — ensures telemetry step precedes
`open-pr`

---

## New Skills (24)

### Architecture Lens Family (13)
`arch-lens-c4-container`, `arch-lens-concurrency`,
`arch-lens-data-lineage`, `arch-lens-deployment`,
`arch-lens-development`, `arch-lens-error-resilience`,
`arch-lens-module-dependency`, `arch-lens-operational`,
`arch-lens-process-flow`, `arch-lens-repository-access`,
`arch-lens-scenarios`, `arch-lens-security`, `arch-lens-state-lifecycle`

### Audit Family (5)
`audit-arch`, `audit-bugs`, `audit-cohesion`, `audit-defense-standards`,
`audit-tests`

### Planning & Diagramming (3)
`elaborate-phase`, `make-arch-diag`, `make-req`

### Bug/Guard Lifecycle (2)
`design-guards`, `verify-diag`

### Pipeline (1)
`open-integration-pr` — creates integration PRs with per-PR details,
arch-lens diagrams, carried-forward `Closes #N` references, and
auto-closes collapsed PRs

### Sprint Planning (1 — gated by sub-recipe)
`sprint-planner` — selects a focused, conflict-free sprint from a triage
manifest

---

## Skill Modifications (Highlights)

- **`analyze-prs`**: merge queue detection, CI/review eligibility
filtering, queue-mode ordering
- **`dry-walkthrough`**: Step 4.5 Historical Regression Check (git
history mining + GitHub issue cross-reference)
- **`review-pr`**: deterministic diff annotation via
`diff_annotator.py`, echo-primary-obligation step, post-completion
confirmation, degraded-mode narration
- **`collapse-issues`**: content fidelity enforcement — per-issue
`fetch_github_issue` calls, copy-mode body assembly (#388)
- **`prepare-issue`**: multi-keyword dedup search, numbered candidate
selection, extend-existing-issue flow
- **`resolve-review`**: GraphQL thread auto-resolution after addressing
findings (#379)
- **`resolve-merge-conflicts`**: conflict resolution decision report
with per-file log (#389)
- **Cross-skill**: output tokens migrated to `key = value` format;
code-index paths made generic with fallback notes; arch-lens references
fully qualified; anti-prose guards at loop boundaries

---

## CLI & Hooks

### New CLI Commands
- `autoskillit install` — plugin installation + cache refresh
- `autoskillit upgrade` — `.autoskillit/scripts/` →
`.autoskillit/recipes/` migration

### CLI Changes
- `doctor`: plugin-aware MCP check, PostToolUse hook scanning, `--fix`
flag removed
- `init`: GitHub repo prompt, `.secrets.yaml` template, plugin-aware
registration
- `chefs-hat`: pre-launch banner, `--dangerously-skip-permissions`
confirmation
- `recipes render`: repurposed from generator to viewer (delegates to
`/render-recipe`)
- `serve`: server import deferred to after `configure_logging()` to
prevent stdout corruption

### New Hooks
- `branch_protection_guard.py` (PreToolUse) — denies
`merge_worktree`/`push_to_remote` targeting protected branches
- `headless_orchestration_guard.py` (PreToolUse) — blocks orchestration
tools in headless sessions
- `pretty_output.py` (PostToolUse) — MCP JSON → Markdown-KV reformatter

### Hook Infrastructure
- `HookDef.event_type` field — registry now handles both PreToolUse and
PostToolUse
- `generate_hooks_json()` groups entries by event type
- `_evict_stale_autoskillit_hooks` and `sync_hooks_to_settings` made
event-type-agnostic

---

## Core & Config

### New Core Modules
- `core/branch_guard.py` — `is_protected_branch()` pure function
- `core/github_url.py` — `parse_github_repo()` +
`normalize_owner_repo()` canonical parsers

### Core Type Expansions
- `AUTOSKILLIT_PRIVATE_ENV_VARS` frozenset
- `WORKER_TOOLS` / `HEADLESS_BLOCKED_UNGATED_TOOLS` split from
`UNGATED_TOOLS`
- `TOOL_CATEGORIES` — categorized listing for `open_kitchen` response
- `CIRunScope` — immutable scope for CI watcher calls
- `MergeQueueWatcher` protocol
- `SkillResult.cli_subtype` + `write_path_warnings` fields
- `SubprocessRunner.env` parameter

### Config
- `safety.protected_branches`: `[main, integration, stable]`
- `github.staged_label`: `"staged"`
- `ci.workflow`: workflow filename filter (e.g., `"tests.yml"`)
- `branching.default_base_branch`: `"integration"` → `"main"`
- `ModelConfig.default`: `str | None` → `str = "sonnet"`

---

## Infrastructure & Release

### Version
- `0.2.0` → `0.3.1` across `pyproject.toml`, `plugin.json`, `uv.lock`
- FastMCP dependency: `>=3.0.2` → `>=3.1.1,<4.0` (#399)

### CI/CD Workflows
- **`version-bump.yml`** (new) — auto patch-bumps `main` on integration
PR merge, force-syncs integration branch one patch ahead
- **`release.yml`** (new) — minor version bump + GitHub Release on merge
to `stable`
- **`codeql.yml`** (new) — CodeQL analysis for `stable` PRs (Python +
Actions)
- **`tests.yml`** — `merge_group:` trigger added; multi-OS now only for
`stable`

### PyPI Readiness
- `pyproject.toml`: `readme`, `license`, `authors`, `keywords`,
`classifiers`, `project.urls`, `hatch.build.targets.sdist` inclusion
list

### readOnlyHint Parallel Execution Fix
- All MCP tools annotated `readOnlyHint=True` — enables Claude Code
parallel tool execution (~7x speedup). One deliberate exception:
`wait_for_merge_queue` uses `readOnlyHint=False` (actually mutates queue
state)

### Tool Response Exception Boundary
- `track_response_size` decorator catches unhandled exceptions and
serializes them as `{"success": false, "subtype": "tool_exception"}` —
prevents FastMCP opaque error wrapping

### SkillResult Subtype Normalization (#358)
- `_normalize_subtype()` gate eliminates dual-source contradiction
between CLI subtype and session outcome
- Class 2 upward: `SUCCEEDED + error_subtype → "success"` (drain-race
artifact)
- Class 1 downward: `non-SUCCEEDED + "success" → "empty_result"` /
`"missing_completion_marker"` / `"adjudicated_failure"`

---

## Test Coverage

**47 new test files** (+12,703 lines) covering:

| Area | Key Tests |
|------|-----------|
| Merge queue watcher state machine | `test_merge_queue.py` (226 lines)
|
| Clone isolation × CI resolution | `test_clone_ci_contract.py`,
`test_remote_resolver.py` |
| PostToolUse hook | `test_pretty_output.py` (1,516 lines, 40+ cases) |
| Branch protection + headless guards |
`test_branch_protection_guard.py`,
`test_headless_orchestration_guard.py` |
| Sub-recipe composition | 5 test files (schema, loading, validation,
sprint mode × 2) |
| Telemetry formatter | `test_telemetry_formatter.py` (281 lines) |
| PR pipeline gates | `test_analyze_prs_gates.py`,
`test_review_pr_fidelity.py` |
| Diff annotator | `test_diff_annotator.py` (242 lines) |
| Skill compliance | Output token format, genericization, loop-boundary
guards |
| Release workflows | Structural contracts for `version-bump.yml`,
`release.yml` |
| Issue content fidelity | Body-assembling skills must call
`fetch_github_issue` per-issue |
| CI watcher scope | `test_ci_params.py` — workflow_id query param
composition |

---

## Consolidated PRs

#293, #295, #314, #315, #316, #317, #318, #319, #323, #332, #336, #337,
#338, #339, #341, #343, #351, #358, #359, #360, #361, #362, #363, #366,
#368, #370, #375, #377, #378, #379, #380, #388, #389, #390, #391, #392,
#393, #395, #396, #397, #399, #405, #406

---

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant