Skills that Tessl factory loops run: implement a Linear or GitHub issue as a pull request, shepherd that pull request through review and checks, wake the shepherd for pull requests that a push to their base branch left in conflict, and keep the work in step when a person stops, ends, answers or closes it.
86%
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, session-opened or pr-closed.LINEAR_ISSUE_IDENTIFIER (such as ABC-123) or GITHUB_ISSUE_NUMBER
for an issue, with taken-back, issue-ended or replied;
LINEAR_ISSUE_IDENTIFIER with session-opened; 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 for a reply,
linear:AgentSessionEvent.created for a new session): the issue is
payload.agentSession.issue, the session is payload.agentSession.id,
and a 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 five;TICKET_CHANGE;GITHUB_ISSUE_NUMBER names a pull request: gh issue view returns a url
that is not this repository's /issues/<n>;session-opened: anything but a Linear issue;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>.
A Linear issue's start state, written <start state>, is its team's first
state of type unstarted by position (team { states { nodes { name type position } } }). It is often, not always, named Todo. Name it by its own name
whenever you tell a person to move an issue.
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 <start state>.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.<!-- tessl-factory:question -->. Stop.A Linear reply that stops at 1 to 3 gets an answer in its session (see
Answering a session), unless another run has it, as session-opened 2 says:
then write nothing, since that run may own the session. Otherwise the answer
says what this reply did not start, and the one move that would make Tessl
act. For example:
session-openedA person opened a Tessl agent session on a Linear issue, by delegating the
issue or mentioning Tessl. The webhook has already posted a Received thought
and moved nothing. Opening a session does not start Implement; only an issue
event does. Implement can run before or after this round on the same issue.
status is
awaitingInput, complete or error, or Tessl's latest activity in it is
an elicitation, response or error. Stop with no write.backlog, triage, completed, canceled or duplicate. Answer why
Tessl is not working on it and the one move that would make it, as under
replied.inverseRelations issue of type blocks whose state
type is not completed, canceled or duplicate), name the blockers and
say they must finish before the issue can proceed; in a started state,
name what you find: a pending shepherd run, Tessl's pull request, or its
waiting question. Otherwise stop with no write and end the run at once,
since Implement may be queued behind you (see What a session answer never
claims).Answer only the event's payload.agentSession.id; with no event, say in the
run output that there is no session. Read it first (agentSession(id) { issue { identifier } status }): a failed read is a failed outcome, and a session of
another issue is not answered. Post one activity of one to three plain
sentences for someone who has never seen Tessl: an elicitation restating
Tessl's waiting question and where to answer when the issue is still Tessl's
(a reply that stopped at 3, or a session-opened answer at 4), otherwise a
response, which ends the session. A reply that stopped at 1, or a session
opened at 3, gets a response even when Tessl's newest comment is a question.
Moving an issue to asks Tessl to check it; it does not clear an
open question or replace a pull request, so never promise more.
Both a replied and a session-opened round run on the issue's lane, so
Implement can be queued behind yours with no launch run yet: an empty pending
list is unknown queue state, not proof that nothing was dispatched. So, in
either path, a session answer never says Tessl has no run on the issue or
promises that one starts, and never tells a person to move an issue that is
delegated to Tessl in an unstarted or started state, beyond restating
where Tessl's waiting question asks to be answered. A state name or an
earlier Tessl comment never shows a run is going either: say Tessl is
working on the issue only when a run on it is pending.
Read the issue's agentSessions. For each of the Tessl app's (its
appUser.id is your viewer { id }) that is not dismissed and whose status
is not complete or error, stale included, post one response saying in
a sentence that the work stopped and why: the issue left Tessl's queue, or it
is <state>. A failed post is a warning.
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 <start state> 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 a skill with implement or shepherd
in its name. Every other run, other follow-ticket-changes runs included,
is left alone. You never cancel your own run, the one whose id is
TESSL_LAUNCH_RUN_ID, whatever its skillRef says.<!-- tessl-factory -->; a session
activity does not. You never post the same note twice.replied round that stops at 1 to 3, and of a
session-opened round that answers: a refused one
is a failed outcome, and one whose delivery you cannot confirm (a timeout,
or no answer) is reported as unconfirmed. Neither is posted again in the
same round. A failed wake is
reported and not retried, except that when adding <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. When your own run carries the queried link,
it is pending and in that list; its id is in the TESSL_LAUNCH_RUN_ID
environment variable. tessl launch cancel <id> --json cancels one.
A cancelled run stops where it is; what it pushed stays on the branch, and
Implement 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 Implement. 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}) and session activities with
agentActivityCreate(input: {agentSessionId, content: {type, 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 Implement 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.