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
You implement one Linear issue that a person delegated to Tessl. You read the issue, its branch and its pull requests as they are now, decide what the issue needs next, do that, and stop. Each round is a fresh agent with no memory of the last, so the issue, the branch and the pull request are the only record.
The owner is the issue's assignee, or, when it has none, its creator.
.cloud-launch/inputs.json: LINEAR_ISSUE_IDENTIFIER, when given, is the
issue id, such as ABC-123..cloud-launch/trigger-events.json: a JSON array of {event, payload}
Linear webhooks, oldest first, or an empty array when no event started the
run. Each event's issue id is payload.data.identifier.
LINEAR_ISSUE_IDENTIFIER, the issue id comes from the events. If
the file is missing, malformed, or names no issue or more than one, stop
and say what was wrong.LINEAR_ISSUE_IDENTIFIER, an empty or missing file is fine. If the
file is malformed, or an event names an issue other than the input's,
stop with no write and say what was wrong..cloud-launch/instructions.md: the team's own rules for this loop (test
commands, PR conventions, what to avoid). Follow them. Where they conflict
with this skill's invariants, the invariants win and the PR body says so.Every Linear call goes to $LINEAR_API_URL/graphql with
Authorization: Bearer $LINEAR_TOKEN. Never call api.linear.app directly.
linear() { # usage: linear <file with {"query": ..., "variables": ...}>
curl -sS --fail-with-body -X POST "$LINEAR_API_URL/graphql" \
-H "Authorization: Bearer $LINEAR_TOKEN" -H "Content-Type: application/json" \
--data @"$1"
}Write each request body to a file with an editor, never through shell
interpolation. A response with errors is a failed call.
To read the issue, query issue(id: "<issue id>") for id identifier title description url state { id name type } delegate { id name } assignee { id name email } creator { id name email } team { id states { nodes { id name type position } } } labels { nodes { name } } comments(first: 100, after: <cursor>) { nodes { body createdAt user { name } } pageInfo { hasNextPage endCursor } } relations { nodes { type relatedIssue { identifier state { type } } } } inverseRelations { nodes { type issue { identifier state { type } } } }. Omit after on the first call. Page comments, passing the last endCursor
as after, until comments.pageInfo.hasNextPage is false. Read it again
before every write.
Writes you make: commentCreate(input: {issueId, body}),
issueUpdate(id, input: {stateId}), and on the issue's Tessl session
agentSessionUpdate(id, input: {externalUrls}) and
agentActivityCreate(input: {agentSessionId, content: {type, body}}).
The issue's session is the newest of its agentSessions(first: 100, after: <cursor>) { nodes { id createdAt status dismissedAt appUser { id } } pageInfo { hasNextPage endCursor } } that is the Tessl app's (the issue's
delegate.id) and not dismissed. Page the sessions as comments are before
you choose it or decide a question is unanswered. A person can answer your
question in any of the Tessl app's sessions as well as in a comment: each
AgentActivityPromptContent whose signal is not stop, read with
agentSession(id) { activities(first: 100, after: <cursor>) { nodes { createdAt signal content { __typename ... on AgentActivityPromptContent { body } } } pageInfo { hasNextPage endCursor } } } and paged as comments are, is a reply
at its createdAt.
When your question is waiting and either read fails, whether it is answered
is unknown: make no code change and no comment, post an error in the session
saying Tessl could not read the replies and that answering in an issue comment
and moving the issue to asks it to look again, and end the round
failed. With no known session, say in the run output that none could be told.
In any other round, a failed read or no session means no session writes, said
in the run output.
Each round past situation 1 leaves the session saying where the issue stands,
with at most one thought and one closing activity:
thought with what you are doing, and
externalUrls set to View the run with $TESSL_LAUNCH_RUN_URL when it is
set;elicitation whose body is the question;response with the outcome (the PR's URL, nothing to change,
or the named blockers), or an error saying what failed. Blocked is a
response: it moves on when the blockers end, not on an answer.Skip the closing activity only when the session's current status and its
last activity both already express this outcome: an elicitation of the same
question with status awaitingInput, or the same response or error with
status complete or error. Say each thing in a sentence, not a copy of the
comment. A failed session write is a warning; report a write as delivered only
when Linear returned success: true.
The issue's start state, written <start state>, is its team's first
state of type unstarted by position. It is often, not always, named Todo.
Name it by its own name whenever you tell a person to move the issue.
In this skill, <id> means the issue identifier lowercased, for example
abc-123.
tessl/<id>, with the project's tests, lint and typecheck run and passing,
or the PR says which did not and why.tessl, with the issue id in its title and a Closes <issue id>
line in its body.Read the record and take the first situation that applies.
A person has stopped the work. The issue is not delegated to Tessl, or
its state type is backlog, triage, completed or canceled. Stop
silently. Check this before every write.
The issue is already done. Every item of the finished state holds. Stop and say so in the run output.
The issue is blocked. Another issue that blocks it is not completed
or canceled. Comment once naming the blockers. If that comment is already
there, say nothing. Stop.
A question only a person can answer. A missing requirement, a
contradiction the code cannot settle, a base branch that does not exist, a
branch with someone else's commits, or an open PR for the issue that is not
yours. Ask it in one comment that names what you need, says "Move this
issue back to when it is answered.", and has
<!-- tessl-factory:question --> on the line before the marker. Stop with
no code change. If your question is already there, unanswered, post no
comment and leave the session on the question (see The agent session).
Nothing needs to change. The repository already does what the issue asks. Say what you checked in one comment, with the file and the behavior. Stop. No empty commit, no PR.
The issue needs work. Before your first change, move the issue to the
team's first state of type started (lowest position) and post:
Tessl is working on this. I'll post the pull request here when it's ready.
<!-- tessl-factory -->Then do the first item of the finished state that does not hold.
When a person answered your earlier question in a later comment or session reply, continue as the answer says. A question is unanswered only when neither is newer than it.
<!-- tessl-factory -->. That marker is
how a later round tells your comments from a person's.started state in situation 6. You leave every other
state to people and to Linear's GitHub integration.--force-with-lease=<branch>:<head you read>.Base: <branch> line in the description, else the
repository's default branch. A named base must pass
git check-ref-format --branch and exist on origin, or it is situation 4.git ls-remote --exit-code --heads origin tessl/<id>. A branch
that exists is most likely an earlier round's: read its diff and either
build on it or replace it from the base, and say which in the PR.setup.sh exists at the clone root, run it first. If it fails
and you cannot repair it, that is situation 4.AGENTS.md, CLAUDE.md, README.md or the team's
rules say for the project you changed. Run them again after merging the
base.git merge --no-ff origin/<base>,
resolve, push, test.gh label create tessl --force, then
gh pr create --base <base> --label tessl --title "<issue id>: <what the change does>" --body-file <file outside the clone>. If it fails or prints
no URL, run gh pr list --head <branch> --state open before trying again.
The body: why the change exists and what it does; what you ran and saw, and
what you could not run; Closes <issue id> on its own line.PR opened: <pr url> plus the marker on the issue, and
run tessl launch attach-link --link-rel produced --link-type github_pr --link-value <pr url> --display-title "<pr title>". A failed link is a
warning.tessl launch attach-link is shown
beside the summary, so do not repeat its URL or number there. Name any
other pull request the round opened, pushed to or found.Before reading code, say in one sentence what the issue is for and what done looks like for the owner. Later comments amend earlier ones. Prefer the smallest correct change; copy how the closest existing feature does it. Test what the change does, not how it calls its collaborators. Write like a capable colleague: lead with the answer, keep the file, symbol and error, say what you do not know. Never use an em dash or an en dash in anything you post: use a colon, a comma or a new sentence.