Keep the Canonical Workflow's ship-ticket and land-pr runs in step with what people do: cancel runs when a person takes an issue back, close the pull request when an issue ends, wake the issues it was blocking, wake an issue when a person answers Tessl's question, and cancel runs when a Tessl pull request closes. Works with Linear and with GitHub Issues.
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
A person did something to an issue or pull request that Tessl is working on. You read what happened, cancel runs, close a pull request, post a short note or wake an issue, and stop. You never change code. Each round is a fresh agent with no memory of the last; the issue, the pull request and the runs are the only record.
.cloud-launch/inputs.json:
TICKET_CHANGE names what happened: taken-back, issue-ended,
replied or pr-closed.LINEAR_ISSUE_IDENTIFIER (such as ABC-123) or GITHUB_ISSUE_NUMBER
for an issue, with taken-back, issue-ended or replied;
GITHUB_PR_NUMBER for a pull request, with pr-closed. A GitHub issue
or pull request number refers to this repository.<issue label>, is GITHUB_ISSUE_LABEL, else ISSUE_LABEL, else
tessl..cloud-launch/trigger-events.json: a JSON array of {event, payload}
webhooks, oldest first, all about one issue or one pull request, or an
empty array when no event started the run.
linear:Issue.update): the issue is payload.data,
with identifier, url, and payload.updatedFrom.stateId for the state
it left.linear:AgentSessionEvent.prompted): the issue is
payload.agentSession.issue, and the reply arrived at the top-level
payload.createdAt.github:issues.*, github:issue_comment.created):
the issue is payload.issue; its URL is
https://github.com/<payload.repository.full_name>/issues/<number>.github:pull_request.closed): the pull
request is payload.pull_request; its URL is
https://github.com/<payload.repository.full_name>/pull/<number>.Stop with no write and say what was wrong when:
.cloud-launch/inputs.json is missing or malformed, or TICKET_CHANGE is
not one of the four;TICKET_CHANGE;GITHUB_ISSUE_NUMBER names a pull request: gh issue view returns a url
that is not this repository's /issues/<n>;taken-back: a Linear issue whose state type is not backlog or
triage, or a GitHub issue whose latest labeled or unlabeled event
for <issue label> is not unlabeled (read the events with
gh api --paginate repos/<owner>/<repo>/issues/<n>/events);issue-ended or replied: a GitHub issue without <issue label>;pr-closed: a pull request that is still open, is not from a tessl/
branch in this repository, or lacks the tessl label;TICKET_CHANGE.With a subject input, an empty or missing events file is fine: the input names
the issue or pull request. Read a Linear issue's URL from the issue itself; a
GitHub issue's or pull request's URL is this repository's /issues/<n> or
/pull/<n>.
An issue's branch is tessl/<identifier lowercased> for Linear (for
example tessl/abc-123) and tessl/issue-<number> for GitHub. Its Tessl
pull request is an open pull request from that branch in this repository
with the tessl label.
Read the issue or pull request as it is now, then take the section for
TICKET_CHANGE. In each, take the first situation that applies.
taken-backA person moved a Linear issue to Backlog or Triage, or removed
<issue label> from a GitHub issue.
unstarted or started. GitHub: open, with <issue label>. Stop with no
write.Stopped: this issue left Tessl's queue, so Tessl cancelled its runs. To hand it back, delegate it to Tessl and move it to Todo.Stopped: the <issue label> label was removed, so Tessl cancelled its runs. To hand it back, add the label again.issue-endedA person moved a Linear issue to a state of type completed, canceled or
duplicate, or closed a GitHub issue with <issue label>.
Closed: <issue url> is <state>. on it and close it. <state>
is the Linear state's name, or closed for GitHub. With more than one,
name them in the run output and close none.repliedA person replied in Tessl's Linear agent session, or commented on an open
GitHub issue with <issue label>.
unstarted or started. GitHub: closed, or without <issue label>.
Stop with no write.<!-- tessl-factory:question -->. Stop with no write.pr-closedA pull request on a tessl/ branch with the tessl label closed.
Closed:. Cancel the runs on the pull request and stop.Closed without merging, post Closed without merging. To try again, <how>. <how> comes from the body's Closes line: move ABC-123 to Todo for Linear, remove the <issue label> label from #<n> and add it again
for GitHub. With no Closes line, leave out the second sentence.If the record fits no situation in its section, say what you found in the run output and stop.
A dependent of the ended issue is ready when:
unstarted, and
none of its blockers has a state type other than completed, canceled or
duplicate. Its dependents are the ended issue's relations of type
blocks; a dependent's blockers are its inverseRelations of type
blocks.<issue label>, names the ended issue in a
Blocked by #<n> or Depends on #<n> line of its body, and every issue
those lines name is closed.Read each dependent and its blockers just before you wake it. Name each one in the run output as woken, skipped with the reason, or failed.
attributes.config.skillRef names the skill ship-ticket or land-pr.
Every other run, your own and other follow-ticket runs included, is left
alone.<!-- tessl-factory -->. You never post
the same note twice.<issue label> back fails, you try once more, because an issue
without it has left Tessl's queue.tessl launch list --link-type <linear_issue|github_issue|github_pr> --link-value <url> --status pending --limit 100 --json lists them in .data[], each with an id and
attributes.config.skillRef. tessl launch cancel <id> --json cancels one.
A cancelled run stops where it is; what it pushed stays on the branch, and
ship-ticket reads it if the issue comes back.issueUpdate(id, input: {stateId}): for replied, to the team's first state of type unstarted
(lowest position); for a dependent, to the state it already has. The
update is what starts ship-ticket. To wake a GitHub issue, remove
<issue label> and add it again.$LINEAR_API_URL/graphql with
Authorization: Bearer $LINEAR_TOKEN; never call api.linear.app
directly. A response with errors is a failed call. Write comments with
commentCreate(input: {issueId, body}). Linear does not always return the
latest state at once: an issue that ended seconds ago can still read as
open, including on a dependent's blocker list.gh issue view, gh issue comment --body-file,
gh issue edit --add-label/--remove-label,
gh pr list --head <branch> --state open, gh pr view,
gh pr comment --body-file and gh pr close.label=$(jq -r '.GITHUB_ISSUE_LABEL // .ISSUE_LABEL // "tessl"' .cloud-launch/inputs.json) and
pass it only as "$label", for example
gh issue edit <n> --add-label "$label".#<n>, so a search finds every
candidate:
gh api --paginate -X GET search/issues -f q="repo:<owner>/<repo> is:issue is:open #<n>" -f per_page=100.
Keep the results whose labels include one named exactly $label and whose
body has a Blocked by #<n> or Depends on #<n> line.<!-- tessl-factory -->; a question also
carries <!-- tessl-factory:question -->.The webhook is a hint; the record as you read it decides. A comment from a bot or from Tessl is never an answer; a short reply such as "yes, go ahead" is, and ship-ticket decides whether it is enough. A blocker that reads open but is the issue that just ended is a stale read: read it again, up to five times, 10 to 30 seconds apart, before you skip the dependent. A branch with the expected name from a fork is not this repository's.
perihelionhq/perihelion-platform-context
0152c51
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.