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.
88%
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 run one sweep. A listing script gathers the facts about every conflicting
factory pull request, you decide from those facts which issue each one belongs
to and whether to wake the fixer for it, and a trigger script makes the
trigger calls you chose and writes the run's result. The fixer is the action
the listing names in fixerAction: the FIXER_ACTION input, shepherd
when unset.
The run is done when the trigger script has written launch-result.json at
the clone root, with a decision for every fact sheet the listing wrote. Take
the first that applies:
TESSL_LAUNCH_DEADLINE is not set. Write a failed result saying the
run has no deadline, and stop.launch-result.json. Leave it as it is,
whatever the script's exit code, and stop.listing.json. Write a failed
result whose reason is the last line it printed, and stop.failed result whose reason is its last line, and
stop.failed result whose reason says so, followed by the
last line it printed, and stop.If the run fits none of these, write a failed result saying what you found.
From the clone root, where <skill-dir> is this skill's directory:
node <skill-dir>/scripts/list-conflicting-prs.mjs --until <listing end time>
node <skill-dir>/scripts/trigger-shepherd-rounds.mjs --until <trigger end time>Both read and write the sweep's directory, $TMPDIR/merge-conflict-sweep
(/tmp/merge-conflict-sweep when TMPDIR is unset). The listing's summary
line prints the absolute paths of its facts and decisions directories.
tessl action trigger calls for the
fixer.gh, no Linear, no re-read of a pull
request or issue, and no trigger call by hand. The fact sheets are the
whole record you decide from; the scripts read everything else.launch-result.json, the listing's files or the fact
sheets. You write and delete only decision files, and write a failed
result where the situations above say to.--until time you give it, and reports what it
did not reach as skipped, with that reason. The run can be stopped at any
moment after TESSL_LAUNCH_DEADLINE, and is never stopped before it.The deadline. TESSL_LAUNCH_DEADLINE holds the time, in ISO 8601, after
which the platform can stop the run. The end times come from it:
These print them, the listing's just before you start it and the trigger script's at any time:
node -e 'const d=Date.parse(process.env.TESSL_LAUNCH_DEADLINE),m=60000;console.log(new Date(Math.min(Date.now()+45*m,d-45*m)).toISOString())'
node -e 'console.log(new Date(Date.parse(process.env.TESSL_LAUNCH_DEADLINE)-5*60000).toISOString())'A listing end time already past leaves the listing nothing to do: it exits 2, and that is situation 5.
The listing reads its inputs from .cloud-launch/inputs.json, and the
pushed branch and repository id from the github:push event in
.cloud-launch/trigger-events.json; a run started by hand sweeps the default
branch. It lists the open pull requests against the branch, keeps the
factory's (author, branch prefix, labels, no fork), and lists again every 30
seconds while any of them has no known mergeability, until shortly before its
end time. It then reads each issue a conflicting one names: from Linear,
every identifier-shaped token in its branch, title and description (a branch
whose suffix is issue-<number> gives none), and from
GitHub, in the repository you are cloned from, the number in a branch suffix
such as issue-12 and every #<number> on a description line that begins
with Closes. It reads Linear from $LINEAR_API_URL/graphql with the run's
LINEAR_TOKEN, and a run without either of them reads nothing from Linear. It
prints one summary line and writes:
listing.json: the counts, the base branch, whether this is a dry run, the
fixer action, and the numbers of the pull requests that got a fact sheet;listing-skipped.json: every other open pull request, with the reason;facts/<number>.json: one fact sheet per conflicting factory pull request.A fact sheet holds the pull request's number, url, headSha,
branch, its branchSuffix after the factory prefix, its title, and
descriptionLines, the description's lines that say Closes or carry an
identifier-shaped token (long ones cut, and the middle dropped past 40 lines,
counted in descriptionLinesOmitted; a line that says Closes is always kept
whole). issues maps what was read to its facts:
ABC-45, to tracker: "linear",
state (its name), stateType (such as started, completed or
canceled), delegateId (the id of its delegate, null for none),
stateEnteredAt (when the issue entered its
current state) and hoursInState as of collectedAt;#12, to tracker: "github", state (OPEN or
CLOSED) and its labels;unknown: true when there is no such issue (a GitHub number that is a
pull request is not an issue), or unreadable with the failed read.The rest come from the factory's settings: tesslAppLinearUserId, the
Tessl App's Linear user id, null when the factory gave none;
linearActiveStates, the Linear
states a woken issue must be in, null for any state; ageLimitHours, the
age limit in hours for each Linear state it names, none for a state it does
not name; githubIssueLabel, the label a GitHub issue must carry; and
issueLabelFromDescription, the label the description's
<!-- tessl-factory:issue-label: <label> --> line names, null for none.
A decision is a file <number>.json in the decisions directory the
listing printed, one per fact sheet, written with an editor or a
here-document:
{
"number": 123,
"issue": "ABC-45",
"issueSource": "branch",
"issueSourceText": "tessl/abc-45",
"decision": "trigger",
"reason": "ABC-45 is delegated to Tessl and is In Review, a started state"
}issue is the identifier upper-cased as a key of the fact sheet's issues,
or null when there is none, and then issueSource and issueSourceText are
null too. issueSource is branch or description; issueSourceText is
copied exactly from the fact sheet: the branch, or the part of a description
line that names the issue. decision is trigger or skip.
The trigger script checks every decision's shape before any call, then,
unless the listing was a dry run, makes one tessl action trigger request for
the fixer per trigger decision, passing the number as GITHUB_PR_NUMBER,
under the id merge-conflict:<number>@<head sha> and linking the pull
request's url as references, retrying a failed
call with backoff for 30 minutes from its first attempt. The trigger route
records an id once, so alreadyRecorded: true means a round was already
requested for that head, and that is a success. Each request holds the key
github:pr:<number> at limit 1, which shepherd's rounds on that pull
request also hold as setup declares it, so its round waits behind them. It
writes launch-result.json from the listing, the fact sheets, your decisions
and the trigger outcomes, and prints one summary line: succeeded when every
call succeeded, failed naming each pull request whose call failed
otherwise.
Running a script. Each can run until close to its end time after a disruptive push. Run it as a long command:
exec_command does), do not use &, nohup or ( … ) &. Start the script
in a persistent session (a tty session you poll with write_stdin).timeout 230 bash -c 'until grep -q EXIT= /tmp/x.log; do sleep 3; done'.
Then wait again while it is still running.The issue comes from the branch and the description, applied to the fact sheet:
branchSuffix is
issue-<number>: issue-12 names #12, never a Linear ISSUE-12. It
names a Linear issue when the whole branchSuffix is a Linear identifier,
such as abc-45 or XYZ-123. fix-abc-45 or abc-45-retry names
nothing.Closes, whatever its
markup: Closes ABC-45, Closes [ABC-45](…), - closes abc-45,
Closes #12. An identifier or number mentioned anywhere else, including
later in a line that does not begin with Closes, is not the reference.
When the Closes lines name more than one issue, skip, naming them.#<number>, the keys of issues.Record the branch as the source when the branch names the issue, and the description otherwise. The trigger script refuses a branch source whose suffix is not the issue, and quoted text that does not contain it.
Eligible means, from the issue's entry in issues, that it is known and
readable, and:
delegateId
equals tesslAppLinearUserId. When tesslAppLinearUserId is null, no
Linear issue is delegated to Tessl, even one whose delegateId is null.
Its state is one of
linearActiveStates, or, when that is null, its stateType is known and
is neither completed nor canceled; and its hoursInState is under the limit
ageLimitHours gives its state, when it gives one. A missing
stateEnteredAt for a state with a limit is not eligible.state is OPEN, and its labels include the
issue label, which is issueLabelFromDescription when that is set and
githubIssueLabel otherwise.Anything else is a skip, and the reason names the fact that ruled it out:
ABC-45 is in Backlog, ABC-45 is not delegated to Tessl, ABC-45 has been in In Review for 52.0 hours, over 24, #12 is closed, #12 does not carry the issue label tessl (naming the label in force).
Decide each pull request from its own fact sheet alone, one at a time. Read each fact sheet whole before you write its decision.