Skip to content

Integration v0.3.1: Merge Queue, Sub-Recipes, PostToolUse Reformatter, Headless Isolation#404

Merged
Trecek merged 65 commits intomainfrom
integration
Mar 15, 2026
Merged

Integration v0.3.1: Merge Queue, Sub-Recipes, PostToolUse Reformatter, Headless Isolation#404
Trecek merged 65 commits intomainfrom
integration

Conversation

@Trecek
Copy link
Collaborator

@Trecek Trecek commented Mar 15, 2026

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 (Implementation Plan: Staged Issue Lifecycle for Non-Default Branch PRs #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.yamlmerge-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: topictask rename, merge queue steps, dry_walkthrough retries:3 with forward-only routing, verifytest 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 (Rectify: collapse-issues Content Fidelity — Issue Body Data Lineage #388)
  • prepare-issue: multi-keyword dedup search, numbered candidate selection, extend-existing-issue flow
  • resolve-review: GraphQL thread auto-resolution after addressing findings (Resolve PR review conversations after addressing feedback #379)
  • resolve-merge-conflicts: conflict resolution decision report with per-file log (Implementation Plan: Merge Conflict Resolution Decision Report #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.pyis_protected_branch() pure function
  • core/github_url.pyparse_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 | Nonestr = "sonnet"

Infrastructure & Release

Version

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.ymlmerge_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

Trecek and others added 30 commits March 10, 2026 19:33
…s bugs

Add exception boundary in track_response_size decorator that catches
unhandled tool exceptions and converts them to structured JSON with
subtype "tool_exception", preventing FastMCP from wrapping them as
opaque error dicts. Fix 5 formatter bugs: clone_repo warning paths,
merge_worktree key mismatch, run_skill pipeline stderr, test_check
error key, and _fmt_generic silent nested structure drops.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Incremented the version number from 0.3.0 to 0.3.1 in `pyproject.toml`, `uv.lock`, and `plugin.json` to reflect the latest release. This ensures consistency across all relevant project files.
- Introduced a new `install` command in `cli/app.py` to facilitate plugin installation and cache refresh.
- Updated the `doctor` command to clear the plugin cache on each run, ensuring a clean state for diagnostics.
- Added a `.gitignore` file to exclude temporary files from version control.
- Adjusted the MCP server registration in `_init_helpers.py` to remove unnecessary arguments.
- Enhanced logging configuration to ensure proper output handling.

This commit improves the CLI's usability and maintains a clean project structure.
…hook

The pretty_output PostToolUse hook received {"result": "<escaped-json>"}
from Claude Code instead of the raw tool payload, causing all formatters
to silently miss every field (success, exit_code, subtype, etc.). Add
inline envelope detection and unwrap in _format_response before dispatch.

Also fix test infrastructure: add tmp_path isolation to PHK-6/7/9/10,
normalize PHK-19/28 assertions, and add 7 production-realistic wrapped-
event tests (PHK-34 through PHK-40) using _wrap_for_claude_code helper.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
## Summary

Add **Step 4.5: Historical Regression Check** to
`dry-walkthrough/SKILL.md` — inserted between "Step 4: Validate Against
Project Rules" and "Step 5: Fix the Plan". The new step runs two
lightweight scans:

1. **Git History Scan (Part A):** Queries the last 100 recent commits on
the branch for commit messages containing fix/revert/remove/replace
keywords that overlap with files or symbols the plan proposes to touch —
classifying matches as actionable (strong: symbol-level match) or
informational (weak: file-level match only).

2. **GitHub Issues Cross-Reference (Part B):** Lists open issues and
recently-closed issues (last 30 days), extracts keywords from the plan
(file basenames, function names, described changes), and
cross-references for overlaps — classifying closed-issue pattern matches
as actionable and open-issue area overlaps as informational.

Actionable findings are written directly into the plan as `> ⚠️
Historical note:` warning annotations on the affected step.
Informational findings are collected and reported in the terminal
summary under a new `### Historical Context` section appended to the
Step 7 output template. A new test file
`tests/skills/test_dry_walkthrough_contracts.py` enforces structural
invariants on the new step via 10 textual contract assertions.

## Architecture Impact

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, '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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    START([START])
    END([END])

    subgraph Loading ["Plan Loading (Step 1)"]
        direction TB
        S1["● Step 1: Load Plan<br/>━━━━━━━━━━<br/>Read from path / paste /<br/>most recent temp/ artifact"]
        MultiPart{"Multi-part<br/>plan?"}
        ScopeWarn["Scope Boundary Warning<br/>━━━━━━━━━━<br/>PART X ONLY — emit<br/>terminal notice + verify block"]
    end

    subgraph Validation ["Validation (Steps 2–4)"]
        direction TB
        S2["Step 2: Validate Phases<br/>━━━━━━━━━━<br/>Files exist, functions exist,<br/>circular deps, wiring"]
        S3["Step 3: Cross-Phase Deps<br/>━━━━━━━━━━<br/>Ordering, implicit deps,<br/>reorder opportunities"]
        S4["Step 4: Project Rules<br/>━━━━━━━━━━<br/>test-all cmd, worktree setup,<br/>arch patterns, no compat code"]
    end

    subgraph HistCheck ["★ Step 4.5: Historical Regression Check (NEW)"]
        direction TB
        ExtractFiles["★ Extract Plan Files<br/>━━━━━━━━━━<br/>Grep plan for src/**/*.py<br/>and tests/**/*.py paths"]
        GitScan["★ Git History Scan (Part A)<br/>━━━━━━━━━━<br/>git log -100 --grep=fix|revert<br/>|remove|replace|delete"]
        SignalDecision{"★ Signal<br/>Strength?"}
        StrongSignal["★ Strong Signal<br/>━━━━━━━━━━<br/>Symbol deleted in diff<br/>(def/class match)"]
        WeakSignal["★ Weak Signal<br/>━━━━━━━━━━<br/>File + keyword match<br/>no symbol overlap"]
        AuthCheck{"★ gh auth<br/>status?"}
        IssuesScan["★ GitHub Issues Scan (Part B)<br/>━━━━━━━━━━<br/>open issues + closed ≤30 days<br/>cross-ref keyword set"]
        IssueDecision{"★ Issue<br/>Match Type?"}
        ClosedMatch["★ Closed Issue Match<br/>━━━━━━━━━━<br/>Pattern was fixed;<br/>plan reintroduces it"]
        OpenMatch["★ Open Issue Overlap<br/>━━━━━━━━━━<br/>Same area tracked<br/>in open issue"]
        SkipIssues["★ Skip Issues Scan<br/>━━━━━━━━━━<br/>gh not authenticated<br/>→ informational note"]
        Actionable["★ Actionable Finding<br/>━━━━━━━━━━<br/>Insert ⚠️ warning note<br/>into affected plan step"]
        Informational["★ Informational Finding<br/>━━━━━━━━━━<br/>Collect for Historical<br/>Context terminal section"]
    end

    subgraph Resolution ["Resolution (Steps 5–7)"]
        direction TB
        S5["Step 5: Fix the Plan<br/>━━━━━━━━━━<br/>Edit plan file directly —<br/>no gap analysis sections"]
        S6["Step 6: Mark Verified<br/>━━━━━━━━━━<br/>Prepend 'Dry-walkthrough<br/>verified = TRUE'"]
        S7["● Step 7: Report to Terminal<br/>━━━━━━━━━━<br/>Summary + Changes Made +<br/>Verified + ★ Historical Context"]
    end

    subgraph TestGuard ["★ Structural Contract Guard"]
        Contracts["★ test_dry_walkthrough_contracts.py<br/>━━━━━━━━━━<br/>10 assertions: Step 4.5 presence,<br/>positioning, git log, keywords,<br/>gh auth guard, actionable/informational,<br/>Historical Context in Step 7"]
    end

    START --> S1
    S1 --> MultiPart
    MultiPart -->|"part suffix detected"| ScopeWarn
    MultiPart -->|"single plan"| S2
    ScopeWarn --> S2
    S2 --> S3 --> S4
    S4 --> ExtractFiles
    ExtractFiles --> GitScan
    ExtractFiles --> AuthCheck
    GitScan --> SignalDecision
    SignalDecision -->|"symbol-level match"| StrongSignal
    SignalDecision -->|"file + keyword only"| WeakSignal
    StrongSignal --> Actionable
    WeakSignal --> Informational
    AuthCheck -->|"authenticated"| IssuesScan
    AuthCheck -->|"not authenticated"| SkipIssues
    SkipIssues --> Informational
    IssuesScan --> IssueDecision
    IssueDecision -->|"closed issue match"| ClosedMatch
    IssueDecision -->|"open issue overlap"| OpenMatch
    ClosedMatch --> Actionable
    OpenMatch --> Informational
    Actionable --> S5
    Informational --> S7
    S5 --> S6 --> S7 --> END

    Contracts -.->|"enforces structure of"| ExtractFiles

    %% CLASS ASSIGNMENTS %%
    class START,END terminal;
    class S1,S2,S3,S4,S5,S6 handler;
    class MultiPart,SignalDecision,AuthCheck,IssueDecision stateNode;
    class ScopeWarn phase;
    class ExtractFiles,GitScan,IssuesScan,StrongSignal,WeakSignal,SkipIssues,OpenMatch,Informational newComponent;
    class Actionable,ClosedMatch detector;
    class S7 output;
    class Contracts newComponent;
```

### Scenarios Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, 'curve': 'basis'}}}%%
flowchart LR
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    subgraph S1 ["SCENARIO 1: Clean Walkthrough (No Regressions)"]
        direction LR
        S1_Entry["User:<br/>dry walkthrough<br/>plan.md"]
        S1_Steps["● SKILL.md Steps 1–4<br/>━━━━━━━━━━<br/>Load, validate phases,<br/>check cross-deps, rules"]
        S1_ExtractFiles["★ Step 4.5: Extract<br/>Plan Files<br/>━━━━━━━━━━<br/>grep src/**/*.py"]
        S1_GitScan["★ git log -100<br/>━━━━━━━━━━<br/>No fix/revert matches<br/>on plan files"]
        S1_IssuesScan["★ gh issue list<br/>━━━━━━━━━━<br/>No keyword overlaps<br/>in open/closed issues"]
        S1_Findings["★ No Findings<br/>━━━━━━━━━━<br/>'No historical regressions<br/>or overlaps detected'"]
        S1_Report["● Step 7: Report<br/>━━━━━━━━━━<br/>### Historical Context:<br/>No regressions detected"]
    end

    subgraph S2 ["SCENARIO 2: Actionable — Git Regression Signal"]
        direction LR
        S2_GitScan["★ git log -100<br/>━━━━━━━━━━<br/>Commit with 'fix:' keyword<br/>matches plan file"]
        S2_SymbolCheck["★ Symbol Match Check<br/>━━━━━━━━━━<br/>git show hash | grep '^-def'<br/>→ symbol deleted in diff"]
        S2_Warning["★ Actionable Finding<br/>━━━━━━━━━━<br/>Strong signal:<br/>symbol-level match"]
        S2_Inject["● Step 5: Plan Edit<br/>━━━━━━━━━━<br/>Insert ⚠️ Historical note<br/>into affected plan step"]
        S2_Report["● Step 7: Report<br/>━━━━━━━━━━<br/>Status: REVISED<br/>warning note injected"]
    end

    subgraph S3 ["SCENARIO 3: Informational — Open Issue Overlap"]
        direction LR
        S3_IssuesScan["★ gh issue list<br/>━━━━━━━━━━<br/>Open issue title matches<br/>plan keyword set"]
        S3_OpenMatch["★ Open Issue Match<br/>━━━━━━━━━━<br/>Informational finding:<br/>area overlap"]
        S3_Collect["★ Collect Informational<br/>━━━━━━━━━━<br/>Deferred to terminal —<br/>plan file untouched"]
        S3_Report["● Step 7: Report<br/>━━━━━━━━━━<br/>### Historical Context:<br/>Issue #N: title — verify alignment"]
    end

    subgraph S4 ["SCENARIO 4: Degraded — gh Not Authenticated"]
        direction LR
        S4_AuthCheck["★ gh auth status<br/>━━━━━━━━━━<br/>Exit code non-zero<br/>→ not authenticated"]
        S4_SkipIssues["★ Skip Issues Scan<br/>━━━━━━━━━━<br/>Record informational note:<br/>'gh not authenticated'"]
        S4_GitOnly["★ Git Scan Only<br/>━━━━━━━━━━<br/>Part A continues;<br/>Part B skipped"]
        S4_Report["● Step 7: Report<br/>━━━━━━━━━━<br/>### Historical Context:<br/>GitHub scan skipped"]
    end

    subgraph S5 ["SCENARIO 5: ★ Contract Guard — Structural Validation"]
        direction LR
        S5_Tests["★ test_dry_walkthrough_contracts.py<br/>━━━━━━━━━━<br/>10 assertions via pytest<br/>(task test-all)"]
        S5_Step45Check["★ Step 4.5 presence check<br/>━━━━━━━━━━<br/>find('Step 4.5') != -1<br/>positioned after Step 4"]
        S5_GitLogCheck["★ git log assertion<br/>━━━━━━━━━━<br/>'git log' in step_45_section<br/>fix + revert keywords present"]
        S5_GhCheck["★ gh auth guard assertion<br/>━━━━━━━━━━<br/>'gh auth' or 'authenticated'<br/>in step_45_section"]
        S5_Step7Check["★ Step 7 template check<br/>━━━━━━━━━━<br/>'Historical Context' in<br/>step_7_section → PASS"]
    end

    %% SCENARIO 1 FLOW %%
    S1_Entry --> S1_Steps --> S1_ExtractFiles
    S1_ExtractFiles --> S1_GitScan
    S1_ExtractFiles --> S1_IssuesScan
    S1_GitScan --> S1_Findings
    S1_IssuesScan --> S1_Findings
    S1_Findings --> S1_Report

    %% SCENARIO 2 FLOW %%
    S2_GitScan --> S2_SymbolCheck --> S2_Warning --> S2_Inject --> S2_Report

    %% SCENARIO 3 FLOW %%
    S3_IssuesScan --> S3_OpenMatch --> S3_Collect --> S3_Report

    %% SCENARIO 4 FLOW %%
    S4_AuthCheck --> S4_SkipIssues --> S4_GitOnly --> S4_Report

    %% SCENARIO 5 FLOW %%
    S5_Tests --> S5_Step45Check --> S5_GitLogCheck --> S5_GhCheck --> S5_Step7Check

    %% CLASS ASSIGNMENTS %%
    class S1_Entry cli;
    class S1_Steps,S1_Report,S2_Inject,S2_Report,S3_Report,S4_Report handler;
    class S1_GitScan,S2_GitScan,S3_IssuesScan,S4_AuthCheck newComponent;
    class S1_ExtractFiles,S1_IssuesScan,S1_Findings,S2_SymbolCheck,S3_OpenMatch,S3_Collect,S4_SkipIssues,S4_GitOnly newComponent;
    class S2_Warning,S5_Tests newComponent;
    class S5_Step45Check,S5_GitLogCheck,S5_GhCheck,S5_Step7Check newComponent;
    class S2_Warning detector;
```

Closes #307

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/impl-307-20260310-073834-430595/temp/make-plan/dry_walkthrough_historical_regression_check_plan_2026-03-10_074500.md`

## Token Usage Summary

# Token Summary

## implement

- input_tokens: 20233
- output_tokens: 499703
- cache_creation_input_tokens: 1532389
- cache_read_input_tokens: 99527456
- invocation_count: 16

## audit_impl

- input_tokens: 7600
- output_tokens: 134323
- cache_creation_input_tokens: 520444
- cache_read_input_tokens: 3863375
- invocation_count: 13

## open_pr

- input_tokens: 925
- output_tokens: 73212
- cache_creation_input_tokens: 247356
- cache_read_input_tokens: 4293763
- invocation_count: 4

## retry_worktree

- input_tokens: 45
- output_tokens: 22321
- cache_creation_input_tokens: 111687
- cache_read_input_tokens: 3405410
- invocation_count: 1

## make_plan

- input_tokens: 44
- output_tokens: 69854
- cache_creation_input_tokens: 121021
- cache_read_input_tokens: 2735054
- invocation_count: 1

## review_pr

- input_tokens: 8568
- output_tokens: 166539
- cache_creation_input_tokens: 420350
- cache_read_input_tokens: 5838460
- invocation_count: 7

## dry_walkthrough

- input_tokens: 57
- output_tokens: 30444
- cache_creation_input_tokens: 114452
- cache_read_input_tokens: 1679519
- invocation_count: 3

## resolve_review

- input_tokens: 404
- output_tokens: 211704
- cache_creation_input_tokens: 502896
- cache_read_input_tokens: 23247211
- invocation_count: 7

## fix

- input_tokens: 291
- output_tokens: 113181
- cache_creation_input_tokens: 415961
- cache_read_input_tokens: 19115239
- invocation_count: 6

## investigate

- input_tokens: 2053
- output_tokens: 12607
- cache_creation_input_tokens: 52059
- cache_read_input_tokens: 382017
- invocation_count: 1

## rectify

- input_tokens: 4616
- output_tokens: 21997
- cache_creation_input_tokens: 75946
- cache_read_input_tokens: 854207
- invocation_count: 1

## open_pr_step

- input_tokens: 52
- output_tokens: 27985
- cache_creation_input_tokens: 102646
- cache_read_input_tokens: 1871801
- invocation_count: 2

## analyze_prs

- input_tokens: 13
- output_tokens: 18525
- cache_creation_input_tokens: 55984
- cache_read_input_tokens: 358201
- invocation_count: 1

## merge_pr

- input_tokens: 54
- output_tokens: 11011
- cache_creation_input_tokens: 124523
- cache_read_input_tokens: 1156759
- invocation_count: 4

## create_review_pr

- input_tokens: 35
- output_tokens: 17881
- cache_creation_input_tokens: 52674
- cache_read_input_tokens: 1060679
- invocation_count: 1

## plan

- input_tokens: 6248
- output_tokens: 238067
- cache_creation_input_tokens: 765044
- cache_read_input_tokens: 9628970
- invocation_count: 9

## verify

- input_tokens: 2284
- output_tokens: 119880
- cache_creation_input_tokens: 539579
- cache_read_input_tokens: 6350120
- invocation_count: 10

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… Per-Step Elapsed Time (#318)

## Summary

This PR implements two related pipeline observability improvements from
issue #302 (combining #218 and #65):

1. **Quota guard event logging (#218)**: Instruments
`hooks/quota_check.py` to write a structured event to
`quota_events.jsonl` at every decision point (approved, blocked,
cache_miss, parse_error), giving operators a diagnostic trail for
quota-guard activity.

2. **Per-step elapsed time in token summary (#65)**: Surfaces the
`elapsed_seconds` field already stored in `TokenEntry` through two
rendering paths: the `_fmt_get_token_summary` formatter in
`pretty_output.py` (what operators see inline during pipeline runs) and
`_format_token_summary` in `tools_status.py` (what
`write_telemetry_files` writes to `token_summary.md`).

## Architecture Impact

### Data Lineage Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
flowchart LR
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    %% ── QUOTA GUARD LINEAGE ── %%
    subgraph QuotaInputs ["Quota Guard Inputs"]
        QCACHE["quota_cache.json<br/>━━━━━━━━━━<br/>utilization: float<br/>resets_at: str|null<br/>fetched_at: ISO ts"]
        HCFG["hook_config.json<br/>━━━━━━━━━━<br/>threshold: float<br/>cache_max_age: int<br/>cache_path: str"]
    end

    subgraph QuotaHook ["● quota_check.py (PreToolUse)"]
        QDECIDE["decision logic<br/>━━━━━━━━━━<br/>compare utilization<br/>vs threshold"]
        QRESOLVE["★ _resolve_quota_log_dir()<br/>━━━━━━━━━━<br/>XDG / macOS / default<br/>AUTOSKILLIT_LOG_DIR env"]
        QWRITE["★ _write_quota_event()<br/>━━━━━━━━━━<br/>event: approved|blocked<br/>|cache_miss|parse_error"]
    end

    subgraph QuotaArtifacts ["★ New Diagnostic Artifacts"]
        QLOG[("quota_events.jsonl<br/>━━━━━━━━━━<br/>append-only JSONL<br/>at log root")]
    end

    %% ── TOKEN ELAPSED LINEAGE ── %%
    subgraph TokenSource ["Token Source (unchanged)"]
        HEADLESS["headless.py<br/>━━━━━━━━━━<br/>token_log.record(<br/>  step_name,<br/>  elapsed_seconds)"]
        TOKENTRY["TokenEntry<br/>━━━━━━━━━━<br/>elapsed_seconds: float<br/>input_tokens, output_tokens<br/>invocation_count"]
    end

    subgraph TokenSurfaces ["Token Summary Surfaces"]
        GETTOK["get_token_summary<br/>━━━━━━━━━━<br/>JSON: steps[].elapsed_seconds<br/>(already present in output)"]
        PRETTYOUT["● _fmt_get_token_summary<br/>━━━━━━━━━━<br/>pretty_output.py<br/>Markdown-KV render"]
        FMTTOKEN["● _format_token_summary<br/>━━━━━━━━━━<br/>tools_status.py<br/>markdown render"]
    end

    subgraph TokenArtifacts ["Updated Artifacts"]
        PRETTYRENDER["● pretty_output render<br/>━━━━━━━━━━<br/>★ + t:{elapsed:.1f}s<br/>per step line"]
        TOKENMD["● token_summary.md<br/>━━━━━━━━━━<br/>★ + elapsed_seconds:<br/>per step section"]
    end

    %% QUOTA FLOWS %%
    QCACHE -->|"read: utilization,<br/>resets_at"| QDECIDE
    HCFG -->|"read: threshold,<br/>cache_max_age"| QDECIDE
    QDECIDE -->|"decision outcome"| QWRITE
    QRESOLVE -->|"log dir path"| QWRITE
    QWRITE -.->|"append JSON line"| QLOG

    %% TOKEN FLOWS %%
    HEADLESS -->|"record(step, elapsed)"| TOKENTRY
    TOKENTRY -->|"to_dict() all 7 fields"| GETTOK
    GETTOK -->|"steps[].elapsed_seconds"| PRETTYOUT
    GETTOK -->|"steps[].elapsed_seconds"| FMTTOKEN
    PRETTYOUT -.->|"★ renders elapsed"| PRETTYRENDER
    FMTTOKEN -.->|"★ renders elapsed"| TOKENMD

    %% CLASS ASSIGNMENTS %%
    class QCACHE,HCFG cli;
    class TOKENTRY,GETTOK stateNode;
    class QDECIDE,QRESOLVE,QWRITE handler;
    class HEADLESS phase;
    class PRETTYOUT,FMTTOKEN newComponent;
    class QLOG,TOKENMD,PRETTYRENDER output;
    class TOKENTRY stateNode;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Input | Data source files (cache, config) |
| Teal | State | In-memory token log and JSON output |
| Orange | Handler | Quota decision + event write logic |
| Purple | Phase | headless.py session executor |
| Green | Modified | Existing formatters updated to emit elapsed |
| Dark Teal | Artifacts | Write-only outputs (JSONL log, markdown files)
|

### Operational Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;

    subgraph Config ["CONFIGURATION"]
        direction LR
        ENVCACHE["AUTOSKILLIT_QUOTA_CACHE<br/>━━━━━━━━━━<br/>quota cache path override"]
        ENVLOG["AUTOSKILLIT_LOG_DIR<br/>━━━━━━━━━━<br/>log root override<br/>(XDG_DATA_HOME fallback)"]
        HOOKCFG["hook_config.json<br/>━━━━━━━━━━<br/>threshold: 90.0<br/>cache_max_age: 300s<br/>cache_path"]
    end

    subgraph Hooks ["HOOKS (Claude Code Lifecycle)"]
        direction TB
        QHOOK["● quota_check.py<br/>━━━━━━━━━━<br/>PreToolUse on run_skill<br/>approve / block decision"]
        PHOOK["● pretty_output.py<br/>━━━━━━━━━━<br/>PostToolUse on all MCP tools<br/>reformats JSON → Markdown-KV"]
    end

    subgraph MCP ["MCP TOOLS (Status & Telemetry)"]
        direction TB
        GETTOK["get_token_summary<br/>━━━━━━━━━━<br/>ungated, inline<br/>JSON: steps[].elapsed_seconds"]
        WRITETELE["write_telemetry_files<br/>━━━━━━━━━━<br/>kitchen-gated<br/>output_dir: str"]
        TOKTOOL["● tools_status.py<br/>━━━━━━━━━━<br/>_format_token_summary<br/>adds elapsed_seconds: line"]
    end

    subgraph Observability ["OBSERVABILITY OUTPUTS (Write-Only)"]
        direction TB
        QLOG["★ quota_events.jsonl<br/>━━━━━━━━━━<br/>append-only JSONL at log root<br/>event: approved|blocked|<br/>cache_miss|parse_error"]
        PRETTYRENDER["● inline token render<br/>━━━━━━━━━━<br/>step x{n} [in:X out:X cached:X t:N.Ns]<br/>Claude's tool response view"]
        TOKENMD["● token_summary.md<br/>━━━━━━━━━━<br/>## step_name<br/>- elapsed_seconds: N<br/>atomic write"]
    end

    subgraph Querying ["OPERATOR QUERY PATTERNS"]
        direction TB
        JQQUERY["jq 'select(.event==\"blocked\")'<br/>━━━━━━━━━━<br/>quota_events.jsonl<br/>filter blocked events"]
        JQCOUNT["jq -r '.event' | sort | uniq -c<br/>━━━━━━━━━━<br/>event type distribution"]
    end

    %% CONFIG → HOOKS %%
    ENVCACHE -->|"cache path"| QHOOK
    ENVLOG -->|"log root"| QHOOK
    HOOKCFG -->|"threshold, cache_max_age"| QHOOK

    %% HOOKS → OUTPUTS %%
    QHOOK -.->|"★ _write_quota_event() appends"| QLOG
    PHOOK -->|"_fmt_get_token_summary route"| PRETTYRENDER

    %% MCP → HOOK INTERACTION %%
    GETTOK -->|"JSON response → PostToolUse"| PHOOK
    GETTOK -->|"steps[].elapsed_seconds"| TOKTOOL
    WRITETELE --> TOKTOOL
    TOKTOOL -.->|"atomic write"| TOKENMD

    %% OPERATOR QUERIES %%
    QLOG -.->|"operator queries"| JQQUERY
    QLOG -.->|"operator queries"| JQCOUNT

    %% CLASS ASSIGNMENTS %%
    class ENVCACHE,ENVLOG,HOOKCFG phase;
    class QHOOK,PHOOK handler;
    class GETTOK,WRITETELE,TOKTOOL stateNode;
    class QLOG,TOKENMD,PRETTYRENDER output;
    class JQQUERY,JQCOUNT cli;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Purple | Config | Configuration sources (env vars, hook config file) |
| Orange | Hooks | Claude Code lifecycle hooks (PreToolUse, PostToolUse)
|
| Teal | MCP Tools | Status and telemetry MCP tool handlers |
| Dark Teal | Outputs | Write-only observability artifacts |
| Dark Blue | Querying | Operator query patterns and diagnostic commands
|

Closes #302

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/impl-302-20260310-073833-386833/temp/make-plan/pipeline_observability_plan_2026-03-10_120000.md`

## Token Usage Summary

# Token Summary

## implement

- input_tokens: 20233
- output_tokens: 499703
- cache_creation_input_tokens: 1532389
- cache_read_input_tokens: 99527456
- invocation_count: 16

## audit_impl

- input_tokens: 7600
- output_tokens: 134323
- cache_creation_input_tokens: 520444
- cache_read_input_tokens: 3863375
- invocation_count: 13

## open_pr

- input_tokens: 925
- output_tokens: 73212
- cache_creation_input_tokens: 247356
- cache_read_input_tokens: 4293763
- invocation_count: 4

## retry_worktree

- input_tokens: 45
- output_tokens: 22321
- cache_creation_input_tokens: 111687
- cache_read_input_tokens: 3405410
- invocation_count: 1

## make_plan

- input_tokens: 44
- output_tokens: 69854
- cache_creation_input_tokens: 121021
- cache_read_input_tokens: 2735054
- invocation_count: 1

## review_pr

- input_tokens: 8568
- output_tokens: 166539
- cache_creation_input_tokens: 420350
- cache_read_input_tokens: 5838460
- invocation_count: 7

## dry_walkthrough

- input_tokens: 57
- output_tokens: 30444
- cache_creation_input_tokens: 114452
- cache_read_input_tokens: 1679519
- invocation_count: 3

## resolve_review

- input_tokens: 404
- output_tokens: 211704
- cache_creation_input_tokens: 502896
- cache_read_input_tokens: 23247211
- invocation_count: 7

## fix

- input_tokens: 291
- output_tokens: 113181
- cache_creation_input_tokens: 415961
- cache_read_input_tokens: 19115239
- invocation_count: 6

## investigate

- input_tokens: 2053
- output_tokens: 12607
- cache_creation_input_tokens: 52059
- cache_read_input_tokens: 382017
- invocation_count: 1

## rectify

- input_tokens: 4616
- output_tokens: 21997
- cache_creation_input_tokens: 75946
- cache_read_input_tokens: 854207
- invocation_count: 1

## open_pr_step

- input_tokens: 52
- output_tokens: 27985
- cache_creation_input_tokens: 102646
- cache_read_input_tokens: 1871801
- invocation_count: 2

## analyze_prs

- input_tokens: 13
- output_tokens: 18525
- cache_creation_input_tokens: 55984
- cache_read_input_tokens: 358201
- invocation_count: 1

## merge_pr

- input_tokens: 54
- output_tokens: 11011
- cache_creation_input_tokens: 124523
- cache_read_input_tokens: 1156759
- invocation_count: 4

## create_review_pr

- input_tokens: 35
- output_tokens: 17881
- cache_creation_input_tokens: 52674
- cache_read_input_tokens: 1060679
- invocation_count: 1

## plan

- input_tokens: 6248
- output_tokens: 238067
- cache_creation_input_tokens: 765044
- cache_read_input_tokens: 9628970
- invocation_count: 9

## verify

- input_tokens: 2284
- output_tokens: 119880
- cache_creation_input_tokens: 539579
- cache_read_input_tokens: 6350120
- invocation_count: 10

## Timing Summary

# Timing Summary

## implement

- total_seconds: 11896.63371942683
- invocation_count: 16

## audit_impl

- total_seconds: 3964.286584958052
- invocation_count: 13

## open_pr

- total_seconds: 1599.6864469241118
- invocation_count: 4

## retry_worktree

- total_seconds: 483.85407130105887
- invocation_count: 1

## make_plan

- total_seconds: 1572.1653282630723
- invocation_count: 1

## review_pr

- total_seconds: 2817.96798053605
- invocation_count: 7

## dry_walkthrough

- total_seconds: 539.9472401479725
- invocation_count: 3

## resolve_review

- total_seconds: 4706.892171586864
- invocation_count: 7

## fix

- total_seconds: 3109.516113938147
- invocation_count: 6

## investigate

- total_seconds: 586.5382829050068
- invocation_count: 1

## rectify

- total_seconds: 725.7269057529047
- invocation_count: 1

## open_pr_step

- total_seconds: 579.5783970400225
- invocation_count: 2

## analyze_prs

- total_seconds: 279.4915256820386
- invocation_count: 1

## merge_pr

- total_seconds: 285.0633245370118
- invocation_count: 4

## create_review_pr

- total_seconds: 441.7614419440506
- invocation_count: 1

## plan

- total_seconds: 6489.170513134953
- invocation_count: 9

## verify

- total_seconds: 2771.1426656231615
- invocation_count: 10

## clone

- total_seconds: 5.5492194169996765
- invocation_count: 6

## capture_base_sha

- total_seconds: 0.017372207999414968
- invocation_count: 6

## create_branch

- total_seconds: 4.241269170999658
- invocation_count: 6

## push_merge_target

- total_seconds: 5.88246012000036
- invocation_count: 6

## test

- total_seconds: 497.636292823996
- invocation_count: 9

## merge

- total_seconds: 877.7062499809981
- invocation_count: 7

## push

- total_seconds: 5.489298081000015
- invocation_count: 5

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…tructure (#316)

## Summary

Add three GitHub Actions workflows to automate the full release
lifecycle:

1. **`version-bump.yml`** — On a merged PR from `integration` into
`main`, atomically bump the patch version across `pyproject.toml`,
`plugin.json`, and `uv.lock`, commit back to `main`, then fast-forward
`integration` to include the bump commit (falling back to a sync PR on
divergence).
2. **`release.yml`** — On any merged PR into `stable`, bump the minor
version (resetting patch to 0), commit to `stable`, create an annotated
`vX.Y.0` git tag, and publish a GitHub Release with auto-generated
notes.
3. **`tests/infra/test_release_workflows.py`** — Structural contract
tests that enforce the shape, guards, and constraints of both new
workflows (mirroring the existing `TestCIWorkflow` pattern).

No changes to `tests.yml` are required — `stable` already has CI on push
and PR.

## Architecture Impact

### Development Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;

    subgraph Project ["PROJECT STRUCTURE"]
        SRC["src/autoskillit/<br/>━━━━━━━━━━<br/>Python package source"]
        TESTS["tests/<br/>━━━━━━━━━━<br/>9 test directories"]
        INFRA["tests/infra/<br/>━━━━━━━━━━<br/>Structural contract tests"]
    end

    subgraph Build ["BUILD TOOLING"]
        PYPROJECT["pyproject.toml<br/>━━━━━━━━━━<br/>hatchling backend<br/>v0.3.1"]
        TASKFILE["Taskfile.yml<br/>━━━━━━━━━━<br/>test-all, test-check<br/>install-worktree"]
        UVLOCK["uv.lock<br/>━━━━━━━━━━<br/>Pinned dependencies"]
    end

    subgraph Hooks ["PRE-COMMIT QUALITY GATES"]
        RFMT["ruff-format<br/>━━━━━━━━━━<br/>Auto-format (writes)"]
        RLINT["ruff check --fix<br/>━━━━━━━━━━<br/>Lint + auto-fix"]
        MYPY["mypy src/<br/>━━━━━━━━━━<br/>Type checking"]
        UVCHECK["uv lock --check<br/>━━━━━━━━━━<br/>Lockfile freshness"]
        GITLEAKS["gitleaks v8.30.0<br/>━━━━━━━━━━<br/>Secret scanning"]
    end

    subgraph Testing ["TEST FRAMEWORK"]
        PYTEST["pytest + xdist<br/>━━━━━━━━━━<br/>-n 4 parallel workers<br/>asyncio_mode=auto"]
        NEWTEST["★ test_release_workflows.py<br/>━━━━━━━━━━<br/>TestVersionBumpWorkflow<br/>TestReleaseWorkflow"]
    end

    subgraph CI ["CI / CD WORKFLOWS"]
        TESTSWF["tests.yml<br/>━━━━━━━━━━<br/>push/PR → main, integration<br/>stable"]
        VBWF["★ version-bump.yml<br/>━━━━━━━━━━<br/>integration→main merged<br/>patch bump + sync"]
        RELWF["★ release.yml<br/>━━━━━━━━━━<br/>PR merged → stable<br/>minor bump + tag + release"]
    end

    EP["autoskillit CLI<br/>━━━━━━━━━━<br/>autoskillit.cli:main"]

    SRC --> PYPROJECT
    TESTS --> PYPROJECT
    INFRA --> PYTEST

    PYPROJECT --> TASKFILE
    PYPROJECT --> UVLOCK

    TASKFILE -->|"test-all"| PYTEST
    PYTEST --> NEWTEST

    SRC --> RFMT
    RFMT --> RLINT
    RLINT --> MYPY
    MYPY --> UVCHECK
    UVCHECK --> GITLEAKS

    PYPROJECT --> TESTSWF
    PYPROJECT --> VBWF
    PYPROJECT --> RELWF

    NEWTEST -.->|"contracts"| VBWF
    NEWTEST -.->|"contracts"| RELWF

    PYPROJECT --> EP

    class SRC,TESTS,INFRA cli;
    class PYPROJECT,UVLOCK phase;
    class TASKFILE stateNode;
    class RFMT,RLINT,MYPY,UVCHECK,GITLEAKS detector;
    class PYTEST handler;
    class NEWTEST newComponent;
    class TESTSWF output;
    class VBWF,RELWF newComponent;
    class EP output;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Structure | Project directories and source |
| Purple | Build | Build configuration and lockfile |
| Teal | Automation | Task runner (Taskfile) |
| Red | Quality Gates | Pre-commit hooks (lint, type, secrets) |
| Orange | Test Runner | pytest with parallel execution |
| Green | New (★) | New CI workflows and contract test added by this PR
|
| Dark Teal | Output | Entry points and existing CI workflow |

Closes #298

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/impl-298-20260310-073830-674423/temp/make-plan/release_ci_automation_plan_2026-03-10_080000.md`

## Token Usage Summary

# Token Summary

## implement

- input_tokens: 20233
- output_tokens: 499703
- cache_creation_input_tokens: 1532389
- cache_read_input_tokens: 99527456
- invocation_count: 16

## audit_impl

- input_tokens: 7600
- output_tokens: 134323
- cache_creation_input_tokens: 520444
- cache_read_input_tokens: 3863375
- invocation_count: 13

## open_pr

- input_tokens: 925
- output_tokens: 73212
- cache_creation_input_tokens: 247356
- cache_read_input_tokens: 4293763
- invocation_count: 4

## retry_worktree

- input_tokens: 45
- output_tokens: 22321
- cache_creation_input_tokens: 111687
- cache_read_input_tokens: 3405410
- invocation_count: 1

## make_plan

- input_tokens: 44
- output_tokens: 69854
- cache_creation_input_tokens: 121021
- cache_read_input_tokens: 2735054
- invocation_count: 1

## review_pr

- input_tokens: 8568
- output_tokens: 166539
- cache_creation_input_tokens: 420350
- cache_read_input_tokens: 5838460
- invocation_count: 7

## dry_walkthrough

- input_tokens: 57
- output_tokens: 30444
- cache_creation_input_tokens: 114452
- cache_read_input_tokens: 1679519
- invocation_count: 3

## resolve_review

- input_tokens: 404
- output_tokens: 211704
- cache_creation_input_tokens: 502896
- cache_read_input_tokens: 23247211
- invocation_count: 7

## fix

- input_tokens: 291
- output_tokens: 113181
- cache_creation_input_tokens: 415961
- cache_read_input_tokens: 19115239
- invocation_count: 6

## investigate

- input_tokens: 2053
- output_tokens: 12607
- cache_creation_input_tokens: 52059
- cache_read_input_tokens: 382017
- invocation_count: 1

## rectify

- input_tokens: 4616
- output_tokens: 21997
- cache_creation_input_tokens: 75946
- cache_read_input_tokens: 854207
- invocation_count: 1

## open_pr_step

- input_tokens: 52
- output_tokens: 27985
- cache_creation_input_tokens: 102646
- cache_read_input_tokens: 1871801
- invocation_count: 2

## analyze_prs

- input_tokens: 13
- output_tokens: 18525
- cache_creation_input_tokens: 55984
- cache_read_input_tokens: 358201
- invocation_count: 1

## merge_pr

- input_tokens: 54
- output_tokens: 11011
- cache_creation_input_tokens: 124523
- cache_read_input_tokens: 1156759
- invocation_count: 4

## create_review_pr

- input_tokens: 35
- output_tokens: 17881
- cache_creation_input_tokens: 52674
- cache_read_input_tokens: 1060679
- invocation_count: 1

## plan

- input_tokens: 6248
- output_tokens: 238067
- cache_creation_input_tokens: 765044
- cache_read_input_tokens: 9628970
- invocation_count: 9

## verify

- input_tokens: 2284
- output_tokens: 119880
- cache_creation_input_tokens: 539579
- cache_read_input_tokens: 6350120
- invocation_count: 10

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

- **install.sh**: POSIX-compatible cross-platform installer for macOS
and Ubuntu — checks Python 3.11+, uv, Claude Code, then installs from
the `stable` branch
- **README.md**: Full rewrite as a quick-start document (~100 lines
rendered). Includes all 8 recipes, ASCII pipeline diagram, key features
(zero footprint, clone isolation, dry-walkthrough gate, 7-dimension PR
review, contract cards), and links to all docs
- **docs/**: 5 new files (installation, getting-started, recipes,
architecture, cli-reference) and 2 updates (configuration, developer)
covering the full documentation surface
- **pyproject.toml**: Added `[project.urls]` section with Homepage,
Repository, Documentation, and Bug Tracker links

All factual claims verified against codebase: 36 skills, 8 recipes, 24
gated tools, 12 ungated tools, 7 audit dimensions, 8 doctor checks.

## Test plan

- [x] All 3525 tests pass (`task test-all`)
- [x] Pre-commit hooks pass (ruff, mypy, uv lock, gitleaks)
- [x] `shellcheck install.sh` — script is POSIX-compatible
- [ ] Verify all internal cross-references between docs resolve
- [ ] Verify README renders correctly on GitHub (ASCII diagram, tables,
links)
- [ ] Verify `uv lock` succeeds after pyproject.toml URL addition

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…315)

## Summary

Three related bugs from issues #264 and #255, combined in #297:

1. **`clone_repo` clones from local filesystem path** instead of the
remote URL. When `source_dir` is a local path, `clone_repo` runs `git
clone /local/path` and only sets `remote_url` afterward. The fix: detect
the remote URL before cloning and use it as the clone source, so clones
are always from the remote.

2. **`base_branch` ingredient descriptions are misleading** across
recipe YAMLs. The default is already `integration` in all recipes, but
descriptions say "defaults to current branch" — confusing the LLM
orchestrator. The fix: update descriptions to say "defaults to
integration".

3. **`cook` command launches a blank session** without showing the
recipe diagram or info. The orchestrator prompt includes raw YAML but no
diagram, and the LLM doesn't always display the preview proactively. The
fix: load (and regenerate if stale) the pre-generated ASCII diagram in
`cook`, pass it to `_build_orchestrator_prompt`, and add an explicit
FIRST ACTION instruction to display it.

## Architecture Impact

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, '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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    %% TERMINALS %%
    START_CLONE([clone_repo called])
    START_COOK([cook invoked])
    END_CLONE([clone result returned])
    END_COOK([claude session launched])
    WARN_CLONE([warning dict returned])

    subgraph CloneFlow ["● clone_repo() — workspace/clone.py"]
        direction TB

        DetectSrc["★ detect_source_dir<br/>━━━━━━━━━━<br/>git rev-parse --show-toplevel<br/>when source_dir is empty"]
        BranchDetect["detect_branch<br/>━━━━━━━━━━<br/>auto-detect current branch"]
        StrategyGate{"strategy == ''?"}
        UncommittedCheck["detect_uncommitted_changes<br/>━━━━━━━━━━<br/>abort if dirty working tree"]
        UnpublishedCheck["detect_unpublished_branch<br/>━━━━━━━━━━<br/>abort if branch not on origin"]

        GetRemoteURL["★ git remote get-url origin<br/>━━━━━━━━━━<br/>detect upstream URL<br/>before cloning (pre-clone)"]
        LsRemote{"★ branch on remote?<br/>━━━━━━━━━━<br/>ls-remote check"}
        CloneSource{"★ clone_source selected<br/>━━━━━━━━━━<br/>remote URL or local path?"}

        CloneFromRemote["★ git clone remote_url<br/>━━━━━━━━━━<br/>clones from upstream origin<br/>origin already correct"]
        CloneFromLocal["git clone source_dir<br/>━━━━━━━━━━<br/>fallback: no remote configured"]
        SetOrigin["● git remote set-url origin<br/>━━━━━━━━━━<br/>rewrite using effective_url<br/>(detected or caller override)"]
        Decontam["decontaminate clone<br/>━━━━━━━━━━<br/>untrack and remove generated files"]
    end

    subgraph CookFlow ["● cook() — cli/app.py"]
        direction TB

        RecipeSelect["recipe selection<br/>━━━━━━━━━━<br/>picker or name argument"]
        RecipeLookup["find_recipe_by_name<br/>━━━━━━━━━━<br/>locate YAML in cwd or bundled"]
        ValidateRecipe["validate_recipe<br/>━━━━━━━━━━<br/>structural validation"]

        CheckStale{"★ diagram stale?<br/>━━━━━━━━━━<br/>check_diagram_staleness<br/>YAML hash + format version"}
        GenDiagram["★ generate_recipe_diagram<br/>━━━━━━━━━━<br/>render ASCII flow<br/>write to recipes/diagrams/"]
        LoadDiagram["★ load_recipe_diagram<br/>━━━━━━━━━━<br/>read pre-generated .md<br/>returns str or None"]

        BuildPrompt["● _build_orchestrator_prompt<br/>━━━━━━━━━━<br/>diagram= param injected<br/>FIRST ACTION instruction added"]
    end

    %% CLONE FLOW %%
    START_CLONE --> DetectSrc
    DetectSrc --> BranchDetect
    BranchDetect --> StrategyGate
    StrategyGate -->|"yes — default strategy"| UncommittedCheck
    StrategyGate -->|"no — clone_local/proceed"| GetRemoteURL
    UncommittedCheck -->|"dirty: abort"| WARN_CLONE
    UncommittedCheck -->|"clean"| UnpublishedCheck
    UnpublishedCheck -->|"not on origin: abort"| WARN_CLONE
    UnpublishedCheck -->|"published"| GetRemoteURL
    GetRemoteURL --> LsRemote
    LsRemote -->|"branch exists on remote"| CloneSource
    LsRemote -->|"no remote / branch absent"| CloneSource
    CloneSource -->|"remote URL"| CloneFromRemote
    CloneSource -->|"local fallback"| CloneFromLocal
    CloneFromRemote --> SetOrigin
    CloneFromLocal --> SetOrigin
    SetOrigin --> Decontam
    Decontam --> END_CLONE

    %% COOK FLOW %%
    START_COOK --> RecipeSelect
    RecipeSelect --> RecipeLookup
    RecipeLookup --> ValidateRecipe
    ValidateRecipe --> CheckStale
    CheckStale -->|"stale or missing"| GenDiagram
    CheckStale -->|"fresh"| LoadDiagram
    GenDiagram --> LoadDiagram
    LoadDiagram --> BuildPrompt
    BuildPrompt --> END_COOK

    %% CLASS ASSIGNMENTS %%
    class START_CLONE,START_COOK,END_CLONE,END_COOK,WARN_CLONE terminal;
    class BranchDetect,UncommittedCheck,UnpublishedCheck,RecipeSelect,RecipeLookup,ValidateRecipe,Decontam,CloneFromLocal handler;
    class StrategyGate,LsRemote,CloneSource,CheckStale stateNode;
    class GetRemoteURL,CloneFromRemote,GenDiagram,LoadDiagram newComponent;
    class DetectSrc newComponent;
    class SetOrigin,BuildPrompt phase;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal | Start, end, and early-return states |
| Green | New Component (★) | New steps added by this PR |
| Purple | Modified (●) | Existing components modified by this PR |
| Orange | Handler | Existing unchanged processing steps |
| Teal | State | Decision points and routing |

### Operational Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    subgraph CLI ["● CLI ENTRY POINTS — cli/app.py"]
        direction TB
        CookCmd["● autoskillit cook<br/>━━━━━━━━━━<br/>launch a recipe session<br/>interactive picker or name arg"]
        RecipesRender["autoskillit recipes render<br/>━━━━━━━━━━<br/>pre-render all recipe diagrams<br/>writes to recipes/diagrams/"]
    end

    subgraph RecipeIngredients ["● RECIPE INGREDIENTS — recipes/*.yaml"]
        direction TB
        SourceDir["● source_dir ingredient<br/>━━━━━━━━━━<br/>desc: Remote URL for source repo<br/>(auto-detected via git remote get-url origin)"]
        BaseBranch["● base_branch ingredient<br/>━━━━━━━━━━<br/>desc: Integration branch to merge into<br/>(defaults to integration)"]
    end

    subgraph CookSession ["● cook() WORKFLOW — cli/app.py"]
        direction TB
        Picker["recipe picker / name resolve<br/>━━━━━━━━━━<br/>find_recipe_by_name + validate_recipe"]
        StaleCheck{"★ check_diagram_staleness<br/>━━━━━━━━━━<br/>YAML hash + format version"}
        DiagramRegen["★ generate_recipe_diagram<br/>━━━━━━━━━━<br/>best-effort, OSError silenced"]
        DiagramLoad["★ load_recipe_diagram<br/>━━━━━━━━━━<br/>reads .md → str or None"]
        PromptBuild["● _build_orchestrator_prompt<br/>━━━━━━━━━━<br/>diagram= kwarg injected<br/>FIRST ACTION block added"]
        Session["claude --append-system-prompt<br/>━━━━━━━━━━<br/>headless recipe session"]
    end

    subgraph SessionDisplay ["★ SESSION FIRST ACTION (NEW)"]
        direction TB
        DiagramDisplay["★ display recipe diagram<br/>━━━━━━━━━━<br/>shown to user before inputs<br/>enforced by prompt instruction"]
        IngredientPrompt["prompt for ingredient values<br/>━━━━━━━━━━<br/>AskUserQuestion<br/>source_dir + base_branch now clearer"]
    end

    subgraph DiagramFiles ["RECIPE DIAGRAM FILES"]
        direction TB
        DiagramStore["recipes/diagrams/{name}.md<br/>━━━━━━━━━━<br/>pre-generated ASCII flow<br/>staleness-checked on cook"]
    end

    %% FLOWS %%
    CookCmd --> Picker
    RecipesRender --> DiagramStore
    Picker --> StaleCheck
    StaleCheck -->|"stale"| DiagramRegen
    StaleCheck -->|"fresh"| DiagramLoad
    DiagramRegen --> DiagramStore
    DiagramStore --> DiagramLoad
    DiagramLoad --> PromptBuild
    PromptBuild --> Session
    Session --> DiagramDisplay
    DiagramDisplay --> IngredientPrompt
    IngredientPrompt --> SourceDir
    IngredientPrompt --> BaseBranch

    %% CLASS ASSIGNMENTS %%
    class CookCmd,RecipesRender cli;
    class Picker,Session handler;
    class StaleCheck stateNode;
    class DiagramRegen,DiagramLoad,DiagramDisplay newComponent;
    class PromptBuild phase;
    class SourceDir,BaseBranch phase;
    class DiagramStore output;
    class IngredientPrompt handler;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | CLI | Command-line entry points |
| Green | New (★) | New capabilities added by this PR |
| Purple | Modified (●) | Existing components modified by this PR |
| Orange | Handler | Existing unchanged processing steps |
| Teal | State | Decision points |
| Dark Teal | Output | Stored artifacts (diagram files) |

Closes #297

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/impl-297-20260310-073829-654395/temp/make-plan/issue_297_source_dir_and_cook_display_plan_2026-03-10_080000.md`

## Token Usage Summary

# Token Summary

## implement

- input_tokens: 20233
- output_tokens: 499703
- cache_creation_input_tokens: 1532389
- cache_read_input_tokens: 99527456
- invocation_count: 16

## audit_impl

- input_tokens: 7600
- output_tokens: 134323
- cache_creation_input_tokens: 520444
- cache_read_input_tokens: 3863375
- invocation_count: 13

## open_pr

- input_tokens: 925
- output_tokens: 73212
- cache_creation_input_tokens: 247356
- cache_read_input_tokens: 4293763
- invocation_count: 4

## retry_worktree

- input_tokens: 45
- output_tokens: 22321
- cache_creation_input_tokens: 111687
- cache_read_input_tokens: 3405410
- invocation_count: 1

## make_plan

- input_tokens: 44
- output_tokens: 69854
- cache_creation_input_tokens: 121021
- cache_read_input_tokens: 2735054
- invocation_count: 1

## review_pr

- input_tokens: 8568
- output_tokens: 166539
- cache_creation_input_tokens: 420350
- cache_read_input_tokens: 5838460
- invocation_count: 7

## dry_walkthrough

- input_tokens: 57
- output_tokens: 30444
- cache_creation_input_tokens: 114452
- cache_read_input_tokens: 1679519
- invocation_count: 3

## resolve_review

- input_tokens: 404
- output_tokens: 211704
- cache_creation_input_tokens: 502896
- cache_read_input_tokens: 23247211
- invocation_count: 7

## fix

- input_tokens: 291
- output_tokens: 113181
- cache_creation_input_tokens: 415961
- cache_read_input_tokens: 19115239
- invocation_count: 6

## investigate

- input_tokens: 2053
- output_tokens: 12607
- cache_creation_input_tokens: 52059
- cache_read_input_tokens: 382017
- invocation_count: 1

## rectify

- input_tokens: 4616
- output_tokens: 21997
- cache_creation_input_tokens: 75946
- cache_read_input_tokens: 854207
- invocation_count: 1

## open_pr_step

- input_tokens: 52
- output_tokens: 27985
- cache_creation_input_tokens: 102646
- cache_read_input_tokens: 1871801
- invocation_count: 2

## analyze_prs

- input_tokens: 13
- output_tokens: 18525
- cache_creation_input_tokens: 55984
- cache_read_input_tokens: 358201
- invocation_count: 1

## merge_pr

- input_tokens: 54
- output_tokens: 11011
- cache_creation_input_tokens: 124523
- cache_read_input_tokens: 1156759
- invocation_count: 4

## create_review_pr

- input_tokens: 35
- output_tokens: 17881
- cache_creation_input_tokens: 52674
- cache_read_input_tokens: 1060679
- invocation_count: 1

## plan

- input_tokens: 6248
- output_tokens: 238067
- cache_creation_input_tokens: 765044
- cache_read_input_tokens: 9628970
- invocation_count: 9

## verify

- input_tokens: 2284
- output_tokens: 119880
- cache_creation_input_tokens: 539579
- cache_read_input_tokens: 6350120
- invocation_count: 10

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ating, and Review-First Enforcement (#317)

## Summary

Three connected enhancements to the PR review pipeline, collapsing
issues #277, #278, and #294:

1. **Fidelity dimension** (`review-pr`): A new 8th audit subagent that
extracts linked GitHub issue numbers from the PR body and commit
messages, fetches their requirements, then reports gaps (unaddressed
requirements) and drift (unrequested changes) against the actual diff.

2. **PR eligibility gates** (`analyze-prs`): Before ordering PRs for
merge, filter the candidate list to exclude PRs with failing CI checks
(`gh pr checks`) and PRs where an unresolved `changes_requested` review
exists. Excluded PRs are reported in the output manifest but not queued
for merge.

3. **Commit status gate** (`set_commit_status` MCP tool + `review-pr`
updates): A new kitchen-gated MCP tool that posts GitHub Commit Status
API entries. The `review-pr` skill uses it to post `pending` at review
start and `success`/`failure` at completion. When
`autoskillit/ai-review` is configured as a required status check in
GitHub branch protection, this enforces the review-first constraint —
merge is blocked until the AI review resolves.

## Architecture Impact

### Module Dependency Diagram

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

    subgraph L3 ["L3 — SERVER LAYER"]
        direction LR
        ServerInit["● server/__init__.py<br/>━━━━━━━━━━<br/>FastMCP app + tool registration<br/>fan-in: entry point"]
        ToolsCI["● server/tools_ci.py<br/>━━━━━━━━━━<br/>wait_for_ci, get_ci_status<br/>★ set_commit_status (new tool)"]
        ServerHelpers["server/helpers.py<br/>━━━━━━━━━━<br/>_run_subprocess, _notify<br/>_require_enabled"]
    end

    subgraph L1P ["L1 — PIPELINE LAYER"]
        direction LR
        PipelineInit["pipeline/__init__.py<br/>━━━━━━━━━━<br/>Re-exports: ToolContext<br/>DefaultGateState, AuditLog<br/>TokenLog, GATED_TOOLS"]
        Fidelity["★ pipeline/fidelity.py<br/>━━━━━━━━━━<br/>extract_linked_issues()<br/>is_valid_fidelity_finding()<br/>stdlib only (re)"]
        PRGates["★ pipeline/pr_gates.py<br/>━━━━━━━━━━<br/>is_ci_passing()<br/>is_review_passing()<br/>partition_prs()<br/>no imports"]
    end

    subgraph L0 ["L0 — CORE LAYER"]
        direction LR
        CoreTypes["● core/types.py<br/>━━━━━━━━━━<br/>GATED_TOOLS, UNGATED_TOOLS<br/>StrEnums, Protocols<br/>★ +set_commit_status"]
        CoreInit["core/__init__.py<br/>━━━━━━━━━━<br/>get_logger, configure_logging<br/>FailureRecord, SubprocessRunner"]
    end

    subgraph EXT ["EXTERNAL"]
        direction LR
        FastMCP["fastmcp<br/>━━━━━━━━━━<br/>Context, FastMCP"]
        Structlog["structlog<br/>━━━━━━━━━━<br/>structured logging"]
        Stdlib["stdlib<br/>━━━━━━━━━━<br/>asyncio, json, re<br/>typing, Literal"]
    end

    subgraph Tests ["TESTS (fan-in consumers)"]
        direction LR
        TestFidelity["★ tests/test_review_pr_fidelity.py<br/>━━━━━━━━━━<br/>imports pipeline.fidelity"]
        TestGates["★ tests/test_analyze_prs_gates.py<br/>━━━━━━━━━━<br/>imports pipeline.pr_gates"]
        TestCI["★ tests/server/test_set_commit_status.py<br/>━━━━━━━━━━<br/>imports server tools_ci"]
    end

    ServerInit -->|"imports all tools_*"| ToolsCI
    ServerInit -->|"imports pipeline surface"| PipelineInit
    ServerInit -->|"imports core"| CoreInit
    ToolsCI -->|"imports core.get_logger"| CoreInit
    ToolsCI -->|"imports server.mcp"| ServerInit
    ToolsCI -->|"imports helpers"| ServerHelpers
    PipelineInit -->|"imports types"| CoreTypes
    ServerHelpers -->|"imports core"| CoreInit
    ToolsCI -->|"asyncio, json, Literal"| Stdlib
    ToolsCI --> FastMCP
    ToolsCI --> Structlog
    Fidelity -->|"re"| Stdlib
    TestFidelity -.->|"test imports"| Fidelity
    TestGates -.->|"test imports"| PRGates
    TestCI -.->|"test imports"| ToolsCI
    Fidelity -.-|"NOT re-exported<br/>standalone module"| PipelineInit
    PRGates -.-|"NOT re-exported<br/>standalone module"| PipelineInit

    class ServerInit,ServerHelpers handler;
    class ToolsCI phase;
    class PipelineInit stateNode;
    class Fidelity,PRGates,TestFidelity,TestGates,TestCI newComponent;
    class CoreTypes,CoreInit cli;
    class FastMCP,Structlog,Stdlib integration;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Core (L0) | Foundation modules, zero autoskillit imports |
| Teal | Pipeline Init | Pipeline public surface, re-exports |
| Purple | Server Tools | MCP tool handler modules |
| Orange | Server | Server orchestration and helpers |
| Green (★) | New/Modified | New modules and test files added in this PR
|
| Red | External | Third-party dependencies (fastmcp, structlog, stdlib)
|
| Dashed | Isolation | Modules not re-exported through package surface |

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, '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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;

    A_START([★ analyze-prs START])
    A_END([★ analyze-prs END])

    subgraph AnalyzePRS ["● analyze-prs: Gate Filtering Phase (new Step 1.5)"]
        direction TB
        A_FetchPRs["Fetch all open PRs<br/>━━━━━━━━━━<br/>gh pr list --base branch --state open<br/>→ ALL_PRS"]
        A_Loop["Per-PR gate loop<br/>━━━━━━━━━━<br/>for PR in ALL_PRS"]
        A_CIFetch["★ Fetch CI checks<br/>━━━━━━━━━━<br/>gh pr checks {PR} --json<br/>name,status,conclusion"]
        A_CIGate{"★ CI Gate<br/>━━━━━━━━━━<br/>FAILING > 0 OR<br/>IN_PROGRESS > 0?"}
        A_ReviewFetch["★ Fetch reviews<br/>━━━━━━━━━━<br/>gh pr view {PR} --json reviews"]
        A_ReviewGate{"★ Review Gate<br/>━━━━━━━━━━<br/>CHANGES_REQUESTED<br/>> 0?"}
        A_Eligible["★ ELIGIBLE_PRS +=<br/>━━━━━━━━━━<br/>Passes both gates<br/>→ proceeds to ordering"]
        A_CIBlock["★ CI_BLOCKED_PRS +=<br/>━━━━━━━━━━<br/>reason: N failed<br/>M in-progress"]
        A_RevBlock["★ REVIEW_BLOCKED_PRS +=<br/>━━━━━━━━━━<br/>reason: N unresolved<br/>CHANGES_REQUESTED"]
        A_Manifest["★ Extended manifest<br/>━━━━━━━━━━<br/>eligible_prs array<br/>ci_blocked_prs array<br/>review_blocked_prs array"]
    end

    A_START --> A_FetchPRs --> A_Loop --> A_CIFetch
    A_CIFetch --> A_CIGate
    A_CIGate -->|"yes — CI failing"| A_CIBlock
    A_CIGate -->|"no — CI passing"| A_ReviewFetch
    A_ReviewFetch --> A_ReviewGate
    A_ReviewGate -->|"yes — blocked"| A_RevBlock
    A_ReviewGate -->|"no — eligible"| A_Eligible
    A_CIBlock -->|"continue loop"| A_Loop
    A_RevBlock -->|"continue loop"| A_Loop
    A_Eligible -->|"continue loop"| A_Loop
    A_Loop -->|"loop done"| A_Manifest
    A_Manifest --> A_END

    R_START([★ review-pr START])
    R_END([★ review-pr END])

    subgraph ReviewPR_Status ["● review-pr: Commit Status Lifecycle (new Steps 0.8 → 2 → 7.5)"]
        direction TB
        R_Init["★ Step 0.8: Declare variables<br/>━━━━━━━━━━<br/>PR_SHA = empty<br/>OWNER_REPO = empty"]
        R_Resolve["● Step 2: Resolve PR metadata<br/>━━━━━━━━━━<br/>gh pr view → OWNER_REPO<br/>gh pr view headRefOid → PR_SHA"]
        R_Pending["★ Step 2: Post pending status<br/>━━━━━━━━━━<br/>POST /statuses/{PR_SHA}<br/>state=pending<br/>context=autoskillit/ai-review<br/>best-effort (|| true)"]
        R_Fidelity_Gate{"★ Step 2.8: Linked issues?<br/>━━━━━━━━━━<br/>LINKED_ISSUES empty?"}
        R_FetchIssues["★ Fetch linked issues<br/>━━━━━━━━━━<br/>gh issue view N --json<br/>title,body,comments"]
        R_FidelityAgent["★ Fidelity subagent<br/>━━━━━━━━━━<br/>Compare diff vs requirements<br/>gaps (critical)<br/>drift (warning)"]
        R_SkipFidelity["Skip fidelity subagent<br/>━━━━━━━━━━<br/>LINKED_ISSUE_DATA = '[]'"]
        R_Audit["● Steps 3-7: Standard audit<br/>━━━━━━━━━━<br/>6 existing subagents<br/>+ optional fidelity findings<br/>→ verdict"]
        R_VerdictGate{"★ Step 7.5: Verdict?<br/>━━━━━━━━━━<br/>approved?<br/>changes_requested?<br/>needs_human?"}
        R_Success["★ POST state=success<br/>━━━━━━━━━━<br/>AI review approved<br/>→ merge unblocked"]
        R_Failure["★ POST state=failure<br/>━━━━━━━━━━<br/>AI review: changes required<br/>→ merge blocked"]
        R_StillPending["★ POST state=pending<br/>━━━━━━━━━━<br/>Human decision required<br/>→ gate stays unresolved"]
        R_GitHubAPI["GitHub Commit Status API<br/>━━━━━━━━━━<br/>/repos/{owner}/{repo}<br/>/statuses/{sha}<br/>context: autoskillit/ai-review"]
    end

    R_START --> R_Init --> R_Resolve --> R_Pending
    R_Pending -.->|"best-effort POST"| R_GitHubAPI
    R_Pending --> R_Fidelity_Gate
    R_Fidelity_Gate -->|"non-empty"| R_FetchIssues
    R_Fidelity_Gate -->|"empty"| R_SkipFidelity
    R_FetchIssues --> R_FidelityAgent
    R_FidelityAgent --> R_Audit
    R_SkipFidelity --> R_Audit
    R_Audit --> R_VerdictGate
    R_VerdictGate -->|"approved"| R_Success
    R_VerdictGate -->|"changes_requested"| R_Failure
    R_VerdictGate -->|"needs_human"| R_StillPending
    R_Success -.->|"POST success"| R_GitHubAPI
    R_Failure -.->|"POST failure"| R_GitHubAPI
    R_StillPending -.->|"POST pending"| R_GitHubAPI
    R_Success --> R_END
    R_Failure --> R_END
    R_StillPending --> R_END

    class A_START,A_END,R_START,R_END terminal;
    class A_FetchPRs,A_Loop,A_CIFetch,A_ReviewFetch,R_Resolve,R_Audit handler;
    class A_Manifest,R_Init output;
    class A_CIGate,A_ReviewGate,R_VerdictGate,R_Fidelity_Gate stateNode;
    class A_CIBlock,A_RevBlock detector;
    class A_Eligible,R_Pending,R_FetchIssues,R_FidelityAgent,R_Success,R_Failure,R_StillPending newComponent;
    class R_SkipFidelity phase;
    class R_GitHubAPI integration;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal | Start and end states |
| Orange | Handler | Existing processing steps (fetch, resolve, audit) |
| Teal | Decision | Decision/routing gates |
| Red | Detector | Blocked PR pools (CI-blocked, review-blocked) |
| Green (★) | New Step | New nodes added in this PR |
| Purple | Phase | Skip/bypass paths |
| Dark Teal | Output | Manifest and variable declarations |
| Dark Red | Integration | GitHub Commit Status API (external) |
| Dashed Lines | Best-Effort | Posts that never block the workflow |

### C4 Container Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%%
graph TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;
    classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;

    USER(["Orchestrator / Recipe<br/>━━━━━━━━━━<br/>Invokes skills via run_skill<br/>or MCP tool calls"])

    subgraph Skills ["SKILL LAYER — Bash/SKILL.md"]
        direction LR
        AnalyzePRS["● analyze-prs skill<br/>━━━━━━━━━━<br/>Bash, SKILL.md<br/>PR ordering + CI/review gates<br/>Outputs extended manifest"]
        ReviewPR["● review-pr skill<br/>━━━━━━━━━━<br/>Bash, SKILL.md<br/>Diff audit + fidelity subagent<br/>Posts commit status"]
    end

    subgraph MCPServer ["MCP SERVER LAYER — FastMCP / Python"]
        direction LR
        ToolsCI["● server/tools_ci.py<br/>━━━━━━━━━━<br/>FastMCP, Python<br/>wait_for_ci, get_ci_status<br/>★ set_commit_status"]
        SetCommitStatus["★ set_commit_status tool<br/>━━━━━━━━━━<br/>Python, gh api<br/>Posts to /statuses/{sha}<br/>context: autoskillit/ai-review<br/>states: pending/success/failure"]
    end

    subgraph Pipeline ["PIPELINE LAYER — Pure Python"]
        direction LR
        Fidelity["★ pipeline/fidelity.py<br/>━━━━━━━━━━<br/>Python (stdlib: re)<br/>extract_linked_issues()<br/>is_valid_fidelity_finding()"]
        PRGates["★ pipeline/pr_gates.py<br/>━━━━━━━━━━<br/>Python (no imports)<br/>is_ci_passing()<br/>is_review_passing()<br/>partition_prs()"]
    end

    subgraph CoreLayer ["CORE LAYER — L0 Foundation"]
        direction LR
        CoreTypes["● core/types.py<br/>━━━━━━━━━━<br/>Python (stdlib)<br/>GATED_TOOLS frozenset<br/>★ +set_commit_status entry"]
    end

    subgraph External ["EXTERNAL — GitHub API (REST/HTTPS)"]
        direction LR
        CommitStatusAPI["★ GitHub Commit Status API<br/>━━━━━━━━━━<br/>REST, HTTPS<br/>POST /repos/{owner}/{repo}<br/>/statuses/{sha}<br/>Stores per-commit status"]
        PRChecksAPI["GitHub PR Checks + Reviews API<br/>━━━━━━━━━━<br/>REST/gh CLI<br/>gh pr checks, gh pr view<br/>gh issue view"]
        BranchProtection["GitHub Branch Protection<br/>━━━━━━━━━━<br/>GitHub UI configuration<br/>Required check: autoskillit/ai-review<br/>Blocks merge until resolved"]
    end

    USER -->|"run_skill / mcp call"| AnalyzePRS
    USER -->|"run_skill / mcp call"| ReviewPR
    AnalyzePRS -->|"gh pr checks (CI gate)"| PRChecksAPI
    AnalyzePRS -->|"gh pr view reviews (review gate)"| PRChecksAPI
    AnalyzePRS -.->|"logic reference"| PRGates
    ReviewPR -->|"gh pr view / gh issue view"| PRChecksAPI
    ReviewPR -->|"POST pending + final status"| CommitStatusAPI
    ReviewPR -.->|"logic reference"| Fidelity
    ToolsCI -->|"hosts"| SetCommitStatus
    SetCommitStatus -->|"gh api POST"| CommitStatusAPI
    CommitStatusAPI -.->|"required status check"| BranchProtection
    ToolsCI -->|"imports"| CoreTypes
    PRGates -.->|"no imports — pure logic"| CoreLayer
    Fidelity -.->|"stdlib re only"| CoreLayer

    class USER terminal;
    class AnalyzePRS,ReviewPR phase;
    class ToolsCI handler;
    class SetCommitStatus,Fidelity,PRGates newComponent;
    class CoreTypes cli;
    class CommitStatusAPI,BranchProtection integration;
    class PRChecksAPI detector;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal/Core | Orchestrator entry point and L0 foundation
types |
| Purple | Skills | Bash skill containers (analyze-prs, review-pr) |
| Orange | MCP Server | FastMCP tool host layer |
| Green (★) | New Containers | New building blocks added in this PR |
| Red | External | GitHub REST API endpoints and branch protection |
| Dashed Lines | Logic Reference | Pure-Python utility modules /
best-effort connections |

Closes #300

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/impl-300-20260310-073831-576901/temp/make-plan/pr_review_pipeline_gates_plan_2026-03-10_120000.md`

## Token Usage Summary

# Token Summary

## implement

- input_tokens: 20233
- output_tokens: 499703
- cache_creation_input_tokens: 1532389
- cache_read_input_tokens: 99527456
- invocation_count: 16

## audit_impl

- input_tokens: 7600
- output_tokens: 134323
- cache_creation_input_tokens: 520444
- cache_read_input_tokens: 3863375
- invocation_count: 13

## open_pr

- input_tokens: 925
- output_tokens: 73212
- cache_creation_input_tokens: 247356
- cache_read_input_tokens: 4293763
- invocation_count: 4

## retry_worktree

- input_tokens: 45
- output_tokens: 22321
- cache_creation_input_tokens: 111687
- cache_read_input_tokens: 3405410
- invocation_count: 1

## make_plan

- input_tokens: 44
- output_tokens: 69854
- cache_creation_input_tokens: 121021
- cache_read_input_tokens: 2735054
- invocation_count: 1

## review_pr

- input_tokens: 8568
- output_tokens: 166539
- cache_creation_input_tokens: 420350
- cache_read_input_tokens: 5838460
- invocation_count: 7

## dry_walkthrough

- input_tokens: 57
- output_tokens: 30444
- cache_creation_input_tokens: 114452
- cache_read_input_tokens: 1679519
- invocation_count: 3

## resolve_review

- input_tokens: 404
- output_tokens: 211704
- cache_creation_input_tokens: 502896
- cache_read_input_tokens: 23247211
- invocation_count: 7

## fix

- input_tokens: 291
- output_tokens: 113181
- cache_creation_input_tokens: 415961
- cache_read_input_tokens: 19115239
- invocation_count: 6

## investigate

- input_tokens: 2053
- output_tokens: 12607
- cache_creation_input_tokens: 52059
- cache_read_input_tokens: 382017
- invocation_count: 1

## rectify

- input_tokens: 4616
- output_tokens: 21997
- cache_creation_input_tokens: 75946
- cache_read_input_tokens: 854207
- invocation_count: 1

## open_pr_step

- input_tokens: 52
- output_tokens: 27985
- cache_creation_input_tokens: 102646
- cache_read_input_tokens: 1871801
- invocation_count: 2

## analyze_prs

- input_tokens: 13
- output_tokens: 18525
- cache_creation_input_tokens: 55984
- cache_read_input_tokens: 358201
- invocation_count: 1

## merge_pr

- input_tokens: 54
- output_tokens: 11011
- cache_creation_input_tokens: 124523
- cache_read_input_tokens: 1156759
- invocation_count: 4

## create_review_pr

- input_tokens: 35
- output_tokens: 17881
- cache_creation_input_tokens: 52674
- cache_read_input_tokens: 1060679
- invocation_count: 1

## plan

- input_tokens: 6248
- output_tokens: 238067
- cache_creation_input_tokens: 765044
- cache_read_input_tokens: 9628970
- invocation_count: 9

## verify

- input_tokens: 2284
- output_tokens: 119880
- cache_creation_input_tokens: 539579
- cache_read_input_tokens: 6350120
- invocation_count: 10

## Timing Summary

# Timing Summary

## implement

- total_seconds: 11896.63371942683
- invocation_count: 16

## audit_impl

- total_seconds: 3964.286584958052
- invocation_count: 13

## open_pr

- total_seconds: 1599.6864469241118
- invocation_count: 4

## retry_worktree

- total_seconds: 483.85407130105887
- invocation_count: 1

## make_plan

- total_seconds: 1572.1653282630723
- invocation_count: 1

## review_pr

- total_seconds: 2817.96798053605
- invocation_count: 7

## dry_walkthrough

- total_seconds: 539.9472401479725
- invocation_count: 3

## resolve_review

- total_seconds: 4706.892171586864
- invocation_count: 7

## fix

- total_seconds: 3109.516113938147
- invocation_count: 6

## investigate

- total_seconds: 586.5382829050068
- invocation_count: 1

## rectify

- total_seconds: 725.7269057529047
- invocation_count: 1

## open_pr_step

- total_seconds: 579.5783970400225
- invocation_count: 2

## analyze_prs

- total_seconds: 279.4915256820386
- invocation_count: 1

## merge_pr

- total_seconds: 285.0633245370118
- invocation_count: 4

## create_review_pr

- total_seconds: 441.7614419440506
- invocation_count: 1

## plan

- total_seconds: 6489.170513134953
- invocation_count: 9

## verify

- total_seconds: 2771.1426656231615
- invocation_count: 10

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…nto integration (#323)

## Integration Summary

Collapsed 7 PRs into `pr-batch/pr-merge-20260310-163009` targeting
`integration`.

## Merged PRs

| # | Title | Complexity | Additions | Deletions | Overlaps |
|---|-------|-----------|-----------|-----------|---------|
| #319 | Add Step 4.5 Historical Regression Check to dry-walkthrough
skill | simple | +200 | -0 | — |
| #318 | Implementation Plan: Pipeline Observability — Quota Guard
Logging and Per-Step Elapsed Time | simple | +267 | -23 | — |
| #316 | Release CI Automation — Version Bump, Branch Sync, and Release
Infrastructure | simple | +471 | -0 | — |
| #314 | docs: complete release documentation sprint | simple | +1344 |
-104 | — |
| #315 | Recipe remediation — source_dir resolution and cook command
display | simple | +317 | -71 | #320 |
| #317 | Implementation Plan: PR Review Pipeline Gates — Fidelity
Checks, CI Gating, and Review-First Enforcement | simple | +1057 | -7 |
#320 |
| #320 | feat: add recipe composition with run_recipe tool and
dev-sprint consumer | needs_check | +724 | -19 | #315, #317 |

## Audit

**Verdict:** GO

## Architecture Impact

### Development Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;

    subgraph Structure ["PROJECT STRUCTURE"]
        direction LR
        SRC["src/autoskillit/<br/>━━━━━━━━━━<br/>105 source files<br/>10 sub-packages"]
        TESTS["tests/<br/>━━━━━━━━━━<br/>170 test files<br/>Mirrors src layout"]
    end

    subgraph Build ["BUILD TOOLING"]
        direction LR
        PYPROJECT["● pyproject.toml<br/>━━━━━━━━━━<br/>hatchling backend<br/>uv package manager"]
        TASKFILE["Taskfile.yml<br/>━━━━━━━━━━<br/>task test-all<br/>task test-check<br/>task install-worktree"]
        UVLOCK["uv.lock<br/>━━━━━━━━━━<br/>Locked manifest"]
    end

    subgraph PreCommit ["PRE-COMMIT HOOKS"]
        direction TB
        RUFF_FMT["ruff-format<br/>━━━━━━━━━━<br/>Auto-format<br/>(writes source)"]
        RUFF_LINT["ruff check<br/>━━━━━━━━━━<br/>Lint --fix<br/>target: py311"]
        MYPY["mypy<br/>━━━━━━━━━━<br/>src/ type check<br/>--ignore-missing"]
        UVCHECK["uv lock --check<br/>━━━━━━━━━━<br/>Lockfile guard"]
        NOGEN["no-generated-configs<br/>━━━━━━━━━━<br/>Blocks hooks.json<br/>recipes/diagrams/"]
        GITLEAKS["gitleaks v8.30.0<br/>━━━━━━━━━━<br/>Secret scanning"]
    end

    subgraph Testing ["TEST FRAMEWORK"]
        direction LR
        PYTEST["pytest -n 4<br/>━━━━━━━━━━<br/>asyncio_mode=auto<br/>timeout=60s"]
        FIXTURES["conftest.py<br/>━━━━━━━━━━<br/>StatefulMockTester<br/>MockSubprocessRunner<br/>tool_ctx fixture"]
        IMPORTLINT["import-linter<br/>━━━━━━━━━━<br/>L0→L1→L2→L3<br/>Architecture gates"]
    end

    subgraph NewTests ["★ NEW TEST FILES"]
        direction LR
        TGATE["★ test_analyze_prs_gates.py<br/>━━━━━━━━━━<br/>PR gate analysis"]
        TFIDELITY["★ test_review_pr_fidelity.py<br/>━━━━━━━━━━<br/>Fidelity checks"]
        TRELEASE["★ test_release_workflows.py<br/>━━━━━━━━━━<br/>CI workflow tests"]
        TDRY["★ test_dry_walkthrough_contracts.py<br/>━━━━━━━━━━<br/>10 contract assertions"]
    end

    subgraph CI ["CI/CD WORKFLOWS"]
        direction TB
        TESTS_CI["tests.yml<br/>━━━━━━━━━━<br/>ubuntu + macos matrix<br/>preflight: uv lock --check<br/>uv sync + task test-all"]
        VBUMP["★ version-bump.yml<br/>━━━━━━━━━━<br/>integration→main merged<br/>MAJOR.MINOR.PATCH+1<br/>sync main→integration"]
        RELEASE["★ release.yml<br/>━━━━━━━━━━<br/>stable branch merge<br/>MAJOR.MINOR+1.0<br/>git tag + GitHub Release"]
    end

    subgraph EntryPoints ["ENTRY POINTS"]
        direction LR
        CLI_EP["autoskillit CLI<br/>━━━━━━━━━━<br/>autoskillit.cli:main<br/>● app.py modified"]
        INSTALL["★ install.sh<br/>━━━━━━━━━━<br/>End-user installer<br/>uv tool install<br/>autoskillit install"]
    end

    %% FLOW %%
    SRC --> PYPROJECT
    TESTS --> PYPROJECT
    PYPROJECT --> TASKFILE
    PYPROJECT --> UVLOCK

    TASKFILE --> RUFF_FMT
    RUFF_FMT --> RUFF_LINT
    RUFF_LINT --> MYPY
    MYPY --> UVCHECK
    UVCHECK --> NOGEN
    NOGEN --> GITLEAKS

    TASKFILE --> PYTEST
    PYTEST --> FIXTURES
    PYTEST --> IMPORTLINT
    PYTEST --> TGATE
    PYTEST --> TFIDELITY
    PYTEST --> TRELEASE
    PYTEST --> TDRY

    PYPROJECT --> TESTS_CI
    TESTS_CI --> VBUMP
    VBUMP --> RELEASE

    PYPROJECT --> CLI_EP
    INSTALL --> CLI_EP

    %% CLASS ASSIGNMENTS %%
    class SRC,TESTS cli;
    class PYPROJECT,TASKFILE,UVLOCK phase;
    class RUFF_FMT,RUFF_LINT,MYPY,UVCHECK,NOGEN,GITLEAKS detector;
    class PYTEST,FIXTURES,IMPORTLINT handler;
    class TGATE,TFIDELITY,TRELEASE,TDRY,VBUMP,RELEASE,INSTALL newComponent;
    class TESTS_CI stateNode;
    class CLI_EP output;
```

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, '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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;

    %% TERMINALS %%
    START([START])
    SUCCESS([SUCCESS])
    VALFAIL([VALIDATION ERROR])

    subgraph Val ["Load-Time Validation"]
        direction LR
        ValidateRecipe["validate_recipe<br/>━━━━━━━━━━<br/>recipe/_api.py"]
        RulesRecipe["★ rules_recipe.py<br/>━━━━━━━━━━<br/>unknown-sub-recipe rule"]
        AvailCtx["★ available_recipes<br/>━━━━━━━━━━<br/>ValidationContext field"]
        NameKnown{"sub-recipe<br/>name known?"}
    end

    subgraph Exec ["Runtime Execution"]
        direction TB
        ToolHandler["★ run_recipe<br/>━━━━━━━━━━<br/>server/tools_recipe.py"]
        FindRecipe["★ _find_recipe()<br/>━━━━━━━━━━<br/>server/helpers.py"]
        FoundDec{"recipe<br/>file found?"}
        RunSub["★ _run_subrecipe_session()<br/>━━━━━━━━━━<br/>server/helpers.py"]
        BuildPrompt["★ build_subrecipe_prompt()<br/>━━━━━━━━━━<br/>cli/_prompts.py"]
        BuildCmd["★ build_subrecipe_cmd()<br/>━━━━━━━━━━<br/>execution/commands.py"]
        SubSess["★ run_subrecipe_session()<br/>━━━━━━━━━━<br/>execution/headless.py"]
    end

    HeadlessProc["Headless Claude<br/>━━━━━━━━━━<br/>AUTOSKILLIT_KITCHEN_OPEN=1<br/>executes sub-recipe YAML"]
    ResultDec{"success: True?"}

    START -->|"recipe YAML loaded"| ValidateRecipe
    ValidateRecipe --> RulesRecipe
    RulesRecipe --> AvailCtx
    AvailCtx --> NameKnown
    NameKnown -->|"unknown name"| VALFAIL
    NameKnown -->|"valid"| ToolHandler
    ToolHandler --> FindRecipe
    FindRecipe --> FoundDec
    FoundDec -->|"not found"| SUCCESS
    FoundDec -->|"found"| RunSub
    RunSub --> BuildPrompt
    RunSub --> BuildCmd
    BuildPrompt -->|"prompt str"| SubSess
    BuildCmd -->|"CLI cmd + env"| SubSess
    SubSess --> HeadlessProc
    HeadlessProc --> ResultDec
    ResultDec -->|"yes"| SUCCESS
    ResultDec -->|"no"| SUCCESS

    %% CLASS ASSIGNMENTS %%
    class START,SUCCESS,VALFAIL terminal;
    class ValidateRecipe handler;
    class RulesRecipe detector;
    class AvailCtx,NameKnown,FoundDec stateNode;
    class ToolHandler,FindRecipe,RunSub,BuildPrompt,BuildCmd,SubSess newComponent;
    class HeadlessProc integration;
    class ResultDec stateNode;
```

### Deployment Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    subgraph DevMachine ["DEVELOPER MACHINE"]
        direction TB

        subgraph Bootstrap ["BOOTSTRAP (★ NEW)"]
            INSTALL_SH["★ install.sh<br/>━━━━━━━━━━<br/>curl from GitHub stable<br/>Python 3.11+ · uv · claude<br/>uv tool install"]
        end

        subgraph UVTool ["INSTALLED PACKAGE"]
            PKG["autoskillit package<br/>━━━━━━━━━━<br/>~/.local/share/uv/tools/<br/>autoskillit/lib/python3.x/<br/>site-packages/autoskillit/"]
            PLUGIN_CACHE["Plugin cache<br/>━━━━━━━━━━<br/>~/.claude/plugins/cache/<br/>autoskillit-local/"]
        end

        subgraph ClaudeCode ["CLAUDE CODE PROCESS"]
            CC["Claude Code IDE<br/>━━━━━━━━━━<br/>Reads hooks from<br/>~/.claude/settings.json<br/>.claude/settings.json"]
            MCP["● MCP Server (stdio)<br/>━━━━━━━━━━<br/>FastMCP · stdin/stdout<br/>12 ungated + 26 kitchen tools<br/>★ run_recipe · ★ get_ci_status"]
        end

        subgraph HeadlessSessions ["HEADLESS SUBPROCESS SESSIONS (pty)"]
            SKILL_SESS["Skill session<br/>━━━━━━━━━━<br/>claude --print prompt<br/>AUTOSKILLIT_HEADLESS=1<br/>KITCHEN_OPEN via open_kitchen"]
            SUBRECIPE["★ Sub-recipe session<br/>━━━━━━━━━━<br/>claude --print sous-chef-prompt<br/>AUTOSKILLIT_KITCHEN_OPEN=1<br/>NO HEADLESS flag<br/>build_subrecipe_cmd"]
        end

        subgraph LocalStorage ["LOCAL STORAGE"]
            SESSION_LOGS[("Session logs<br/>━━━━━━━━━━<br/>~/.local/share/autoskillit/<br/>logs/sessions/*.jsonl<br/>proc_trace · anomalies")]
            CRASH_TMP[("Crash traces<br/>━━━━━━━━━━<br/>/dev/shm/<br/>autoskillit_trace_pid.jsonl<br/>Linux tmpfs")]
            PROJ_STORE[("Project storage<br/>━━━━━━━━━━<br/>project/.autoskillit/<br/>config.yaml · .secrets.yaml<br/>temp/ · recipes/")]
            CLAUDE_LOGS[("Claude Code logs<br/>━━━━━━━━━━<br/>~/.claude/projects/<br/>encoded-cwd/<br/>session-id.jsonl")]
        end
    end

    subgraph GitHub ["GITHUB INFRASTRUCTURE"]
        direction TB
        GH_ACTIONS["GitHub Actions<br/>━━━━━━━━━━<br/>tests.yml: ubuntu + macos-15<br/>uv sync · task test-all"]
        VBUMP_WF["★ version-bump.yml<br/>━━━━━━━━━━<br/>integration→main merge<br/>patch bump + uv lock<br/>sync main→integration"]
        RELEASE_WF["★ release.yml<br/>━━━━━━━━━━<br/>stable branch merge<br/>minor bump + uv lock<br/>git tag + GitHub Release"]
        GH_REPO["GitHub Repository<br/>━━━━━━━━━━<br/>main · integration · stable<br/>Issues · PRs · Releases"]
        GH_API["GitHub API<br/>━━━━━━━━━━<br/>api.github.com<br/>Actions runs · CI jobs<br/>Issues · PR reviews"]
    end

    subgraph Anthropic ["EXTERNAL: ANTHROPIC"]
        ANT_API["Anthropic API<br/>━━━━━━━━━━<br/>api.anthropic.com<br/>/api/oauth/usage<br/>5-hour quota check"]
    end

    %% BOOTSTRAP FLOW %%
    INSTALL_SH -->|"uv tool install git@stable"| PKG
    PKG -->|"autoskillit install<br/>claude plugin install"| PLUGIN_CACHE

    %% CLAUDE CODE %%
    PLUGIN_CACHE -->|"plugin load"| CC
    CC -->|"spawns stdio"| MCP

    %% MCP → SESSIONS %%
    MCP -->|"run_skill: spawns pty"| SKILL_SESS
    MCP -->|"★ run_recipe: spawns pty<br/>KITCHEN_OPEN=1"| SUBRECIPE

    %% STORAGE WRITES %%
    SKILL_SESS -->|"writes diagnostics"| SESSION_LOGS
    SKILL_SESS -->|"crash: writes"| CRASH_TMP
    SUBRECIPE -->|"writes diagnostics"| SESSION_LOGS
    MCP -->|"reads/writes"| PROJ_STORE
    CC -->|"writes JSONL"| CLAUDE_LOGS

    %% CI/RELEASE %%
    GH_REPO -->|"push/PR event"| GH_ACTIONS
    GH_REPO -->|"integration→main merge"| VBUMP_WF
    GH_REPO -->|"★ PR→stable merge"| RELEASE_WF
    RELEASE_WF -->|"creates"| GH_REPO
    VBUMP_WF -->|"pushes commits"| GH_REPO

    %% EXTERNAL API CALLS %%
    MCP -->|"httpx HTTPS<br/>CI polling"| GH_API
    GH_ACTIONS -->|"gh CLI"| GH_API
    MCP -->|"httpx HTTPS<br/>quota check"| ANT_API
    SKILL_SESS -->|"Anthropic API<br/>inference"| ANT_API

    %% CLASS ASSIGNMENTS %%
    class INSTALL_SH,VBUMP_WF,RELEASE_WF,SUBRECIPE newComponent;
    class CC,SKILL_SESS cli;
    class MCP,GH_ACTIONS handler;
    class PKG,PLUGIN_CACHE phase;
    class SESSION_LOGS,CRASH_TMP,PROJ_STORE,CLAUDE_LOGS stateNode;
    class GH_REPO,GH_API,ANT_API integration;
```

Closes #307
Closes #302
Closes #298
Closes #297
Closes #300
Closes #303

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

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…el execution

## What this fixes

Claude Code v2.0.71+ uses the MCP `readOnlyHint` annotation to decide
whether tool calls can run in parallel or must be serialized. Tools
WITHOUT `readOnlyHint: true` are queued one-by-one. This caused all
`run_skill` calls (and every other MCP tool) to serialize instead of
running concurrently — making pipeline execution 7x slower when
processing multiple issues in parallel.

## Why readOnlyHint=True is correct for ALL tools

Every tool in this server is safe for concurrent execution:
- `run_skill`: spawns independent headless subprocesses
- `run_cmd`: spawns independent shell processes
- `clone_repo`/`push_to_remote`: operate on separate directories
- Status/query tools: read-only by nature
- All tools operate on independent state per pipeline run

## DO NOT REMOVE these annotations

If you are resolving merge conflicts or refactoring tool decorators,
you MUST preserve `annotations={"readOnlyHint": True}` on every
@mcp.tool() decorator. Removing it silently breaks parallel execution
and makes pipelines serialize without any error message.

Reference: anthropics/claude-code#14353
Repro: https://github.com/greynewell/mcp-serialization-repro

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ssue #307) (#337)

## Summary

The `dry-walkthrough` skill's Step 4.5 (Historical Regression Check) is
already fully implemented in `SKILL.md` — including the git history
scan, GitHub issues cross-reference, `gh auth` guard, actionable vs.
informational classification, and the `### Historical Context` section
in Step 7. Contract tests for Step 4.5 exist and pass in
`tests/skills/test_dry_walkthrough_contracts.py`.

The remaining work is **REQ-GEN-002** from Issue #311 (consolidated
here): replace the hardcoded `task test-all` references in Step 4 of
`SKILL.md` with a config-driven reference to `test_check.command`, so
the skill validates correctly for any project regardless of its test
runner.

## Architecture Impact

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, '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 output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;

    %% TERMINALS %%
    START([START])
    PASS([PASS — Ready to implement])
    REVISED([REVISED — See changes])

    subgraph Load ["Step 1: Load & Detect"]
        direction TB
        LOAD["Load Plan<br/>━━━━━━━━━━<br/>arg path / pasted / temp/ scan"]
        MULTIPART{"Multi-part plan?<br/>━━━━━━━━━━<br/>filename contains _part_?"}
        SCOPE_WARN["Scope Warning<br/>━━━━━━━━━━<br/>Insert boundary block<br/>Restrict to this part only"]
    end

    subgraph Validate ["Steps 2–3: Validate Phases"]
        direction TB
        PHASE_V["Phase Subagents<br/>━━━━━━━━━━<br/>files exist · fns exist<br/>assumptions correct<br/>wiring complete"]
        XPHASE["Cross-Phase Deps<br/>━━━━━━━━━━<br/>Phase ordering · implicit deps<br/>reorder safety"]
    end

    subgraph Rules ["● Step 4: Validate Against Project Rules"]
        direction TB
        RULE_CHK["Rule Checklist<br/>━━━━━━━━━━<br/>No compat code · no hidden fallbacks<br/>no stakeholder sections · arch patterns"]
        READ_CFG["● Read test_check.command<br/>━━━━━━━━━━<br/>.autoskillit/config.yaml<br/>default: task test-check"]
        TEST_CMD{"● Non-config test<br/>command in plan?<br/>━━━━━━━━━━<br/>pytest / python -m pytest<br/>make test etc."}
        WORKTREE{"Hardcoded worktree<br/>setup in plan?<br/>━━━━━━━━━━<br/>uv venv / pip install etc."}
    end

    subgraph History ["Step 4.5: Historical Regression Check"]
        direction TB
        GH_AUTH{"gh auth status<br/>━━━━━━━━━━<br/>GitHub auth available?"}
        GIT_SCAN["Git History Scan<br/>━━━━━━━━━━<br/>git log -100 on PLAN_FILES<br/>fix/revert/remove/replace/delete"]
        GIT_SIG{"Signal strength?<br/>━━━━━━━━━━<br/>Symbol-level match?"}
        GH_ISSUES["GitHub Issues XRef<br/>━━━━━━━━━━<br/>open + closed (last 30d)<br/>keyword cross-reference"]
        GH_MATCH{"Issue type?<br/>━━━━━━━━━━<br/>open vs closed match"}
    end

    subgraph Fix ["Steps 5–6: Fix & Mark"]
        direction TB
        FIX["Fix the Plan<br/>━━━━━━━━━━<br/>Direct edits to plan file<br/>No gap-analysis sections"]
        MARK["Mark Verified<br/>━━━━━━━━━━<br/>Dry-walkthrough verified = TRUE<br/>(first line of plan)"]
    end

    REPORT["● Step 7: Report to Terminal<br/>━━━━━━━━━━<br/>Changes Made · Verified<br/>### Historical Context · Recommendation"]

    %% FLOW %%
    START --> LOAD
    LOAD --> MULTIPART
    MULTIPART -->|"YES"| SCOPE_WARN
    MULTIPART -->|"NO"| PHASE_V
    SCOPE_WARN --> PHASE_V
    PHASE_V --> XPHASE
    XPHASE --> RULE_CHK
    RULE_CHK --> READ_CFG
    READ_CFG --> TEST_CMD
    TEST_CMD -->|"YES — replace"| FIX
    TEST_CMD -->|"NO"| WORKTREE
    WORKTREE -->|"YES — flag & replace"| FIX
    WORKTREE -->|"NO"| GH_AUTH
    GH_AUTH -->|"available"| GIT_SCAN
    GH_AUTH -->|"unavailable"| GIT_SCAN
    GIT_SCAN --> GIT_SIG
    GIT_SIG -->|"strong — actionable"| FIX
    GIT_SIG -->|"weak — informational"| REPORT
    GH_AUTH -->|"available"| GH_ISSUES
    GH_ISSUES --> GH_MATCH
    GH_MATCH -->|"closed issue — actionable"| FIX
    GH_MATCH -->|"open issue — informational"| REPORT
    FIX --> MARK
    MARK --> REPORT
    REPORT -->|"issues found"| REVISED
    REPORT -->|"no issues"| PASS

    %% CLASS ASSIGNMENTS %%
    class START,PASS,REVISED terminal;
    class LOAD,PHASE_V,XPHASE,GIT_SCAN,GH_ISSUES handler;
    class RULE_CHK,READ_CFG phase;
    class MULTIPART,TEST_CMD,WORKTREE,GH_AUTH,GIT_SIG,GH_MATCH stateNode;
    class FIX,MARK detector;
    class SCOPE_WARN output;
    class REPORT output;
```

**● Modified component** | **★ New component**

Closes #307

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/impl-307-20260310-203646-026624/temp/make-plan/dry_walkthrough_historical_regression_plan_2026-03-10_120000.md`

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

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

## Summary

The `version-bump.yml` workflow currently syncs `main` back into
`integration` after the patch version bump using `git merge --ff-only`
with a `gh pr create` fallback (if fast-forward fails). Issue #310
specifies that this case — where the only delta is the version bump
commit — must instead **force-push** `integration` to `main`'s HEAD.

The `version-bump.yml` job guard (`head.ref == 'integration'`) already
ensures the workflow runs exclusively for integration→main merges, so
the force-push is always safe and conflict-free. The PR fallback is
unnecessary complexity: the only divergence is the version bump commit,
conflicts are impossible, and there is no value in gating CI on a
single-line version change. Replacing the conditional merge+PR path with
a single unconditional force-push is the correct design.

Two tests that currently assert the old approach
(`test_syncs_main_into_integration` asserting `--ff-only`,
`test_fallback_sync_pr_creation` asserting `gh pr create`) must be
replaced, and `test_pull_requests_write_permission` must be inverted
since `pull-requests: write` is no longer needed.

## Architecture Impact

### Development Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;

    subgraph Workflows ["GITHUB ACTIONS WORKFLOWS"]
        direction TB
        TESTS_WF["tests.yml<br/>━━━━━━━━━━<br/>push: main/stable<br/>PR: main/integration/stable"]
        BUMP_WF["● version-bump.yml<br/>━━━━━━━━━━<br/>PR closed → main<br/>guard: head.ref == integration"]
        RELEASE_WF["release.yml<br/>━━━━━━━━━━<br/>PR closed → stable<br/>guard: head.ref == main"]
    end

    subgraph BumpJob ["● BUMP-AND-SYNC JOB (version-bump.yml)"]
        direction TB
        CHECKOUT["checkout@v4<br/>━━━━━━━━━━<br/>ref: main, fetch-depth: 0"]
        SETUP_UV["setup-uv@v7<br/>━━━━━━━━━━<br/>uv-version: 0.9.21"]
        COMPUTE_VER["Compute Version<br/>━━━━━━━━━━<br/>tomllib → PATCH+1"]
        UPDATE_PYPROJECT["Update pyproject.toml<br/>━━━━━━━━━━<br/>version = X.Y.Z"]
        UPDATE_PLUGIN["Update plugin.json<br/>━━━━━━━━━━<br/>version: X.Y.Z"]
        UVL["uv lock<br/>━━━━━━━━━━<br/>regenerate lockfile"]
        COMMIT["Commit + push to main<br/>━━━━━━━━━━<br/>github-actions[bot]"]
        FORCE_PUSH["● Force-push integration<br/>━━━━━━━━━━<br/>git push --force origin<br/>origin/main:refs/heads/integration"]
    end

    subgraph TestInfra ["TEST INFRASTRUCTURE"]
        direction TB
        PYTEST["pytest<br/>━━━━━━━━━━<br/>asyncio_mode=auto<br/>timeout=60s, -n 4"]
        INFRA_TESTS["● tests/infra/test_release_workflows.py<br/>━━━━━━━━━━<br/>TestVersionBumpWorkflow (13 tests)<br/>TestReleaseWorkflow (14 tests)"]
        TASK_TEST["task test-all<br/>━━━━━━━━━━<br/>lint-imports + pytest<br/>output: temp/test-*.txt"]
    end

    subgraph Quality ["QUALITY GATES (pre-commit)"]
        direction LR
        RUFF_FMT["ruff format<br/>━━━━━━━━━━<br/>auto-fix"]
        RUFF_CHK["ruff check<br/>━━━━━━━━━━<br/>auto-fix"]
        MYPY["mypy<br/>━━━━━━━━━━<br/>type check"]
        UV_LOCK_CHK["uv lock check<br/>━━━━━━━━━━<br/>lockfile sync"]
    end

    %% FLOW %%
    BUMP_WF --> CHECKOUT
    CHECKOUT --> SETUP_UV --> COMPUTE_VER
    COMPUTE_VER --> UPDATE_PYPROJECT --> UPDATE_PLUGIN --> UVL --> COMMIT --> FORCE_PUSH

    TESTS_WF --> TASK_TEST
    TASK_TEST --> PYTEST
    PYTEST --> INFRA_TESTS

    RUFF_FMT --> RUFF_CHK --> MYPY --> UV_LOCK_CHK

    %% CLASS ASSIGNMENTS %%
    class TESTS_WF,RELEASE_WF cli;
    class BUMP_WF,INFRA_TESTS handler;
    class CHECKOUT,SETUP_UV,COMPUTE_VER phase;
    class UPDATE_PYPROJECT,UPDATE_PLUGIN,UVL,COMMIT handler;
    class FORCE_PUSH newComponent;
    class PYTEST,TASK_TEST output;
    class RUFF_FMT,RUFF_CHK,MYPY,UV_LOCK_CHK detector;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Structure | Unchanged CI workflows and entry points |
| Purple | Phase | Checkout and setup steps |
| Orange | Handler | ● Modified: version-bump.yml job steps and test
file |
| Green | New Component | ● Force-push sync step (replaces ff-only
merge+PR path) |
| Red | Quality | Pre-commit quality gate hooks |
| Dark Teal | Output | Task runner and pytest test runner |

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, '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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    %% TERMINALS %%
    START(["PR closed → main"])
    SKIP(["SKIP"])
    COMPLETE(["COMPLETE<br/>main + integration in sync"])

    subgraph Guard ["TRIGGER GUARD"]
        direction TB
        G1{"merged == true &&<br/>head.ref == 'integration'?"}
    end

    subgraph BumpPhase ["PATCH VERSION BUMP"]
        direction TB
        B1["Compute version<br/>━━━━━━━━━━<br/>tomllib reads pyproject.toml<br/>PATCH+1 → outputs old/new"]
        B2["Update pyproject.toml<br/>━━━━━━━━━━<br/>writes version = X.Y.Z"]
        B3["Update plugin.json<br/>━━━━━━━━━━<br/>writes version: X.Y.Z"]
        B4["uv lock<br/>━━━━━━━━━━<br/>regenerates uv.lock"]
        B5["Commit + push to main<br/>━━━━━━━━━━<br/>github-actions[bot]<br/>no-op if diff empty"]
    end

    subgraph SyncPhase ["● INTEGRATION BACK-SYNC (CHANGED)"]
        direction TB
        S1["● Force-push integration<br/>━━━━━━━━━━<br/>git push --force origin<br/>origin/main:refs/heads/integration"]
    end

    %% FLOW %%
    START --> G1
    G1 -->|"no"| SKIP
    G1 -->|"yes"| B1
    B1 --> B2 --> B3 --> B4 --> B5
    B5 --> S1
    S1 --> COMPLETE

    %% CLASS ASSIGNMENTS %%
    class START,SKIP,COMPLETE terminal;
    class G1 detector;
    class B1,B2,B3,B4,B5 handler;
    class S1 newComponent;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal | Start, skip, and complete states |
| Red | Detector | Guard / decision point |
| Orange | Handler | Existing, unchanged processing steps |
| Green | New Component | ● Force-push sync step (replaces conditional
merge+PR path) |

Closes #327

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/impl-327-20260310-203635-087170/temp/make-plan/release_ci_force_push_plan_2026-03-10_000000.md`

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…336)

## Summary

Two enhancements to `src/autoskillit/skills/prepare-issue/SKILL.md`:

1. **TRIG** — Update the `description:` YAML frontmatter with natural
language trigger phrases and add a `## When to Use` section so Claude
routes natural requests like "open an issue" or "file a bug" to this
skill without requiring the explicit slash command.

2. **DEDUP** — Replace the single-candidate, comment-and-exit dedup
logic in Step 4 with a multi-candidate display, a numbered interactive
prompt offering "extend existing" or "create new", and an extend path
that appends context to the selected issue then continues through the
full triage pipeline (Steps 6–9) rather than exiting immediately.

No Python source changes are needed. All logic is LLM-executed prose in
`SKILL.md`. One new contract test file section validates the contracts
for both enhancements.

## Requirements

### DEDUP — Interactive Duplicate Detection

- **REQ-DEDUP-001:** The system must search existing open issues for
topic overlap when creating a new issue.
- **REQ-DEDUP-002:** The system must display potential duplicate issues
with their number, title, and URL to the user.
- **REQ-DEDUP-003:** The system must prompt the user to choose between
extending an existing issue or creating a new one when duplicates are
found.
- **REQ-DEDUP-004:** The system must append context to the selected
existing issue and skip new issue creation when the user chooses to
extend.
- **REQ-DEDUP-005:** The system must proceed with normal issue creation
when the user chooses to create new despite duplicates.
- **REQ-DEDUP-006:** The system must bypass the dedup check when the
`--issue N` flag is provided.

### TRIG — Broader Trigger Phrases

- **REQ-TRIG-001:** The skill must activate on natural language phrases
indicating intent to create a GitHub issue (e.g., "open an issue",
"create an issue", "file a bug").
- **REQ-TRIG-002:** The skill's trigger metadata must be updated in
SKILL.md or plugin registration to include these broader phrases.

## Architecture Impact

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, '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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    START(["● START<br/>━━━━━━━━━━<br/>Broader triggers:<br/>open/create/file issue"])
    COMPLETE([COMPLETE])
    ABORT([ABORT])

    subgraph Setup ["Setup (Steps 1–3)"]
        ParseArgs["Parse Arguments<br/>━━━━━━━━━━<br/>--issue N, --split<br/>--dry-run, --repo"]
        Auth["Authenticate<br/>━━━━━━━━━━<br/>gh auth status"]
        AuthFail([AUTH ERROR])
        ResolveRepo["Resolve Repo<br/>━━━━━━━━━━<br/>gh repo view"]
        IssueFlag{"--issue N<br/>provided?"}
    end

    subgraph Dedup ["● Step 4: Enhanced Dedup Check"]
        ExtractKW["● Multi-Keyword Search<br/>━━━━━━━━━━<br/>Individual terms +<br/>phrase combos per set"]
        HasCandidates{"● Candidates<br/>found?"}
        DisplayAll["● Display All Candidates<br/>━━━━━━━━━━<br/>[N] number, title, URL<br/>for every match"]
        UserChoice{"● User Choice<br/>━━━━━━━━━━<br/>[1..N] extend<br/>or C create new"}
    end

    subgraph ExtendPath ["● Extend Existing Issue Path"]
        AppendCtx["● Append Context<br/>━━━━━━━━━━<br/>gh issue comment<br/>or gh issue edit"]
        AdoptIssue["● Adopt Issue Number<br/>━━━━━━━━━━<br/>fetch updated body<br/>issue_number = selected"]
    end

    subgraph CreatePath ["Create New Issue (Steps 4a–5)"]
        ShowDraft["Show Draft & Confirm<br/>━━━━━━━━━━<br/>Y / n / edit prompt"]
        CreateIssue["Create Issue<br/>━━━━━━━━━━<br/>gh issue create"]
    end

    subgraph Triage ["Triage Phase (Steps 6–9)"]
        LLMClass["LLM Classification<br/>━━━━━━━━━━<br/>route + type<br/>+ confidence"]
        ConfGate{"Confidence<br/>Gate"}
        ReqGen["Requirement Generation<br/>━━━━━━━━━━<br/>REQ-GRP-NNN<br/>(impl route only)"]
        MixedConcern["Mixed-Concern Detection<br/>━━━━━━━━━━<br/>split if --split set"]
        Labels["Label Application<br/>━━━━━━━━━━<br/>recipe:* + bug/enhancement"]
    end

    START --> ParseArgs --> Auth
    Auth -->|"fail"| AuthFail
    Auth -->|"ok"| ResolveRepo --> IssueFlag
    IssueFlag -->|"yes: bypass dedup"| ShowDraft
    IssueFlag -->|"no: run dedup"| ExtractKW

    ExtractKW --> HasCandidates
    HasCandidates -->|"none found"| ShowDraft
    HasCandidates -->|"found"| DisplayAll --> UserChoice
    UserChoice -->|"C create new"| ShowDraft
    UserChoice -->|"[1..N] extend"| AppendCtx

    AppendCtx --> AdoptIssue --> LLMClass

    ShowDraft -->|"n abort"| ABORT
    ShowDraft -->|"Y confirm"| CreateIssue --> LLMClass

    LLMClass --> ConfGate
    ConfGate -->|"high"| ReqGen
    ConfGate -->|"low: user confirms"| ReqGen
    ReqGen --> MixedConcern --> Labels --> COMPLETE

    class START,COMPLETE,ABORT,AuthFail terminal;
    class ParseArgs,Auth,ResolveRepo,ShowDraft,CreateIssue handler;
    class ExtractKW,DisplayAll,AppendCtx,AdoptIssue newComponent;
    class HasCandidates,UserChoice,IssueFlag,ConfGate stateNode;
    class LLMClass,ReqGen,MixedConcern,Labels phase;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal | Start, complete, abort, and error states |
| Orange | Handler | Shell commands and I/O steps |
| Green | Modified | ● Enhanced nodes changed by this PR |
| Purple | Phase | Analysis, classification, triage steps |
| Teal | State | Decision points and branching |

Closes #308

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/impl-308-20260310-203641-762966/temp/make-plan/prepare_issue_enhancements_plan_2026-03-10_000000.md`

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…PART A ONLY (#332)

## Summary

Three related display output bugs share a common architectural pattern:
**split-brain constant definitions**. The most structurally urgent
instance is Bug 3, where the sentinel strings `"escalate"` and `"done"`
are authoritative routing targets defined precisely once in
`validator.py` as `_TERMINAL_TARGETS`, yet re-hardcoded independently in
at least six other locations (`_analysis.py`, `rules_inputs.py`,
`diagrams.py`, `schema.py` default value, `io.py` parse default).
`build_recipe_graph` in `_analysis.py` is the one module that re-encodes
the sentinel concept incorrectly — it assumes any unresolved routing
target is a mistake and warns, rather than recognizing "escalate" as a
legal sentinel. This produces 171 spurious warnings across 6 affected
bundled recipes.

The architectural fix: **promote `_TERMINAL_TARGETS` from `validator.py`
to `schema.py`**, making `schema.py` the single authoritative data-model
constant — no circular import risk since `_analysis.py` already imports
`schema.py`. All split-brain sites import from this single source. Add
an action-step guard to `build_recipe_graph` mirroring
`_build_step_graph`. The test infrastructure gets warning-count
assertions so this class of silent warning drift is caught immediately.

Part B covers the formatter contract enforcement and display determinism
fixes for Bugs 1 and 2.

## Architecture Impact

### Module Dependency Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
graph TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;

    subgraph CoreLayer ["L0 — CORE (zero autoskillit imports)"]
        direction LR
        TYPES["● core/types.py<br/>━━━━━━━━━━<br/>★ TOOL_CATEGORIES<br/>UNGATED_TOOLS, SKILL_TOOLS<br/>PIPELINE_FORBIDDEN_TOOLS"]
        CORE_INIT["● core/__init__.py<br/>━━━━━━━━━━<br/>re-export facade"]
    end

    subgraph RecipeLayer ["L2 — RECIPE"]
        direction TB
        SCHEMA["● recipe/schema.py<br/>━━━━━━━━━━<br/>★ _TERMINAL_TARGETS<br/>RecipeStep, Recipe<br/>DataFlowReport"]
        ANALYSIS["● recipe/_analysis.py<br/>━━━━━━━━━━<br/>build_recipe_graph<br/>★ imports _TERMINAL_TARGETS<br/>★ sentinel-aware edge skip"]
        VALIDATOR["● recipe/validator.py<br/>━━━━━━━━━━<br/>validate_recipe<br/>★ imports _TERMINAL_TARGETS<br/>from schema (not local def)"]
        RULES_INPUTS["● recipe/rules_inputs.py<br/>━━━━━━━━━━<br/>unreachable-step rule<br/>★ imports _TERMINAL_TARGETS<br/>★ removes hardcoded discards"]
        DIAGRAMS["● recipe/diagrams.py<br/>━━━━━━━━━━<br/>build_recipe_graph caller<br/>diagram rendering"]
    end

    subgraph HooksLayer ["HOOKS (stdlib-only)"]
        PRETTY["● hooks/pretty_output.py<br/>━━━━━━━━━━<br/>PostToolUse formatter<br/>★ load_recipe formatter<br/>★ list_recipes formatter<br/>no autoskillit imports"]
    end

    subgraph ServerLayer ["L3 — SERVER"]
        KITCHEN["● server/tools_kitchen.py<br/>━━━━━━━━━━<br/>open_kitchen handler<br/>★ _build_tool_listing()<br/>★ imports TOOL_CATEGORIES"]
    end

    subgraph CliLayer ["L3 — CLI"]
        APP["● cli/app.py<br/>━━━━━━━━━━<br/>cook command<br/>validate_recipe caller<br/>diagram wiring"]
        CHEFS["● cli/_chefs_hat.py<br/>━━━━━━━━━━<br/>chefs-hat launcher<br/>★ startup display card"]
    end

    subgraph TestLayer ["TESTS"]
        TEST_RULES["★ tests/recipe/test_rules_inputs.py<br/>━━━━━━━━━━<br/>sentinel-structural contract<br/>AST-based hardcode detection"]
    end

    %% L0 → L2: core exports consumed by recipe %%
    TYPES -->|"SKILL_TOOLS, get_logger"| ANALYSIS
    TYPES -->|"RecipeSource"| SCHEMA
    TYPES -->|"AUTOSKILLIT_INSTALLED_VERSION<br/>SKILL_TOOLS, Severity"| RULES_INPUTS
    TYPES -->|"_atomic_write"| DIAGRAMS
    CORE_INIT -->|"re-exports all"| TYPES

    %% L2 schema → L2 consumers: _TERMINAL_TARGETS fan-in %%
    SCHEMA -->|"★ _TERMINAL_TARGETS"| ANALYSIS
    SCHEMA -->|"★ _TERMINAL_TARGETS"| VALIDATOR
    SCHEMA -->|"★ _TERMINAL_TARGETS"| RULES_INPUTS
    SCHEMA -->|"Recipe"| DIAGRAMS

    %% L2 _analysis → consumers %%
    ANALYSIS -->|"build_recipe_graph<br/>_is_infrastructure_step"| DIAGRAMS
    ANALYSIS -->|"ValidationContext<br/>_build_step_graph"| VALIDATOR

    %% L0 → L3 server %%
    TYPES -->|"★ TOOL_CATEGORIES<br/>UNGATED_TOOLS"| KITCHEN

    %% L2 → L3 CLI %%
    VALIDATOR -->|"validate_recipe (deferred)"| APP
    DIAGRAMS -->|"generate/load diagram (deferred)"| APP

    %% Test → L2 %%
    TEST_RULES -->|"AST checks rules_inputs.py"| RULES_INPUTS

    %% CLASS ASSIGNMENTS %%
    class TYPES,CORE_INIT stateNode;
    class SCHEMA stateNode;
    class ANALYSIS,VALIDATOR,RULES_INPUTS,DIAGRAMS handler;
    class KITCHEN phase;
    class APP,CHEFS cli;
    class PRETTY output;
    class TEST_RULES newComponent;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Teal | Core/Schema | L0 `core/types.py` and L2 `schema.py` — high
fan-in, authoritative constants |
| Orange | Recipe Consumers | `_analysis.py`, `validator.py`,
`rules_inputs.py`, `diagrams.py` — import from schema |
| Purple | Server Tools | `tools_kitchen.py` — consumes
`TOOL_CATEGORIES` from L0 |
| Dark Blue | CLI | `app.py`, `_chefs_hat.py` — application layer entry
points |
| Dark Teal | Hooks | `pretty_output.py` — stdlib-only, no import
coupling |
| Green | New Tests | `test_rules_inputs.py` — structural contract test
|

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 55, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;

    START([START — recipe validation or diagram gen])

    subgraph GraphBuild ["● build_recipe_graph (_analysis.py) — UPDATED FLOW"]
        direction TB
        EXTRACT["_extract_routing_edges<br/>━━━━━━━━━━<br/>yields RoutingEdge per step<br/>(edge_type, target)"]
        ACTION_GUARD{"★ edge_type == 'exhausted'<br/>AND step.action is not None?<br/>━━━━━━━━━━<br/>action steps have no retry"}
        TARGET_KNOWN{"target in name_to_id?<br/>━━━━━━━━━━<br/>step name exists in graph"}
        SENTINEL_GUARD{"★ target in _TERMINAL_TARGETS?<br/>━━━━━━━━━━<br/>schema._TERMINAL_TARGETS<br/>= frozenset({'done','escalate'})"}
        ADD_EDGE["add edge to igraph<br/>━━━━━━━━━━<br/>valid structural edge"]
        SILENT_SKIP["★ silent skip<br/>━━━━━━━━━━<br/>known sentinel — no graph edge<br/>no warning emitted"]
        WARN["● logger.warning()<br/>━━━━━━━━━━<br/>genuinely unknown target<br/>(not sentinel, not step)"]
    end

    subgraph ReachableRule ["● rules_inputs.py — unreachable-step rule UPDATED"]
        direction TB
        COLLECT_REFS["collect referenced routing targets<br/>━━━━━━━━━━<br/>all on_success/on_failure/... values"]
        SENTINEL_DISCARD{"★ for sentinel in _TERMINAL_TARGETS:<br/>━━━━━━━━━━<br/>referenced.discard(sentinel)<br/>(was: 2 hardcoded .discard() calls)"}
        CHECK_UNREACHABLE["check remaining targets<br/>━━━━━━━━━━<br/>flag steps not in referenced set"]
    end

    subgraph KitchenOpen ["● open_kitchen (tools_kitchen.py) — DETERMINISTIC DISPLAY"]
        direction TB
        GATE_CHECK["enable kitchen gate<br/>━━━━━━━━━━<br/>reveal 26 gated tools"]
        BUILD_LISTING["★ _build_tool_listing()<br/>━━━━━━━━━━<br/>iterates TOOL_CATEGORIES<br/>(from core/types.py L0)<br/>deterministic ordered output"]
        STATIC_CARD["★ return static card<br/>━━━━━━━━━━<br/>categorized tool listing<br/>same output every call"]
    end

    subgraph DisplayPipeline ["● pretty_output.py PostToolUse hook — DISPLAY PIPELINE"]
        direction TB
        HOOK_FIRE["PostToolUse fires<br/>━━━━━━━━━━<br/>tool_name + result JSON"]
        DISPATCH{"tool_name dispatch<br/>━━━━━━━━━━<br/>_FORMATTERS lookup"}
        FMT_LOAD_RECIPE["★ _fmt_load_recipe()<br/>━━━━━━━━━━<br/>extracts diagram field<br/>renders verbatim ASCII art"]
        FMT_LIST_RECIPES["★ _fmt_list_recipes()<br/>━━━━━━━━━━<br/>renders name: description<br/>one line per recipe<br/>no 500-char truncation"]
        FMT_GENERIC["● _fmt_generic()<br/>━━━━━━━━━━<br/>hardened for list-of-dicts<br/>key fields inline per item"]
        DISPLAY_OUT["deterministic display output<br/>━━━━━━━━━━<br/>passed verbatim to LLM<br/>no reconstruction needed"]
    end

    COMPLETE([COMPLETE])

    START --> EXTRACT
    EXTRACT --> ACTION_GUARD
    ACTION_GUARD -->|"yes — skip<br/>no retry semantics"| EXTRACT
    ACTION_GUARD -->|"no — process edge"| TARGET_KNOWN
    TARGET_KNOWN -->|"yes"| ADD_EDGE
    TARGET_KNOWN -->|"no"| SENTINEL_GUARD
    SENTINEL_GUARD -->|"yes — sentinel"| SILENT_SKIP
    SENTINEL_GUARD -->|"no — unknown"| WARN
    ADD_EDGE --> EXTRACT
    SILENT_SKIP --> EXTRACT
    WARN --> EXTRACT

    COLLECT_REFS --> SENTINEL_DISCARD
    SENTINEL_DISCARD --> CHECK_UNREACHABLE
    CHECK_UNREACHABLE --> COMPLETE

    GATE_CHECK --> BUILD_LISTING
    BUILD_LISTING --> STATIC_CARD
    STATIC_CARD --> COMPLETE

    HOOK_FIRE --> DISPATCH
    DISPATCH -->|"load_recipe"| FMT_LOAD_RECIPE
    DISPATCH -->|"list_recipes"| FMT_LIST_RECIPES
    DISPATCH -->|"other tools"| FMT_GENERIC
    FMT_LOAD_RECIPE --> DISPLAY_OUT
    FMT_LIST_RECIPES --> DISPLAY_OUT
    FMT_GENERIC --> DISPLAY_OUT
    DISPLAY_OUT --> COMPLETE

    %% CLASS ASSIGNMENTS %%
    class START,COMPLETE terminal;
    class EXTRACT,ADD_EDGE,BUILD_LISTING,STATIC_CARD handler;
    class ACTION_GUARD,TARGET_KNOWN,SENTINEL_GUARD,DISPATCH stateNode;
    class SILENT_SKIP,FMT_LOAD_RECIPE,FMT_LIST_RECIPES newComponent;
    class WARN detector;
    class HOOK_FIRE,GATE_CHECK phase;
    class FMT_GENERIC,COLLECT_REFS,SENTINEL_DISCARD,CHECK_UNREACHABLE output;
    class DISPLAY_OUT output;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal | Start and end points |
| Orange | Handler | Execution: edge insertion, tool listing
construction |
| Teal | Decision | Decision points: guard checks, formatter dispatch |
| Green | New Behavior | `★` new logic: sentinel guard,
`_fmt_load_recipe`, `_fmt_list_recipes`, `_build_tool_listing` |
| Red | Detector | Warning emission for genuinely unknown targets |
| Purple | Phase | Hook/gate coordination nodes |
| Dark Teal | Updated | Modified existing behavior: `_fmt_generic`,
sentinel discard loop |

### Operational Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 45, 'rankSpacing': 60, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    subgraph CLIEntry ["● CLI ENTRY POINTS (cli/app.py, cli/_chefs_hat.py)"]
        direction LR
        COOK["● autoskillit cook<br/>━━━━━━━━━━<br/>--recipe &lt;name&gt;<br/>orchestrator launcher"]
        CHEFS["● autoskillit chefs-hat<br/>━━━━━━━━━━<br/>interactive sous-chef<br/>session launcher"]
    end

    subgraph CookFlow ["● cook command flow (cli/app.py)"]
        direction TB
        VALIDATE_RECIPE["validate_recipe()<br/>━━━━━━━━━━<br/>schema + semantic rules<br/>fail-fast on bad config"]
        STALE_CHECK["check_diagram_staleness()<br/>━━━━━━━━━━<br/>re-generate if stale<br/>load_recipe_diagram()"]
        PRINT_DIAGRAM["★ print(diagram)<br/>━━━━━━━━━━<br/>terminal-printed before session<br/>NOT LLM-rendered"]
        BUILD_PROMPT["_build_orchestrator_prompt()<br/>━━━━━━━━━━<br/>system prompt construction<br/>diagram injected as context"]
        LAUNCH_CLAUDE["claude --system-prompt ...<br/>━━━━━━━━━━<br/>headless orchestrator session"]
    end

    subgraph ChefsFlow ["● chefs-hat flow (cli/_chefs_hat.py)"]
        direction TB
        PRINT_CARD["★ print(startup card)<br/>━━━━━━━━━━<br/>deterministic kitchen card<br/>before launching session"]
        LAUNCH_CHEFS["claude --add-dir skills/<br/>━━━━━━━━━━<br/>interactive sous-chef session<br/>skills available"]
    end

    subgraph MCPTools ["● MCP TOOLS (server/tools_kitchen.py)"]
        direction TB
        OPEN_KITCHEN["● open_kitchen<br/>━━━━━━━━━━<br/>enable gate → reveal tools"]
        BUILD_LISTING["★ _build_tool_listing()<br/>━━━━━━━━━━<br/>iterates TOOL_CATEGORIES<br/>from core/types.py L0<br/>same output every call"]
        KITCHEN_CARD["★ static kitchen card<br/>━━━━━━━━━━<br/>● Execution: run_cmd...<br/>● Testing: test_check...<br/>...8 more categories"]
    end

    subgraph HookLayer ["● PostToolUse HOOK (hooks/pretty_output.py)"]
        direction TB
        HOOK_DISPATCH["● tool_name dispatch<br/>━━━━━━━━━━<br/>_FORMATTERS lookup"]
        FMT_LOAD["★ _fmt_load_recipe<br/>━━━━━━━━━━<br/>extracts diagram field<br/>renders ASCII art verbatim"]
        FMT_LIST["★ _fmt_list_recipes<br/>━━━━━━━━━━<br/>name: description per line<br/>no 500-char truncation"]
        FMT_GEN["● _fmt_generic (hardened)<br/>━━━━━━━━━━<br/>list-of-dicts: inline per item<br/>key fields, 20-item cap"]
    end

    subgraph Output ["TERMINAL OUTPUT — DETERMINISTIC"]
        direction LR
        TERM_DIAG["★ Recipe diagram<br/>━━━━━━━━━━<br/>printed to stdout<br/>before session starts"]
        MCP_OUT["★ MCP tool output<br/>━━━━━━━━━━<br/>structured Markdown-KV<br/>verbatim display text"]
    end

    COOK --> VALIDATE_RECIPE
    VALIDATE_RECIPE --> STALE_CHECK
    STALE_CHECK --> PRINT_DIAGRAM
    PRINT_DIAGRAM --> BUILD_PROMPT
    BUILD_PROMPT --> LAUNCH_CLAUDE
    PRINT_DIAGRAM -->|"★ terminal print"| TERM_DIAG

    CHEFS --> PRINT_CARD
    PRINT_CARD --> LAUNCH_CHEFS
    PRINT_CARD -->|"★ terminal print"| TERM_DIAG

    OPEN_KITCHEN --> BUILD_LISTING
    BUILD_LISTING --> KITCHEN_CARD
    KITCHEN_CARD --> HOOK_DISPATCH

    HOOK_DISPATCH -->|"load_recipe"| FMT_LOAD
    HOOK_DISPATCH -->|"list_recipes"| FMT_LIST
    HOOK_DISPATCH -->|"other"| FMT_GEN
    FMT_LOAD --> MCP_OUT
    FMT_LIST --> MCP_OUT
    FMT_GEN --> MCP_OUT

    %% CLASS ASSIGNMENTS %%
    class COOK,CHEFS cli;
    class VALIDATE_RECIPE,STALE_CHECK handler;
    class PRINT_DIAGRAM,PRINT_CARD,BUILD_LISTING,KITCHEN_CARD newComponent;
    class BUILD_PROMPT,LAUNCH_CLAUDE,LAUNCH_CHEFS phase;
    class OPEN_KITCHEN handler;
    class HOOK_DISPATCH stateNode;
    class FMT_LOAD,FMT_LIST newComponent;
    class FMT_GEN output;
    class TERM_DIAG,MCP_OUT output;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | CLI | `cook` and `chefs-hat` entry points |
| Orange | Handler | `validate_recipe`, `check_diagram_staleness`,
`open_kitchen` |
| Green | New Behavior | `★` new: terminal print, `_build_tool_listing`,
`_fmt_load_recipe`, `_fmt_list_recipes` |
| Purple | Phase | Claude session launchers, prompt builders |
| Teal | Decision | PostToolUse hook formatter dispatch |
| Dark Teal | Output | Terminal display and MCP output artifacts |

Closes #329

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/investigate-20260310-200515-444123/temp/rectify/rectify_display-output-bugs-329_2026-03-10_201742_part_a.md`

## Token Usage Summary

## Token Usage Summary

| Step | input | output | cached | count |
|------|-------|--------|--------|-------|
| investigate | 14 | 10.1k | 277.7k | 1 |
| rectify | 20 | 27.3k | 615.7k | 1 |
| review | 8.1k | 7.9k | 275.2k | 1 |
| dry_walkthrough | 61 | 51.3k | 4.2M | 2 |
| implement | 4.4k | 315.4k | 62.7M | 11 |
| assess | 40 | 18.5k | 1.8M | 1 |
| audit_impl | 8.3k | 91.2k | 2.6M | 9 |
| **Total** | **32.4k** | **1.5M** | **136.0M** | **28** |

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary

Four targeted stability fixes for public release readiness, drawn from
issue #304. All fixes are surgical — no architectural changes, only
correctness patches:

1. **Stale `run_skill_retry` test references** — rename one test method
and fix five docstrings that still reference the removed tool.
2. **`classify_fix` validation gaps** — add path existence check, add
`git fetch` before diff, add missing test coverage.
3. **Unguarded telemetry in headless session lifecycle** — wrap
`token_log.record()` in `run_headless_core` and both telemetry calls in
`run_subrecipe_session` in `try/except Exception`, ensuring a
fully-built `SkillResult` is never lost to an observability side-effect
failure.
4. **`timeout_scope` `None` dereference in process termination** — add a
`None` guard before `timeout_scope.cancelled_caught` to prevent
`AttributeError` when an outer cancel scope fires before the
`anyio.move_on_after` block is entered.

## Architecture Impact

### Error/Resilience Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, '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 output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;

    subgraph HeadlessCore ["● run_headless_core() — TELEMETRY CONTAINMENT (headless.py)"]
        HC_GATE["assert runner is not None<br/>━━━━━━━━━━<br/>Hard gate → AssertionError"]
        HC_RUN["await runner(...)<br/>━━━━━━━━━━<br/>try/finally: crash → flush crashed"]
        HC_NONE["if _result is None<br/>━━━━━━━━━━<br/>→ raise RuntimeError"]
        HC_TOKEN["● try: ctx.token_log.record()<br/>━━━━━━━━━━<br/>except Exception → debug log + swallow"]
        HC_OK(["return SkillResult"])
    end

    subgraph SubrecipeSession ["● run_subrecipe_session() — TELEMETRY CONTAINMENT (headless.py)"]
        SR_PATH["if not Path(cwd).is_dir()<br/>━━━━━━━━━━<br/>→ SkillResult(success=False)"]
        SR_GATE["assert runner is not None<br/>━━━━━━━━━━<br/>Hard gate → AssertionError"]
        SR_RUN["await runner(...)<br/>━━━━━━━━━━<br/>try/finally: crash → flush crashed"]
        SR_NONE["if _result is None<br/>━━━━━━━━━━<br/>→ raise RuntimeError"]
        SR_TELEM["● try: timing_log + token_log<br/>━━━━━━━━━━<br/>except Exception → debug log + swallow"]
        SR_OK(["return SkillResult"])
    end

    subgraph ProcessManaged ["● run_managed_async() — RACE GUARD (process.py)"]
        PM_INIT["timeout_scope = None<br/>━━━━━━━━━━<br/>Initialized before task group"]
        PM_SCOPE["move_on_after() block<br/>━━━━━━━━━━<br/>May be skipped by outer cancel"]
        PM_GUARD["● if timeout_scope is not None<br/>   and cancelled_caught<br/>━━━━━━━━━━<br/>None-guarded access"]
        PM_KILL["kill_process_tree(proc.pid)<br/>━━━━━━━━━━<br/>Cleanup on timeout"]
        PM_BASE["except BaseException<br/>━━━━━━━━━━<br/>if 'proc' in locals() → kill tree<br/>then re-raise"]
        PM_RESULT(["return SubprocessResult"])
    end

    subgraph ClassifyFix ["● classify_fix() — VALIDATION CHAIN (tools_git.py)"]
        CF_KITCHEN["if _require_enabled() is not None<br/>━━━━━━━━━━<br/>→ gate error string"]
        CF_PATH["● if not os.path.isdir(worktree_path)<br/>━━━━━━━━━━<br/>→ {restart_scope: 'error'}"]
        CF_FETCH["● git fetch origin {base}<br/>━━━━━━━━━━<br/>if fetch_rc != 0 → FULL_RESTART"]
        CF_DIFF["git diff --name-only origin/{base}...HEAD<br/>━━━━━━━━━━<br/>if returncode != 0 → FULL_RESTART"]
        CF_OK(["return JSON classify result"])
    end

    T_ASSERT([AssertionError — runner missing])
    T_RUNTIME([RuntimeError — runner returned None])
    T_ATTR_FIXED([● AttributeError PREVENTED — None guard])
    T_FAIL_SKILL([SkillResult success=False])
    T_FULL_RESTART([restart_scope FULL_RESTART])
    T_GATE_ERR([restart_scope error])

    HC_GATE -->|"runner is None"| T_ASSERT
    HC_GATE -->|"runner ok"| HC_RUN
    HC_RUN -->|"crash: flush + continue"| HC_NONE
    HC_NONE -->|"_result is None"| T_RUNTIME
    HC_NONE -->|"result ok"| HC_TOKEN
    HC_TOKEN -->|"log failure raised — swallowed"| HC_OK
    HC_TOKEN -->|"success"| HC_OK

    SR_PATH -->|"path missing"| T_FAIL_SKILL
    SR_PATH -->|"path ok"| SR_GATE
    SR_GATE -->|"runner is None"| T_ASSERT
    SR_GATE -->|"runner ok"| SR_RUN
    SR_RUN -->|"crash: flush + continue"| SR_NONE
    SR_NONE -->|"_result is None"| T_RUNTIME
    SR_NONE -->|"result ok"| SR_TELEM
    SR_TELEM -->|"telemetry raised — swallowed"| SR_OK
    SR_TELEM -->|"success"| SR_OK

    PM_INIT --> PM_SCOPE
    PM_SCOPE -->|"outer cancel before scope binds"| PM_GUARD
    PM_SCOPE -->|"scope bound, timeout fires"| PM_GUARD
    PM_GUARD -->|"None → skip kill"| T_ATTR_FIXED
    PM_GUARD -->|"not None + cancelled_caught"| PM_KILL
    PM_KILL --> PM_RESULT
    PM_BASE -->|"unexpected exception"| PM_RESULT

    CF_KITCHEN -->|"not enabled"| T_GATE_ERR
    CF_KITCHEN -->|"enabled"| CF_PATH
    CF_PATH -->|"path absent"| T_GATE_ERR
    CF_PATH -->|"path ok"| CF_FETCH
    CF_FETCH -->|"fetch failed"| T_FULL_RESTART
    CF_FETCH -->|"fetch ok"| CF_DIFF
    CF_DIFF -->|"diff failed"| T_FULL_RESTART
    CF_DIFF -->|"diff ok"| CF_OK

    class HC_GATE,CF_KITCHEN,CF_PATH,CF_FETCH,CF_DIFF,SR_PATH,SR_GATE detector;
    class HC_RUN,HC_NONE,SR_RUN,SR_NONE,PM_SCOPE handler;
    class HC_TOKEN,SR_TELEM,PM_GUARD,PM_INIT gap;
    class PM_KILL output;
    class PM_BASE phase;
    class HC_OK,SR_OK,CF_OK,PM_RESULT stateNode;
    class T_ASSERT,T_RUNTIME,T_ATTR_FIXED,T_FAIL_SKILL,T_FULL_RESTART,T_GATE_ERR terminal;
```

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, '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 output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;

    subgraph ClassifyFix ["● classify_fix() — VALIDATION CHAIN (tools_git.py)"]
        CF_GATE{"_require_enabled()<br/>━━━━━━━━━━<br/>kitchen open?"}
        CF_PATH{"● os.path.isdir<br/>(worktree_path)<br/>━━━━━━━━━━<br/>path exists?"}
        CF_FETCH["● git fetch origin {base}<br/>━━━━━━━━━━<br/>refresh remote-tracking ref"]
        CF_FETCH_OK{"fetch_rc == 0?"}
        CF_DIFF["git diff --name-only<br/>━━━━━━━━━━<br/>origin/{base}...HEAD"]
        CF_DIFF_OK{"returncode == 0?"}
        CF_FILTER["_filter_changed_files()<br/>━━━━━━━━━━<br/>partition by prefix"]
        CF_RESULT(["JSON classify result"])
    end

    subgraph ManagedAsync ["● run_managed_async() — TIMEOUT SCOPE FLOW (process.py)"]
        PM_INIT["timeout_scope = None<br/>━━━━━━━━━━<br/>initialized before task group"]
        PM_TG["async task group<br/>━━━━━━━━━━<br/>spawn watchers + process"]
        PM_SCOPE["move_on_after() block<br/>━━━━━━━━━━<br/>binds timeout_scope"]
        PM_GUARD{"● timeout_scope<br/>is not None?<br/>━━━━━━━━━━<br/>None guard"}
        PM_CAUGHT{"cancelled_caught?"}
        PM_KILL["kill_process_tree<br/>━━━━━━━━━━<br/>terminate on timeout"]
        PM_RESULT(["SubprocessResult"])
    end

    subgraph HeadlessCore ["● run_headless_core() — SESSION LIFECYCLE (headless.py)"]
        HC_ASSERT["assert runner is not None<br/>━━━━━━━━━━<br/>hard precondition"]
        HC_RUN["await runner(cmd, ...)<br/>━━━━━━━━━━<br/>try/finally block"]
        HC_NONE{"_result is None?"}
        HC_TOKEN["● try: ctx.token_log.record()<br/>━━━━━━━━━━<br/>except Exception → swallow"]
        HC_OK(["return SkillResult"])
    end

    subgraph SubrecipeSession ["● run_subrecipe_session() — SESSION LIFECYCLE (headless.py)"]
        SR_PATH{"Path(cwd).is_dir()?<br/>━━━━━━━━━━<br/>cwd exists?"}
        SR_GATE["assert runner is not None"]
        SR_RUN["await runner(cmd, ...)<br/>━━━━━━━━━━<br/>try/finally block"]
        SR_NONE{"_result is None?"}
        SR_TELEM["● try: timing_log + token_log<br/>━━━━━━━━━━<br/>except Exception → swallow"]
        SR_OK(["return SkillResult"])
    end

    T_GATE_ERR([gate error string])
    T_PATH_ERR([restart_scope error])
    T_FETCH_FAIL([restart_scope FULL_RESTART])
    T_DIFF_FAIL([restart_scope FULL_RESTART])
    T_ASSERT_ERR([AssertionError])
    T_RUNTIME([RuntimeError — no result])
    T_SKILL_FAIL([SkillResult success=False])
    T_NO_KILL([skip kill — scope unbound])

    CF_GATE -->|"not enabled"| T_GATE_ERR
    CF_GATE -->|"enabled"| CF_PATH
    CF_PATH -->|"absent"| T_PATH_ERR
    CF_PATH -->|"exists"| CF_FETCH
    CF_FETCH --> CF_FETCH_OK
    CF_FETCH_OK -->|"no"| T_FETCH_FAIL
    CF_FETCH_OK -->|"yes"| CF_DIFF
    CF_DIFF --> CF_DIFF_OK
    CF_DIFF_OK -->|"no"| T_DIFF_FAIL
    CF_DIFF_OK -->|"yes"| CF_FILTER
    CF_FILTER --> CF_RESULT

    PM_INIT --> PM_TG
    PM_TG --> PM_SCOPE
    PM_SCOPE -->|"outer cancel before bind"| PM_GUARD
    PM_SCOPE -->|"timeout fires after bind"| PM_GUARD
    PM_GUARD -->|"None (scope unbound)"| T_NO_KILL
    PM_GUARD -->|"not None"| PM_CAUGHT
    PM_CAUGHT -->|"yes"| PM_KILL
    PM_CAUGHT -->|"no (process exited)"| PM_RESULT
    PM_KILL --> PM_RESULT

    HC_ASSERT -->|"runner ok"| HC_RUN
    HC_ASSERT -->|"runner None"| T_ASSERT_ERR
    HC_RUN --> HC_NONE
    HC_NONE -->|"yes"| T_RUNTIME
    HC_NONE -->|"no"| HC_TOKEN
    HC_TOKEN --> HC_OK

    SR_PATH -->|"missing"| T_SKILL_FAIL
    SR_PATH -->|"exists"| SR_GATE
    SR_GATE --> SR_RUN
    SR_RUN --> SR_NONE
    SR_NONE -->|"yes"| T_RUNTIME
    SR_NONE -->|"no"| SR_TELEM
    SR_TELEM --> SR_OK

    class CF_GATE,CF_PATH,CF_FETCH_OK,CF_DIFF_OK,PM_GUARD,PM_CAUGHT,HC_NONE,SR_PATH,SR_NONE detector;
    class CF_FETCH,CF_DIFF,CF_FILTER,PM_TG,PM_SCOPE,HC_RUN,SR_RUN handler;
    class PM_INIT,HC_ASSERT,SR_GATE phase;
    class HC_TOKEN,SR_TELEM,PM_KILL gap;
    class CF_RESULT,HC_OK,SR_OK,PM_RESULT stateNode;
    class T_GATE_ERR,T_PATH_ERR,T_FETCH_FAIL,T_DIFF_FAIL,T_ASSERT_ERR,T_RUNTIME,T_SKILL_FAIL,T_NO_KILL output;
```

Closes #304

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/impl-304-20260310-203650-088116/temp/make-plan/pre_release_stability_fixes_plan_2026-03-10_000000.md`

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… Cycle (#343)

## Summary

Issues #277 (fidelity audit), #278 (CI/review gates in analyze-prs), and
#294 (commit
status blocking CI until review completes) are **already implemented**:
- `pipeline/fidelity.py` + review-pr SKILL.md Steps 2.8, 3 (fidelity
subagent), 7.5 (commit status)
- `pipeline/pr_gates.py` + analyze-prs SKILL.md Step 1.5 (CI + review
eligibility gates)

This plan implements **#325 only**: add a mergeability gate and
automated review cycle to
`pr-merge-pipeline.yaml` between `create_review_pr` and `ci_watch_pr`.
Currently the recipe
skips directly from PR creation to CI — this plan inserts eight steps
that (a) check and
resolve merge conflicts, (b) run automated review, and (c) apply review
fixes before CI runs.

## Requirements

### Mergeability Gate (MERGE)

- **REQ-MERGE-001:** After `create_review_pr` completes, the pipeline
must check the review PR's mergeability status via the
`check_pr_mergeable` MCP tool before proceeding to any review or CI
step.
- **REQ-MERGE-002:** The mergeability check must tolerate GitHub's async
computation delay (retry with backoff until `mergeable` is no longer
`UNKNOWN`).
- **REQ-MERGE-003:** When the review PR is `CONFLICTING`, the pipeline
must attempt automated conflict resolution (rebase
`context.integration_branch` onto `inputs.base_branch`, resolve,
force-push).
- **REQ-MERGE-004:** When automated conflict resolution fails, the
pipeline must escalate to `cleanup_failure` with clear diagnostics
identifying the conflicting files.
- **REQ-MERGE-005:** The pipeline must only proceed to `review_pr` once
`check_pr_mergeable` confirms the PR is `MERGEABLE`.

### Review Cycle (REVIEW)

- **REQ-REVIEW-001:** The pipeline must include a `review_pr` step that
invokes `/autoskillit:review-pr` against the integration PR, matching
the pattern used by `implementation.yaml`, `remediation.yaml`,
`audit-and-fix.yaml`, and `implementation-groups.yaml`.
- **REQ-REVIEW-002:** When `review_pr` returns
`verdict=changes_requested`, the pipeline must route to a
`resolve_review` step that invokes `/autoskillit:resolve-review`.
- **REQ-REVIEW-003:** After `resolve_review` succeeds, the pipeline must
re-push the integration branch via a `re_push_review` step before
proceeding to `ci_watch_pr`.
- **REQ-REVIEW-004:** When `review_pr` returns `verdict=approved` or
`verdict=needs_human`, the pipeline must proceed directly to
`ci_watch_pr`.
- **REQ-REVIEW-005:** The `resolve_review` step must support retries (at
least 2) before escalating to `cleanup_failure`.

### Recipe Structure (RECIPE)

- **REQ-RECIPE-001:** The new steps must be inserted between the
existing `create_review_pr` and `ci_watch_pr` steps without altering the
behavior of any other step in the recipe.
- **REQ-RECIPE-002:** The recipe `summary` field must be updated to
reflect the new steps in the pipeline flow.

## Architecture Impact

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, '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 modified fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;

    START([START])
    DONE([DONE])
    FAIL([ESCALATE_STOP])

    subgraph Preamble ["Pipeline Preamble (unchanged)"]
        direction TB
        CLONE["clone<br/>━━━━━━━━━━<br/>clone_repo → work_dir"]
        ANALYZE["analyze_prs + create_integration_branch<br/>━━━━━━━━━━<br/>PR queue + integration_branch"]
        LOOP["PR merge loop<br/>━━━━━━━━━━<br/>merge_pr / plan+implement+test<br/>sequential, one PR at a time"]
    end

    subgraph PostLoop ["Post-Loop (unchanged)"]
        direction TB
        AUDIT{"audit_impl?<br/>━━━━━━━━━━<br/>GO / NO GO / skipped"}
        CREATE_PR["create_review_pr<br/>━━━━━━━━━━<br/>run_skill: create-review-pr<br/>captures: pr_url"]
    end

    subgraph MergeGate ["● Mergeability Gate (pr-merge-pipeline.yaml + tools_git.py)"]
        direction TB
        WAIT["● wait_for_review_pr_mergeability<br/>━━━━━━━━━━<br/>poll gh until mergeable != UNKNOWN<br/>12 × 15s intervals (3 min max)<br/>captures: review_pr_number"]
        CHECK{"● check_mergeability<br/>━━━━━━━━━━<br/>check_pr_mergeable tool<br/>reads: mergeable_status field"}
        RESOLVE["● resolve_integration_conflicts<br/>━━━━━━━━━━<br/>run_skill: resolve-merge-conflicts<br/>rebase integration_branch → base_branch"]
        FPUSH["● force_push_after_rebase<br/>━━━━━━━━━━<br/>git push --force-with-lease<br/>updates review PR after rebase"]
        RECHECK{"● check_mergeability_post_rebase<br/>━━━━━━━━━━<br/>check_pr_mergeable tool<br/>verify resolution succeeded"}
    end

    subgraph ReviewCycle ["● Review Cycle (pr-merge-pipeline.yaml)"]
        direction TB
        REVIEW{"● review_pr_integration<br/>━━━━━━━━━━<br/>run_skill: review-pr<br/>captures: review_verdict"}
        RESOLVE_REVIEW["● resolve_review_integration<br/>━━━━━━━━━━<br/>run_skill: resolve-review<br/>retries: 2, on_exhausted: cleanup_failure"]
        REPUSH["● re_push_review_integration<br/>━━━━━━━━━━<br/>push_to_remote: integration_branch"]
    end

    subgraph CICleanup ["CI & Cleanup (unchanged)"]
        direction TB
        CI["ci_watch_pr<br/>━━━━━━━━━━<br/>wait_for_ci: integration_branch<br/>timeout: 300s"]
        CONFIRM["confirm_cleanup<br/>━━━━━━━━━━<br/>user gate: delete clone?"]
    end

    CLEANUP["cleanup_failure<br/>━━━━━━━━━━<br/>remove_clone → escalate_stop"]

    START --> CLONE
    CLONE -->|"on_success"| ANALYZE
    CLONE -->|"on_failure"| FAIL
    ANALYZE -->|"PRs found"| LOOP
    LOOP -->|"all PRs merged"| AUDIT
    AUDIT -->|"GO / skipped"| CREATE_PR
    AUDIT -->|"NO GO"| LOOP
    CREATE_PR -->|"on_success"| WAIT
    CREATE_PR -->|"on_failure"| CLEANUP

    WAIT -->|"on_success"| CHECK
    WAIT -->|"on_failure"| CLEANUP
    CHECK -->|"MERGEABLE"| REVIEW
    CHECK -->|"CONFLICTING"| RESOLVE
    CHECK -->|"other / timeout"| CLEANUP
    RESOLVE -->|"on_success"| FPUSH
    RESOLVE -->|"on_failure / escalation"| CLEANUP
    FPUSH -->|"on_success"| RECHECK
    FPUSH -->|"on_failure"| CLEANUP
    RECHECK -->|"MERGEABLE"| REVIEW
    RECHECK -->|"other"| CLEANUP

    REVIEW -->|"changes_requested"| RESOLVE_REVIEW
    REVIEW -->|"needs_human / approved"| CI
    REVIEW -->|"on_failure"| RESOLVE_REVIEW
    RESOLVE_REVIEW -->|"on_success"| REPUSH
    RESOLVE_REVIEW -->|"exhausted (2 retries) / failure"| CLEANUP
    REPUSH -->|"on_success"| CI
    REPUSH -->|"on_failure"| CLEANUP

    CI -->|"on_success"| CONFIRM
    CI -->|"on_failure"| CLEANUP
    CONFIRM -->|"yes"| DONE
    CONFIRM -->|"no"| DONE
    CLEANUP --> FAIL

    class START,DONE,FAIL terminal;
    class CLONE,ANALYZE,LOOP,CREATE_PR,CI,CONFIRM handler;
    class AUDIT phase;
    class WAIT,RESOLVE,FPUSH,RESOLVE_REVIEW,REPUSH modified;
    class CHECK,RECHECK,REVIEW modified;
    class CLEANUP detector;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal | START, DONE, ESCALATE_STOP |
| Orange | Handler | Existing unchanged pipeline steps |
| Purple | Phase | Routing/decision nodes (audit_impl) |
| Green (●) | Modified | New steps added by this PR |
| Red | Detector | Failure escalation path |

### Error/Resilience Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, '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 output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;
    classDef modified fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;

    T_SUCCESS([SUCCESS: ci_watch_pr])
    T_FAIL([ESCALATE_STOP])

    CREATE_PR["create_review_pr<br/>━━━━━━━━━━<br/>Existing step (unchanged)"]

    subgraph MergeGate ["● Mergeability Gate — Failure Paths"]
        direction TB
        WAIT_OK["● wait_for_review_pr_mergeability<br/>━━━━━━━━━━<br/>Poll 12 × 15s until not UNKNOWN"]
        WAIT_TIMEOUT["TIMEOUT / FAIL<br/>━━━━━━━━━━<br/>mergeable still UNKNOWN after 3min<br/>OR gh command failure"]

        CHECK_OK{"● check_mergeability<br/>━━━━━━━━━━<br/>reads: mergeable_status (MERGEABLE /<br/>CONFLICTING / UNKNOWN)"}
        CHECK_FAIL["UNSUPPORTED STATE<br/>━━━━━━━━━━<br/>mergeable_status != MERGEABLE<br/>and != CONFLICTING"]

        RESOLVE_OK["● resolve_integration_conflicts<br/>━━━━━━━━━━<br/>Single-attempt rebase + resolve"]
        RESOLVE_FAIL["RESOLUTION FAIL<br/>━━━━━━━━━━<br/>rebase failed OR escalation_required"]

        FPUSH_OK["● force_push_after_rebase<br/>━━━━━━━━━━<br/>git push --force-with-lease"]
        FPUSH_FAIL["PUSH FAIL<br/>━━━━━━━━━━<br/>remote diverged during rebase<br/>OR network failure"]

        RECHECK_OK{"● check_mergeability_post_rebase<br/>━━━━━━━━━━<br/>reads: mergeable_status<br/>NO SECOND RESOLUTION ATTEMPT"}
        RECHECK_FAIL["STILL NOT MERGEABLE<br/>━━━━━━━━━━<br/>automated resolution insufficient<br/>→ human intervention required"]
    end

    subgraph ReviewCycle ["● Review Cycle — Failure Paths"]
        direction TB
        REVIEW_OK{"● review_pr_integration<br/>━━━━━━━━━━<br/>verdict: changes_requested /<br/>needs_human / approved"}
        REVIEW_TOOL_FAIL["TOOL FAILURE<br/>━━━━━━━━━━<br/>on_failure → resolve_review<br/>(soft failure, not cleanup)"]

        RESOLVE_REVIEW_OK["● resolve_review_integration<br/>━━━━━━━━━━<br/>retries: 2 (3 total attempts)"]
        RESOLVE_REVIEW_EXHAUST["RETRIES EXHAUSTED<br/>━━━━━━━━━━<br/>3 attempts failed or budget spent<br/>on_exhausted → cleanup_failure"]

        REPUSH_OK["● re_push_review_integration<br/>━━━━━━━━━━<br/>push_to_remote: integration_branch"]
        REPUSH_FAIL["PUSH FAIL<br/>━━━━━━━━━━<br/>network / remote rejection"]
    end

    CLEANUP["cleanup_failure<br/>━━━━━━━━━━<br/>remove_clone (keep_on_fail?)<br/>→ escalate_stop"]

    CREATE_PR -->|"on_success"| WAIT_OK
    CREATE_PR -->|"on_failure"| CLEANUP

    WAIT_OK -->|"not UNKNOWN within 3min"| CHECK_OK
    WAIT_OK -->|"timeout / failure"| WAIT_TIMEOUT
    WAIT_TIMEOUT --> CLEANUP

    CHECK_OK -->|"MERGEABLE"| REVIEW_OK
    CHECK_OK -->|"CONFLICTING"| RESOLVE_OK
    CHECK_OK -->|"other (UNKNOWN/error)"| CHECK_FAIL
    CHECK_FAIL --> CLEANUP

    RESOLVE_OK -->|"on_success"| FPUSH_OK
    RESOLVE_OK -->|"failure / escalation_required"| RESOLVE_FAIL
    RESOLVE_FAIL --> CLEANUP

    FPUSH_OK -->|"on_success"| RECHECK_OK
    FPUSH_OK -->|"on_failure"| FPUSH_FAIL
    FPUSH_FAIL --> CLEANUP

    RECHECK_OK -->|"MERGEABLE"| REVIEW_OK
    RECHECK_OK -->|"not MERGEABLE"| RECHECK_FAIL
    RECHECK_FAIL --> CLEANUP

    REVIEW_OK -->|"needs_human / approved"| T_SUCCESS
    REVIEW_OK -->|"changes_requested"| RESOLVE_REVIEW_OK
    REVIEW_OK -->|"tool failure (soft)"| REVIEW_TOOL_FAIL
    REVIEW_TOOL_FAIL --> RESOLVE_REVIEW_OK

    RESOLVE_REVIEW_OK -->|"on_success (attempt 1–3)"| REPUSH_OK
    RESOLVE_REVIEW_OK -->|"exhausted (budget spent)"| RESOLVE_REVIEW_EXHAUST
    RESOLVE_REVIEW_OK -->|"on_failure (single attempt)"| CLEANUP
    RESOLVE_REVIEW_EXHAUST --> CLEANUP

    REPUSH_OK -->|"on_success"| T_SUCCESS
    REPUSH_OK -->|"on_failure"| REPUSH_FAIL
    REPUSH_FAIL --> CLEANUP

    CLEANUP --> T_FAIL

    class T_SUCCESS,T_FAIL terminal;
    class CREATE_PR handler;
    class WAIT_OK,RESOLVE_OK,FPUSH_OK,RESOLVE_REVIEW_OK,REPUSH_OK modified;
    class CHECK_OK,RECHECK_OK,REVIEW_OK modified;
    class WAIT_TIMEOUT,CHECK_FAIL,RESOLVE_FAIL,FPUSH_FAIL,RECHECK_FAIL,REVIEW_TOOL_FAIL,RESOLVE_REVIEW_EXHAUST,REPUSH_FAIL gap;
    class CLEANUP detector;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal | SUCCESS (ci_watch_pr) and ESCALATE_STOP |
| Orange | Handler | Existing unchanged step (create_review_pr) |
| Green (●) | Modified | New steps from this PR |
| Yellow/Orange | Failed State | Failure conditions requiring handling |
| Red | Detector | cleanup_failure escalation path |

### State Lifecycle Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;
    classDef modified fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;

    subgraph ExistingFields ["Existing INIT_ONLY Context Fields (unchanged)"]
        direction LR
        PR_URL["context.pr_url<br/>━━━━━━━━━━<br/>INIT_ONLY<br/>Written: create_review_pr<br/>Read: wait step"]
        INT_BRANCH["context.integration_branch<br/>━━━━━━━━━━<br/>INIT_PRESERVE<br/>Written: analyze_prs<br/>Read: resolve_conflicts, review_pr, resolve_review, re_push"]
    end

    subgraph NewFields ["● New Context Fields (this PR)"]
        direction TB
        REVIEW_PR_NUM["● context.review_pr_number<br/>━━━━━━━━━━<br/>INIT_ONLY (write-once)<br/>Written: wait_for_review_pr_mergeability<br/>from result.stdout (gh pr number)<br/>Read: check_mergeability.pr_number<br/>Read: check_mergeability_post_rebase.pr_number"]
        REVIEW_VERDICT["● context.review_verdict<br/>━━━━━━━━━━<br/>MUTABLE (observability only)<br/>Written: review_pr_integration<br/>from result.verdict<br/>NOT used in on_result routing<br/>(routing reads result.verdict directly)"]
    end

    subgraph DerivedFields ["● Derived / Ephemeral Fields (this PR)"]
        direction TB
        MERGEABLE_STATUS["● result.mergeable_status<br/>━━━━━━━━━━<br/>DERIVED — tool return only<br/>Source: check_pr_mergeable in tools_git.py<br/>Value: raw GitHub string<br/>MERGEABLE | CONFLICTING | UNKNOWN<br/>Read: on_result routing in<br/>check_mergeability +<br/>check_mergeability_post_rebase<br/>NOT captured into context"]
    end

    subgraph Writers ["Writers (steps that produce state)"]
        direction TB
        WAIT_WRITE["● wait_for_review_pr_mergeability<br/>━━━━━━━━━━<br/>WRITES: context.review_pr_number<br/>Source: gh pr view pr_url | .number"]
        REVIEW_WRITE["● review_pr_integration<br/>━━━━━━━━━━<br/>WRITES: context.review_verdict<br/>Source: result.verdict from review-pr skill"]
        TOOL_WRITE["● check_pr_mergeable (tools_git.py)<br/>━━━━━━━━━━<br/>WRITES: result.mergeable_status<br/>Source: data.get('mergeable', '')"]
    end

    subgraph Readers ["Readers (steps that consume state)"]
        direction TB
        CHECK_READ["● check_mergeability<br/>━━━━━━━━━━<br/>READS: context.review_pr_number → pr_number<br/>READS: result.mergeable_status → on_result routing"]
        RECHECK_READ["● check_mergeability_post_rebase<br/>━━━━━━━━━━<br/>READS: context.review_pr_number → pr_number<br/>READS: result.mergeable_status → on_result routing"]
        RESOLVE_READ["● resolve_integration_conflicts<br/>━━━━━━━━━━<br/>READS: context.integration_branch<br/>READS: inputs.base_branch"]
    end

    PR_URL -->|"consumed by"| WAIT_WRITE
    WAIT_WRITE -->|"writes"| REVIEW_PR_NUM
    REVIEW_PR_NUM -->|"read as pr_number"| CHECK_READ
    REVIEW_PR_NUM -->|"read as pr_number"| RECHECK_READ

    TOOL_WRITE -->|"returns"| MERGEABLE_STATUS
    CHECK_READ -->|"invokes tool → reads"| MERGEABLE_STATUS
    RECHECK_READ -->|"invokes tool → reads"| MERGEABLE_STATUS

    INT_BRANCH -->|"consumed by"| RESOLVE_READ
    INT_BRANCH -->|"consumed by"| REVIEW_WRITE

    REVIEW_WRITE -->|"writes"| REVIEW_VERDICT

    class PR_URL,INT_BRANCH handler;
    class REVIEW_PR_NUM,REVIEW_VERDICT,MERGEABLE_STATUS modified;
    class WAIT_WRITE,REVIEW_WRITE,TOOL_WRITE modified;
    class CHECK_READ,RECHECK_READ,RESOLVE_READ modified;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Orange | Handler | Existing context fields (unchanged) |
| Green (●) | Modified | New fields and steps introduced by this PR |

Closes #328

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/impl-328-20260310-203632-557990/temp/make-plan/pr_pipeline_gates_plan_2026-03-11_120000.md`

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

---------

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

## Summary

Adds three pipeline observability capabilities: a new `get_quota_events`
MCP tool surfacing quota guard decisions from `quota_events.jsonl`,
`wall_clock_seconds` merged into `get_token_summary` output for per-step
wall-clock visibility, and a `.telemetry_cleared_at` replay fence
preventing token accounting drift when the MCP server restarts after a
`clear=True` call. Includes a follow-up refactor extracting
`_get_log_root()` in `tools_status.py` to eliminate three identical
inline log-root expressions.

<details>
<summary>Individual Plan Details</summary>

### Group 1: Pipeline Observability — Quota Guard Logging and Per-Step
Elapsed Time

Three related pipeline observability improvements, tracked as GitHub
issue #302 (collapsing #218, #65, and the #304/#148 token accounting
item):

1. **Quota guard MCP tool** (#218): The `quota_check.py` hook already
writes `quota_events.jsonl` with approved/blocked/cache-miss events. Add
a new ungated `get_quota_events` tool to `tools_status.py` to surface
those decisions through the MCP API.

2. **Wall-clock time in token summary** (#65): Merge `total_seconds`
from the timing log into each step's `get_token_summary` output as
`wall_clock_seconds`, so operators see wall-clock duration alongside
token counts in one call. Updates `_format_token_summary` and
`write_telemetry_files`.

3. **Token accounting drift fix** (#304/#148): Persist a
`.telemetry_cleared_at` timestamp when any log is cleared.
`_state._initialize` reads this on startup and uses `max(now - 24h,
marker_ts)` as the effective replay lower bound, excluding
already-cleared sessions.

### Group 2: Remediation — Extract `_get_log_root()` helper in
`tools_status.py`

The audit identified that `tools_status.py` had three identical inline
expressions — `resolve_log_dir(_get_ctx().config.linux_tracing.log_dir)`
— repeated in `get_pipeline_report`, `get_token_summary`, and
`get_timing_summary`. This remediation adds `_get_log_root()` to
centralize that computation and replaces all three inline call sites. No
behavioral change.

</details>

## Architecture Impact

### Operational Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 65, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    subgraph UngatedTools ["UNGATED MCP TOOLS (tools_status.py)"]
        GTS["● get_token_summary<br/>━━━━━━━━━━<br/>clear=False<br/>+ ● wall_clock_seconds<br/>from timing_log"]
        GTIM["● get_timing_summary<br/>━━━━━━━━━━<br/>clear=False<br/>total_seconds per step"]
        GPR["● get_pipeline_report<br/>━━━━━━━━━━<br/>clear=False<br/>audit failures"]
        GQE["★ get_quota_events<br/>━━━━━━━━━━<br/>n=50<br/>quota guard decisions"]
    end

    subgraph LogRoot ["★ _get_log_root() helper (tools_status.py)"]
        LR["★ _get_log_root()<br/>━━━━━━━━━━<br/>resolve_log_dir(ctx.config<br/>.linux_tracing.log_dir)"]
    end

    subgraph InMemory ["IN-MEMORY PIPELINE LOGS"]
        TK["token_log<br/>━━━━━━━━━━<br/>step_name → tokens<br/>elapsed_seconds"]
        TI["timing_log<br/>━━━━━━━━━━<br/>step_name → total_seconds<br/>(monotonic clock)"]
        AU["audit_log<br/>━━━━━━━━━━<br/>list FailureRecord"]
    end

    subgraph DiskLogs ["PERSISTENT LOG FILES (~/.local/share/autoskillit/logs/)"]
        QE["quota_events.jsonl<br/>━━━━━━━━━━<br/>approved / blocked<br/>cache_miss / parse_error"]
        CM["★ .telemetry_cleared_at<br/>━━━━━━━━━━<br/>UTC ISO timestamp fence<br/>written on clear=True"]
    end

    subgraph Startup ["SERVER STARTUP (_state._initialize)"]
        INIT["● _state._initialize<br/>━━━━━━━━━━<br/>since = max(now−24h, marker)<br/>load_from_log_dir × 3"]
    end

    subgraph Hook ["HOOK (quota_check.py)"]
        QH["quota_check.py<br/>━━━━━━━━━━<br/>PreToolUse: approve/block<br/>_write_quota_log_event"]
    end

    GTS -->|"clear=True"| LR
    GTIM -->|"clear=True"| LR
    GPR -->|"clear=True"| LR
    LR -->|"write_telemetry_clear_marker"| CM
    GQE -->|"_read_quota_events(n)"| QE
    QH -->|"append event"| QE
    GTS -->|"get_report()"| TK
    GTS -->|"● merge wall_clock_seconds"| TI
    GTIM -->|"get_report()"| TI
    GPR -->|"get_report()"| AU
    CM -->|"read marker → since bound"| INIT
    INIT -->|"load_from_log_dir since=effective"| TK
    INIT -->|"load_from_log_dir since=effective"| TI
    INIT -->|"load_from_log_dir since=effective"| AU

    class GTS,GTIM,GPR cli;
    class GQE newComponent;
    class LR newComponent;
    class TK,TI,AU stateNode;
    class QE stateNode;
    class CM newComponent;
    class INIT phase;
    class QH detector;
```

**Color Legend:** Dark Blue = MCP query tools | Green = New components
(`get_quota_events`, `_get_log_root`, `.telemetry_cleared_at`) | Teal =
State/logs | Purple = Server startup | Dark Red = PreToolUse hook

### State Lifecycle Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 65, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;

    subgraph InMem ["IN-MEMORY (clearable — MUTABLE)"]
        TK["● token_log<br/>━━━━━━━━━━<br/>MUTABLE<br/>step_name → tokens + elapsed_seconds<br/>cleared on clear=True"]
        TI["timing_log<br/>━━━━━━━━━━<br/>MUTABLE<br/>step_name → total_seconds<br/>cleared on clear=True"]
        AU["audit_log<br/>━━━━━━━━━━<br/>MUTABLE<br/>list FailureRecord<br/>cleared on clear=True"]
    end

    subgraph Derived ["DERIVED (computed per query)"]
        WC["★ wall_clock_seconds<br/>━━━━━━━━━━<br/>DERIVED<br/>timing_log.get_report()<br/>merged into token summary response<br/>never persisted"]
    end

    subgraph ClearFence ["★ CLEAR FENCE (write-then-read across restarts)"]
        CM["★ .telemetry_cleared_at<br/>━━━━━━━━━━<br/>WRITE-FENCE<br/>UTC ISO timestamp<br/>written atomically by write_telemetry_clear_marker<br/>read exactly once by _initialize"]
    end

    subgraph DiskReplay ["DISK REPLAY (bounded by clear fence)"]
        SJ["sessions.jsonl + session/<br/>━━━━━━━━━━<br/>REPLAY-SOURCE<br/>historical token + timing + audit data<br/>replayed with since= lower bound"]
        QE["quota_events.jsonl<br/>━━━━━━━━━━<br/>APPEND-ONLY<br/>quota hook writes, never rewrites<br/>read by ★ get_quota_events"]
    end

    subgraph ClearGate ["CLEAR GATE (state mutation trigger)"]
        ClearTrue["clear=True in<br/>● get_token_summary /<br/>● get_timing_summary /<br/>● get_pipeline_report<br/>━━━━━━━━━━<br/>1. Clear in-memory log<br/>2. ★ Write .telemetry_cleared_at"]
    end

    subgraph StartupGate ["★ STARTUP REPLAY GATE (_state._initialize)"]
        INIT["● _state._initialize<br/>━━━━━━━━━━<br/>1. Read .telemetry_cleared_at<br/>2. since = max(now−24h, marker)<br/>3. load_from_log_dir × 3<br/>Guards: no double-counting"]
    end

    ClearTrue -->|"1. in_memory.clear()"| TK
    ClearTrue -->|"1. in_memory.clear()"| TI
    ClearTrue -->|"1. in_memory.clear()"| AU
    ClearTrue -->|"2. write_telemetry_clear_marker()"| CM
    TI -->|"get_report() per query"| WC
    WC -->|"★ merged into response"| TK
    CM -->|"read → since bound"| INIT
    SJ -->|"load_from_log_dir since=effective"| INIT
    INIT -->|"populate (bounded)"| TK
    INIT -->|"populate (bounded)"| TI
    INIT -->|"populate (bounded)"| AU

    class TK,TI,AU handler;
    class WC newComponent;
    class CM newComponent;
    class ClearTrue detector;
    class INIT phase;
    class SJ,QE stateNode;
```

**Color Legend:** Orange = MUTABLE in-memory logs | Green = New
(wall_clock_seconds, .telemetry_cleared_at fence) | Dark Red =
clear=True trigger | Purple = startup replay gate | Teal = persistent
disk state

### Module Dependency Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
graph TB
    %% CLASS DEFINITIONS %%
    classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;
    classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff;
    classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff;
    classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;

    subgraph L3 ["L3 — SERVER (tools_status.py, _state.py, helpers.py)"]
        direction LR
        TS["● tools_status.py<br/>━━━━━━━━━━<br/>★ _get_log_root()<br/>★ get_quota_events<br/>● 3 clear=True paths"]
        ST["● _state.py<br/>━━━━━━━━━━<br/>● _initialize<br/>reads clear marker"]
        HLP["● helpers.py<br/>━━━━━━━━━━<br/>re-exports<br/>write/read_telemetry_clear_marker<br/>resolve_log_dir"]
    end

    subgraph L1 ["L1 — EXECUTION (execution/__init__.py, session_log.py)"]
        direction LR
        EINIT["● execution/__init__.py<br/>━━━━━━━━━━<br/>★ exports write/read_telemetry_clear_marker<br/>public API surface"]
        SL["● session_log.py<br/>━━━━━━━━━━<br/>★ write_telemetry_clear_marker()<br/>★ read_telemetry_clear_marker()<br/>_CLEAR_MARKER_FILENAME"]
    end

    subgraph L0 ["L0 — CORE (core/types.py)"]
        TY["● core/types.py<br/>━━━━━━━━━━<br/>● UNGATED_TOOLS frozenset<br/>+ get_quota_events"]
    end

    TS -->|"import resolve_log_dir<br/>write/read_telemetry_clear_marker<br/>(via helpers shim)"| HLP
    ST -->|"★ import read_telemetry_clear_marker<br/>(direct from execution)"| EINIT
    HLP -->|"re-export from execution"| EINIT
    EINIT -->|"defined in"| SL
    TS -.->|"UNGATED_TOOLS check<br/>(via pipeline.gate)"| TY

    class TS,ST,HLP cli;
    class EINIT,SL handler;
    class TY stateNode;
```

**Color Legend:** Dark Blue = L3 server layer | Orange = L1 execution
layer | Teal = L0 core types | Dashed = indirect (via pipeline.gate) |
All imports flow downward (no violations)

Closes #302

## Implementation Plans

Plan files:
- `temp/make-plan/302_pipeline_observability_plan_2026-03-10_204500.md`
-
`temp/make-plan/302_remediation_get_log_root_plan_2026-03-10_210500.md`

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…333, #342 into integration (#351)

## Integration Summary

Collapsed 9 PRs into `pr-batch/pr-merge-20260311-133920` targeting
`integration`.

## Merged PRs

| # | Title | Complexity | Additions | Deletions | Overlaps |
|---|-------|-----------|-----------|-----------|---------|
| #337 | Implementation Plan: Dry Walkthrough — Test Command
Genericization (Issue #307) | simple | +29 | -2 | — |
| #339 | Implementation Plan: Release CI — Force-Push Integration
Back-Sync | simple | +88 | -45 | — |
| #336 | Enhance prepare-issue with Duplicate Detection and Broader
Triggers | needs_check | +161 | -8 | — |
| #332 | Rectify: Display Output Bugs #329 — Terminal Targets
Consolidation — PART A ONLY | needs_check | +783 | -13 | — |
| #338 | Implementation Plan: Pre-release Readiness — Stability Fixes |
needs_check | +238 | -36 | — |
| #343 | Implementation Plan: PR Pipeline Gates — Mergeability Gate and
Review Cycle | needs_check | +384 | -5 | #338 |
| #341 | Pipeline observability: quota events, wall-clock timing, drift
fix | needs_check | +480 | -5 | #332, #338 |
| #333 | Remove run_recipe — Eliminate Sub-Orchestrator Pattern |
needs_check | +538 | -655 | #332, #338, #341 |
| #342 | feat: genericize codebase and bundle external dependencies for
public release | needs_check | +5286 | -1062 | #332, #333, #338, #341,
#343 |

## Audit

**Verdict:** GO

## Architecture Impact

### Development Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;

    subgraph SourceTree ["PROJECT STRUCTURE (● = modified)"]
        direction TB
        SRC["● src/autoskillit/<br/>━━━━━━━━━━<br/>105 .py source files<br/>cli · config · core<br/>execution · hooks · pipeline<br/>recipe · server · workspace"]
        SKILLS["● + ★ src/autoskillit/skills/<br/>━━━━━━━━━━<br/>52 bundled skills<br/>★ 13 arch-lens-* SKILL.md added<br/>★ 3 audit-* SKILL.md added<br/>● 14 existing skills updated"]
        RECIPES["● src/autoskillit/recipes/<br/>━━━━━━━━━━<br/>8 bundled YAML recipes<br/>All recipes updated"]
        TESTS["● + ★ tests/<br/>━━━━━━━━━━<br/>173 .py test files<br/>★ 6 new test files added"]
    end

    subgraph Build ["BUILD TOOLING"]
        direction TB
        PYPROJECT["● pyproject.toml<br/>━━━━━━━━━━<br/>hatchling build backend<br/>uv package manager<br/>10 runtime deps"]
        TASKFILE["Taskfile.yml<br/>━━━━━━━━━━<br/>test-all · test-check<br/>test-smoke · install-worktree"]
    end

    subgraph Quality ["CODE QUALITY GATES"]
        direction TB
        RFMT["ruff-format<br/>━━━━━━━━━━<br/>Auto-fix formatting"]
        RLINT["ruff<br/>━━━━━━━━━━<br/>Lint + auto-fix"]
        MYPY["mypy src/<br/>━━━━━━━━━━<br/>--ignore-missing-imports"]
        UVLOCK["uv lock --check<br/>━━━━━━━━━━<br/>Lock file integrity"]
        SECRETS["gitleaks<br/>━━━━━━━━━━<br/>Secret scanning"]
        GUARD["★ headless_orchestration_guard.py<br/>━━━━━━━━━━<br/>★ PreToolUse hook<br/>Blocks run_skill/run_cmd/run_python<br/>from headless sessions"]
    end

    subgraph Testing ["TEST FRAMEWORK"]
        direction TB
        PYTEST["pytest + asyncio_mode=auto<br/>━━━━━━━━━━<br/>xdist -n 4 parallel<br/>timeout=60s signal method"]
        NEWTEST["★ New Test Files<br/>━━━━━━━━━━<br/>★ test_headless_orchestration_guard<br/>★ test_audit_and_fix_degradation<br/>★ test_rules_inputs<br/>★ test_skill_genericization<br/>★ test_pyproject_metadata<br/>★ test_release_sanity"]
    end

    subgraph CI ["CI/CD WORKFLOWS"]
        direction LR
        TESTS_WF["tests.yml<br/>━━━━━━━━━━<br/>PR test gate"]
        RELEASE_WF["release.yml<br/>━━━━━━━━━━<br/>Release automation"]
        BUMP_WF["● version-bump.yml<br/>━━━━━━━━━━<br/>● Force-push back-sync<br/>integration → main"]
    end

    subgraph EntryPoints ["ENTRY POINTS"]
        EP["autoskillit CLI<br/>━━━━━━━━━━<br/>serve · init · skills<br/>recipes · doctor · workspace"]
    end

    SRC --> PYPROJECT
    SKILLS --> PYPROJECT
    TESTS --> PYTEST
    PYPROJECT --> TASKFILE
    PYPROJECT --> RFMT
    RFMT --> RLINT
    RLINT --> MYPY
    MYPY --> UVLOCK
    UVLOCK --> SECRETS
    SECRETS --> GUARD
    GUARD --> PYTEST
    PYTEST --> NEWTEST
    NEWTEST --> BUMP_WF
    TESTS_WF --> PYTEST
    PYPROJECT --> EP

    class SRC,TESTS stateNode;
    class SKILLS,RECIPES newComponent;
    class PYPROJECT,TASKFILE phase;
    class RFMT,RLINT,MYPY,UVLOCK,SECRETS detector;
    class GUARD newComponent;
    class PYTEST handler;
    class NEWTEST newComponent;
    class TESTS_WF,RELEASE_WF phase;
    class BUMP_WF newComponent;
    class EP output;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Teal | Structure | Source directories and test suite |
| Green (★) | New/Modified | New files and components added in this PR |
| Purple | Build | Build configuration and task automation |
| Red | Quality Gates | Pre-commit hooks, linters, type checker |
| Orange | Test Runner | pytest execution engine |
| Dark Teal | Entry Points | CLI commands |

### Module Dependency Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
graph TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;

    subgraph L0 ["L0 — CORE (zero autoskillit imports)"]
        direction LR
        TYPES["● core/types.py<br/>━━━━━━━━━━<br/>GATED_TOOLS · UNGATED_TOOLS<br/>RecipeSource (★ promoted here)<br/>ClaudeFlags · StrEnums<br/>fan-in: ~75 files"]
        COREIO["core/io.py · logging.py · paths.py<br/>━━━━━━━━━━<br/>Atomic write · Logger · pkg_root()"]
    end

    subgraph L1P ["L1 — PIPELINE (imports L0 only)"]
        direction TB
        GATE["● pipeline/gate.py<br/>━━━━━━━━━━<br/>DefaultGateState<br/>gate_error_result()<br/>★ headless_error_result()<br/>re-exports GATED/UNGATED_TOOLS"]
        PIPEINIT["● pipeline/__init__.py<br/>━━━━━━━━━━<br/>Re-exports public surface<br/>ToolContext · AuditLog<br/>TokenLog · DefaultGateState"]
    end

    subgraph L1E ["L1 — EXECUTION (imports L0 only)"]
        direction TB
        HEADLESS["● execution/headless.py<br/>━━━━━━━━━━<br/>Headless Claude sessions<br/>Imports core types via TYPE_CHECKING<br/>for ToolContext (no runtime cycle)"]
        COMMANDS["● execution/commands.py<br/>━━━━━━━━━━<br/>ClaudeHeadlessCmd builder"]
        SESSION_LOG["● execution/session_log.py<br/>━━━━━━━━━━<br/>Session diagnostics writer"]
    end

    subgraph L2 ["L2 — RECIPE (imports L0+L1)"]
        direction TB
        SCHEMA["● recipe/schema.py<br/>━━━━━━━━━━<br/>Recipe · RecipeStep · DataFlowWarning<br/>RecipeSource (now from L0)"]
        RULES["● recipe/rules_inputs.py<br/>━━━━━━━━━━<br/>★ Ingredient validation rules<br/>reads GATED_TOOLS from L0 via<br/>pipeline re-export"]
        ANALYSIS["● recipe/_analysis.py<br/>━━━━━━━━━━<br/>Step graph builder"]
        VALIDATOR["● recipe/validator.py<br/>━━━━━━━━━━<br/>validate_recipe()"]
    end

    subgraph L3S ["L3 — SERVER (imports all layers)"]
        direction TB
        HELPERS["● server/helpers.py<br/>━━━━━━━━━━<br/>_require_enabled() — reads gate<br/>★ _require_not_headless()<br/>Shared by all tool handlers"]
        TOOLS_EX["● server/tools_execution.py<br/>━━━━━━━━━━<br/>run_cmd · run_python · run_skill<br/>✗ run_recipe REMOVED<br/>Uses _require_not_headless()"]
        TOOLS_GIT["● server/tools_git.py<br/>━━━━━━━━━━<br/>merge_worktree · classify_fix<br/>● check_pr_mergeable (new gate)"]
        TOOLS_K["● server/tools_kitchen.py<br/>━━━━━━━━━━<br/>open_kitchen · close_kitchen"]
        FACTORY["● server/_factory.py<br/>━━━━━━━━━━<br/>Composition root<br/>Wires ToolContext"]
    end

    subgraph L3H ["L3 — HOOKS (stdlib only for guard)"]
        direction LR
        HOOK_GUARD["★ hooks/headless_orchestration_guard.py<br/>━━━━━━━━━━<br/>★ PreToolUse hook (stdlib only)<br/>Blocks run_skill/run_cmd/run_python<br/>from AUTOSKILLIT_HEADLESS=1 sessions<br/>NO autoskillit imports"]
        PRETTY["● hooks/pretty_output.py<br/>━━━━━━━━━━<br/>PostToolUse response formatter"]
    end

    subgraph L3C ["L3 — CLI (imports all layers)"]
        direction LR
        CLI_APP["● cli/app.py<br/>━━━━━━━━━━<br/>serve · init · skills · recipes<br/>doctor · workspace"]
        CLI_PROMPTS["● cli/_prompts.py<br/>━━━━━━━━━━<br/>Orchestrator prompt builder"]
    end

    TYPES -->|"fan-in ~75"| GATE
    TYPES -->|"fan-in ~75"| HEADLESS
    TYPES -->|"fan-in ~75"| SCHEMA
    COREIO --> PIPEINIT
    GATE --> PIPEINIT
    PIPEINIT -->|"gate_error_result<br/>headless_error_result"| HELPERS
    HEADLESS --> HELPERS
    COMMANDS --> HEADLESS
    SESSION_LOG --> HELPERS
    SCHEMA -->|"RecipeSource from L0"| RULES
    RULES --> VALIDATOR
    ANALYSIS --> VALIDATOR
    HELPERS -->|"_require_not_headless"| TOOLS_EX
    HELPERS --> TOOLS_GIT
    HELPERS --> TOOLS_K
    VALIDATOR --> FACTORY
    PIPEINIT --> FACTORY
    FACTORY --> CLI_APP
    FACTORY --> CLI_PROMPTS
    HOOK_GUARD -.->|"ENV: AUTOSKILLIT_HEADLESS<br/>zero autoskillit imports"| TOOLS_EX

    class TYPES,COREIO stateNode;
    class GATE,PIPEINIT phase;
    class HEADLESS,COMMANDS,SESSION_LOG handler;
    class SCHEMA,RULES,ANALYSIS,VALIDATOR phase;
    class HELPERS,TOOLS_EX,TOOLS_GIT,TOOLS_K handler;
    class FACTORY cli;
    class CLI_APP,CLI_PROMPTS cli;
    class HOOK_GUARD newComponent;
    class PRETTY handler;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Teal | L0 Core | High fan-in foundation types (zero reverse deps) |
| Purple | L1/L2 Control | Pipeline gate, recipe schema and rules |
| Orange | L1/L3 Processors | Execution handlers, server tool handlers |
| Dark Blue | L3 CLI | Composition root and CLI entry points |
| Green (★) | New Components | headless_orchestration_guard — standalone
hook |
| Dashed | ENV Signal | OS-level check; no code import relationship |

Closes #307
Closes #327
Closes #308
Closes #329
Closes #304
Closes #328
Closes #302
Closes #330
Closes #311

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


---

## Merge Conflict Resolution

The batch branch was rebased onto `integration` to resolve 17 file
conflicts. All conflicts arose because PRs #337#341 were squash-merged
into both `integration` (directly) and the batch branch (via the
pipeline), while PRs #333 and #342 required conflict resolution work
that only exists on the batch branch.

**Resolution principle:** Batch branch version wins for all files
touched by #333/#342 conflict resolution and remediation, since that
state was fully tested (3752 passed). Integration-only additions (e.g.
`TestGetQuotaEvents`) were preserved where they don't overlap.

### Per-file decisions

| File | Decision | Rationale |
|------|----------|-----------|
| `CLAUDE.md` | **Batch wins** | Batch has corrected tool inventory
(run_recipe removed, get_quota_events added, 25 kitchen tools) |
| `core/types.py` | **Batch wins** | Batch splits monolithic
UNGATED_TOOLS into WORKER_TOOLS + HEADLESS_BLOCKED_UNGATED_TOOLS;
removes run_recipe from GATED_TOOLS |
| `execution/__init__.py` | **Batch wins** | Batch removes dead exports
(build_subrecipe_cmd, run_subrecipe_session) |
| `execution/headless.py` | **Batch wins** | Batch deletes
run_subrecipe_session function (530+ lines); keeps run_headless_core
with token_log error handling |
| `hooks/pretty_output.py` | **Batch wins** | Batch removes run_recipe
from _UNFORMATTED_TOOLS, adds get_quota_events |
| `recipes/pr-merge-pipeline.yaml` | **Batch wins** | Batch has
base_branch required:true, updated kitchen rules (main instead of
integration) |
| `server/_state.py` | **Batch wins** | Batch adds .telemetry_cleared_at
marker reading in _initialize |
| `server/helpers.py` | **Batch wins** | Batch removes _run_subrecipe
and run_subrecipe_session import; adds _require_not_headless |
| `server/tools_git.py` | **Batch wins** | Batch has updated
classify_fix with git fetch and check_pr_mergeable gate |
| `server/tools_kitchen.py` | **Batch wins** | Batch adds headless gates
to open_kitchen/close_kitchen; adds TOOL_CATEGORIES listing |
| `server/tools_status.py` | **Merge both** | Batch headless gates +
wall_clock_seconds merged with integration's TestGetQuotaEvents
(deduplicated) |
| `tests/conftest.py` | **Batch wins** | Batch replaces
AUTOSKILLIT_KITCHEN_OPEN with AUTOSKILLIT_HEADLESS in fixture |
| `tests/execution/test_headless.py` | **Batch wins** | Batch removes
run_subrecipe_session tests (deleted code); updates docstring |
| `tests/recipe/test_bundled_recipes.py` | **Merge both** | Batch
base_branch=main assertions + integration WF7 graph test both kept |
| `tests/server/test_tools_kitchen.py` | **Batch wins** | Batch adds
headless gate denial tests for open/close kitchen |
| `tests/server/test_tools_status.py` | **Merge both** | Batch headless
gate tests merged with integration quota events tests |

### Post-rebase fixes
- Removed duplicate `TestGetQuotaEvents` class (existed in both batch
commit and auto-merged integration code)
- Fixed stale `_build_tool_listing` → `_build_tool_category_listing`
attribute reference
- Added `if diagram: print(diagram)` to `cli/app.py` cook function (test
expected terminal output)

### Verification
- **3752 passed**, 23 skipped, 0 failures
- 7 architecture contracts kept, 0 broken
- Pre-commit hooks all pass

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary

Add GitHub merge queue awareness to the `analyze-prs` skill. When a
merge queue is
active on the target branch, the skill uses queue-position ordering and
tags all
`MERGEABLE` queue entries as `simple`. When the queue is absent or empty
it falls back
to the existing file-overlap / topological-sort path. The PR manifest
format is
identical in both modes.

Two files change: `src/autoskillit/execution/github.py` gains
`parse_merge_queue_response()`, a pure function that extracts and
normalises merge queue entries from a raw GraphQL response dict; and
`src/autoskillit/skills/analyze-prs/SKILL.md` gains Step 0.5 (merge
queue detection) and branches the existing Steps 1–4 so they are skipped
or simplified when `QUEUE_MODE = true`.

## Requirements

### INFRA — Merge Queue Infrastructure

- **REQ-INFRA-001:** The `integration` branch ruleset must have merge
queue enabled.
- **REQ-INFRA-002:** The CI workflow must include a `merge_group`
trigger so status checks fire on `gh-readonly-queue/*` temporary
branches.

### DETECT — Merge Queue Detection

- **REQ-DETECT-001:** The `analyze-prs` skill must query the GitHub
GraphQL API to determine whether a merge queue is enabled on the target
branch.
- **REQ-DETECT-002:** The `analyze-prs` skill must retrieve all merge
queue entries with their `position`, `state`, and associated PR metadata
when a queue is detected.
- **REQ-DETECT-003:** The system must fall back to existing file-overlap
analysis when the merge queue is not enabled or returns no entries.

### ORDER — Queue-Position Ordering

- **REQ-ORDER-001:** When merge queue entries are available, the PR
manifest must list PRs in queue-position order (ascending `position`
field).
- **REQ-ORDER-002:** PRs with `state: MERGEABLE` in the queue must be
tagged as `simple` in the manifest.
- **REQ-ORDER-003:** The manifest format must remain identical
regardless of whether queue ordering or computed ordering was used.

### FALLBACK — Conflict Resolution Fallback

- **REQ-FALLBACK-001:** If a queue-ordered PR unexpectedly conflicts
during merge, the pipeline must fall back to the existing conflict
resolution cycle.
- **REQ-FALLBACK-002:** The fallback path must not require any changes
to the `pr-merge-pipeline.yaml` recipe structure.

## Architecture Impact

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 45, 'rankSpacing': 55, '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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output      fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector    fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    %% TERMINALS %%
    START([START])
    DONE([COMPLETE])

    %% STEP 0 %%
    subgraph S0 ["Step 0 — Authenticate & List PRs"]
        direction TB
        ListPRs["gh pr list<br/>━━━━━━━━━━<br/>--base branch --state open<br/>--json number,title,headRefName,..."]
        ZeroPRs{"0 PRs?"}
    end

    %% STEP 0.5 %%
    subgraph S05 ["● Step 0.5 — Detect GitHub Merge Queue"]
        direction TB
        ResolveRepo["● gh repo view<br/>━━━━━━━━━━<br/>extract OWNER + REPO"]
        GraphQL["● gh api graphql<br/>━━━━━━━━━━<br/>mergeQueue(branch) entries<br/>position · state · pullRequest"]
        ParseFn["● parse_merge_queue_response()<br/>━━━━━━━━━━<br/>github.py — pure function<br/>returns sorted entry list"]
        QueueDecision{"MERGEABLE<br/>entries?"}
    end

    %% QUEUE PATH %%
    subgraph QP ["Queue Path (QUEUE_MODE = true)"]
        direction TB
        FetchMeta["gh pr view (parallel ≤8)<br/>━━━━━━━━━━<br/>headRefName · files · additions<br/>deletions · changedFiles<br/>(no diffs, no body)"]
        QueueOrder["Order by position ASC<br/>━━━━━━━━━━<br/>from QUEUE_ENTRIES<br/>overlap_with = [] for all"]
        TagSimple["Tag complexity<br/>━━━━━━━━━━<br/>MERGEABLE → simple<br/>other states → needs_check"]
    end

    %% EXISTING PATH %%
    subgraph EP ["● Existing Path (QUEUE_MODE = false)"]
        direction TB
        FetchDiffs["● Step 1: Fetch PR diffs (parallel ≤8)<br/>━━━━━━━━━━<br/>gh pr diff + gh pr view files<br/>+ requirements section"]
        CIReview["● Step 1.5: CI & Review gate<br/>━━━━━━━━━━<br/>gh pr checks · gh pr view reviews<br/>→ ELIGIBLE / CI_BLOCKED / REVIEW_BLOCKED"]
        Overlap["● Step 2: File overlap matrix<br/>━━━━━━━━━━<br/>shared_files per PR pair<br/>conflict if shared_files ≠ []"]
        TopoSort["● Step 3: Topological sort<br/>━━━━━━━━━━<br/>no-overlap first (additions ASC)<br/>then overlap graph order"]
        TagComplex["● Step 4: Tag complexity<br/>━━━━━━━━━━<br/>simple / needs_check<br/>per overlap + size rules"]
    end

    %% STEP 5 %%
    subgraph S5 ["Step 5 — Write Outputs (identical format both modes)"]
        direction TB
        WriteJSON["pr_order_{ts}.json<br/>━━━━━━━━━━<br/>integration_branch · base_branch<br/>prs · ci_blocked_prs · review_blocked_prs"]
        WriteMD["pr_analysis_plan_{ts}.md<br/>━━━━━━━━━━<br/>human-readable plan<br/>notes queue source when QUEUE_MODE"]
    end

    S6["Step 6: Verify & Report<br/>━━━━━━━━━━<br/>validate JSON · emit output tokens"]

    %% FLOW %%
    START --> ListPRs
    ListPRs --> ZeroPRs
    ZeroPRs -->|"yes — empty manifest"| DONE
    ZeroPRs -->|"no"| ResolveRepo
    ResolveRepo --> GraphQL
    GraphQL -->|"parse response"| ParseFn
    ParseFn --> QueueDecision

    QueueDecision -->|"yes → QUEUE_MODE=true"| FetchMeta
    QueueDecision -->|"no → QUEUE_MODE=false"| FetchDiffs
    GraphQL -->|"error (auth/rate/network)"| FetchDiffs

    FetchMeta --> QueueOrder --> TagSimple
    FetchDiffs --> CIReview --> Overlap --> TopoSort --> TagComplex

    TagSimple  --> WriteJSON
    TagComplex --> WriteJSON
    WriteJSON  --> WriteMD
    WriteMD    --> S6
    S6         --> DONE

    %% CLASS ASSIGNMENTS %%
    class START,DONE terminal;
    class ZeroPRs,QueueDecision stateNode;
    class ListPRs,FetchDiffs,CIReview,Overlap,TopoSort,TagComplex handler;
    class ResolveRepo,GraphQL,ParseFn newComponent;
    class FetchMeta,QueueOrder,TagSimple newComponent;
    class WriteJSON,WriteMD output;
    class S6 phase;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal | START / COMPLETE states |
| Teal | State | Decision points (0-PR exit, QUEUE_MODE branch) |
| Orange | Handler | Existing processing steps |
| Green (●) | Modified | Merge queue detection steps — `github.py` +
`SKILL.md` |
| Dark Teal | Output | Written manifest files |
| Purple | Phase | Verify & Report |

Closes #347

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/merge-queue-20260311-214047-666786/temp/make-plan/analyze_prs_merge_queue_plan_2026-03-11_120000.md`

## Token Usage Summary

# Token Summary

## fix

- input_tokens: 629
- output_tokens: 243257
- cache_creation_input_tokens: 826397
- cache_read_input_tokens: 45631198
- invocation_count: 12
- elapsed_seconds: 5893.771816905999

## resolve_review

- input_tokens: 391
- output_tokens: 168303
- cache_creation_input_tokens: 449347
- cache_read_input_tokens: 21658363
- invocation_count: 7
- elapsed_seconds: 4158.092358417003

## audit_impl

- input_tokens: 2756
- output_tokens: 166320
- cache_creation_input_tokens: 581154
- cache_read_input_tokens: 4335466
- invocation_count: 13
- elapsed_seconds: 4878.894512576997

## open_pr

- input_tokens: 239
- output_tokens: 155616
- cache_creation_input_tokens: 470597
- cache_read_input_tokens: 7709497
- invocation_count: 9
- elapsed_seconds: 3999.4891979619897

## review_pr

- input_tokens: 177
- output_tokens: 244448
- cache_creation_input_tokens: 479046
- cache_read_input_tokens: 6204544
- invocation_count: 7
- elapsed_seconds: 4626.790592216992

## plan

- input_tokens: 1665
- output_tokens: 187361
- cache_creation_input_tokens: 655841
- cache_read_input_tokens: 11017819
- invocation_count: 8
- elapsed_seconds: 3779.893521053007

## verify

- input_tokens: 8274
- output_tokens: 151282
- cache_creation_input_tokens: 664012
- cache_read_input_tokens: 12450916
- invocation_count: 10
- elapsed_seconds: 3138.836677256011

## implement

- input_tokens: 785
- output_tokens: 269097
- cache_creation_input_tokens: 876108
- cache_read_input_tokens: 61001114
- invocation_count: 10
- elapsed_seconds: 6131.576118467008

## analyze_prs

- input_tokens: 15
- output_tokens: 29915
- cache_creation_input_tokens: 67472
- cache_read_input_tokens: 374939
- invocation_count: 1
- elapsed_seconds: 524.8599999569997

## merge_pr

- input_tokens: 166
- output_tokens: 56815
- cache_creation_input_tokens: 314110
- cache_read_input_tokens: 4466380
- invocation_count: 9
- elapsed_seconds: 1805.746022643012

## create_review_pr

- input_tokens: 27
- output_tokens: 20902
- cache_creation_input_tokens: 62646
- cache_read_input_tokens: 856008
- invocation_count: 1
- elapsed_seconds: 444.64073076299974

## resolve_merge_conflicts

- input_tokens: 88
- output_tokens: 52987
- cache_creation_input_tokens: 122668
- cache_read_input_tokens: 7419558
- invocation_count: 1
- elapsed_seconds: 975.0682389580033

# Timing Summary

## fix

- total_seconds: 5893.887897783006
- invocation_count: 12

## resolve_review

- total_seconds: 4158.092358417003
- invocation_count: 7

## audit_impl

- total_seconds: 4878.975284532004
- invocation_count: 13

## open_pr

- total_seconds: 3999.5348451210048
- invocation_count: 9

## review_pr

- total_seconds: 4626.790592216992
- invocation_count: 7

## plan

- total_seconds: 3780.017026652018
- invocation_count: 8

## verify

- total_seconds: 3138.9388441649935
- invocation_count: 10

## implement

- total_seconds: 6131.693511301022
- invocation_count: 10

## analyze_prs

- total_seconds: 524.8599999569997
- invocation_count: 1

## merge_pr

- total_seconds: 1805.746022643012
- invocation_count: 9

## create_review_pr

- total_seconds: 444.64073076299974
- invocation_count: 1

## resolve_merge_conflicts

- total_seconds: 975.0682389580033
- invocation_count: 1

## clone

- total_seconds: 14.340457123005763
- invocation_count: 3

## update_ticket

- total_seconds: 1.0304174909979338
- invocation_count: 1

## capture_base_sha

- total_seconds: 0.00960076300543733
- invocation_count: 3

## create_branch

- total_seconds: 1.9964898860052926
- invocation_count: 3

## push_merge_target

- total_seconds: 2.7216036769968923
- invocation_count: 3

## test

- total_seconds: 394.38787825701
- invocation_count: 6

## merge

- total_seconds: 395.920931871995
- invocation_count: 3

## push

- total_seconds: 1.8540772910200758
- invocation_count: 2

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary

- **Branch protection**: Protected-branch validation for merge and push
operations with hooks and structural tests
- **Pipeline observability**: Canonical TelemetryFormatter, quota
events, wall-clock timing, drift fix
- **PR pipeline gates**: Mergeability gate, review cycle, fidelity
checks, CI gating, review-first enforcement
- **Release CI**: Version bump automation, branch sync, force-push
integration back-sync, release workflows
- **Skill hardening**: Anti-prose guards for loop constructs,
loop-boundary detector, skill compliance tests, end-turn hazard
documentation
- **Recipe validation**: Unknown-skill-command semantic rule, arch
import violation fixes
- **PostToolUse hook**: Pretty output formatter with exception boundary
and data-loss bug fixes
- **Dry walkthrough**: Test command genericization, Step 4.5 historical
regression check
- **prepare-issue**: Duplicate detection and broader triggers
- **Display output**: Terminal targets consolidation (Part A)
- **Pre-release stability**: Test failures, arch exemptions, init
idempotency, CLAUDE.md corrections
- **Documentation**: Release docs sprint, getting-started, CLI
reference, architecture, configuration, installation guides
- **readOnlyHint**: Added to all MCP tools for parallel execution
support
- **review-pr skill**: Restored bundled skill and reverted erroneous
deletions

## Test plan

- [ ] CI passes (Preflight checks + Tests on ubuntu-latest)
- [ ] No regressions in existing test suite
- [ ] New tests for branch protection, telemetry formatter, pretty
output, release workflows, skill compliance all pass

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary

Replace the mechanical "Prompt for input values using AskUserQuestion"
instruction — which causes Claude to call `AskUserQuestion` once per
ingredient field — with a conversational block that asks the user what
they want to do, infers ingredient values from the free-form response,
and only follows up on required ingredients that could not be inferred.

The change touches exactly two text locations:
`src/autoskillit/cli/_prompts.py` (`_build_orchestrator_prompt()`) and
`src/autoskillit/server/tools_recipe.py` (`load_recipe` docstring). Both
receive identical replacement text. A cross-reference comment
establishes the sync contract, satisfying REQ-ALIGN-001 without Python
logic changes.

## Requirements

### PROMPT

- **REQ-PROMPT-001:** The orchestrator prompt must instruct Claude to
ask the user what they want to do conversationally rather than prompting
for each ingredient field individually.
- **REQ-PROMPT-002:** Claude must extract ingredient values from the
user's free-form response and only follow up on required values that
could not be inferred.
- **REQ-PROMPT-003:** The ingredient collection instructions must be
identical between the `cook` path (`_build_orchestrator_prompt`) and the
`load_recipe` tool docstring path.

### ALIGN

- **REQ-ALIGN-001:** The orchestrator behavioral instructions shared
between `cook` and `open_kitchen` + `load_recipe` must originate from a
single source of truth.

## Architecture Impact

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;

    %% TERMINALS %%
    START([Recipe Loaded])
    DONE([Pipeline Executing])

    subgraph Entry ["Entry Paths"]
        direction LR
        CLIPath["autoskillit cook recipe<br/>━━━━━━━━━━<br/>CLI Entry Point"]
        MCPPath["load_recipe MCP tool<br/>━━━━━━━━━━<br/>Agent calls tool"]
    end

    subgraph Prompt ["● Prompt Construction (modified)"]
        direction LR
        OrchestratorPrompt["● _build_orchestrator_prompt()<br/>━━━━━━━━━━<br/>cli/_prompts.py<br/>Injects --append-system-prompt"]
        LoadRecipeDoc["● load_recipe docstring<br/>━━━━━━━━━━<br/>server/tools_recipe.py<br/>LLM-visible behavioral contract"]
    end

    subgraph Collection ["● Conversational Ingredient Collection (modified)"]
        direction TB
        AskOpen["● Ask open-ended question<br/>━━━━━━━━━━<br/>What would you like to do?<br/>Single question only"]
        Infer["● Infer ingredient values<br/>━━━━━━━━━━<br/>task, source_dir, run_name…<br/>from free-form response"]
        Gate{"● Required values<br/>all inferred?"}
        FollowUp["● Follow-up question<br/>━━━━━━━━━━<br/>Ask only missing required<br/>values in one question"]
        Defaults["Accept optional ingredients<br/>━━━━━━━━━━<br/>Use defaults unless user<br/>explicitly overrode them"]
    end

    Execute["Execute pipeline steps<br/>━━━━━━━━━━<br/>Call MCP tools directly<br/>per recipe step sequence"]

    %% FLOW %%
    START --> CLIPath & MCPPath
    CLIPath --> OrchestratorPrompt
    MCPPath --> LoadRecipeDoc
    OrchestratorPrompt --> AskOpen
    LoadRecipeDoc --> AskOpen
    AskOpen --> Infer
    Infer --> Gate
    Gate -->|"yes — all required inferred"| Defaults
    Gate -->|"no — gaps remain"| FollowUp
    FollowUp --> Defaults
    Defaults --> Execute
    Execute --> DONE

    %% CLASS ASSIGNMENTS %%
    class START,DONE terminal;
    class CLIPath,MCPPath cli;
    class OrchestratorPrompt,LoadRecipeDoc handler;
    class AskOpen,Infer,FollowUp,Defaults newComponent;
    class Gate stateNode;
    class Execute phase;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal/CLI | Entry points and completion state |
| Orange | Handler | ● Modified prompt construction locations |
| Green | New Component | ● Modified conversational collection flow
nodes |
| Teal | State | Decision: all required values inferred? |
| Purple | Phase | Pipeline execution (unchanged) |

Closes #331

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/conv-ingredients-20260311-214046-753062/temp/make-plan/conversational_ingredient_collection_plan_2026-03-11_120000.md`

## Token Usage Summary

# Token Summary

## fix

- input_tokens: 650
- output_tokens: 261005
- cache_creation_input_tokens: 873078
- cache_read_input_tokens: 46076601
- invocation_count: 13
- elapsed_seconds: 6260.165456347986

## resolve_review

- input_tokens: 391
- output_tokens: 168303
- cache_creation_input_tokens: 449347
- cache_read_input_tokens: 21658363
- invocation_count: 7
- elapsed_seconds: 4158.092358417003

## audit_impl

- input_tokens: 2771
- output_tokens: 174758
- cache_creation_input_tokens: 612009
- cache_read_input_tokens: 4594262
- invocation_count: 14
- elapsed_seconds: 5043.723946458995

## open_pr

- input_tokens: 261
- output_tokens: 170926
- cache_creation_input_tokens: 528835
- cache_read_input_tokens: 8445137
- invocation_count: 10
- elapsed_seconds: 4311.458001982992

## review_pr

- input_tokens: 187
- output_tokens: 246219
- cache_creation_input_tokens: 493945
- cache_read_input_tokens: 6342581
- invocation_count: 8
- elapsed_seconds: 4665.401066615992

## plan

- input_tokens: 1665
- output_tokens: 187361
- cache_creation_input_tokens: 655841
- cache_read_input_tokens: 11017819
- invocation_count: 8
- elapsed_seconds: 3779.893521053007

## verify

- input_tokens: 8274
- output_tokens: 151282
- cache_creation_input_tokens: 664012
- cache_read_input_tokens: 12450916
- invocation_count: 10
- elapsed_seconds: 3138.836677256011

## implement

- input_tokens: 822
- output_tokens: 279442
- cache_creation_input_tokens: 928381
- cache_read_input_tokens: 62486155
- invocation_count: 11
- elapsed_seconds: 6355.765075421015

## analyze_prs

- input_tokens: 15
- output_tokens: 29915
- cache_creation_input_tokens: 67472
- cache_read_input_tokens: 374939
- invocation_count: 1
- elapsed_seconds: 524.8599999569997

## merge_pr

- input_tokens: 166
- output_tokens: 56815
- cache_creation_input_tokens: 314110
- cache_read_input_tokens: 4466380
- invocation_count: 9
- elapsed_seconds: 1805.746022643012

## create_review_pr

- input_tokens: 27
- output_tokens: 20902
- cache_creation_input_tokens: 62646
- cache_read_input_tokens: 856008
- invocation_count: 1
- elapsed_seconds: 444.64073076299974

## resolve_merge_conflicts

- input_tokens: 88
- output_tokens: 52987
- cache_creation_input_tokens: 122668
- cache_read_input_tokens: 7419558
- invocation_count: 1
- elapsed_seconds: 975.0682389580033

## diagnose_ci

- input_tokens: 26
- output_tokens: 3415
- cache_creation_input_tokens: 31027
- cache_read_input_tokens: 326408
- invocation_count: 2
- elapsed_seconds: 97.75469558501209

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…daries (#359)

## Summary

`test_check` spawns pytest without stripping `AUTOSKILLIT_HEADLESS=1`
from the subprocess environment, causing import-time FastMCP kitchen
gate mutation in the child process. A conftest fixture provides
process-level defense-in-depth but leaves the subprocess boundary
unguarded. Separately, `test_check` does not normalize `worktree_path`
with `os.path.realpath()`, unlike its sibling tools.

The architectural weakness is the **absence of a named subprocess
environment contract**: internal env vars (those controlling
server-level behavior) can silently leak into user-code subprocesses
because no layer defines which vars are "server-private" or enforces
their removal at process spawn boundaries. Similarly, path normalization
is a per-tool choice rather than a structural contract.

The solution introduces a named constant
(`AUTOSKILLIT_PRIVATE_ENV_VARS`) that declares which env vars must not
cross the subprocess boundary, a `build_sanitized_env()` function that
enforces the contract at the subprocess spawn site, and structural path
normalization at the tool handler layer. Tests assert directly on the
`env=` kwarg captured by `MockSubprocessRunner`, making the boundary
verifiable rather than relying on fixture state.

## Architecture Impact

### Security Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;
    classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;

    subgraph ServerProc ["MCP SERVER PROCESS (trusted zone)"]
        HEADLESS_VAR["AUTOSKILLIT_HEADLESS=1<br/>━━━━━━━━━━<br/>Controls gate state<br/>Set by headless session parent"]
        PRIVATE_CONST["● core/types.py<br/>━━━━━━━━━━<br/>AUTOSKILLIT_PRIVATE_ENV_VARS<br/>frozenset — single source of truth"]
        CORE_INIT["● core/__init__.py<br/>━━━━━━━━━━<br/>Re-exports AUTOSKILLIT_PRIVATE_ENV_VARS<br/>L0 public surface"]
    end

    subgraph ToolBoundary ["TOOL HANDLER BOUNDARY — ● tools_workspace.py"]
        PATH_RAW["worktree_path: str<br/>━━━━━━━━━━<br/>Caller-supplied path<br/>(may be relative or symlinked)"]
        PATH_NORM["● test_check handler<br/>━━━━━━━━━━<br/>os.path.realpath(worktree_path)<br/>Canonical absolute path"]
    end

    subgraph EnvBoundary ["SUBPROCESS ENV BOUNDARY — ● execution/testing.py"]
        BUILD_ENV["● build_sanitized_env()<br/>━━━━━━━━━━<br/>Reads os.environ<br/>Strips AUTOSKILLIT_PRIVATE_ENV_VARS<br/>Returns clean dict"]
        RUNNER_CALL["● DefaultTestRunner.run(cwd)<br/>━━━━━━━━━━<br/>env = build_sanitized_env()<br/>self._runner(cmd, cwd=cwd, env=env)"]
    end

    subgraph ProcessBoundary ["PROCESS EXECUTION BOUNDARY — execution/process.py"]
        SUBPROCESS_RUNNER["DefaultSubprocessRunner<br/>━━━━━━━━━━<br/>run_managed_async(cmd, env=env)<br/>anyio.open_process(env=sanitized)"]
        PYTEST_PROC["pytest subprocess<br/>━━━━━━━━━━<br/>AUTOSKILLIT_HEADLESS: absent<br/>import-time mcp.disable stays correct"]
    end

    HEADLESS_VAR -.->|"leaks without fix"| SUBPROCESS_RUNNER
    HEADLESS_VAR --> PRIVATE_CONST
    PRIVATE_CONST --> CORE_INIT
    CORE_INIT --> BUILD_ENV
    PATH_RAW --> PATH_NORM
    PATH_NORM --> RUNNER_CALL
    BUILD_ENV -->|"sanitized env"| RUNNER_CALL
    RUNNER_CALL --> SUBPROCESS_RUNNER
    SUBPROCESS_RUNNER --> PYTEST_PROC

    class HEADLESS_VAR gap;
    class PRIVATE_CONST,CORE_INIT stateNode;
    class PATH_RAW cli;
    class PATH_NORM detector;
    class BUILD_ENV,RUNNER_CALL newComponent;
    class SUBPROCESS_RUNNER phase;
    class PYTEST_PROC output;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Orange (gap) | Leaking var | AUTOSKILLIT_HEADLESS=1 — leaks without
fix |
| Teal | Contract constant | AUTOSKILLIT_PRIVATE_ENV_VARS — single
source of truth |
| Dark Blue | Caller input | Raw path string from MCP caller |
| Red | Validation gate | Path normalization and env sanitization guards
|
| Green | ● Modified components | build_sanitized_env(),
DefaultTestRunner.run(), test_check handler |
| Purple | Execution | DefaultSubprocessRunner — subprocess launch |
| Dark Teal | Safe output | pytest subprocess — no longer sees private
vars |

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;
    classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;

    START([MCP caller invokes test_check])

    subgraph HandlerEntry ["● TOOL HANDLER — tools_workspace.py:test_check"]
        RAW_PATH["worktree_path: str<br/>━━━━━━━━━━<br/>Raw caller-supplied path<br/>(may be relative or symlinked)"]
        REALPATH["● os.path.realpath(worktree_path)<br/>━━━━━━━━━━<br/>Resolves symlinks + .. segments<br/>Returns canonical absolute path"]
    end

    subgraph TestRunner ["● DefaultTestRunner.run(cwd) — execution/testing.py"]
        BUILD_ENV["● build_sanitized_env()<br/>━━━━━━━━━━<br/>Copy of os.environ<br/>minus AUTOSKILLIT_PRIVATE_ENV_VARS"]
        RUNNER_INVOKE["● self._runner(command, cwd=cwd,<br/>timeout=timeout, env=sanitized)<br/>━━━━━━━━━━<br/>Calls SubprocessRunner protocol"]
    end

    subgraph SubprocExec ["PROCESS EXECUTION — execution/process.py"]
        OPEN_PROC["anyio.open_process(env=sanitized)<br/>━━━━━━━━━━<br/>Spawns pytest subprocess<br/>with clean environment"]
        WAIT["Wait for subprocess completion<br/>━━━━━━━━━━<br/>Collects stdout, returncode"]
        RETURNCODE{"returncode == 0?<br/>━━━━━━━━━━<br/>check_test_passed()"}
    end

    RESULT_PASS([PASS — return passed=True, output])
    RESULT_FAIL([FAIL — return passed=False, output])

    START --> RAW_PATH
    RAW_PATH --> REALPATH
    REALPATH -->|"resolved cwd"| BUILD_ENV
    BUILD_ENV -->|"sanitized env dict"| RUNNER_INVOKE
    RUNNER_INVOKE --> OPEN_PROC
    OPEN_PROC --> WAIT
    WAIT --> RETURNCODE
    RETURNCODE -->|"yes"| RESULT_PASS
    RETURNCODE -->|"no"| RESULT_FAIL

    class START terminal;
    class RAW_PATH cli;
    class REALPATH detector;
    class BUILD_ENV,RUNNER_INVOKE newComponent;
    class OPEN_PROC,WAIT phase;
    class RETURNCODE stateNode;
    class RESULT_PASS,RESULT_FAIL terminal;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal/Input | Caller entry point, pass/fail results |
| Dark Blue | CLI | Raw caller-supplied worktree_path |
| Red | Detector | os.path.realpath() — path normalization gate |
| Green | ● Modified | build_sanitized_env(), DefaultTestRunner.run() —
new env boundary |
| Purple | Phase | anyio subprocess spawn and wait |
| Teal | State | returncode routing decision |

Closes #334

## Implementation Plan

Plan file:
`temp/rectify/rectify_subprocess_env_isolation_path_normalization_2026-03-11_202021.md`

## Token Usage Summary

## Token Usage Summary

| Step | Calls | Input | Output | Cached | Time (s) |
|------|-------|-------|--------|--------|----------|
| investigate | 3 | 8.5k | 24.3k | 683.2k | 1383.0 |
| rectify | 3 | 88 | 75.7k | 3.8M | 2369.0 |
| dry_walkthrough | 4 | 70 | 50.7k | 2.9M | 1141.5 |
| implement | 14 | 1.1k | 453.3k | 83.2M | 10149.6 |
| verify | 10 | 8.3k | 151.3k | 13.1M | 3138.8 |
| assess | 3 | 72 | 24.1k | 2.2M | 856.4 |
| audit_impl | 10 | 4.1k | 137.0k | 4.1M | 4335.6 |
| **Total** | | **25.1k** | **1.9M** | **205.6M** | |

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…n recipes (#361)

## Summary

Both `implementation.yaml` and `remediation.yaml` call `release_issue`
on the success path
(`release_issue_success` step), which removes the `in-progress` label
from the GitHub issue
after CI passes and the PR is opened. This is incorrect — the
in-progress label should stay on
the issue while there is an open PR. Only the failure path should
release the label.

The fix removes the `release_issue_success` step from both recipes and
routes `ci_watch.on_success`
directly to `confirm_cleanup`. The `release_issue_failure` step and all
failure-path routing
remain unchanged.

## Requirements

### RECIPE

- **REQ-RECIPE-001:** The `implementation.yaml` recipe must NOT call
`release_issue` on the success path when a PR has been opened.
- **REQ-RECIPE-002:** The `remediation.yaml` recipe must NOT call
`release_issue` on the success path when a PR has been opened.
- **REQ-RECIPE-003:** The `ci_watch` step `on_success` must route
directly to `confirm_cleanup` (bypassing `release_issue_success`) in
both recipes.
- **REQ-RECIPE-004:** The `release_issue_failure` step must remain
unchanged — the in-progress label is still released on pipeline failure.

## Architecture Impact

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, '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;

    %% TERMINALS %%
    UPSTREAM(["upstream steps<br/>(review_pr / re_push / re_push_review)"])
    DONE(["done<br/>(clone preserved)"])
    DELETE_CLONE(["delete_clone<br/>(clone removed)"])
    ESCALATE(["escalate_stop"])

    %% CI WATCH — MODIFIED %%
    CW["● ci_watch<br/>━━━━━━━━━━<br/>wait_for_ci<br/>branch: merge_target<br/>timeout: 300s<br/>skip when open_pr=false"]

    subgraph SuccessPath ["SUCCESS PATH (label stays on issue — PR still open)"]
        direction TB
        CC["confirm_cleanup<br/>━━━━━━━━━━<br/>action: confirm<br/>Delete clone now?"]
    end

    subgraph FailureDiagnosis ["FAILURE PATH — CI diagnosis & retry loop"]
        direction TB
        DC["diagnose_ci<br/>━━━━━━━━━━<br/>run_skill: diagnose-ci<br/>writes diagnosis_path"]
        RC["resolve_ci<br/>━━━━━━━━━━<br/>run_skill: resolve-failures<br/>fixes CI issues"]
        RP["re_push<br/>━━━━━━━━━━<br/>push_to_remote<br/>triggers new CI run"]
    end

    subgraph FailureCleanup ["FAILURE CLEANUP — label released only on failure"]
        direction TB
        RIF["release_issue_failure<br/>━━━━━━━━━━<br/>release_issue tool<br/>skip when no issue_url<br/>removes in-progress label"]
        CLF["cleanup_failure<br/>━━━━━━━━━━<br/>remove_clone keep=true<br/>preserves clone on disk"]
    end

    %% FLOW %%
    UPSTREAM --> CW
    CW -->|"on_success"| CC
    CW -->|"on_failure"| DC

    CC -->|"yes (on_success)"| DELETE_CLONE
    CC -->|"no (on_failure)"| DONE

    DC -->|"on_success / on_failure"| RC
    RC --> RP
    RP -->|"on_success"| CW

    RP -->|"on_failure / max retries"| RIF
    RIF -->|"on_success / on_failure"| CLF
    CLF -->|"on_success / on_failure"| ESCALATE

    %% CLASS ASSIGNMENTS %%
    class UPSTREAM,DONE,DELETE_CLONE,ESCALATE terminal;
    class CW handler;
    class CC phase;
    class DC,RC,RP handler;
    class RIF,CLF detector;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal | Start/end states (upstream, done, escalate) |
| Orange | Handler | Recipe tool-call steps (ci_watch, diagnose_ci,
resolve_ci, re_push) |
| Purple | Phase | Control/confirmation steps (confirm_cleanup) |
| Red | Detector | Failure guards and cleanup steps
(release_issue_failure, cleanup_failure) |

**Modification Key:** `●` = Modified by this PR

Closes #335

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/fix-release-label-20260311-214045-907910/temp/make-plan/fix_release_label_plan_2026-03-11_214500.md`

## Token Usage Summary

# Token Summary

## fix

- input_tokens: 617
- output_tokens: 241470
- cache_creation_input_tokens: 802321
- cache_read_input_tokens: 45488590
- invocation_count: 11
- elapsed_seconds: 5789.439858307993

## resolve_review

- input_tokens: 391
- output_tokens: 168303
- cache_creation_input_tokens: 449347
- cache_read_input_tokens: 21658363
- invocation_count: 7
- elapsed_seconds: 4158.092358417003

## audit_impl

- input_tokens: 2744
- output_tokens: 159180
- cache_creation_input_tokens: 545076
- cache_read_input_tokens: 4174746
- invocation_count: 12
- elapsed_seconds: 4455.282969588996

## open_pr

- input_tokens: 213
- output_tokens: 137769
- cache_creation_input_tokens: 422276
- cache_read_input_tokens: 6866150
- invocation_count: 8
- elapsed_seconds: 3639.0721166939948

## review_pr

- input_tokens: 177
- output_tokens: 244448
- cache_creation_input_tokens: 479046
- cache_read_input_tokens: 6204544
- invocation_count: 7
- elapsed_seconds: 4626.790592216992

## plan

- input_tokens: 1665
- output_tokens: 187361
- cache_creation_input_tokens: 655841
- cache_read_input_tokens: 11017819
- invocation_count: 8
- elapsed_seconds: 3779.893521053007

## verify

- input_tokens: 8274
- output_tokens: 151282
- cache_creation_input_tokens: 664012
- cache_read_input_tokens: 12450916
- invocation_count: 10
- elapsed_seconds: 3138.836677256011

## implement

- input_tokens: 785
- output_tokens: 269097
- cache_creation_input_tokens: 876108
- cache_read_input_tokens: 61001114
- invocation_count: 10
- elapsed_seconds: 6131.576118467008

## analyze_prs

- input_tokens: 15
- output_tokens: 29915
- cache_creation_input_tokens: 67472
- cache_read_input_tokens: 374939
- invocation_count: 1
- elapsed_seconds: 524.8599999569997

## merge_pr

- input_tokens: 166
- output_tokens: 56815
- cache_creation_input_tokens: 314110
- cache_read_input_tokens: 4466380
- invocation_count: 9
- elapsed_seconds: 1805.746022643012

## create_review_pr

- input_tokens: 27
- output_tokens: 20902
- cache_creation_input_tokens: 62646
- cache_read_input_tokens: 856008
- invocation_count: 1
- elapsed_seconds: 444.64073076299974

## resolve_merge_conflicts

- input_tokens: 88
- output_tokens: 52987
- cache_creation_input_tokens: 122668
- cache_read_input_tokens: 7419558
- invocation_count: 1
- elapsed_seconds: 975.0682389580033

## Timing Summary

## fix

- total_seconds: 5789.520738078008
- invocation_count: 11

## resolve_review

- total_seconds: 4158.092358417003
- invocation_count: 7

## audit_impl

- total_seconds: 4455.326102384002
- invocation_count: 12

## open_pr

- total_seconds: 3639.0721166939948
- invocation_count: 8

## review_pr

- total_seconds: 4626.790592216992
- invocation_count: 7

## plan

- total_seconds: 3780.017026652018
- invocation_count: 8

## verify

- total_seconds: 3138.9388441649935
- invocation_count: 10

## implement

- total_seconds: 6131.693511301022
- invocation_count: 10

## analyze_prs

- total_seconds: 524.8599999569997
- invocation_count: 1

## merge_pr

- total_seconds: 1805.746022643012
- invocation_count: 9

## create_review_pr

- total_seconds: 444.64073076299974
- invocation_count: 1

## resolve_merge_conflicts

- total_seconds: 975.0682389580033
- invocation_count: 1

## clone

- total_seconds: 14.340457123005763
- invocation_count: 3

## update_ticket

- total_seconds: 1.0304174909979338
- invocation_count: 1

## capture_base_sha

- total_seconds: 0.00960076300543733
- invocation_count: 3

## create_branch

- total_seconds: 1.9964898860052926
- invocation_count: 3

## push_merge_target

- total_seconds: 2.7216036769968923
- invocation_count: 3

## test

- total_seconds: 332.84049233200494
- invocation_count: 5

## merge

- total_seconds: 395.920931871995
- invocation_count: 3

## push

- total_seconds: 0.9490148440090707
- invocation_count: 1

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
) (#358)

## Summary

`SkillResult` carried two independently-sourced status fields that could
contradict each other: `success` (computed by multi-signal adjudication)
and `subtype` (verbatim copy of the raw Claude CLI NDJSON string). When
adjudication overrode the CLI's reported outcome — e.g., the CLI emitted
`"success"` but the session had an empty result — the field pair became
`success=False, subtype="success"`. This contradiction propagated
unchanged into the MCP JSON response, the PostToolUse hook display
(`FAIL [success]`), the pipeline audit log, and session diagnostics on
disk.

The fix promotes `SkillResult.subtype` from a raw copy to an
**adjudicated field** consistent with `success`, via a new
`_normalize_subtype()` gate function, while preserving the raw CLI
diagnostic in a new `cli_subtype` field. This eliminates the
contradiction at the source and makes the data model self-consistent
everywhere it travels.

## Architecture Impact

### State Lifecycle Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%%
flowchart TB
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;
    classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;

    subgraph RawSignals ["RAW CLI SIGNALS"]
        CLI["Claude CLI NDJSON<br/>━━━━━━━━━━<br/>subtype: raw string<br/>is_error: bool"]
        SESSION["ClaudeSessionResult<br/>━━━━━━━━━━<br/>.subtype (raw CLI copy)<br/>.is_error (raw CLI copy)"]
    end

    subgraph Adjudication ["OUTCOME ADJUDICATION"]
        OUTCOME["_compute_outcome()<br/>━━━━━━━━━━<br/>Multi-signal: returncode +<br/>termination + channel<br/>→ SessionOutcome enum"]
    end

    subgraph NormGate ["● NORMALIZATION GATE (FIXED)"]
        NORMALIZE["● _normalize_subtype()<br/>━━━━━━━━━━<br/>outcome + cli_subtype<br/>SUCCEEDED+error_subtype → 'success'<br/>FAILED+'success'+empty → 'empty_result'<br/>FAILED+'success'+no_marker → 'missing_completion_marker'<br/>else → passthrough"]
    end

    GAP["✗ BEFORE (eliminated)<br/>━━━━━━━━━━<br/>verbatim copy path:<br/>session.subtype → SkillResult.subtype<br/>No gate → contradiction possible"]

    subgraph SkillResultContract ["● SkillResult FIELD CONTRACT"]
        SUBTYPE_ADJ["● .subtype (adjudicated)<br/>━━━━━━━━━━<br/>INVARIANT: subtype=='success'<br/>iff success==True<br/>Consumer: hook display, routing"]
        CLI_SUBTYPE_FIELD["★ .cli_subtype (diagnostic)<br/>━━━━━━━━━━<br/>Raw CLI value preserved<br/>Consumer: logs, debugging<br/>Default: '' (backward compat)"]
        OTHER_FIELDS["success / needs_retry / is_error<br/>━━━━━━━━━━<br/>Computed from outcome<br/>Unchanged fields"]
    end

    subgraph Consumers ["CONSUMERS"]
        HOOK["_fmt_run_skill()<br/>━━━━━━━━━━<br/>pretty_output.py<br/>Reads .subtype for display<br/>No longer contradictory"]
        AUDIT["FailureRecord.subtype<br/>━━━━━━━━━━<br/>_capture_failure()<br/>Receives normalized subtype"]
        SESLOG["● session_log / sessions.jsonl<br/>━━━━━━━━━━<br/>Records .subtype (adjudicated)<br/>Records .cli_subtype (raw)"]
        TESTS["★ test_normalize_subtype.py<br/>━━━━━━━━━━<br/>Unit tests for gate<br/>Invariant enforcement"]
    end

    CLI --> SESSION
    SESSION --> OUTCOME
    SESSION --> CLI_SUBTYPE_FIELD
    OUTCOME --> NORMALIZE
    SESSION --> NORMALIZE
    NORMALIZE --> SUBTYPE_ADJ
    OUTCOME --> OTHER_FIELDS
    SUBTYPE_ADJ --> HOOK
    SUBTYPE_ADJ --> AUDIT
    SUBTYPE_ADJ --> SESLOG
    CLI_SUBTYPE_FIELD --> SESLOG
    SUBTYPE_ADJ --> TESTS

    class CLI cli;
    class SESSION handler;
    class OUTCOME phase;
    class NORMALIZE stateNode;
    class GAP gap;
    class SUBTYPE_ADJ stateNode;
    class CLI_SUBTYPE_FIELD newComponent;
    class OTHER_FIELDS phase;
    class HOOK output;
    class AUDIT output;
    class SESLOG output;
    class TESTS newComponent;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | CLI | Raw Claude CLI NDJSON input signal |
| Orange | Handler | Raw signal parsing (`ClaudeSessionResult`) |
| Purple | Phase | Adjudication engine and computed fields |
| Dark Teal | Gate/State | `● _normalize_subtype()` gate + adjudicated
`.subtype` |
| Bright Green | New/Modified | `★ cli_subtype` diagnostic field + new
test file |
| Dark Teal (output) | Consumers | Hook display, audit log, session log
|
| Amber | Eliminated | Pre-fix verbatim copy path (removed) |

Closes #346

## Implementation Plan

Plan file:
`/home/talon/projects/generic_automation_mcp/temp/rectify/rectify_skill_result_dual_source_subtype_2026-03-11_202312.md`

## Token Usage Summary

## Token Usage Summary

| Step | Calls | Input | Output | Cached | Time (s) |
|------|-------|-------|--------|--------|----------|
| investigate | 3 | 8.5k | 24.3k | 683.2k | 1383.0 |
| rectify | 3 | 88 | 75.7k | 3.8M | 2369.0 |
| dry_walkthrough | 4 | 70 | 50.7k | 2.9M | 1141.5 |
| implement | 14 | 1.1k | 453.3k | 83.2M | 10149.6 |
| verify | 10 | 8.3k | 151.3k | 13.1M | 3138.8 |
| assess | 3 | 72 | 24.1k | 2.2M | 856.4 |
| audit_impl | 10 | 4.1k | 137.0k | 4.1M | 4335.6 |
| **Total** | | **25.1k** | **1.9M** | **205.6M** | |

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… PART A ONLY (#360)

## Summary

The diagram renderer (`recipe/diagrams.py`) has three active layout bugs
— no line-length
guard on horizontal chains, unbounded side-leg indentation, and
unnormalized ingredient
descriptions — but no test has ever detected them because every test
that touches the
renderer either (a) compares output against itself (circular golden-file
roundtrip), (b) mocks
the renderer with synthetic strings, or (c) checks structural tokens
without spatial
constraints. The core architectural weakness is the **absence of a
committed correctness
oracle**: there is no machine-checkable encoding of what "correct"
renderer output looks like.

Part A installs three layers of immunity: (1) a committed spec-oracle
fixture — the definition
of correct renderer output; (2) spatial property tests — width
constraints that make the bug
class impossible to hide; and (3) a public version constant with a
consistency gate — forces
deliberate version bumps when rendering logic changes.

## Architecture Impact

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 55, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;

    START([START])
    END([END])

    subgraph DiagramGen ["● DIAGRAM GENERATION (diagrams.py)"]
        direction TB
        GEN["● generate_recipe_diagram()<br/>━━━━━━━━━━<br/>Entry: pipeline_path, recipes_dir, out_dir"]
        LAYOUT["_compute_layout(recipe)<br/>━━━━━━━━━━<br/>Classify: main_chain, side_legs,<br/>routing_blocks, hidden"]
        CHAINW{"chain width<br/>> 115 chars?"}
        HORIZ["_render_for_each_chain()<br/>━━━━━━━━━━<br/>Horizontal: ─── join"]
        VERT["_render_for_each_chain()<br/>━━━━━━━━━━<br/>Vertical fallback: ↓ per step"]
        VER["● DIAGRAM_FORMAT_VERSION<br/>━━━━━━━━━━<br/>Public export, v7<br/>Embedded in HTML comment"]
        DIAG_OUT["diagram .md artifact<br/>━━━━━━━━━━<br/>Written atomically to<br/>recipes/diagrams/{name}.md"]
    end

    subgraph LoadPipeline ["● LOAD RECIPE PIPELINE (_api.py)"]
        direction TB
        CACHE{"cache hit?<br/>━━━━━━━━━━<br/>mtime + version check"}
        FIND["find_recipe_by_name()<br/>━━━━━━━━━━<br/>Searches project + bundled"]
        VALIDATE["validate_recipe()<br/>━━━━━━━━━━<br/>Structural + semantic rules"]
        STALE{"diagram stale?<br/>━━━━━━━━━━<br/>check_diagram_staleness()"}
        WARN["append stale-diagram<br/>warning to suggestions"]
        LOAD_DIAG["load_recipe_diagram()<br/>━━━━━━━━━━<br/>Read pre-generated .md from disk"]
        RESULT["● LoadRecipeResult<br/>━━━━━━━━━━<br/>valid, diagram, suggestions,<br/>kitchen_rules"]
    end

    subgraph HookFormatter ["● PRETTY OUTPUT HOOK (pretty_output.py)"]
        direction TB
        HOOK_IN["PostToolUse stdin event<br/>━━━━━━━━━━<br/>tool_name + tool_response JSON"]
        UNWRAP{"MCP envelope?<br/>━━━━━━━━━━<br/>result is string?"}
        DISPATCH{"short_name in<br/>_FORMATTERS?"}
        FMT_LR["● _fmt_load_recipe()<br/>━━━━━━━━━━<br/>Renders: header + diagram<br/>+ suggestions bullets"]
        FMT_GEN["_fmt_generic()"]
        HOOK_OUT["hookSpecificOutput<br/>━━━━━━━━━━<br/>updatedMCPToolOutput → Claude"]
    end

    subgraph QualityGates ["★ NEW QUALITY GATES (test_diagrams.py)"]
        direction TB
        FIXTURES["★ spec_diagram_recipe.yaml<br/>★ spec_diagram_expected.md<br/>━━━━━━━━━━<br/>Committed correctness oracle"]
        TSPEC["★ T-SPEC-1<br/>━━━━━━━━━━<br/>actual == spec_diagram_expected.md<br/>char-for-char oracle"]
        TSPATIAL["★ T-SPATIAL-1<br/>━━━━━━━━━━<br/>max line width ≤ 120<br/>all bundled recipes"]
        TVER["★ T-VER-1<br/>━━━━━━━━━━<br/>spec embedded version ==<br/>DIAGRAM_FORMAT_VERSION"]
    end

    START --> GEN
    GEN --> LAYOUT
    LAYOUT --> CHAINW
    CHAINW -->|"fits"| HORIZ
    CHAINW -->|"overflow"| VERT
    HORIZ --> VER
    VERT --> VER
    VER --> DIAG_OUT
    DIAG_OUT -->|"on-disk artifact"| CACHE
    CACHE -->|"miss"| FIND
    CACHE -->|"hit"| RESULT
    FIND --> VALIDATE
    VALIDATE --> STALE
    STALE -->|"yes"| WARN
    STALE -->|"no"| LOAD_DIAG
    WARN --> LOAD_DIAG
    LOAD_DIAG --> RESULT
    RESULT -->|"MCP tool response"| HOOK_IN
    HOOK_IN --> UNWRAP
    UNWRAP -->|"yes — unwrap inner JSON"| DISPATCH
    UNWRAP -->|"no"| DISPATCH
    DISPATCH -->|"load_recipe"| FMT_LR
    DISPATCH -->|"other"| FMT_GEN
    FMT_LR --> HOOK_OUT
    FMT_GEN --> HOOK_OUT
    HOOK_OUT --> END
    FIXTURES -->|"input"| TSPEC
    VER -->|"constant"| TVER
    DIAG_OUT -->|"output under test"| TSPATIAL
    GEN -->|"actual output"| TSPEC

    class START,END terminal;
    class GEN,LAYOUT,HORIZ,VERT handler;
    class VER,DIAG_OUT stateNode;
    class CACHE,CHAINW,STALE,UNWRAP,DISPATCH phase;
    class FIND,VALIDATE,LOAD_DIAG handler;
    class RESULT stateNode;
    class HOOK_IN,HOOK_OUT cli;
    class FMT_LR,FMT_GEN handler;
    class WARN detector;
    class TSPEC,TSPATIAL,TVER,FIXTURES newComponent;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal/CLI | Start/end states; hook I/O events |
| Orange | Handler | Processing functions: renderer, validator,
formatter |
| Teal | State | Data artifacts: format version constant, diagram file,
LoadRecipeResult |
| Purple | Decision | Decision points: cache check, chain width guard,
staleness, dispatch |
| Green | New Component | New quality gates: T-SPEC-1, T-SPATIAL-1,
T-VER-1, committed fixtures |
| Red | Detector | Warning path: stale-diagram suggestion appended |

### Module Dependency Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
graph TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;

    subgraph L3 ["L3 — HOOKS LAYER"]
        direction LR
        HOOKS["● hooks/pretty_output.py<br/>━━━━━━━━━━<br/>Stdlib-only at runtime<br/>TYPE_CHECKING → LoadRecipeResult"]
    end

    subgraph L2 ["L2 — RECIPE PACKAGE (all ●)"]
        direction TB
        INIT["● recipe/__init__.py<br/>━━━━━━━━━━<br/>Public surface aggregator<br/>Exports: LoadRecipeResult,<br/>generate_recipe_diagram,<br/>DIAGRAM_FORMAT_VERSION"]
        API["● recipe/_api.py<br/>━━━━━━━━━━<br/>● LoadRecipeResult TypedDict<br/>load_and_validate() pipeline<br/>Fan-in: 3 (init, repo, hooks)"]
        DIAGS["● recipe/diagrams.py<br/>━━━━━━━━━━<br/>● DIAGRAM_FORMAT_VERSION (public)<br/>generate_recipe_diagram()<br/>Fan-in: 2 (init, _api)"]
        REPO["● recipe/repository.py<br/>━━━━━━━━━━<br/>DefaultRecipeRepository<br/>Delegates to _api"]
        INTERNALS["recipe internals<br/>━━━━━━━━━━<br/>_analysis, contracts, io,<br/>schema, staleness_cache,<br/>validator, rules_*"]
    end

    subgraph L0 ["L0 — CORE"]
        direction LR
        CORE["core/<br/>━━━━━━━━━━<br/>_atomic_write, get_logger,<br/>load_yaml, pkg_root,<br/>LoadResult, YAMLError"]
    end

    subgraph Tests ["TEST LAYER (●/★)"]
        direction LR
        TDIAGS["● test_diagrams.py<br/>━━━━━━━━━━<br/>★ T-SPEC-1, T-SPATIAL-1, T-VER-1<br/>imports: diagrams.DIAGRAM_FORMAT_VERSION"]
        TAPI["● test_api.py<br/>━━━━━━━━━━<br/>imports: recipe._api.LoadRecipeResult"]
        TPRETTY["● test_pretty_output.py<br/>━━━━━━━━━━<br/>imports: hooks.pretty_output"]
        FIXTURES["★ spec_diagram_recipe.yaml<br/>★ spec_diagram_expected.md<br/>━━━━━━━━━━<br/>Committed oracle fixtures"]
    end

    HOOKS -.->|"TYPE_CHECKING only<br/>LoadRecipeResult"| API
    INIT -->|"imports"| API
    INIT -->|"imports"| DIAGS
    INIT -->|"imports"| REPO
    INIT -->|"imports"| INTERNALS
    REPO -->|"imports"| API
    API -->|"imports"| DIAGS
    API -->|"imports"| INTERNALS
    DIAGS -->|"imports"| INTERNALS
    API -->|"imports"| CORE
    DIAGS -->|"imports"| CORE
    INIT -->|"imports"| CORE
    TDIAGS -->|"imports DIAGRAM_FORMAT_VERSION"| DIAGS
    TAPI -->|"imports LoadRecipeResult"| API
    TPRETTY -->|"imports"| HOOKS
    FIXTURES -.->|"oracle input"| TDIAGS

    class HOOKS cli;
    class INIT,REPO phase;
    class API stateNode;
    class DIAGS handler;
    class INTERNALS handler;
    class CORE integration;
    class TDIAGS,TAPI,TPRETTY,FIXTURES newComponent;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Hooks/CLI | L3 hooks layer: pretty_output.py |
| Purple | Recipe Aggregators | recipe/__init__.py and repository.py |
| Teal | High Fan-In | recipe/_api.py — 3 dependents; LoadRecipeResult
TypedDict lives here |
| Orange | Processors | recipe/diagrams.py and internals |
| Red | External/Core | core/ — foundation layer |
| Green | New/Test | New test fixtures, new public exports |
| Dashed Lines | TYPE_CHECKING | Runtime-invisible import — hooks →
recipe coupling is type-check only |

Closes #353

## Implementation Plan

Plan file:
`/home/talon/projects/generic_automation_mcp/temp/rectify/rectify_cook_recipe_card_display_destroyed_tests_validate_stability_not_correctness_2026-03-11_202028.md`

## Token Usage Summary

| Step | Calls | Input | Output | Cached | Time (s) |
|------|-------|-------|--------|--------|----------|
| investigate | 3 | 8.5k | 24.3k | 683.2k | 1383.0 |
| rectify | 3 | 88 | 75.7k | 3.8M | 2369.0 |
| dry_walkthrough | 4 | 70 | 50.7k | 2.9M | 1141.5 |
| implement | 14 | 1.1k | 453.3k | 83.2M | 10149.6 |
| verify | 10 | 8.3k | 151.3k | 13.1M | 3138.8 |
| assess | 3 | 72 | 24.1k | 2.2M | 856.4 |
| audit_impl | 10 | 4.1k | 137.0k | 4.1M | 4335.6 |
| **Total** | | **25.1k** | **1.9M** | **205.6M** | |

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary

The merge queue integration adds a `wait_for_merge_queue` MCP tool
backed by a new L1 polling service (`DefaultMergeQueueWatcher`) that
monitors PR state through GitHub's merge queue until merged, ejected, or
timed out — including stuck-queue detection with auto-merge toggle
recovery. Recipes (`pr-merge-pipeline`, `implementation`, `remediation`)
gain queue-aware routing paths that auto-merge PRs via the queue when
available, with automated conflict resolution for ejected PRs and
graceful fallback to the classic batch-branch path when no queue is
detected. The `analyze-prs` skill emits a `queue_mode` token to enable
queue detection, and documentation covers the
`min_entries_to_merge_wait_minutes=0` configuration recommendation.

<details>
<summary>Individual Group Plans</summary>

### Group A: Implementation Plan: Merge Queue Integration — PART A ONLY
Part A implements the `wait_for_merge_queue` MCP tool: a new L1 polling
service (`DefaultMergeQueueWatcher`) in `execution/merge_queue.py`, a
new gated tool handler in `server/tools_ci.py`, plumbing through
`core/types.py` / `pipeline/context.py` / `server/_factory.py`, and full
test coverage with 9 watcher tests + 5 tool handler tests.

### Group B: Implementation Plan: Merge Queue Integration — PART B ONLY
Part B adapts three recipes and one skill for merge queue awareness:
`analyze-prs` skill emits `queue_mode=true|false`;
`pr-merge-pipeline.yaml` gains dual-path routing (queue mode bypasses
the batch-branch cycle); `implementation.yaml` and `remediation.yaml`
gain a queue-aware finale after `ci_watch` success; and
`docs/configuration.md` gains a merge queue configuration section.

</details>

## Requirements

### TOOL — wait_for_merge_queue MCP Tool

- **REQ-TOOL-001:** The system must provide a `wait_for_merge_queue` MCP
tool that polls a PR's merge queue state until merged, ejected, or timed
out.
- **REQ-TOOL-002:** The tool must query the GitHub GraphQL merge queue
API to determine PR queue position and state.
- **REQ-TOOL-003:** The tool must detect stuck PRs (auto-merge enabled,
checks passing, CLEAN status, not in queue) and toggle auto-merge to
recover.
- **REQ-TOOL-004:** The tool must return a structured result indicating
`merged`, `ejected`, or `timeout` with a reason string.
- **REQ-TOOL-005:** The tool must use the same timeout and polling
pattern as `wait_for_ci` (default 600s, 15s interval).

### PIPELINE — pr-merge-pipeline Queue Mode

- **REQ-PIPELINE-001:** When `analyze-prs` detects a merge queue on the
target branch, the pipeline must enter all PRs into the queue via `gh pr
merge --squash --auto` in the analyzed order.
- **REQ-PIPELINE-002:** The pipeline must use `wait_for_merge_queue` to
monitor each PR until merged or ejected.
- **REQ-PIPELINE-003:** Ejected PRs must route to the existing conflict
resolution cycle (plan → implement → test → push → re-enter queue →
wait).
- **REQ-PIPELINE-004:** The batch branch (`pr-batch/pr-merge-*`) must be
skipped in queue mode — PRs merge directly into the target branch.
- **REQ-PIPELINE-005:** When no merge queue is detected, the pipeline
must fall back to the existing sequential merge path unchanged.

### RECIPE — Implementation and Remediation Adaptation

- **REQ-RECIPE-001:** Implementation and remediation recipes must detect
merge queue availability on the target branch after review resolution.
- **REQ-RECIPE-002:** When a merge queue is available, recipes must
enable auto-merge and wait for queue completion using
`wait_for_merge_queue`.
- **REQ-RECIPE-003:** When a queue-entered PR is ejected, the recipe
must invoke `resolve-failures`, push, re-enter the queue, and wait
again.
- **REQ-RECIPE-004:** When no merge queue is available, recipes must
leave the PR open with passing checks for the pr-merge-pipeline to
handle.

### CONFIG — Queue Settings

- **REQ-CONFIG-001:** The `min_entries_to_merge_wait_minutes` setting on
the integration branch ruleset should be documented as recommended `0`
for automation use cases.

## Architecture Impact

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 38, 'rankSpacing': 48, 'curve': 'basis'}}}%%
flowchart TB
    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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    MERGED([success=True<br/>pr_state=merged])
    EJECTED([success=False<br/>pr_state=ejected])
    TIMEOUT([success=False<br/>pr_state=timeout])

    subgraph Watcher ["★ DefaultMergeQueueWatcher.wait() — execution/merge_queue.py"]
        direction TB
        ValidateRepo["★ Validate repo<br/>━━━━━━━━━━<br/>repo must be 'owner/name'<br/>else → error"]
        DeadlineLoop{"★ deadline<br/>exceeded?"}
        FetchPR["★ _fetch_pr_state()<br/>━━━━━━━━━━<br/>GET /repos/{owner}/{repo}/pulls/{n}<br/>→ state, merged, auto_merge,<br/>mergeable_state"]
        MergedCheck{"★ pr merged?"}
        ClosedCheck{"★ pr closed<br/>not merged?"}
        FetchQueue["★ _fetch_queue_entries()<br/>━━━━━━━━━━<br/>POST /graphql<br/>→ mergeQueue.entries<br/>{pr_number, state}[]"]
        InQueueCheck{"★ PR in<br/>queue?"}
        UnmergeableCheck{"★ state ==<br/>UNMERGEABLE?"}
        StuckCheck{"★ _is_stuck()?<br/>━━━━━━━━━━<br/>auto_merge set &<br/>mergeable CLEAN/<br/>has_hooks"}
        StuckCap{"★ stuck_cycles<br/>< 1?"}
        ToggleAuto["★ _toggle_auto_merge()<br/>━━━━━━━━━━<br/>DELETE /auto-merge<br/>sleep(2s)<br/>PUT /auto-merge squash"]
        Sleep["★ asyncio.sleep<br/>━━━━━━━━━━<br/>poll_interval (15s)"]
    end

    subgraph PipelineQueueMode ["● pr-merge-pipeline.yaml — Queue Mode Path (after analyze_prs)"]
        direction TB
        RouteQueueMode{"● route_by_queue_mode<br/>━━━━━━━━━━<br/>context.queue_mode == true?"}
        EnqueueAll["★ enqueue_all_prs<br/>━━━━━━━━━━<br/>gh pr merge each PR<br/>--squash --auto"]
        GetFirstPR["★ get_first_pr_number<br/>━━━━━━━━━━<br/>jq .[0].number<br/>from pr_order_file"]
        WaitQueuePR["★ wait_queue_pr<br/>━━━━━━━━━━<br/>wait_for_merge_queue<br/>current_pr_number"]
        PRStateRoute{"★ pr_state?"}
        NextQueuePR["★ next_queue_pr_or_done<br/>━━━━━━━━━━<br/>advance current_pr_index"]
        MorePRs{"★ more_prs?"}
        GetEjectedBranch["★ get_ejected_pr_branch<br/>━━━━━━━━━━<br/>gh pr view --json headRefName"]
        CheckoutEjected["★ checkout_ejected_pr<br/>━━━━━━━━━━<br/>git fetch + checkout"]
        ResolveEjected["★ resolve_ejected_conflicts<br/>━━━━━━━━━━<br/>/resolve-merge-conflicts skill"]
        PushEjectedFix["★ push_ejected_fix<br/>━━━━━━━━━━<br/>git push --force-with-lease"]
        ReenterQueue["★ reenter_queue<br/>━━━━━━━━━━<br/>gh pr merge --squash --auto"]
        ClassicPath["create_integration_branch<br/>━━━━━━━━━━<br/>(classic batch-branch path)"]
        CollectArtifacts["collect_artifacts<br/>━━━━━━━━━━<br/>→ create_review_pr"]
    end

    subgraph ImplQueueFinale ["● implementation.yaml / remediation.yaml — Queue Finale (after ci_watch)"]
        direction TB
        CheckMQ["★ check_merge_queue<br/>━━━━━━━━━━<br/>gh api graphql<br/>mergeQueue(branch) { id }<br/>→ queue_available=true|false"]
        RouteQueueAvail{"★ route_queue_mode<br/>━━━━━━━━━━<br/>queue_available == true?"}
        EnableAutoMerge["★ enable_auto_merge<br/>━━━━━━━━━━<br/>gh pr merge {pr_number}<br/>--squash --auto"]
        WaitForQueue["★ wait_for_queue<br/>━━━━━━━━━━<br/>wait_for_merge_queue<br/>pr_number, base_branch"]
        ImplPRState{"★ pr_state?"}
        QueueEjectedFix["★ queue_ejected_fix<br/>━━━━━━━━━━<br/>/resolve-merge-conflicts skill<br/>on feature branch"]
        RePushFix["★ re_push_queue_fix<br/>━━━━━━━━━━<br/>push_to_remote<br/>merge_target branch"]
        ReenterMQ["★ reenter_merge_queue<br/>━━━━━━━━━━<br/>gh pr merge {pr_number}<br/>--squash --auto"]
        ConfirmCleanup["confirm_cleanup<br/>━━━━━━━━━━<br/>done / clone deletion"]
    end

    ValidateRepo --> DeadlineLoop
    DeadlineLoop -->|"within budget"| FetchPR
    DeadlineLoop -->|"exceeded"| TIMEOUT
    FetchPR -->|"HTTP error → retry"| Sleep
    FetchPR --> MergedCheck
    MergedCheck -->|"yes"| MERGED
    MergedCheck -->|"no"| ClosedCheck
    ClosedCheck -->|"yes"| EJECTED
    ClosedCheck -->|"no"| FetchQueue
    FetchQueue -->|"error → treat as []"| InQueueCheck
    FetchQueue --> InQueueCheck
    InQueueCheck -->|"yes"| UnmergeableCheck
    UnmergeableCheck -->|"UNMERGEABLE"| EJECTED
    UnmergeableCheck -->|"other state"| Sleep
    InQueueCheck -->|"no"| StuckCheck
    StuckCheck -->|"not stuck"| EJECTED
    StuckCheck -->|"stuck"| StuckCap
    StuckCap -->|"cycles < 1"| ToggleAuto
    StuckCap -->|"cycles >= 1"| EJECTED
    ToggleAuto --> Sleep
    Sleep --> DeadlineLoop

    RouteQueueMode -->|"queue_mode=true"| EnqueueAll
    RouteQueueMode -->|"queue_mode=false"| ClassicPath
    EnqueueAll --> GetFirstPR
    GetFirstPR --> WaitQueuePR
    WaitQueuePR --> PRStateRoute
    PRStateRoute -->|"merged"| NextQueuePR
    PRStateRoute -->|"ejected"| GetEjectedBranch
    PRStateRoute -->|"timeout"| ClassicPath
    NextQueuePR --> MorePRs
    MorePRs -->|"more_prs"| WaitQueuePR
    MorePRs -->|"all_done"| CollectArtifacts
    GetEjectedBranch --> CheckoutEjected
    CheckoutEjected --> ResolveEjected
    ResolveEjected -->|"escalation=false"| PushEjectedFix
    ResolveEjected -->|"escalation=true"| ClassicPath
    PushEjectedFix --> ReenterQueue
    ReenterQueue --> WaitQueuePR

    CheckMQ --> RouteQueueAvail
    RouteQueueAvail -->|"queue_available=true"| EnableAutoMerge
    RouteQueueAvail -->|"queue_available=false"| ConfirmCleanup
    EnableAutoMerge -->|"failure → degrade"| ConfirmCleanup
    EnableAutoMerge --> WaitForQueue
    WaitForQueue --> ImplPRState
    ImplPRState -->|"merged"| ConfirmCleanup
    ImplPRState -->|"timeout"| ConfirmCleanup
    ImplPRState -->|"ejected"| QueueEjectedFix
    QueueEjectedFix --> RePushFix
    RePushFix --> ReenterMQ
    ReenterMQ --> WaitForQueue

    class MERGED,EJECTED,TIMEOUT terminal;
    class MergedCheck,ClosedCheck,InQueueCheck,UnmergeableCheck,StuckCheck,StuckCap,DeadlineLoop stateNode;
    class RouteQueueMode,PRStateRoute,MorePRs,RouteQueueAvail,ImplPRState stateNode;
    class FetchPR,FetchQueue,Sleep,ClassicPath,CollectArtifacts,ConfirmCleanup handler;
    class ValidateRepo,ToggleAuto,EnqueueAll,GetFirstPR,WaitQueuePR,NextQueuePR newComponent;
    class GetEjectedBranch,CheckoutEjected,ResolveEjected,PushEjectedFix,ReenterQueue newComponent;
    class CheckMQ,EnableAutoMerge,WaitForQueue,QueueEjectedFix,RePushFix,ReenterMQ newComponent;
    class Watcher,PipelineQueueMode,ImplQueueFinale phase;
```

### C4 Container Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 48, 'rankSpacing': 55, 'curve': 'basis'}}}%%
graph TB
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;

    Orchestrator(["Orchestrator<br/>━━━━━━━━━━<br/>Recipe pipeline<br/>running headless session"])

    subgraph Recipes ["● Recipe Layer — recipes/ + skills/"]
        direction TB
        ImplRecipe["● implementation.yaml<br/>━━━━━━━━━━<br/>YAML recipe<br/>Queue-aware finale after ci_watch"]
        PipelineRecipe["● pr-merge-pipeline.yaml<br/>━━━━━━━━━━<br/>YAML recipe<br/>Queue mode dual-path routing"]
        RemedRecipe["● remediation.yaml<br/>━━━━━━━━━━<br/>YAML recipe<br/>Queue-aware finale (same as impl)"]
        AnalyzePRs["● analyze-prs/SKILL.md<br/>━━━━━━━━━━<br/>Bash skill<br/>Emits queue_mode=true|false"]
    end

    subgraph Server ["● server/ — FastMCP Tool Layer (L3)"]
        direction TB
        ToolsCI["● tools_ci.py<br/>━━━━━━━━━━<br/>FastMCP / Python<br/>wait_for_ci + ★ wait_for_merge_queue"]
        Factory["● _factory.py<br/>━━━━━━━━━━<br/>Python composition root<br/>Instantiates ★ DefaultMergeQueueWatcher"]
    end

    subgraph Pipeline ["● pipeline/ — DI Container Layer (L1)"]
        direction TB
        ToolContext["● pipeline/context.py<br/>━━━━━━━━━━<br/>Python dataclass<br/>Adds ★ merge_queue_watcher field"]
    end

    subgraph Core ["● core/ — Foundation Layer (L0)"]
        direction TB
        Types["● core/types.py<br/>━━━━━━━━━━<br/>Python Protocol + frozenset<br/>★ MergeQueueWatcher protocol<br/>GATED_TOOLS += wait_for_merge_queue"]
        CoreInit["● core/__init__.py<br/>━━━━━━━━━━<br/>Python re-export<br/>Re-exports ★ MergeQueueWatcher"]
    end

    subgraph Execution ["★ execution/ — L1 Service Layer"]
        direction TB
        MergeQueueWatcher["★ execution/merge_queue.py<br/>━━━━━━━━━━<br/>Python/httpx async<br/>DefaultMergeQueueWatcher<br/>Polls PR + queue state"]
        ExecInit["● execution/__init__.py<br/>━━━━━━━━━━<br/>Python re-export<br/>Re-exports ★ DefaultMergeQueueWatcher"]
    end

    subgraph External ["External Systems"]
        direction LR
        GHREST["GitHub REST API<br/>━━━━━━━━━━<br/>HTTPS/JSON<br/>GET /pulls/{n} → PR state"]
        GHGQL["GitHub GraphQL API<br/>━━━━━━━━━━<br/>HTTPS/GraphQL<br/>mergeQueue.entries query"]
        GHCLI["GitHub CLI (gh)<br/>━━━━━━━━━━<br/>Subprocess<br/>gh pr merge --squash --auto"]
    end

    Orchestrator -->|"loads recipe"| ImplRecipe
    Orchestrator -->|"loads recipe"| PipelineRecipe
    Orchestrator -->|"invokes"| AnalyzePRs

    ImplRecipe -->|"calls wait_for_merge_queue"| ToolsCI
    PipelineRecipe -->|"calls wait_for_merge_queue"| ToolsCI
    RemedRecipe -->|"calls wait_for_merge_queue"| ToolsCI
    ImplRecipe -->|"calls run_cmd: gh pr merge"| GHCLI
    PipelineRecipe -->|"calls run_cmd: gh pr merge"| GHCLI

    AnalyzePRs -->|"writes queue_mode=true|false"| PipelineRecipe

    ToolsCI -->|"reads merge_queue_watcher"| ToolContext
    Factory -->|"instantiates + injects"| MergeQueueWatcher
    Factory -->|"builds"| ToolContext

    ToolContext -->|"holds ref to"| MergeQueueWatcher
    Types -->|"defines protocol"| MergeQueueWatcher
    CoreInit -->|"re-exports"| Types
    ExecInit -->|"re-exports"| MergeQueueWatcher

    MergeQueueWatcher -->|"GET /repos/../pulls/{n}"| GHREST
    MergeQueueWatcher -->|"POST /graphql mergeQueue"| GHGQL
    MergeQueueWatcher -->|"DELETE+PUT /auto-merge"| GHREST

    class Orchestrator cli;
    class ImplRecipe,PipelineRecipe,RemedRecipe,AnalyzePRs output;
    class ToolsCI,Factory handler;
    class ToolContext stateNode;
    class Types,CoreInit handler;
    class MergeQueueWatcher,ExecInit newComponent;
    class GHREST,GHGQL,GHCLI integration;
```

### Module Dependency Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 46, 'rankSpacing': 55, 'curve': 'basis'}}}%%
graph TB
    classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;
    classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff;
    classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff;
    classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;

    subgraph L3 ["L3 — SERVER (changed)"]
        direction LR
        ToolsCI2["● server/tools_ci.py<br/>━━━━━━━━━━<br/>wait_for_merge_queue tool<br/>+ _infer_repo_from_remote()"]
        Factory2["● server/_factory.py<br/>━━━━━━━━━━<br/>make_context()<br/>instantiates DefaultMergeQueueWatcher"]
    end

    subgraph L1_Exec ["L1 — EXECUTION (changed)"]
        direction LR
        MergeQueueMod["★ execution/merge_queue.py<br/>━━━━━━━━━━<br/>DefaultMergeQueueWatcher<br/>_fetch_pr_state, _fetch_queue_entries<br/>_toggle_auto_merge, _is_stuck"]
        ExecInit2["● execution/__init__.py<br/>━━━━━━━━━━<br/>re-exports DefaultMergeQueueWatcher<br/>(fan-in: Factory)"]
    end

    subgraph L1_Pipeline ["L1 — PIPELINE (changed)"]
        direction LR
        CtxModule["● pipeline/context.py<br/>━━━━━━━━━━<br/>ToolContext dataclass<br/>+ merge_queue_watcher field<br/>(fan-in: ToolsCI, Factory)"]
    end

    subgraph L0 ["L0 — CORE (changed)"]
        direction LR
        Types2["● core/types.py<br/>━━━━━━━━━━<br/>★ MergeQueueWatcher Protocol<br/>GATED_TOOLS updated<br/>(fan-in: context, __init__)"]
        CoreInit2["● core/__init__.py<br/>━━━━━━━━━━<br/>re-exports MergeQueueWatcher<br/>(fan-in: pipeline/context.py)"]
    end

    subgraph L0_Ext ["L0 — EXTERNAL"]
        direction LR
        HttpxPkg["httpx<br/>━━━━━━━━━━<br/>AsyncClient<br/>HTTP/GraphQL calls"]
        StdlibPkg["stdlib: asyncio, time, re<br/>━━━━━━━━━━<br/>async runtime + timing<br/>regex for remote URL"]
    end

    subgraph Tests ["Tests (new + modified)"]
        direction LR
        TestMQ["★ tests/execution/test_merge_queue.py<br/>━━━━━━━━━━<br/>TestDefaultMergeQueueWatcher<br/>9 test cases"]
        TestToolsCI2["● tests/server/test_tools_ci.py<br/>━━━━━━━━━━<br/>TestWaitForMergeQueue<br/>5 test cases added"]
        TestPipeline["★ tests/recipe/test_pr_merge_pipeline_queue.py<br/>━━━━━━━━━━<br/>5 recipe structural tests<br/>queue mode steps"]
    end

    Factory2 -->|"imports DefaultMergeQueueWatcher"| ExecInit2
    ExecInit2 -->|"re-exports from"| MergeQueueMod

    ToolsCI2 -->|"reads tool_ctx.merge_queue_watcher"| CtxModule
    Factory2 -->|"builds ToolContext with watcher"| CtxModule

    CtxModule -->|"imports MergeQueueWatcher type"| CoreInit2
    CoreInit2 -->|"re-exports from"| Types2

    MergeQueueMod -->|"imports get_logger"| CoreInit2

    MergeQueueMod -->|"httpx.AsyncClient"| HttpxPkg
    MergeQueueMod -->|"asyncio.sleep / subprocess"| StdlibPkg

    TestMQ -->|"imports DefaultMergeQueueWatcher"| MergeQueueMod
    TestToolsCI2 -->|"imports wait_for_merge_queue"| ToolsCI2
    TestPipeline -->|"loads pr-merge-pipeline recipe"| L0

    class ToolsCI2,Factory2 cli;
    class MergeQueueMod newComponent;
    class ExecInit2,CtxModule handler;
    class Types2,CoreInit2 stateNode;
    class HttpxPkg,StdlibPkg integration;
    class TestMQ,TestToolsCI2,TestPipeline phase;
```

Closes #349

## Implementation Plan

Plan files:
-
`/home/talon/projects/autoskillit-runs/merge-queue-20260312-173206/temp/make-plan/merge_queue_integration_plan_2026-03-12_173206_part_a.md`
-
`/home/talon/projects/autoskillit-runs/merge-queue-20260312-173206/temp/make-plan/merge_queue_integration_plan_2026-03-12_173206_part_b.md`

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## 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

```mermaid
%%{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;
```

**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](https://claude.com/claude-code) via
AutoSkillit

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…splay

Eliminates the dual-formatter anti-pattern where two independent formatting
implementations (server-side bullet lists and hook-side Markdown-KV) produced
incompatible outputs for the same telemetry data.

- Create TelemetryFormatter in pipeline layer with format_token_table(),
  format_timing_table(), and format_compact_kv() methods
- Add format parameter to get_token_summary and get_timing_summary MCP tools
  (format="json" default, format="table" returns pre-formatted markdown)
- Rewrite write_telemetry_files to use TelemetryFormatter and merge
  wall_clock_seconds (previously bypassed, root cause of field never
  reaching file output)
- Delete _format_token_summary() and _format_timing_summary() server-side
  formatters
- Update PostToolUse hook to prefer wall_clock_seconds, add dedicated
  _fmt_get_timing_summary formatter, handle pre-formatted responses
- Simplify recipe TOKEN SUMMARY instructions from 5-step manual chain
  to single get_token_summary(format=table) call
- Add telemetry-before-open-pr semantic rule (WARNING)
- Fix stale open-pr skill_contracts.yaml entry
- Add comprehensive tests: TelemetryFormatter unit tests, format parameter
  tests, output-equivalence test (hook ≡ canonical), format-structural
  assertions replacing weak presence-only checks, semantic rule tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Trecek and others added 13 commits March 15, 2026 03:10
## Summary

Implements sub-recipe composition — a mechanism for `implementation` and
`remediation` recipes to optionally load a sprint workflow prefix
(triage → sprint-plan → confirm → dispatch → report) via a hidden
ingredient gate. Part A adds the infrastructure: new schema fields
(`hidden`, `sub_recipe`, `gate`), lazy-loading logic that either drops
or merges sub-recipe steps at load time, validation updates, a new
`rules_recipe` semantic rule module, and MCP tool parameter extensions.
Part B delivers the content: the `sprint-prefix` sub-recipe YAML,
updates to `implementation.yaml` and `remediation.yaml`, removal of the
standalone `dev-sprint` recipe, integration tests, and documentation.

<details>
<summary>Individual Group Plans</summary>

### Group 1: Sub-Recipe Composition — Lazy-Loaded Recipe Prefixes — PART
A ONLY

Part A implements the **sub-recipe composition infrastructure**: new
schema fields, lazy-loading logic in the recipe loader, ingredient
hiding, validation updates, a new semantic rule module, and MCP tool
parameter extensions. No sprint sub-recipe content is created in this
part.

### Group 2: Sub-Recipe Composition — Sprint Content & Cleanup — PART B
ONLY

Part B delivers the **content and cleanup**: the sprint sub-recipe YAML,
diagram, updates to `implementation.yaml` and `remediation.yaml`,
removal of `dev-sprint` artifacts, integration tests, and documentation.

</details>

## Requirements

### Schema (SCHEMA)

- **REQ-SCHEMA-001:** The `RecipeStep` dataclass must support a field
that references a sub-recipe by name for prefix/suffix injection.
- **REQ-SCHEMA-002:** The recipe parser must recognize sub-recipe
references and resolve them to file paths without loading their content
eagerly.
- **REQ-SCHEMA-003:** Ingredients must support a `hidden: true` field
that suppresses display in parameter tables and interactive prompts
while still accepting explicit values.
- **REQ-SCHEMA-004:** Sub-recipe declarations must live in separate YAML
files — never inlined in the parent recipe.

### Loading (LOAD)

- **REQ-LOAD-001:** When a hidden ingredient gating a sub-recipe is
`"false"` (default), the loader must not read, parse, or present the
sub-recipe file to the agent.
- **REQ-LOAD-002:** When the gating ingredient is `"true"`, the loader
must resolve the sub-recipe file, parse it, and merge its steps into the
parent recipe's step graph at the declared attachment point.
- **REQ-LOAD-003:** The combined step graph (parent + sub-recipe) must
be presented to the agent as a single coherent recipe with correct
routing between parent and sub-recipe steps.
- **REQ-LOAD-004:** Sub-recipe ingredient values must be resolvable from
the parent recipe's context and captured values at load time.

### Validation (VALID)

- **REQ-VALID-001:** `validate_recipe` must verify that referenced
sub-recipes exist by name at validation time.
- **REQ-VALID-002:** Validation must detect circular sub-recipe
references (A references B which references A).
- **REQ-VALID-003:** Semantic rules must cover sub-recipe step types for
dataflow and routing analysis.
- **REQ-VALID-004:** Validation of a parent recipe with a hidden
sub-recipe must validate both the standalone parent and the combined
(parent + sub-recipe) graph.

### Sprint Sub-Recipe (SPRINT)

- **REQ-SPRINT-001:** The sprint sub-recipe must analyze open GitHub
issues and partition them into conflict-free parallel groups based on
file and component overlap.
- **REQ-SPRINT-002:** Issue selection must be influenced by the user's
target feature, milestone, or area of concern when one is specified.
- **REQ-SPRINT-003:** The sprint planner must have access to
`issue-splitter`, `collapse-issues`, and `enrich-issues` MCP tools and
the discretion to use them when reorganizing work boundaries would
improve parallelism or when issues lack sufficient requirements.
- **REQ-SPRINT-004:** Sprint items must be made aware of each other's
affected files and components, even when perfect merge-conflict
avoidance is not achievable.
- **REQ-SPRINT-005:** The sprint sub-recipe must present the planned
sprint to the user for approval with issue numbers, titles, routes,
affected systems, and a component overlap map.
- **REQ-SPRINT-006:** Sprint selection must be conversational — the user
can approve, remove issues, adjust the sprint composition, or abort.
- **REQ-SPRINT-007:** The sprint sub-recipe must dispatch each approved
issue to the parent recipe (implementation or remediation) with
appropriate ingredient values.
- **REQ-SPRINT-008:** The sprint size must be configurable via an
ingredient. Default sprint size: 4 issues.
- **REQ-SPRINT-009:** The sprint sub-recipe must produce a sprint
summary report with per-issue status and PR URLs.

### Cleanup (CLEAN)

- **REQ-CLEAN-001:** The standalone `dev-sprint.yaml` recipe file must
be removed.
- **REQ-CLEAN-002:** The standalone `diagrams/dev-sprint.md` diagram
must be removed.
- **REQ-CLEAN-003:** The `TestDevSprintRecipe` test class and related
module-level tests must be removed.
- **REQ-CLEAN-004:** The sprint sub-recipe must have its own diagram
file stored alongside the sub-recipe YAML.

### Documentation (DOC)

- **REQ-DOC-001:** The sub-recipe composition mechanism must be
documented in repo-level documentation (not surfaced at runtime).
- **REQ-DOC-002:** The `sprint_mode` hidden ingredient and its behavior
must be documented in the same repo-level docs.
- **REQ-DOC-003:** Runtime recipe loading, rendering, and help output
must not reference hidden ingredients or sub-recipes.

## Architecture Impact

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, '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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    %% TERMINALS %%
    START([load_recipe / open_kitchen called])
    DONE([Agent receives active recipe])
    ERROR([Error returned])

    subgraph Entry ["● Entry Points (extended with overrides)"]
        direction TB
        LoadRecipeTool["● load_recipe MCP tool<br/>━━━━━━━━━━<br/>+ overrides: dict[str,str] param"]
        OpenKitchenTool["● open_kitchen MCP tool<br/>━━━━━━━━━━<br/>+ overrides: dict[str,str] param"]
    end

    subgraph Parse ["1 · Parse Phase"]
        direction TB
        CacheCheck["● load_and_validate<br/>━━━━━━━━━━<br/>cache_key now includes overrides<br/>hit → return cached result"]
        FindRecipe{"recipe found?"}
        ParseYAML["● _parse_recipe<br/>━━━━━━━━━━<br/>hidden on ingredients<br/>sub_recipe + gate on steps"]
        HasSubRecipe{"sub_recipe steps<br/>present?"}
    end

    subgraph BuildActive ["2 · ★ _build_active_recipe (new)"]
        direction TB
        EvalGate["★ evaluate gate ingredient<br/>━━━━━━━━━━<br/>check overrides first<br/>fall back to ingredient.default<br/>gate_value = 'true' / 'false'"]
        GateDecision{"gate_value<br/>== 'true'?"}
        DropStep["★ _drop_sub_recipe_step<br/>━━━━━━━━━━<br/>Remove placeholder step<br/>Recipe identical to today"]
        FindSR["★ find_sub_recipe_by_name<br/>━━━━━━━━━━<br/>project-local first<br/>then builtin sub-recipes/"]
        SRFound{"sub-recipe<br/>file found?"}
        MergeSteps["★ _merge_sub_recipe<br/>━━━━━━━━━━<br/>prefix step names<br/>fix routing: done→on_success<br/>merge ingredients & kitchen_rules"]
    end

    subgraph Validate ["3 · ● Validation Phase (extended)"]
        direction TB
        ValidateStandalone["● validate_recipe (standalone)<br/>━━━━━━━━━━<br/>sub_recipe is valid discriminator<br/>gate + on_success required"]
        ValidateCombined["★ validate_recipe (combined)<br/>━━━━━━━━━━<br/>only when gate=true<br/>REQ-VALID-004"]
        SemanticRules["● run_semantic_rules<br/>━━━━━━━━━━<br/>● ValidationContext + available_sub_recipes<br/>★ rules_recipe: unknown-sub-recipe<br/>★ rules_recipe: circular-sub-recipe"]
    end

    subgraph Serve ["4 · Serve Phase"]
        direction TB
        FormatTable["● format_ingredients_table<br/>━━━━━━━━━━<br/>★ skips hidden=True ingredients<br/>merged when gate=true"]
        BuildResult["build LoadRecipeResult<br/>━━━━━━━━━━<br/>active recipe YAML content<br/>diagram + suggestions"]
    end

    %% FLOW %%
    START --> LoadRecipeTool
    START --> OpenKitchenTool
    LoadRecipeTool --> CacheCheck
    OpenKitchenTool --> CacheCheck
    CacheCheck --> FindRecipe
    FindRecipe -->|"not found"| ERROR
    FindRecipe -->|"found"| ParseYAML
    ParseYAML --> HasSubRecipe
    HasSubRecipe -->|"no (unchanged path)"| ValidateStandalone
    HasSubRecipe -->|"yes"| EvalGate
    EvalGate --> GateDecision
    GateDecision -->|"false (default)"| DropStep
    GateDecision -->|"true"| FindSR
    DropStep --> ValidateStandalone
    FindSR --> SRFound
    SRFound -->|"not found"| ERROR
    SRFound -->|"found"| MergeSteps
    MergeSteps --> ValidateStandalone
    MergeSteps --> ValidateCombined
    ValidateStandalone --> SemanticRules
    ValidateCombined --> SemanticRules
    SemanticRules --> FormatTable
    FormatTable --> BuildResult
    BuildResult --> DONE

    %% CLASS ASSIGNMENTS %%
    class START,DONE,ERROR terminal;
    class LoadRecipeTool,OpenKitchenTool,CacheCheck,ParseYAML,ValidateStandalone,SemanticRules,FormatTable,BuildResult handler;
    class FindRecipe,HasSubRecipe,GateDecision,SRFound stateNode;
    class EvalGate,DropStep phase;
    class FindSR,MergeSteps,ValidateCombined newComponent;
```

### Module Dependency Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
graph TB
    %% CLASS DEFINITIONS %%
    classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;
    classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff;
    classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff;
    classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;

    subgraph L3 ["LAYER 3 — SERVER (MCP Tool Handlers)"]
        direction LR
        ToolsRecipe["● tools_recipe.py<br/>━━━━━━━━━━<br/>load_recipe MCP tool<br/>+ overrides param"]
        ToolsKitchen["● tools_kitchen.py<br/>━━━━━━━━━━<br/>open_kitchen MCP tool<br/>+ overrides param"]
    end

    subgraph L2 ["LAYER 2 — RECIPE API (Repository Facade)"]
        direction LR
        Repository["● repository.py<br/>━━━━━━━━━━<br/>DefaultRecipeRepository<br/>+ ingredient_overrides param"]
    end

    subgraph L1 ["LAYER 1 — RECIPE LOGIC"]
        direction LR
        Api["● _api.py<br/>━━━━━━━━━━<br/>load_and_validate<br/>★ _build_active_recipe<br/>★ _drop_sub_recipe_step<br/>★ _merge_sub_recipe"]
        RulesRecipe["★ rules_recipe.py<br/>━━━━━━━━━━<br/>unknown-sub-recipe rule<br/>circular-sub-recipe rule"]
    end

    subgraph L0 ["LAYER 0 — RECIPE SCHEMA / IO"]
        direction LR
        Schema["● schema.py<br/>━━━━━━━━━━<br/>RecipeIngredient.hidden<br/>RecipeStep.sub_recipe<br/>RecipeStep.gate"]
        Io["● io.py<br/>━━━━━━━━━━<br/>★ find_sub_recipe_by_name<br/>★ builtin_sub_recipes_dir"]
        Analysis["● _analysis.py<br/>━━━━━━━━━━<br/>ValidationContext<br/>+ available_sub_recipes"]
        Validator["● validator.py<br/>━━━━━━━━━━<br/>sub_recipe discriminator<br/>gate validation rules"]
        Registry["registry.py<br/>━━━━━━━━━━<br/>semantic_rule decorator<br/>RuleFinding"]
    end

    subgraph Core ["CORE (L0 Foundation)"]
        direction LR
        Types["● core/types.py<br/>━━━━━━━━━━<br/>Severity enum<br/>shared type constants"]
    end

    subgraph Data ["DATA (Recipe YAML Files)"]
        direction LR
        SprintPrefixYaml["★ sub-recipes/sprint-prefix.yaml<br/>━━━━━━━━━━<br/>sprint workflow definition<br/>triage → plan → confirm → dispatch"]
        ImplYaml["● implementation.yaml<br/>━━━━━━━━━━<br/>+ sprint_mode ingredient (hidden)<br/>+ sprint_entry sub_recipe step"]
        RemYaml["● remediation.yaml<br/>━━━━━━━━━━<br/>+ sprint_mode ingredient (hidden)<br/>+ sprint_entry sub_recipe step"]
    end

    subgraph Skills ["SKILLS"]
        direction LR
        SprintPlanner["★ sprint-planner/SKILL.md<br/>━━━━━━━━━━<br/>sprint planning skill<br/>used by sprint-prefix dispatch"]
    end

    %% VALID DEPENDENCIES (Downward) %%
    ToolsRecipe -->|"ingredient_overrides"| Repository
    ToolsKitchen -->|"ingredient_overrides"| Repository
    Repository -->|"delegates"| Api
    Api -->|"parses schema"| Schema
    Api -->|"sub-recipe lookup"| Io
    Api -->|"builds context"| Analysis
    Api -->|"validates"| Validator
    RulesRecipe -->|"imports ValidationContext"| Analysis
    RulesRecipe -->|"registers rule"| Registry
    RulesRecipe -->|"imports load_recipe"| Io
    Analysis -->|"imports"| Schema
    Validator -->|"imports"| Schema
    Io -->|"reads"| SprintPrefixYaml
    Io -->|"reads"| ImplYaml
    Io -->|"reads"| RemYaml
    Registry -->|"imports Severity"| Types
    RulesRecipe -->|"imports Severity"| Types

    %% CLASS ASSIGNMENTS %%
    class ToolsRecipe,ToolsKitchen cli;
    class Repository phase;
    class Api handler;
    class RulesRecipe newComponent;
    class Schema,Io,Analysis,Validator,Registry stateNode;
    class Types output;
    class SprintPrefixYaml newComponent;
    class ImplYaml,RemYaml handler;
    class SprintPlanner newComponent;
```

### State Lifecycle Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;

    subgraph SchemaFields ["● INIT_ONLY SCHEMA FIELDS (set at parse, never mutated)"]
        direction LR
        HiddenField["● RecipeIngredient.hidden<br/>━━━━━━━━━━<br/>bool = False<br/>suppresses display + prompting<br/>accepted if explicitly passed"]
        SubRecipeField["● RecipeStep.sub_recipe<br/>━━━━━━━━━━<br/>str | None = None<br/>references sub-recipe by name<br/>new step discriminator"]
        GateField["● RecipeStep.gate<br/>━━━━━━━━━━<br/>str | None = None<br/>ingredient name controlling activation<br/>required if sub_recipe is set"]
    end

    subgraph ContextFields ["● INIT_ONLY CONTEXT FIELDS"]
        direction LR
        AvailSubRecipes["● ValidationContext.available_sub_recipes<br/>━━━━━━━━━━<br/>frozenset[str]<br/>populated from builtin + project dirs<br/>immutable after construction"]
    end

    subgraph MutableInputs ["MUTABLE CALL-TIME INPUTS"]
        direction LR
        Overrides["ingredient_overrides<br/>━━━━━━━━━━<br/>dict[str, str] | None<br/>caller-supplied gate values<br/>e.g. {sprint_mode: 'true'}"]
    end

    subgraph StructuralGates ["● GATE 1: STRUCTURAL VALIDATION (validator.py, extended)"]
        direction TB
        GateDiscriminator["● sub_recipe discriminator check<br/>━━━━━━━━━━<br/>FAIL if sub_recipe AND any of<br/>tool / action / python / constant"]
        GateRequiresGate["● gate field required<br/>━━━━━━━━━━<br/>FAIL if sub_recipe set but gate is None<br/>error: 'must have gate field'"]
        GateKnownIngredient["● gate must be known ingredient<br/>━━━━━━━━━━<br/>FAIL if gate not in ingredient_names<br/>error: 'undeclared ingredient'"]
        GateRequiresOnSuccess["● on_success required<br/>━━━━━━━━━━<br/>FAIL if sub_recipe step has no on_success<br/>error: 'must have on_success'"]
    end

    subgraph SemanticGates ["★ GATE 2: SEMANTIC VALIDATION (rules_recipe.py, new)"]
        direction TB
        UnknownSubRecipe["★ unknown-sub-recipe rule<br/>━━━━━━━━━━<br/>FAIL if sub_recipe name not in<br/>available_sub_recipes (when non-empty)<br/>fail-open if registry unavailable"]
        CircularSubRecipe["★ circular-sub-recipe rule<br/>━━━━━━━━━━<br/>FAIL if sub_recipe chain forms cycle<br/>DFS traversal of reference graph<br/>graceful skip for missing files"]
    end

    subgraph GateEvaluation ["★ GATE 3: RUNTIME GATE EVALUATION (_build_active_recipe)"]
        direction TB
        GateEval["★ evaluate gate ingredient<br/>━━━━━━━━━━<br/>gate_value = overrides.get(gate_name, ingredient.default)<br/>resolution: overrides win > default > 'false'"]
        GateResult{"gate_value<br/>in ('true','1','yes')?"}
        ActiveRecipe["★ active_recipe (DERIVED)<br/>━━━━━━━━━━<br/>gate=false: recipe with placeholder dropped<br/>gate=true: recipe with sub-recipe merged<br/>immutable dataclass, not stored in cache<br/>until full pipeline completes"]
    end

    subgraph HiddenContract ["★ HIDDEN INGREDIENT CONTRACT"]
        direction TB
        HiddenFilter["★ format_ingredients_table filter<br/>━━━━━━━━━━<br/>hidden=True → excluded from display<br/>never prompted, never shown to agent<br/>accepted if caller passes explicitly"]
        HiddenCacheKey["● cache key includes overrides<br/>━━━━━━━━━━<br/>cache_key = (name, pdir, suppressed, overrides_tuple)<br/>prevents cache poisoning across calls<br/>with different sprint_mode values"]
    end

    %% FLOW %%
    HiddenField --> GateDiscriminator
    SubRecipeField --> GateDiscriminator
    GateField --> GateDiscriminator
    GateDiscriminator --> GateRequiresGate
    GateRequiresGate --> GateKnownIngredient
    GateKnownIngredient --> GateRequiresOnSuccess
    GateRequiresOnSuccess --> UnknownSubRecipe

    AvailSubRecipes --> UnknownSubRecipe
    UnknownSubRecipe --> CircularSubRecipe

    Overrides --> GateEval
    CircularSubRecipe --> GateEval
    GateEval --> GateResult
    GateResult -->|"false (default)"| ActiveRecipe
    GateResult -->|"true"| ActiveRecipe

    ActiveRecipe --> HiddenFilter
    ActiveRecipe --> HiddenCacheKey

    %% CLASS ASSIGNMENTS %%
    class HiddenField,SubRecipeField,GateField detector;
    class AvailSubRecipes gap;
    class Overrides phase;
    class GateDiscriminator,GateRequiresGate,GateKnownIngredient,GateRequiresOnSuccess stateNode;
    class UnknownSubRecipe,CircularSubRecipe,GateEval newComponent;
    class GateResult stateNode;
    class ActiveRecipe output;
    class HiddenFilter,HiddenCacheKey newComponent;
```

Closes #303

## Implementation Plan

Plan files:
-
`/home/talon/projects/autoskillit-runs/impl-303-20260314-171148-945007/temp/make-plan/sub_recipe_composition_plan_2026-03-14_120000_part_a.md`
-
`/home/talon/projects/autoskillit-runs/impl-303-20260314-171148-945007/temp/make-plan/sub_recipe_composition_plan_2026-03-14_120000_part_b.md`

## Token Usage Summary

No token data collected

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…390)

## Summary

Add an `auto_merge` boolean-string ingredient (default `"true"`) to
`implementation.yaml` and `remediation.yaml`. When set to `"false"`, the
`route_queue_mode` step in each recipe routes directly to
`confirm_cleanup`, bypassing merge-queue enrollment
(`enable_auto_merge`, `wait_for_queue`, and the ejection-recovery loop)
entirely. The ingredient is evaluated as the first condition inside
`route_queue_mode`, before the existing `queue_available` check. No
changes are required to the `open-pr` skill itself — all routing logic
lives in the recipes.

## Requirements

### INGR — Ingredient Definition

- **REQ-INGR-001:** Both `implementation.yaml` and `remediation.yaml`
must declare an `auto_merge` ingredient of type string with default
`"true"`.
- **REQ-INGR-002:** The `auto_merge` ingredient must accept values
`"true"` and `"false"`.

### ROUTE — Recipe Routing

- **REQ-ROUTE-001:** When `auto_merge` is `"true"` (default), the recipe
must preserve current behavior — checking for a merge queue and
enrolling the PR via `gh pr merge --squash --auto`.
- **REQ-ROUTE-002:** When `auto_merge` is `"false"`, the
`route_queue_mode` step must skip directly to `confirm_cleanup`,
bypassing `enable_auto_merge` and `wait_for_queue` entirely.
- **REQ-ROUTE-003:** The routing decision must evaluate `auto_merge`
before checking `context.queue_available`, so that `auto_merge=false`
short-circuits without making the GraphQL query or can skip after it.

### VALID — Validation

- **REQ-VALID-001:** `validate_recipe` must accept the new `auto_merge`
ingredient without errors on both affected recipes.
- **REQ-VALID-002:** Existing recipe tests must continue to pass with
the default `auto_merge=true` behavior unchanged.

## Architecture Impact

### Process Flow Diagram

```mermaid
%%{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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    START([PR Created])
    DONE([Cleanup / Done])

    subgraph CI_Wait ["CI Monitoring"]
        ci_watch["ci_watch<br/>━━━━━━━━━━<br/>wait_for_ci MCP tool"]
        resolve_ci["resolve_ci<br/>━━━━━━━━━━<br/>resolve-failures skill<br/>retries: 2"]
    end

    subgraph Queue_Detection ["Queue Detection"]
        check_merge_queue["check_merge_queue<br/>━━━━━━━━━━<br/>gh api graphql<br/>captures: queue_available"]
    end

    subgraph Routing ["● Route Decision (modified)"]
        route_queue_mode{"● route_queue_mode<br/>━━━━━━━━━━<br/>1. auto_merge == false?<br/>2. queue_available == true?"}
    end

    subgraph Queue_Enroll ["Merge Queue Enrollment"]
        enable_auto_merge["enable_auto_merge<br/>━━━━━━━━━━<br/>gh pr merge --squash --auto"]
        wait_for_queue["wait_for_queue<br/>━━━━━━━━━━<br/>wait_for_merge_queue MCP<br/>timeout: 900s"]
        queue_ejected_fix["queue_ejected_fix<br/>━━━━━━━━━━<br/>resolve-merge-conflicts<br/>retries: 1"]
        re_push["re_push_queue_fix<br/>━━━━━━━━━━<br/>push_to_remote"]
        reenter["reenter_merge_queue<br/>━━━━━━━━━━<br/>gh pr merge --squash --auto"]
    end

    START --> ci_watch
    ci_watch -->|"CI failed"| resolve_ci
    resolve_ci --> ci_watch
    ci_watch -->|"passed"| check_merge_queue
    check_merge_queue -->|"success"| route_queue_mode
    check_merge_queue -->|"failure"| DONE

    route_queue_mode -->|"● auto_merge == false"| DONE
    route_queue_mode -->|"queue_available == true"| enable_auto_merge
    route_queue_mode -->|"no queue"| DONE

    enable_auto_merge -->|"enrolled"| wait_for_queue
    enable_auto_merge -->|"failure"| DONE
    wait_for_queue -->|"merged"| DONE
    wait_for_queue -->|"ejected"| queue_ejected_fix
    wait_for_queue -->|"timeout"| DONE
    queue_ejected_fix --> re_push
    re_push --> reenter
    reenter --> wait_for_queue

    class START,DONE terminal;
    class ci_watch,resolve_ci handler;
    class check_merge_queue phase;
    class route_queue_mode stateNode;
    class enable_auto_merge,wait_for_queue,queue_ejected_fix,re_push,reenter handler;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal | PR created / cleanup done states |
| Purple | Phase | Queue detection step |
| Orange | Handler | Execution steps (CI watch, queue ops) |
| Teal | State | ● Modified route decision node |

Closes #381

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/impl-381-20260314-205351-889011/temp/make-plan/auto_merge_ingredient_plan_2026-03-14_210000.md`

## Token Usage Summary

No token data collected.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary

`resolve-merge-conflicts` accumulates per-file conflict decisions
(category, confidence, strategy, justification) during its resolution
loop and writes a structured Markdown report to
`temp/resolve-merge-conflicts/` after every successful resolution. A new
`conflict_report_path` output token is declared in
`skill_contracts.yaml` and documented in the skill. The `merge-prs.yaml`
recipe captures this token from both `resolve_ejected_conflicts` and
`resolve_integration_conflicts` steps using `capture_list`, accumulating
paths in `context.all_conflict_report_paths`, which is then passed to
`open-integration-pr`, `open-pr`, and `audit-impl`. Those three skills
are updated to accept and embed/cross-reference the report:
`open-integration-pr` and `open-pr` embed a "Conflict Resolution
Decisions" section in their PR bodies; `audit-impl`'s Step 2.5 gains a
cross-reference check that flags decisions inconsistent with the
original plan.

## Requirements

### Report Generation (RPT)

- **REQ-RPT-001:** The `resolve-merge-conflicts` skill must write a
structured markdown report file to `temp/` after every successful
conflict resolution, containing a per-file table with columns: file
path, conflict category (1–3), confidence level (HIGH/MEDIUM),
resolution strategy (ours/theirs/combined), and a one-sentence
justification.
- **REQ-RPT-002:** The report must include a summary header with:
worktree path, base branch, number of conflicting files, number
resolved, and timestamp.
- **REQ-RPT-003:** The report must be machine-parseable — each per-file
entry must use a consistent markdown table or structured section format
that downstream tools can extract programmatically.

### Output Contract (CTR)

- **REQ-CTR-001:** The `resolve-merge-conflicts` output contract in
`skill_contracts.yaml` must add a `conflict_report_path` token emitted
on successful resolution.
- **REQ-CTR-002:** The skill's SKILL.md output section must document the
new `conflict_report_path` token alongside the existing `worktree_path`
and `branch_name` tokens.

### Pipeline Integration (PIP)

- **REQ-PIP-001:** The `pr-merge-pipeline` recipe must capture the
`conflict_report_path` output token and propagate it to downstream steps
that create PRs.
- **REQ-PIP-002:** The `create-review-pr` skill must accept an optional
conflict report path and, when provided, embed a "Conflict Resolution
Decisions" section in the integration PR body containing the per-file
decision table.
- **REQ-PIP-003:** The `open-pr` skill must accept an optional conflict
report path and, when provided, embed the same "Conflict Resolution
Decisions" section in the PR body.

### Audit Integration (AUD)

- **REQ-AUD-001:** The `audit-impl` skill must be able to
cross-reference the conflict resolution report against the original
conflict plan to verify that resolution decisions are consistent with
the stated intent of each side.

## Architecture Impact

### Data Lineage Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
flowchart LR
    %% CLASS DEFINITIONS %%
    classDef cli 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 output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;

    subgraph Origins ["Data Origins"]
        direction TB
        GIT_CONFLICTS["Git Rebase Conflicts<br/>━━━━━━━━━━<br/>Conflicting hunks per file<br/>during merge-prs pipeline"]
        PLAN_INTENT["plan_path argument<br/>━━━━━━━━━━<br/>Implementation intent<br/>for justification context"]
    end

    subgraph Creation ["● Report Creation (resolve-merge-conflicts)"]
        direction TB
        RESOLVE["● resolve-merge-conflicts<br/>━━━━━━━━━━<br/>Step 4: accumulate resolution_log<br/>Step 6: write report file<br/>Step 7: emit conflict_report_path="]
        REPORT[("conflict_resolution_report_*.md<br/>━━━━━━━━━━<br/>temp/resolve-merge-conflicts/<br/>│ File │ Cat │ Conf │ Strategy │ Why │<br/>Written only when conflicts exist")]
    end

    subgraph Pipeline ["● Pipeline Accumulation (merge-prs.yaml)"]
        direction TB
        EJECTED["● resolve_ejected_conflicts step<br/>━━━━━━━━━━<br/>capture_list:<br/>all_conflict_report_paths"]
        INTEGRATION["● resolve_integration_conflicts step<br/>━━━━━━━━━━<br/>capture_list:<br/>all_conflict_report_paths"]
        CTX[("context.all_conflict_report_paths<br/>━━━━━━━━━━<br/>comma-separated paths<br/>empty string when no conflicts")]
    end

    subgraph Consumers ["● Modified Consumers"]
        direction TB
        OPEN_INT["● open-integration-pr<br/>━━━━━━━━━━<br/>5th arg: conflict_report_paths<br/>reads Per-File Resolution Decisions<br/>embeds in PR body"]
        OPEN_PR["● open-pr<br/>━━━━━━━━━━<br/>6th arg: conflict_report_path<br/>Step 2c: load report<br/>embeds in PR body"]
        AUDIT["● audit-impl<br/>━━━━━━━━━━<br/>4th arg: conflict_report_paths<br/>Step 2.5: cross-reference<br/>vs plan intent"]
    end

    subgraph Outputs ["Output Artifacts"]
        direction TB
        PR_SECTION["## Conflict Resolution Decisions<br/>━━━━━━━━━━<br/>Per-file table in GitHub PR body<br/>visible to reviewers"]
        AUDIT_FLAG["Audit Findings<br/>━━━━━━━━━━<br/>CONFLICT / MISSING tags<br/>forces NO GO verdict"]
    end

    GIT_CONFLICTS -->|"per-file decisions"| RESOLVE
    PLAN_INTENT -->|"intent context"| RESOLVE
    RESOLVE -->|"write"| REPORT
    REPORT -->|"conflict_report_path="| EJECTED
    REPORT -->|"conflict_report_path="| INTEGRATION
    EJECTED -->|"append"| CTX
    INTEGRATION -->|"append"| CTX
    CTX -->|"all_conflict_report_paths (5th arg)"| OPEN_INT
    CTX -.->|"all_conflict_report_paths → open-pr gets singular path"| OPEN_PR
    CTX -->|"all_conflict_report_paths (4th arg)"| AUDIT
    OPEN_INT -->|"embed section"| PR_SECTION
    OPEN_PR -.->|"embed section (when provided)"| PR_SECTION
    AUDIT -->|"flag inconsistency"| AUDIT_FLAG

    class GIT_CONFLICTS,PLAN_INTENT cli;
    class RESOLVE handler;
    class REPORT,CTX stateNode;
    class EJECTED,INTEGRATION phase;
    class OPEN_INT,OPEN_PR,AUDIT integration;
    class PR_SECTION,AUDIT_FLAG output;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Input | Data origins (git conflicts, plan file) |
| Orange | Handler | `● resolve-merge-conflicts` skill (report creator)
|
| Teal | Storage | Report file on disk;
`context.all_conflict_report_paths` variable |
| Purple | Phase | `● merge-prs.yaml` capture steps (accumulation) |
| Red | Consumers | `● open-integration-pr`, `● open-pr`, `● audit-impl`
|
| Dark Teal | Output | PR body section; audit findings |

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 55, '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;

    START([START — merge-prs pipeline])

    subgraph QueueMode ["Queue Mode: Ejected PR Handling"]
        direction TB
        WAIT_Q["wait_queue_pr<br/>━━━━━━━━━━<br/>Poll GitHub merge queue"]
        Q_EJECT{"ejected?"}
        CHECKOUT["checkout_ejected_pr<br/>━━━━━━━━━━<br/>Prepare worktree"]
        RESOLVE_EJ["● resolve_ejected_conflicts<br/>━━━━━━━━━━<br/>run resolve-merge-conflicts<br/>capture_list: all_conflict_report_paths"]
        EJ_ESC{"escalation_required?"}
        PUSH_FIX["push_ejected_fix → reenter_queue"]
    end

    subgraph ClassicMode ["Classic Mode: Post-Merge Integration"]
        direction TB
        MERGE_ALL["merge_pr (all PRs)<br/>━━━━━━━━━━<br/>Squash-merge each PR<br/>into integration branch"]
        PUSH_INT["push_integration_branch"]
    end

    subgraph PostMerge ["Shared Post-Merge Phase"]
        direction TB
        COLLECT["collect_artifacts<br/>━━━━━━━━━━<br/>Gather all_plan_paths"]
        CHECK_PLANS{"plans > 0?"}
        AUDIT["● audit_impl<br/>━━━━━━━━━━<br/>arg 4: all_conflict_report_paths<br/>Step 2.5: cross-reference vs plan"]
        AUDIT_VERDICT{"verdict?"}
        OPEN_INT["● open_integration_pr<br/>━━━━━━━━━━<br/>arg 5: all_conflict_report_paths<br/>embeds Conflict Resolution Decisions"]
    end

    subgraph MergeCheck ["Mergeability Gate"]
        direction TB
        WAIT_MERGE["wait_for_review_pr_mergeability"]
        CHECK_MERGE{"MERGEABLE?"}
        RESOLVE_INT["● resolve_integration_conflicts<br/>━━━━━━━━━━<br/>run resolve-merge-conflicts<br/>capture_list: all_conflict_report_paths"]
        INT_ESC{"escalation_required?"}
        FORCE_PUSH["force_push_after_rebase"]
        CHECK_POST{"post-rebase MERGEABLE?"}
        REVIEW["review_pr_integration → CI watch → done"]
    end

    ESCALATE([ESCALATE — human intervention])
    DONE([DONE — pipeline complete])

    START -->|"queue_mode=true"| WAIT_Q
    START -->|"queue_mode=false"| MERGE_ALL

    WAIT_Q --> Q_EJECT
    Q_EJECT -->|"merged"| PUSH_INT
    Q_EJECT -->|"ejected"| CHECKOUT
    CHECKOUT --> RESOLVE_EJ
    RESOLVE_EJ -->|"appends path → context.all_conflict_report_paths"| EJ_ESC
    EJ_ESC -->|"false"| PUSH_FIX
    PUSH_FIX -->|"loop: next ejected PR"| WAIT_Q
    EJ_ESC -->|"true"| ESCALATE

    MERGE_ALL --> PUSH_INT
    PUSH_INT --> COLLECT

    COLLECT --> CHECK_PLANS
    CHECK_PLANS -->|"0 plans (skip audit)"| OPEN_INT
    CHECK_PLANS -->|">0 plans AND audit=true"| AUDIT
    AUDIT -->|"appends all_conflict_report_paths"| AUDIT_VERDICT
    AUDIT_VERDICT -->|"GO"| OPEN_INT
    AUDIT_VERDICT -->|"NO GO"| ESCALATE

    OPEN_INT -->|"embeds conflict resolution section when paths non-empty"| WAIT_MERGE
    WAIT_MERGE --> CHECK_MERGE
    CHECK_MERGE -->|"MERGEABLE"| REVIEW
    CHECK_MERGE -->|"CONFLICTING"| RESOLVE_INT
    RESOLVE_INT -->|"appends path → context.all_conflict_report_paths"| INT_ESC
    INT_ESC -->|"false"| FORCE_PUSH
    INT_ESC -->|"true"| ESCALATE
    FORCE_PUSH --> CHECK_POST
    CHECK_POST -->|"MERGEABLE"| REVIEW
    CHECK_POST -->|"other"| ESCALATE
    REVIEW --> DONE

    %% CLASS ASSIGNMENTS %%
    class START,DONE,ESCALATE terminal;
    class WAIT_Q,MERGE_ALL,PUSH_INT,COLLECT,PUSH_FIX,CHECKOUT,FORCE_PUSH handler;
    class RESOLVE_EJ,RESOLVE_INT,AUDIT,OPEN_INT phase;
    class Q_EJECT,EJ_ESC,CHECK_PLANS,AUDIT_VERDICT,CHECK_MERGE,INT_ESC,CHECK_POST stateNode;
    class WAIT_MERGE,REVIEW detector;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal | START, DONE, ESCALATE states |
| Orange | Handler | Pipeline utility steps (merge, push, collect) |
| Purple | Phase | `●` Modified steps that interact with
`conflict_report_path` |
| Teal | State | Decision / routing points |
| Red | Detector | CI and mergeability gate steps |

### State Lifecycle Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 65, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;

    subgraph ContractDecl ["INIT_ONLY — ● skill_contracts.yaml Contract Declaration"]
        direction TB
        CONTRACT["● skill_contracts.yaml<br/>━━━━━━━━━━<br/>resolve-merge-conflicts outputs:<br/>  • conflict_report_path: file_path<br/>INIT_ONLY: declared once, never modified"]
        PATTERN["expected_output_patterns<br/>━━━━━━━━━━<br/>worktree_path only (required)<br/>conflict_report_path: optional (no pattern enforcement)"]
    end

    subgraph EmissionGate ["CONDITIONAL — ● resolve-merge-conflicts Emission Gate"]
        direction TB
        RESOLVE_LOG{"resolution_log<br/>━━━━━━━━━━<br/>non-empty?"}
        WRITE_REPORT["● resolve-merge-conflicts<br/>━━━━━━━━━━<br/>Step 6: write report file<br/>conflict_resolution_report_{ts}.md"]
        EMIT["Step 7: emit token<br/>━━━━━━━━━━<br/>conflict_report_path={path}<br/>CONDITIONAL: omit line if clean rebase"]
        SKIP["(token line omitted)<br/>━━━━━━━━━━<br/>worktree_path + branch_name only"]
    end

    subgraph Accumulation ["APPEND_ONLY — ● merge-prs.yaml Context Accumulation"]
        direction TB
        CAPTURE_EJ["● resolve_ejected_conflicts<br/>━━━━━━━━━━<br/>capture_list: all_conflict_report_paths<br/>APPEND: each ejected-PR cycle appends"]
        CAPTURE_INT["● resolve_integration_conflicts<br/>━━━━━━━━━━<br/>capture_list: all_conflict_report_paths<br/>APPEND: integration conflict appends"]
        CTX_VAR["context.all_conflict_report_paths<br/>━━━━━━━━━━<br/>APPEND_ONLY field<br/>empty string when no conflicts occurred<br/>never overwritten — only grows"]
    end

    subgraph EnforcementChain ["VALIDATION GATES — ● test_conflict_resolution_guards.py"]
        direction TB
        L1["Layer 1: SKILL.md prose contract<br/>━━━━━━━━━━<br/>• emits conflict_report_path token<br/>• writes conflict_resolution_report_*<br/>• documents all 4 table columns<br/>• documents summary header counts"]
        L2["● Layer 2: YAML contract sync<br/>━━━━━━━━━━<br/>skill_contracts.yaml declares<br/>conflict_report_path in outputs<br/>prevents SKILL.md/YAML drift"]
        L3["● Layer 3: Pipeline capture wiring<br/>━━━━━━━━━━<br/>merge-prs.yaml capture_list block<br/>threads token to downstream steps"]
        L4["● Layer 4: Consumer verification<br/>━━━━━━━━━━<br/>open-integration-pr, open-pr,<br/>audit-impl SKILL.md document<br/>embedding/cross-referencing report"]
    end

    subgraph Consumers ["READ — ● Consumer Skills"]
        direction TB
        AUDIT_READ["● audit-impl<br/>━━━━━━━━━━<br/>READ all_conflict_report_paths<br/>Step 2.5 cross-reference<br/>vs plan intent → CONFLICT/MISSING"]
        OPEN_INT_READ["● open-integration-pr<br/>━━━━━━━━━━<br/>READ all_conflict_report_paths<br/>Step 4b: parse Per-File table<br/>embed Conflict Resolution Decisions"]
        OPEN_PR_READ["● open-pr<br/>━━━━━━━━━━<br/>READ conflict_report_path (singular)<br/>Step 2c: load report<br/>embed Conflict Resolution Decisions"]
    end

    CONTRACT --> PATTERN
    PATTERN -->|"contract gates emission"| RESOLVE_LOG
    RESOLVE_LOG -->|"non-empty (conflicts existed)"| WRITE_REPORT
    RESOLVE_LOG -->|"empty (clean rebase)"| SKIP
    WRITE_REPORT --> EMIT
    EMIT -->|"conflict_report_path= token"| CAPTURE_EJ
    EMIT -->|"conflict_report_path= token"| CAPTURE_INT
    CAPTURE_EJ -->|"append"| CTX_VAR
    CAPTURE_INT -->|"append"| CTX_VAR
    CTX_VAR -->|"4th arg"| AUDIT_READ
    CTX_VAR -->|"5th arg"| OPEN_INT_READ
    CTX_VAR -.->|"optional 6th arg (singular path)"| OPEN_PR_READ

    L1 --> L2 --> L3 --> L4
    L2 -.->|"validates"| CONTRACT
    L3 -.->|"validates"| CAPTURE_EJ
    L4 -.->|"validates"| OPEN_INT_READ

    class CONTRACT,PATTERN detector;
    class RESOLVE_LOG stateNode;
    class WRITE_REPORT,EMIT handler;
    class SKIP gap;
    class CAPTURE_EJ,CAPTURE_INT phase;
    class CTX_VAR handler;
    class L1,L2,L3,L4 cli;
    class AUDIT_READ,OPEN_INT_READ,OPEN_PR_READ output;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Red | INIT_ONLY | Contract declarations (never modified per run) |
| Teal | Conditional gate | Decision point — emit only when conflicts
exist |
| Orange | Emission / Accumulation | `resolve-merge-conflicts` write
step; `context.all_conflict_report_paths` APPEND_ONLY field |
| Purple | APPEND_ONLY | `capture_list` accumulation steps |
| Yellow | Skip path | Clean rebase — token line omitted entirely |
| Dark Blue | Enforcement | Four-layer validation chain in test suite |
| Dark Teal | Consumer READ | `● audit-impl`, `● open-integration-pr`,
`● open-pr` |

Closes #352

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/impl-352-20260314-205352-433219/temp/make-plan/merge_conflict_decision_report_plan_2026-03-14_210900.md`

## Token Usage Summary

No token data collected.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…391)

## Summary

The orchestrator's global rules (`sous-chef/SKILL.md`, injected into
every cook session)
had no guidance for handling multiple issues at once. The orchestrator
was incorrectly
inferring a single-issue constraint from the recipe structure, then
offering irrelevant
alternatives ("use implementation-groups", "pick one to start") instead
of following a
simple decision flow.

**Fix:** Added a `MULTIPLE ISSUES — MANDATORY` section to
`sous-chef/SKILL.md` that
specifies exactly:
- User says "parallel" → launch N instances in parallel immediately
- User says "sequential" → run one at a time
- User says nothing → ask exactly one question: "sequential or
parallel?"

No code changes. No new Python modules. One file edit + two new tests.

## Architecture Impact

### Process Flow Diagram

```mermaid
%%{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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    %% TERMINALS %%
    START([START: User provides N issues])
    PAR_END([COMPLETE: N parallel pipelines])
    SEQ_END([COMPLETE: Sequential execution])
    SINGLE_END([COMPLETE: Single-issue pipeline])

    %% SESSION INITIATION %%
    subgraph Init ["Session Initiation"]
        direction TB
        OpenKitchen["open_kitchen<br/>━━━━━━━━━━<br/>Load recipe, display<br/>ingredients table"]
        CollectIng["Collect ingredients<br/>━━━━━━━━━━<br/>Gather required fields<br/>from user conversationally"]
    end

    %% MULTI-ISSUE GATE (MODIFIED) %%
    subgraph Gate ["● sous-chef/SKILL.md — MULTIPLE ISSUES GATE (modified)"]
        direction TB
        CountCheck{"N issues?<br/>━━━━━━━━━━<br/>Count issues in<br/>user request"}
        ModeCheck{"● Mode specified?<br/>━━━━━━━━━━<br/>Did user say parallel<br/>or sequential?"}
        AskUser["● AskUserQuestion<br/>━━━━━━━━━━<br/>Exactly one question:<br/>Sequential or parallel?<br/>(two options ONLY)"]
        UserAnswer{"● User answers"}
    end

    %% EXECUTION BRANCHES %%
    subgraph Parallel ["Parallel Execution"]
        LaunchN["Launch N sessions<br/>━━━━━━━━━━<br/>Independent clones,<br/>branches, PRs per issue"]
    end

    subgraph Sequential ["Sequential Execution"]
        RunOneByOne["Run one at a time<br/>━━━━━━━━━━<br/>Issue 1 → done →<br/>Issue 2 → done…"]
    end

    SingleIssue["Single-issue pipeline<br/>━━━━━━━━━━<br/>Normal recipe flow"]

    %% CONNECTIONS %%
    START --> OpenKitchen
    OpenKitchen --> CollectIng
    CollectIng --> CountCheck

    CountCheck -->|"N = 1"| SingleIssue
    CountCheck -->|"N > 1"| ModeCheck

    ModeCheck -->|"said parallel"| LaunchN
    ModeCheck -->|"said sequential"| RunOneByOne
    ModeCheck -->|"not specified"| AskUser
    AskUser --> UserAnswer
    UserAnswer -->|"parallel"| LaunchN
    UserAnswer -->|"sequential"| RunOneByOne

    LaunchN --> PAR_END
    RunOneByOne --> SEQ_END
    SingleIssue --> SINGLE_END

    %% CLASS ASSIGNMENTS %%
    class START,PAR_END,SEQ_END,SINGLE_END terminal;
    class OpenKitchen,CollectIng phase;
    class CountCheck,UserAnswer stateNode;
    class ModeCheck,AskUser newComponent;
    class LaunchN,RunOneByOne,SingleIssue handler;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal | Start and completion states |
| Purple | Phase | Session initiation and ingredient collection |
| Teal | State | Issue count check and user answer routing |
| Green | Modified | ● Rule added to sous-chef/SKILL.md — mode check and
AskUserQuestion |
| Orange | Handler | Pipeline execution branches |

### Scenarios Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, 'curve': 'basis'}}}%%
flowchart LR
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    subgraph SC1 ["SCENARIO 1: User says 'run these in parallel'"]
        direction LR
        SC1_User["User: N issues<br/>+ parallel"]
        SC1_Rule["● sous-chef SKILL.md<br/>MULTIPLE ISSUES rule<br/>reads: 'parallel' signal"]
        SC1_Launch["N independent<br/>pipeline sessions<br/>launched immediately"]
        SC1_Out["N branches,<br/>N PRs created"]
    end

    subgraph SC2 ["SCENARIO 2: User says 'run these sequentially'"]
        direction LR
        SC2_User["User: N issues<br/>+ sequential"]
        SC2_Rule["● sous-chef SKILL.md<br/>MULTIPLE ISSUES rule<br/>reads: 'sequential' signal"]
        SC2_Run["Issue 1 runs →<br/>completes → Issue 2<br/>runs → … one by one"]
        SC2_Out["N pipelines,<br/>run in order"]
    end

    subgraph SC3 ["SCENARIO 3: User gives issues, no mode stated"]
        direction LR
        SC3_User["User: N issues<br/>(no mode)"]
        SC3_Rule["● sous-chef SKILL.md<br/>MULTIPLE ISSUES rule<br/>no mode detected"]
        SC3_Ask["● AskUserQuestion<br/>Sequential or parallel?<br/>Exactly two options"]
        SC3_Answer["User answers →<br/>SC1 or SC2<br/>path taken"]
    end

    subgraph SC4 ["SCENARIO 4: Prompt injection verification (● tests/cli/test_cli_prompts.py)"]
        direction LR
        SC4_Build["_build_orchestrator_prompt<br/>loads sous-chef SKILL.md<br/>into system prompt"]
        SC4_MI1["● MI1: assert 'sequential'<br/>and 'parallel' in prompt"]
        SC4_MI2["● MI2: assert 'sequential or parallel'<br/>in prompt (two-option constraint)"]
        SC4_Pass["Both tests pass →<br/>rule is injected and<br/>correctly worded"]
    end

    %% SC1 FLOW %%
    SC1_User --> SC1_Rule --> SC1_Launch --> SC1_Out

    %% SC2 FLOW %%
    SC2_User --> SC2_Rule --> SC2_Run --> SC2_Out

    %% SC3 FLOW %%
    SC3_User --> SC3_Rule --> SC3_Ask --> SC3_Answer

    %% SC4 FLOW %%
    SC4_Build --> SC4_MI1 --> SC4_MI2 --> SC4_Pass

    %% CLASS ASSIGNMENTS %%
    class SC1_User,SC2_User,SC3_User cli;
    class SC1_Rule,SC2_Rule,SC3_Rule newComponent;
    class SC1_Launch,SC2_Run handler;
    class SC1_Out,SC2_Out,SC3_Answer output;
    class SC3_Ask newComponent;
    class SC4_Build phase;
    class SC4_MI1,SC4_MI2 newComponent;
    class SC4_Pass output;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Entry | User request entry points |
| Green | Modified | ● Rules added to sous-chef SKILL.md; ● new tests in
test_cli_prompts.py |
| Orange | Handler | Pipeline execution paths |
| Purple | Phase | Prompt builder initialization |
| Dark Teal | Output | Outcomes and validation results |

Closes #383

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/impl-383-20260314-205350-578940/temp/make-plan/orchestrator_multi_issue_plan_2026-03-14_210000.md`

## Token Usage Summary

No token data collected.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary

- Add `_inject_cwd_anchor` prompt directive to anchor all relative
`temp/` paths to the session's working directory, preventing
`set_project_path` from contaminating model path resolution
- Add `_extract_output_paths` + `_validate_output_paths` post-session
validation that detects when structured output paths (e.g. `plan_path`)
land outside the session's cwd, returning `subtype="path_contamination"`
with `needs_retry=True`
- Update 34 file-producing SKILL.md files with `(relative to the current
working directory)` language on output path instructions

## Test plan

- [x] 19 new unit tests across 5 test classes for `_inject_cwd_anchor`,
`_extract_output_paths`, `_validate_output_paths`, path contamination
detection in `_build_skill_result`, and cwd anchor injection in
`run_headless_core`
- [x] New parametrized `test_file_producing_skills_have_cwd_anchor`
enforces all 36 file-producing skills contain cwd-anchoring language
- [x] New `test_output_path_tokens_synchronized` ensures
`_OUTPUT_PATH_TOKENS` stays aligned with `UNSPACED_OUTPUT_TOKEN`
registry
- [x] Full test suite passes: 4163 passed, 16 skipped

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
#392)

## Summary

The implementation pipeline fails to manage GitHub issue state correctly
when PRs target
non-default branches (e.g., `integration`). Two bugs exist:

1. `implementation.yaml` has no `release_issue_success` step — the
`in-progress` label is
never removed on the success path, leaving issues permanently marked
"in-progress" after
   merge into `integration`.
2. GitHub only auto-closes issues via "Closes #N" when PRs merge into
the **default branch**
(`main`). PRs targeting `integration` merge successfully but leave
linked issues open with
   no indication of their actual status.

The fix introduces a three-point `staged` lifecycle:
- **Success into non-default branch** → remove `in-progress`, apply
`staged` (work is done, waiting for promotion to `main`)
- **Failure at any stage** → remove `in-progress` only (leave open for
retry)
- **Integration PR to `main`** → carry forward "Closes #N" so issues
auto-close (already handled by `open-integration-pr`)

Additionally, the `open-pr` skill now blocks PRs that target `stable`
from any branch other than `main`.

## Requirements

### STAGE — Staged Label Lifecycle

- **REQ-STAGE-001:** When a PR merges into `integration`, the pipeline
must remove the `in-progress` label and apply a `staged` label to the
linked issue.
- **REQ-STAGE-002:** The `staged` label must be created automatically
(via `gh label create --force`) if it does not already exist.
- **REQ-STAGE-003:** The issue must remain open while the `staged` label
is applied — it is only closed when the code reaches `main`.

### CLOSE — Issue Close Behavior

- **REQ-CLOSE-001:** Issues must only be closed when the associated code
merges into `main` (the default branch).
- **REQ-CLOSE-002:** The integration PR (merging `integration → main`)
must carry forward all `Closes #N` references from the constituent PRs
so GitHub auto-closes the issues.
- **REQ-CLOSE-003:** If `Closes #N` auto-close does not trigger (e.g.
due to reference format), the pipeline must explicitly close the issue
via `gh issue close` after confirming the merge to `main`.

### RELEASE — Claim Release

- **REQ-RELEASE-001:** On the success path, `release_issue` must
transition the issue from `in-progress` to `staged` when the target
branch is not `main`.
- **REQ-RELEASE-002:** On the success path targeting `main`,
`release_issue` must remove `in-progress` and allow the issue to close
normally.
- **REQ-RELEASE-003:** On the failure path, `release_issue` must remove
the `in-progress` label but must not apply `staged`, leaving the issue
open for retry.

### GUARD — Branch Enforcement

- **REQ-GUARD-001:** PRs targeting `stable` must be blocked unless the
source branch is `main`.
- **REQ-GUARD-002:** The enforcement must be implemented via GitHub
branch protection rules if possible, or via a PreToolUse hook or
recipe-level guard as a fallback.
- **REQ-GUARD-003:** The `open-pr` skill or `merge_worktree` tool must
reject attempts to target `stable` from any branch other than `main`
with a clear error message.

## Architecture Impact

### State Lifecycle Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;
    classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;

    %% TERMINAL %%
    IssueOpen(["Issue: Open<br/>━━━━━━━━━━<br/>No pipeline labels"])

    subgraph InitOnly ["INIT_ONLY FIELDS (set once)"]
        direction LR
        InputURL["inputs.issue_url<br/>━━━━━━━━━━<br/>INIT_ONLY<br/>Never modified"]
        InputBranch["inputs.base_branch<br/>━━━━━━━━━━<br/>INIT_ONLY<br/>Never modified"]
    end

    subgraph ClaimGate ["VALIDATION GATE: claim_issue"]
        direction TB
        IdempotencyCheck{"Label already<br/>in-progress?<br/>━━━━━━━━━━<br/>Idempotency Guard"}
        ClaimTool["● claim_issue MCP tool<br/>━━━━━━━━━━<br/>ensure_label: in-progress<br/>add_labels: in-progress"]
        EscalateStop["escalate_stop<br/>━━━━━━━━━━<br/>Another session owns it<br/>→ abort pipeline"]
    end

    subgraph InProgressState ["MUTABLE STATE: In-Progress"]
        InProgressLabel["Issue Label: in-progress<br/>━━━━━━━━━━<br/>MUTABLE<br/>Added by claim_issue"]
    end

    subgraph BranchGate ["★ VALIDATION GATE: Branch Check (NEW)"]
        direction TB
        BranchDerived{"target_branch<br/>━━━━━━━━━━<br/>DERIVED<br/>== default_base_branch?"}
    end

    subgraph ReleaseGate ["★ VALIDATION GATE: release_issue (EXTENDED)"]
        direction TB
        SkipCheck{"skip_when_false:<br/>inputs.issue_url<br/>━━━━━━━━━━<br/>Guard: no URL → skip"}
        ReleaseSuccess["★ release_issue_success step<br/>━━━━━━━━━━<br/>tool: release_issue<br/>with: target_branch=inputs.base_branch"]
        ReleaseFailure["release_issue_failure step<br/>━━━━━━━━━━<br/>tool: release_issue<br/>no target_branch → failure path"]
    end

    subgraph StagedConfig ["APPEND_ONLY CONFIG"]
        direction TB
        StagedLabelCfg["★ github.staged_label<br/>━━━━━━━━━━<br/>APPEND_ONLY config<br/>Default: 'staged'<br/>Color: #0075ca"]
        EnsureLabel["● release_issue MCP tool<br/>━━━━━━━━━━<br/>ensure_label (422-safe)<br/>add_labels: staged<br/>remove_label: in-progress"]
    end

    subgraph IssueStates ["FINAL ISSUE STATES"]
        direction LR
        StateStagedNew["★ Issue: open + staged<br/>━━━━━━━━━━<br/>Merged to integration<br/>Awaiting promotion to main"]
        StateOpenRetry["Issue: open<br/>━━━━━━━━━━<br/>Failure path: no staged<br/>Available for retry"]
        StateClosed["Issue: closed<br/>━━━━━━━━━━<br/>Merged to main<br/>Closes #N triggered"]
    end

    subgraph StableGuard ["★ STABLE BRANCH GUARD (NEW)"]
        StableCheck{"base_branch == stable<br/>AND feature != main?<br/>━━━━━━━━━━<br/>open-pr pre-flight"}
        GuardExit["Exit code 1<br/>━━━━━━━━━━<br/>Block PR to stable<br/>from non-main source"]
    end

    %% FLOW %%
    IssueOpen --> InitOnly
    InputURL --> IdempotencyCheck
    InputBranch --> BranchDerived

    IdempotencyCheck -->|"not claimed"| ClaimTool
    IdempotencyCheck -->|"already claimed"| EscalateStop
    ClaimTool --> InProgressLabel

    InProgressLabel --> SkipCheck
    SkipCheck -->|"issue_url present"| ReleaseSuccess
    SkipCheck -->|"no issue_url"| StateClosed

    ReleaseSuccess --> BranchDerived
    BranchDerived -->|"target == main (default)"| StateOpenRetry
    BranchDerived -->|"target != main (staging)"| StagedLabelCfg

    StagedLabelCfg --> EnsureLabel
    EnsureLabel --> StateStagedNew

    StateOpenRetry -->|"Closes #N on main merge"| StateClosed

    InProgressLabel -->|"any failure path"| ReleaseFailure
    ReleaseFailure --> StateOpenRetry

    StateStagedNew -->|"Integration PR → main"| StateClosed

    %% STABLE GUARD %%
    InputBranch --> StableCheck
    StableCheck -->|"stable + non-main source"| GuardExit
    StableCheck -->|"allowed"| ClaimTool

    %% CLASS ASSIGNMENTS %%
    class IssueOpen terminal;
    class InputURL,InputBranch detector;
    class IdempotencyCheck,SkipCheck,BranchDerived,StableCheck detector;
    class ClaimTool,ReleaseFailure phase;
    class InProgressLabel stateNode;
    class ReleaseSuccess,StagedLabelCfg,EnsureLabel,StableGuard,BranchGate,ReleaseGate newComponent;
    class StateStagedNew newComponent;
    class StateOpenRetry stateNode;
    class StateClosed output;
    class EscalateStop,GuardExit gap;
```

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;
    classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;

    PipelineStart(["Pipeline Start<br/>━━━━━━━━━━<br/>inputs: issue_url, base_branch"])

    subgraph StableGuardPhase ["★ STABLE BRANCH GUARD (open-pr/SKILL.md)"]
        direction TB
        StableCheck{"● open-pr pre-flight<br/>━━━━━━━━━━<br/>base_branch == 'stable'<br/>AND feature != 'main'?"}
        BlockedExit["Exit code 1<br/>━━━━━━━━━━<br/>Error: stable-only from main<br/>No PR created"]
    end

    subgraph PipelineCore ["PIPELINE CORE (plan → implement → test → merge)"]
        direction TB
        PipelineSteps["plan / implement / test<br/>━━━━━━━━━━<br/>Worktree creation, implementation,<br/>test_check, merge_worktree, push_to_remote"]
        FailureAny["Any step on_failure<br/>━━━━━━━━━━<br/>Routes: release_issue_failure"]
    end

    subgraph MergeQueueRouting ["MERGE QUEUE ROUTING (● implementation.yaml)"]
        direction TB
        CheckMQ["check_merge_queue step<br/>━━━━━━━━━━<br/>gh api GraphQL<br/>captures: queue_available"]
        RouteMQ{"● route_queue_mode<br/>━━━━━━━━━━<br/>queue_available == true?"}
        EnableAutoMerge["enable_auto_merge step<br/>━━━━━━━━━━<br/>Sets up auto-merge on PR"]
        WaitForQueue["wait_for_queue step<br/>━━━━━━━━━━<br/>wait_for_merge_queue tool<br/>captures: pr_state"]
        QueueResult{"● wait_for_queue<br/>━━━━━━━━━━<br/>pr_state == ?"}
        QueueEjected["queue_ejected_fix<br/>━━━━━━━━━━<br/>Resolve ejection → retry"]
    end

    subgraph ReleaseRouting ["★ RELEASE ROUTING (NEW in implementation.yaml)"]
        direction TB
        ReleaseSuccess["★ release_issue_success step<br/>━━━━━━━━━━<br/>tool: release_issue<br/>with: target_branch=inputs.base_branch<br/>optional: true"]
        SkipGuard{"skip_when_false:<br/>━━━━━━━━━━<br/>inputs.issue_url present?"}
    end

    subgraph ReleaseTool ["★ release_issue MCP TOOL LOGIC (● tools_integrations.py)"]
        direction TB
        RemoveInProgress["remove_label: in-progress<br/>━━━━━━━━━━<br/>Always called on success path"]
        BranchDecision{"★ Branch Gate<br/>━━━━━━━━━━<br/>target_branch<br/>== default_base_branch?"}
        EnsureStaged["★ ensure_label: staged<br/>━━━━━━━━━━<br/>422-safe create<br/>Color: #0075ca"]
        AddStaged["★ add_labels: staged<br/>━━━━━━━━━━<br/>Apply staged label<br/>to issue"]
        ReturnStaged["return staged=True<br/>━━━━━━━━━━<br/>staged_label in response"]
        ReturnNoStaged["return staged=False<br/>━━━━━━━━━━<br/>No staging for main target"]
    end

    subgraph FailurePath ["FAILURE PATH (release_issue_failure)"]
        direction TB
        ReleaseFailureTool["release_issue_failure step<br/>━━━━━━━━━━<br/>tool: release_issue<br/>NO target_branch → no staging"]
        CleanupFailure["cleanup_failure<br/>━━━━━━━━━━<br/>Worktree teardown"]
    end

    subgraph SuccessPath ["SUCCESS TERMINAL"]
        direction TB
        ConfirmCleanup["confirm_cleanup<br/>━━━━━━━━━━<br/>Worktree teardown"]
        IssueStaged["Issue: staged<br/>━━━━━━━━━━<br/>★ staged label applied<br/>Awaiting promotion"]
        IssueDone["Issue: released<br/>━━━━━━━━━━<br/>in-progress removed<br/>Normal close flow"]
    end

    %% FLOW %%
    PipelineStart --> StableCheck
    StableCheck -->|"blocked"| BlockedExit
    StableCheck -->|"allowed"| PipelineSteps

    PipelineSteps --> CheckMQ
    PipelineSteps --> FailureAny
    FailureAny --> ReleaseFailureTool
    ReleaseFailureTool --> CleanupFailure

    CheckMQ --> RouteMQ
    CheckMQ -->|"on_failure"| ReleaseSuccess

    RouteMQ -->|"queue == true"| EnableAutoMerge
    RouteMQ -->|"queue == false (● was confirm_cleanup)"| ReleaseSuccess

    EnableAutoMerge --> WaitForQueue
    WaitForQueue --> QueueResult

    QueueResult -->|"merged (● was confirm_cleanup)"| ReleaseSuccess
    QueueResult -->|"ejected"| QueueEjected
    QueueResult -->|"timeout/other (● was confirm_cleanup)"| ReleaseSuccess
    QueueEjected -->|"retry"| WaitForQueue

    ReleaseSuccess --> SkipGuard
    SkipGuard -->|"no issue_url"| ConfirmCleanup
    SkipGuard -->|"issue_url present"| RemoveInProgress

    RemoveInProgress --> BranchDecision
    BranchDecision -->|"target != main"| EnsureStaged
    BranchDecision -->|"target == main"| ReturnNoStaged
    EnsureStaged --> AddStaged
    AddStaged --> ReturnStaged
    ReturnStaged --> IssueStaged
    ReturnNoStaged --> IssueDone
    IssueStaged --> ConfirmCleanup
    IssueDone --> ConfirmCleanup

    %% CLASS ASSIGNMENTS %%
    class PipelineStart terminal;
    class StableCheck,BranchDecision,QueueResult,SkipGuard,RouteMQ detector;
    class BlockedExit gap;
    class PipelineSteps,FailureAny,CheckMQ,EnableAutoMerge,WaitForQueue,QueueEjected handler;
    class ReleaseSuccess,RemoveInProgress,EnsureStaged,AddStaged,ReturnStaged,IssueStaged newComponent;
    class ReleaseFailureTool,CleanupFailure,ConfirmCleanup phase;
    class ReturnNoStaged,IssueDone stateNode;
```

Closes #382

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/impl-382-20260314-205351-453631/temp/make-plan/pipeline_staged_issue_lifecycle_plan_2026-03-14_210500.md`

## Token Usage Summary

No token data collected.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary

`DefaultCIWatcher` queried the GitHub Actions runs API without a
`workflow_id` filter, causing
any unrelated workflow on the branch (version bumps, labelers,
stale-check) to satisfy the
look-back check and trigger false failure pipelines. This PR introduces
`CIRunScope` as a
frozen value object carrying `workflow` and `head_sha` scope parameters,
threads it through
the `CIWatcher` protocol and `DefaultCIWatcher` implementation, adds a
project-level `CIConfig`
so operators set the workflow name once rather than in every recipe
step, expands the
`FAILED_CONCLUSIONS` set from 1 to 4 values (adding `timed_out`,
`startup_failure`,
`cancelled`), and updates all five recipe `wait_for_ci` steps and the
`diagnose-ci` skill.

## Requirements

### CI (CI Watcher Workflow Filtering)

- **REQ-CI-001:** `DefaultCIWatcher.wait()` must accept an optional
`workflow` parameter that, when provided, is passed as `workflow_id` to
the GitHub Actions runs API query.
- **REQ-CI-002:** `DefaultCIWatcher.status()` must accept an optional
`workflow` parameter with the same behavior as REQ-CI-001.
- **REQ-CI-003:** `_fetch_completed_runs` and `_fetch_active_runs` must
include the `workflow_id` query parameter when a workflow value is
provided.
- **REQ-CI-004:** The `wait_for_ci` MCP tool handler must accept an
optional `workflow` parameter and pass it through to the CI watcher.
- **REQ-CI-005:** The `get_ci_status` MCP tool handler must accept an
optional `workflow` parameter and pass it through to the CI watcher.

### RECIPE (Recipe Integration)

- **REQ-RECIPE-001:** All recipe `ci_watch` steps must be updated to
pass a `workflow` value matching the repository's test workflow
filename.
- **REQ-RECIPE-002:** The `diagnose-ci` skill must accept an optional
workflow name argument and pass `--workflow` to `gh run list` when
provided.

### JOBS (Failed Jobs Robustness)

- **REQ-JOBS-001:** `_fetch_failed_jobs` must include jobs with
conclusions `"timed_out"`, `"startup_failure"`, and `"cancelled"` in
addition to `"failure"`.

## Architecture Impact

### Data Lineage Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%%
flowchart LR
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;

    subgraph Origins ["Data Origins"]
        YAML["● defaults.yaml<br/>━━━━━━━━━━<br/>ci.workflow: null<br/>operator default layer"]
        RECIPE["● Recipe ci_watch step<br/>━━━━━━━━━━<br/>with: workflow: tests.yml<br/>step-level override"]
        GIT["● wait_for_ci handler<br/>━━━━━━━━━━<br/>git rev-parse HEAD<br/>or caller-supplied"]
    end

    subgraph Config ["Config Layer"]
        CICONFIG["★ CIConfig<br/>━━━━━━━━━━<br/>workflow: str | None<br/>config/settings.py"]
        AUTOMCONFIG["● AutomationConfig<br/>━━━━━━━━━━<br/>.ci: CIConfig<br/>from_dynaconf()"]
        CTX["● ToolContext<br/>━━━━━━━━━━<br/>★ .default_ci_scope<br/>pipeline/context.py"]
    end

    subgraph ScopeAssembly ["★ Scope Assembly"]
        SCOPE["★ CIRunScope<br/>━━━━━━━━━━<br/>workflow: str | None<br/>head_sha: str | None<br/>core/types.py"]
    end

    subgraph Protocol ["● CIWatcher Protocol"]
        WAIT["● wait(branch,<br/>━━━━━━━━━━<br/>scope: CIRunScope)<br/>core/types.py"]
        STATUS["● status(branch,<br/>━━━━━━━━━━<br/>scope: CIRunScope)<br/>core/types.py"]
    end

    subgraph Impl ["● DefaultCIWatcher"]
        FCR["● _fetch_completed_runs<br/>━━━━━━━━━━<br/>★ +workflow_id<br/>+head_sha in params"]
        FAR["● _fetch_active_runs<br/>━━━━━━━━━━<br/>★ +workflow_id<br/>+head_sha in params"]
        FJOBS["● _fetch_failed_jobs<br/>━━━━━━━━━━<br/>★ FAILED_CONCLUSIONS<br/>4 values incl. timed_out"]
    end

    subgraph GH ["GitHub API"]
        RUNS["GET /actions/runs<br/>━━━━━━━━━━<br/>branch + workflow_id<br/>+ head_sha (scoped)"]
        JOBS["GET /runs/{id}/jobs<br/>━━━━━━━━━━<br/>all failure conclusions<br/>incl. timed_out"]
    end

    subgraph Handlers ["● MCP Handlers"]
        WFH["● wait_for_ci<br/>━━━━━━━━━━<br/>★ +workflow param<br/>merges with default"]
        GCS["● get_ci_status<br/>━━━━━━━━━━<br/>★ +workflow param<br/>scope (no head_sha)"]
    end

    YAML -->|"ci.workflow"| AUTOMCONFIG
    AUTOMCONFIG -->|"config.ci"| CICONFIG
    CICONFIG -->|"default_ci_scope"| CTX
    CTX -->|"fallback scope"| SCOPE
    RECIPE -->|"workflow: override"| WFH
    RECIPE -->|"workflow: override"| GCS
    GIT -->|"head_sha"| WFH
    WFH -->|"CIRunScope<br/>workflow+head_sha"| SCOPE
    GCS -->|"CIRunScope<br/>workflow only"| SCOPE
    SCOPE --> WAIT
    SCOPE --> STATUS
    WAIT --> FCR
    WAIT --> FAR
    STATUS --> FCR
    FCR -->|"workflow_id + head_sha"| RUNS
    FAR -->|"workflow_id + head_sha"| RUNS
    RUNS -->|"conclusion in<br/>FAILED_CONCLUSIONS"| FJOBS
    FJOBS -->|"job names list"| JOBS

    class YAML,RECIPE,GIT cli;
    class AUTOMCONFIG,CICONFIG,CTX stateNode;
    class SCOPE newComponent;
    class WAIT,STATUS phase;
    class FCR,FAR,FJOBS handler;
    class WFH,GCS handler;
    class RUNS,JOBS integration;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Origins | Config files, recipe YAML, git inference |
| Teal | Config Layer | `CIConfig`, `AutomationConfig`, `ToolContext`
state |
| Green (★) | New | `CIRunScope` — new scope value object |
| Purple | Protocol | `CIWatcher` protocol method signatures |
| Orange | Handlers/Impl | MCP handlers and `DefaultCIWatcher` helpers |
| Red | External | GitHub REST API endpoints |

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 55, 'curve': 'basis'}}}%%
flowchart TB
    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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    START([wait_for_ci / get_ci_status<br/>MCP handler called])

    subgraph ScopeAssembly ["● Scope Assembly (tools_ci.py)"]
        direction TB
        INFER["● Infer head_sha<br/>━━━━━━━━━━<br/>git rev-parse HEAD in cwd<br/>(wait_for_ci only)"]
        BUILD["★ Build CIRunScope<br/>━━━━━━━━━━<br/>workflow = arg or default_ci_scope.workflow<br/>head_sha = inferred or arg"]
    end

    RESOLVE{"● _resolve_repo<br/>━━━━━━━━━━<br/>repo determinable?"}

    NO_REPO([RETURN: conclusion=no_runs<br/>error: repo unknown])

    subgraph Phase1 ["● wait() Phase 1: Look-back"]
        direction TB
        FCR1["● _fetch_completed_runs<br/>━━━━━━━━━━<br/>★ params: workflow_id + head_sha<br/>lookback_seconds cutoff"]
        COMP1{"completed<br/>runs found?"}
    end

    subgraph Phase2 ["● wait() Phase 2: Poll Active"]
        direction TB
        FAR["● _fetch_active_runs<br/>━━━━━━━━━━<br/>★ params: workflow_id + head_sha<br/>per_page=1"]
        ACTIVE{"active run<br/>found?"}
        FCR2["● _fetch_completed_runs<br/>━━━━━━━━━━<br/>re-check while polling<br/>race condition guard"]
        COMP2{"completed<br/>during poll?"}
        SLEEP["_jittered_sleep(attempt)<br/>━━━━━━━━━━<br/>capped to remaining deadline"]
        DEADLINE1{"deadline<br/>exceeded?"}
    end

    NO_RUNS([RETURN: conclusion=no_runs<br/>failed_jobs=])

    subgraph Phase3 ["● wait() Phase 3: Wait for Completion"]
        direction TB
        POLL["_poll_run_status(run_id)<br/>━━━━━━━━━━<br/>direct ID fetch — no scope"]
        DONE{"status ==<br/>completed?"}
        DEADLINE2{"deadline<br/>exceeded?"}
    end

    TIMEOUT([RETURN: conclusion=timed_out<br/>failed_jobs=])

    subgraph FCGuard ["★ FAILED_CONCLUSIONS Guard"]
        direction TB
        FC{"● conclusion in<br/>★ FAILED_CONCLUSIONS?<br/>━━━━━━━━━━<br/>failure|timed_out|<br/>startup_failure|cancelled"}
        FETCHJOBS["● _fetch_failed_jobs<br/>━━━━━━━━━━<br/>★ FAILED_CONCLUSIONS filter<br/>GET /runs/{id}/jobs"]
    end

    SUCCESS([RETURN: conclusion + failed_jobs])
    ERROR([RETURN: conclusion=error<br/>httpx exception caught])

    START --> INFER
    INFER --> BUILD
    BUILD -->|"scope: CIRunScope"| RESOLVE
    RESOLVE -->|"no"| NO_REPO
    RESOLVE -->|"yes"| FCR1
    FCR1 --> COMP1
    COMP1 -->|"yes — early exit"| FC
    COMP1 -->|"no"| FAR
    FAR --> ACTIVE
    ACTIVE -->|"yes — break loop"| Phase3
    ACTIVE -->|"no"| FCR2
    FCR2 --> COMP2
    COMP2 -->|"yes — early exit"| FC
    COMP2 -->|"no"| SLEEP
    SLEEP --> DEADLINE1
    DEADLINE1 -->|"not yet"| FAR
    DEADLINE1 -->|"expired"| NO_RUNS
    POLL --> DONE
    DONE -->|"yes"| FC
    DONE -->|"no"| DEADLINE2
    DEADLINE2 -->|"not yet"| POLL
    DEADLINE2 -->|"expired"| TIMEOUT
    FC -->|"yes"| FETCHJOBS
    FC -->|"no"| SUCCESS
    FETCHJOBS --> SUCCESS
    BUILD -.->|"httpx error"| ERROR

    class START terminal;
    class NO_REPO,NO_RUNS,TIMEOUT,SUCCESS,ERROR terminal;
    class INFER,FCR1,FCR2,FAR,POLL,SLEEP,FETCHJOBS handler;
    class BUILD newComponent;
    class COMP1,COMP2,ACTIVE,DONE,DEADLINE1,DEADLINE2,RESOLVE stateNode;
    class FC detector;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal | Start, return, and error states |
| Green (★) | New | `CIRunScope` assembly — new scope value object |
| Orange | Handler | `_fetch_*` helpers, polling, sleep |
| Teal | Decision | Conditional routing and deadline checks |
| Red | Guard | `FAILED_CONCLUSIONS` gate (★ expanded from 1 to 4
values) |

Closes #371

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/remediation-371-r2-20260314-224923-398672/temp/rectify/rectify_ci_watcher_workflow_scope_2026-03-14_210000.md`

## Token Usage Summary

## Token Summary\n\nNo token data captured for this run.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ls (#396)

## Summary

Three dedicated MCP tools (`prepare_issue`, `enrich_issues`,
`report_bug`) called `executor.run()` without registering the structured
output blocks their skills emit as `expected_output_patterns`. This
severed them from the contract enforcement chain that already exists and
works correctly for the generic `run_skill` tool. The consequence was a
**dual-parsing architecture**: session success (`_compute_success`) and
structured output presence (`_extract_block`) were adjudicated
independently, producing contradictory results — most visibly as
`{"status": "complete", "success": false, "error": "no result block
found"}`. In addition, all three tools silently discarded `session_id`,
`stderr`, `subtype`, and `exit_code` on failure, making diagnosis
impossible.

The fix unifies all dedicated tools with `run_skill` under the same
contract enforcement chain: `skill_contracts.yaml` →
`output_pattern_resolver` → `expected_output_patterns` →
`_check_session_content Gate 5`. The block-presence check is now an
integral part of session adjudication rather than a post-hoc parse that
can contradict the adjudicated outcome.

## Architecture Impact

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 45, 'rankSpacing': 55, '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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;

    START([MCP Tool Called<br/>prepare_issue / enrich_issues / report_bug])

    subgraph Phase1 ["● Phase 1: Contract Lookup (tools_integrations.py)"]
        direction TB
        Resolver["● output_pattern_resolver<br/>━━━━━━━━━━<br/>tool_ctx.output_pattern_resolver(skill_cmd)<br/>→ resolve_skill_name() + get_skill_contract()"]
        Contracts["● skill_contracts.yaml<br/>━━━━━━━━━━<br/>prepare-issue: [---prepare-issue-result---]<br/>enrich-issues: [---enrich-issues-result---]<br/>report-bug: [---bug-fingerprint---]"]
        Patterns["expected_output_patterns: list[str]<br/>━━━━━━━━━━<br/>[] if skill not in manifest<br/>['---...-result---'] if found"]
        Contracts -.->|"lru_cache lookup"| Resolver
        Resolver --> Patterns
    end

    subgraph Phase2 ["Phase 2: Session Execution"]
        direction TB
        Executor["executor.run(<br/>━━━━━━━━━━<br/>skill_command, cwd,<br/>● expected_output_patterns=patterns)"]
        Claude["Claude headless subprocess<br/>━━━━━━━━━━<br/>runs /autoskillit:prepare-issue et al.<br/>emits result block or not"]
        Executor --> Claude
    end

    subgraph Phase3 ["Phase 3: Session Adjudication (_check_session_content)"]
        direction TB
        ChannelQ{"Channel<br/>Confirmation?"}
        ChannelB["Channel B path<br/>━━━━━━━━━━<br/>JSONL log confirmed exit<br/>bypasses Gate 5 content check"]
        Gate5{"● Gate 5: Pattern Check<br/>━━━━━━━━━━<br/>any(re.search(p, result_text))<br/>for p in patterns — now active"}
        Gate5Fail["pattern absent<br/>━━━━━━━━━━<br/>success=False<br/>subtype=expected_artifact_absent"]
        Gate5Pass["pattern matched<br/>━━━━━━━━━━<br/>success=True<br/>result block is present"]
        ChannelQ -->|"CHANNEL_B"| ChannelB
        ChannelQ -->|"CHANNEL_A / UNMONITORED"| Gate5
        Gate5 -->|"no match"| Gate5Fail
        Gate5 -->|"matched"| Gate5Pass
    end

    subgraph Phase4 ["● Phase 4: Layered Response Construction (tools_integrations.py)"]
        direction TB
        SuccessQ{"result.success?"}
        EmptyQ{"result.result<br/>non-empty?"}
        ParsedErrQ{"parsed<br/>has error?"}
        FailResp["● Session Failure Response<br/>━━━━━━━━━━<br/>success=False, status=failed<br/>session_id, stderr, subtype, exit_code<br/>(full diagnostics always included)"]
        DrainResp["● Channel B Drain Race<br/>━━━━━━━━━━<br/>success=False, status=failed<br/>error='output was empty (drain race)'<br/>session_id, subtype, exit_code"]
        BlockParseErr["● Block Parse Error Response<br/>━━━━━━━━━━<br/>success=False, status=failed<br/>error from parsed block<br/>session_id, subtype, exit_code"]
        ParseBlock["_parse_*_result(result.result)<br/>━━━━━━━━━━<br/>block IS present — no contradiction<br/>extract JSON from delimited block"]
        OKResp["● Success Response<br/>━━━━━━━━━━<br/>success=True, status=complete<br/>● _without_success_key(parsed) merged<br/>success key NOT overwritable"]
        SuccessQ -->|"False"| FailResp
        SuccessQ -->|"True"| EmptyQ
        EmptyQ -->|"empty (drain race)"| DrainResp
        EmptyQ -->|"non-empty"| ParseBlock
        ParseBlock --> ParsedErrQ
        ParsedErrQ -->|"has error"| BlockParseErr
        ParsedErrQ -->|"clean parse"| OKResp
    end

    START --> Phase1
    Phase1 --> Phase2
    Phase2 --> Phase3
    ChannelB --> Phase4
    Gate5Fail --> Phase4
    Gate5Pass --> Phase4

    FAIL_OUT([MCP Error Response<br/>with full diagnostics])
    OK_OUT([MCP Success Response<br/>structured block data])
    ★ContractTests(["★ tests/contracts/test_skill_contracts.py<br/>━━━━━━━━━━<br/>asserts each skill present in manifest<br/>with non-empty expected_output_patterns"])
    ★IntegTests(["● tests/server/test_tools_integrations.py<br/>━━━━━━━━━━<br/>new executor-path tests: pattern propagation,<br/>drain race, diagnostics, no-overwrite"])

    FailResp --> FAIL_OUT
    DrainResp --> FAIL_OUT
    BlockParseErr --> FAIL_OUT
    OKResp --> OK_OUT

    Contracts -.->|"verified by"| ★ContractTests
    Phase4 -.->|"tested by"| ★IntegTests

    %% CLASS ASSIGNMENTS %%
    class START,FAIL_OUT,OK_OUT terminal;
    class ★ContractTests,★IntegTests newComponent;
    class Resolver,Patterns phase;
    class Contracts stateNode;
    class Executor,Claude,ParseBlock handler;
    class ChannelQ,SuccessQ,EmptyQ,ParsedErrQ stateNode;
    class ChannelB,Gate5Pass phase;
    class Gate5,Gate5Fail detector;
    class FailResp,DrainResp,BlockParseErr detector;
    class OKResp output;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal | Start and end states (success and failure) |
| Green | New Component | ★ New test files; ● new test cases added |
| Orange | Handler | Processing nodes: executor, subprocess, block
parser |
| Teal | State | Decision routing: channel, success, result checks;
contract manifest |
| Purple | Phase | Contract resolver, pattern list, channel B path |
| Red | Detector | Validation Gate 5 and all failure response
constructors |
| Dark Teal | Output | Success response construction |

### State Lifecycle Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;
    classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;

    subgraph Contracts ["INIT_ONLY: ● skill_contracts.yaml Contract Manifest"]
        direction LR
        Yaml["● skill_contracts.yaml<br/>━━━━━━━━━━<br/>+ prepare-issue: [---prepare-issue-result---]<br/>+ enrich-issues: [---enrich-issues-result---]<br/>+ report-bug: [---bug-fingerprint---]<br/>+ collapse-issues, issue-splitter, process-issues"]
        Cache["lru_cache manifest<br/>━━━━━━━━━━<br/>loaded once per process<br/>immutable after startup"]
        Yaml -->|"load at startup"| Cache
    end

    subgraph Resolver ["INIT_ONLY: output_pattern_resolver Closure"]
        direction LR
        Resolve["output_pattern_resolver(skill_cmd)<br/>━━━━━━━━━━<br/>resolve_skill_name() → get_skill_contract()<br/>returns list[str] or []"]
        Patterns["expected_output_patterns<br/>━━━━━━━━━━<br/>INIT_ONLY per invocation<br/>never modified after lookup"]
        Resolve --> Patterns
    end

    subgraph SessionResult ["INIT_PRESERVE: SkillResult Fields (never overwritten by parsed block)"]
        direction TB
        SR_success["result.success: bool<br/>━━━━━━━━━━<br/>INIT_PRESERVE<br/>set by adjudicator — AUTHORITATIVE"]
        SR_session_id["result.session_id: str<br/>━━━━━━━━━━<br/>INIT_PRESERVE<br/>always propagated to response"]
        SR_stderr["result.stderr: str<br/>━━━━━━━━━━<br/>INIT_PRESERVE<br/>● now always included on failure"]
        SR_subtype["result.subtype: str<br/>━━━━━━━━━━<br/>INIT_PRESERVE<br/>● now always included on failure"]
        SR_exit_code["result.exit_code: int<br/>━━━━━━━━━━<br/>INIT_PRESERVE<br/>● now always included on failure"]
        SR_result["result.result: str<br/>━━━━━━━━━━<br/>MUTABLE during session<br/>adjudicated output text"]
    end

    subgraph Gates ["VALIDATION GATES"]
        direction TB
        Gate5["Gate 5: _check_session_content<br/>━━━━━━━━━━<br/>any(re.search(p, result_text))<br/>● now active for prepare_issue/<br/>enrich_issues / report_bug"]
        PatternMismatch["pattern absent<br/>━━━━━━━━━━<br/>→ success=False<br/>prevents overwrite hazard reaching tool"]
        PatternMatch["pattern present<br/>━━━━━━━━━━<br/>→ success=True<br/>block guaranteed in result.result"]
        Gate5 -->|"no match"| PatternMismatch
        Gate5 -->|"matched"| PatternMatch
    end

    subgraph ResponseConstruction ["● DERIVED: Response Field Construction (tools_integrations.py)"]
        direction TB
        SuccessField["response.success<br/>━━━━━━━━━━<br/>DERIVED from result.success<br/>● NEVER overwritable by **parsed spread"]
        StatusField["response.status<br/>━━━━━━━━━━<br/>DERIVED: 'complete' iff success=True<br/>● invariant: success=False → status≠'complete'"]
        MergedFields["● _without_success_key(parsed)<br/>━━━━━━━━━━<br/>block data merged with key filter<br/>success key stripped from parsed dict<br/>before merge — cannot collide"]
        DiagFields["● Diagnostic fields on failure<br/>━━━━━━━━━━<br/>session_id, stderr, subtype, exit_code<br/>always included when success=False"]
    end

    subgraph ContractTests ["★ test_skill_contracts.py / ● test_tools_integrations.py"]
        direction TB
        YamlTests["★ tests/contracts/test_skill_contracts.py<br/>━━━━━━━━━━<br/>asserts each skill in manifest<br/>asserts patterns non-empty<br/>verifies INIT_ONLY contract"]
        IntegTests["● tests/server/test_tools_integrations.py<br/>━━━━━━━━━━<br/>no-overwrite assertion<br/>drain race: success=False invariant<br/>full diagnostics on failure<br/>patterns passed to executor"]
    end

    Cache -->|"read by"| Resolve
    Patterns -->|"passed as expected_output_patterns"| Gate5
    Gate5 -->|"adjudicates"| SR_success
    SR_success -->|"authoritative signal"| SuccessField
    PatternMismatch --> SR_success
    PatternMatch --> SR_result
    SR_result -->|"parsed only when success=True AND non-empty"| MergedFields
    SR_session_id --> DiagFields
    SR_stderr --> DiagFields
    SR_subtype --> DiagFields
    SR_exit_code --> DiagFields
    SuccessField --> StatusField
    MergedFields --> StatusField

    Yaml -.->|"verified by"| YamlTests
    ResponseConstruction -.->|"verified by"| IntegTests

    %% CLASS ASSIGNMENTS %%
    class Yaml,Cache detector;
    class Resolve,Patterns phase;
    class SR_success gap;
    class SR_session_id,SR_stderr,SR_subtype,SR_exit_code handler;
    class SR_result stateNode;
    class Gate5 stateNode;
    class PatternMismatch detector;
    class PatternMatch output;
    class SuccessField,StatusField detector;
    class MergedFields,DiagFields newComponent;
    class YamlTests,IntegTests newComponent;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Red | INIT_ONLY / Invariant | Immutable after init; invariant fields
that must never be overwritten |
| Orange | INIT_PRESERVE | Session result fields propagated to response;
never overwritten by parsed block |
| Purple | Resolver | Contract lookup closure and pattern list |
| Teal | Adjudication | Gate 5 content check and mutable result field |
| Yellow-Orange | Authoritative | `result.success` — the primary truth
signal |
| Green | New / Modified | New and modified components providing
contract coverage |
| Dark Teal | Passing Gate | Path when pattern matched; success response
construction |

Closes #384

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/remediation-384-r2-20260314-224923-956178/temp/rectify/rectify_prepare-issue-contract-enforcement_2026-03-14_211500.md`

## Token Usage Summary

## Token Summary\n\nNo token data captured for this run.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…h Tracking and resolve-failures Code-Index Fix (#397)

## Summary

Three remaining gaps survive PR #393's prompt-level CWD anchoring:

1. **Undetected tool-call writes** — `_validate_output_paths` only
checks 19 structured output tokens emitted as text; actual
`Write`/`Edit`/`Bash` tool calls to source-repo paths in the JSONL
stream are invisible to it.
2. **code-index exposes source-repo paths** — `set_project_path` is
called with `ORIGINAL_PROJECT_PATH` before and after worktree
exploration; the injected prompt directive is the only enforcement.
3. **`resolve-failures` never switches code-index to the worktree** —
the source-repo path remains active throughout the entire skill
execution.

The plan addresses all three gaps with a defense-in-depth approach:
- Add a post-session JSONL scanner (`_scan_jsonl_write_paths`) that
detects Write/Edit/Bash tool calls to paths outside the session's `cwd`.
- Surface detected violations as `write_path_warnings` on `SkillResult`,
so the orchestrator can observe them in `run_skill` responses.
- Persist `write_path_warnings` in the session diagnostic log
(`summary.json` and `sessions.jsonl` index), enabling offline scanning
with `jq`.
- Fix `resolve-failures` SKILL.md to switch code-index from
`PROJECT_ROOT` to `WORKTREE_PATH` after the worktree environment is
confirmed.

Gap 2 (code-index path exposure) is a prompt-level risk that cannot be
enforced at the Python layer without blocking all code-index usage. The
`_inject_cwd_anchor` directive already addresses it at the
model-instruction level. The JSONL scanner provides the technical
backstop: even if the model ignores the directive and writes to the
wrong path, the write will appear in the JSONL and be flagged.

## Architecture Impact

### Error/Resilience Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 45, 'rankSpacing': 55, '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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;

    START([run_skill called])

    subgraph Gate ["GATE LAYER (pipeline/gate.py ●)"]
        direction LR
        GATE_CHECK{"kitchen<br/>enabled?"}
        GATE_ERR["● gate_error_result<br/>━━━━━━━━━━<br/>success=False<br/>subtype=gate_error<br/>write_path_warnings=[]"]
    end

    subgraph Session ["HEADLESS SESSION (execution/headless.py ●)"]
        direction TB
        INJECT["_inject_cwd_anchor<br/>━━━━━━━━━━<br/>Prompt-level CWD directive<br/>(soft enforcement)"]
        SUBPROCESS["claude --output-format stream-json<br/>━━━━━━━━━━<br/>Emits raw JSONL stdout<br/>Write/Edit/Bash tool calls in-band"]
    end

    subgraph Detection ["DETECTION GATES (_build_skill_result ●)"]
        direction TB
        GATE1["● _validate_output_paths<br/>━━━━━━━━━━<br/>19 structured output tokens<br/>vs cwd prefix — BLOCKING<br/>→ path_contamination override"]
        GATE2["● _scan_jsonl_write_paths<br/>━━━━━━━━━━<br/>Raw JSONL tool-use records<br/>Write/Edit file_path + Bash abs paths<br/>NON-BLOCKING → warnings list"]
    end

    subgraph Result ["SKILL RESULT (core/types.py ●)"]
        direction TB
        SR_CONT["● SkillResult (path_contamination)<br/>━━━━━━━━━━<br/>success=False, needs_retry=True<br/>subtype=path_contamination<br/>write_path_warnings: list[str]"]
        SR_OK["● SkillResult (success)<br/>━━━━━━━━━━<br/>success=True<br/>write_path_warnings: list[str]<br/>Always present in to_json()"]
    end

    subgraph DiagLog ["SESSION DIAGNOSTIC LOG (session_log.py ●)"]
        direction TB
        SUMMARY["● summary.json<br/>━━━━━━━━━━<br/>write_path_warnings: [...] full list<br/>per-session detail artifact"]
        INDEX["● sessions.jsonl<br/>━━━━━━━━━━<br/>write_path_warnings_count: N<br/>compact index — jq queryable"]
    end

    T_BLOCKED([BLOCKED — path_contamination])
    T_GATE_ERR([BLOCKED — gate_error])
    T_WARNED([WARNED — write_path_warnings])
    T_SUCCESS([SUCCESS])

    START --> GATE_CHECK
    GATE_CHECK -->|"closed"| GATE_ERR
    GATE_CHECK -->|"open"| INJECT
    GATE_ERR --> T_GATE_ERR
    INJECT --> SUBPROCESS
    SUBPROCESS -->|"JSONL stdout"| GATE1
    SUBPROCESS -->|"JSONL stdout"| GATE2
    GATE1 -->|"token outside cwd"| SR_CONT
    GATE1 -->|"clean"| SR_OK
    GATE2 -->|"paths outside cwd"| SR_OK
    SR_CONT --> T_BLOCKED
    SR_OK -->|"write_path_warnings present"| T_WARNED
    SR_OK -->|"write_path_warnings empty"| T_SUCCESS
    SR_OK --> SUMMARY
    SR_OK --> INDEX
    SR_CONT --> SUMMARY
    SR_CONT --> INDEX

    class INJECT handler;
    class SUBPROCESS phase;
    class GATE_CHECK stateNode;
    class GATE1 detector;
    class GATE2 newComponent;
    class GATE_ERR integration;
    class SR_OK handler;
    class SR_CONT gap;
    class SUMMARY,INDEX output;
    class T_BLOCKED,T_GATE_ERR,T_WARNED,T_SUCCESS terminal;
    class START terminal;
```

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 42, 'rankSpacing': 52, '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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;

    START([run_skill invoked])

    subgraph GatePhase ["GATE PHASE (pipeline/gate.py ●)"]
        direction TB
        G_CHECK{"● kitchen<br/>enabled?"}
        G_BLOCK["● gate_error_result<br/>━━━━━━━━━━<br/>write_path_warnings=[]<br/>schema-compatible response"]
    end

    subgraph ExecPhase ["EXECUTION PHASE (execution/headless.py ●)"]
        direction TB
        ANCHOR["_inject_cwd_anchor<br/>━━━━━━━━━━<br/>Writes CWD directive to<br/>session prompt (soft guard)"]
        PROC["claude subprocess<br/>━━━━━━━━━━<br/>stream-json JSONL stdout<br/>reads: skill prompt + cwd<br/>writes: JSONL stdout stream"]
        TERM_BRANCH{"termination<br/>reason?"}
        STALE_RECV["stale recovery<br/>━━━━━━━━━━<br/>attempt stdout parse"]
        TIMEOUT_SR["synthetic SubprocessResult<br/>━━━━━━━━━━<br/>returncode=-1, subtype=timeout"]
        PARSE["parse_session_result<br/>━━━━━━━━━━<br/>reads: JSONL stdout<br/>→ ClaudeSessionResult"]
    end

    subgraph PostProc ["POST-PROCESSING (_build_skill_result ●)"]
        direction TB
        OUTCOME["_compute_outcome<br/>━━━━━━━━━━<br/>→ SessionOutcome<br/>→ retry_reason"]
        CAPTURE["_capture_failure<br/>━━━━━━━━━━<br/>writes: AuditStore<br/>(non-blocking side-effect)"]
        VALIDATE["● _validate_output_paths<br/>━━━━━━━━━━<br/>reads: 19 output tokens vs cwd<br/>BLOCKING gate"]
        SCAN["● _scan_jsonl_write_paths<br/>━━━━━━━━━━<br/>reads: raw JSONL stdout<br/>Write/Edit file_path + Bash paths<br/>NON-BLOCKING scanner"]
        CONT_CHECK{"path_contamination<br/>detected?"}
        BUDGET["_apply_budget_guard<br/>━━━━━━━━━━<br/>override needs_retry<br/>if retry budget exhausted"]
    end

    subgraph ResultPhase ["RESULT (core/types.py ●)"]
        direction TB
        SR_CONT["● SkillResult (path_contamination)<br/>━━━━━━━━━━<br/>success=False, needs_retry=True<br/>subtype=path_contamination<br/>write_path_warnings: list[str]"]
        SR_OK["● SkillResult (success / other)<br/>━━━━━━━━━━<br/>success=True or subtype variant<br/>write_path_warnings: list[str]<br/>always in to_json()"]
    end

    subgraph DiagPhase ["DIAGNOSTICS (execution/session_log.py ●)"]
        direction TB
        FLUSH["● flush_session_log<br/>━━━━━━━━━━<br/>writes: summary.json<br/>write_path_warnings full list<br/>writes: sessions.jsonl count"]
    end

    subgraph ResolveFailures ["RESOLVE-FAILURES SKILL (SKILL.md ●)"]
        direction LR
        RF_INIT["set_project_path(PROJECT_ROOT)<br/>━━━━━━━━━━<br/>Step 0.3: exploration phase<br/>reads: source-repo index"]
        RF_SWITCH["● set_project_path(worktree_path)<br/>━━━━━━━━━━<br/>Step 0.7: after env confirmed<br/>reads: worktree index only"]
        RF_FIX["investigate + fix<br/>━━━━━━━━━━<br/>reads: worktree-relative paths<br/>writes: worktree files"]
    end

    T_GATE_BLOCKED([BLOCKED — gate_error])
    T_BLOCKED([BLOCKED — path_contamination])
    T_WARNED([WARNED — write_path_warnings present])
    T_SUCCESS([SUCCESS])

    START --> G_CHECK
    G_CHECK -->|"closed"| G_BLOCK
    G_CHECK -->|"open"| ANCHOR
    G_BLOCK --> T_GATE_BLOCKED
    ANCHOR --> PROC
    PROC -->|"JSONL stdout"| TERM_BRANCH
    TERM_BRANCH -->|"STALE"| STALE_RECV
    TERM_BRANCH -->|"TIMED_OUT"| TIMEOUT_SR
    TERM_BRANCH -->|"NATURAL_EXIT"| PARSE
    STALE_RECV -->|"recovery fails"| SR_CONT
    STALE_RECV -->|"recovery succeeds"| PARSE
    TIMEOUT_SR --> PARSE
    PARSE --> OUTCOME
    OUTCOME --> CAPTURE
    CAPTURE --> VALIDATE
    VALIDATE --> SCAN
    SCAN --> CONT_CHECK
    CONT_CHECK -->|"yes"| SR_CONT
    CONT_CHECK -->|"no"| BUDGET
    BUDGET --> SR_OK
    SR_OK --> FLUSH
    SR_CONT --> FLUSH
    FLUSH --> T_BLOCKED
    FLUSH --> T_WARNED
    FLUSH --> T_SUCCESS

    RF_INIT --> RF_SWITCH
    RF_SWITCH --> RF_FIX

    class START terminal;
    class T_GATE_BLOCKED,T_BLOCKED,T_WARNED,T_SUCCESS terminal;
    class G_CHECK,TERM_BRANCH,CONT_CHECK stateNode;
    class G_BLOCK detector;
    class ANCHOR,PROC handler;
    class STALE_RECV,TIMEOUT_SR,PARSE phase;
    class OUTCOME,CAPTURE,BUDGET phase;
    class VALIDATE detector;
    class SCAN newComponent;
    class SR_OK handler;
    class SR_CONT gap;
    class FLUSH output;
    class RF_INIT gap;
    class RF_SWITCH newComponent;
    class RF_FIX handler;
```

### State Lifecycle Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 45, 'rankSpacing': 52, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;
    classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;

    subgraph SkillResultFields ["● SKILLRESULT FIELDS (core/types.py ●)"]
        direction TB
        INIT_ONLY["INIT_ONLY fields<br/>━━━━━━━━━━<br/>result, session_id<br/>subtype, cli_subtype<br/>is_error, exit_code<br/>stderr, token_usage<br/>worktree_path"]
        MUTABLE["MUTABLE fields<br/>━━━━━━━━━━<br/>success, needs_retry<br/>retry_reason<br/>overrideable by budget_guard<br/>and path_contamination branch"]
        WARN_FIELD["● APPEND_ONLY field<br/>━━━━━━━━━━<br/>write_path_warnings: list[str]<br/>set at construction<br/>default_factory=list<br/>never appended after init"]
        DERIVED["DERIVED property<br/>━━━━━━━━━━<br/>outcome: SessionOutcome<br/>computed from (success, needs_retry)<br/>never stored, not in to_json()"]
    end

    subgraph ConstructionGates ["CONSTRUCTION VALIDATION (_build_skill_result ●)"]
        direction TB
        GATE_CONT["● _validate_output_paths<br/>━━━━━━━━━━<br/>triggers path_contamination branch:<br/>success=False, needs_retry=True<br/>subtype=path_contamination"]
        GATE_SCAN["● _scan_jsonl_write_paths<br/>━━━━━━━━━━<br/>populates write_path_warnings<br/>NEVER changes success/subtype<br/>contract: non-blocking always"]
        GATE_BUDGET["_apply_budget_guard<br/>━━━━━━━━━━<br/>may override needs_retry=False<br/>retry_reason=BUDGET_EXHAUSTED<br/>contract: post-construction override"]
    end

    subgraph Serialization ["to_json() CONTRACT (core/types.py ●)"]
        direction LR
        JSON_ALWAYS["Always-present keys<br/>━━━━━━━━━━<br/>success, result, session_id<br/>subtype, cli_subtype, is_error<br/>exit_code, needs_retry<br/>retry_reason, stderr, token_usage<br/>● write_path_warnings"]
        JSON_COND["Conditional key<br/>━━━━━━━━━━<br/>worktree_path<br/>(only when not None)"]
    end

    subgraph GateSchema ["● gate_error_result SCHEMA (pipeline/gate.py ●)"]
        direction TB
        GATE_FIXED["● gate_error_result fixed fields<br/>━━━━━━━━━━<br/>success=False, subtype=gate_error<br/>is_error=True, exit_code=-1<br/>needs_retry=False<br/>retry_reason=none (string literal)<br/>● write_path_warnings=[]<br/>— schema-compatible with SkillResult"]
    end

    subgraph DiagContracts ["DIAGNOSTIC LOG CONTRACTS (session_log.py ●)"]
        direction TB
        SUMMARY_CONTRACT["● summary.json write contract<br/>━━━━━━━━━━<br/>write_path_warnings: list[str]<br/>full list persisted<br/>WRITE-ONLY artifact — never read<br/>for system logic"]
        INDEX_CONTRACT["● sessions.jsonl write contract<br/>━━━━━━━━━━<br/>write_path_warnings_count: int<br/>count only for compact scan<br/>WRITE-ONLY artifact<br/>queryable: jq '.write_path_warnings_count'"]
    end

    CONSTRUCTION_START([_build_skill_result begins])
    RESULT_OUT([SkillResult constructed])
    FLUSH_OUT([flush_session_log called])

    CONSTRUCTION_START --> GATE_CONT
    GATE_CONT -->|"contamination → override MUTABLE fields"| MUTABLE
    GATE_CONT -->|"always runs after"| GATE_SCAN
    GATE_SCAN -->|"populates"| WARN_FIELD
    WARN_FIELD --> GATE_BUDGET
    GATE_BUDGET -->|"may mutate"| MUTABLE
    MUTABLE --> RESULT_OUT
    INIT_ONLY --> RESULT_OUT
    WARN_FIELD --> RESULT_OUT
    RESULT_OUT --> JSON_ALWAYS
    RESULT_OUT --> JSON_COND
    JSON_ALWAYS --> FLUSH_OUT
    FLUSH_OUT --> SUMMARY_CONTRACT
    FLUSH_OUT --> INDEX_CONTRACT

    class CONSTRUCTION_START,RESULT_OUT,FLUSH_OUT terminal;
    class INIT_ONLY detector;
    class MUTABLE phase;
    class WARN_FIELD newComponent;
    class DERIVED stateNode;
    class GATE_CONT detector;
    class GATE_SCAN newComponent;
    class GATE_BUDGET handler;
    class JSON_ALWAYS output;
    class JSON_COND gap;
    class GATE_FIXED newComponent;
    class SUMMARY_CONTRACT,INDEX_CONTRACT output;
```

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/impl-cwd-gap-20260314-235747-669479/temp/make-plan/cwd_contamination_gaps_plan_2026-03-15_000000.md`

## Token Usage Summary

No token data collected.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…388)

## Summary

`collapse-issues` assembled multi-issue combined bodies by fetching all
bodies in bulk via `gh issue list --json body` — which truncates content
at ~256 characters — and then instructing the LLM to insert `<full body
of issue N, verbatim>` into the output. The angle-bracket syntax
signaled a fill-in-the-blank template slot, causing the LLM to
substitute a one-sentence summary or hyperlink rather than the actual
body text. No contract test caught either defect.

The fix establishes a **content-fidelity lineage contract**:
`collapse-issues` now calls `fetch_github_issue` per-issue (REST
endpoint, full body) and uses explicit verbatim-paste language with a
COPY MODE instruction. Three new contract tests enforce this for
`collapse-issues` specifically, and a new cross-skill sweep
(`test_issue_content_fidelity.py`) makes the defect structurally
impossible in any future skill that assembles `## From #N` body
sections.

## Requirements

### FETCH — Source Issue Content Retrieval

- **REQ-FETCH-001:** The skill must fetch the full body of each source
issue via `gh issue view N --json body` before composing the collapsed
issue.
- **REQ-FETCH-002:** The skill must not summarize, truncate, or
paraphrase source issue content during collapse.

### INLINE — Content Inlining

- **REQ-INLINE-001:** The collapsed issue body must contain the complete
body text from every source issue, inlined under clearly labeled
sections.
- **REQ-INLINE-002:** Structured sections from source issues
(requirements, acceptance criteria, design proposals, etc.) must be
preserved verbatim in the collapsed output.
- **REQ-INLINE-003:** Each inlined section must identify its source
issue number for provenance (e.g., "From #29").

### SELF — Self-Containment

- **REQ-SELF-001:** A reader of the collapsed issue must be able to
understand all source content without clicking through to original
issues.
- **REQ-SELF-002:** Cross-reference links to originals must serve as
provenance markers, not as substitutes for inlined content.

### STALE — Staleness Handling

- **REQ-STALE-001:** When a source issue is known to be outdated, the
collapsed issue must include a staleness note adjacent to that source's
inlined content.

## Architecture Impact

### Data Lineage Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
flowchart LR
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    subgraph GitHub ["GitHub API"]
        BULK["● gh issue list<br/>━━━━━━━━━━<br/>number, title, labels<br/>body removed from query"]
        REST["fetch_github_issue<br/>━━━━━━━━━━<br/>GET /issues/{N} (REST)<br/>Full untruncated body"]
    end

    subgraph Skill ["● collapse-issues SKILL.md"]
        STEP3["● Step 3: Metadata Fetch<br/>━━━━━━━━━━<br/>number, title, labels only<br/>no body — grouping input"]
        STEP4["Step 4: LLM Grouping<br/>━━━━━━━━━━<br/>title + labels scoring<br/>form candidate groups"]
        STEP5["★ Step 5: Per-Issue Fetch<br/>━━━━━━━━━━<br/>fetch_github_issue per issue<br/>include_comments=true"]
        FETCHED["★ fetched_content[N]<br/>━━━━━━━━━━<br/>content.body field<br/>full untruncated text"]
        STEP6B["● Step 6b: Body Assembly<br/>━━━━━━━━━━<br/>SWITCH TO COPY MODE<br/>verbatim paste only"]
        NEVER["● NEVER Block<br/>━━━━━━━━━━<br/>no summarize/paraphrase<br/>no angle-bracket syntax<br/>no bulk body field"]
    end

    subgraph Output ["Combined Issue"]
        COMBINED["Combined Issue Body<br/>━━━━━━━━━━<br/>## From #N sections<br/>full verbatim content"]
    end

    subgraph Tests ["★ Contract Enforcement"]
        T1["● test_collapse_issues_contracts.py<br/>━━━━━━━━━━<br/>+ uses_per_issue_fetch<br/>+ never_summarize<br/>+ no_angle_bracket_placeholder"]
        T2["★ test_issue_content_fidelity.py<br/>━━━━━━━━━━<br/>cross-skill sweep:<br/>## From # → fetch_github_issue<br/>no angle-bracket syntax<br/>NEVER summarize block"]
        T3["● test_github_ops.py<br/>━━━━━━━━━━<br/>stale pytest.skip guards<br/>removed — tests active"]
    end

    BULK -->|"number,title,labels"| STEP3
    STEP3 -->|"issue list (no body)"| STEP4
    STEP4 -->|"qualifying groups"| STEP5
    STEP5 -->|"per-issue REST call"| REST
    REST -->|"full body"| FETCHED
    FETCHED -->|"content field"| STEP6B
    NEVER -.->|"constrains"| STEP6B
    STEP6B -->|"verbatim sections"| COMBINED

    T1 -.->|"enforces"| STEP5
    T1 -.->|"enforces"| NEVER
    T2 -.->|"cross-skill guard"| STEP5
    T3 -.->|"activates skipped tests"| T1

    class BULK handler;
    class REST stateNode;
    class STEP3 handler;
    class STEP4 phase;
    class STEP5 newComponent;
    class FETCHED newComponent;
    class STEP6B newComponent;
    class NEVER newComponent;
    class COMBINED output;
    class T1 detector;
    class T2 newComponent;
    class T3 detector;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Input | Data origins and entry points |
| Orange | Handler | Existing fetch/processing steps |
| Purple | Phase | LLM control and grouping analysis |
| Green | New/Modified | ★ New or ● modified components |
| Teal | Data | REST source (authoritative data) |
| Dark Teal | Output | Combined issue result |
| Red | Contract Tests | Enforcement guards |

Closes #372

## Implementation Plan

Plan file:
`/home/talon/projects/autoskillit-runs/remediation-372-20260314-205033-708941/temp/rectify/rectify_collapse-issues-content-fidelity_2026-03-14_205033.md`

## Token Usage Summary

## Token Summary\n\nNo token data captured for this run.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary

Upgrade the `fastmcp` dependency constraint in `pyproject.toml` from
`>=3.0.2` to
`>=3.1.1,<4.0` and regenerate the lockfile. Before bumping the version,
migrate all test
introspection that relies on the private
`mcp._local_provider._components` API to
FastMCP's public `Client` API (`from fastmcp.client import Client`),
making the test
suite resilient to FastMCP internal restructuring. After the version
bump, verify that
two existing runtime workarounds (`RESERVED_LOG_RECORD_KEYS` filter and
the `_notify()`
triple-exception guard) remain correct against v3.1.1.

The change is zero-risk to production behavior: FastMCP v3.0.2 → v3.1.1
has no breaking
changes on the API surface AutoSkillit uses. The only breaking change is
in the tests'
dependency on private internals.

## Architecture Impact

### Module Dependency Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
graph TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;

    subgraph DepMgmt ["● DEPENDENCY MANAGEMENT"]
        direction LR
        PYPROJECT["● pyproject.toml<br/>━━━━━━━━━━<br/>fastmcp>=3.1.1,<4.0<br/>(was >=3.0.2)"]
        UVLOCK["● uv.lock<br/>━━━━━━━━━━<br/>Regenerated<br/>fastmcp 3.1.1"]
        PYPROJECT -->|"pins"| UVLOCK
    end

    subgraph ExtDep ["EXTERNAL — fastmcp"]
        direction TB
        FASTMCP_CORE["fastmcp<br/>━━━━━━━━━━<br/>FastMCP, Context"]
        FASTMCP_DI["fastmcp.dependencies<br/>━━━━━━━━━━<br/>CurrentContext"]
        FASTMCP_CLIENT["fastmcp.client<br/>━━━━━━━━━━<br/>Client (public API)"]
    end

    subgraph Tests ["● TESTS (server/)"]
        direction TB
        TEST_INIT["● test_server_init.py<br/>━━━━━━━━━━<br/>Client(mcp) inline<br/>list_tools() assertions"]
        TEST_RECIPE["● test_tools_recipe.py<br/>━━━━━━━━━━<br/>Client(mcp) inline<br/>tool description checks"]
    end

    subgraph L3Server ["L3 — SERVER (fastmcp boundary)"]
        direction TB
        SERVER_INIT["server/__init__.py<br/>━━━━━━━━━━<br/>FastMCP('autoskillit')<br/>mcp app object"]
        TOOL_HANDLERS["server/tools_*.py (10 files)<br/>━━━━━━━━━━<br/>Context, CurrentContext<br/>@mcp.tool handlers"]
        HELPERS["server/helpers.py<br/>━━━━━━━━━━<br/>_notify() guard<br/>RESERVED_LOG_RECORD_KEYS"]
    end

    subgraph L2 ["L2 — RECIPE / MIGRATION"]
        direction LR
        RECIPE["recipe/"]
        MIGRATION["migration/"]
    end

    subgraph L1 ["L1 — SERVICES"]
        direction LR
        PIPELINE["pipeline/"]
        EXECUTION["execution/"]
        WORKSPACE["workspace/"]
    end

    subgraph L0 ["L0 — FOUNDATION (core/)"]
        CORE["core/<br/>━━━━━━━━━━<br/>Zero fastmcp imports"]
    end

    %% Dependency management → resolves external
    PYPROJECT -.->|"version constraint"| FASTMCP_CORE

    %% L3 server imports fastmcp
    SERVER_INIT -->|"imports FastMCP"| FASTMCP_CORE
    TOOL_HANDLERS -->|"imports Context"| FASTMCP_CORE
    TOOL_HANDLERS -->|"imports CurrentContext"| FASTMCP_DI

    %% Tests drive server via public Client API
    TEST_INIT -->|"inline import<br/>fastmcp.client.Client"| FASTMCP_CLIENT
    TEST_RECIPE -->|"inline import<br/>fastmcp.client.Client"| FASTMCP_CLIENT
    TEST_INIT -->|"drives"| SERVER_INIT
    TEST_RECIPE -->|"drives"| TOOL_HANDLERS

    %% Downward valid dependencies
    SERVER_INIT --> TOOL_HANDLERS
    SERVER_INIT --> HELPERS
    L3Server --> L2
    L2 --> L1
    L1 --> L0

    %% CLASS ASSIGNMENTS %%
    class PYPROJECT,UVLOCK output;
    class FASTMCP_CORE,FASTMCP_DI,FASTMCP_CLIENT integration;
    class SERVER_INIT phase;
    class TOOL_HANDLERS,HELPERS handler;
    class TEST_INIT,TEST_RECIPE stateNode;
    class RECIPE,MIGRATION,PIPELINE,EXECUTION,WORKSPACE,CORE cli;
```

### Development Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 output fill:#00695c,stroke:#4db6ac,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;

    subgraph DepMgmt ["● DEPENDENCY MANAGEMENT"]
        direction LR
        PYPROJECT["● pyproject.toml<br/>━━━━━━━━━━<br/>fastmcp>=3.1.1,<4.0<br/>hatchling build backend"]
        UVLOCK["● uv.lock<br/>━━━━━━━━━━<br/>Regenerated<br/>fastmcp 3.1.1"]
        UVLOCKHOOK["uv-lock-check hook<br/>━━━━━━━━━━<br/>Verifies lockfile<br/>consistency on commit"]
        PYPROJECT -->|"uv lock"| UVLOCK
        UVLOCK -->|"validates"| UVLOCKHOOK
    end

    subgraph Quality ["QUALITY GATES (pre-commit)"]
        direction LR
        FORMAT["ruff format<br/>━━━━━━━━━━<br/>Auto-fix<br/>code style"]
        LINT["ruff check --fix<br/>━━━━━━━━━━<br/>Auto-fix<br/>code quality"]
        TYPECHECK["mypy src/<br/>━━━━━━━━━━<br/>--ignore-missing-imports<br/>type safety"]
        SECRETS["gitleaks<br/>━━━━━━━━━━<br/>Secret scanning<br/>v8.30.0"]
        FORMAT --> LINT --> TYPECHECK
        TYPECHECK -.->|"parallel"| SECRETS
    end

    subgraph Testing ["TEST FRAMEWORK"]
        direction TB
        CONFTEST["conftest.py<br/>━━━━━━━━━━<br/>tool_ctx fixture<br/>_clear_headless_env (autouse)<br/>_structlog_to_null (autouse)"]
        TESTINIT["● test_server_init.py<br/>━━━━━━━━━━<br/>15 test classes<br/>Client(mcp) public API<br/>kitchen visibility tests"]
        TESTRECIPE["● test_tools_recipe.py<br/>━━━━━━━━━━<br/>5 test classes<br/>Client(mcp) public API<br/>docstring contract tests"]
        CONFTEST --> TESTINIT
        CONFTEST --> TESTRECIPE
    end

    subgraph Runner ["TEST RUNNER"]
        direction TB
        PYTEST["pytest<br/>━━━━━━━━━━<br/>asyncio_mode=auto<br/>timeout=60s"]
        XDIST["pytest-xdist<br/>━━━━━━━━━━<br/>-n 4 parallel workers<br/>RAM tmpfs on Linux"]
        ASYNCIO["pytest-asyncio<br/>━━━━━━━━━━<br/>async test support<br/>function-scoped loop"]
        PYTEST --> XDIST
        PYTEST --> ASYNCIO
    end

    subgraph Tasks ["TASK RUNNER (Taskfile)"]
        direction LR
        TESTALL["task test-all<br/>━━━━━━━━━━<br/>lint-imports + pytest<br/>human-facing"]
        TESTCHECK["task test-check<br/>━━━━━━━━━━<br/>PASS/FAIL output<br/>automation/MCP"]
        INSTALLWT["task install-worktree<br/>━━━━━━━━━━<br/>uv venv + pip install<br/>worktree isolation"]
    end

    subgraph CI ["CI/CD (.github/workflows/)"]
        direction TB
        TESTSWF["tests.yml<br/>━━━━━━━━━━<br/>PR to main/integration/stable<br/>ubuntu + macos matrix"]
        PREFLIGHT["preflight job<br/>━━━━━━━━━━<br/>uv lock --check<br/>OS matrix compute"]
        TESTJOB["test job<br/>━━━━━━━━━━<br/>uv sync --locked<br/>task test-all"]
        TESTSWF --> PREFLIGHT --> TESTJOB
    end

    subgraph EntryPoints ["ENTRY POINTS"]
        CLI["autoskillit CLI<br/>━━━━━━━━━━<br/>autoskillit.cli:main<br/>serve / init / doctor"]
    end

    %% FLOW %%
    DepMgmt -->|"resolves"| Quality
    Quality -->|"gate passes"| Testing
    Testing -->|"run via"| Runner
    Runner -->|"invoked by"| Tasks
    Tasks -->|"in CI"| CI
    DepMgmt -->|"installs package"| EntryPoints

    %% CLASS ASSIGNMENTS %%
    class PYPROJECT,UVLOCK output;
    class UVLOCKHOOK detector;
    class FORMAT,LINT,TYPECHECK,SECRETS detector;
    class CONFTEST stateNode;
    class TESTINIT,TESTRECIPE stateNode;
    class PYTEST,XDIST,ASYNCIO handler;
    class TESTALL,TESTCHECK,INSTALLWT phase;
    class TESTSWF,PREFLIGHT,TESTJOB cli;
    class CLI output;
```

Closes #385

## Implementation Plan

Plan file:
`temp/make-plan/upgrade_fastmcp_3_1_1_plan_2026-03-15_000000.md`

## Token Usage Summary

## token_summary


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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary

`prepare_issue` and `enrich_issues` each have three failure paths for
headless session results. All three paths were hand-rolled dicts. Two of
the three omitted `stderr` — the most diagnostic field when debugging
intermittent failures. There was no shared error-response contract; no
structural mechanism prevented omissions. Additionally,
`prepare_issue`'s session-failure path called `_parse_prepare_result()`
on an already-failed result, silently replacing the actual failure
reason (e.g., `"stale"`, `"timeout"`) with the generic sentinel `"no
result block found"`. This PR introduces
`_build_headless_error_response()` — a single canonical builder for all
headless session failure responses — and adds a parametric contract test
that exercises every failure path of every headless session tool,
enforcing the full diagnostic field set structurally.

## Architecture Impact

### Error/Resilience Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, '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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;

    CALL(["Tool call<br/>prepare_issue / enrich_issues"])

    subgraph Headless ["● HEADLESS EXECUTION (headless.py)"]
        EXEC["● _build_skill_result<br/>━━━━━━━━━━<br/>Launch Claude subprocess"]
        CB_GATE{"● Channel B<br/>wins?"}
        RECOVER["● _recover_block_from<br/>_assistant_messages<br/>━━━━━━━━━━<br/>Scan assistant_messages<br/>for expected patterns"]
        STALE{"● STALE<br/>termination?"}
        STALE_REC["● Stale recovery<br/>━━━━━━━━━━<br/>success+result present?<br/>→ recovered_from_stale"]
        BUDGET["● _apply_budget_guard<br/>━━━━━━━━━━<br/>consecutive_failures > 3<br/>→ BUDGET_EXHAUSTED"]
    end

    subgraph SessionValidation ["● SESSION VALIDATION (session.py)"]
        IS_ERR{"● is_error?"}
        EMPTY_R{"● result<br/>empty?"}
        SUB_FAIL{"● failure<br/>subtype?"}
        MARKER{"● completion<br/>marker ok?"}
        PATTERNS["● _check_expected_patterns<br/>━━━━━━━━━━<br/>all(re.search(p, result))"]
    end

    subgraph SkillResultCarrier ["● SkillResult (session.py)"]
        SR["● SkillResult<br/>━━━━━━━━━━<br/>success · result · session_id<br/>stderr · subtype · exit_code<br/>needs_retry · retry_reason"]
    end

    subgraph ToolPaths ["● TOOL ERROR PATHS (tools_integrations.py)"]
        P1{"● PATH 1<br/>not result.success?"}
        P2{"● PATH 2<br/>result empty?"}
        P3{"● PATH 3<br/>block parse<br/>error?"}
        PARSE["_parse_prepare/enrich_result<br/>━━━━━━━━━━<br/>Extract ---block--- delimiter"]
        BUILDER["● _build_headless_error_response<br/>━━━━━━━━━━<br/>Contract: success=False · status<br/>error · session_id · stderr<br/>subtype · exit_code"]
    end

    T_SUCCESS(["SUCCESS<br/>Parseable block returned"])
    T_FAIL(["● FAILURE RESPONSE<br/>All 7 diagnostic fields present"])
    T_RETRY(["RETRY<br/>needs_retry=True"])

    CALL --> EXEC
    EXEC --> CB_GATE
    CB_GATE -->|"yes — skip marker check"| RECOVER
    CB_GATE -->|"no"| IS_ERR
    RECOVER -->|"patterns found in assistant_messages"| IS_ERR
    RECOVER -->|"patterns NOT found → False"| SR

    IS_ERR -->|"yes"| SR
    IS_ERR -->|"no"| EMPTY_R
    EMPTY_R -->|"yes"| SR
    EMPTY_R -->|"no"| SUB_FAIL
    SUB_FAIL -->|"stale / error_max_turns"| SR
    SUB_FAIL -->|"no"| MARKER
    MARKER -->|"absent or only marker"| SR
    MARKER -->|"ok"| PATTERNS
    PATTERNS -->|"all match → True"| SR
    PATTERNS -->|"missing → False"| SR

    STALE -->|"yes"| STALE_REC
    STALE -->|"no"| SR
    STALE_REC -->|"valid result found"| T_SUCCESS
    STALE_REC -->|"no valid result"| BUDGET

    SR -->|"success=False, needs_retry=True"| STALE
    SR -->|"success=False, needs_retry=True"| BUDGET
    BUDGET -->|"budget ok → needs_retry=True"| T_RETRY
    BUDGET -->|"budget exhausted → needs_retry=False"| SR

    SR --> P1
    P1 -->|"yes"| BUILDER
    P1 -->|"no (success=True)"| P2
    P2 -->|"yes"| BUILDER
    P2 -->|"no"| PARSE
    PARSE --> P3
    P3 -->|"yes"| BUILDER
    P3 -->|"no — valid block"| T_SUCCESS
    BUILDER --> T_FAIL

    class CALL terminal;
    class EXEC,RECOVER,STALE_REC handler;
    class CB_GATE,STALE,IS_ERR,EMPTY_R,SUB_FAIL,MARKER detector;
    class PATTERNS,P1,P2,P3 detector;
    class SR stateNode;
    class PARSE phase;
    class BUILDER newComponent;
    class BUDGET gap;
    class T_SUCCESS,T_FAIL,T_RETRY terminal;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal | Entry point and final states |
| Orange | Handler | Headless execution and recovery operations |
| Red | Detector | Validation gates and routing decisions |
| Dark Teal | State | SkillResult data carrier |
| Purple | Phase | Block parsing phase |
| Green | Contract | `_build_headless_error_response` — shared error
builder |
| Yellow | Guard | Budget guard — circuit breaker for consecutive
retries |

Closes #384

## Implementation Plan

Plan file:
`temp/rectify/rectify_headless_error_response_contract_2026-03-15_091900_part_a.md`

## Token Usage Summary

## Token Summary

No token data collected (headless sessions do not report tokens to the
orchestrator).

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… — PART A ONLY (#406)

## Summary

`clone_repo` rewrites `origin` to `file://{clone_path}` (deliberate
isolation) and stores
the real GitHub URL in `upstream`. Four independent consumers —
`_parse_repo_from_remote`
(ci.py), `infer_repo_from_remote` (helpers.py), two recipe inline shell
commands — all call
`git remote get-url origin` and apply diverging `github.com[:/]` regex
patterns that silently
return empty/None on any `file://` URL. The real URL
(`context.remote_url`) is already
available in the recipe context but is never threaded into CI or
merge-queue tools.

Part A delivers the canonical service layer: a single
`parse_github_repo` pure function
in `core/`, a single `resolve_remote_repo` resolver in `execution/` that
encodes the correct
remote priority (`upstream` > `origin`), and wiring of both into all
existing Python consumers.

## Architecture Impact

### Module Dependency Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
graph TB
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;

    subgraph L3 ["L3 — Server Layer"]
        direction LR
        HELPERS["● server/helpers.py<br/>━━━━━━━━━━<br/>infer_repo_from_remote<br/>delegates to resolver"]
        TOOLS_CI["● server/tools_ci.py<br/>━━━━━━━━━━<br/>wait_for_ci handler<br/>remote_url param added"]
    end

    subgraph L1 ["L1 — Execution Layer"]
        direction LR
        CI["● execution/ci.py<br/>━━━━━━━━━━<br/>DefaultCIWatcher<br/>_resolve_repo delegates"]
        RESOLVER["★ execution/remote_resolver.py<br/>━━━━━━━━━━<br/>resolve_remote_repo<br/>upstream → origin priority"]
    end

    subgraph L0 ["L0 — Core Layer (stdlib only)"]
        direction LR
        CORE_INIT["● core/__init__.py<br/>━━━━━━━━━━<br/>re-exports parse_github_repo"]
        GITHUB_URL["★ core/github_url.py<br/>━━━━━━━━━━<br/>parse_github_repo<br/>single canonical regex"]
    end

    subgraph Ext ["External / stdlib"]
        direction LR
        ASYNCIO["asyncio<br/>━━━━━━━━━━<br/>subprocess exec"]
        HTTPX["httpx<br/>━━━━━━━━━━<br/>GitHub REST API"]
        RE["re<br/>━━━━━━━━━━<br/>URL regex"]
    end

    %% VALID DOWNWARD DEPENDENCIES %%
    HELPERS -->|"imports resolve_remote_repo"| RESOLVER
    TOOLS_CI -->|"imports helpers"| HELPERS
    CI -->|"_resolve_repo calls"| RESOLVER
    RESOLVER -->|"imports parse_github_repo"| CORE_INIT
    CORE_INIT -->|"re-exports"| GITHUB_URL
    RESOLVER -->|"uses"| ASYNCIO
    CI -->|"uses"| HTTPX
    GITHUB_URL -->|"uses"| RE

    %% CLASS ASSIGNMENTS %%
    class HELPERS,TOOLS_CI handler;
    class CI phase;
    class RESOLVER newComponent;
    class CORE_INIT stateNode;
    class GITHUB_URL newComponent;
    class ASYNCIO,HTTPX,RE integration;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Orange | Handler | Server-layer tools and helpers |
| Purple | Phase | Execution-layer service (CI watcher) |
| Green | New Component | ★ New modules added by this PR |
| Teal | Core | ● Modified core exports |
| Red | External | stdlib / third-party dependencies |

### Data Lineage Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
flowchart LR
    %% CLASS DEFINITIONS %%
    classDef cli 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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;

    subgraph Origins ["Data Origins"]
        direction TB
        SRC_REPO["Source Repo<br/>━━━━━━━━━━<br/>Real GitHub URL<br/>github.com/owner/repo"]
        CLONE_TOOL["clone_repo()<br/>━━━━━━━━━━<br/>Returns result.remote_url<br/>(real GitHub URL)"]
    end

    subgraph GitRemotes ["Git Remote Storage"]
        direction TB
        UPSTREAM["git upstream remote<br/>━━━━━━━━━━<br/>= real GitHub URL<br/>(set by clone_repo)"]
        ORIGIN["git origin remote<br/>━━━━━━━━━━<br/>= file:///clone_path<br/>(isolation — file:// URL)"]
    end

    subgraph Parsing ["★ Canonical Parsing Layer"]
        direction TB
        PARSER["★ parse_github_repo(url)<br/>━━━━━━━━━━<br/>core/github_url.py<br/>str → owner/repo | None<br/>file:// → None (safe)"]
        RESOLVER["★ resolve_remote_repo(cwd, hint?)<br/>━━━━━━━━━━<br/>execution/remote_resolver.py<br/>1. hint (if full URL: parse it)<br/>2. upstream → parse<br/>3. origin → parse<br/>→ owner/repo | None"]
    end

    subgraph Consumers ["● Updated Consumers"]
        direction TB
        CI_RESOLVE["● DefaultCIWatcher._resolve_repo<br/>━━━━━━━━━━<br/>execution/ci.py<br/>delegates to resolve_remote_repo"]
        INFER["● infer_repo_from_remote(cwd)<br/>━━━━━━━━━━<br/>server/helpers.py<br/>delegates to resolve_remote_repo<br/>returns '' on None"]
    end

    subgraph APIs ["External GitHub APIs"]
        direction TB
        REST["GitHub REST API<br/>━━━━━━━━━━<br/>/repos/{owner}/{repo}/actions/runs"]
        GRAPHQL["GitHub GraphQL API<br/>━━━━━━━━━━<br/>merge queue query"]
    end

    SRC_REPO -->|"real URL captured"| CLONE_TOOL
    CLONE_TOOL -->|"sets upstream"| UPSTREAM
    CLONE_TOOL -->|"sets file:// origin"| ORIGIN

    UPSTREAM -->|"git remote get-url upstream"| RESOLVER
    ORIGIN -->|"file:// → None (skipped)"| RESOLVER

    RESOLVER -->|"calls for each URL"| PARSER
    RESOLVER -->|"owner/repo"| CI_RESOLVE
    RESOLVER -->|"owner/repo"| INFER

    CI_RESOLVE -->|"owner/repo"| REST
    INFER -->|"owner/repo"| GRAPHQL

    %% CLASS ASSIGNMENTS %%
    class SRC_REPO,CLONE_TOOL cli;
    class UPSTREAM,ORIGIN stateNode;
    class PARSER,RESOLVER newComponent;
    class CI_RESOLVE,INFER handler;
    class REST,GRAPHQL integration;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Origin | Data source — real GitHub URL and clone operation
|
| Teal | Git Storage | Git remote values (upstream=real, origin=file://)
|
| Green | New | ★ Canonical parser and resolver added by this PR |
| Orange | Consumer | ● Updated Python consumers now routing through
resolver |
| Red | External | GitHub REST and GraphQL API endpoints |

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 55, '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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    %% TERMINALS %%
    CI_CALL(["● _resolve_repo(repo, cwd)<br/>━━━━━━━━━━<br/>execution/ci.py"])
    MQ_CALL(["● infer_repo_from_remote(cwd)<br/>━━━━━━━━━━<br/>server/helpers.py"])
    RETURN_REPO(["→ owner/repo"])
    RETURN_NONE_CI(["→ CI error: no repo"])
    RETURN_EMPTY_MQ(["→ '' (merge queue skipped)"])

    subgraph Delegation ["● Delegation Shims"]
        direction TB
        CI_DELEGATE["● _resolve_repo delegates<br/>━━━━━━━━━━<br/>hint = repo (or None)"]
        MQ_DELEGATE["● infer_repo_from_remote delegates<br/>━━━━━━━━━━<br/>no hint (cwd only)"]
    end

    subgraph Resolver ["★ resolve_remote_repo(cwd, hint?)"]
        direction TB
        CHECK_HINT{"hint<br/>provided?"}
        CHECK_FORMAT{"Is hint already<br/>owner/repo format?"}
        PARSE_HINT["★ parse_github_repo(hint)<br/>━━━━━━━━━━<br/>core/github_url.py"]
        CHECK_PARSE{"parsed<br/>non-None?"}
        LOOP_START["for remote in<br/>(upstream, origin)"]
        GIT_CMD["git remote get-url {remote}<br/>━━━━━━━━━━<br/>asyncio subprocess"]
        CHECK_RC{"returncode<br/>== 0?"}
        PARSE_URL["★ parse_github_repo(url)<br/>━━━━━━━━━━<br/>file:// → None<br/>github.com/x/y → owner/repo"]
        CHECK_PARSED{"parsed<br/>non-None?"}
        CHECK_MORE{"more remotes<br/>to try?"}
    end

    %% CONSUMER ENTRY POINTS %%
    CI_CALL --> CI_DELEGATE
    MQ_CALL --> MQ_DELEGATE
    CI_DELEGATE -->|"hint=repo"| CHECK_HINT
    MQ_DELEGATE -->|"no hint"| CHECK_HINT

    %% HINT RESOLUTION PATH %%
    CHECK_HINT -->|"yes"| CHECK_FORMAT
    CHECK_HINT -->|"no"| LOOP_START

    CHECK_FORMAT -->|"yes (owner/repo)"| RETURN_REPO
    CHECK_FORMAT -->|"no (full URL)"| PARSE_HINT
    PARSE_HINT --> CHECK_PARSE
    CHECK_PARSE -->|"yes"| RETURN_REPO
    CHECK_PARSE -->|"no (non-GitHub URL)"| LOOP_START

    %% REMOTE ITERATION PATH %%
    LOOP_START --> GIT_CMD
    GIT_CMD --> CHECK_RC
    CHECK_RC -->|"fail (remote not set)"| CHECK_MORE
    CHECK_RC -->|"success"| PARSE_URL
    PARSE_URL --> CHECK_PARSED
    CHECK_PARSED -->|"yes (GitHub URL)"| RETURN_REPO
    CHECK_PARSED -->|"no (file:// → None)"| CHECK_MORE
    CHECK_MORE -->|"yes (try next)"| GIT_CMD
    CHECK_MORE -->|"no (exhausted)"| RETURN_NONE_CI
    CHECK_MORE -->|"no (exhausted)"| RETURN_EMPTY_MQ

    %% CONSUMER OUTCOMES %%
    RETURN_NONE_CI -.->|"CI: conclusion=no_runs"| CI_CALL
    RETURN_EMPTY_MQ -.->|"MQ: skips merge queue"| MQ_CALL

    %% CLASS ASSIGNMENTS %%
    class CI_CALL,MQ_CALL,RETURN_REPO,RETURN_NONE_CI,RETURN_EMPTY_MQ terminal;
    class CI_DELEGATE,MQ_DELEGATE handler;
    class CHECK_HINT,CHECK_FORMAT,CHECK_RC,CHECK_PARSED,CHECK_PARSE,CHECK_MORE stateNode;
    class PARSE_HINT,PARSE_URL newComponent;
    class LOOP_START,GIT_CMD phase;
```

**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal | Entry points and return values |
| Orange | Handler | ● Updated delegation shims |
| Teal | State | Decision diamonds — routing guards |
| Green | New | ★ Canonical parser calls (new in this PR) |
| Purple | Phase | Loop iteration and subprocess execution |

Closes #400

## Implementation Plan

Plan file:
`temp/rectify/rectify_clone-remote-url-breaks-merge-queue-ci-watcher_2026-03-15_092200_part_a.md`

## Token Usage Summary

## Token Summary\n\nNo token usage data accumulated during this pipeline
run.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@Trecek Trecek changed the title Round up Integration v0.3.1: Merge Queue, Sub-Recipes, PostToolUse Reformatter, Headless Isolation Mar 15, 2026
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

10 audit dimensions | 5 critical | 42 warning | 17 info | 70 total findings

Critical Findings

  1. remediation.yaml:671release_issue_success step deleted but queue-merge success path never calls release_issue. Issues stay labeled in-progress after merge.
  2. version-bump.yml:36,159tomllib.loads() called with read_text() (returns str) but requires bytes. Will raise TypeError at runtime.
  3. release.yml:31 — Same tomllib.loads()/read_text() bug.

Warning Highlights

  • headless.py:562extracted_worktree_path may be uninitialized (NameError at runtime)
  • ci.py:72 — Logic inversion in _resolve_repo when repo provided but cwd empty
  • gate.py:85,93headless_error_result() envelope asymmetric with gate_error_result()
  • All arch-lens skills — Colon in temp directory paths (filesystem-invalid)
  • audit-cohesion SKILL.mdset_project_path uses literal placeholder path
  • helpers.py:1806_require_not_headless passes msg=None to headless_error_result()

See inline comments for all 47 actionable findings.

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 Review — Warning findings batch 1/3

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 Review — Warning findings batch 2/3

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 Review — Warning findings batch 3/3

Trecek and others added 5 commits March 15, 2026 14:17
…zy init, test improvements

- Fix release.yml read_bytes() → read_text() for tomllib.loads()
- Add remote_url to merge-prs.yaml wait_queue_pr step
- Add GraphQL error check in merge_queue._fetch_queue_entries
- Fix dead elif cwd: branch in headless.py → else:
- Fix headless_error_result() field parity with gate_error_result()
- Scope CI workflow permissions to job level (release.yml, version-bump.yml)
- Add contents: read + actions: read to codeql.yml
- Fix skill text: analyze-prs token syntax, audit-cohesion placeholder, resolve-review config-driven test cmd
- Delete dead isinstance assertion in test_rules_contracts.py
- Replace disjunctive assertion in test_sub_recipe_validation.py
- Fix mock patch path in test_tools_ci.py
- Relocate resolve_ingredient_defaults from server/helpers.py to config/ingredient_defaults.py
- Lazy-init _BUNDLED_SKILL_NAMES via lru_cache in rules_skills.py
- Add exc_info=True to merge_queue.py warning logs
- Refactor version-bump.yml /tmp/ payload to GITHUB_OUTPUT
- Refactor clone_isolation_repo fixture to session scope
- Extract _WARN_MARK constant in pretty_output.py
- Add tests: field parity, GraphQL errors, lazy init, config import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add autoskillit.config to allowed imports for server/tools_*.py (L1, same as pipeline)
- Update workflow permission tests to check job-level instead of workflow-level
- Fix mock patch paths in test_mcp_overrides.py for relocated function
- Fix test_helpers.py imports to use new config location
- Fix test_dual_validation assertion to match actual validator output

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

- Initialize _plugin_check_detail before try block in _doctor.py to prevent
  UnboundLocalError if subprocess raises unexpected exception type
- Remove resolve_ingredient_defaults re-export from server/__init__.py (now
  canonical at autoskillit.config)
- Update monkeypatch paths in test_cli_prompts.py and test_mcp_overrides.py

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

config/ is L1 and cannot import from execution/ (also L1). Replace the
cross-L1 import of REMOTE_PRECEDENCE with a local constant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Trecek Trecek merged commit e7ff9c0 into main Mar 15, 2026
2 checks passed
Trecek added a commit that referenced this pull request Mar 15, 2026
Manual version bump — the version-bump workflow could not trigger for
PR #404 because the workflow file was delivered by that same merge
(chicken-and-egg). Future integration→main merges will auto-bump.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
github-merge-queue bot pushed a commit that referenced this pull request Mar 17, 2026
… Analysis (#421)

## Summary

Enhance `open-integration-pr` to produce rich, structured PR
descriptions by partitioning the integration diff into 7 canonical
domains and launching parallel Task subagents to analyze each. Each
domain subagent catalogs commits, contributing PRs, and key changes
within its area. The results are compiled into a `## Domain Analysis`
section in the PR body, following the PR #404 template structure. A new
pure Python function `partition_files_by_domain()` is added to
`pipeline/pr_gates.py` to make domain assignment deterministic and
unit-testable.

## Requirements

### DIFF — Diff Domain Partitioning

- **REQ-DIFF-001:** The system must partition a git diff between two
branches into N independent analysis domains based on directory
structure.
- **REQ-DIFF-002:** The system must enumerate all commits between the
base and head branches and extract PR numbers from commit messages.
- **REQ-DIFF-003:** Each analysis domain must be scoped to a set of file
path patterns so sub-agents receive only their relevant portion of the
diff.

### AGENT — Parallel Sub-Agent Analysis

- **REQ-AGENT-001:** The system must launch 7 or more sub-agents
concurrently, each analyzing a distinct diff domain.
- **REQ-AGENT-002:** Each sub-agent must produce a structured
bullet-point summary covering all changes in its domain, with PR number
references.
- **REQ-AGENT-003:** Sub-agent domains must collectively cover the
entire diff with no gaps and minimal overlap.

### BODY — PR Description Assembly

- **REQ-BODY-001:** The system must compile sub-agent outputs into a
single structured markdown PR body.
- **REQ-BODY-002:** The compiled body must include: summary with stats,
major features, new tools/skills tables, per-area changelogs,
consolidated PR list, and test coverage summary.
- **REQ-BODY-003:** The output format must follow the PR #404 template
structure as the exemplar.
- **REQ-BODY-004:** The system must apply the generated description to
the target PR via GitHub API.

### INTEG — Pipeline Integration

- **REQ-INTEG-001:** The capability must be invocable as a standalone
skill or as a step within the merge-prs recipe pipeline.
- **REQ-INTEG-002:** The system must integrate with or extend the
existing open-integration-pr skill without breaking its current
functionality.

## Architecture Impact

### Module Dependency Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
graph TB
    %% CLASS DEFINITIONS %%
    classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;
    classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff;
    classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff;
    classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;

    subgraph L1_Pipeline ["● L1 — pipeline/ (modified)"]
        direction TB
        INIT["● pipeline/__init__.py<br/>━━━━━━━━━━<br/>Re-exports public surface<br/>+ DOMAIN_PATHS<br/>+ partition_files_by_domain"]
        PRGATES["● pipeline/pr_gates.py<br/>━━━━━━━━━━<br/>is_ci_passing, is_review_passing<br/>partition_prs<br/>★ DOMAIN_PATHS (new)<br/>★ partition_files_by_domain (new)"]
    end

    subgraph L1_Other ["L1 — pipeline/ (unchanged)"]
        direction LR
        AUDIT["pipeline/audit.py"]
        GATE["pipeline/gate.py"]
        TOKENS["pipeline/tokens.py"]
        CONTEXT["pipeline/context.py"]
    end

    subgraph L0_Core ["L0 — core/ (unchanged)"]
        CORE["core/__init__.py<br/>━━━━━━━━━━<br/>FailureRecord<br/>is_protected_branch"]
    end

    subgraph Skills ["● Skills (modified)"]
        SKILL["● open-integration-pr/SKILL.md<br/>━━━━━━━━━━<br/>Workflow doc — new Steps 4c–4g<br/>partition_files_by_domain usage<br/>7-domain parallel analysis"]
    end

    subgraph Tests ["★ Tests (new)"]
        TPART["★ tests/test_pr_domain_partitioner.py<br/>━━━━━━━━━━<br/>Unit tests for<br/>DOMAIN_PATHS + partition_files_by_domain"]
        TSKILL["★ tests/skills/test_open_integration_pr_domain_analysis.py<br/>━━━━━━━━━━<br/>SKILL.md contract tests<br/>(reads SKILL.md via Path)"]
    end

    %% VALID DOWNWARD DEPENDENCIES %%
    INIT -->|"imports from"| PRGATES
    INIT -->|"imports from"| AUDIT
    INIT -->|"imports from"| GATE
    INIT -->|"imports from"| TOKENS
    INIT -->|"imports from"| CONTEXT
    CONTEXT -->|"imports from"| CORE
    PRGATES -.->|"stdlib only<br/>(no autoskillit imports)"| EXT

    %% TEST IMPORTS %%
    TPART -->|"direct import"| PRGATES
    TSKILL -->|"reads via Path"| SKILL

    %% EXTERNAL %%
    EXT["stdlib / external<br/>━━━━━━━━━━<br/>from __future__ import annotations"]

    %% CLASS ASSIGNMENTS %%
    class INIT phase;
    class PRGATES stateNode;
    class AUDIT,GATE,TOKENS,CONTEXT handler;
    class CORE cli;
    class SKILL output;
    class TPART,TSKILL newComponent;
    class EXT integration;
```

### Process Flow Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, '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 newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;

    START([START])
    COMPLETE([COMPLETE])

    subgraph Init ["Phase 1-3 — Initialization"]
        direction TB
        A1["Parse Args<br/>━━━━━━━━━━<br/>integration_branch, base_branch<br/>pr_order_file, audit_verdict"]
        A2["Read pr_order_file<br/>━━━━━━━━━━<br/>pr_list: number, title, branch<br/>complexity, files_changed"]
        A3["Fetch Closes/Fixes Refs<br/>━━━━━━━━━━<br/>gh pr view N --json body<br/>closing_refs list"]
    end

    subgraph Collect ["Phase 4-4b — Data Collection"]
        direction TB
        B1["Get Changed Files<br/>━━━━━━━━━━<br/>git diff --name-only<br/>changed_files, new_files, modified_files"]
        B2["Load Conflict Reports<br/>━━━━━━━━━━<br/>Read conflict_report_path_list<br/>conflict_resolution_table"]
    end

    subgraph DomainPrep ["★ Phase 4c-4f — Domain Preparation NEW"]
        direction TB
        C1["★ ● partition_files_by_domain<br/>━━━━━━━━━━<br/>python3 pipeline.pr_gates<br/>changed_files → domain_partitions"]
        C2{"any non-empty<br/>domains?"}
        C3["★ Fetch Domain Diffs parallel<br/>━━━━━━━━━━<br/>git diff per domain<br/>truncate at 12 000 chars<br/>domain_diffs dict"]
        C4["★ Identify PRs per Domain<br/>━━━━━━━━━━<br/>cross-ref domain_partitions<br/>vs pr.files_changed<br/>domain_pr_numbers dict"]
        C5["★ Fetch Domain Commits parallel<br/>━━━━━━━━━━<br/>git log --oneline per domain<br/>domain_commits dict"]
    end

    subgraph DomainAnalysis ["★ Phase 4g — Parallel Domain Subagents NEW"]
        direction LR
        D1["★ Server MCP Tools<br/>━━━━━━━━━━<br/>Task sonnet"]
        D2["★ Pipeline Execution<br/>━━━━━━━━━━<br/>Task sonnet"]
        D3["★ Recipe Validation<br/>━━━━━━━━━━<br/>Task sonnet"]
        D4["★ CLI Workspace<br/>━━━━━━━━━━<br/>Task sonnet"]
        D5["★ Skills<br/>━━━━━━━━━━<br/>Task sonnet"]
        D6["★ Tests<br/>━━━━━━━━━━<br/>Task sonnet"]
        D7["★ Core Config Infra<br/>━━━━━━━━━━<br/>Task sonnet"]
        D8["★ Collect Summaries<br/>━━━━━━━━━━<br/>Parse JSON per subagent<br/>domain_summaries list"]
    end

    subgraph ArchViz ["Phase 5-6 — Architecture Visualization"]
        direction TB
        E1["Select Arch-Lens Lenses<br/>━━━━━━━━━━<br/>Task subagent 1-3 lenses"]
        E2["Generate Diagrams<br/>━━━━━━━━━━<br/>Skill tool per lens<br/>validate markers<br/>validated_diagrams"]
    end

    subgraph Assembly ["● Phase 7-9 — Assembly and Publication"]
        direction TB
        F1["● Compose PR Body<br/>━━━━━━━━━━<br/>Merged PRs table<br/>★ Domain Analysis section<br/>Arch diagrams, Closes refs"]
        F2{"gh auth<br/>available?"}
        F3["Create Integration PR<br/>━━━━━━━━━━<br/>gh pr create --body-file<br/>new_pr_url"]
        F4["Close Original PRs<br/>━━━━━━━━━━<br/>gh pr close N --comment"]
        F5["Output pr_url"]
    end

    START --> A1 --> A2 --> A3
    A3 --> B1 --> B2
    B2 --> C1
    C1 --> C2
    C2 -->|"yes"| C3
    C2 -->|"no files"| E1
    C3 --> C4 --> C5
    C5 --> D1 & D2 & D3 & D4 & D5 & D6 & D7
    D1 & D2 & D3 & D4 & D5 & D6 & D7 --> D8
    D8 --> E1
    E1 --> E2
    E2 --> F1
    F1 --> F2
    F2 -->|"yes"| F3 --> F4 --> F5
    F2 -->|"no"| F5
    F5 --> COMPLETE

    %% CLASS ASSIGNMENTS %%
    class START,COMPLETE terminal;
    class A1 phase;
    class A2,A3,B1,B2 handler;
    class C1,C3,C4,C5 newComponent;
    class C2 detector;
    class D1,D2,D3,D4,D5,D6,D7,D8 newComponent;
    class E1,E2 handler;
    class F1 stateNode;
    class F2 detector;
    class F3,F4 handler;
    class F5 output;
```

### Concurrency Diagram

```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, '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 output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;

    START([START])
    COMPLETE([COMPLETE])

    subgraph Sequential ["MAIN SESSION — Sequential Preparation"]
        direction TB
        S1["● partition_files_by_domain<br/>━━━━━━━━━━<br/>python3 pipeline.pr_gates<br/>→ domain_partitions<br/>(single call, deterministic)"]
        S2["Check non-empty domains<br/>━━━━━━━━━━<br/>skip if changed_files empty"]
    end

    subgraph Fan1 ["★ FANOUT 1 — Domain Diffs parallel"]
        direction LR
        DD1["★ git diff<br/>Server/MCP Tools<br/>→ diff text"]
        DD2["★ git diff<br/>Pipeline/Exec<br/>→ diff text"]
        DD3["★ git diff<br/>Recipe/Valid<br/>→ diff text"]
        DD4["★ git diff<br/>CLI/Workspace<br/>→ diff text"]
        DD5["★ git diff<br/>Skills<br/>→ diff text"]
        DD6["★ git diff<br/>Tests<br/>→ diff text"]
        DD7["★ git diff<br/>Core/Config<br/>→ diff text"]
    end

    subgraph Barrier1 ["★ BARRIER 1 — Collect diffs + truncate"]
        B1["★ Collect domain_diffs<br/>━━━━━━━━━━<br/>truncate at 12 000 chars<br/>drop empty domains"]
    end

    subgraph Fan2 ["★ FANOUT 2 — Domain Commits parallel"]
        direction LR
        DC1["★ git log<br/>Server<br/>--oneline"]
        DC2["★ git log<br/>Pipeline<br/>--oneline"]
        DC3["★ git log<br/>Recipe<br/>--oneline"]
        DC4["★ git log<br/>CLI<br/>--oneline"]
        DC5["★ git log<br/>Skills<br/>--oneline"]
        DC6["★ git log<br/>Tests<br/>--oneline"]
        DC7["★ git log<br/>Core<br/>--oneline"]
    end

    subgraph Barrier2 ["★ BARRIER 2 — Collect domain_commits"]
        B2["★ domain_commits dict<br/>━━━━━━━━━━<br/>list of sha+msg per domain"]
    end

    subgraph Fan3 ["★ FANOUT 3 — Parallel Domain Subagents NEW"]
        direction LR
        DA1["★ Task:sonnet<br/>Server/MCP Tools<br/>━━━━━━━━━━<br/>reads: domain diff<br/>commits, pr_numbers<br/>writes: JSON result"]
        DA2["★ Task:sonnet<br/>Pipeline/Execution<br/>━━━━━━━━━━<br/>reads: domain diff<br/>commits, pr_numbers<br/>writes: JSON result"]
        DA3["★ Task:sonnet<br/>Recipe/Validation<br/>━━━━━━━━━━<br/>reads: domain diff<br/>commits, pr_numbers<br/>writes: JSON result"]
        DA4["★ Task:sonnet<br/>CLI/Workspace<br/>━━━━━━━━━━<br/>reads: domain diff<br/>commits, pr_numbers<br/>writes: JSON result"]
        DA5["★ Task:sonnet<br/>Skills<br/>━━━━━━━━━━<br/>reads: domain diff<br/>commits, pr_numbers<br/>writes: JSON result"]
        DA6["★ Task:sonnet<br/>Tests<br/>━━━━━━━━━━<br/>reads: domain diff<br/>commits, pr_numbers<br/>writes: JSON result"]
        DA7["★ Task:sonnet<br/>Core/Config/Infra<br/>━━━━━━━━━━<br/>reads: domain diff<br/>commits, pr_numbers<br/>writes: JSON result"]
    end

    subgraph Barrier3 ["★ BARRIER 3 — Parse and Collect Summaries"]
        B3["★ Collect domain_summaries<br/>━━━━━━━━━━<br/>JSON parse each result<br/>log warning on parse fail<br/>→ domain_summaries list"]
    end

    subgraph Downstream ["● Downstream — Assembly"]
        DS["● Compose PR Body<br/>━━━━━━━━━━<br/>insert Domain Analysis section<br/>ordered by domain name"]
    end

    START --> S1 --> S2
    S2 -->|"non-empty domains"| DD1 & DD2 & DD3 & DD4 & DD5 & DD6 & DD7
    DD1 & DD2 & DD3 & DD4 & DD5 & DD6 & DD7 --> B1
    B1 --> DC1 & DC2 & DC3 & DC4 & DC5 & DC6 & DC7
    DC1 & DC2 & DC3 & DC4 & DC5 & DC6 & DC7 --> B2
    B2 --> DA1 & DA2 & DA3 & DA4 & DA5 & DA6 & DA7
    DA1 & DA2 & DA3 & DA4 & DA5 & DA6 & DA7 --> B3
    B3 --> DS --> COMPLETE

    %% CLASS ASSIGNMENTS %%
    class START,COMPLETE terminal;
    class S1,S2 phase;
    class DD1,DD2,DD3,DD4,DD5,DD6,DD7 newComponent;
    class B1,B2,B3 detector;
    class DC1,DC2,DC3,DC4,DC5,DC6,DC7 newComponent;
    class DA1,DA2,DA3,DA4,DA5,DA6,DA7 newComponent;
    class DS stateNode;
    class B2 detector;
```

Closes #408

## Implementation Plan

Plan file:
`temp/make-plan/auto_generate_rich_integration_pr_descriptions_plan_2026-03-16_203049.md`

## Token Usage Summary

No token summary available

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Trecek added a commit that referenced this pull request Mar 19, 2026
PR #404 was squash-merged into main, breaking the merge topology between
integration and main. This merge absorbs main's 2 commits (the squash +
version bump) into integration's history, establishing proper ancestry
so future integration → main merges work without conflicts.

No content changes — integration's tree is preserved exactly.

Co-Authored-By: Claude Opus 4.6 (1M context) <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