From 54c67f7cd7ace3269eee4fe628f974b090a4d699 Mon Sep 17 00:00:00 2001 From: Scott Haug Date: Tue, 21 Jul 2026 22:30:22 -0700 Subject: [PATCH 1/2] feat: integrate carved ticket publication ## Summary - Add an authority-gated carved-stack publication path to `implement-ticket` - Pass decomposition authority and verify plural child results in `implement-epic` - Add raw-artifact evaluations for oversized, split-rubric, redesign, and stack-verification cases ## Why - Keep oversized coherent ticket candidates reviewable without silently publishing monolithic PRs - Preserve one lifecycle owner per PR and the existing acyclic suite boundaries --- CHANGELOG.md | 4 +- README.md | 15 +- skills/implement-epic/SKILL.md | 67 ++++--- skills/implement-epic/evals/cases.json | 6 + skills/implement-epic/evals/expectations.json | 5 + skills/implement-epic/references/github.md | 6 +- skills/implement-epic/references/linear.md | 6 +- .../tests/test_orchestration_contract.py | 17 +- skills/implement-ticket/SKILL.md | 186 ++++++++++++------ skills/implement-ticket/agents/claude-code.md | 10 +- skills/implement-ticket/agents/openai.yaml | 4 +- skills/implement-ticket/evals/cases.json | 25 +++ .../implement-ticket/evals/expectations.json | 20 ++ .../implement-ticket/evals/forward_cases.json | 90 +++++++++ .../evals/forward_expectations.json | 35 ++++ .../references/babysit-pr-handoff.md | 13 +- .../references/carve-changesets-handoff.md | 131 ++++++++++++ .../references/cleanup-and-result.md | 55 ++++-- skills/implement-ticket/references/github.md | 47 +++-- skills/implement-ticket/references/linear.md | 7 +- .../references/review-and-merge-gates.md | 54 ++--- .../scripts/evals/claude_executor.py | 16 ++ .../scripts/evals/fixture_executor.py | 66 ++++++- .../scripts/tests/test_forward_evals.py | 23 ++- .../tests/test_implement_ticket_contract.py | 42 +++- 25 files changed, 762 insertions(+), 188 deletions(-) create mode 100644 skills/implement-ticket/references/carve-changesets-handoff.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 4085241..fb06a4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,11 @@ summary: Chronological history of repository and skill changes. # Changelog -## 2026-07-21 — Completed carve-changesets skill and verification suite +## 2026-07-21 — Completed carve-changesets and integrated ticket publication +- feat: integrate carved ticket publication - refactor: derive the eval action vocabulary from expectations + (`e30b5f1021538d673eb931b2978287cfd21ae4ae`) - fix: require the two-part source freshness override (`eb8612300d75d1483995677d75f54fe1a32b60d7`) - test: complete the carve-changesets verification suite diff --git a/README.md b/README.md index ecf6eda..51a6958 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,10 @@ Current reusable agent skills: explicitly authorized completion policy - `skills/implement-ticket` — implement exactly one standalone ticket or named epic child through isolated execution and initial repository-owned review, - delegate the published PR lifecycle to `babysit-pr`, then verify tracker, - mainline, and cleanup outcomes; this is the canonical owner of generic - single-ticket execution rules consumed by `implement-epic` + publish one ordinary PR through `babysit-pr` or an explicitly authorized + carved stack through `carve-changesets`, then verify tracker, mainline, and + cleanup outcomes; this is the canonical owner of generic single-ticket + execution rules consumed by `implement-epic` - `skills/implement-epic` — traverse live GitHub or Linear epic graphs and delegate each selected child to `implement-ticket`, then refresh graph state and verify separately authorized epic closeout @@ -40,8 +41,12 @@ The composed implementation dependency chain is: implement-epic └── implement-ticket ├── review-code-change # initial candidate review - └── babysit-pr # published PR lifecycle - └── review-code-change # after a head-changing fix + ├── babysit-pr # ordinary single-PR lifecycle + │ └── review-code-change # after a head-changing fix + └── carve-changesets # authority-gated oversized path + ├── review-code-change # each exact changeset + └── babysit-pr # each changeset PR lifecycle + └── review-code-change # after a head-changing fix carve-changesets ├── review-code-change # direct per-changeset review diff --git a/skills/implement-epic/SKILL.md b/skills/implement-epic/SKILL.md index e1a5735..18b53c4 100644 --- a/skills/implement-epic/SKILL.md +++ b/skills/implement-epic/SKILL.md @@ -11,15 +11,16 @@ Orchestrate the live work graph. Delegate each selected child to ## Require the ticket skill Before an epic run, verify that `implement-ticket` is available, readable, and -supports `ready_pr`, `merged`, `blocked`, and `requires_epic` terminal results. -Return `blocked` before mutation when the dependency or result contract is -missing or untrustworthy. Do not substitute a generic implementation agent, -inline a copy of the ticket workflow, or weaken any gate. +supports `ready_pr`, `ready_prs`, `merged`, `blocked`, and `requires_epic` +terminal results. Return `blocked` before mutation when the dependency or result +contract is missing or untrustworthy. Do not substitute a generic implementation +agent, inline a copy of the ticket workflow, or weaken any gate. `implement-ticket` owns ticket readiness, isolated implementation, validation, -`review-code-change`, PR state, remote gates, merge, tracker transition, per-PR -cleanup, and terminal evidence. Do not invoke individual review lenses or -`review-code-change` directly from this skill. +`review-code-change`, publication-path selection, PR or stack state, remote +gates, merge, tracker transition, per-candidate cleanup, and terminal evidence. +Do not invoke individual review lenses, `review-code-change`, `babysit-pr`, or +`carve-changesets` directly from this skill. ## Require compatible runtime capabilities @@ -38,11 +39,12 @@ A compatible agentic runtime must be able to: taking ownership of local review. The portable dependency chain is `implement-epic` → `implement-ticket` → -(`review-code-change`, `babysit-pr`), with `babysit-pr` → `review-code-change` -after a head-changing fix. Verify `implement-ticket` directly and require its -result to prove that its own dependencies and applicable capabilities were -available. Do not make this skill invoke `review-code-change` or `babysit-pr` -itself. +(`review-code-change`, `babysit-pr`, `carve-changesets`), with +`carve-changesets` → `babysit-pr` per changeset and `babysit-pr` → +`review-code-change` after a head-changing fix. Verify `implement-ticket` +directly and require its result to prove that its own dependencies and +applicable capabilities were available. Do not make this skill invoke +`review-code-change`, `babysit-pr`, or `carve-changesets` itself. Stop before child mutation with an explicit limitation when an applicable capability or dependency is unavailable. Product-specific discovery metadata @@ -76,13 +78,17 @@ Before selecting work, discover or receive and verify: - serial execution by default, with parallel execution only when explicitly authorized and proven non-overlapping; and - authority for child execution, merge, manual transitions, graph edits, - follow-up creation, branch deletion, parent closeout, deployment, production + follow-up creation, decomposition of an oversized coherent candidate into a + stacked chain, branch deletion, parent closeout, deployment, production mutation, and destructive operations. -Pass authority into `implement-ticket` without expansion. Ready-PR authority -does not imply merge. Child merge authority does not imply parent closeout. -Words such as `finish`, `complete`, or `end to end` do not independently grant -merge, graph mutation, deployment, or closeout authority. +Pass authority into `implement-ticket` without expansion. The +`decompose oversized candidates into stacked changesets` grant is off by default +and must be passed through verbatim; this skill gains no decomposition +mechanics. Ready-PR authority does not imply merge. Child merge authority does +not imply parent closeout. Words such as `finish`, `complete`, or `end to end` +do not independently grant decomposition, merge, graph mutation, deployment, or +closeout authority. Use this source order: @@ -120,6 +126,13 @@ outcome exists in its authoritative repository, artifact registry, tracker, or environment. Treat canceled or not-planned blockers with missing required outcomes as unresolved. +Prevent predictable oversizing here. When the live ticket already describes +independently valuable and trackable parts too large for one child, route it to +tracker-level decomposition before invoking `implement-ticket`; do not use +`carve-changesets` to compensate for a known non-PR-sized child. The carved +publication path is reserved for one coherent child whose completed +implementation turns out materially larger than the live guardrails predicted. + When multiple children are ready, prefer contracts and additive foundations before consumers or cutovers, then prefer the child that unlocks the most downstream work without widening scope. Do not absorb a missing sibling outcome @@ -133,7 +146,8 @@ Invoke `implement-ticket` once with a concise handoff containing: - parent outcome and only the dependency/sibling evidence needed for safe independent shipping; - repository, PR host, base, and named specifications; -- completion policy and every granted or withheld authority; and +- completion policy and every granted or withheld authority; +- the explicit decomposition grant or its explicit absence; and - any epic-level rollout or merge-order constraint that qualifies the child. The primary context may follow `implement-ticket` directly. A delegated worker, @@ -153,8 +167,15 @@ transition, and cleanup evidence are internally consistent and match live state. current-candidate non-merge gate with only merge withheld. Do not count the child complete or unblock dependents that require merge. Continue only with another independently ready child when the requested scope permits it. +- `ready_prs`: verify the reported PR count, ordered predecessor-base topology, + final-only closing syntax, per-PR candidate and non-merge gate evidence, and + whole-chain equivalence with the ticket candidate. Do not count the child + complete or unblock dependents that require merge. - `merged`: verify mainline and tracker evidence, then refresh the complete live - graph before any selection or completion claim. + graph before any selection or completion claim. For a stacked child, also + verify `all_merged`, every PR merge and propagation step, and full-chain + representation on the base. Do not reproduce decomposition or propagation + mechanics while verifying the result. - `blocked`: preserve the exact reason and partial artifacts. Never count it as complete. Select another independently ready child only when the requested scope permits; otherwise stop for the missing decision, outcome, or @@ -206,7 +227,7 @@ Difficulty, ordinary CI wait time, or unrelated ready children are not blockers. ## Report the epic result Report the requested scope, each invoked ticket and its terminal state, merged -and ready PRs, refreshed graph state, serial critical-path and parallel-ready -work, parent acceptance and closeout evidence, intentionally deferred work, and -one concrete next action. Never report a child or parent complete from stale or -unverified evidence. +and ready PRs or stacks, refreshed graph state, serial critical-path and +parallel-ready work, parent acceptance and closeout evidence, intentionally +deferred work, and one concrete next action. Never report a child or parent +complete from stale or unverified evidence. diff --git a/skills/implement-epic/evals/cases.json b/skills/implement-epic/evals/cases.json index d29cb62..dd7d492 100644 --- a/skills/implement-epic/evals/cases.json +++ b/skills/implement-epic/evals/cases.json @@ -107,5 +107,11 @@ "request": "Continue epic G-270 across three independently selected children.", "graph": "Each child is selected only when independently ready; implement-ticket owns all direct dependencies.", "ticket_results": ["G-271 ready_pr at the complete non-merge gate", "G-272 merged with verified mainline and transition evidence", "G-273 blocked with preserved artifacts"] + }, + { + "id": "verify-stacked-child-result", + "request": "Continue epic G-280 after authorized child G-281 returns a three-PR carved result.", + "graph": "G-282 is blocked until G-281 is fully merged.", + "ticket_results": ["G-281 reports all_merged with three ordered PRs, per-PR gate evidence, propagation evidence, and full-chain representation on main"] } ] diff --git a/skills/implement-epic/evals/expectations.json b/skills/implement-epic/evals/expectations.json index 4fbe518..8f34db1 100644 --- a/skills/implement-epic/evals/expectations.json +++ b/skills/implement-epic/evals/expectations.json @@ -88,5 +88,10 @@ "case_id": "transitive-babysit-results", "workflow_state": "mixed_ticket_results", "required_actions": ["consume ready_pr merged and blocked unchanged", "do not invoke babysit-pr directly", "do not count ready_pr or blocked as merged", "refresh graph only after verified merged"] + }, + { + "case_id": "verify-stacked-child-result", + "workflow_state": "stack_child_verified", + "required_actions": ["verify stack topology and every PR gate", "verify all_merged and full-chain representation on base", "do not invoke carve-changesets directly", "refresh graph before considering G-282"] } ] diff --git a/skills/implement-epic/references/github.md b/skills/implement-epic/references/github.md index 63712f1..e756bd4 100644 --- a/skills/implement-epic/references/github.md +++ b/skills/implement-epic/references/github.md @@ -27,8 +27,10 @@ not-planned blockers with missing outcomes as unresolved. After a returned `merged` result, verify the GitHub issue transition and reread the complete parent/sub-issue/blocker graph before selecting another child or -claiming completion. A `ready_pr` result does not satisfy a dependency that -requires merge. +claiming completion. A `ready_pr` or `ready_prs` result does not satisfy a +dependency that requires merge. For a stacked `merged` result, verify the +reported topology, every PR merge, and full-chain representation on the base +without taking ownership of decomposition mechanics. When duplicate implementation paths exist, do not choose a competing path; pass the canonical ownership evidence into `implement-ticket` or return `blocked` diff --git a/skills/implement-epic/references/linear.md b/skills/implement-epic/references/linear.md index fc055c5..c16b733 100644 --- a/skills/implement-epic/references/linear.md +++ b/skills/implement-epic/references/linear.md @@ -25,8 +25,10 @@ prerequisite outcomes. Treat canceled or not-planned blockers with missing outcomes as unresolved. After `implement-ticket` returns `merged`, verify the expected Linear transition -and reread the complete epic relationship state. Do not treat `ready_pr` as a -completed child or unblock a dependent that requires merge. +and reread the complete epic relationship state. For a stacked result, also +verify every PR merge and full-chain representation on the base. Do not treat +`ready_pr` or `ready_prs` as a completed child or unblock a dependent that +requires merge. ## Separate tracker and PR host diff --git a/skills/implement-epic/scripts/tests/test_orchestration_contract.py b/skills/implement-epic/scripts/tests/test_orchestration_contract.py index a51a591..a86295d 100644 --- a/skills/implement-epic/scripts/tests/test_orchestration_contract.py +++ b/skills/implement-epic/scripts/tests/test_orchestration_contract.py @@ -62,19 +62,26 @@ def test_product_neutral_runtime_contract(self): def test_dependency_chain_is_stable_and_acyclic(self): self.assertIn( "`implement-epic` → `implement-ticket` → " - "(`review-code-change`, `babysit-pr`)", + "(`review-code-change`, `babysit-pr`, `carve-changesets`)", self.contract, ) self.assertIn( - "Do not make this skill invoke `review-code-change` or `babysit-pr` itself", + "Do not make this skill invoke `review-code-change`, `babysit-pr`, or " + "`carve-changesets` itself", self.contract, ) self.assertIn("never recursively invoke this skill", self.contract) def test_child_terminal_states_are_stable(self): - for state in ("ready_pr", "merged", "blocked", "requires_epic"): + for state in ("ready_pr", "ready_prs", "merged", "blocked", "requires_epic"): self.assertIn(f"`{state}`", self.contract) + def test_epic_only_passes_authority_and_verifies_stack_results(self): + self.assertIn("off by default", self.contract) + self.assertIn("ordered predecessor-base topology", self.contract) + self.assertIn("full-chain representation on the base", self.contract) + self.assertIn("gains no decomposition mechanics", self.contract) + def test_epic_does_not_own_lens_mechanics(self): self.assertNotIn("review-solution-simplicity", self.contract) self.assertNotIn("review-correctness", self.contract) @@ -107,6 +114,10 @@ def test_eval_expectations_preserve_critical_boundaries(self): "missing-asynchronous-wait", ): self.assertEqual("blocked", self.expectations[case_id]["workflow_state"]) + self.assertEqual( + "stack_child_verified", + self.expectations["verify-stacked-child-result"]["workflow_state"], + ) if __name__ == "__main__": diff --git a/skills/implement-ticket/SKILL.md b/skills/implement-ticket/SKILL.md index b188913..ca855e2 100644 --- a/skills/implement-ticket/SKILL.md +++ b/skills/implement-ticket/SKILL.md @@ -1,6 +1,6 @@ --- name: implement-ticket -description: Implement, build, or fix exactly one standalone GitHub or Linear ticket or issue, or one named child of a larger epic, through an isolated branch and pull request. Use when asked to implement a ticket end to end; resolves live ticket and dependency context, enforces readiness and authority boundaries, implements and validates one coherent change, runs an initial repository-owned review, delegates the published PR lifecycle to babysit-pr, and verifies tracker, mainline, and cleanup outcomes. Detects whole-epic requests before mutation and routes them toward implement-epic without creating a circular skill dependency. +description: Implement, build, or fix exactly one standalone GitHub or Linear ticket or issue, or one named child of a larger epic, through an isolated candidate and either one pull request or an explicitly authorized carved stack. Use when asked to implement a ticket end to end; resolves live ticket and dependency context, enforces readiness and authority boundaries, implements and validates one coherent change, runs an initial repository-owned review, chooses the publication path from the live cognitive-load guardrails, delegates the published lifecycle, and verifies tracker, mainline, and cleanup outcomes. Detects whole-epic requests before mutation and routes them toward implement-epic without creating a circular skill dependency. --- # Implement Ticket @@ -10,11 +10,13 @@ claiming a parent epic is complete. Treat live tracker and repository evidence as execution state; use old plans or summaries only for orientation. Treat this skill as the canonical owner of generic single-ticket readiness, -implementation, initial review, PR publication, tracker transition, mainline -verification, cleanup, and terminal reporting. Delegate the post-publication PR -lifecycle to repository-owned `babysit-pr`. `implement-epic` consumes this -contract for each selected child. Do not copy either skill's rules back into -epic orchestration or create a third shared workflow abstraction. +implementation, initial review, publication-path selection, tracker transition, +mainline verification, cleanup, and terminal reporting. Delegate a normal PR's +post-publication lifecycle to repository-owned `babysit-pr`; delegate an +oversized candidate's decomposition and stacked lifecycle to repository-owned +`carve-changesets`. `implement-epic` consumes this contract for each selected +child. Do not copy any delegated skill's rules back into epic orchestration or +create a third shared workflow abstraction. ## Load the applicable references @@ -23,9 +25,12 @@ epic orchestration or create a third shared workflow abstraction. - Read [the Linear adapter](references/linear.md) whenever Linear owns ticket, parent, dependency, or status state. - Always read [review and merge gates](references/review-and-merge-gates.md) - before publishing the pull request. + before publishing the candidate. - Always read [the babysit-pr handoff](references/babysit-pr-handoff.md) before creating implementation state and again before transferring PR ownership. +- Read [the carve-changesets handoff](references/carve-changesets-handoff.md) + after the initial review whenever the size gate classifies the candidate as + oversized, and again before transferring candidate ownership. - Always read [cleanup and result](references/cleanup-and-result.md) before a merge or terminal handoff. @@ -40,6 +45,8 @@ A compatible agentic runtime must be able to: - load `implement-ticket`, repository-owned `review-code-change`, and repository-owned `babysit-pr` by stable skill name or an equivalent repository-owned dependency mechanism; +- load repository-owned `carve-changesets` by stable name at the publication + size gate so its live guardrails and optional handoff are available; - read repository instructions, tracker state, and structured relationships; - inspect and create isolated branch/worktree state; - edit files, run commands, commit, push, and manage PRs when authorized; @@ -72,26 +79,32 @@ Before mutation, discover or receive and verify: - required local, CI, human, connector, thread, build, integration, and manual validation gates; and - authority for ticket edits, dependency changes, follow-up creation, review - replies and resolution, merge, branch deletion, manual ticket transitions, - deployment, production mutation, and destructive operations. + replies and resolution, decomposition of an oversized candidate into stacked + changesets, merge, branch deletion, manual ticket transitions, deployment, + production mutation, and destructive operations. Use this default authority matrix unless the user or repository is stricter: - `ready PR only` permits isolated implementation, validation, commit, feature branch push, PR creation or update, evidence-based review replies, and resolution of fully addressed threads; -- `merge after gates` additionally permits merging this ticket's PR and safely - deleting its verified merged feature branch; +- `merge after gates` additionally permits merging this ticket's ordinary PR or + carved stack and safely deleting its verified merged feature branches; - `merge plus manual transition` additionally permits the explicitly requested status or close transition for this ticket only; +- `decompose oversized candidates into stacked changesets` permits an oversized + but coherent ticket candidate to be transferred to `carve-changesets`; it is + off by default and is independent of every completion policy; - ticket-body edits, dependency mutations, and follow-up creation require explicit ticket-management authority; and - deployment, production mutation, destructive data operations, and parent closure always require separate explicit authority. -Do not infer merge, issue-close, parent-close, deployment, or production -authority from words such as `implement`, `finish`, `complete`, or `end to end`. -When merge authority is unclear, stop at a ready PR. +Do not infer decomposition, merge, issue-close, parent-close, deployment, or +production authority from words such as `implement`, `finish`, `complete`, or +`end to end`. When merge authority is unclear, stop at a ready PR or ready PR +stack. When decomposition authority is absent, never silently publish an +oversized monolith or silently carve it. Treat an automatic ticket transition caused by the selected closing syntax as a disclosed consequence of authorized merge, not as an independently requested @@ -108,18 +121,24 @@ generic self-review, private PR loop, runtime download, or stranded unmonitored PR path. A whole-epic `requires_epic` result occurs before these ticket-only dependencies are invoked. -The dependency graph is deliberately acyclic: +The dependency graph is deliberately acyclic. The two publication paths are +mutually exclusive: ```text implement-epic └── implement-ticket ├── review-code-change # initial candidate review - └── babysit-pr # published PR lifecycle - └── review-code-change # after a head-changing fix + ├── babysit-pr # ordinary single-PR lifecycle + │ └── review-code-change # after a head-changing fix + └── carve-changesets # authority-gated oversized path + ├── review-code-change # each exact changeset + └── babysit-pr # each changeset PR lifecycle + └── review-code-change # after a head-changing fix ``` -`babysit-pr` must never invoke `implement-ticket`. Do not re-enter this skill -while consuming a babysitter result. +`babysit-pr` and `carve-changesets` must never invoke `implement-ticket`. +`carve-changesets` must never invoke `implement-epic`. Do not re-enter this +skill while consuming either delegated result. ## Establish source-of-truth precedence @@ -174,7 +193,9 @@ Proceed only when the selected ticket: behavior, and required verification; - contains no unresolved product, data, authorization, migration, destructive, or architecture decision; -- represents one coherent, independently reviewable PR; and +- represents one coherent candidate that is expected to fit one reviewable PR, + with the publication size gate reserved for implementations that turn out + materially larger than predicted; and - can merge without exposing incomplete, misleading, or unusable behavior. Treat a closed, canceled, or not-planned prerequisite as unresolved when its @@ -184,9 +205,10 @@ unimplemented sibling is required; never absorb that sibling into this PR. When an open canonical PR or branch already owns the ticket, return `blocked` with its identity and require explicit ownership transfer before modifying it; -do not report another worker's candidate as this run's `ready_pr`. When a merged -PR is verified on the base and the ticket is already complete, return `merged` -with that evidence without creating new implementation state. +do not report another worker's candidate as this run's `ready_pr` or +`ready_prs`. When a merged PR or stack is verified on the base and the ticket is +already complete, return `merged` with that evidence without creating new +implementation state. When ticket editing is authorized, make an unclear ticket implementation-ready and re-read it. Otherwise stop with the missing decision rather than @@ -201,7 +223,8 @@ improvising. - Create one feature branch and clean isolated worktree from the verified base, unless the current clean worktree is already the user's explicit ticket workspace. -- Use one ticket per branch, worktree, and PR. +- Use one ticket per candidate branch and worktree. Publication is either one PR + or one carved stack; never combine another ticket into either form. - Install documented dependencies and start required local services before classifying missing-tool failures as feature failures. @@ -238,17 +261,7 @@ Report commands and exact outcomes. Distinguish bootstrap or environment failures from feature failures. Do not claim completion while required validation is failing or unavailable. -### 4. Publish one focused PR - -- Recheck that no PR or branch already owns the ticket. -- Commit with repository conventions and push only the feature branch. -- Describe the branch-wide outcome, important non-goals, and actual validation. -- Use the owning tracker's correct closing or reference syntax. -- Confirm the PR base and head match the ticket worktree. - -Do not combine independently useful tickets in one PR. - -### 5. Run bounded repository-owned review +### 4. Run bounded repository-owned review Follow [review and merge gates](references/review-and-merge-gates.md). Keep every mutation in the implementation context. Give `review-code-change` only raw @@ -258,37 +271,76 @@ intended solution, prior conclusions, and suspected findings. Apply only material ticket-scoped blocking and strong-recommendation findings. Preserve deferred findings without expanding scope. After a fix, rerun affected -and required validation, commit and push a new head, rebuild the evidence, and -follow the suite's re-review instruction. Use at most three full fix/re-review -cycles by default. +and required validation, commit a new head, rebuild the evidence, and follow the +suite's re-review instruction. Use at most three full fix/re-review cycles by +default. Treat a missing dependency, malformed result, `blocked` verdict, reviewer mutation, or unavailable required evidence as a failed local gate. The review -suite stays read-only. This skill owns accepted fixes, commits, and pushes -during the initial review loop. Post-publication fixes, replies, thread -resolution, pushes, and merge belong to `babysit-pr` after explicit ownership -transfer; cleanup remains here. - -### 6. Delegate the published PR lifecycle - -Follow [the babysit-pr handoff](references/babysit-pr-handoff.md). After the -initial review loop is clean, reread the live PR, build the verified handoff, -and transfer exclusive mutation ownership to `babysit-pr` or follow it in the -same exclusive context. Do not maintain a second CI, feedback, base-drift, -post-fix review, or merge loop here. - -Map `ready PR only` to `ready_to_merge`. Map both merge policies to -`merge_when_ready`. Normal ticket execution never uses `watch_until_closed`. -Ordinary pending CI or review time is not a blocker; retain task ownership and -continue until the mapped policy reaches a terminal result or a genuine +suite stays read-only. This skill owns accepted fixes and commits during the +initial review loop, but withholds the first remote push until the publication +path is selected. Finish with every intended change committed, a clean worktree, +and a clean review bound to the exact candidate and base. + +### 5. Choose exactly one publication path + +After the candidate is complete, validated, committed, clean, and review-clean, +load `carve-changesets` by stable repository-owned name and read its live +normative cognitive-load guardrails. Do not copy their thresholds or substitute +local heuristics. Record the candidate-bound guardrail evidence and classify the +candidate before any remote publication. + +- When the candidate fits the guardrails, use the ordinary single-PR path. +- When it is oversized, decide whether the ticket should be split or the branch + should be carved. Prefer tracker-level ticket decomposition when the parts are + independently valuable and trackable. Prefer `carve-changesets` only when the + ticket remains one coherent deliverable whose implementation diff is simply + too large for one reviewable PR. +- The operator decides between those outcomes from the recorded evidence. When + the ticket should be split, or the decision is unresolved, stop before remote + publication with `blocked`; tracker-splitting mechanics are out of scope. +- An oversized coherent candidate may use the carved path only with the explicit + `decompose oversized candidates into stacked changesets` authority grant. + Without it, stop and ask or return `blocked` with the guardrail evidence. + +Recheck that no canonical PR, stack, or branch already owns the ticket. Never +publish both paths for one candidate. + +### 6. Publish and delegate the selected path + +For the ordinary path, push the candidate branch, open one focused PR, and +follow [the babysit-pr handoff](references/babysit-pr-handoff.md). Map +`ready PR only` to `ready_to_merge`; map both merge policies to +`merge_when_ready`. + +For the carved path, follow +[the carve-changesets handoff](references/carve-changesets-handoff.md) and +transfer the immutable source candidate to `carve-changesets`. Map +`ready PR only` to its publish boundary and `prs_open`; map both merge policies +to its merge-and-propagate boundary and `all_merged`. `implement-ticket` +performs no direct `babysit-pr` handoff, watcher, retry, feedback, fix, or merge +loop for any stack PR. Exactly one watcher owner exists per PR. + +In either path, describe the ticket-wide outcome, important non-goals, and +actual validation. Use the owning tracker's correct closing syntax on the one +ordinary PR or only on the final changeset PR. Intermediate stack PRs use a +non-closing reference and must remain behaviorally safe under the +`carve-changesets` equivalence contract. Verify the ticket transition only after +the ordinary PR merges or `carve-changesets` returns `all_merged`. + +Normal ticket execution never uses `watch_until_closed`. Ordinary pending CI or +review time is not a blocker; retain task ownership through the selected +delegate until its mapped policy reaches a terminal result or a genuine user-help-required condition occurs. Validate the returned identity and evidence against live GitHub state. After an -authorized merge, independently verify remote merge state, mainline -representation, and the owning tracker's ticket transition before cleanup. For -an epic child, reread affected native dependency relationships and report newly -unblocked work without selecting or mutating it. Never close or verify a parent -epic from this skill. +authorized ordinary merge or `all_merged`, independently verify remote merge +state, complete mainline representation, and the owning tracker's ticket +transition before cleanup. A mid-stack material redesign that invalidates an +earlier merged changeset returns `blocked`; never paper it over by mutating +merged history. For an epic child, reread affected native dependency +relationships and report newly unblocked work without selecting or mutating it. +Never close or verify a parent epic from this skill. ## Stop conditions @@ -310,12 +362,16 @@ sibling work is not a blocker. Follow [cleanup and result](references/cleanup-and-result.md). Return exactly one terminal state: -- `ready_pr`: the one-ticket PR is open and mergeable at the reported candidate, - every applicable current-candidate non-merge gate has passed, merge was - withheld, and this run owns or was explicitly handed ownership of the +- `ready_pr`: the ticket's one ordinary PR is open and mergeable at the reported + candidate, every applicable current-candidate non-merge gate has passed, merge + was withheld, and this run owns or was explicitly handed ownership of the candidate; -- `merged`: the PR is verified on the base, the ticket transition is verified, - and authorized cleanup is complete or precisely limited; +- `ready_prs`: the ticket's carved stack is open with verified topology and + every PR at its applicable non-merge gate, merge was withheld, and + `carve-changesets` returned current-candidate `prs_open` evidence; +- `merged`: either the ordinary PR is verified on the base or the full carved + stack is verified there after `all_merged`; in both cases the ticket + transition and authorized cleanup are verified; - `blocked`: give one concrete blocking reason and next action, preserving any partial artifacts; or - `requires_epic`: no mutation occurred and the handoff names `implement-epic` diff --git a/skills/implement-ticket/agents/claude-code.md b/skills/implement-ticket/agents/claude-code.md index d42f073..dab9a95 100644 --- a/skills/implement-ticket/agents/claude-code.md +++ b/skills/implement-ticket/agents/claude-code.md @@ -5,14 +5,14 @@ does not constrain the skill's portable contract. - Display name: Implement Ticket. - Suggested prompt: "Use the implement-ticket skill to implement this ticket, - run its initial review, delegate the published PR to babysit-pr, and verify - the authorized result." + run its initial review, choose the authorized single-PR or carved-stack + publication path, and verify the authorized result." - Isolated implementation state: create the ticket branch in a dedicated git worktree (for example via Claude Code's worktree support) owned exclusively by one mutating context. - Fresh read-only review context: invoke repository-owned `review-code-change` in a subagent (Agent tool) restricted to read-only tools, giving it only raw candidate evidence — never the implementation transcript. -- Long waits: follow babysit-pr's Claude adapter for CI and review monitoring; - ordinary pending CI is not a blocker, so keep the task alive with background - monitoring rather than returning early. +- Long waits: follow the selected delegate's monitoring contract; ordinary + pending CI is not a blocker, so keep the task alive with background monitoring + rather than returning early. diff --git a/skills/implement-ticket/agents/openai.yaml b/skills/implement-ticket/agents/openai.yaml index ba114b6..066bf75 100644 --- a/skills/implement-ticket/agents/openai.yaml +++ b/skills/implement-ticket/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Implement Ticket" - short_description: "Implement and babysit one ticket PR" - default_prompt: "Use $implement-ticket to implement this ticket, run its initial review, delegate the published PR to babysit-pr, and verify the authorized result." + short_description: "Implement and publish one ticket candidate" + default_prompt: "Use $implement-ticket to implement this ticket, run its initial review, choose the authorized single-PR or carved-stack publication path, and verify the authorized result." diff --git a/skills/implement-ticket/evals/cases.json b/skills/implement-ticket/evals/cases.json index d1c45ef..897a04f 100644 --- a/skills/implement-ticket/evals/cases.json +++ b/skills/implement-ticket/evals/cases.json @@ -120,5 +120,30 @@ "tracker_state": "G-41 is otherwise ready and requires asynchronous CI and review gates.", "authority": "merge_after_gates", "runtime_profile": "The runtime cannot poll, wait, or resume after asynchronous gates." + }, + { + "id": "oversized-without-decomposition-authority", + "request": "Implement GitHub issue G-42 to a ready publication.", + "tracker_state": "G-42 is one coherent ticket, but its completed validated review-clean candidate exceeds the live carve-changesets cognitive-load guardrails.", + "authority": "ready_pr_only; decomposition authority absent" + }, + { + "id": "oversized-authorized-carved-stack", + "request": "Implement GitHub issue G-43 as an authorized ready stack.", + "tracker_state": "G-43 remains one coherent deliverable; its validated review-clean candidate is oversized and the operator selects branch carving.", + "authority": "ready_pr_only plus decompose oversized candidates into stacked changesets", + "candidate_state": "carve-changesets returns prs_open for three correctly based, gated PRs with closing syntax only on the final PR" + }, + { + "id": "oversized-ticket-split-rubric", + "request": "Implement GitHub issue G-44.", + "tracker_state": "The completed candidate is oversized because it contains three independently valuable and trackable outcomes.", + "authority": "ticket-management and decomposition authority granted; operator selects tracker-level ticket splitting" + }, + { + "id": "mid-stack-material-redesign", + "request": "Finish the authorized carved stack for G-45.", + "authority": "merge_after_gates plus decompose oversized candidates into stacked changesets", + "candidate_state": "Changeset 1 is merged; material feedback on changeset 2 requires redesign that invalidates changeset 1." } ] diff --git a/skills/implement-ticket/evals/expectations.json b/skills/implement-ticket/evals/expectations.json index 061c55e..803498f 100644 --- a/skills/implement-ticket/evals/expectations.json +++ b/skills/implement-ticket/evals/expectations.json @@ -98,5 +98,25 @@ "case_id": "missing-asynchronous-wait", "terminal_state": "blocked", "required_actions": ["name asynchronous wait as unavailable", "perform no implementation mutation", "do not pretend CI or review gates passed"] + }, + { + "case_id": "oversized-without-decomposition-authority", + "terminal_state": "blocked", + "required_actions": ["record live guardrail evidence", "stop before remote publication", "publish neither monolith nor stack", "request explicit operator authority"] + }, + { + "case_id": "oversized-authorized-carved-stack", + "terminal_state": "ready_prs", + "required_actions": ["invoke carve-changesets with publish authority", "skip direct babysit-pr handoff", "verify ordered stack and per-PR gates", "put closing syntax on final PR only"] + }, + { + "case_id": "oversized-ticket-split-rubric", + "terminal_state": "blocked", + "required_actions": ["recommend tracker-level decomposition", "honor the operator decision", "stop before remote publication", "do not invoke carve-changesets"] + }, + { + "case_id": "mid-stack-material-redesign", + "terminal_state": "blocked", + "required_actions": ["preserve partial stack state", "report the invalidated merged changeset", "require product or architecture resolution", "do not rewrite merged history"] } ] diff --git a/skills/implement-ticket/evals/forward_cases.json b/skills/implement-ticket/evals/forward_cases.json index 4b9e515..32062bb 100644 --- a/skills/implement-ticket/evals/forward_cases.json +++ b/skills/implement-ticket/evals/forward_cases.json @@ -322,5 +322,95 @@ "worktree": {"path": null, "exclusive_owner": true, "tracked": [], "untracked": []}, "handoff": {"ticket_results": ["ready_pr", "merged", "blocked"], "result_well_formed": true} } + }, + { + "id": "oversized-without-decomposition-authority", + "target_skill": "implement-ticket", + "request": "Publish the completed G-323 candidate to readiness.", + "authority": {"merge": false, "decompose_oversized": false}, + "capabilities": {"review_code_change": true, "babysit_pr": true, "carve_changesets": true}, + "artifacts": { + "ticket": {"tracker": "github", "id": "G-323", "state": "open", "whole_epic": false}, + "repository": {"repo": "example/project", "tracker": "github", "pr_host": "github", "instructions": "one ticket per candidate"}, + "pr": {"state": "absent", "merged": false, "head": "head-323", "base": "base-1", "mergeable": null}, + "diff": {"base": "base-1", "head": "head-323", "patch": "large coherent candidate", "resulting_tree": "tree-323", "guardrail": "oversized"}, + "checks": {"status": "not_published", "items": []}, + "reviews": {"initial": "clean", "head": "head-323", "items": []}, + "threads": {"items": [], "unresolved": 0}, + "worktree": {"path": "/worktrees/g323", "exclusive_owner": true, "tracked": [], "untracked": []}, + "handoff": {"created": false, "result_well_formed": true, "rubric": "carve_coherent_branch"} + } + }, + { + "id": "oversized-authorized-carved-stack", + "target_skill": "implement-ticket", + "request": "Publish G-324 as an authorized ready carved stack without merging.", + "authority": {"merge": false, "decompose_oversized": true}, + "capabilities": {"review_code_change": true, "babysit_pr": true, "carve_changesets": true}, + "artifacts": { + "ticket": {"tracker": "github", "id": "G-324", "state": "open", "whole_epic": false}, + "repository": {"repo": "example/project", "tracker": "github", "pr_host": "github", "instructions": "closing syntax only on final stack PR"}, + "pr": {"state": "multiple_open", "merged": false, "head": "stack-tip-324", "base": "base-1", "mergeable": true}, + "diff": {"base": "base-1", "head": "head-324", "patch": "large coherent candidate", "resulting_tree": "tree-324", "guardrail": "oversized"}, + "checks": {"status": "success", "items": [{"pr": 1}, {"pr": 2}, {"pr": 3}]}, + "reviews": {"initial": "clean", "head": "head-324", "per_changeset": "clean", "items": []}, + "threads": {"items": [], "unresolved": 0}, + "worktree": {"path": "/worktrees/g324", "exclusive_owner": true, "tracked": [], "untracked": []}, + "handoff": {"created": true, "result_well_formed": true, "rubric": "carve_coherent_branch", "carve_terminal": "prs_open", "stack_count": 3, "topology": "verified", "closing_syntax": "final_only"} + } + }, + { + "id": "oversized-ticket-split-rubric", + "target_skill": "implement-ticket", + "request": "Publish the completed G-325 candidate.", + "authority": {"merge": false, "decompose_oversized": true, "ticket_management": true}, + "capabilities": {"review_code_change": true, "babysit_pr": true, "carve_changesets": true}, + "artifacts": { + "ticket": {"tracker": "github", "id": "G-325", "state": "open", "whole_epic": false}, + "repository": {"repo": "example/project", "tracker": "github", "pr_host": "github", "instructions": "split independently valuable outcomes at tracker level"}, + "pr": {"state": "absent", "merged": false, "head": "head-325", "base": "base-1", "mergeable": null}, + "diff": {"base": "base-1", "head": "head-325", "patch": "three independently valuable outcomes", "resulting_tree": "tree-325", "guardrail": "oversized"}, + "checks": {"status": "not_published", "items": []}, + "reviews": {"initial": "clean", "head": "head-325", "items": []}, + "threads": {"items": [], "unresolved": 0}, + "worktree": {"path": "/worktrees/g325", "exclusive_owner": true, "tracked": [], "untracked": []}, + "handoff": {"created": false, "result_well_formed": true, "rubric": "ticket_split", "operator_decision": "split_ticket"} + } + }, + { + "id": "mid-stack-material-redesign", + "target_skill": "implement-ticket", + "request": "Finish and merge the authorized G-326 carved stack.", + "authority": {"merge": true, "decompose_oversized": true}, + "capabilities": {"review_code_change": true, "babysit_pr": true, "carve_changesets": true}, + "artifacts": { + "ticket": {"tracker": "github", "id": "G-326", "state": "open", "whole_epic": false}, + "repository": {"repo": "example/project", "tracker": "github", "pr_host": "github", "instructions": "never rewrite merged history"}, + "pr": {"state": "partial_stack", "merged": false, "head": "stack-326-2", "base": "base-1", "mergeable": false}, + "diff": {"base": "base-1", "head": "head-326", "patch": "large coherent candidate", "resulting_tree": "tree-326", "guardrail": "oversized"}, + "checks": {"status": "blocked", "items": [{"pr": 1, "state": "merged"}, {"pr": 2, "state": "open"}]}, + "reviews": {"initial": "clean", "items": [{"pr": 2, "material_redesign": true, "invalidates_merged_pr": 1}]}, + "threads": {"items": [{"pr": 2, "unresolved": true}], "unresolved": 1}, + "worktree": {"path": "/worktrees/g326", "exclusive_owner": true, "tracked": [], "untracked": []}, + "handoff": {"created": true, "result_well_formed": true, "rubric": "carve_coherent_branch", "carve_terminal": "blocked", "mid_stack_redesign": true, "partial_stack": ["merged", "open", "unpublished"]} + } + }, + { + "id": "implement-epic-verifies-stacked-child", + "target_skill": "implement-epic", + "request": "Consume the merged three-PR result for child G-328 in epic G-327.", + "authority": {"merge": true, "decompose_oversized": true}, + "capabilities": {"implement_ticket": true, "babysit_pr": false, "carve_changesets": false}, + "artifacts": { + "ticket": {"tracker": "github", "id": "G-327", "state": "open", "whole_epic": true, "children": ["G-328", "G-329"]}, + "repository": {"repo": "example/project", "tracker": "github", "pr_host": "github", "instructions": "epic verifies ticket results without decomposition mechanics"}, + "pr": {"state": "multiple_merged", "merged": true, "head": "stack-tip-328", "base": "base-2", "mergeable": null}, + "diff": {"base": "base-2", "head": "stack-tip-328", "patch": "three changesets", "resulting_tree": "tree-328"}, + "checks": {"status": "success", "items": [{"pr": 1}, {"pr": 2}, {"pr": 3}]}, + "reviews": {"initial": "owned by ticket result", "per_changeset": "clean", "items": []}, + "threads": {"items": [], "unresolved": 0}, + "worktree": {"path": null, "exclusive_owner": true, "tracked": [], "untracked": []}, + "handoff": {"result_well_formed": true, "stack_child_result": "all_merged", "stack_count": 3, "topology": "verified", "full_chain_on_base": true, "per_pr_gates": "verified"} + } } ] diff --git a/skills/implement-ticket/evals/forward_expectations.json b/skills/implement-ticket/evals/forward_expectations.json index d6106d1..acabc20 100644 --- a/skills/implement-ticket/evals/forward_expectations.json +++ b/skills/implement-ticket/evals/forward_expectations.json @@ -236,5 +236,40 @@ ], "target_skill": "implement-epic", "terminal_state": "mixed_ticket_results" + }, + { + "case_id": "oversized-without-decomposition-authority", + "forbidden_actions": ["invoke_carve_changesets", "invoke_ready_to_merge", "invoke_merge_when_ready"], + "required_actions": ["record_guardrail_evidence", "stop_before_publication", "do_not_publish_monolithic_pr", "do_not_invoke_carve_changesets"], + "target_skill": "implement-ticket", + "terminal_state": "blocked" + }, + { + "case_id": "oversized-authorized-carved-stack", + "forbidden_actions": ["invoke_ready_to_merge", "invoke_merge_when_ready", "route_to_tracker_split"], + "required_actions": ["record_guardrail_evidence", "invoke_carve_changesets", "skip_direct_babysit_handoff", "place_closing_syntax_final_pr_only", "verify_stack_topology", "verify_each_pr_gate"], + "target_skill": "implement-ticket", + "terminal_state": "ready_prs" + }, + { + "case_id": "oversized-ticket-split-rubric", + "forbidden_actions": ["invoke_carve_changesets", "invoke_ready_to_merge", "invoke_merge_when_ready"], + "required_actions": ["record_guardrail_evidence", "route_to_tracker_split", "stop_before_publication", "do_not_invoke_carve_changesets"], + "target_skill": "implement-ticket", + "terminal_state": "blocked" + }, + { + "case_id": "mid-stack-material-redesign", + "forbidden_actions": ["invoke_merge_when_ready", "verify_merge_live"], + "required_actions": ["preserve_partial_stack", "report_mid_stack_redesign", "do_not_rewrite_merged_history"], + "target_skill": "implement-ticket", + "terminal_state": "blocked" + }, + { + "case_id": "implement-epic-verifies-stacked-child", + "forbidden_actions": ["invoke_carve_changesets", "invoke_ready_to_merge", "invoke_merge_when_ready"], + "required_actions": ["verify_stack_topology", "verify_each_pr_gate", "verify_full_stack_on_base", "do_not_own_decomposition_mechanics", "refresh_graph_after_merged_only"], + "target_skill": "implement-epic", + "terminal_state": "mixed_ticket_results" } ] diff --git a/skills/implement-ticket/references/babysit-pr-handoff.md b/skills/implement-ticket/references/babysit-pr-handoff.md index a652d31..73b5a65 100644 --- a/skills/implement-ticket/references/babysit-pr-handoff.md +++ b/skills/implement-ticket/references/babysit-pr-handoff.md @@ -6,6 +6,11 @@ evaluations, and result contract before delegation. If its delivered contract differs materially from this boundary, stop and reconcile ownership rather than copying lifecycle mechanics into `implement-ticket`. +This reference applies only to the ordinary single-PR publication path. When the +size gate selects a carved stack, use +[the carve-changesets handoff](carve-changesets-handoff.md) and perform no +direct `babysit-pr` handoff from `implement-ticket`. + ## Responsibility boundary `implement-ticket` retains ticket resolution and readiness, epic routing, @@ -26,10 +31,10 @@ handoff construction, result validation, and post-merge work. ## Pre-mutation dependency gate -Every successful ticket run publishes a PR that must be reconciled. Verify -`babysit-pr` and `review-code-change` by stable repository-owned name before -creating a branch or worktree. Missing `babysit-pr` returns `blocked` before -mutation; never download an external implementation at runtime, restore a +Every successful ticket run publishes at least one PR that must be reconciled. +Verify `babysit-pr` and `review-code-change` by stable repository-owned name +before creating a branch or worktree. Missing `babysit-pr` returns `blocked` +before mutation; never download an external implementation at runtime, restore a private copy of the old PR loop, or publish a PR that no owner will monitor. Whole-epic routing still happens before dependency invocation and returns diff --git a/skills/implement-ticket/references/carve-changesets-handoff.md b/skills/implement-ticket/references/carve-changesets-handoff.md new file mode 100644 index 0000000..4c8b72f --- /dev/null +++ b/skills/implement-ticket/references/carve-changesets-handoff.md @@ -0,0 +1,131 @@ +# Carve-changesets handoff and result mapping + +Use repository-owned `carve-changesets` as the sole owner of decomposing and +publishing an oversized ticket candidate. This path is selected only after the +complete candidate is validated, committed, worktree-clean, and clean under the +initial repository-owned `review-code-change` pass. + +Read the live `carve-changesets` skill, its +[normative contract](../../carve-changesets/references/SPEC.md), and its +[suite handoffs](../../carve-changesets/references/suite-handoffs.md) before +classifying candidate size or transferring ownership. Those sources remain +authoritative for guardrails, decomposition, equivalence, per-changeset review, +publication, lifecycle delegation, propagation, and terminal evidence. Do not +copy their thresholds or mechanics into `implement-ticket`. + +## Responsibility boundary + +`implement-ticket` retains ticket resolution and readiness, candidate +implementation, complete validation, the initial whole-candidate review, the +publication size gate, the split-ticket versus split-branch recommendation, +operator-decision enforcement, authority mapping, tracker semantics, terminal +result validation, mainline and ticket-transition verification, cleanup, and +reporting. + +After handoff, `carve-changesets` exclusively owns changeset planning, +materialization, chain identity, per-changeset validation and review, +equivalence, publication, each `babysit-pr` delegation, sequential merge and +downstream propagation, and its own authorized cleanup. `implement-ticket` must +not directly invoke `babysit-pr`, run a competing watcher, mutate a stack +branch, disposition stack feedback, or reproduce propagation mechanics. + +The reverse boundary is equally strict. `carve-changesets` does not change the +ticket contract, split tracker work, transition the ticket, verify an epic, +close a parent, or perform caller-owned mainline and tracker closeout. + +## Publication decision and authority gate + +Evaluate the exact clean candidate against the live +[cognitive-load guardrails](../../carve-changesets/references/SPEC.md#cognitive-load-guardrails). +Record the effective diff, semantic shape, mechanical exceptions, cohesive +intent, and independent-reviewability evidence. Never reduce this decision to a +duplicated numeric threshold. + +When the candidate is oversized, recommend tracker-level ticket decomposition if +its parts are independently valuable and trackable. Recommend branch carving +only when the ticket remains one coherent deliverable and the diff is simply too +large for one reviewable PR. The operator decides. An unresolved decision or a +ticket-split choice returns `blocked` before publication; this workflow does not +create or edit the replacement tickets. + +The explicit `decompose oversized candidates into stacked changesets` grant is +required before handoff and is off by default. Ready-PR, merge, ticket-edit, or +generic completion authority never implies it. Without the grant, preserve the +clean source candidate and return `blocked` with the guardrail evidence; do not +publish a monolithic PR or begin a local chain. + +## Verified handoff + +Immediately before transfer, capture and verify: + +- ticket identity, owning tracker, complete observable goal, acceptance + criteria, non-goals, and preserved behavior; +- repository instructions and named architecture, design, contract, migration, + and rollout documents; +- source repository, immutable source branch and SHA, base branch and SHA, merge + base, complete effective diff, resulting tree, and commit history; +- tracked, staged, unstaged, untracked, and ignored worktree state; +- focused and full validation commands and exact outcomes; +- clean initial `review-code-change` result bound to the exact source and base, + plus reviewer-integrity evidence; +- guardrail evidence, the operator's branch-carving decision, and the explicit + decomposition grant; +- requested terminal boundary, completion policy, retry and review-cycle + budgets, and every granted or withheld mutation, publish, reply, resolution, + merge, propagation, and cleanup authority; +- tracker reference and closing behavior, including the expected transition only + after the full stack merges; and +- exclusive mutation ownership of the immutable source candidate and every stack + branch created from it. + +Reject a stale, dirty, behind-base, conflicting, incomplete, review-stale, or +ambiguously owned candidate. The source branch becomes immutable at handoff. + +## Policy and tracker mapping + +- `ready PR only` maps to `carve-changesets` publish authority and the + `prs_open` boundary. Merge and propagation remain withheld. +- `merge after gates` maps to merge-and-propagate authority and the `all_merged` + boundary. +- `merge plus manual transition` also maps to `all_merged`; the separately + authorized manual tracker transition stays with `implement-ticket` after + mainline verification. + +One ticket owns one candidate. That candidate publishes as either one ordinary +PR or one ordered carved stack. Put the tracker's closing syntax only on the +final changeset PR, or on none when the completion policy forbids automatic +transition. Intermediate PRs use non-closing references, remain behaviorally +safe at their chain positions, and never transition the ticket. Verify the +ticket transition only after a current, independently verified `all_merged` +result. + +## Terminal-result mapping + +Reread live git, GitHub, and tracker state before accepting a returned result. +The ticket, repository, source, base, stack topology, PR identities, authority, +validation, review, equivalence, gate, and cleanup evidence must match the +handoff. + +- `prs_open` maps to `ready_prs` only when every changeset PR is open and + correctly based, every applicable current-candidate non-merge gate passes, + final-only closing syntax is verified, whole-chain equivalence holds, merge is + withheld, and ownership is consistent. +- `all_merged` maps to `merged` only after `implement-ticket` independently + verifies every sequential merge and propagation step, complete representation + and required validation on the base, the expected ticket transition, and + authorized cleanup. +- `blocked` maps to `blocked` with the exact phase, source, base, stack, PR, + candidate, preserved artifacts, last trustworthy evidence, and one action + needed to resume. +- `plan_ready` or `chain_ready` cannot satisfy an `implement-ticket` publication + policy and map to `blocked` unless the caller explicitly changes the requested + boundary. + +A material redesign raised on changeset N that invalidates an earlier merged +changeset is a genuine `blocked` result. Preserve the partial stack and report +the required product or architecture decision; never rewrite merged history or +hide the inconsistency in a later changeset. + +Resume from the live `carve-changesets` source, branches, PRs, and mainline +state. Do not duplicate prior publication, watcher, review, merge, or +propagation actions from cached handoff evidence. diff --git a/skills/implement-ticket/references/cleanup-and-result.md b/skills/implement-ticket/references/cleanup-and-result.md index 700d71c..91e7cac 100644 --- a/skills/implement-ticket/references/cleanup-and-result.md +++ b/skills/implement-ticket/references/cleanup-and-result.md @@ -3,26 +3,27 @@ Verify remote, mainline, tracker, and local state before deleting anything or returning a terminal handoff. -## Safe per-PR cleanup +## Safe per-candidate cleanup -01. Confirm the PR is merged remotely. +01. Confirm the ordinary PR or every carved-stack PR is merged remotely. 02. Fetch and prune the remote. -03. Confirm the branch result is represented on the verified base. Use ancestry - first and patch equivalence after squash or rebase when needed. -04. Map the exact ticket worktree, local branch, upstream, PR branch, recorded - PR head, and base branch. Never rely on the current directory or a - branch-name guess. +03. Confirm the ordinary branch or complete stack result is represented on the + verified base. Use ancestry first and patch equivalence after squash or + rebase when needed. +04. Map the exact ticket worktree, source and published branches, upstreams, PR + heads, and base branch. Never rely on the current directory or a branch-name + guess. 05. Inspect tracked, staged, unstaged, untracked, and ignored state in that exact worktree. Classify ignored and untracked paths as reproducible output or non-reproducible/user-created data. Preserve credentials, `.env` files, local databases, and all non-reproducible artifacts. -06. Confirm the local branch has no commits absent from its pushed PR branch. - When the remote branch is gone, compare it with the recorded PR head. -07. If the pushed branch exists, confirm it did not advance beyond the recorded - PR head and that the recorded result is represented on the base. +06. Confirm each local published branch has no commits absent from its pushed PR + branch. When a remote branch is gone, compare it with the recorded PR head. +07. If a pushed branch exists, confirm it did not advance beyond the recorded PR + head and that the recorded result is represented on the base. 08. Remove only a clean disposable worktree. Never force removal. -09. Delete only the verified merged local feature branch, then its remote - feature branch when policy and authority permit. +09. Delete only verified merged local feature branches, then their remote + branches when policy and authority permit. 10. Prune worktree metadata and verify the intended path and branches are gone. Stop cleanup and report exact dirty paths, ignored paths, unique commits, or @@ -33,7 +34,8 @@ branches, ignored files, untracked files, and user edits. After merge, verify: -- the remote base advanced or otherwise contains the merged result; +- the remote base advanced or otherwise contains the complete ordinary or + stacked result; - the implemented behavior and tests exist on the base; - the owning tracker transitioned the ticket as expected; - for an epic child, affected native dependency relationships were reread after @@ -50,14 +52,19 @@ unblocked work. Report newly ready work only as context. Return a concise documented handoff. Do not require a machine-readable schema unless the caller has one. Include every applicable field: -- `terminal_state`: `ready_pr`, `merged`, `blocked`, or `requires_epic`; +- `terminal_state`: `ready_pr`, `ready_prs`, `merged`, `blocked`, or + `requires_epic`; - ticket identity, tracker, repository, PR host, and base identity; -- branch, worktree, candidate head, and PR identity when created; +- branch, worktree, candidate head, publication path, and PR or ordered stack + identity when created; - completion policy and the authority actually used; - focused and full validation commands, outcomes, and limitations; - initial `review-code-change` verdict and reviewed candidate identity; - `babysit-pr` policy, terminal state, returned candidate identity, authority used, mutation ownership, and independently verified live-state match; +- for a stack, `carve-changesets` source identity, guardrail and operator + decision, authority, terminal state, ordered PR topology, equivalence, + propagation, closing-syntax placement, and verified live-state match; - applicable CI, human, connector, comment, formal-review, and thread state; - merge, mainline, ticket transition, and cleanup state; - deferred findings and intentionally unperformed work; and @@ -86,11 +93,17 @@ still-current open and mergeable PR. Every applicable non-merge gate must pass; the only withheld action is merge. Do not list ordinary pending CI or review as a remaining gate on a terminal `ready_pr`. -For `merged`, require a verified `babysit-pr: merged` result plus the -independent mainline, tracker-transition, dependency-refresh, and cleanup checks -above. A `closed` babysitter result becomes `blocked` with -`PR closed without merge` and preserves local artifacts unless another canonical -completion is proven. +For `ready_prs`, require a verified `carve-changesets: prs_open` result for the +still-current ordered stack. Every PR must be open, correctly based, mergeable, +and at its applicable non-merge gate; whole-chain equivalence and final-only +closing syntax must be verified. The only withheld actions are merge and +propagation. Do not list ordinary pending CI or review as a remaining gate. + +For `merged`, require a verified `babysit-pr: merged` or +`carve-changesets: all_merged` result plus the independent mainline, +tracker-transition, dependency-refresh, and cleanup checks above. A `closed` +babysitter result becomes `blocked` with `PR closed without merge` and preserves +local artifacts unless another canonical completion is proven. For `requires_epic`, require all of: diff --git a/skills/implement-ticket/references/github.md b/skills/implement-ticket/references/github.md index ae4de47..1dd7ebb 100644 --- a/skills/implement-ticket/references/github.md +++ b/skills/implement-ticket/references/github.md @@ -37,10 +37,11 @@ shipping. Never select another child or mutate the graph from this skill. When duplicate branches or PRs exist, compare actual patches or resulting trees and retain one canonical implementation path. For an open canonical path owned by another worker, return `blocked` with its identity unless ownership is -explicitly transferred; never claim its candidate as this run's `ready_pr`. When -a merged PR is verified on the base and the ticket is already complete, return -`merged` with that evidence without creating new state. Return `blocked` rather -than creating a competing PR when canonical ownership is unresolved. +explicitly transferred; never claim its candidate as this run's `ready_pr` or +`ready_prs`. When a merged PR is verified on the base and the ticket is already +complete, return `merged` with that evidence without creating new state. Return +`blocked` rather than creating a competing PR when canonical ownership is +unresolved. ## PR-host preflight and contract @@ -48,13 +49,17 @@ than creating a competing PR when canonical ownership is unresolved. authentication. - Confirm the remote base, current checkout, branch, and worktree topology. - Inspect open and merged PRs that reference the owning tracker ticket. -- Use one tracker ticket per branch and PR. +- Use one tracker ticket per candidate. Publish that candidate as exactly one + ordinary PR or one ordered carved stack. - When GitHub owns ticket state, use the repository's closing syntax, normally `Fixes #`. - Determine whether that syntax will automatically close or transition the ticket on merge and disclose the consequence in the resolved completion policy before publishing or merging. Use a non-closing reference when automatic closure would conflict with that policy. +- For a carved stack, put closing syntax only on the final changeset PR; every + intermediate PR uses a non-closing reference. Verify transition only after the + full stack is merged. - When another tracker owns state, use its required reference and avoid GitHub closing syntax unless a real GitHub issue is also intentionally in scope. - Describe the branch as a whole, preserve material non-goals, and report actual @@ -66,23 +71,27 @@ Markdown. ## Handoff and caller-owned closeout -Capture the exact PR head and base, effective candidate, worktree state, -validation, initial review, required remote-gate policy, connector contract, -completion policy, and authority required by -[the babysit-pr handoff](babysit-pr-handoff.md). Then delegate GitHub Actions, -published feedback, human and connector review, thread disposition, -candidate-changing fixes, base drift, mergeability, and optional merge to the -repository-owned `babysit-pr` skill. +For the ordinary path, capture the exact PR head and base, effective candidate, +worktree state, validation, initial review, required remote-gate policy, +connector contract, completion policy, and authority required by +[the babysit-pr handoff](babysit-pr-handoff.md), then delegate the PR to +repository-owned `babysit-pr`. + +For the carved path, capture the immutable source candidate, guardrail and +operator-decision evidence, completion policy, tracker semantics, and authority +required by [the carve-changesets handoff](carve-changesets-handoff.md), then +delegate the entire stack lifecycle to repository-owned `carve-changesets`. Do not infer a gate's absence from an empty read. Pass the documented policy and all known current evidence so the babysitter can establish current-candidate state. Do not also poll, mutate, reply, resolve, or merge from this caller after ownership transfer. -After a babysitter `merged` result, independently verify PR state, merged -candidate representation on the base, and the GitHub issue transition before -cleanup. When the ticket is an epic child, reread its affected native `blocking` -and sibling `blockedBy` relationships and report newly unblocked work without -selecting or mutating it. If local worktree ownership prevents the CLI from -switching to the base, use a read-only remote verification path and perform -local cleanup separately. Never close a parent issue from this skill. +After a babysitter `merged` result or a carve `all_merged` result, independently +verify PR or stack state, complete candidate representation on the base, and the +GitHub issue transition before cleanup. When the ticket is an epic child, reread +its affected native `blocking` and sibling `blockedBy` relationships and report +newly unblocked work without selecting or mutating it. If local worktree +ownership prevents the CLI from switching to the base, use a read-only remote +verification path and perform local cleanup separately. Never close a parent +issue from this skill. diff --git a/skills/implement-ticket/references/linear.md b/skills/implement-ticket/references/linear.md index 85325af..6f4353f 100644 --- a/skills/implement-ticket/references/linear.md +++ b/skills/implement-ticket/references/linear.md @@ -39,10 +39,13 @@ independently. unavailable, migration, compatibility, and rollout behavior. - Use the repository's required Linear reference in branch, commit, and PR metadata. -- Keep one ticket per PR. +- Keep one ticket per candidate. Publish it as one ordinary PR or one ordered + carved stack, with the Linear reference on every PR and any transition-causing + closing integration only on the final PR. - Update Linear status only when that workflow state was actually reached and the completion policy authorizes a manual transition. -- After merge, verify the result on the base and the expected Linear transition. +- After the ordinary merge or a verified `all_merged` stack result, verify the + complete result on the base and the expected Linear transition. - When the ticket is an epic child, reread affected native dependency relationships after transition and report newly unblocked work without selecting or mutating it. diff --git a/skills/implement-ticket/references/review-and-merge-gates.md b/skills/implement-ticket/references/review-and-merge-gates.md index f9b97d2..f362666 100644 --- a/skills/implement-ticket/references/review-and-merge-gates.md +++ b/skills/implement-ticket/references/review-and-merge-gates.md @@ -2,15 +2,16 @@ Apply these gates to the complete initial ticket candidate. Repository instructions may add stricter requirements but must not silently weaken them. -Delegate the published PR's continuing lifecycle to repository-owned -`babysit-pr`; do not duplicate its CI, feedback, drift, post-fix review, or -merge mechanics here. +After review, select exactly one publication path. Delegate an ordinary PR's +continuing lifecycle to repository-owned `babysit-pr`, or delegate an oversized +candidate's entire stacked lifecycle to repository-owned `carve-changesets`. Do +not duplicate either delegate's mechanics here. ## Initial bounded review loop -Require repository-owned `review-code-change` before a PR can be handed to -`babysit-pr`. Fail closed when it is missing or unreadable. Do not substitute -another skill, a generic self-review, or an unreviewed path. +Require repository-owned `review-code-change` before the publication size gate. +Fail closed when it is missing or unreadable. Do not substitute another skill, a +generic self-review, or an unreviewed path. Require every intended ticket change to be committed and the implementation worktree to be clean before review. If unrelated user artifacts prevent a clean @@ -41,36 +42,43 @@ tractable, and ticket-scoped. Preserve deferred findings without expanding the PR. Reply with evidence when a finding no longer applies. After a material initial-review fix, run affected and required validation, -commit and push the new head, rebuild the raw evidence packet, and follow the -returned re-review instruction. Use at most three full fix/re-review cycles by -default. A clean aggregate ends the initial loop. If material findings remain -after the final cycle, keep the PR open and return `blocked` with unresolved -evidence. +commit the new head, rebuild the raw evidence packet, and follow the returned +re-review instruction. Push only after the publication path is selected. Use at +most three full fix/re-review cycles by default. A clean aggregate ends the +initial loop. If material findings remain after the final cycle, preserve the +candidate and return `blocked` with unresolved evidence. -## Delegation gate +## Publication and delegation gate -Before invoking `babysit-pr`: +Before invoking either delegate: - verify the initial review is clean for the exact live head and applicable base; -- verify the PR identity, effective diff, resulting tree, validation, worktree, - ticket reference, and authority are internally consistent; -- assemble every field required by - [the handoff contract](babysit-pr-handoff.md); +- evaluate the exact candidate against the live `carve-changesets` guardrails + without duplicating their thresholds; +- verify the selected single-PR or stack identity, effective diff, resulting + tree, validation, worktree, ticket reference, and authority are internally + consistent; +- assemble every field required by the applicable + [babysit-pr](babysit-pr-handoff.md) or + [carve-changesets](carve-changesets-handoff.md) handoff contract; - map the completion policy without broadening authority; and - establish one exclusive mutating owner. Treat a missing dependency, malformed result, `blocked` verdict, reviewer mutation, stale identity, or unavailable required evidence as a failed gate. Do -not claim `ready_pr` merely because a PR exists or an initial review is clean. +not claim `ready_pr` or `ready_prs` merely because a PR or stack exists or an +initial review is clean. ## Caller-side completion verification -After `babysit-pr` returns, reread live GitHub state and apply the result -mapping in [the handoff contract](babysit-pr-handoff.md). A `ready_pr` requires -a validated current `ready_to_merge` result. A `merged` result requires -independent remote merge, mainline, tracker-transition, dependency-refresh, and -cleanup verification by `implement-ticket`. +After the selected delegate returns, reread live GitHub state and apply the +applicable [babysit-pr](babysit-pr-handoff.md) or +[carve-changesets](carve-changesets-handoff.md) result mapping. A `ready_pr` +requires a validated current `ready_to_merge` result. A `ready_prs` requires a +validated current `prs_open` result. A `merged` result requires independent +remote merge or `all_merged`, mainline, tracker-transition, dependency-refresh, +and cleanup verification by `implement-ticket`. If the live head, base, PR state, ownership, or gate evidence differs from the result, reconcile the live candidate or fail closed. Never carry stale evidence diff --git a/skills/implement-ticket/scripts/evals/claude_executor.py b/skills/implement-ticket/scripts/evals/claude_executor.py index 855a4e2..7e56f98 100644 --- a/skills/implement-ticket/scripts/evals/claude_executor.py +++ b/skills/implement-ticket/scripts/evals/claude_executor.py @@ -26,6 +26,7 @@ TERMINAL_STATES = ( "ready_pr", + "ready_prs", "merged", "blocked", "requires_epic", @@ -41,6 +42,10 @@ "consume_ticket_states_unchanged", "deduplicate_prior_actions", "do_not_invoke_babysit_pr_directly", + "do_not_invoke_carve_changesets", + "do_not_own_decomposition_mechanics", + "do_not_publish_monolithic_pr", + "do_not_rewrite_merged_history", "do_not_reply_or_resolve", "fail_before_mutation", "fresh_review_code_change", @@ -48,28 +53,39 @@ "invalidate_head_bound_evidence", "invoke_merge_when_ready", "invoke_ready_to_merge", + "invoke_carve_changesets", "make_no_code_mutation", "name_missing_babysit_pr", "perform_no_mutation", + "place_closing_syntax_final_pr_only", "preserve_artifacts", "preserve_feedback_gate", + "preserve_partial_stack", "preserve_tracker_pr_host_separation", "rebuild_remote_gates", + "record_guardrail_evidence", "refresh_graph_after_merged_only", "reject_concurrent_mutation", "reject_stale_connector_verdict", "reject_stale_or_malformed_result", "report_closed_without_merge", + "report_mid_stack_redesign", "reread_live_pr", "retain_only_proven_unaffected_evidence", "retry_diagnosed_run_only", "revalidate_candidate_identity", "revalidate_commit_push", "route_before_ticket_dependencies", + "route_to_tracker_split", + "skip_direct_babysit_handoff", + "stop_before_publication", "ticket_scoped_fix", "transfer_exclusive_mutation_ownership", "verify_merge_live", "verify_non_merge_gates", + "verify_each_pr_gate", + "verify_full_stack_on_base", + "verify_stack_topology", ) diff --git a/skills/implement-ticket/scripts/evals/fixture_executor.py b/skills/implement-ticket/scripts/evals/fixture_executor.py index 72e91ac..628cae3 100644 --- a/skills/implement-ticket/scripts/evals/fixture_executor.py +++ b/skills/implement-ticket/scripts/evals/fixture_executor.py @@ -28,12 +28,15 @@ def action_result(payload: dict) -> dict: "implement-ticket": ( "`review-code-change` and `babysit-pr` are available", "Map `ready PR only` to `ready_to_merge`", + "`prs_open`", + "`ready_prs`", "Normal ticket execution never uses `watch_until_closed`", ), "implement-epic": ( "Do not make this skill invoke", - "`review-code-change` or `babysit-pr`", + "`carve-changesets` itself", "`ready_pr`", + "`ready_prs`", ), }[target] if not all(compact(fragment) in prompt for fragment in required_contract): @@ -55,6 +58,18 @@ def action_result(payload: dict) -> dict: actions = [] if target == "implement-epic": + if handoff.get("stack_child_result"): + return { + "target_skill": target, + "terminal_state": "mixed_ticket_results", + "actions": [ + "verify_stack_topology", + "verify_each_pr_gate", + "verify_full_stack_on_base", + "do_not_own_decomposition_mechanics", + "refresh_graph_after_merged_only", + ], + } return { "target_skill": target, "terminal_state": "mixed_ticket_results", @@ -82,6 +97,55 @@ def action_result(payload: dict) -> dict: "actions": ["fail_before_mutation", "name_missing_babysit_pr"], } + if artifacts["diff"].get("guardrail") == "oversized": + actions.append("record_guardrail_evidence") + if handoff.get("rubric") == "ticket_split": + return { + "target_skill": target, + "terminal_state": "blocked", + "actions": actions + + [ + "route_to_tracker_split", + "stop_before_publication", + "do_not_invoke_carve_changesets", + ], + } + if not authority.get("decompose_oversized"): + return { + "target_skill": target, + "terminal_state": "blocked", + "actions": actions + + [ + "stop_before_publication", + "do_not_publish_monolithic_pr", + "do_not_invoke_carve_changesets", + ], + } + if handoff.get("mid_stack_redesign"): + return { + "target_skill": target, + "terminal_state": "blocked", + "actions": actions + + [ + "preserve_partial_stack", + "report_mid_stack_redesign", + "do_not_rewrite_merged_history", + ], + } + if handoff.get("carve_terminal") == "prs_open": + return { + "target_skill": target, + "terminal_state": "ready_prs", + "actions": actions + + [ + "invoke_carve_changesets", + "skip_direct_babysit_handoff", + "place_closing_syntax_final_pr_only", + "verify_stack_topology", + "verify_each_pr_gate", + ], + } + if pr.get("state") == "closed" and not pr.get("merged"): return { "target_skill": target, diff --git a/skills/implement-ticket/scripts/tests/test_forward_evals.py b/skills/implement-ticket/scripts/tests/test_forward_evals.py index b437b9a..6be62a8 100644 --- a/skills/implement-ticket/scripts/tests/test_forward_evals.py +++ b/skills/implement-ticket/scripts/tests/test_forward_evals.py @@ -42,7 +42,7 @@ def test_every_packet_contains_raw_live_shaped_artifact_categories(self): "worktree", "handoff", } - self.assertEqual(18, len(self.cases)) + self.assertEqual(23, len(self.cases)) for case in self.cases: self.assertEqual(required, set(case["artifacts"]), case["id"]) @@ -64,16 +64,13 @@ def test_forward_cases_execute_fresh_and_pass_separate_grading(self): [sys.executable, str(EXECUTOR_PATH)], ) self.assertEqual([], failures) - self.assertEqual(18, len(observations)) + self.assertEqual(23, len(observations)) process_ids = {result["executor_pid"] for result in observations.values()} - self.assertEqual(18, len(process_ids)) + self.assertEqual(23, len(process_ids)) def test_reference_executor_evaluates_the_supplied_skill_prompt(self): payload = RUNNER.build_payload(self.cases[2]) - payload["skill_prompt"] = payload["skill_prompt"].replace( - "Map `ready PR only` to `ready_to_merge`", - "", - ) + payload["skill_prompt"] = payload["skill_prompt"].replace("`ready_prs`", "") observed = RUNNER.run_executor( [sys.executable, str(EXECUTOR_PATH)], payload, @@ -130,6 +127,18 @@ def test_required_composition_cases_are_executable(self): "do_not_invoke_babysit_pr_directly", observations["implement-epic-consumes-ticket-results"]["actions"], ) + self.assertEqual( + "ready_prs", + observations["oversized-authorized-carved-stack"]["terminal_state"], + ) + self.assertIn( + "route_to_tracker_split", + observations["oversized-ticket-split-rubric"]["actions"], + ) + self.assertIn( + "verify_full_stack_on_base", + observations["implement-epic-verifies-stacked-child"]["actions"], + ) if __name__ == "__main__": diff --git a/skills/implement-ticket/scripts/tests/test_implement_ticket_contract.py b/skills/implement-ticket/scripts/tests/test_implement_ticket_contract.py index a9859b8..5c1842c 100644 --- a/skills/implement-ticket/scripts/tests/test_implement_ticket_contract.py +++ b/skills/implement-ticket/scripts/tests/test_implement_ticket_contract.py @@ -33,6 +33,9 @@ def setUpClass(cls): cls.linear = read(SKILL_ROOT / "references" / "linear.md") cls.gates = read(SKILL_ROOT / "references" / "review-and-merge-gates.md") cls.handoff = read(SKILL_ROOT / "references" / "babysit-pr-handoff.md") + cls.carve_handoff = read( + SKILL_ROOT / "references" / "carve-changesets-handoff.md" + ) cls.result = read(SKILL_ROOT / "references" / "cleanup-and-result.md") cls.skill_compact = compact(cls.skill) cls.handoff_compact = compact(cls.handoff) @@ -42,7 +45,13 @@ def setUpClass(cls): + read(SKILL_ROOT / "evals" / "expectations.json") ) cls.all_contract = compact( - cls.skill + cls.github + cls.linear + cls.gates + cls.handoff + cls.result + cls.skill + + cls.github + + cls.linear + + cls.gates + + cls.handoff + + cls.carve_handoff + + cls.result ) cls.cases = { item["id"]: item @@ -61,7 +70,7 @@ def test_frontmatter_and_product_neutral_contract(self): self.assertNotIn("OpenAI", self.eval_contract) def test_terminal_states_are_stable(self): - for state in ("ready_pr", "merged", "blocked", "requires_epic"): + for state in ("ready_pr", "ready_prs", "merged", "blocked", "requires_epic"): self.assertIn(state, self.skill) self.assertIn(state, self.result_compact) @@ -93,8 +102,25 @@ def test_dependency_names_are_repository_owned_and_acyclic(self): self.assertIn("review-code-change", self.skill_compact) self.assertIn("babysit-pr", self.skill_compact) self.assertIn( - "`babysit-pr` must never invoke `implement-ticket`", self.skill_compact + "`babysit-pr` and `carve-changesets` must never invoke `implement-ticket`", + self.skill_compact, + ) + self.assertIn("carve-changesets", self.skill_compact) + self.assertIn( + "`carve-changesets` must never invoke `implement-epic`", + self.skill_compact, + ) + + def test_oversized_publication_contract_is_authority_gated(self): + contract = compact(self.skill + self.carve_handoff + self.result) + self.assertIn( + "`decompose oversized candidates into stacked changesets`", contract ) + self.assertIn("`prs_open` maps to `ready_prs`", contract) + self.assertIn("`all_merged` maps to `merged`", contract) + self.assertIn("final changeset PR", contract) + self.assertIn("The operator decides", contract) + self.assertNotIn("few hundred", contract) def test_instruction_file_naming_is_host_neutral(self): self.assertIn("CLAUDE.md", self.skill_compact) @@ -125,6 +151,16 @@ def test_eval_expectations_enforce_routing_and_authority(self): "missing-asynchronous-wait", ): self.assertEqual("blocked", self.expectations[case_id]["terminal_state"]) + self.assertEqual( + "blocked", + self.expectations["oversized-without-decomposition-authority"][ + "terminal_state" + ], + ) + self.assertEqual( + "ready_prs", + self.expectations["oversized-authorized-carved-stack"]["terminal_state"], + ) def test_runtime_adapters_exist_for_both_products(self): metadata = read(SKILL_ROOT / "agents" / "openai.yaml") From dc4f5c1f3e33c25ad6258f7365506bd33255ed82 Mon Sep 17 00:00:00 2001 From: Scott Haug Date: Tue, 21 Jul 2026 22:42:31 -0700 Subject: [PATCH 2/2] fix: fail closed on invalid carved handoffs ## Summary - Reject oversized publication when `carve-changesets` is unavailable - Reject stale, malformed, or incomplete `prs_open` evidence before `ready_prs` - Add fresh-process negative evaluations for both failure modes ## Why - Keep the carved publication contract candidate-bound and fail closed - Prevent behavioral evals from accepting an untrustworthy stack result --- CHANGELOG.md | 2 + .../implement-ticket/evals/forward_cases.json | 36 ++++++++++++++++ .../evals/forward_expectations.json | 14 +++++++ .../scripts/evals/claude_executor.py | 1 + .../scripts/evals/fixture_executor.py | 42 +++++++++++++++---- .../scripts/tests/test_forward_evals.py | 14 +++++-- 6 files changed, 99 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb06a4c..8d92ca1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,9 @@ summary: Chronological history of repository and skill changes. ## 2026-07-21 — Completed carve-changesets and integrated ticket publication +- fix: fail closed on invalid carved handoffs - feat: integrate carved ticket publication + (`54c67f7cd7ace3269eee4fe628f974b090a4d699`) - refactor: derive the eval action vocabulary from expectations (`e30b5f1021538d673eb931b2978287cfd21ae4ae`) - fix: require the two-part source freshness override diff --git a/skills/implement-ticket/evals/forward_cases.json b/skills/implement-ticket/evals/forward_cases.json index 32062bb..fe320f1 100644 --- a/skills/implement-ticket/evals/forward_cases.json +++ b/skills/implement-ticket/evals/forward_cases.json @@ -412,5 +412,41 @@ "worktree": {"path": null, "exclusive_owner": true, "tracked": [], "untracked": []}, "handoff": {"result_well_formed": true, "stack_child_result": "all_merged", "stack_count": 3, "topology": "verified", "full_chain_on_base": true, "per_pr_gates": "verified"} } + }, + { + "id": "missing-carve-changesets", + "target_skill": "implement-ticket", + "request": "Publish the authorized oversized G-330 candidate.", + "authority": {"merge": false, "decompose_oversized": true}, + "capabilities": {"review_code_change": true, "babysit_pr": true, "carve_changesets": false}, + "artifacts": { + "ticket": {"tracker": "github", "id": "G-330", "state": "open", "whole_epic": false}, + "repository": {"repo": "example/project", "tracker": "github", "pr_host": "github", "instructions": "fail closed when the publication dependency is unavailable"}, + "pr": {"state": "absent", "merged": false, "head": "head-330", "base": "base-1", "mergeable": null}, + "diff": {"base": "base-1", "head": "head-330", "patch": "large coherent candidate", "resulting_tree": "tree-330", "guardrail": "oversized"}, + "checks": {"status": "not_published", "items": []}, + "reviews": {"initial": "clean", "head": "head-330", "items": []}, + "threads": {"items": [], "unresolved": 0}, + "worktree": {"path": "/worktrees/g330", "exclusive_owner": true, "tracked": [], "untracked": []}, + "handoff": {"created": false, "result_well_formed": true, "rubric": "carve_coherent_branch"} + } + }, + { + "id": "stale-carved-result", + "target_skill": "implement-ticket", + "request": "Accept the ready carved stack for G-331.", + "authority": {"merge": false, "decompose_oversized": true}, + "capabilities": {"review_code_change": true, "babysit_pr": true, "carve_changesets": true}, + "artifacts": { + "ticket": {"tracker": "github", "id": "G-331", "state": "open", "whole_epic": false}, + "repository": {"repo": "example/project", "tracker": "github", "pr_host": "github", "instructions": "candidate-bound stack results"}, + "pr": {"state": "multiple_open", "merged": false, "head": "stack-tip-331b", "base": "base-1", "mergeable": true}, + "diff": {"base": "base-1", "head": "head-331b", "patch": "large coherent candidate", "resulting_tree": "tree-331b", "guardrail": "oversized"}, + "checks": {"status": "success", "items": [{"pr": 1}, {"pr": 2}]}, + "reviews": {"initial": "clean", "head": "head-331b", "per_changeset": "clean", "items": []}, + "threads": {"items": [], "unresolved": 0}, + "worktree": {"path": "/worktrees/g331", "exclusive_owner": true, "tracked": [], "untracked": []}, + "handoff": {"created": true, "result_well_formed": false, "result_stale": true, "rubric": "carve_coherent_branch", "carve_terminal": "prs_open", "result_head": "stack-tip-331a"} + } } ] diff --git a/skills/implement-ticket/evals/forward_expectations.json b/skills/implement-ticket/evals/forward_expectations.json index acabc20..5025550 100644 --- a/skills/implement-ticket/evals/forward_expectations.json +++ b/skills/implement-ticket/evals/forward_expectations.json @@ -271,5 +271,19 @@ "required_actions": ["verify_stack_topology", "verify_each_pr_gate", "verify_full_stack_on_base", "do_not_own_decomposition_mechanics", "refresh_graph_after_merged_only"], "target_skill": "implement-epic", "terminal_state": "mixed_ticket_results" + }, + { + "case_id": "missing-carve-changesets", + "forbidden_actions": ["invoke_carve_changesets", "invoke_ready_to_merge", "invoke_merge_when_ready"], + "required_actions": ["stop_before_publication", "name_missing_carve_changesets"], + "target_skill": "implement-ticket", + "terminal_state": "blocked" + }, + { + "case_id": "stale-carved-result", + "forbidden_actions": ["invoke_carve_changesets", "verify_stack_topology", "verify_each_pr_gate"], + "required_actions": ["reject_stale_or_malformed_result", "reread_live_pr"], + "target_skill": "implement-ticket", + "terminal_state": "blocked" } ] diff --git a/skills/implement-ticket/scripts/evals/claude_executor.py b/skills/implement-ticket/scripts/evals/claude_executor.py index 7e56f98..294ff78 100644 --- a/skills/implement-ticket/scripts/evals/claude_executor.py +++ b/skills/implement-ticket/scripts/evals/claude_executor.py @@ -56,6 +56,7 @@ "invoke_carve_changesets", "make_no_code_mutation", "name_missing_babysit_pr", + "name_missing_carve_changesets", "perform_no_mutation", "place_closing_syntax_final_pr_only", "preserve_artifacts", diff --git a/skills/implement-ticket/scripts/evals/fixture_executor.py b/skills/implement-ticket/scripts/evals/fixture_executor.py index 628cae3..b84fa31 100644 --- a/skills/implement-ticket/scripts/evals/fixture_executor.py +++ b/skills/implement-ticket/scripts/evals/fixture_executor.py @@ -97,6 +97,22 @@ def action_result(payload: dict) -> dict: "actions": ["fail_before_mutation", "name_missing_babysit_pr"], } + if artifacts["diff"].get("guardrail") == "oversized" and not capabilities.get( + "carve_changesets" + ): + return { + "target_skill": target, + "terminal_state": "blocked", + "actions": ["stop_before_publication", "name_missing_carve_changesets"], + } + + if not handoff.get("result_well_formed", True) or handoff.get("result_stale"): + return { + "target_skill": target, + "terminal_state": "blocked", + "actions": ["reject_stale_or_malformed_result", "reread_live_pr"], + } + if artifacts["diff"].get("guardrail") == "oversized": actions.append("record_guardrail_evidence") if handoff.get("rubric") == "ticket_split": @@ -133,6 +149,25 @@ def action_result(payload: dict) -> dict: ], } if handoff.get("carve_terminal") == "prs_open": + stack_count = handoff.get("stack_count") + if not ( + pr.get("state") == "multiple_open" + and pr.get("mergeable") is True + and isinstance(stack_count, int) + and stack_count > 0 + and handoff.get("topology") == "verified" + and handoff.get("closing_syntax") == "final_only" + and checks.get("status") == "success" + and reviews.get("per_changeset") == "clean" + ): + return { + "target_skill": target, + "terminal_state": "blocked", + "actions": [ + "reject_stale_or_malformed_result", + "reread_live_pr", + ], + } return { "target_skill": target, "terminal_state": "ready_prs", @@ -153,13 +188,6 @@ def action_result(payload: dict) -> dict: "actions": ["preserve_artifacts", "report_closed_without_merge"], } - if not handoff.get("result_well_formed", True) or handoff.get("result_stale"): - return { - "target_skill": target, - "terminal_state": "blocked", - "actions": ["reject_stale_or_malformed_result", "reread_live_pr"], - } - if handoff.get("delegated"): if not handoff.get("exclusive_transfer"): return { diff --git a/skills/implement-ticket/scripts/tests/test_forward_evals.py b/skills/implement-ticket/scripts/tests/test_forward_evals.py index 6be62a8..c7cf4dd 100644 --- a/skills/implement-ticket/scripts/tests/test_forward_evals.py +++ b/skills/implement-ticket/scripts/tests/test_forward_evals.py @@ -42,7 +42,7 @@ def test_every_packet_contains_raw_live_shaped_artifact_categories(self): "worktree", "handoff", } - self.assertEqual(23, len(self.cases)) + self.assertEqual(25, len(self.cases)) for case in self.cases: self.assertEqual(required, set(case["artifacts"]), case["id"]) @@ -64,9 +64,9 @@ def test_forward_cases_execute_fresh_and_pass_separate_grading(self): [sys.executable, str(EXECUTOR_PATH)], ) self.assertEqual([], failures) - self.assertEqual(23, len(observations)) + self.assertEqual(25, len(observations)) process_ids = {result["executor_pid"] for result in observations.values()} - self.assertEqual(23, len(process_ids)) + self.assertEqual(25, len(process_ids)) def test_reference_executor_evaluates_the_supplied_skill_prompt(self): payload = RUNNER.build_payload(self.cases[2]) @@ -139,6 +139,14 @@ def test_required_composition_cases_are_executable(self): "verify_full_stack_on_base", observations["implement-epic-verifies-stacked-child"]["actions"], ) + self.assertIn( + "name_missing_carve_changesets", + observations["missing-carve-changesets"]["actions"], + ) + self.assertIn( + "reject_stale_or_malformed_result", + observations["stale-carved-result"]["actions"], + ) if __name__ == "__main__":