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'
Install
npx skills add https://github.com/ConnorGriffin/skills/tree/main/skills/drivers/ticket
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install connorgriffin-skills@llmmart
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
triagereads the ticket and the repo, interviews the user through/scopewhen 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.startruns 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) unlessProfile: hardeningreplaces it below Full depth, opens the pull request, and stops. Agents never merge.reviseactions one review round on the open pull request: reload the ticket and the order, fix, re-verify, push.finalizeruns 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 itsArchive 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)
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.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--sessionand--agentwhenever 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--roleto say what the session is doing on the ticket:coordinator(the session driving the ticket, and the default),worker(an agent building one chunk), orreviewer(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--verbistriage,start,revise, orfinalize, 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 Codexworkspace-writesandbox, 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.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, keyoperator. No file, no key, or an empty value all mean the nameless form. Then the content. Never post an unattributed comment.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.startandreviselocate 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 unrecognizedEXECUTION LOCKversion orSource:mode refuses the same way rather than falling back to an older comment. A legacyWORK ORDERkeeps today's sufficiency rules with no inferred pin, forever; any supersession uses the new protocol.One worktree, one branch, per ticket, for the whole lifecycle.
triagecuts the branch and worktree throughspin-worktree;startandrevisereuse them;finalizetears 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 followsoperations.archive.guidancein 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 tomain. 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).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.
Ground in what the repo already says. Read the repo's own decision and change records,
docs/, and recentgit logbefore forming opinions. Read the standing-decisions source named below when a project configured one.Status transitions. Verbs move the ticket:
triageto triaged,startto in progress when the branch is cut,startto pending review when the pull request opens,finalizeto 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.Stop at the pull request boundary. Opening the pull request ends
start. Merging is human.finalizeonly runs after a human merged.Fresh-session contract.
startassumes no memory of triage. Under a legacyWORK ORDER, everything it needs must be on the ticket, in the description plus the work order's own copied prose. Under anEXECUTION 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. Foropenspecandrepository-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. Forinline, there is no commit to resolve: the fence's ownContext/Do/Done whenpayload 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.mdorCLAUDE.mdfor 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"}
readyorindexed: query the graph as exactly thatproject. 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 owningcbm-onboardskill'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
ensureprinted 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:
- The repo has an OpenSpec layout (
openspec/): write the change folder on the ticket branch (proposal.md,tasks.md, anddesign.mdwhen 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'soperations.archive.guidancedetermines 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. - 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.
- 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.
Reviews (0)
No reviews yet.
No comments yet.