Claude Skill

ticket

Drive one tracked ticket from arrival to resolution through four verbs: triage, start, revise, finalize. Use when the user says triage/start/revise/finalize with a ticket id, asks to turn a ticket into a locked brief, to execute a work order, to action a review round on a ticket'

LLM Mart · 0 points · 16 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download ConnorGriffin-skills-skills_drivers_ticket-872be56.zip · 65 KB
Part of connorgriffin/skills — 25 skills

Install

skills CLI npx skills add https://github.com/ConnorGriffin/skills/tree/main/skills/drivers/ticket
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install connorgriffin-skills@llmmart
Git git clone https://github.com/ConnorGriffin/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole connorgriffin/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Ticket

One ticket, one verb at a time. Each verb is a full procedure in verbs/<verb>.md; read that file before doing anything else, then follow it. This page holds only what every verb shares.

Invocation

/ticket <verb> <ticket-id>, where verb is triage, start, revise, or finalize. No verb or no ticket id: ask for it, one line. Unknown verb: list the four.

The pipeline

  • triage reads the ticket and the repo, interviews the user through /scope when scope is thin, and ends by posting the work order: a locked brief as a ticket comment. Work too big for one agent's context is sliced into sub-orders in that same comment (references/slicing.md). It writes nothing to the repo except scope and spec documents committed in the ticket's worktree. An epic child treats its issue-body order as a draft and may commit a required parent-plan amendment in the child worktree before posting the reviewed lock; that amendment travels with the implementation pull request.
  • start runs in a fresh session, fetches the work order, refuses if there is none, implements it on a branch in an isolated worktree (or, on a sliced order, coordinates one agent per chunk), iterates the verification step until the result matches the order's expectation, passes an adversarial review at the order's stamped depth (references/review-depth.md) unless Profile: hardening replaces it below Full depth, opens the pull request, and stops. Agents never merge.
  • revise actions one review round on the open pull request: reload the ticket and the order, fix, re-verify, push.
  • finalize runs after a human merged: verify the merge and post-merge workflow, complete the repository's post-merge archive guidance for an ordinary OpenSpec change, which opens a reviewed archive pull request and posts its Archive PR: locator before stopping, then on a later finalization, once a human merged that pull request, close the ticket with a comment linking the pull request, record what the ticket actually cost in context, and tear the worktree down, so this repo's slicing calibration is tuned against measured numbers rather than intuition.

The tracker contract

Every tracker interaction goes through four operations: read a ticket, post a comment on a ticket, move a ticket's status, and locate the newest work order on a ticket. references/tracker-contract.md defines them, and one binding page supplies them for one tracker. GitHub issues ship as the reference binding (bindings/github-issues.md).

The procedures below and in verbs/ call the contract, never a tracker's API directly. A verb that cannot reach the contract stops and names what is missing.

Review front door

start and revise reach code review as /review on the changed code, which routes to code-review. Neither verb calls a reviewer any other way, and neither substitutes a lighter check for the depth the order stamped. Below Full depth under Profile: hardening, start and revise use its exception instead.

Delegation authority

This authority covers triage's mandatory /plan-review and start/revise's /review route. Invoking /ticket authorizes every sub-agent dispatch that this procedure marks mandatory, including the coordinator's mandatory reviewer dispatch. Do not ask again solely because a session-level preference says "do not spawn agents"; apply that preference to discretionary delegation only. An explicit task-level refusal of this required review or revocation of delegation overrides this authorization: stop and state that the requested workflow cannot run without its required independent review.

When Ticket work is delegated, the delegation prompt identifies the mandatory-review handoff. At that boundary the worker returns or writes its review-ready result through the coordinator-recorded durable result locator and does not launch a reviewer. The coordinator dispatches every mandatory reviewer through the existing adapter after collecting the result, verifies the returned verdict, and resumes the same worker. Actionable findings resume it for correction; a verified clean verdict resumes it to finish. A failed launch, nonzero exit, missing result artifact, or missing verdict is reported as unavailable and blocks the workflow from advancing as reviewed. Direct nested adapter dispatch by the worker is unsupported.

Selected-ticket mutation boundary

Triage may mutate operator-local workflow state required by the installed workflow to execute the selected ticket lifecycle. Current examples include the lifecycle claim, exact-worktree Codebase Memory state, reviewer-memory store, and local remote-tracking refs used to resolve and verify the selected ticket's base. These examples make the purpose concrete; they are not an exhaustive list.

Triage may also mutate repository or tracker state belonging to the selected ticket lifecycle without ancillary approval. Current examples include the selected worktree and branch, an ordinary ticket's active change, the selected ticket's comment and status, and the defined Epic-child parent-plan amendment carried by that child's implementation pull request. These examples make the ownership concrete; they are not an exhaustive list.

This authority does not authorize state for a distinct external concern: independently addressable work outside the selected lifecycle, such as another branch, pull request, issue, ticket, or repository artifact outside the selected branch. Broad read-only grounding never authorizes it.

Shared rules (every verb)

  1. Open with the ticket summary. Before any other work, read the ticket and give the user an extremely high-level, human-readable summary: what the ticket is and what this verb is about to do on it (as simple as "implementing <ticket-id>, which is <one-line description>"). Then mark a chapter titled <ticket-id> <verb> when the harness offers a chapter tool, so the user can scroll back to it. Skip the chapter silently when it does not.

  2. Claim the session. Immediately after the ticket summary, run python3 <ticket-skill-directory>/scripts/ticket.py claim <ticket-id> --verb <current verb>, so the sessions that worked this ticket are recorded as they work it rather than guessed from prose afterwards. Pass --session and --agent whenever the environment cannot answer on its own: no session id in it, or more than one, which is what a worker launched from another agent's session sees. Pass --role to say what the session is doing on the ticket: coordinator (the session driving the ticket, and the default), worker (an agent building one chunk), or reviewer (a session that only reviews). The role decides which costs are evidence about how big the work was, so a session claimed under the wrong one is a measurement error. The required --verb is triage, start, revise, or finalize, matching the lifecycle verb this session is running. One session serves one lifecycle verb: same-verb resumes reuse the claim, while changing verbs requires a fresh session. A cross-verb re-claim keeps and prints the persisted claim, reports the persisted and submitted verbs as one visible conflict, and exits successfully; telemetry never claims the submitted metadata landed. A claim that fails is said in one line and never blocks the verb: telemetry is a measurement, not a gate. A sandboxed session (a Codex workspace-write sandbox, for one) that cannot write the claims file under ~/.config/ticket/ sees that one-line denial name the path and the fix: rerun the same claim command outside the sandbox or with escalated permissions.

  3. Attribution first. Every comment this skill posts opens with a one-line quote block. With an operator name configured:

    Written by an AI agent operating for <operator>. Verify before relying on it.

    With none configured:

    Written by an AI agent. Verify before relying on it.

    The name comes from ~/.config/ticket/config.json, key operator. No file, no key, or an empty value all mean the nameless form. Then the content. Never post an unattributed comment.

  4. The lock is the only entry to execution. A work order is a ticket comment whose fence header starts EXECUTION LOCK (any version) or, on a ticket still running the legacy protocol, WORK ORDER. start and revise locate it with the tracker contract's locate operation (references/tracker-contract.md): newest comment wins by post time across both protocols, and no field is ever merged from an older comment into a newer one. No order, no execution: refuse and route to /ticket triage <ticket-id>. Admission is the consumer's job: an unrecognized EXECUTION LOCK version or Source: mode refuses the same way rather than falling back to an older comment. A legacy WORK ORDER keeps today's sufficiency rules with no inferred pin, forever; any supersession uses the new protocol.

  5. One worktree, one branch, per ticket, for the whole lifecycle. triage cuts the branch and worktree through spin-worktree; start and revise reuse them; finalize tears them down. The first repository action after the summary-and-claim opening, before grounding or any repo read, is to cut or reuse the ticket's worktree. The one pre-worktree exception is fresh epic-child triage: it fetches and verifies the issue body's pinned remote parent-plan base, then passes that branch to the helper. Outside an epic child, grounding, scope ledgers, and the active change record are written and committed there; post-merge archiving follows operations.archive.guidance in a sibling archive checkout, which is the one narrow post-merge exception to this rule and lands through its own reviewed pull request rather than a push to main. An epic child keeps its instrumentation in session scratch and relies on its parent record. The control checkout may be dirty, stale, or on another branch: its working tree is never read or written, and it never switches branches. Never commit, stash, move, or clean its files, and never substitute another task's worktree as the control checkout. It holds the ticket's branch ref, which is what the worktree is cut from. Before its first write, every verb confirms that its working directory is the path the worktree helper reported; a mismatch stops the verb. A chunked order is the one exception and does not loosen the rule: chunk agents work in per-chunk worktrees cut from the ticket branch and torn down as each chunk merges back into it, so the ticket still ends with one branch and one pull request (references/coordinator-mode.md).

  6. Working state lives on the ticket. No scratch directories live on the branch. Outside an epic, the branch carries shipping code plus the repo's own change record. An epic child creates no per-child change record; its branch carries implementation plus any required parent-plan amendment that triage committed, while the parent epic's active change remains the authority.

  7. Ground in what the repo already says. Read the repo's own decision and change records, docs/, and recent git log before forming opinions. Read the standing-decisions source named below when a project configured one.

  8. Status transitions. Verbs move the ticket: triage to triaged, start to in progress when the branch is cut, start to pending review when the pull request opens, finalize to done. Status is the contract's one non-fatal operation: when a move is unavailable or fails, say so in one line and continue. Never retry a failed move and never force a workaround.

  9. Stop at the pull request boundary. Opening the pull request ends start. Merging is human. finalize only runs after a human merged.

  10. Fresh-session contract. start assumes no memory of triage. Under a legacy WORK ORDER, everything it needs must be on the ticket, in the description plus the work order's own copied prose. Under an EXECUTION LOCK, self-sufficiency means deterministic acquisition and verification of the authorized source instead of copied prose, and what that means depends on the source mode. For openspec and repository-native, the pinned commit OID plus the lock's own execution-shape fields (session fit, verification, expected diff) are enough for a fresh session to resolve, read, and admit the source itself, per start step 5. For inline, there is no commit to resolve: the fence's own Context/Do/Done when payload is the self-sufficient copy, the same as a legacy order's. Either way, if what a fresh session needs is not there, that is a triage defect: refuse and say what is missing.

The verification step

Every order names one verification step and one expectation for its output. The step is a slot:

  • Default: the target repo's own lint and tests, discovered from the repo. Read its AGENTS.md or CLAUDE.md for a test command, then its CI workflows, then its package scripts. Name the command in the order.
  • A binding may fill the slot with something stronger. An infrastructure preview is the worked example: a read-only plan against real state, run locally before the pull request. When a binding fills the slot, the order's expectation line describes that tool's output instead of a test result.
  • The rule that survives either way: iterate locally until the result is exactly what the order's expectation says. CI is the check of record, not the iteration loop.
  • Never fabricate expected output. When verification cannot run at all (no credentials, no access, no runnable suite), say so, open the pull request as a draft, and name the missing evidence.

The graph identity

Before a verb reads code structurally, it binds its current checkout to exactly one Codebase Memory project, from that checkout's own path:

python3 <cbm-onboard-skill-directory>/scripts/cbm-lifecycle.py ensure <worktree path>

It prints one object, and the verb reports it verbatim:

{"root_path": "<canonical physical checkout>", "project": "cbm-onboard-v1-<sha256>", "status": "ready"}
  • ready or indexed: query the graph as exactly that project. Never pick the graph by project name, branch-like label, list order, apparent recency, or because it was the only result.
  • unavailable (exit 2): follow the owning cbm-onboard skill's bounded supported-version and sandbox retry/fallback sequence first. An active-generation conflict means wait and retry the same checkout; it is not a sandbox escalation or authority to close another session. After that owned sequence is exhausted, say so in one line and use ordinary discovery.
  • Any other failure (exit 1): stop the verb and report what ensure printed on stderr, which names the cause — a path that is not a checkout, or an installed tool answering for the wrong project or root. Neither is a case where guessing a graph is safe.
  • The command never ran at all, no exit code, because the harness or sandbox refused it (a permission classifier declining the Bash call, for example): say so in one line and use ordinary discovery for the rest of the session, the same as unavailable.

Every session recomputes this from the checkout it just verified, never from chat memory or a remembered earlier run. It names one machine's paths, so it never goes into a work order or any other tracker comment; a chunk agent is handed its own in its prompt.

Before Git removes a worktree this skill authored, the same directory's cbm-teardown.sh deletes that checkout's project, while the checkout still exists for the identity to be derived from. Teardown fails loudly on a machine with no Codebase Memory installed, which is expected: report it in one line and carry on with the removal. It never holds up the removal, and it is never retried.

The hardening profile

The target repo declares Harden: <command> beside its test command in repo facts. Triage stamps Profile: hardening only when that line exists. It replaces the review rounds as start and revise specify. A hardening command that cannot run is an error, never a pass. The profile order's QA script lives in its pull request body.

Standing decisions

A project may point this skill at a knowledge base of standing decisions and traps to read before grounding in the repo. Its location is the project's to name: a path in the repo, a file the operator configured, or a page the binding knows about.

Absent, the verb says so in one line and continues. It never refuses a ticket for want of it.

The change record

The skill records the change where the target repo already records changes.

Whoever executes the change ticks its checklist as work completes, and a checked item means implemented and verified, not attempted. That is why a checkbox commit in a pinned source is the executor's own bookkeeping rather than an amendment (start step 5).

An epic child creates no per-child change record. Its parent epic owns the active change and its post-merge archive. Triage may commit a required parent-plan amendment in the child worktree; start, revise, and coordinator mode preserve the parent-plan bytes through the implementation pull request, and finalize leaves the parent active and unarchived.

Outside an epic, follow this per-ticket rule:

  1. The repo has an OpenSpec layout (openspec/): write the change folder on the ticket branch (proposal.md, tasks.md, and design.md when the work embodies a real decision). Start and revise keep the active change and its deltas reviewable in the ticket pull request; they do not fold or archive it before merge. The repository's operations.archive.guidance determines when finalization archives a verified merge, and the archive itself lands through a reviewed follow-up pull request that a human merges, never a direct push to the default branch. /openspec-adopt, when it is installed, is what adopts OpenSpec in a repo that lacks it. OpenSpec is the worked example, never a requirement.
  2. The repo has a different convention (a changelog, a decision-record tree, a design log): follow that convention exactly as the repo already uses it.
  3. The repo has no convention: write down what changed and why, where that repo's readers would look. Do not invent a convention for it.
Files (skills)
  • agents
    • openai.yaml 498 B
      interface:
        display_name: "Ticket"
        short_description: "Drive one tracked ticket through triage, start, revise, and finalize, one verb at a time"
        default_prompt: "Use $ticket to drive one ticket through triage, start, revise, or finalize. Explicit GPT-6 Astra executor admission is separate from reviewer eligibility. For delegated Ticket work, the coordinator dispatches every mandatory reviewer, resumes the same worker with the verified verdict, and missing review evidence is unavailable."
      
  • bindings
    • github-issues.md 4.3 KB
      # Binding: GitHub issues
      
      The reference binding for [the tracker contract](../references/tracker-contract.md).
      Ticket ids are issue numbers. Transport is the GitHub CLI (`gh`), authenticated
      for the target repository.
      
      Run every command with `--repo <org/repo>` so the binding works from a worktree
      whose remote is not the ticket's repository.
      
      ## Requirements
      
      * `gh` on `PATH`, authenticated (`gh auth status`).
      * Push and issue-write access to `<org/repo>`.
      
      `gh` absent or unauthenticated is a stop, not a degraded mode: report
      `ticket: github issues binding needs an authenticated gh; run gh auth login`,
      name the operation that could not run, and stop. Never substitute a local file,
      another tracker, or a guess at the ticket's contents.
      
      ## 1. Read a ticket
      
      ```sh
      gh issue view <id> --repo <org/repo> --json number,title,body,state,labels,parent,comments
      ```
      
      `parent` lets triage find a candidate parent; a second read through this same
      operation supplies that parent's `labels` so triage can confirm the `epic` type.
      `comments` arrives oldest-first, each with `body` and `createdAt`. Sort by
      `createdAt` when a verb wants newest-first.
      
      A missing issue exits non-zero with `Could not resolve to an issue`. That is the
      absent-ticket failure: stop, naming the id.
      
      ## 2. Post a comment on a ticket
      
      ```sh
      gh issue comment <id> --repo <org/repo> --body-file <path to the comment body>
      ```
      
      Write the body to a file in the ticket's worktree, outside the branch's tracked
      content, and pass it with `--body-file`. Passing prose through `--body` puts
      fenced blocks and quote lines at the mercy of shell quoting.
      
      ## 3. Move a ticket's status
      
      GitHub issues have no status workflow, so this binding maps the four states onto
      labels plus the issue's own open or closed state:
      
      | State | What the binding does |
      |---|---|
      | triaged | Receives triage's classification. `code` first creates (if needed) and attaches `build`, then adds `ticket:triaged`; `investigation` and `manual` add only `ticket:triaged`. |
      | in progress | add `ticket:in-progress`, remove `ticket:triaged` |
      | pending review | add `ticket:pending-review`, remove `ticket:in-progress` |
      | done | `gh issue close <id> --repo <org/repo>` and remove `ticket:pending-review` |
      
      Create a missing status label once with
      `gh label create ticket:<state> --repo <org/repo>`. For a `code` triage, first
      ensure and attach the independent type label:
      
      ```sh
      gh label create build --repo <org/repo> --color 1d76db --description "Implementable ticket" --force
      gh issue edit <id> --repo <org/repo> --add-label build
      gh issue edit <id> --repo <org/repo> --add-label ticket:triaged
      ```
      
      Creation failure or attachment failure is the contract's one non-fatal status
      failure: report it, retain the posted work order, and do not run the later
      `ticket:triaged` command. `investigation` does not create or attach `build` and
      applies only `ticket:triaged`. `manual` does not create or attach `build` and
      applies only `ticket:triaged`. Every other `ticket:*` transition
      remains exactly as listed above.
      
      A repository whose project board owns status is served the same way. The labels
      are this binding's status channel, and the board stays the humans' view.
      
      ## 4. Locate the newest work order
      
      ````sh
      gh issue view <id> --repo <org/repo> --json comments \
        --jq '[.comments[] | select(.body | test("(?m)^```[a-z]*\\s*\\r?\\n(EXECUTION LOCK |WORK ORDER)"))] | last'
      ````
      
      The scan is newest-first in effect: `gh` returns comments oldest-first, so the
      last match is the newest order, whichever protocol it is. The `EXECUTION LOCK `
      half of the predicate is version-agnostic on purpose: it matches any lock version,
      so a newer `EXECUTION LOCK v3` fence outranks an older `EXECUTION LOCK v2` one the
      same way a newer comment of either protocol outranks an older one. This operation
      never checks whether the version after `EXECUTION LOCK ` is one `start` or
      `revise` recognizes; that check is the consumer's fail-closed admission step, not
      this one. Empty output means no order, which is the refusal in `start` and
      `revise`, not an error to work around.
      
      A non-zero exit is a transport failure, and is reported as such rather than as an
      absent order.
      
      ## Markup
      
      GitHub-flavored markdown. Headings are `##`, the attribution line is a `>` quote,
      and the work order sits in a triple-backtick fence with no language tag. The
      templates carry the substance; this binding decides the markup.
      
  • references
    • brief-quality.md 573 B
      # Brief quality
      
      Apply this checklist while drafting the work order. It sharpens the order; it does
      not add an issue-body rewrite.
      
      * Separate verified facts, each with its evidence, from intent or assumptions.
      * State current behavior separately from desired behavior.
      * Name the key interfaces and traps the implementer must explore or preserve.
      * Describe the caller-facing interface shape, or say that the change is internal
        to an existing interface.
      * Make every Done when criterion observable and regression-proof.
      * State explicit out-of-scope work in Boundaries.
      
    • coordinator-mode.md 10.1 KB
      # Coordinator mode
      
      Reached from `start` step 6, on a chunked order only. `/orchestrate`'s rules bind:
      delegate the work, verify every result, never write the implementation yourself.
      Its carve-out binds too, and this flow leans on it twice: small mechanical glue
      stays with the coordinator, so a mechanical merge conflict (step 5) and a finding
      whose chunk agent is already gone (step 8) are fixed in place and mentioned in the
      report. Anything larger than mechanical goes back to a delegate.
      
      What follows is what this skill adds on top. It replaces `start` steps 8 through
      12, and rejoins that verb at step 13 when the last chunk has merged.
      
      An epic child creates no per-child change record. Coordinator work preserves the
      parent-plan bytes that triage committed, carries them through the child pull request,
      and leaves the parent active and unarchived at finalization; the parent owns the
      archive. Outside an epic, coordinator work keeps the ticket's active change and
      deltas reviewable until the human merge, then finalization follows the repository's
      archive guidance.
      
      1. **One branch, one pull request, still.** The ticket branch from `start` step 4 is
         the trunk. Each chunk gets its own branch cut from it and merges back. Nothing
         chunk-related reaches the default branch directly.
      
      2. **Spin a worktree per chunk**, from the ticket branch rather than from the remote
         default branch:
      
         ```sh
         git -C <control checkout> branch <ticket branch>-c<n> <ticket branch>
         python3 <spin-worktree-skill-directory>/scripts/spin-worktree.py \
           --repo <control checkout> \
           --branch <ticket branch>-c<n> \
           --name <ticket-id-lowercased>-c<n>
         ```
      
         Creating the chunk branch locally first starts it from the ticket branch's own
         tip, so the ticket branch does not need to be pushed. Spin parallel chunks'
         worktrees together, and serial ones only once their predecessor has merged into
         the ticket branch, because a serial chunk cut early misses the work it depends on.
      
      ## Chunk preparation
      
      Bind each chunk worktree's own graph identity first, per the skill page's
      graph-identity rule.
      
      `Surface lifecycle:` is part of that executable interface. Before dispatch,
         confirm every UI-affecting sub-order says `build` or `revise` and names the lock
         manifest or shipped behavior ledger/replay that mode consumes. On a rendered-
         surface chunk, `none`, a missing legacy field, or a missing contract is a triage
         defect. The worker loads the named UI Craft mode before implementing its `Do`
         section; non-UI chunks keep `none`.
      
      ## Chunk-worker dispatch
      
      3. **Dispatch one agent per chunk** at the tier its `Agent:` line names. The
         coordinator supplies the selected adapter, the explicit worker model resolved for
         that tier, and explicit worker effort. An explicitly admitted Astra coordinator
         may use these existing worker routes; its admission does not promote Astra into
         a worker or reviewer ladder. Dispatch only through
         `skills/drivers/orchestrate/scripts/codex-worker.py` or
         `skills/drivers/orchestrate/scripts/claude-worker.py`. Never use the built-in
         Agent tool, Workflow tool, background-agent machinery, or native agent dispatch.
      
         While a dispatched worker is still running, re-polled unchanged worker state,
         files, result sets, and result locators produce no update, including after three
         batches. Elapsed time alone is no news and never a milestone. Report completion,
         failure, an abandoned wait, and a predeclared operator-relevant external-state or
         coordinator decision/action milestone immediately. The shared state-change rule
         still reports any external state change the coordinator caused.
      
         The durable-order rule for this write-mode dispatch lives at
         `skills/drivers/orchestrate/SKILL.md` `## Collect child results`.
      
         For chunk `<n>` and dispatch attempt `<attempt>`, the coordinator writes the
         complete prompt bytes to
         `<session-scratch>/ticket-<ticket-id-lowercase>-chunk-<n>-attempt-<attempt>.prompt`
         and passes that file's contents as the adapter's positional prompt. The prompt
         carries the sub-lock plus verified source coordinates and selected identifiers,
         never restated plan prose: it is the sub-order fence verbatim, followed only by
         that chunk's worktree path, branch name, `root_path`, and `project`; the worker
         follows the graph-identity rule and uses as given the supplied `root_path` and
         `project` rather than resolving its own. An `unavailable` identity is passed
         through as such. A chunk never receives the ticket worktree's identity or a
         sibling's, and never coordinator commentary. A chunk agent that cannot read the
         sub-lock's pinned source from its own worktree stops and reports; the
         coordinator does not restate the source's plan prose to make the prompt
         self-contained.
      
         Start the worker through the selected adapter in `workspace-write` mode, with its
         cwd set to that chunk's worktree and the coordinator's checkout supplied as the
         control checkout. The coordinator owns
         `<session-scratch>/ticket-<ticket-id-lowercase>-chunk-<n>-attempt-<attempt>.state.json`
         for that dispatch. Same-worker follow-ups use the adapter's resume surface with
         that state file. If recovery is required, the coordinator runs the adapter's
         scoped stop surface and then its scoped verify surface before a successor receives
         the chunk worktree; a successor uses a new `<attempt>` and state file.
      
      ## Worker accounting
      
      Once the dispatcher exposes a stable transcript id, claim each unique
      implementation-worker session through the shared claim rule, passing
      `--verb start`, `--role worker`, `--session <id>`, `--agent <agent>`, and
      `--project <chunk-worktree>`. The role, agent and project name what the worker
      did, which agent did it, and its actual working directory, not the
      coordinator's. `--role worker` is what makes a chunk's cost evidence about
      chunk size; a worker claimed without it is read as coordinator overhead. Keep
      identifiers in coordinator bookkeeping; they never enter sub-order prompts or
      published comments. If the dispatcher exposes no stable transcript id, report the
      omitted claim in one line and continue. Claim failures use the shared visible,
      non-blocking rule.
      
      ## Reviewer selection
      
      4. **Review each chunk as it lands**, at that sub-order's stamped depth, before
         merging it. Two things happen, in order, and neither substitutes for the other:
      
         a. Run `/review` for the chunk diff, then apply the selected review skill's matrix
         entry from
         [review-routing.md](../../orchestrate/references/review-routing.md) using that
         chunk's stamped depth. Builder tier is not an input. Dispatch the selected
         reviewer on the chunk's branch against the ticket branch. Findings go back to
         the chunk's own agent to fix. Claim each dispatched reviewer session through the
         shared claim rule with `--verb start` and `--role reviewer`, plus the same `--session <id>`,
         `--agent <agent>` and `--project` it ran in, so review overhead is measured as
         overhead and never as chunk size. A reviewer with no stable transcript id is
         reported in one line like an unclaimable worker, and a claim failure follows the
         same shared non-blocking rule: neither ever holds up dispatching the review.
      
         b. Verify the result yourself, as `/orchestrate` requires of every delegated
         result: read the diff, run the verification command, check the chunk's Done when
         clause. A failed verification retries once in the chunk's agent with the specific
         finding, then escalates one tier per the routing table. Same-session retries are
         not re-claimed; claim every fresh implementation escalation once, using the
         escalation's stable transcript id, agent, and chunk worktree.
      
      ## Chunk integration
      
      5. **Merge into the ticket branch** in the ticket's worktree, one chunk at a time,
         with `--no-ff`. A conflict between two chunks that declared disjoint ownership
         means the slice was wrong: resolve it yourself only when it is mechanical, and say
         so in the report; anything else goes back to the user as a slicing defect. After a
         chunk merges, remove its worktree and delete its branch (run
         `<cbm-onboard-skill-directory>/scripts/cbm-teardown.sh <path>` while that checkout
         still exists, then `rm -f <path>/ORDER.md`,
         `git -C <control checkout> worktree remove <path>`, and
         `git -C <control checkout> branch -D <chunk branch>`). Chunk branches are never
         pushed, so there is no remote branch to delete.
      
      6. **Record the change yourself**, on the ticket branch, after the chunks have
         merged, per the skill page's change-record rule. Chunks never touch it, which is
         why parallel chunks cannot collide there.
      
      7. **Run the verification command on the merged branch**, not per chunk. The order's
         expectation describes the whole ticket.
      
      8. **Repo-rules audit, then whole-diff review, before the pull request.** Re-read the
         repo's `AGENTS.md` or `CLAUDE.md` and audit the merged diff against it rule by
         rule, including any completion checklist it defines, exactly as the flat path
         does; chunk agents each saw only their own slice, so nothing has audited the whole.
         Fix violations, then run `/review` on the ticket branch against the default
         branch, at the depth [review-depth.md](review-depth.md) sets for a whole diff.
         Findings route back to the chunk agent that owns the file when its session is
         still alive, and otherwise the coordinator fixes them and says so.
      
      9. **Preflight the outbound OpenSpec change.** Only an ordinary OpenSpec-backed
         ticket uses this gate. After all chunks merge, whole-diff review and fixes finish,
         and the coordinator records the active change, run `git fetch origin` immediately
         before:
      
         ```sh
         python3 <ticket-skill-directory>/scripts/ticket.py preflight-openspec \
           --repo <ticket-worktree> \
           --base-ref refs/remotes/origin/HEAD
         ```
      
         The fetch refreshes the base that the command resolves locally. A ticket using
         another or no change-record convention, or an epic child, bypasses this gate
         unchanged. Fetch, ref, or preflight failure stops visibly; do not rejoin pull
         request creation. This adds no chunk integration or review ownership, preserves
         one branch and one pull request, and leaves finalization the sole authoritative
         archive owner.
      
    • drafting-conventions.md 1023 B
      # Drafting conventions
      
      ## Drafting conventions
      
      Transcribe a target repository's `AGENTS.md` `Test:` entry byte-exact into the
      order. When this host's interpreter substitution matters, state it separately and
      exactly: `Run every python3 above as /opt/homebrew/bin/python3.14; bare python3
      on this host is 3.9.6.`
      
      Adapter prompts are prompt text passed positionally. The coordinator writes each
      complete prompt to session scratch, passes that file's contents as the adapter's
      positional prompt text, and never invents adapter flags or changes. Each dispatch
      has one coordinator-owned state file; state is lifecycle metadata, never the
      worker's result. The durable-order rule for adapter prompts lives at
      `skills/drivers/orchestrate/SKILL.md` `## Collect child results`.
      
      An expected diff is a closed allowlist of repository-relative paths. It has no
      escape clause. A generated-facts appendix records deterministic commands and their
      byte-complete literal output; every cited line is regenerated from the checked-out
      tree.
      
    • review-actions.md 850 B
      # Review actions
      
      Ground each finding before choosing exactly one disposition.
      
      * **Fix before completion.** It breaks the work order; fix and verify it in this
        pull request.
      * **Necessary follow-up.** It is real but outside the order; file a ticket with
        evidence, desired outcome, and a checked duplicate search. Keep it out of this
        pull request. Under an epic, read
        [the epic tracker contract](../../epic/references/tracker-contract.md): when the
        epic destination requires it, file the follow-up as an in-scope native child;
        otherwise file it as a native child with its `spike` or `build` type plus
        `deferred`, and report it on the originating ticket.
      * **Ask the maintainer.** A real choice remains; surface it before editing.
      * **Discard as preference.** It is unsupported reviewer taste; make no change and
        say why in the reply.
      
    • review-depth.md 3 KB
      # Review depth
      
      Applies to every order, flat or chunked. Triage stamps one depth per order or
      sub-order; `start` and `revise` execute at that depth.
      
      ## The three depths
      
      | Depth | What the reviewer checks | Fits |
      |---|---|---|
      | **Focused** | The exact change asked for, and that nothing else moved | A one-line value change, a version bump, a doc typo |
      | **Targeted** | The changed behavior end to end, plus the repo rules that govern it | Most orders: a new resource, a workflow step, a bounded refactor |
      | **Full** | The whole diff, every check the repo defines, adversarially | Anything sensitive, anything wide, anything the floor below forces |
      
      ## Stamping
      
      * Triage stamps a depth with a one-line reason on every order and every
        sub-order: `Review depth: targeted (one new resource in one target, no shared
        behavior)`.
      * An order arriving without a depth is reviewed **Targeted**. Absence is a triage
        defect, not a licence to review lightly.
      * Depth escalates mid-review whenever the diff turns out wider or more sensitive
        than the stamp assumed. It never downgrades: a Full stamp stays Full even when
        the diff looks small.
      
      ## Sensitivity floor
      
      Judgment, not a keyword match. A change is **Full**, non-negotiably, when it
      touches any of:
      
      * authentication, authorization, or identity (trust policies, role assumption,
        single sign-on, token scope)
      * secrets: creation, rotation, scope, or exposure surface
      * destructive or irreversible operations (deletes, replaces, force-applies,
        data-bearing resources)
      * behavior shared across an organization (a shared library, an organization-level
        setting, a workflow every repo inherits)
      
      For workflow machinery every repo inherits: Full when the change alters contract
      semantics; Targeted for pure relocation, citation repoints, and additive paragraphs
      that no existing consumer's behavior depends on.
      
      These override a lower stamp without discussion.
      ## What blocks
      
      A finding blocks only when it breaks the order's **Done when** clause. That is the
      contract; reviewer taste is not.
      
      * Blocking: the acceptance criteria will not hold, the verification step's
        expectation will not match, a repo rule the order named is violated.
      * Not blocking: anything real but outside the order. It becomes a follow-up ticket
        or it is discarded. Never a silent fix, never a scope expansion.
      
      ## Reviewer dispatch boundary
      
      Under `Profile: hardening`, Targeted and Focused orders get no reviewer; Full-depth
      orders keep one review round after hardening.
      
      Review depth is an input to
      [review-routing.md](../../orchestrate/references/review-routing.md), which owns
      reviewer classification, eligibility, and model precedence.
      
      An executor or coordinator admitted through explicit host metadata is not thereby
      eligible to review. Keep the existing reviewer route, Full-depth requirement, and
      headroom checks; report an unavailable reviewer route rather than silently
      downgrading it.
      
      * A whole diff assembled from chunks is reviewed Targeted, or Full when any chunk
        was Full.
      
    • slicing.md 8.8 KB
      # Slicing a work order into chunks
      
      Read during `triage`. Decides whether one order or several, how big each chunk
      is, and what model tier each one names.
      
      ## The trait rubric
      
      Slice when **two or more** of these hold. One or zero: the order stays flat.
      
      | Trait | Fires when |
      |---|---|
      | Multiple targets or environments | The change lands in more than one deployment target, environment, or region |
      | Live-resource import or tool port | Live resources are brought under the repo's control, or moved from one tool to another |
      | Writes across a trust boundary | The change writes in more than one account, project, or trust boundary |
      | Multiple deliverable artifacts | More than one shippable or independently reviewed thing: a library change plus its consumers, a workflow plus the scripts it calls, code plus a runbook, or a command plus the workflow and specification that consume it |
      | Live run inside the ticket | Acceptance requires standing up and running the artifact before the pull request — real infrastructure, or a local harness the ticket must build first (a browser driver, a seeded database, an offline server) — so what the run exposes is corrected in the same session |
      | Split-path evidence | Acceptance requires proving the same behavior on more than one code path that a single run cannot both exercise — a platform or feature-flag branch, or a re-implementation in another language held identical by test — so each path costs its own harness |
      | Lockstep copies of one fact | One fact is obliged to appear in more than two encodings that no single tool checks together: a source of truth, a hand-maintained transcription, a fixture generator and the fixture it freezes, or one rule restated in separately installed artifacts that never see each other at run time |
      | Lifecycle-gated surface revision | A shipped user-facing surface must first lock its visual contract, then implement it and prove it through a browser evidence matrix; the lock, implementation, and evidence each consume the same ticket's context |
      | In-flight scope replacement | A new work order rejects a pull-request-sized implementation already on the ticket branch, and reconciling it and building the replacement are each projected at or above the 120k chunk floor |
      
      The traits are proxies for context load, not for effort. A long-but-uniform change
      (twenty near-identical grants in one target) fires nothing and stays flat, while a
      short change that writes across two accounts and imports live resources fires
      twice and gets sliced. The first four ask where the code lands. The fifth asks
      whether the ticket also has to operate it, which costs a discovery-and-fix cycle
      per surprise plus whatever the run itself takes. The sixth asks what it costs to
      prove the change: a diff of two lines can owe two harnesses when the paths it
      touches cannot both run at once, and building the second one is the work. The
      seventh asks how many places one fact is written down: every encoding of that
      fact costs its own pass, whether they are chained from a single source of truth
      or restated independently of one another, and they drift because nothing checks
      them together.
      
      ## Sizing
      
      Every execution session carries roughly 90k of fixed overhead (skill load,
      grounding, review) before it touches the work.
      
      That overhead and the two thresholds below describe what one agent building one
      piece of work costs, so only a session claimed `--role worker` measures them. A coordinator's peak and a
      reviewer's are recorded separately and tune nothing here. A coordinator over the
      band on an otherwise-held slice is `coordination-degraded`; carry less in that
      session, never cut more chunks.
      
      * Target each chunk at a projected peak under 180k.
      * Never slice below one pull-request-sized piece of work. A chunk that would peak
        under 120k is mostly overhead; fold it into a neighbour.
      * Practical ceiling: four chunks. Promote to `/epic` only when more than four
        projected chunks leave at least one decision unsettled. Purely mechanical
        oversize is hand-split into serial `build` tickets instead: the epic apparatus
        is for fog, not bulk.
      
      ## Where these numbers came from
      
      These thresholds were measured on one operator's own sessions, on one machine,
      against that operator's repositories. The mechanism generalizes and the constants
      may not: fixed overhead moves with how much a project's grounding costs, and the
      degradation band moves with the model in use.
      
      So another installation re-tunes rather than trusting them, and it re-tunes per
      repository. This page carries no measured anchors of its own. Calibration accrues
      in each repository's reviewer-memory store, which `finalize` appends a slicing
      record to at the end of every finished ticket and `triage` reads before choosing a
      shape. After a handful of tickets, that store holds this repository's own anchors
      and shows whether the 180k target and the 120k floor are right on its work.
      
      Moving the thresholds themselves is a change to this page, not to a store: the
      180k target and the 120k floor are paired with constants in the ticket helper
      (`scripts/ticket.py`), so a genuine move changes the prose and the constants
      together. That is operator-initiated skills-repo work; `finalize` reports a
      misprediction and proposes nothing.
      
      ## Chunk shape
      
      Each chunk is a self-contained sub-lock that a fresh agent can execute with only
      the ticket and that sub-lock in front of it. No chunk may say "as established in
      chunk 1".
      
      * **Mode** is `parallel` (no ordering constraint against other parallel chunks) or
        `serial after <n>` (needs another chunk's result on the branch first). Two chunks
        that touch the same file are serial, not parallel.
      * **File ownership** is declared per chunk. Every chunk names the files or targets
        it owns, and two parallel chunks' ownership is disjoint, so they cannot collide.
      * **Task and acceptance-anchor ownership** is declared per chunk when the header
        lock's `Source:` is `openspec` or `repository-native`. Whole-change ownership is
        a property of the ticket, not of the chunking: the header's own `Selected
        tasks:`/`Acceptance anchors:` already carry the ticket's full entitlement — `all`
        for an ordinary ticket, the epic child's owned subset otherwise — and chunking
        never widens or narrows that entitlement. Each sub-lock's `Selected tasks:` and
        `Acceptance anchors:` are a disjoint positional slice of the header's own
        selection, and the sub-locks together cover exactly what the header selected, no
        more and no less; no sub-lock restates another sub-lock's selection or the
        change's Context/Do/Done-when prose, since the pinned commit is the authority. An
        `inline` header carries no pinned source to select against, so its sub-locks keep
        today's per-chunk `Do` steps instead.
      * **Capability ownership** is declared per chunk. A chunk owns one coherent
        capability together with its named files or targets; every capability has exactly
        one owning chunk.
      * **Shared-contract ownership** is explicit. Name every shared contract and give it
        exactly one owning chunk. Other chunks may rely on the stated shared contract,
        never on that chunk's private capability.
      * **Parallel isolation** follows from that ownership. A parallel chunk must not
        implement, revise, or depend on another chunk's private capability; make it
        serial when that work or dependency is real.
      * **Agent tier** comes from
        [the routing table](../../orchestrate/references/routing-table.md): classify the
        chunk into an area (exploration, hermetic implementation, documentation, review)
        and read the route off. Never name Fable, which is the coordinator tier only.
      * **Review depth** comes from [review-depth.md](review-depth.md), stamped with its
        one-line reason.
      * A ticket firing **live run inside the ticket** slices at the run: one chunk
        builds and tests the artifact against a stub, and a `serial after` chunk runs it
        with the operator and folds what the run exposes back into the code and the
        runbook.
      * When **multiple deliverable artifacts**, **live run inside the ticket**, and
        **lockstep copies of one fact** all fire together, a server sub-order must not
        own both domain projection/association semantics and registry, concurrency, or
        cache-lifetime behavior, and a surface sub-order must not own both the shipped
        consumer state machine and its generated fixture/mirror/recovery-matrix
        evidence: slice into four — source/projection semantics, registry and
        lifecycle contract, shipped consumer, then generated evidence and live
        replay — folding a piece back into its neighbour only when it would fall
        below the 120k floor.
      
      ## Orchestrator tier
      
      The order's `Open as:` names the tier the coordinator session must run at: the
      **highest** tier any chunk names (haiku < sonnet < opus), and never Haiku, which
      cannot review
      ([review-routing.md](../../orchestrate/references/review-routing.md)). The coordinator never launches
      an agent smarter than itself.
      
    • tracker-contract.md 4.9 KB
      # The tracker contract
      
      Four operations, and nothing else. The verbs call these; they never call a
      tracker's API directly, and they never reach a second tracker when the first one
      is unreachable. A verb that cannot reach the contract stops and names what is
      missing.
      
      One binding page supplies all four for one tracker.
      [bindings/github-issues.md](../bindings/github-issues.md) is the reference
      binding and ships with the skill.
      
      ## 1. Read a ticket
      
      * **Input:** a ticket id.
      * **Output:** the ticket's title, its description body, and its comments in a
        known order, each comment carrying its body text and its creation time. A verb
        that needs newest-first order sorts by that time itself.
      * **Failure:** the ticket does not exist, or the tracker is unreachable. The verb
        stops, names the ticket id, and names the transport that failed. It never
        proceeds on a partial read.
      
      ## 2. Post a comment on a ticket
      
      * **Input:** a ticket id and one comment body, written by the skill in the markup
        the binding declares.
      * **Output:** confirmation that the comment landed, and its locator when the
        tracker returns one.
      * **Failure:** the post is rejected or the transport fails. The verb prints the
        comment body it was going to post, so nothing is lost, then stops. It never
        retries silently and never drops the content.
      
      ## 3. Move a ticket's status
      
      * **Input:** a ticket id and one target state from this skill's vocabulary:
        triaged, in progress, pending review, or done. A move to `triaged` also receives
        triage's classification: `code`, `investigation`, or `manual`.
      * **Output:** the ticket's state after the move.
      * **Classification rule:** for a code classification, the binding first ensures
        the `build` label exists and is attached, then applies `ticket:triaged`. For
        `investigation` and `manual`, it neither creates nor attaches `build`, and applies
        only the status transition. Type labels and `ticket:*` status remain independent.
      * **Failure:** the state does not exist in the tracker's workflow, the move is not
        permitted, or the transport fails. A `code` path that cannot create or attach
        `build` is this operation's one non-fatal status failure: report it in one line,
        retain any posted work order, and do not apply the later `ticket:triaged` label.
        Never retry a failed move, and never substitute a different state to make the move
        succeed.
      
      ## 4. Locate the newest work order
      
      * **Input:** a ticket id.
      * **Output:** the body of the newest comment whose fence header starts
        `EXECUTION LOCK ` (any version; flat lock or chunked header) or the legacy
        `WORK ORDER`, or nothing when no comment has either header. The header match is
        version-agnostic by design: a v3 lock posted after a v2 lock is a newer
        `EXECUTION LOCK ` header and wins, whatever version each names. Newest wins
        across both protocols by comment time, regardless of which protocol is newer;
        older orders of either protocol are superseded, never merged, and no field is
        ever merged from an older comment into a newer one, protocol boundary or not.
        This operation matches on the fence header alone; it does not parse or validate
        the `EXECUTION LOCK` version or `Source:` mode inside the fence.
      * **Admission is the consumer's job, and it fails closed.** `start` and `revise`
        parse the located comment before acting on it. An unrecognized `EXECUTION LOCK`
        version, or a `Source:` mode this protocol does not define, refuses execution
        and routes to `/ticket triage <ticket-id>`; it never falls back to an older
        comment, located or not; and this operation is not consulted again to find one.
        This is the only place an unrecognized version can surface: the locate operation
        above always returns the newest header-matching comment, recognized or not.
      * **Failure:** nothing found means no execution. `start` and `revise` refuse and
        route to `/ticket triage <ticket-id>`. A transport failure is not the same
        answer as an absent order: report which one happened.
      
      ## What a binding page supplies
      
      One page, per operation:
      
      1. The concrete command or tool call, with its inputs named.
      2. What must be installed and authenticated for that call to work.
      3. What the operation maps onto when the tracker has no equivalent (status is the
         usual case).
      4. The markup comments are written in, since the binding owns markup and the
         templates do not.
      5. The exact message the binding produces when its tracker or its transport is
         absent.
      
      ## Writing a binding for another tracker
      
      One page, the four operations above, in the same order, with the five items
      above filled in for each.
      
      * A binding whose tracker or transport is absent stops with a clear message that
        names what is missing and how to supply it.
      * It never falls back to a different tracker, and never invents a local
        substitute for a ticket.
      * A binding that cannot implement an operation says so on the page. Read and
        post are load-bearing: a binding missing either one cannot run the verbs.
      
  • scripts
    • ticket.py 38.9 KB
      #!/usr/bin/env python3
      """Context telemetry for the ticket workflow: measure what a ticket cost.
      
      Each verb claims its own session against the ticket it is working, so the set
      of sessions that worked a ticket is recorded rather than inferred, each with the
      role it played and the lifecycle verb that produced it. Reports peak context per
      claimed session, then records the actuals so the slicing rubric can be retuned
      against real numbers. Every record carries counts and labels supplied on the
      command line: never a transcript excerpt, a prompt, or any other prose from a
      session.
      """
      
      from __future__ import annotations
      
      import argparse
      import inspect
      import json
      import os
      import re
      import shutil
      import subprocess
      import sys
      import tarfile
      import tempfile
      from datetime import datetime, timezone
      from pathlib import Path, PurePosixPath
      from typing import Iterator, Optional
      
      PROJECTS_DIR = Path(
          os.environ.get("CLAUDE_PROJECTS_DIR", str(Path.home() / ".claude" / "projects"))
      ).expanduser()
      CLAIMS_PATH = Path(
          os.environ.get(
              "TICKET_CLAIMS",
              str(Path.home() / ".config" / "ticket" / "claims.jsonl"),
          )
      ).expanduser()
      CODEX_SESSIONS_DIR = Path(
          os.environ.get("CODEX_HOME", str(Path.home() / ".codex"))
      ).expanduser() / "sessions"
      CODEX_ARCHIVED_SESSIONS_DIR = CODEX_SESSIONS_DIR.parent / "archived_sessions"
      
      # Which environment variable carries the running session id, per agent. A verb
      # that cannot read one passes --session instead.
      SESSION_VARIABLES = (("claude", "CLAUDE_CODE_SESSION_ID"), ("codex", "CODEX_SESSION_ID"))
      
      # What a claimed session was doing, so cost is attributed to the work that spent
      # it. The coordinator drives the ticket, a worker builds one chunk, a reviewer
      # only reviews. A claim written before roles existed is read back as `legacy`,
      # which is not a guess about which of the three it was.
      ROLES = ("coordinator", "worker", "reviewer")
      LEGACY_ROLE = "legacy"
      
      # Which lifecycle phase produced a claim. A claim written before verbs existed
      # is read back as `legacy`; the reader never guesses from role or ordering.
      VERBS = ("triage", "start", "revise", "finalize")
      LEGACY_VERB = "legacy"
      
      # A session past this peaked into the degradation band: it should have been
      # sliced below this line.
      DEGRADE_PEAK = 180_000
      # A chunk session under this was mostly fixed overhead, not real work.
      FLOOR_PEAK = 120_000
      
      
      class TelemetryError(RuntimeError):
          """A safe, user-facing telemetry failure."""
      
      
      class OpenSpecPreflightError(RuntimeError):
          """A safe, user-facing failure while proving an OpenSpec change applies."""
      
          def __init__(self, message: str, exit_status: int = 1) -> None:
              super().__init__(message)
              self.exit_status = exit_status
      
      
      def validate_ticket_id(value: str) -> str:
          if not value or any(character.isspace() for character in value):
              raise TelemetryError("ticket id must be a single token with no whitespace")
          return value
      
      
      def validate_session_id(value: str) -> str:
          """A session id is pasted in from elsewhere and reaches a filesystem glob.
      
          An id carrying glob syntax matches transcripts the claim never named:
          `--session '*'` resolved to every transcript on the machine and reported
          their maximum as one session's peak.
          """
          if not value or any(character.isspace() for character in value):
              raise TelemetryError("session id must be a single token with no whitespace")
          forbidden = set("*?[]/\\")
          if forbidden & set(value):
              raise TelemetryError("session id must not contain glob or path characters")
          return value
      
      
      def _normalize_remote(url: str) -> str:
          """Collide an ssh and an https remote for one repository to one string.
      
          `git@github.com:owner/repo.git` and `https://github.com/owner/repo` name
          the same repository; both fold to `github.com/owner/repo` so a claim made
          through either form resolves to the same identity.
          """
          url = url.strip()
          if url.endswith(".git"):
              url = url[: -len(".git")]
          scp_match = re.match(r"^[^/@]+@([^:]+):(.+)$", url)
          if scp_match:
              return f"{scp_match.group(1)}/{scp_match.group(2)}"
          url_match = re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*://(?:[^/@]+@)?([^/]+)/(.+)$", url)
          if url_match:
              return f"{url_match.group(1)}/{url_match.group(2)}"
          return url
      
      
      def resolve_repo(path: Path) -> Optional[str]:
          """Name the repository a checkout path belongs to, derived from disk.
      
          Never raises: a missing `git`, a path outside any checkout, or a checkout
          with no origin remote all fall through to the next step rather than
          blocking whichever claim, scan, or record call needs this. Tried in order:
          the origin remote (normalized so ssh and https collide), then the
          checkout's own toplevel path, then `None`.
          """
          try:
              remote = subprocess.run(
                  ["git", "-C", str(path), "remote", "get-url", "origin"],
                  stdout=subprocess.PIPE,
                  stderr=subprocess.PIPE,
                  text=True,
                  check=False,
              )
          except OSError:
              remote = None
          if remote is not None and remote.returncode == 0 and remote.stdout.strip():
              return _normalize_remote(remote.stdout.strip())
      
          try:
              toplevel = subprocess.run(
                  ["git", "-C", str(path), "rev-parse", "--show-toplevel"],
                  stdout=subprocess.PIPE,
                  stderr=subprocess.PIPE,
                  text=True,
                  check=False,
              )
          except OSError:
              toplevel = None
          if toplevel is not None and toplevel.returncode == 0 and toplevel.stdout.strip():
              return toplevel.stdout.strip()
      
          return None
      
      
      def context_size(usage: dict) -> int:
          return sum(
              int(usage.get(field) or 0)
              for field in (
                  "input_tokens",
                  "cache_read_input_tokens",
                  "cache_creation_input_tokens",
              )
          )
      
      
      def detect_session(explicit: Optional[str], agent: Optional[str]) -> tuple[str, str]:
          """Name the session this verb is running in, so the claim is a fact.
      
          The old scan searched every transcript for the ticket id and counted the
          sessions whose operator prose contained it. That guessed at attribution and
          got it wrong in both directions: digits inside a percentage or a commit sha
          counted, while an agent-filed ticket whose id the operator never typed did
          not. The session working a ticket is known at the moment it works it, so it
          is recorded here instead.
      
          An agent that publishes its session id in the environment needs no flags. A
          dispatched worker whose coordinator holds the id passes both, because which
          agent wrote a session decides where its transcript lives and how its context
          is counted.
          """
          visible = [
              (name, os.environ[variable])
              for name, variable in SESSION_VARIABLES
              if os.environ.get(variable)
          ]
          names = " or ".join(name for name, _ in SESSION_VARIABLES)
          if agent and not explicit:
              raise TelemetryError("--agent needs --session")
          if agent:
              return agent, explicit
          # A Codex worker launched from a Claude session inherits that session's
          # variable, so two visible ids mean the environment cannot say which agent
          # is running. Guessing there records the coordinator's transcript against
          # the worker's ticket.
          if len(visible) > 1:
              raise TelemetryError(f"more than one agent session in the environment: pass --agent ({names})")
          if explicit:
              if visible:
                  return visible[0][0], explicit
              raise TelemetryError(f"--session needs --agent ({names}) outside a known agent")
          if visible:
              return visible[0]
          variables = " or ".join(variable for _, variable in SESSION_VARIABLES)
          raise TelemetryError(f"no session id: pass --session, or run where {variables} is set")
      
      
      def read_claims(ticket_id: str) -> list[dict]:
          if not CLAIMS_PATH.exists():
              return []
          claims = []
          for line in CLAIMS_PATH.read_text(encoding="utf-8").splitlines():
              if not line.strip():
                  continue
              try:
                  claim = json.loads(line)
              except json.JSONDecodeError:
                  continue
              if claim.get("ticket_id") == ticket_id and claim.get("session_id"):
                  claims.append(claim)
          return claims
      
      
      def append_claim(claim: dict) -> tuple[dict, bool]:
          """Record one session against one ticket, returning authoritative state."""
          for existing in read_claims(claim["ticket_id"]):
              if existing.get("session_id") == claim["session_id"]:
                  return existing, False
          CLAIMS_PATH.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
          with CLAIMS_PATH.open("a", encoding="utf-8") as handle:
              handle.write(json.dumps(claim) + "\n")
          CLAIMS_PATH.chmod(0o600)
          return claim, True
      
      
      def transcripts_for(claim: dict, projects_dir: Path) -> list[Path]:
          """Find a claimed session's transcripts by id, never by their contents.
      
          A resumed session writes more than one file, so this returns every match
          and the caller takes the peak across them. Picking one would report
          whichever the filesystem happened to sort first.
          """
          session_id = claim["session_id"]
          if claim.get("agent") == "codex":
              pattern = f"**/rollout-*-{session_id}.jsonl"
              return sorted(
                  {
                      *CODEX_SESSIONS_DIR.glob(pattern),
                      *CODEX_ARCHIVED_SESSIONS_DIR.glob(pattern),
                  }
              )
          parent_sessions = projects_dir.glob(f"*/{session_id}.jsonl")
          native_workers = projects_dir.glob(f"*/*/subagents/agent-{session_id}.jsonl")
          return sorted([*parent_sessions, *native_workers])
      
      
      def claude_peaks(raw: bytes, *, claimed_worker: bool = False) -> tuple[Optional[str], int, int]:
          started = None
          peak = 0
          subagent_peak = 0
          for line in raw.splitlines():
              if not line.strip():
                  continue
              try:
                  entry = json.loads(line)
              except json.JSONDecodeError:
                  continue
              if started is None and entry.get("timestamp"):
                  started = entry["timestamp"]
              if entry.get("type") != "assistant":
                  continue
              size = context_size(entry.get("message", {}).get("usage", {}))
              if entry.get("isSidechain") and not claimed_worker:
                  subagent_peak = max(subagent_peak, size)
              else:
                  peak = max(peak, size)
          return started, peak, subagent_peak
      
      
      def codex_peaks(raw: bytes) -> tuple[Optional[str], int, int]:
          """Codex reports one input total per turn, already counting its cached part.
      
          Summing the cached field back in would double-count it, so this is a
          maximum over one field rather than the three-field sum a Claude transcript
          needs. Codex writes no sub-agent turns into a rollout, so that peak is zero.
          """
          started = None
          peak = 0
          for line in raw.splitlines():
              if not line.strip():
                  continue
              try:
                  entry = json.loads(line)
              except json.JSONDecodeError:
                  continue
              if started is None and entry.get("timestamp"):
                  started = entry["timestamp"]
              payload = entry.get("payload", {})
              if entry.get("type") != "event_msg" or payload.get("type") != "token_count":
                  continue
              usage = (payload.get("info") or {}).get("last_token_usage") or {}
              peak = max(peak, int(usage.get("input_tokens") or 0))
          return started, peak, 0
      
      
      def session_cost(claim: dict, projects_dir: Path) -> dict:
          paths = transcripts_for(claim, projects_dir)
          started = None
          peak = 0
          subagent_peak = 0
          for path in paths:
              if claim.get("agent") == "codex":
                  first, own, sub = codex_peaks(path.read_bytes())
              else:
                  first, own, sub = claude_peaks(
                      path.read_bytes(), claimed_worker=path.parent.name == "subagents"
                  )
              started = min(x for x in (started, first) if x) if (started or first) else None
              peak = max(peak, own)
              subagent_peak = max(subagent_peak, sub)
          return {
              "session_id": claim["session_id"],
              "agent": claim.get("agent"),
              "role": claim.get("role") or LEGACY_ROLE,
              "verb": claim.get("verb") or LEGACY_VERB,
              "project": claim.get("project"),
              "started": started or claim.get("claimed_at"),
              "peak_context": peak,
              "subagent_peak": subagent_peak,
              "transcripts": [str(path) for path in paths],
          }
      
      
      def scan(ticket_id: str, projects_dir: Path, current_repo: Optional[str]) -> dict:
          """Report peak context for this ticket id, scoped to one repository.
      
          A claim's `repo` was resolved once, at claim time, from the checkout it
          ran in. Reading it back here keeps two repositories' same-numbered
          tickets from merging into one measurement: a claim from another
          repository is counted but never folded in, and a claim with no
          resolvable repository (a pre-existing claim written before this field
          existed, or a resolution failure) is named rather than silently dropped
          or silently counted.
          """
          own_claims = []
          excluded = 0
          unattributable = []
          for claim in read_claims(ticket_id):
              repo = claim.get("repo")
              if repo:
                  if repo == current_repo:
                      own_claims.append(claim)
                  else:
                      excluded += 1
              else:
                  unattributable.append(claim["session_id"])
      
          sessions = [session_cost(claim, projects_dir) for claim in own_claims]
          sessions.sort(key=lambda session: session["started"] or "")
          measured = [session for session in sessions if session["transcripts"]]
      
          def peaks_for(role: str) -> list[int]:
              return [session["peak_context"] for session in measured if session["role"] == role]
      
          def peak_for_verb(verb: str) -> int:
              return max(
                  (session["peak_context"] for session in measured if session["verb"] == verb),
                  default=0,
              )
      
          return {
              "ticket_id": ticket_id,
              "repo": current_repo,
              "session_count": len(measured),
              "claim_count": len(sessions),
              "unreadable": [
                  session["session_id"] for session in sessions if not session["transcripts"]
              ],
              "excluded_claims": excluded,
              "unattributable": unattributable,
              "peak_context": max((session["peak_context"] for session in measured), default=0),
              "subagent_peak": max((session["subagent_peak"] for session in measured), default=0),
              "coordinator_peak": max(peaks_for("coordinator"), default=0),
              "worker_peaks": peaks_for("worker"),
              "reviewer_peak": max(peaks_for("reviewer"), default=0),
              "legacy_peak": max(peaks_for(LEGACY_ROLE), default=0),
              "verb_peaks": {
                  verb: peak_for_verb(verb) for verb in (*VERBS, LEGACY_VERB)
              },
              "claimed_workers": len([session for session in sessions if session["role"] == "worker"]),
              "sessions": sessions,
          }
      
      
      def verdict(actual: dict, chunked: bool, chunks: int) -> tuple[str, str]:
          """Compare the shape triage stamped against what the work actually cost.
      
          Only the sessions that built the work are evidence about how big the work
          was. Review-only sessions are overhead on every order, so their peaks are
          reported and never judged. A flat order is judged on its own peak.
      
          A chunked order is judged on its implementation workers' peaks alone.
          A coordinator's context grows with dispatches, returned results, review
          rounds and merges, so slicing the same work more finely can raise it while
          lowering every chunk's peak: reading it as chunk size inverts the answer.
          When every chunk worker held under the band but the coordinator did not,
          `coordination-degraded` reports that the slice was right and coordination
          was not. Incomplete worker coverage never yields `coordination-degraded`;
          it falls through to `ok` only when no earlier branch fired, because an
          unmeasured chunk could itself have crossed the band.
          `subagent_peak` cannot stand in either, because a coordinator dispatches
          review panels as sub-agents too and the transcript cannot tell the two
          apart — and per ADR 70 attribution comes from explicit claims, never from
          transcript shape. With no measured worker there is therefore no measurement
          of chunk size, which is `coordinator-only` rather than a guess.
          """
          if actual["claim_count"] == 0:
              excluded = actual.get("excluded_claims") or 0
              unattributable = actual.get("unattributable") or []
              if excluded or unattributable:
                  parts = []
                  if excluded:
                      parts.append(f"{excluded} claim(s) from another repository")
                  if unattributable:
                      parts.append(f"{len(unattributable)} claim(s) with no resolvable repository")
                  repo_name = actual.get("repo") or "this repository"
                  return (
                      "no-data",
                      "this ticket had claims, but " + " and ".join(parts)
                      + f", none of them from {repo_name}, so nothing measured it here",
                  )
              return "no-data", "no session claimed this ticket, so nothing measured it"
      
          if not chunked:
              eligible = [
                  session
                  for session in actual["sessions"]
                  if session["verb"] == "start" and session["role"] != "reviewer"
              ]
              usable = [
                  session["peak_context"]
                  for session in eligible
                  if session["transcripts"] and session["peak_context"]
              ]
              if not usable:
                  unreadable = len([session for session in eligible if not session["transcripts"]])
                  zero_peak = len(
                      [
                          session
                          for session in eligible
                          if session["transcripts"] and not session["peak_context"]
                      ]
                  )
                  if eligible:
                      details = []
                      if unreadable:
                          unreadable_codex = len(
                              [
                                  session
                                  for session in eligible
                                  if session["agent"] == "codex" and not session["transcripts"]
                              ]
                          )
                          if unreadable_codex:
                              details.append(
                                  f"{unreadable_codex} Codex session(s) carried a start claim, "
                                  "but their rollout files could not be read"
                              )
                          if unreadable > unreadable_codex:
                              details.append(
                                  f"{unreadable - unreadable_codex} start claim(s) were unreadable "
                                  "because their transcripts are gone"
                              )
                      if zero_peak:
                          details.append(f"{zero_peak} start claim(s) recorded no usable context peak")
                      reason = " and ".join(details)
                  else:
                      reason = (
                          "lifecycle, reviewer, or legacy claims were visible but none was an "
                          "eligible non-reviewer start claim"
                      )
                  return "unmeasurable", f"flat order could not be measured: {reason}"
              own = max(usable)
              if own >= DEGRADE_PEAK:
                  return (
                      "under-sliced",
                      f"flat order execution peaked at {own:,} tokens, past the "
                      f"{DEGRADE_PEAK:,} degradation band",
                  )
              return "ok", f"flat order execution peaked at {own:,} tokens"
      
          if actual["session_count"] == 0:
              if actual["claim_count"]:
                  unreadable_codex = [
                      session
                      for session in actual["sessions"]
                      if session["agent"] == "codex" and not session["transcripts"]
                  ]
                  if unreadable_codex:
                      return (
                          "unmeasurable",
                          f"{len(unreadable_codex)} Codex session(s) claimed this ticket, but their "
                          "rollout files could not be read, so nothing could be measured",
                      )
                  return (
                      "unmeasurable",
                      f"{actual['claim_count']} session(s) claimed this ticket and their "
                      "transcripts are gone, so nothing could be measured",
                  )
          judged = [session for session in actual["sessions"] if session["role"] != "reviewer"]
          own = max((session["peak_context"] for session in judged), default=0)
          worker_peaks = [peak for peak in actual["worker_peaks"] if peak]
          if not worker_peaks:
              if actual["peak_context"] == 0:
                  return (
                      "unmeasurable",
                      f"{chunks} chunk(s), but no usable context peak was measured, so chunk size "
                      "could not be measured",
                  )
              measured_roles = {
                  session["role"] for session in actual["sessions"] if session["transcripts"]
              }
              if measured_roles and measured_roles <= {"reviewer"}:
                  shape = "only review-only sessions were measured"
              else:
                  shape = f"the sessions that were measured peaked at {own:,} tokens"
              return (
                  "coordinator-only",
                  f"{chunks} chunk(s), but chunk size was not measured: "
                  f"{actual['claimed_workers']} claim(s) carried an implementation-worker role "
                  f"and 0 of them were measurable; {shape}",
              )
      
          peak = max(worker_peaks)
          if peak >= DEGRADE_PEAK:
              return (
                  "still-degraded",
                  f"{chunks} chunk(s) and the largest implementation worker still peaked at "
                  f"{peak:,} tokens, past the {DEGRADE_PEAK:,} degradation band; the chunks were too big",
              )
          if chunks > 1 and peak < FLOOR_PEAK:
              # over-sliced says no chunk was big enough to need its own agent, which
              # takes a measured worker for every chunk. An unreadable or never-claimed
              # worker leaves a chunk whose cost is unknown, and an unknown chunk
              # cannot be the small one. More workers than chunks is ordinary rather
              # than suspicious: a chunk that escalates a tier claims the escalation as
              # a second worker session, so the test is coverage, not equality.
              if len(worker_peaks) >= chunks:
                  return (
                      "over-sliced",
                      f"{chunks} chunks but no implementation worker exceeded {peak:,} tokens; "
                      "one agent would have held it",
                  )
              return (
                  "ok",
                  f"{len(worker_peaks)} of {chunks} chunk(s) measured an implementation worker, "
                  f"peaking at {peak:,} tokens; too few to call it over-sliced",
              )
          if len(worker_peaks) >= chunks and actual["coordinator_peak"] >= DEGRADE_PEAK:
              return (
                  "coordination-degraded",
                  f"{chunks} chunk(s) and every measured implementation worker held under the "
                  f"{DEGRADE_PEAK:,} degradation band, but the coordinator peaked at "
                  f"{actual['coordinator_peak']:,} tokens; the slice was right while the "
                  "coordinating session was not",
              )
          return "ok", f"implementation workers peaked at {peak:,} tokens across {chunks} chunk(s)"
      
      
      def report_write_denial(path: Path, error: OSError) -> None:
          """Tell a sandboxed session its telemetry write was denied and how to fix it.
      
          A sandbox permission denial is not a workflow failure: telemetry is a
          measurement, never a gate. One line to stderr names the denied path and
          the remedy, so a rerun of the same command outside the sandbox (or with
          escalated permissions) succeeds.
          """
          print(
              f"ticket: could not write {path} ({error.strerror or error}); "
              "rerun this command outside the sandbox or with escalated permissions",
              file=sys.stderr,
          )
      
      
      def command_scan(arguments: argparse.Namespace) -> int:
          ticket_id = validate_ticket_id(arguments.ticket_id)
          current_repo = resolve_repo(Path(arguments.project) if arguments.project else Path.cwd())
          result = scan(ticket_id, arguments.projects_dir, current_repo)
          print(json.dumps(result, indent=2))
          return 0
      
      
      def command_record(arguments: argparse.Namespace) -> int:
          ticket_id = validate_ticket_id(arguments.ticket_id)
          current_repo = resolve_repo(Path(arguments.project) if arguments.project else Path.cwd())
          actual = scan(ticket_id, arguments.projects_dir, current_repo)
          call, reason = verdict(actual, arguments.chunked, arguments.chunks)
          record = {
              "ticket_id": ticket_id,
              "verbs": arguments.verb,
              "traits": arguments.trait,
              "depth": arguments.depth,
              "chunked": arguments.chunked,
              "chunks": arguments.chunks,
              "repo": actual["repo"],
              "session_count": actual["session_count"],
              "claim_count": actual["claim_count"],
              "unreadable": actual["unreadable"],
              "excluded_claims": actual["excluded_claims"],
              "unattributable": actual["unattributable"],
              "peak_context": actual["peak_context"],
              "subagent_peak": actual["subagent_peak"],
              "coordinator_peak": actual["coordinator_peak"],
              "worker_peaks": actual["worker_peaks"],
              "reviewer_peak": actual["reviewer_peak"],
              "legacy_peak": actual["legacy_peak"],
              "verb_peaks": actual["verb_peaks"],
              "claimed_workers": actual["claimed_workers"],
              "session_peaks": [
                  session["peak_context"] for session in actual["sessions"] if session["transcripts"]
              ],
              "verdict": call,
              "reason": reason,
              "recorded_at": datetime.now(timezone.utc).isoformat(),
          }
          print(json.dumps(record, indent=2))
          return 0
      
      
      def command_claim(arguments: argparse.Namespace) -> int:
          ticket_id = validate_ticket_id(arguments.ticket_id)
          agent, session_id = detect_session(
              validate_session_id(arguments.session) if arguments.session else None,
              arguments.agent,
          )
          project = arguments.project or str(Path.cwd())
          claim = {
              "ticket_id": ticket_id,
              "session_id": session_id,
              "agent": agent,
              "role": arguments.role,
              "verb": arguments.verb,
              "project": project,
              "repo": resolve_repo(Path(project)),
              "claimed_at": datetime.now(timezone.utc).isoformat(),
          }
          try:
              persisted, written = append_claim(claim)
              already_claimed = not written
              persisted_verb = persisted.get("verb") or LEGACY_VERB
              if already_claimed and persisted_verb != arguments.verb:
                  print(
                      "ticket telemetry: claim conflict: "
                      f"persisted verb '{persisted_verb}', submitted verb '{arguments.verb}'; "
                      "kept the persisted claim",
                      file=sys.stderr,
                  )
          except OSError as error:
              report_write_denial(CLAIMS_PATH, error)
              persisted = claim
              already_claimed = False
          print(json.dumps({**persisted, "already_claimed": already_claimed}, indent=2))
          return 0
      
      
      def contains_symlink(root: Path) -> bool:
          """Reject links before an OpenSpec subprocess can traverse a copied change."""
          if root.is_symlink():
              return True
          for directory, directories, files in os.walk(root, followlinks=False):
              if any((Path(directory) / name).is_symlink() for name in [*directories, *files]):
                  return True
          return False
      
      
      def command_preflight_openspec(arguments: argparse.Namespace) -> int:
          """Prove the one changed active OpenSpec change applies without mutating either tree."""
          repo = arguments.repo.expanduser().resolve()
          source = repo / "openspec"
          if source.is_symlink():
              raise OpenSpecPreflightError(f"OpenSpec root must not be a symlink: {source}", 2)
          if not source.is_dir():
              raise OpenSpecPreflightError(f"OpenSpec root not found: {source}", 2)
      
          try:
              resolved = subprocess.run(
                  [
                      "git",
                      "rev-parse",
                      "--verify",
                      "--end-of-options",
                      f"{arguments.base_ref}^{{commit}}",
                  ],
                  cwd=repo,
                  text=True,
                  capture_output=True,
                  check=False,
              )
          except (OSError, UnicodeDecodeError) as error:
              raise OpenSpecPreflightError(f"could not resolve base ref: {error}", 2) from error
          if resolved.returncode or not resolved.stdout.strip():
              raise OpenSpecPreflightError(
                  f"base ref does not resolve to a local commit: {arguments.base_ref}", 2
              )
          base_commit = resolved.stdout.strip()
      
          try:
              merged = subprocess.run(
                  ["git", "merge-base", "HEAD", base_commit],
                  cwd=repo,
                  text=True,
                  capture_output=True,
                  check=False,
              )
              changed_paths = subprocess.run(
                  ["git", "diff", "--name-only", merged.stdout.strip(), "--", "openspec/changes"],
                  cwd=repo,
                  text=True,
                  capture_output=True,
                  check=False,
              )
          except (OSError, UnicodeDecodeError) as error:
              raise OpenSpecPreflightError(f"could not discover changed OpenSpec changes: {error}") from error
          if merged.returncode or not merged.stdout.strip() or changed_paths.returncode:
              raise OpenSpecPreflightError("could not discover changed OpenSpec changes")
      
          changes: set[str] = set()
          for line in changed_paths.stdout.splitlines():
              parts = PurePosixPath(line).parts
              active = source / "changes" / parts[2] if len(parts) >= 3 else None
              if active is None or parts[:2] != ("openspec", "changes") or parts[2] == "archive":
                  continue
              if active.is_symlink():
                  raise OpenSpecPreflightError("active OpenSpec change contains a symlink", 2)
              if active.is_dir():
                  changes.add(parts[2])
          if not changes:
              print("ticket: no changed active OpenSpec change")
              return 0
          if len(changes) > 1:
              raise OpenSpecPreflightError(
                  "more than one changed active OpenSpec change: " + ", ".join(sorted(changes)), 2
              )
          change = next(iter(changes))
      
          try:
              with tempfile.TemporaryDirectory(prefix="ticket-openspec-preflight-") as temporary:
                  root = Path(temporary)
                  bundle_path = root / "openspec.tar"
                  exported = subprocess.run(
                      [
                          "git",
                          "archive",
                          "--format=tar",
                          f"--output={bundle_path}",
                          base_commit,
                          "openspec",
                      ],
                      cwd=repo,
                      text=True,
                      capture_output=True,
                      check=False,
                  )
                  if exported.returncode:
                      raise OpenSpecPreflightError("could not export the base OpenSpec tree")
                  try:
                      with tarfile.open(bundle_path) as bundle:
                          extract_options = (
                              {"filter": "data"}
                              if "filter" in inspect.signature(bundle.extract).parameters
                              else {}
                          )
                          for member in bundle.getmembers():
                              destination = root / member.name
                              try:
                                  destination.resolve().relative_to(root.resolve())
                              except ValueError as error:
                                  raise OpenSpecPreflightError("could not export the base OpenSpec tree") from error
                              if not (member.isdir() or member.isreg()):
                                  raise OpenSpecPreflightError("could not export the base OpenSpec tree")
                              bundle.extract(member, root, **extract_options)
                  except (OSError, tarfile.TarError) as error:
                      raise OpenSpecPreflightError("could not export the base OpenSpec tree") from error
      
                  copied_change = source / "changes" / change
                  disposable_change = root / "openspec" / "changes" / change
                  if not (root / "openspec").is_dir() or not copied_change.is_dir():
                      raise OpenSpecPreflightError("could not overlay the active OpenSpec change", 2)
                  if contains_symlink(copied_change):
                      raise OpenSpecPreflightError("active OpenSpec change contains a symlink", 2)
                  try:
                      if disposable_change.exists():
                          shutil.rmtree(disposable_change)
                      shutil.copytree(copied_change, disposable_change, symlinks=True)
                      if contains_symlink(disposable_change):
                          raise OpenSpecPreflightError("active OpenSpec change contains a symlink", 2)
                  except (OSError, shutil.Error) as error:
                      raise OpenSpecPreflightError("could not overlay the active OpenSpec change") from error
      
                  try:
                      result = subprocess.run(
                          ["openspec", "archive", change, "--json", "--yes"],
                          cwd=root,
                          text=True,
                          capture_output=True,
                          check=False,
                      )
                  except (OSError, UnicodeDecodeError) as error:
                      raise OpenSpecPreflightError(f"could not launch OpenSpec archive: {error}") from error
          except (OSError, UnicodeDecodeError) as error:
              raise OpenSpecPreflightError(f"could not prepare disposable OpenSpec tree: {error}") from error
      
          try:
              payload = json.loads(result.stdout)
          except json.JSONDecodeError as error:
              message = "OpenSpec archive returned invalid JSON"
              raw_diagnostic = result.stderr.rstrip("\r\n")
              if raw_diagnostic:
                  message += f": {json.dumps(raw_diagnostic)[1:-1]}"
              raise OpenSpecPreflightError(message) from error
          if not isinstance(payload, dict):
              raise OpenSpecPreflightError("OpenSpec archive JSON must be an object")
          status = payload.get("status", [])
          if not isinstance(status, list) or any(not isinstance(item, dict) for item in status):
              raise OpenSpecPreflightError("OpenSpec archive status must be a list of objects")
          errors = [item for item in status if item.get("severity") == "error"]
          if errors:
              spec_update_error = next(
                  (item for item in errors if item.get("code") == "archive_spec_update_failed"), None
              )
              if spec_update_error is not None:
                  message = spec_update_error.get("message")
                  unmatched = message if isinstance(message, str) and message else "OpenSpec archive preflight failed"
                  print(f"ticket: {unmatched}", file=sys.stderr)
                  print(
                      "ticket: if this requirement was renamed, add a `## RENAMED Requirements` "
                      "mapping from its current baseline header to the unmatched delta header; "
                      "otherwise correct the MODIFIED header.",
                      file=sys.stderr,
                  )
                  return result.returncode or 1
              message = errors[0].get("message")
              raise OpenSpecPreflightError(
                  message if isinstance(message, str) and message else "OpenSpec archive preflight failed",
                  result.returncode or 1,
              )
          archive = payload.get("archive")
          if not isinstance(archive, dict):
              raise OpenSpecPreflightError("OpenSpec archive result must be a non-null object")
          if result.returncode:
              raise OpenSpecPreflightError(f"OpenSpec archive exited with status {result.returncode}", result.returncode)
      
          print(f"ticket: OpenSpec change {change} applies cleanly in a disposable copy")
          return 0
      
      
      def add_common_flags(target: argparse.ArgumentParser) -> None:
          target.add_argument("ticket_id", help="ticket id, exactly as the tracker names it")
      
      
      def parse_arguments() -> argparse.Namespace:
          parser = argparse.ArgumentParser(description=__doc__)
          parser.add_argument(
              "--projects-dir",
              type=lambda value: Path(value).expanduser(),
              default=PROJECTS_DIR,
              help=f"local session transcript root (default {PROJECTS_DIR})",
          )
          subparsers = parser.add_subparsers(dest="command", required=True)
      
          claim_parser = subparsers.add_parser(
              "claim", help="record that this session is working this ticket"
          )
          add_common_flags(claim_parser)
          claim_parser.add_argument(
              "--session", default=None, help="session id, when the environment carries none"
          )
          claim_parser.add_argument(
              "--agent",
              choices=[name for name, _ in SESSION_VARIABLES],
              default=None,
              help="which agent wrote the session; needed with --session outside a known agent",
          )
          claim_parser.add_argument(
              "--role",
              choices=ROLES,
              default="coordinator",
              help="what this session is doing on the ticket (default: coordinator)",
          )
          claim_parser.add_argument(
              "--verb",
              choices=VERBS,
              required=True,
              help="ticket lifecycle verb that produced this claim",
          )
          claim_parser.add_argument(
              "--project", default=None, help="working directory to record (default: this one)"
          )
          claim_parser.set_defaults(handler=command_claim)
      
          scan_parser = subparsers.add_parser(
              "scan", help="report peak context per session that worked this ticket id"
          )
          add_common_flags(scan_parser)
          scan_parser.add_argument(
              "--project", default=None, help="working directory to record (default: this one)"
          )
          scan_parser.set_defaults(handler=command_scan)
      
          record_parser = subparsers.add_parser(
              "record", help="append this ticket's measured cost and print the verdict"
          )
          add_common_flags(record_parser)
          record_parser.add_argument(
              "--project", default=None, help="working directory to record (default: this one)"
          )
          record_parser.add_argument(
              "--verb", action="append", required=True, help="workflow verb that ran; repeatable"
          )
          record_parser.add_argument(
              "--trait",
              action="append",
              default=[],
              help="slicing rubric trait that fired; repeatable; omit when none fired",
          )
          record_parser.add_argument("--depth", required=True, help="review depth that was stamped")
          record_parser.add_argument("--chunked", action="store_true", help="the order was chunked")
          record_parser.add_argument(
              "--chunks", type=int, default=1, help="chunk count when --chunked is set"
          )
          record_parser.set_defaults(handler=command_record)
      
          preflight_openspec_parser = subparsers.add_parser(
              "preflight-openspec",
              help="prove the changed active OpenSpec change applies without mutating its source",
          )
          preflight_openspec_parser.add_argument(
              "--repo", type=Path, required=True, help="ticket worktree containing the OpenSpec change"
          )
          preflight_openspec_parser.add_argument(
              "--base-ref", required=True, help="local Git ref naming the base OpenSpec tree"
          )
          preflight_openspec_parser.set_defaults(handler=command_preflight_openspec)
      
          return parser.parse_args()
      
      
      def main() -> int:
          arguments = parse_arguments()
          try:
              return int(arguments.handler(arguments))
          except TelemetryError as error:
              print(f"ticket: {error}", file=sys.stderr)
              return 1
          except OpenSpecPreflightError as error:
              print(f"ticket: {error}", file=sys.stderr)
              return error.exit_status
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
  • templates
    • work-order.md 16.1 KB
      # Work order template
      
      Post as one ticket comment. The quote block, the human summary, and the headings
      sit outside the fence. The fence is the executable brief and must stand alone.
      
      The binding decides the comment's markup ([the tracker contract](../references/tracker-contract.md)).
      The shapes below are written in markdown, which is what the GitHub issues binding
      posts; another binding converts the same substance to its own markup and keeps the
      fence a fence.
      
      The human summary comes before the fenced order and is written for a teammate, not
      an agent: lead with what the work gets us in one sentence, then short plain-English
      bullets (one per major piece of the order, stating what happens and why in domain
      terms, no file paths or resource type names), and close with a "not in this ticket"
      bullet naming the sibling work. Match the order's substance exactly; it is a
      translation, not a second spec.
      
      ## The execution lock
      
      Every posted order is an **EXECUTION LOCK v2** envelope: `Source:` names where the
      durable plan lives and at what exact commit, and the fence itself carries only what
      that source cannot — explicit human authorization to execute, which revision is
      authorized, session and model fit, execution shape and chunk ownership,
      verification, review depth, the expected diff, and the stop-at-pull-request
      ceiling. `<lock-id>` on the version line is a plain positive integer, unique within
      the ticket: 1 for that ticket's first lock, and one more than the highest
      `<lock-id>` already posted on it for every lock that supersedes a prior one. A
      legacy `WORK ORDER` carries no `<lock-id>`, so it contributes no number to that
      maximum: the first lock superseding a legacy order is that ticket's first lock and
      reads 1. It exists so a reader can tell two locks
      on the same ticket apart; it plays no role in which comment is newest — that is
      comment post time, per [the tracker contract](../references/tracker-contract.md).
      The envelope has one grammar and three source modes:
      
      * **`openspec <change-path>@<full-commit-oid>`** — the plan is an OpenSpec change on
        the ticket branch at that full commit. `Selected tasks:` and
        `Acceptance anchors:` are plain positional numbers (no inline ID markers, no
        hashes) resolved against the pinned commit's `tasks.md` checklist and spec-delta
        requirements. Whole-change ownership is a property of the ticket, not of each
        fence: an ordinary ticket owns its whole change, an epic child owns a subset of
        its shared parent change, and a flat lock's own `Selected tasks:` reads `all`
        (ordinary) or that subset (epic child). A chunked lock's header `Selected tasks:`
        reads the same value, and its sub-locks partition it: each sub-lock's own
        `Selected tasks:` and `Acceptance anchors:` are a disjoint slice of the header's
        selection, and the sub-locks together cover exactly what the header selected —
        no more, no less.
      * **`repository-native <path>@<full-commit-oid>`** — the plan is some other versioned,
        reviewable artifact this repository already keeps (a design doc, a checklist) at
        that exact commit. `Selected tasks:` and `Acceptance anchors:` are that artifact's
        own positional numbering, resolved the same way.
      * **`inline`** — there is no durable plan record; the fence carries today's full
        `Context` / `Do` / `Done when` payload verbatim as its body, unchanged from the
        pre-lock work order. `Selected tasks:` and `Acceptance anchors:` do not apply and
        are omitted.
      
      `openspec` and `repository-native` both pin exact bytes; a consumer resolves the
      commit, confirms the selected tasks and anchors exist at those bytes, and refuses
      rather than substituting the branch head or a nearby commit. Amending the pinned
      source after the lock is posted requires posting a newer lock; nothing re-derives
      authorization from an unpinned edit.
      
      Two shapes. **Flat** is the default: one fence, one agent. **Chunked** is for
      orders the slicing rubric sliced ([references/slicing.md](../references/slicing.md)):
      one header fence plus one sub-lock fence per sub-order, all in the same comment.
      Every fence of either shape carries a `Review depth:` line
      ([references/review-depth.md](../references/review-depth.md)).
      
      Every fence also carries `Surface lifecycle:`. Use `none` when no rendered surface
      changes, `build` for a greenfield or accepted-fallback lock, and `revise` for a
      shipped surface whose behavior ledger and replay remain its contract. The chunked
      header carries the one active lifecycle across the whole diff (`none` only when all
      chunks say `none`); each sub-lock names its own route so the fence remains
      executable without coordinator commentary. One ticket does not mix `build` and
      `revise`: split those into separate tickets rather than inventing a fourth state.
      
      Every flat and chunked-header fence also carries `Profile:`. Use `hardening` only
      for a flat order whose target repo declares `Harden:`; use `none` otherwise,
      including every chunked order. A `QA script` follows `Done when` in the flat
      fence only when the profile is `hardening`: numbered Given/When/Then steps a
      human follows in the running app to confirm that clause.
      
      ## Two authoring checks before the draft leaves triage
      
      * **Wiring table.** Every value the order names (an environment variable, an input,
        an output, a file, a flag) has both a producer and a consumer stated in the order.
        Anything named once is a defect in the draft, not a detail for the implementer. In
        one measured review, all six round-one blockers were this single class: a thing
        named in one place with no counterpart anywhere.
      * **Executable logic is spiked, not prosed.** When the order depends on executable
        logic (a regex, a shell fragment, a workflow expression, a query), write the
        artifact in the worktree now, run it, commit it, and have the order reference it. A
        scratch file with a table test is enough. Prose in the order states intent; the
        literal lives only in executed code. An order that pins literals in markdown is an
        implementation nothing can compile, and review rounds against it find defects one
        at a time that a test run finds at once.
      
      ---
      
      ## Flat
      
      > Written by an AI agent operating for `<operator>`. Verify before relying on it.
      
      ## Summary
      
      `<one sentence: what the work gets us>`
      
      * `<plain-English bullet per major piece: what happens and why>`
      * `<...>`
      * Not in this ticket: `<sibling work, with ticket ids>`
      
      ## Work order
      
      Before filling either shape, apply
      [brief quality](../references/brief-quality.md). It is a drafting checklist, not
      an extra ticket-comment format.
      
      ```
      EXECUTION LOCK v2 <ticket-id> <lock-id>
      Source: <openspec <change-path>@<full-commit-oid> | repository-native <path>@<full-commit-oid> | inline>
      Open as: <model> / <effort>.
      Session fit: <selected execution row's Ladder value, with each model's display name in ladder order>; or `explicit Astra executor admission` when `Open as:` selects GPT-6 Astra. The latter uses current authoritative host metadata and the installed-adapter probe, is not a benchmark rung or reviewer admission, and requires the actual effort.
      Execution: single agent.
      
      Classification: <code | investigation | manual>
      Surface lifecycle: <none | build | revise>
      Repo(s): <org/repo> (branch from the default branch)
      Verification: <the command that verifies this change>
      Expectation: <what that command must report before the pull request opens>
      Review depth: <focused | targeted | full> (<one-line reason>)
      Profile: <none | hardening>
      
      Drafting conventions: Read `skills/drivers/ticket/references/drafting-conventions.md` before acting on this order.
      
      Selected tasks: <all (ordinary ticket) or the epic-child's owned subset, as positional numbers from the pinned source's numbered tasks; omit when Source is inline>
      Acceptance anchors: <positional requirement/scenario numbers from the pinned source that this lock selects; omit when Source is inline>
      
      Context
      <2-5 bullets: what exists today, what constrains this change, decisions already made
       (from the scoping interview or repo history) that the implementation must respect.
       Present for every Source; for openspec and repository-native, this is orientation
       only — the pinned source is the authority, not a restatement of it.>
      
      Do
      <present only when Source is inline: numbered, concrete steps: files, targets,
       resources, workflows. Name what to create, what to modify, and what must not
       change. For openspec and repository-native, the pinned source's tasks are the Do
       steps; nothing here duplicates them.>
      
      Done when
      <observable acceptance: verification output, CI green, specific behavior. Not
       "works". For openspec and repository-native, this is the lock's own delivery
       acceptance only — the verification command's expectation and the
       stop-at-pull-request condition — never a restatement of the pinned source's own
       acceptance criteria; that criteria lives at the pinned commit, and only the
       executing agent's later verification checks it.>
      
      Expected diff
      <closed allowlist of repository-relative paths this order may touch. No escape
       clause: a path not listed here is out of scope, whichever Source mode is active.>
      
      QA script
      <present only when Profile is hardening: a human follows these numbered steps in
       the running app to confirm Done when.>
      1. Given <starting state and fixture>
      2. When <human action>
      3. Then <observable acceptance>
      
      Boundaries
      * Iterate the verification step locally; open the pull request when it matches the expectation.
      * Execute the selected tasks and acceptance anchors only (openspec, repository-native) or the Do steps only (inline); do not expand scope beyond the pinned source.
      * Record the change where this repo already records changes.
      * Stop at the pull request. Do not merge. Do not touch <explicitly out-of-scope things>.
      ```
      
      ---
      
      ## Chunked
      
      Same comment, same attribution and summary. The summary gains one bullet naming how
      the work is split and why. Then the header fence, then each sub-lock fence in
      execution order.
      
      ## Work order
      
      ```
      EXECUTION LOCK v2 <ticket-id> <lock-id>
      Source: <openspec <change-path>@<full-commit-oid> | repository-native <path>@<full-commit-oid> | inline>
      Open as: <orchestrator model> / <effort>.
      Executor admission: <ladder admission | explicit Astra executor admission from authoritative current-session metadata; reviewer routing remains independent>.
      Execution: chunked, <n> sub-orders (<n> parallel, <n> serial).
      Launch: open a session at the model above and run `/ticket start <ticket-id>`.
              It loads /orchestrate and coordinates the sub-orders itself.
      
      Classification: <code | investigation | manual>
      Surface lifecycle: <none | build | revise>
      Repo(s): <org/repo> (one ticket branch, one pull request)
      Verification: <the command that verifies the merged branch>
      Expectation: <what that command must report before the pull request opens>
      Review depth (whole diff): <targeted | full> (<one-line reason>)
      Profile: <none | hardening>
      
      Drafting conventions: Read `skills/drivers/ticket/references/drafting-conventions.md` before acting on this order.
      
      Why sliced
      <the rubric traits that fired, one line each, and whether a nearby
       reviewer-memory anchor agreed, disagreed, or was absent — the fact of the match
       only, never the anchor's content>
      
      Selected tasks: <all (ordinary ticket) or the epic-child's owned subset, as positional numbers from the pinned source's numbered tasks — the whole selection every sub-lock below partitions; omit when Source is inline>
      Acceptance anchors: <positional requirement/scenario numbers from the pinned source that this lock selects — the whole selection every sub-lock below partitions; omit when Source is inline>
      
      Context
      <2-5 bullets shared by every chunk: what exists today, what constrains the change,
       decisions already made that all chunks must respect. Chunks repeat what they need;
       this section is not a substitute for a sub-lock standing alone. Present for every
       Source; for openspec and repository-native this is orientation, not a restatement
       of the pinned tasks.>
      
      Done when (whole ticket)
      <observable acceptance for the merged branch, not per chunk>
      
      Boundaries
      * One ticket branch, one pull request. Chunks land on per-chunk branches cut from it
        and merge back.
      * Each chunk commits only its owned work and returns its commit and bounded evidence
        to the coordinator. Only the coordinator records the aggregate change or opens
        the ticket pull request.
      * Stop at the pull request. Do not merge. Do not touch <explicitly out-of-scope things>.
      ```
      
      ```
      SUB-ORDER 1/<n> <ticket-id>: <chunk title>
      Mode: parallel | serial after <n>
      Agent: <haiku | sonnet | opus>
      Surface lifecycle: <none | build | revise>
      Review depth: <focused | targeted | full> (<one-line reason>)
      Capability owned: <one coherent capability; exactly one sub-order owns it>
      Shared contracts owned: <none | each named contract this sub-order owns>
      
      Drafting conventions: Read `skills/drivers/ticket/references/drafting-conventions.md` before acting on this order.
      
      Selected tasks: <this sub-order's disjoint positional slice of the header's Selected tasks; every sibling sub-lock's slice is disjoint from this one, and together they cover the header's whole selection; omit when the header's Source is inline>
      Acceptance anchors: <this sub-order's disjoint positional slice of the header's Acceptance anchors, on the same terms; omit when the header's Source is inline>
      
      Context
      <everything this chunk needs to stand alone in a fresh agent. Never "as established
       in chunk 1". For openspec and repository-native, this is orientation onto the
       pinned source's selected tasks, never a restatement of them.>
      
      ### Session fit
      
      Session fit: <the selected execution row's Ladder value, with each model's display name in ladder order>; selected Agent rung: <Rung>. An explicit GPT-6 Astra coordinator admission is recorded separately from this ladder and never changes reviewer eligibility or a worker's selected rung.
      
      ### Builder self-check
      
      Before declaring the change ready, run each check below.
      
      1. **External surface by execution.** Before coding against a CLI or API surface, run `--help` or a probe call against that surface; do not infer flags, arguments, or behavior from memory.
      2. **Fail-first tests.** For changed executable behavior where a meaningful negative test exists, run it against the pre-change behavior or a deliberately broken variant and observe the expected failure. A fake that accepts every input or a mock of the function under test is not evidence. Prose behavior uses the admitted bounded fresh-session evidence, not a string-matching test.
      3. **Boundaries by execution.** Prove a security or confinement claim by attempting the forbidden action in a real run; configuration inspection alone is not evidence.
      4. **Post-fix sweep.** After each late fix, sweep its affected path for uncalled symbols, dead parameters, and prose that still describes the pre-fix behavior.
      
      Do
      <present only when the header's Source is inline: numbered, concrete steps scoped
       to this chunk only. For openspec and repository-native, this sub-lock's Selected
       tasks are the Do steps; nothing here duplicates them.>
      
      Done when
      <observable acceptance for this chunk alone. Under the header's openspec or
       repository-native Source, this is the lock's own delivery acceptance only —
       verification and stop-at-pull-request — never a restatement of the pinned
       source's acceptance criteria for this sub-lock's selected tasks.>
      
      Expected diff
      <this sub-order's closed allowlist of repository-relative paths. No escape clause,
       and disjoint from every parallel sub-order's allowlist.>
      
      Boundaries
      * Re-read `ORDER.md` before each commit and again before declaring the work done;
        `Done when` is closed, so when it is met stop and report, and propose any further
        improvement rather than making it. If `ORDER.md` cannot be found or read, stop and
        report rather than continuing from memory.
      * Touch only <files/targets this chunk owns>. Another chunk owns <the rest>.
      * Execute the selected tasks and acceptance anchors only (openspec, repository-native) or the Do steps only (inline); do not expand scope beyond the pinned source.
      * A parallel chunk must not implement, revise, or depend on this chunk's private
        capability. Name any shared contract and its one owning sub-order instead.
      * Do not record the change; the coordinator owns that.
      * Commit on this chunk's branch. Do not open a pull request, do not merge.
      ```
      
  • verbs
    • finalize.md 11.9 KB
      # /ticket finalize `<ticket-id>`
      
      Close the loop after a human merged (or abandoned) the pull request. Nothing syncs
      the code host back to the tracker; this verb is that sync. Its fresh session claims
      `--verb finalize` and never reuses the session that ran start or revise.
      
      ## Procedure
      
      1. **Verify the end state.** Read the ticket for the pull request link, then
         `gh pr view <n> --json state,mergedAt,mergeCommit`. Pull request still open:
         refuse, because finalize runs after a merge, not instead of one.
      
      2. **Merged path.**
      
         a. Confirm the merge: an empty `mergedAt` means not merged, so back to step 1.
      
         b. Confirm the post-merge workflow succeeded (`gh run list` on the repo). If it
         failed, stop and report; the ticket is not done while the post-merge run is red.
      
         c. **Read the archive locator.** An ordinary OpenSpec change is archived across
         two attended finalizations, because the archive lands through an ordinary
         reviewed pull request that a human merges. Scan the ticket's comments
         newest-first for a line reading `Archive PR: <url>`; the newest one wins. None
         found: this is the first stage, so run step 2d and stop there. Found: this is the
         second stage, so skip to step 2e. An epic child creates no child change record,
         has no archive locator, skips steps 2d and 2e, and leaves the parent active and
         unarchived for the parent epic's archive guidance.
      
         d. **First stage: open the archive pull request, then stop.** Follow
         `operations.archive.guidance` exactly. In this repository, work in a sibling
         archive checkout whose `main` is updated to `origin/main`, cut a dedicated
         archive branch there, run `openspec archive <change-name> --json --yes`, verify
         its archive JSON, run `openspec validate --all --strict`, create a Signed-off-by
         archive commit, push that branch, and open a follow-up pull request against
         `main`, scoring the body with `/pr-body` when it is installed. Never push an
         archive commit directly or forcibly to `main`, and never merge the archive pull
         request. Then comment on the ticket (attribution first) with the merged
         implementation pull request link, the post-merge evidence, and a line reading
         `Archive PR: <url>`, and stop: the ticket is not done, and steps 3 and 4 wait for
         the later finalization. OpenSpec is Git-unaware: this verb trusts the verified
         GitHub merge state and adds no enforcement layer. An archive, validation, commit,
         push, pull-request creation, or comment failure stops finalization here and
         leaves the archive branch and its checkout in place for the operator to recover
         by hand; this verb adds no automatic recovery, and a pull request opened without
         its locator comment is recovered by the operator, not by a later run.
      
         e. **Second stage: finish only after a human merged the archive pull request.**
         Run `gh pr view <archive-pr> --json state,mergedAt` on the locator's pull
         request. Still open: report it pending human review and stop without completing
         the ticket. Closed without merge: stop for operator direction, and never open a
         replacement archive pull request automatically. Merged: confirm its post-merge
         workflow succeeded (`gh run list`) and continue; a red run stops finalization
         before the completion comment and done transition.
      
         f. Comment on the ticket (attribution first): the merged pull request link, a
         one-line outcome, the post-merge evidence, and, for an ordinary change, the
         merged archive pull request as the completed archive evidence.
      
         g. Move the ticket to done. Report a failed move; do not retry.
      
      3. **Record the actuals.** On the merged path only, and for an ordinary OpenSpec
         change only after step 2e's human-merged archive pull request. When record needs
         a target worktree, do this before cleanup; otherwise it may follow cleanup. Read the
         order's shape off the ticket comment (flat or chunked, chunk count, which rubric
         traits triage said fired, the stamped depths), then:
      
         ```sh
         python3 <ticket-skill-directory>/scripts/ticket.py record <ticket-id> \
           --verb <verb that ran, repeated> \
           [--trait <trait that fired, repeated>] \
           --depth <stamped depth> \
           [--chunked --chunks <n>] \
           [--project <target-worktree>]
         ```
      
         Omit `--trait` when no slicing trait fired. Never invent a sentinel trait;
         repeat the flag only for traits the work order says fired.
      
         When the ticket ran outside the coordinator's own checkout, pass its target
         worktree through `--project` on both `record` and any preceding `scan`:
      
         ```sh
         python3 <ticket-skill-directory>/scripts/ticket.py scan <ticket-id> \
           --project <target-worktree>
         ```
      
         Record before removing that target worktree in step 4, so its repository
         identity is still available to the helper.
      
         The helper prints one JSON record and returns one verdict. The existing next
         step appends those same captured bytes to reviewer memory.
      
         Retain a successful record command's standard output in `record_json` and
         print it once for the coordinator. Then pipe those same captured bytes, without
         rerunning `record`, into reviewer memory:
      
         ```sh
         printf '%s\n' "$record_json" |
           python3 <reviewer-memory-skill-directory>/scripts/memory.py append-slicing <repo>
         ```
      
         `<repo>` is the ticket's target repository, matching the repository identity
         resolved from `--project` when that option is present. Obey the
         [reviewer-memory failure rule](../../../tools/reviewer-memory/SKILL.md#failure-rule),
         including its not-installed carve-out.
      
         Keep the record and store content local; never copy them into a tracker comment,
         work order, pull request body, or target-repository file.
      
         * `ok`: the shape matched what the ticket cost.
         * `under-sliced`: a flat order that peaked past the degradation band.
         * `still-degraded`: a chunked order whose chunks were themselves too big.
         * `over-sliced`: chunks that no single agent would have struggled with.
         * `coordination-degraded`: a chunked order whose chunks all held, but whose
           coordinator peaked past the degradation band.
         * `coordinator-only`: a chunked order where no implementation worker was
           measured, so its cost was recorded but chunk size was not measured.
         * `unmeasurable`: claimed sessions supplied no usable peak, so no slicing
           call was made.
         * `no-data`: no session claimed this ticket, so nothing measured it.
      
         The helper reads the sessions that claimed this ticket, so nothing is inferred
         from prose and there is nothing to narrow. A claimed session whose transcript
         has been deleted appears under `unreadable`: report it rather than treating it
         as a session that cost nothing.
      
         **Role and lifecycle verb decide what counts.** This verb's own session claims
         `--verb finalize --role coordinator`, like every session that drives a ticket
         rather than building or reviewing one chunk of it. A chunked order's
         slice-size judgment comes from the peaks of the sessions claimed `--role
         worker` and from nothing else. The coordinator's own peak and the reviewers'
         are recorded beside them, as `coordinator_peak` and `reviewer_peak`; the former
         can separately return `coordination-degraded` when every chunk worker held, but
         neither can make a chunked order read as under-sliced: coordinating more chunks
         costs the coordinator more, not less. A flat order is judged only from measurable
         non-reviewer sessions claimed `--verb start`; triage, revise, finalize, and
         reviewer costs remain visible as overhead and never change that verdict. Claims
         written before lifecycle verbs existed read back with verb `legacy` and are never
         guessed into a phase. Attributable claims without measurable eligible start
         evidence return `unmeasurable`, while no attributable claim remains `no-data`.
         Because a transcript is the measurement unit, every lifecycle verb runs in its
         own session; only same-verb resumes reuse a claim. Claims written before roles
         existed also keep role `legacy`, which is why a chunked ticket claimed entirely
         under legacy roles returns `coordinator-only`.
         `python3 <ticket-skill-directory>/scripts/ticket.py scan <ticket-id>` reports peak
         context per claimed session without recording anything, which is the way to look
         before committing a record.
      
         **On `under-sliced`, `still-degraded`, or `over-sliced`, report the
         misprediction.** Say which rubric call was wrong and by how much: the traits
         triage read, the shape it chose, and the peak that contradicted it. That is the
         whole path. The slicing record appended in the previous step already landed the
         lesson in this repository's reviewer-memory store, which is where triage reads
         its anchors, so the calibration is durable without anything further. Finalize
         drafts no rubric diff, offers no pull request, never edits
         [references/slicing.md](../references/slicing.md), and asks the operator nothing
         here. Moving the 180k target or the 120k floor themselves is
         operator-initiated skills-repo work, because it changes the rubric prose and the
         helper's constants together; finalize never proposes it.
      
         `no-data`, `unmeasurable`, `coordinator-only`, and `coordination-degraded` are not
         mispredictions, and none of them reports one. `no-data` has two readings the
         report keeps apart: no session claimed the ticket, so it ran outside this
         machine's transcripts, or claims for another repository were excluded.
         `unmeasurable` means this repository had claims but no measurable eligible start
         execution on a flat order, or their transcripts or Codex rollouts supplied no
         usable peak; say which reason the helper names.
         `coordinator-only` means the ticket's cost was recorded but its chunk size was
         not measured, so say that plainly — the reason names how many worker claims
         existed and how many were readable, which is the difference between a forgotten
         `--role worker` and a lost transcript. Never report it as evidence that the
         chunks were the wrong size in either direction. `coordination-degraded` means
         the slice was right while the coordinating session was not: report the cost and
         advise carrying less in that session, with fewer review rounds held in its own
         context or a handoff between chunks, never more chunks.
      
      4. **Tear the worktree and branch down.** On the merged path only, after recording
         actuals:
      
         ```sh
         <cbm-onboard-skill-directory>/scripts/cbm-teardown.sh <worktree path>
         rm -f <worktree path>/ORDER.md
         git -C <control checkout> worktree remove <worktree path>
         git -C <control checkout> branch -D <ticket branch>
         git -C <control checkout> worktree prune
         ```
      
         A dirty worktree makes `worktree remove` fail. Report that and stop; never force
         it. Then check the remote branch with
         `git ls-remote --heads origin <ticket branch>` and delete it with
         `git push origin --delete <ticket branch>` only if it is still there, since the
         code host usually deletes it on merge.
      
         Then retire the sibling archive checkout the same way, because it is a checkout
         this skill authored and carries its own graph identity: run
         `<cbm-onboard-skill-directory>/scripts/cbm-teardown.sh <archive checkout>` first,
         then `git -C <control checkout> worktree remove <archive checkout>` when it was cut
         as a worktree and delete the directory otherwise, and delete its archive branch
         locally and on the remote.
      
      5. **Abandoned path** (pull request closed unmerged, or the work cancelled): comment
         why, then on explicit user confirmation run the same teardown as step 4, without
         its archive-checkout paragraph: this path never reached step 2d, so no archive
         branch or checkout exists to retire. Never pick this path without the user
         confirming. Move the ticket wherever the user
         says, and never pick the terminal state yourself.
      
      6. **Report.** The ticket's state, the comment link, the recorded verdict, and
         anything left open (a red post-merge run, a failed archive, an archive pull
         request still awaiting human review or closed unmerged, a surviving branch or
         worktree).
      
    • revise.md 8.9 KB
      # /ticket revise `<ticket-id>`
      
      Action one review round on the ticket's open pull request. The session claims
      `--verb revise`; it may resume another revise round, but it must not reuse a session
      claimed by start or finalize. Always a single agent, even when the order was
      chunked: the chunks merged long ago and the pull request is one diff.
      
      ## Procedure
      
      1. **Reload context.** Read the ticket and its comments for the work order and the
         pull request link, then `gh pr view <url> --json state,reviews,comments` and
         `gh pr checks`. Pull request merged or closed: stop, and point at
         `/ticket finalize <ticket-id>` or the user.
      
         Locate the newest order with the same contract operation `start` used — every
         round re-locates fresh; nothing is cached from the round that opened this pull
         request. Under a legacy `WORK ORDER`, nothing more to admit. Under an
         `EXECUTION LOCK`, admission runs in two places, split by what each row needs.
         Here, with no checkout beyond the located comment itself, admit the
         checkout-independent rows of [start](start.md) step 5's matrix: recognized
         version and mode, grammar, delivery fields, and ownership. Model fit is not
         among them: `start` step 3 owns that check, and `revise` has no model-check
         step to re-run it in. The
         remaining rows — commit resolution, branch pin, change state, selection
         completeness, and unauthorized amendment — read the ticket branch and the
         pinned commit, which do not exist here yet; step 2 finishes admission against
         them immediately after it establishes the worktree, before this round
         proceeds any further. Whichever half a row falls in, the refusal is the same:
         name the row, stop, and route to `/ticket triage <ticket-id>`. The newest
         recognized lock always wins, which is what reconciles "reuses the same lock
         and pin" with a mid-round amendment: when nothing has changed since `start`,
         the newest lock is still the one it admitted, so this round reuses the same
         pin. When triage posted a newer lock since, authorizing an amendment or an
         expanded selection, that newer lock is now the newest and is what this round
         admits instead. When no newer lock exists, the newest lock is still the old
         one, pinning the old commit — so a source amendment with no newer lock
         refuses here on the same unauthorized-amendment row `start` defines, never on
         a comparison against "the lock that opened this pull request" as a separate,
         cached reference. `revise` never posts a lock itself.
      
      2. **Worktree.** If the ticket's worktree still exists (verify with
         `git -C <control checkout> worktree list`), check it is on the pull request's
         head branch: `git -C <worktree> branch --show-current` must equal the pull
         request's `headRefName`. Match: work there. Mismatch: the worktree belongs to a
         different round or ticket state, so remove it if clean (run
         `<cbm-onboard-skill-directory>/scripts/cbm-teardown.sh <path>` while the checkout
         still exists, then `rm -f <path>/ORDER.md`, reporting and stopping if
         `git -C <worktree> status --short` is non-empty, then
         `git -C <control checkout> worktree remove <path>`, never forcing)
         and respin fresh. No worktree at all: same respin.
      
         ```sh
         python3 <spin-worktree-skill-directory>/scripts/spin-worktree.py \
           --repo <control checkout> \
           --pr <number> \
           --name <ticket-id-lowercased>
         ```
      
         Never fix review comments in the control checkout.
      
         Bind that worktree's graph identity per the skill page's graph-identity rule
         before step 4 reads any code, and report what it printed. Each round resolves it
         afresh, because the worktree it runs against may be the one this step respun.
      
         Now, from this worktree, finish the `EXECUTION LOCK` admission step 1 deferred:
         commit resolution, branch pin, change state, selection completeness, and
         unauthorized amendment, exactly as [start](start.md) step 5 runs them, all
         against this worktree specifically — never the control checkout, which never
         switches branches and may be on anything. A failing row here refuses the same
         way step 1's rows do: name the row, stop, and route to `/ticket triage
         <ticket-id>`. Skipping this half silently, or running it against the control
         checkout instead of this worktree, both leave the matrix unenforced; neither
         is acceptable. A legacy `WORK ORDER` has nothing further to admit.
      
      3. **Read the standing decisions**, per the skill page's standing-decisions slot,
         before actioning the round. Absent, say so in one line and continue. This never
         refuses the round.
      
      4. **Collect the round.** Read
         [drafting conventions](../references/drafting-conventions.md) before evaluating
         an order revision. Then collect unresolved review comments (human and automated),
         failing checks, and any new verification output CI posted. List them before
         touching code.
      
      5. **Judge, then fix.** Ground every item and classify it with
         [references/review-actions.md](../references/review-actions.md). Never silently
         ignore one. What blocks is what breaks the order's Done when clause;
         [references/review-depth.md](../references/review-depth.md) governs that call,
         and the order's stamped depth (Full when any chunk was Full) sets how far to
         look. The order is still the contract.
      
         Under an `EXECUTION LOCK`, a review item is an ordinary fix only when it stays
         inside the located lock's `Selected tasks:` and `Acceptance anchors:`. An item
         that asks to touch the pinned source outside that selection is scope
         expansion: refuse it here rather than folding it in, name it in the round's
         status comment, and say it needs a newer lock from `/ticket triage
         <ticket-id>` before any round may act on it.
      
      6. **Refresh mergeability.** Before handing the pull request back for human merge,
         `git fetch origin`, refresh `baseRefName` and `mergeStateStatus` with `gh pr
         view`, then rebase once onto `origin/<baseRefName>`. Do not retry the rebase. If
         GitHub reports a conflict or the rebase has a semantic conflict you cannot
         resolve safely, abort it and stop; surface the conflict to the human rather than
         force a resolution.
      
      7. **Re-verify and re-review.** Re-run the order's verification command after the
         changes and rebase, same rules as `start`: the output must match the order's
         expectation. Re-read the repo's `AGENTS.md` or `CLAUDE.md` and audit the changes
         you are about to push against it, including any completion checklist it defines.
         Fix violations. A delegated `revise` worker returns its review-ready revised diff
         to its coordinator through the coordinator-recorded durable result locator,
         then stops at this boundary. Its coordinator dispatches `/review`, verifies the
         verdict, and resumes the same worker with actionable findings or a verified
         clean verdict. The worker must not launch a nested reviewer. A coordinator-run
         revise follows the profile route directly. Under `Profile: hardening`, re-run the repo's `Harden:` command
         with the same stop rule and three-pass cap as `start`, and run `/review` only
         when the stamped depth is Full. Under `Profile: none`, run `/review` at the
         order's stamped review depth. Fix confirmed findings and repeat verification if
         code changed.
      
      8. **Push and respond.** Before pushing, update the reviewable change record. Outside
         an epic, update the active change record if its checklist moved, and update its
         decision record if a decision changed during review; its active change and deltas
         remain reviewable until the human merge, so do not fold or archive them. An epic
         child creates no per-child change record. Preserve the parent-plan bytes already
         carried by the branch, including grounded review fixes to that amendment; the
         parent epic's active change remains authoritative.
      
         **Preflight the outbound OpenSpec change.** Only an ordinary OpenSpec-backed
         ticket uses this gate. After the rebase and every active-change, checklist, and
         decision edit, run `git fetch origin` again immediately before:
      
         ```sh
         python3 <ticket-skill-directory>/scripts/ticket.py preflight-openspec \
           --repo <ticket-worktree> \
           --base-ref origin/<baseRefName>
         ```
      
         The earlier rebase fetch does not satisfy this final refresh. A ticket using
         another or no change-record convention, or an epic child, bypasses this gate
         unchanged. Fetch, ref, or preflight failure stops visibly; do not push. The gate
         never archives the authoritative change: finalization remains the sole
         authoritative archive owner.
      
         After the gate succeeds, push to the same branch. Reply to each addressed comment
         on the pull request, resolving or answering it.
      
      9. **Status.** Comment on the ticket (attribution first) only if the round
         materially changed the plan. Routine fix-and-push rounds need no ticket comment.
      
      ## Refusals
      
      * No open pull request on the ticket: nothing to revise, so route to
        `/ticket start <ticket-id>` or the user.
      * The review asks for something the work order forbids: stop and surface the
        conflict to the user rather than choosing sides.
      
    • start.md 19.5 KB
      # /ticket start `<ticket-id>`
      
      Execute a locked work order in a fresh session. Ends at the open pull request.
      
      ## Procedure
      
      1. **Complete the shared opening.** Complete shared rules 1–2: read and summarize
         the ticket, then claim the session before locating the work order or reaching
         any later refusal. This session drives the ticket, so it claims itself with
         `--verb start --role coordinator` (the default role) on a flat and a chunked
         order alike. Use the shared claim command and its visible, non-blocking failure
         semantics; do not duplicate them here.
      
      2. **Fetch the order.** Use the contract's locate operation. None found: refuse,
         say "no work order on `<ticket-id>`; run /ticket triage `<ticket-id>`", and stop.
         The comment's `Execution:` line says `single agent` or `chunked`; an order with
         no `Execution:` line is a flat order. Note the fence header now, since it
         decides which path step 5 takes: legacy `WORK ORDER`, or `EXECUTION LOCK
         <version>`.
      
      3. **Model-check.** An order explicitly selecting `gpt-6-astra` for executor or
         coordinator work is admitted separately from every benchmark ladder. First use
         an exact current-model declaration from system/developer context naming GPT-6
         Astra. Otherwise, on Codex use only this session's local rollout:
         `CODEX_THREAD_ID` must match `session_meta.payload.id` or
         `session_meta.payload.session_id`, and the latest matching `turn_context` must
         report `payload.model: gpt-6-astra`. Never select the newest unrelated rollout,
         a global default, or another worker's state. Compare the actual current effort
         to the effort requested by `Open as:`. Consume an already provided actual effort;
         ask once only when it is unavailable. A lower effort stops the dependent work.
         Then verify the installed adapter's existing read-only probe and the repository
         read/write capability this order requires. Missing identity, effort, or capability
         admits neither execution nor a substitute session. This explicit executor
         admission is not reviewer admission: preserve the stamped depth and route review
         independently. Do not infer cross-family strength or hidden effort.
      
         On a flat non-Astra order with `Session fit:`, a session whose system-prompt
         model is named in that paragraph at or above the selected rung proceeds directly
         to step 4, skipping the remainder of Model-check and without asking about model
         fit or effort. On a chunked non-Astra order, take that same fast path only when
         every `SUB-ORDER` has exactly one `Session fit:` paragraph whose ladder is an ordered non-empty sequence of display-name rungs byte-identical across every `SUB-ORDER`, whose exactly one `selected Agent rung: <Rung>` annotation names
         exactly one rung in that paragraph, and whose coordinator system-prompt model is
         named at or above that selected rung in every paragraph. Otherwise, the order's
         `Open as:` line names a required model and effort. A session cannot reliably
         introspect its own reasoning effort from context, so do not guess it or answer
         from memory of an earlier guess. Consume actual effort already provided by the
         host; otherwise ask the user in prose to confirm it. Compare both against
         `Open as:`. Weaker on either axis: say so and stop, so the user relaunches
         correctly. Same or stronger on both: proceed. On a chunked order, also check
         every `SUB-ORDER`'s `Agent:` line against the confirmed model: the session must
         be at least as strong as the strongest chunk. Weaker than any one and the whole
         order is refused. Never run part of it or launch an agent smarter than the
         coordinator.
      
      4. **Worktree and branch.** This is the first repository action after the shared
         opening. Never work in the control checkout. Reuse the worktree
         and branch triage cut, verifying with
         `git -C <worktree> branch --show-current`. Only cut fresh when triage's worktree
         is gone, with the same command shape triage used
         ([verbs/triage.md](triage.md), step 2). A dirty control checkout is valid and
         remains untouched. The helper refuses an existing target path; surface that
         ownership check and do not force it.
         Use the worktree path the helper printed as the working directory for every step
         below. Move the ticket to in progress. Then bind that worktree's graph identity
         per the skill page's graph-identity rule, before step 5 reads any code, and
         report what it printed: a fresh session resolves this from the checkout it just
         verified rather than inheriting triage's.
      
      5. **Sufficiency check.** From the ticket worktree, admit the located order before
         reading it as instructions. Validate every row below before any implementation
         or worker dispatch, including the coordinator-mode switch in step 6.
      
         **Legacy `WORK ORDER` header.** Today's sufficiency rules apply with no inferred
         pin, forever: read the order against the actual repo. If the repo has drifted
         since triage (files moved, the constraint it names is gone), stop and report the
         mismatch; the fix is a re-triage, not improvisation. On a chunked order, confirm
         the chunks' declared file and target ownership is still disjoint; an overlap
         that drift introduced is a re-triage, not a merge problem to solve later. A
         supersession uses the new protocol below.
      
         **`EXECUTION LOCK` header — recognized version and mode.** An unrecognized
         `EXECUTION LOCK` version, or a `Source:` mode this protocol does not define,
         refuses execution: report "unrecognized lock version or source mode on
         `<ticket-id>`; run /ticket triage `<ticket-id>`" and stop. Never fall back to an
         older comment, and never consult the locate operation again to find one; this is
         the newest recognized lock or nothing.
      
         **`EXECUTION LOCK v2` admission matrix.** Once the version and mode are
         recognized, every row below must hold before the order is sufficient. Any one
         failure refuses the same way — name the row, report it, and route to
         `/ticket triage <ticket-id>` for attended re-triage — and admission never merges
         a field from an older comment to patch a failing row.
      
         * **Grammar.** The `Source:` line names exactly one of `openspec`,
           `repository-native`, or `inline`, with exactly one version and exactly one
           commit or path, per the grammar in
           [templates/work-order.md](../templates/work-order.md). A missing or malformed
           `Source:` line refuses: bad version or mode.
         * **Commit resolution** (`openspec`, `repository-native` only). The pinned
           reference is a full commit OID. `git cat-file -e <oid>^{commit}` from the
           ticket worktree; a short or unresolvable OID refuses. Then confirm the named
           source path exists at that commit's tree with
           `git ls-tree <oid> -- <path>`; an empty result is a missing path and refuses.
         * **Branch pin.** The ticket branch contains the pinned commit —
           `git merge-base --is-ancestor <oid> HEAD` from the ticket worktree, exit 0
           required. A branch that does not contain it refuses: branch missing the pin.
           Never substitute the branch head for the pin, even when the head is newer.
         * **Change state** (`openspec` only). The named change is active at the pinned
           commit (present under `openspec/changes/`, not archived); an archived change
           refuses. Then it is strictly valid at that pinned tree
           (`openspec validate <change> --strict` run against the pinned commit's
           checkout); a change that fails strict validation refuses: invalid change.
         * **Selection completeness.** Every `Selected tasks:` and `Acceptance anchors:`
           positional number resolves against the pinned commit's own numbered
           `tasks.md` checklist (`openspec`) or the pinned artifact's own positional
           numbering (`repository-native`). Any number absent at that commit refuses:
           absent anchors. An empty selection refuses the same way.
         * **Unauthorized amendment.** From the ticket worktree,
           `git log --oneline <oid>..<ticket-branch-head> -- <path>`. A non-empty result
           means the source changed after this lock pinned `<oid>`. Read what changed:
           an **amendment** is any edit to what the source authorizes — its prose, its
           requirements and scenarios, the text of a task, or a task added or removed.
           Ticking or unticking a `tasks.md` checkbox is not an amendment but the
           executor's own bookkeeping, which the workflow requires of it as work
           completes, so those commits pass this row. For a real amendment, the newest
           located lock must be the one naming that later commit; if it still names the
           older `<oid>`, refuse: the amendment is unauthorized until a newer lock pins
           it. This is the only place a source amendment is read from — never diff the
           pinned commit against branch head to "catch up" the pin.
         * **Delivery fields.** `Verification:`, `Expectation:`, and `Expected diff`
           are each present and non-empty; any one missing refuses. Without the closed
           allowlist there is nothing bounding what the executor may touch. On a chunked
           lock the allowlist is per fence, so check it on the header and on every
           sub-lock: one sub-lock missing its `Expected diff` refuses the whole order,
           because that chunk's fence would otherwise dispatch with nothing closing its
           scope.
         * **Ownership.** On a chunked order, confirm the chunks' declared file and
           target ownership is still disjoint; an overlap refuses, whether triage stated
           it wrong or drift introduced it since.
         * **Model fit.** Checked at step 3; a mismatch found there stands as this row's
           refusal and is not re-litigated here.
      
         `inline` sources carry no pinned commit: commit resolution, branch pin, change
         state, selection completeness, and unauthorized amendment do not apply, since
         there is nothing external to resolve. Grammar, delivery fields, ownership, and
         model fit still apply.
      
         Resolve the order's `Surface lifecycle:` before implementation. `build` requires
         the named locked manifest. `revise` requires the named shipped behavior ledger,
         replay, and repo-declared safe dev-server entrypoint plus fixture source. `none`
         selects no UI Craft mode. An unknown value or a named contract that is absent is
         drift and requires re-triage. For a legacy order posted before this slot existed,
         infer `build` only when it explicitly names a locked manifest; otherwise select
         no UI Craft mode. A legacy order that still asks to change a rendered surface
         without either contract is insufficient and requires re-triage. On a chunked
         order, apply the same check to every sub-order before switching to coordinator
         mode.
      
      6. **Chunked order: switch to coordinator mode.** If the `Execution:` line says
         `chunked`, load `/orchestrate` now, then follow
         [references/coordinator-mode.md](../references/coordinator-mode.md) instead of
         steps 8 through 12, and rejoin at step 13. Flat orders skip this and continue at
         step 7.
      
      7. **Read the standing decisions**, per the skill page's standing-decisions slot,
         before implementing. Absent, say so in one line and continue. This never refuses
         the order.
      
      ### Builder self-check
      
      Before declaring the change ready, run each check below.
      
      1. **External surface by execution.** Before coding against a CLI or API surface, run `--help` or a probe call against that surface; do not infer flags, arguments, or behavior from memory.
      2. **Fail-first tests.** For changed executable behavior where a meaningful negative test exists, run it against the pre-change behavior or a deliberately broken variant and observe the expected failure. A fake that accepts every input or a mock of the function under test is not evidence. Prose behavior uses the admitted bounded fresh-session evidence, not a string-matching test.
      3. **Boundaries by execution.** Prove a security or confinement claim by attempting the forbidden action in a real run; configuration inspection alone is not evidence.
      4. **Post-fix sweep.** After each late fix, sweep its affected path for uncalled symbols, dead parameters, and prose that still describes the pre-fix behavior.
      
      8. **Implement.** Read [drafting conventions](../references/drafting-conventions.md)
         with the locked order, then read the repo's `AGENTS.md` or `CLAUDE.md`; its rules
         bind everything you write on this branch. Never add or edit one yourself; if the
         repo has none, work to the user's global standards. Follow the order's Do section.
         Match the repo's existing idioms, reading neighboring code first. Record the
         change where the repo already records changes, on the same branch, per the skill
         page's change-record rule. An epic child creates no per-child change record: the
         parent epic's existing change record is authoritative, and implementation
         preserves the parent-plan bytes already committed by triage.
      
         **Route by shape of the change.** `Surface lifecycle: build` runs `/ui-craft
         build` against the named locked manifest. `Surface lifecycle: revise` runs
         `/ui-craft revise` against the named shipped behavior ledger and replay through
         the repo-declared safe dev-server entrypoint and fixture source. `Surface
         lifecycle: none` skips UI Craft. A new module or interface loads
         `/codebase-design` vocabulary before the seam is cut. With `Profile: none`, new
         behavior with testable acceptance criteria goes test-first through `/tdd`. With
         `Profile: hardening`, write tests through the public interface without `/tdd`.
         CI or workflow-file changes read `/ci-design` first.
      
      9. **Verification loop.** Run the order's `Verification:` command locally and
         iterate until its output matches the order's `Expectation:` line exactly, per the
         skill page's verification-step rule. Never fabricate the output.
      
      10. **Repo-rules audit and adversarial review, before the pull request.** An order
          with no `Profile:` line is `Profile: none`.
      
          A delegated `start` worker returns its review-ready implementation diff to its
          coordinator through the coordinator-recorded durable result locator, then
          stops at this boundary. Its coordinator dispatches `/review`, verifies the
          verdict, and resumes the same worker with actionable findings or a verified
          clean verdict. The worker must not launch a nested reviewer. A coordinator-run
          start follows the profile route below directly.
      
          **Profile: none.** Re-read the
         repo's `AGENTS.md` or `CLAUDE.md`, which has decayed from context by now, and
         audit the full diff against it rule by rule, including any completion checklist
         it defines. Fix violations, then hand off to `/review` on the branch's changes
         since the default branch, with the work order as the spec: one axis checks the
         diff against the order, the other against the repo's documented conventions. Run
         it at the order's stamped `Review depth:`, reading
         [references/review-depth.md](../references/review-depth.md) for what each depth
         checks and what counts as blocking. Classify every grounded finding with
         [references/review-actions.md](../references/review-actions.md). Fix confirmed
         findings, re-run the verification loop if code changed, then review once more.
         Two rounds maximum; findings still open after round two go into the pull request
         body as known issues, never silently dropped.
      
          **Profile: hardening.** Run `/clean` on the branch diff, then run the repo's
          `Harden:` command. Fix uncovered lines, surviving mutants, and high-CRAP
          functions, re-running until the command exits 0 and every survivor is killed by
          a public-interface test or listed as equivalent with a one-line reason in the
          pull request body. Stop after at most three passes and open the pull request as
          a draft naming the residue. A `Harden:` command that cannot run (tool missing,
          parse failure, wrong runtime) is an error: open a draft pull request, name the
          missing evidence, and never a pass. Targeted and Focused orders run no
          `/review`; Full orders run one `/review` round after hardening.
      
      11. **Preserve the change record for merge.** Outside an epic, the active change
          and its deltas remain reviewable in the pull request. Do not fold or archive
          them before merge. An epic child creates no per-child change record and
          preserves the parent-plan bytes with implementation in the pull request. After
          a human merge, `finalize` leaves the parent active for epic-owned archive.
      
      12. **Preflight the outbound OpenSpec change.** Only an ordinary OpenSpec-backed
         ticket uses this gate. After implementation, review, and all active-change fixes
         are complete, run `git fetch origin` immediately before the command, then run:
      
         ```sh
         python3 <ticket-skill-directory>/scripts/ticket.py preflight-openspec \
           --repo <ticket-worktree> \
           --base-ref refs/remotes/origin/HEAD
         ```
      
         The fetch refreshes the base that the command resolves locally. A ticket using
         another or no change-record convention, or an epic child, bypasses this gate
         unchanged. Fetch, ref, or preflight failure stops visibly; do not open the pull
         request. Finalization remains the sole authoritative archive owner.
      
      13. **Open the pull request.** `gh pr create` against the default branch. The body
          follows an existing template when one exists, in this order: the repo's
          `.github/pull_request_template.md` (or its `PULL_REQUEST_TEMPLATE/` directory),
          then the organization default in the organization's `.github` repo. Keep the
          template's headings and checklist verbatim, fill its sections with the substance
          below, and tick only checklist items actually done. Add a section the template
          lacks only when required content has no home in it. Never discard or rewrite a
          template because it seems unsuitable: open the pull request with it filled as
          best as possible, and raise the mismatch to the user. Only when no template
          exists anywhere, write the body free-form. Either way the body carries what
          changed, the verification output in a fence, and a link to the ticket. Under
          `Profile: hardening`, it also carries the `Harden:` output in a fence, the
          survivor list with dispositions, and the QA script verbatim. When
          `/pr-body` is installed, score the body with it before opening. Then move the
          ticket to pending review and comment on it (attribution first) with the pull
          request link and a one-line status.
      
          **Surface evidence follows the lifecycle.** A `build` attaches paired
          mock-versus-build screenshots (same fixture and viewports as the lock) and a
          fidelity ledger walking every lock-manifest term to met, re-settle, or blocked.
          A `revise` attaches base-versus-revision before/after screenshots from the same
          safe fixture, the amended frozen behavior ledger, and raw replay output against
          the built revision. Missing lifecycle evidence is a blocking gap, not a nit;
          `revise` never invents a lock manifest or fidelity ledger after the fact.
      
      14. **Stop.** Report the pull request URL, the verification evidence, review
          findings fixed or carried, and anything from the order left undone and why. On a
          chunked order also report each chunk's outcome and tier. Do not merge, do not
          self-approve, and do not respond to reviews; that is `/ticket revise`.
      
      ## Refusals
      
      * No work order (step 2), model mismatch (step 3), repo drift or overlapping chunk
        ownership (step 5).
      * The order's classification is `manual`: nothing to execute, so say so.
      * Verification cannot be run at all (no credentials, no access, no runnable suite):
        stop after implementation, open the pull request as a draft, and say which
        evidence is missing. Never fabricate expected output.
      * A chunked order whose sub-orders are not independently executable: refuse the
        order and route back to `/ticket triage <ticket-id>`. Do not rewrite the slice
        in flight.
      
    • triage.md 23.4 KB
      # /ticket triage `<ticket-id>`
      
      Turn a ticket into a locked work order, or establish why it cannot be one yet.
      Runs in the ticket's worktree. Outside an epic it writes the tracker and the
      applicable scope and spec documents there. An **epic child** creates no per-child
      change record. Its review instrumentation stays in untracked session scratch, while
      any required parent-plan amendment is committed in the child worktree and travels
      with the implementation pull request; the parent epic retains archive ownership.
      
      Use the shared selected-ticket mutation boundary. Keep grounding broad and
      read-only. When a distinct external prerequisite would otherwise contradict a
      recorded destination, constraint, acceptance criterion, risk, or sequence, cite
      that clause, disclose the exact external target and mutation, and stop before
      mutation until a subsequent operator response explicitly authorizes the previously
      disclosed target and exact mutation.
      
      ## Procedure
      
      1. **Read the ticket.** Use the contract's read operation for the description and
         every comment. Note the parent, the links, and any prior work order comment. If
         an order already exists, say so and ask whether to supersede it; a new order
         posted later wins. A non-null parent is only an epic-child candidate: read that
         parent through the tracker contract and select the epic-child lifecycle only
         when its labels include the `epic` label. A missing parent or a parent without
         `epic` leaves this as an ordinary ticket.
      
      2. **Cut or reuse the worktree.** Verify an epic-child draft first. Before any
         grounding or repo read, derive a short kebab slug from the ticket title.
      
         For a fresh epic-child worktree, treat the issue-body draft as untrusted input.
         Require exactly one `Parent plan base: <parent-plan branch>@<pinned full commit>`
         field, with an unprefixed remote branch name and a full commit. Then run:
      
         ```sh
         git -C <control checkout> fetch origin
         git -C <control checkout> rev-parse origin/<parent-plan branch>
         ```
      
         Require the resolved commit to equal the pinned full commit exactly. Resolve the
         branch only as `origin/<parent-plan branch>`; never accept a local branch or an
         abbreviated commit. A missing or mismatched value is a stale draft: post nothing,
         create no worktree, and return to the attended epic session for a new issue-body
         draft and pin.
      
         Then cut or reuse the worktree:
      
         a. Numeric ticket id:
      
         ```sh
         python3 <spin-worktree-skill-directory>/scripts/spin-worktree.py \
           --repo <control checkout> \
           --issue <ticket-id> \
           --slug <slug> \
           --name <ticket-id> \
           [--base <parent-plan branch>]
         ```
      
         b. Non-numeric ticket id: create the branch ref from the remote default branch
         first, without switching the control checkout, then spin the worktree onto it:
      
         ```sh
         git -C <control checkout> fetch origin
         git -C <control checkout> branch <prefix>/<ticket-id-lowercased>-<slug> origin/<default branch>
         python3 <spin-worktree-skill-directory>/scripts/spin-worktree.py \
           --repo <control checkout> \
           --branch <prefix>/<ticket-id-lowercased>-<slug> \
           --name <ticket-id-lowercased>
         ```
      
         For an epic child, the bracketed `--base <parent-plan branch>` is required and
         receives the same unprefixed branch name verified above. For an ordinary ticket,
         omit it.
      
         c. `<prefix>` is whatever `spin-worktree` resolves: its `--branch-prefix` flag,
         then `branchPrefix` in `~/.config/spin-worktree/config.json`, else no prefix.
         When no prefix resolves, omit `<prefix>/` entirely. Never invent a prefix here,
         and keep both paths on the same one so the branch name does not depend on which
         path ran.
      
         d. Use the worktree path the helper printed as the working directory for every
         step below. If the ticket's worktree already exists, verify it is on the
         ticket's branch (`git -C <worktree> branch --show-current`) and reuse it rather
         than respinning.
      
         e. A dirty control checkout is valid: leave its files untouched and spin from
         it directly. Never search the existing task worktrees for a clean substitute.
         The helper still refuses an existing target path; surface that refusal to the
         user rather than forcing past it.
      
         f. Bind that worktree's graph identity before grounding, per the skill page's
         graph-identity rule, and report what it printed. A reused worktree gets the same
         check: the identity is recomputed here every session, never remembered from the
         run that cut it.
      
      3. **Identify the repo or repos.** From the ticket text, its parent, and its links.
         If the target repo does not exist yet, stop: repo scaffolding happens outside
         this skill. Post nothing, and tell the user which ticket has to land first.
      
      4. **Read the standing decisions**, per the skill page's standing-decisions slot,
         before grounding in the repo. Absent, say so in one line and continue. This
         never refuses the ticket.
      
         Then, for each target repo, run:
      
         ```sh
         python3 <reviewer-memory-skill-directory>/scripts/memory.py ensure <repo>
         ```
      
         Read the store index path it names, following links only where relevant to the
         ticket. When it reports an empty bundle, say so in one line and continue. Keep
         the index and store content in worker prompts only; never copy them into the
         work order, a tracker comment, a pull request body, or the target repository.
         Obey the [reviewer-memory failure rule](../../../tools/reviewer-memory/SKILL.md#failure-rule),
         including its not-installed carve-out.
      
      5. **Ground.** In each target repo:
      
         a. The repo's own change and decision records, active and archived.
      
         b. `docs/` (architecture, runbooks), `README`, `CONTRIBUTING`, and the repo's
         `AGENTS.md` or `CLAUDE.md`.
      
         c. `git log --oneline -30`, plus recent pull requests touching the same area.
      
         d. The CI workflows that will judge the change.
      
         Record what the repo already decided that constrains this ticket.
      
         **Verify live state live, never from docs or ticket comments.** Any claim the
         order depends on about what exists right now (a deployed resource, repository
         secrets, required checks) gets checked against the live source: run the
         read-only describe, hit the API, list the state. Repo docs and prior ticket
         comments both go stale, in opposite directions: one measured triage found the
         repo claiming a production target did not exist while a ticket comment claimed
         the platform work was done, and both were part right. A wrong premise here
         poisons every downstream decision in the order.
      
         **When the change alters documented behavior, build a closed document
         inventory.** Grep the repo for the behavior's terms (the command, the setting,
         the promise) across all docs, templates, and comments. Never sample the docs you
         already know about. The order lists the inventory, so review checks it for
         completeness instead of discovering documents one per round; one measured review
         leaked one stale document per round for four rounds because each pass sampled
         instead of searching.
      
         **Route the grounding by ticket shape.** A bug report is reproduced before
         anything is drafted, and the reproduction goes into the order. A ticket touching
         CI or a workflow file reads `/ci-design` before the order is drafted.
      
      6. **Classify.** One of:
      
         * `code`: lands as a pull request.
         * `investigation`: the deliverable is findings on the ticket, no pull request.
         * `manual`: human-only operational work. Triage still grounds and scopes it, but
           the order says what a human does, not what an agent implements.
      
      7. **Run `/scope`, always, unconditionally.** After grounding and classification,
         invoke it with the ticket summary and what grounding found. It classifies the
         dominant uncertainty and routes it, and its interview is the only interface for
         putting decisions to the user. Never present findings and free-form questions
         instead. A single missing fact with no judgment attached (a hostname, a version)
         is the only exception. When nothing is genuinely uncertain, scope says so and
         returns without asking anything; that outcome is the pass signal, not a wasted
         step. Resolved answers go into the order. The epic issue-body draft enters this
         ordinary grounding, scope, and mandatory-review path; it is never executable by
         itself. For an epic child, every `/scope`
         specialist keeps its instrumentation in untracked session scratch outside the branch, discards it
         after the final order, and creates no scope ledger or docs/scope state. `/epic` alone owns the
         proposal, design, and tasks. If triage discovers a required parent-plan
         amendment, edit and commit it in the child worktree, include those paths and
         acceptance effects in mandatory review, and carry it into the order that will
         govern the implementation pull request. This is still the parent active change,
         not a per-child change record.
      
         **Resolve the surface lifecycle.** Every order and sub-order gets one closed
         `Surface lifecycle:` value:
      
         * `none` when it changes no rendered surface.
         * `build` for a greenfield surface after `/ui-craft lock`, or for UI Craft's
           explicit shipped-surface fallback. Name the lock manifest; on fallback also
           name the predecessor behavior ledger and replay.
         * `revise` for a shipped surface. Run UI Craft's setup/router during triage.
           Only after it verifies the repo-declared safe-start and manufactured data
           source may triage run the behavior sweep in this ticket worktree. Freeze the
           behavior ledger and replay through the existing sanction procedure before
           source admission or lock posting. This prepares evidence only: triage does
           not implement the revision, replace the inherited checkout/base, or invent a
           replacement mock. If either prerequisite is unavailable, name it and post no
           executable lock.
      
         Every non-`none` lifecycle also stamps one `Design record:` line naming the
         design-doc and behavior-ledger updates the change carries, or the explicit
         deferral it inherits (a parent-plan pointer, a ticket). An order that changes
         a rendered surface with neither is incomplete: the update travels with the
         implementation pull request, and a deferral is written down, never assumed.
      
         A UI Craft refusal or ambiguous route blocks the order. In a chunked order the
         header carries the one non-`none` lifecycle active across the whole diff and
         every sub-order carries its own; an affected sub-order must repeat the contract
         paths it needs to stand alone. If chunks would mix `build` and `revise`, split
         them into separate tickets instead of adding another lifecycle value.
      
      8. **Decide the shape: flat or chunked.** Read
         [references/slicing.md](../references/slicing.md) and run its trait rubric
         against what grounding found. The rubric carries the traits and the thresholds;
         this repo's own anchors live in its reviewer-memory slicing records, so read
         those for the shapes and measured peaks the traits are calibrated against here,
         and say when a nearby record contradicts the call the rubric points at. The
         rubric decides flat or chunked and, when chunked,
         sizes the chunks and names each one's mode, coherent capability ownership, file
         or target ownership, shared-contract ownership, agent tier, and the orchestrator
         tier. Every capability and shared contract has exactly one owning chunk. A
         parallel chunk must not implement, revise, or depend on another chunk's private
         capability; make that work serial when the dependency is real. Record which
         traits fired, and whether a nearby reviewer-memory anchor agreed with the call,
         disagreed with it, or was absent from the store. Record the fact of that match,
         never the record: store content stays out of the order under step 4's rule, so
         an anchor is never quoted, named, or given its measured peaks here. The order
         carries that reasoning, so a wrong call is visible later.
      
      9. **Stamp the review depth.** Read
         [references/review-depth.md](../references/review-depth.md) and stamp one depth
         with a one-line reason on the order, and on each sub-order when chunked. Check
         its sensitivity floor first; the floor overrides any judgment about how small
         the change looks.
      
         **Separate executor admission.** When the operator explicitly selects GPT-6
         Astra to execute or coordinate, record that it is admitted from authoritative
         available host metadata under the current-session rule in `start` step 3. This
         does not add Astra to a benchmark ladder, select it as a reviewer, or relax the
         existing Full-depth and headroom gates. If the required metadata or adapter
         capability is unavailable, name the unresolved executor route rather than
         substituting a reviewer or guessing a stronger model.
      
      10. **Stamp the profile.** Read the target repo's `CLAUDE.md` or `AGENTS.md` repo
          facts for a `Harden:` line. When the user or ticket asks for the hardening
          profile and that line exists, stamp `Profile: hardening` and write the QA script
          from the acceptance criteria. When the line is absent, stamp `Profile: none`,
          say so in the order's Context, and continue with the default workflow. Stamp
          `Profile: none` on every chunked order: the profile is flat-only.
      
      11. **Draft the work order.** Read
          [drafting conventions](../references/drafting-conventions.md), apply
          [references/brief-quality.md](../references/brief-quality.md), then fill
          [templates/work-order.md](../templates/work-order.md), the flat shape or the
          chunked shape per step 8, and run that page's two authoring checks before the
          draft leaves this step. Each fenced block must be self-sufficient for a fresh
          session, which under a pinned `Source:` means deterministic acquisition rather
          than copied prose: a competent agent with the ticket, that one block, and the
          source the block pins should produce the right change without asking anyone
          what was meant. Under `Source: inline`, the block alone carries it. Name files
          and targets concretely. State what must
          not change. Set the verification command and its expectation. In a chunked
          order, no sub-lock may reference another sub-lock's content. Every chunk
          names its coherent capability and its files or targets; every capability and
          shared contract has exactly one owning chunk, so parallel chunks cannot collide
          or depend on private capability. Check that every fence's `Surface lifecycle:`
          value matches the route and contract artifacts settled in step 7.
      
          **Author, validate, commit, then pin.** The fence is an `EXECUTION LOCK v2`
          envelope, not a second copy of the plan: which `Source:` mode it pins depends
          on what this repo keeps. Set `<lock-id>` to 1 for a ticket's first lock, or to
          one more than the highest `<lock-id>` already posted on this ticket — it
          identifies this lock, and never resets. A legacy `WORK ORDER` carries no
          `<lock-id>` and contributes no number, so a lock superseding one reads 1.
      
          * **OpenSpec repository.** Author the change on the ticket branch — `proposal.md`,
            `tasks.md`, `design.md` when a decision needs one, and its `specs/` deltas —
            from what grounding and `/scope` settled. Run `openspec validate <change-id>
            --strict` and resolve every failure; a change that does not validate strictly
            is not eligible to be pinned. Commit the authored change on the ticket branch,
            then read back its full commit OID
            (`git -C <worktree> rev-parse HEAD`) — never a value remembered from before
            the commit, and never an abbreviated OID. Set `Source: openspec
            <change-path>@<full-commit-oid>` to that exact pair. Whole-change ownership is
            a property of the ticket, not of the fence: set the flat lock's or chunked
            header's `Selected tasks:` and `Acceptance anchors:` positionally against the
            pinned commit's `tasks.md` and spec-delta requirements — `all` for an ordinary
            ticket, which owns its whole change, or the epic child's owned subset — and,
            when chunked, set each sub-lock's `Selected tasks:` and `Acceptance anchors:`
            to a disjoint positional slice of that same header selection, together
            covering it exactly. Leave `Context` as orientation only. Omit `Do` entirely —
            the pinned tasks are the Do steps. `Done when` states only this lock's own
            delivery acceptance (the verification command's expectation and the
            stop-at-pull-request condition), never a restatement of the pinned source's
            acceptance criteria; the pinned commit is the sole authority for what those
            criteria are.
          * **Repository-native plan.** When the repo keeps some other versioned,
            reviewable plan artifact instead of OpenSpec, commit it on the ticket branch
            the same way, read back its full commit OID the same way, and set `Source:
            repository-native <path>@<full-commit-oid>` to that exact pair. `Selected tasks:`,
            `Acceptance anchors:`, `Context`, `Do`, and `Done when` follow the same rules
            as the OpenSpec path, against that artifact's own positional numbering.
          * **Neither exists.** Set `Source: inline` and omit `Selected tasks:` and
            `Acceptance anchors:`; the fence carries the full `Context` / `Do` / `Done
            when` payload verbatim, as today's work order does.
      
          Set `Expected diff` on every fence — the flat lock, the chunked header, and
          each sub-lock: a closed allowlist of repository-relative paths, with no escape
          clause, per [drafting conventions](../references/drafting-conventions.md).
          A chunked order's sub-lock allowlists are pairwise disjoint across parallel
          chunks. Never post a lock whose `Source:` commit has not been read back from
          the actual commit just made; a remembered or predicted OID is not a pin.
      
          An `investigation` posts a lock only when the work will be dispatched to a
          bounded worker; that lock is an ordinary one whose `Expected diff` names the
          findings it may write. An investigation the attended session works itself
          needs none — the ticket plus its posted findings are the record — and
          `start` is not run on it.
      
      For a flat order, copy the already-selected execution row's `Ladder` value from [`routing-table.md`](../../orchestrate/references/routing-table.md) into the template's `Session fit:` paragraph, keeping each model's display name and ladder order.
      
      ### Chunked session fit
      
      For a chunked order, select one coordinator execution row with the same grounded, fail-closed classification rule that selects a flat order's execution row. Copy that row's `Ladder` value into exactly one `Session fit:` paragraph in every sub-order fence, keeping each model's display name and ladder order, and annotate each paragraph with exactly one `selected Agent rung: <Rung>`. A missing, duplicate, malformed, unresolved, or ineligible ladder or selected rung returns through `/scope` and produces no draft or comment.
      
      12. **Adversarial review, mandatory.** Every draft order gets reviewed before it is
          shown to the user or posted; there is no unreviewed path to step 13. A
          delegated `triage` worker returns its review-ready draft to its coordinator
          through the coordinator-recorded durable result locator, then stops at this
          boundary. Its coordinator dispatches `/plan-review`, verifies the verdict, and
          resumes the same worker with actionable findings or a verified clean verdict.
          The worker must not launch a nested reviewer. A coordinator-run triage runs
          `/plan-review` against the draft directly: it spawns cold reviewer agents with the
          five-axis rubric (grounding, acceptance, interface shape, scope, cost) and
          returns objections and a verdict. Review depth follows that skill's stakes
          tiering: an ordinary order gets one panel, and a load-bearing one ends only
          when a fresh cold pass returns no blocking objections.
          The ticket skill page's `## Delegation authority` section covers this mandatory
          `/plan-review`.
      
          When the stamped profile is hardening, run `/plan-review` only on Full-depth
          orders. Default-workflow orders keep this review unconditionally.
      
          Triage-specific additions on top of that skill:
      
          a. Verify every finding against ground truth (the repo, a provider's source,
          live state) before acting on it, and carry verified facts forward in each next
          reviewer's prompt so settled points do not re-litigate. This gate is
          structural, not advisory: no objection reaches a fix round until its factual
          claims are reproduced (execute the regex, parse the shell, open the cited
          file), and the fix-round prompt carries the evidence per objection. A claim
          that fails reproduction is recorded as refuted and never forwarded. One
          measured review forwarded a single unverified reviewer claim; it got baked into
          the order and cost a full round to retract.
      
          b. Outside an epic child, instrument every round in the scope ledger:
          blockers found, each tagged `authoring` (present since the draft) or
          `injected` (introduced by a prior fix round). For an epic child, keep the same
          instrumentation in untracked session scratch outside the branch and discard it after the final
          order; create no scope ledger or docs/scope state. The reviewed parent-plan amendment from
          step 7 is the only planning artifact this child branch may write. Injected blockers climbing
          across rounds is the rewrite-clean signal firing.
      
          c. Reviewers get the facts already verified live this session and the user's
          settled decisions, marked do-not-re-litigate.
      
          d. Each objection states the claim or gap, the evidence (file and line, or the
          live query), why it breaks the build if unfixed, and the cheapest fix, marked
          **blocks posting** or **note**. Anything that would not change what gets built
          is discarded rather than reported. Taste is not an objection. An empty list on a
          sound order is a successful review.
      
          e. Chunked orders get one more axis: does each sub-order stand alone in a fresh
          agent; does every capability and shared contract have exactly one owning chunk;
          are two parallel chunks' file, target, and capability ownership disjoint; and
          does the serial ordering reflect a real dependency rather than habit. A chunk
          failing any of those is a blocking objection.
      
          Hard cap at three review panels regardless of tier. Blocking objections still
          arriving at the cap mean the order has unsettled decisions, not undiscovered
          typos: route them through `/scope` (step 7) and stop drafting until it resolves
          them. When rounds accumulate, rewrite the order clean instead of patching it; in
          one measured run, roughly a third of late findings were defects the patches
          themselves introduced. Stop earlier when a round yields only wording polish,
          because the executing agent grounds in the same repo and resolves polish itself.
      
      13. **Confirm, then post.** Show the user the draft. On approval, post it as one
         ticket comment through the contract's post operation: attribution quote block
         first, then the human summary, then the fenced `EXECUTION LOCK v2` lock or
         header-plus-sub-locks. One comment carries the whole order, chunked or not. For
         an epic child, this is the only fenced execution lock; the issue-body draft
         never substitutes for it.
      
      14. **Move the status** to triaged, passing the classification from step 6. Report
          a failed move; do not retry. A failed code classification `build` creation or
          attachment retains the posted work order but prevents `ticket:triaged`.
      
      ## Refusals
      
      * The ticket is really a parent: triage the child, not the parent.
      * Scope requires decisions only a human can make and the user is unavailable: post
        nothing, and list the open questions in the session.
      
  • SKILL.md 18.9 KB
    ---
    name: ticket
    description: "Drive one tracked ticket from arrival to resolution through four verbs: triage, start, revise, finalize. Use when the user says triage/start/revise/finalize with a ticket id, asks to turn a ticket into a locked brief, to action a review round on a ticket's pull request, or to close out a merged ticket. When delegated, the coordinator dispatches every mandatory reviewer and resumes the same worker."
    ---
    
    # Ticket
    
    One ticket, one verb at a time. Each verb is a full procedure in `verbs/<verb>.md`;
    read that file before doing anything else, then follow it. This page holds only
    what every verb shares.
    
    ## Invocation
    
    `/ticket <verb> <ticket-id>`, where verb is `triage`, `start`, `revise`, or
    `finalize`. No verb or no ticket id: ask for it, one line. Unknown verb: list the
    four.
    
    ## The pipeline
    
    * `triage` reads the ticket and the repo, interviews the user through `/scope`
      when scope is thin, and ends by posting the **work order**: a locked brief as a
      ticket comment. Work too big for one agent's context is sliced into sub-orders
      in that same comment ([references/slicing.md](references/slicing.md)). It
      writes nothing to the repo except scope and spec documents committed in the
      ticket's worktree. An epic child treats its issue-body order as a draft and may
      commit a required parent-plan amendment in the child worktree before posting the
      reviewed lock; that amendment travels with the implementation pull request.
    * `start` runs in a fresh session, fetches the work order, refuses if there is
      none, implements it on a branch in an isolated worktree (or, on a sliced order,
      coordinates one agent per chunk), iterates the verification step until the
      result matches the order's expectation, passes an adversarial review at the
      order's stamped depth ([references/review-depth.md](references/review-depth.md))
      unless `Profile: hardening` replaces it below Full depth,
      opens the pull request, and stops. Agents never merge.
    * `revise` actions one review round on the open pull request: reload the ticket
      and the order, fix, re-verify, push.
    * `finalize` runs after a human merged: verify the merge and post-merge workflow,
      complete the repository's post-merge archive guidance for an ordinary OpenSpec
      change, which opens a reviewed archive pull request and posts its `Archive PR:`
      locator before stopping, then on a later finalization, once a human merged that
      pull request, close the ticket with a comment linking the pull request, record
      what the ticket actually cost in context, and tear the worktree down, so this
      repo's slicing calibration is tuned against measured numbers rather than
      intuition.
    
    ## The tracker contract
    
    Every tracker interaction goes through four operations: read a ticket, post a
    comment on a ticket, move a ticket's status, and locate the newest work order on
    a ticket. [references/tracker-contract.md](references/tracker-contract.md)
    defines them, and one binding page supplies them for one tracker. GitHub issues
    ship as the reference binding
    ([bindings/github-issues.md](bindings/github-issues.md)).
    
    The procedures below and in `verbs/` call the contract, never a tracker's API
    directly. A verb that cannot reach the contract stops and names what is missing.
    
    ## Review front door
    
    `start` and `revise` reach code review as `/review` on the changed code, which
    routes to `code-review`. Neither verb calls a reviewer any other way, and neither
    substitutes a lighter check for the depth the order stamped. Below Full depth under
    `Profile: hardening`, [start](verbs/start.md) and [revise](verbs/revise.md) use its
    exception instead.
    
    ## Delegation authority
    
    This authority covers triage's mandatory `/plan-review` and start/revise's `/review` route. Invoking `/ticket` authorizes every sub-agent dispatch that this procedure marks mandatory, including the coordinator's mandatory reviewer dispatch. Do not ask again solely because a session-level preference says "do not spawn agents"; apply that preference to discretionary delegation only. An explicit task-level refusal of this required review or revocation of delegation overrides this authorization: stop and state that the requested workflow cannot run without its required independent review.
    
    When Ticket work is delegated, the delegation prompt identifies the
    mandatory-review handoff. At that boundary the worker returns or writes its
    review-ready result through the coordinator-recorded durable result locator and
    does not launch a reviewer. The coordinator dispatches every mandatory reviewer
    through the existing adapter after collecting the result, verifies the returned
    verdict, and resumes the same worker. Actionable findings resume it for correction; a
    verified clean verdict resumes it to finish. A failed launch, nonzero exit,
    missing result artifact, or missing verdict is reported as unavailable and blocks
    the workflow from advancing as reviewed. Direct nested adapter dispatch by the
    worker is unsupported.
    
    ## Selected-ticket mutation boundary
    
    Triage may mutate operator-local workflow state required by the installed workflow
    to execute the selected ticket lifecycle. Current examples include the lifecycle
    claim, exact-worktree Codebase Memory state, reviewer-memory store, and local
    remote-tracking refs used to resolve and verify the selected ticket's base. These
    examples make the purpose concrete; they are not an exhaustive list.
    
    Triage may also mutate repository or tracker state belonging to the selected ticket
    lifecycle without ancillary approval. Current examples include the selected worktree
    and branch, an ordinary ticket's active change, the selected ticket's comment and
    status, and the defined Epic-child parent-plan amendment carried by that child's
    implementation pull request. These examples make the ownership concrete; they are
    not an exhaustive list.
    
    This authority does not authorize state for a distinct external concern:
    independently addressable work outside the selected lifecycle, such as another
    branch, pull request, issue, ticket, or repository artifact outside the selected
    branch. Broad read-only grounding never authorizes it.
    
    ## Shared rules (every verb)
    
    1. **Open with the ticket summary.** Before any other work, read the ticket and
       give the user an extremely high-level, human-readable summary: what the ticket
       is and what this verb is about to do on it (as simple as "implementing
       `<ticket-id>`, which is `<one-line description>`"). Then mark a chapter titled
       `<ticket-id> <verb>` when the harness offers a chapter tool, so the user can
       scroll back to it. Skip the chapter silently when it does not.
    
    2. **Claim the session.** Immediately after the ticket summary, run
       `python3 <ticket-skill-directory>/scripts/ticket.py claim <ticket-id> --verb
       <current verb>`, so the
       sessions that worked this ticket are recorded as they work it rather than
       guessed from prose afterwards. Pass `--session` and `--agent` whenever the
       environment cannot answer on its own: no session id in it, or more than one,
       which is what a worker launched from another agent's session sees. Pass
       `--role` to say what the session is doing on the ticket: `coordinator` (the
       session driving the ticket, and the default), `worker` (an agent building one
       chunk), or `reviewer` (a session that only reviews). The role decides which
       costs are evidence about how big the work was, so a session claimed under the
       wrong one is a measurement error. The required `--verb` is `triage`, `start`,
       `revise`, or `finalize`, matching the lifecycle verb this session is running.
       One session serves one lifecycle verb: same-verb resumes reuse the claim, while
       changing verbs requires a fresh session. A cross-verb re-claim keeps and prints
       the persisted claim, reports the persisted and submitted verbs as one visible
       conflict, and exits successfully; telemetry never claims the submitted metadata
       landed. A claim that fails is said in one
       line and never blocks the verb: telemetry is a measurement, not a gate. A
       sandboxed session (a Codex `workspace-write` sandbox, for one) that cannot
       write the claims file under `~/.config/ticket/` sees that one-line denial
       name the path and the fix: rerun the same claim command outside the sandbox
       or with escalated permissions.
    
    3. **Attribution first.** Every comment this skill posts opens with a one-line
       quote block. With an operator name configured:
    
       > Written by an AI agent operating for `<operator>`. Verify before relying on it.
    
       With none configured:
    
       > Written by an AI agent. Verify before relying on it.
    
       The name comes from `~/.config/ticket/config.json`, key `operator`. No file, no
       key, or an empty value all mean the nameless form. Then the content. Never post
       an unattributed comment.
    
    4. **The lock is the only entry to execution.** A work order is a ticket comment
       whose fence header starts `EXECUTION LOCK ` (any version) or, on a ticket still
       running the legacy protocol, `WORK ORDER`. `start` and `revise` locate it with
       the tracker contract's locate operation
       ([references/tracker-contract.md](references/tracker-contract.md)): newest
       comment wins by post time across both protocols, and no field is ever merged
       from an older comment into a newer one. No order, no execution: refuse and
       route to `/ticket triage <ticket-id>`. Admission is the consumer's job: an
       unrecognized `EXECUTION LOCK` version or `Source:` mode refuses the same way
       rather than falling back to an older comment. A legacy `WORK ORDER` keeps
       today's sufficiency rules with no inferred pin, forever; any supersession uses
       the new protocol.
    
    5. **One worktree, one branch, per ticket, for the whole lifecycle.** `triage`
       cuts the branch and worktree through `spin-worktree`; `start` and `revise`
       reuse them; `finalize` tears them down. The first repository action after the
       summary-and-claim opening, before grounding or any repo read, is to cut or
       reuse the ticket's worktree. The one pre-worktree exception is fresh epic-child
       triage: it fetches and verifies the issue body's pinned remote parent-plan base,
       then passes that branch to the helper. Outside an epic child, grounding, scope ledgers, and
       the active change record are written and committed there; post-merge archiving
       follows `operations.archive.guidance` in a sibling archive checkout, which is the
       one narrow post-merge exception to this rule and lands through its own reviewed
       pull request rather than a push to `main`. An epic child keeps its
       instrumentation in session scratch and relies on its parent record. The control checkout may be dirty, stale, or
       on another branch: its working tree is never read or written, and it never
       switches branches. Never commit, stash, move, or clean its files, and never
       substitute another task's worktree as the control checkout. It holds the
       ticket's branch ref, which is what the worktree is cut from. Before its first
       write, every verb confirms that its working directory is the path the worktree
       helper reported; a mismatch stops the verb.
       A chunked order is the one exception and does not loosen the rule: chunk agents
       work in per-chunk worktrees cut from the ticket branch and torn down as each
       chunk merges back into it, so the ticket still ends with one branch and one
       pull request ([references/coordinator-mode.md](references/coordinator-mode.md)).
    
    6. **Working state lives on the ticket.** No scratch directories live on the branch.
       Outside an epic, the branch carries shipping code plus the repo's own change
       record. An epic child creates no per-child change record; its branch carries
       implementation plus any required parent-plan amendment that triage committed,
       while the parent epic's active change remains the authority.
    
    7. **Ground in what the repo already says.** Read the repo's own decision and
       change records, `docs/`, and recent `git log` before forming opinions. Read the
       standing-decisions source named below when a project configured one.
    
    8. **Status transitions.** Verbs move the ticket: `triage` to triaged, `start` to
       in progress when the branch is cut, `start` to pending review when the pull
       request opens, `finalize` to done. Status is the contract's one non-fatal
       operation: when a move is unavailable or fails, say so in one line and
       continue. Never retry a failed move and never force a workaround.
    
    9. **Stop at the pull request boundary.** Opening the pull request ends `start`.
       Merging is human. `finalize` only runs after a human merged.
    
    10. **Fresh-session contract.** `start` assumes no memory of triage. Under a
       legacy `WORK ORDER`, everything it needs must be on the ticket, in the
       description plus the work order's own copied prose. Under an
       `EXECUTION LOCK`, self-sufficiency means deterministic acquisition and
       verification of the authorized source instead of copied prose, and what that
       means depends on the source mode. For `openspec` and `repository-native`,
       the pinned commit OID plus the lock's own execution-shape fields (session
       fit, verification, expected diff) are enough for a fresh session to resolve,
       read, and admit the source itself, per [start](verbs/start.md) step 5. For
       `inline`, there is no commit to resolve: the fence's own `Context`/`Do`/`Done
       when` payload is the self-sufficient copy, the same as a legacy order's.
       Either way, if what a fresh session needs is not there, that is a triage
       defect: refuse and say what is missing.
    
    ## The verification step
    
    Every order names one verification step and one expectation for its output. The
    step is a slot:
    
    * **Default:** the target repo's own lint and tests, discovered from the repo. Read
      its `AGENTS.md` or `CLAUDE.md` for a test command, then its CI workflows, then
      its package scripts. Name the command in the order.
    * **A binding may fill the slot with something stronger.** An infrastructure
      preview is the worked example: a read-only plan against real state, run locally
      before the pull request. When a binding fills the slot, the order's expectation
      line describes that tool's output instead of a test result.
    * **The rule that survives either way:** iterate locally until the result is
      exactly what the order's expectation says. CI is the check of record, not the
      iteration loop.
    * **Never fabricate expected output.** When verification cannot run at all (no
      credentials, no access, no runnable suite), say so, open the pull request as a
      draft, and name the missing evidence.
    
    ## The graph identity
    
    Before a verb reads code structurally, it binds its current checkout to exactly one
    Codebase Memory project, from that checkout's own path:
    
    ```sh
    python3 <cbm-onboard-skill-directory>/scripts/cbm-lifecycle.py ensure <worktree path>
    ```
    
    It prints one object, and the verb reports it verbatim:
    
    ```json
    {"root_path": "<canonical physical checkout>", "project": "cbm-onboard-v1-<sha256>", "status": "ready"}
    ```
    
    * `ready` or `indexed`: query the graph as exactly that `project`. Never pick the
      graph by project name, branch-like label, list order, apparent recency, or because
      it was the only result.
    * `unavailable` (exit 2): follow the owning `cbm-onboard` skill's bounded
      supported-version and sandbox retry/fallback sequence first.
      An active-generation conflict means wait and retry the same checkout; it is not a
      sandbox escalation or authority to close another session. After that owned
      sequence is exhausted, say so in one line and use ordinary discovery.
    * Any other failure (exit 1): stop the verb and report what `ensure` printed on
      stderr, which names the cause — a path that is not a checkout, or an installed
      tool answering for the wrong project or root. Neither is a case where guessing a
      graph is safe.
    * The command never ran at all, no exit code, because the harness or sandbox
      refused it (a permission classifier declining the Bash call, for example): say so
      in one line and use ordinary discovery for the rest of the session, the same as
      `unavailable`.
    
    Every session recomputes this from the checkout it just verified, never from chat
    memory or a remembered earlier run. It names one machine's paths, so it never goes
    into a work order or any other tracker comment; a chunk agent is handed its own in
    its prompt.
    
    Before Git removes a worktree this skill authored, the same directory's
    `cbm-teardown.sh` deletes that checkout's project, while the checkout still exists
    for the identity to be derived from. Teardown fails loudly on a machine with no
    Codebase Memory installed, which is expected: report it in one line and carry on
    with the removal. It never holds up the removal, and it is never retried.
    
    ## The hardening profile
    
    The target repo declares `Harden: <command>` beside its test command in repo facts.
    Triage stamps `Profile: hardening` only when that line exists.
    It replaces the review rounds as [start](verbs/start.md) and [revise](verbs/revise.md) specify.
    A hardening command that cannot run is an error, never a pass.
    The profile order's QA script lives in its pull request body.
    
    ## Standing decisions
    
    A project may point this skill at a knowledge base of standing decisions and
    traps to read before grounding in the repo. Its location is the project's to
    name: a path in the repo, a file the operator configured, or a page the binding
    knows about.
    
    Absent, the verb says so in one line and continues. It never refuses a ticket for
    want of it.
    
    ## The change record
    
    The skill records the change where the target repo already records changes.
    
    Whoever executes the change ticks its checklist as work completes, and a checked
    item means implemented and verified, not attempted. That is why a checkbox commit
    in a pinned source is the executor's own bookkeeping rather than an amendment
    ([start](verbs/start.md) step 5).
    
    An **epic child** creates no per-child change record. Its parent epic owns the active
    change and its post-merge archive. Triage may commit a required parent-plan
    amendment in the child worktree; start, revise, and coordinator mode preserve the
    parent-plan bytes through the implementation pull request, and finalize leaves the
    parent active and unarchived.
    
    Outside an epic, follow this per-ticket rule:
    
    1. The repo has an OpenSpec layout (`openspec/`): write the change folder on the
       ticket branch (`proposal.md`, `tasks.md`, and `design.md` when the work
       embodies a real decision). Start and revise keep the active change and its
       deltas reviewable in the ticket pull request; they do not fold or archive it
       before merge. The repository's `operations.archive.guidance` determines when
       finalization archives a verified merge, and the archive itself lands through a
       reviewed follow-up pull request that a human merges, never a direct push to the
       default branch. `/openspec-adopt`, when it is installed,
       is what adopts OpenSpec in a repo that lacks it. OpenSpec is the worked example,
       never a requirement.
    2. The repo has a different convention (a changelog, a decision-record tree, a
       design log): follow that convention exactly as the repo already uses it.
    3. The repo has no convention: write down what changed and why, where that repo's
       readers would look. Do not invent a convention for it.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related