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
// What the sweep's two scripts share: their bounds, the end time the agent
// gives each, and running one command to its timeout.
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const SECOND = 1_000;
const MINUTE = 60 * SECOND;
/**
* Every wait and retry the scripts make, in one place. No call starts unless
* it would end, at its timeout, inside the bound of its phase and the script's
* end time, so a call still running counts against both.
*/
export const BOUNDS = {
// Each `gh`, Linear and `tessl` call; a call that runs longer has failed.
callTimeoutMs: 2 * MINUTE,
// While a factory PR has no known mergeability, list the open PRs this often.
mergeablePollMs: 30 * SECOND,
// The listing stops waiting on mergeability this long before its end time,
// so the issue reads that follow the wait still have time to run.
issueReadsMs: 5 * MINUTE,
// A failed read: the repository, the PR list, a Linear request. Long
// enough that a read timing out at 2 minutes gets several attempts.
readRetry: {
firstDelayMs: 5 * SECOND,
maxDelayMs: MINUTE,
forMs: 10 * MINUTE,
},
// Each PR's failed trigger calls, for this long from its first attempt; the
// request id makes a repeat harmless.
triggerRetry: {
firstDelayMs: 15 * SECOND,
maxDelayMs: 5 * MINUTE,
forMs: 30 * MINUTE,
},
};
/** Why work a script could not start before its end time is skipped. */
export const NOT_REACHED_REASON =
"not reached before the script's end time; the next sweep takes it up";
/** Where the scripts keep the sweep's files. */
export const DEFAULT_DIR = join(tmpdir(), 'merge-conflict-sweep');
/** The `--until` end time as milliseconds, or an error naming what is wrong with it. */
export function parseUntil(value) {
if (!value) throw new Error('--until is required: the time to end by');
const at = Date.parse(value);
if (Number.isNaN(at) || !/^\d{4}-\d\d-\d\dT/.test(value))
throw new Error(`--until is not an ISO 8601 time: ${value}`);
return at;
}
/**
* Runs a command to the end or for `timeoutMs`, whichever comes first, and
* reports its output or why it failed, with the output of a command that
* exited non-zero, which can still carry an answer. A call that times out has failed, and
* is killed outright so a command that ignores SIGTERM cannot outlive it.
*/
export function runCommand(
command,
args,
{ timeoutMs = BOUNDS.callTimeoutMs } = {},
) {
const result = spawnSync(command, args, {
encoding: 'utf8',
maxBuffer: 256 * 1024 * 1024,
timeout: timeoutMs,
killSignal: 'SIGKILL',
});
if (result.error?.code === 'ETIMEDOUT')
return {
ok: false,
reason: `${command} timed out after ${timeoutMs / SECOND}s`,
};
if (result.error)
return {
ok: false,
reason: `${command} could not run: ${result.error.message}`,
};
if (result.status !== 0) {
const detail =
(result.stderr || result.stdout || '').trim().split('\n').at(-1) ?? '';
return {
ok: false,
reason: `${command} exited ${result.status}${detail ? `: ${detail}` : ''}`,
stdout: result.stdout,
};
}
return { ok: true, stdout: result.stdout };
}
export function parseJson(text) {
try {
return JSON.parse(text);
} catch {
return null;
}
}
export function readJson(path) {
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : null;
}
export function writeJson(path, value) {
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
}
/** The value after `name` on the command line, or undefined. */
export function flagValue(name, argv = process.argv) {
const index = argv.indexOf(name);
return index === -1 ? undefined : argv[index + 1];
}
/**
* The bounds to run with: `BOUNDS`, with any field the
* `MERGE_CONFLICT_SWEEP_BOUNDS` JSON names replaced, so a test can run a
* script in milliseconds. Every field must be a positive finite number.
*/
export function boundsFrom(text) {
if (!text) return BOUNDS;
const override = parseJson(text);
if (override === null || typeof override !== 'object')
throw new Error('MERGE_CONFLICT_SWEEP_BOUNDS is not a JSON object');
const merge = (defaults, values, path) => {
const merged = { ...defaults };
for (const [name, value] of Object.entries(values)) {
const field = `${path}${name}`;
if (!(name in defaults))
throw new Error(`MERGE_CONFLICT_SWEEP_BOUNDS has no field ${field}`);
if (typeof defaults[name] === 'object') {
if (value === null || typeof value !== 'object')
throw new Error(
`MERGE_CONFLICT_SWEEP_BOUNDS ${field} is not an object`,
);
merged[name] = merge(defaults[name], value, `${field}.`);
} else if (
typeof value !== 'number' ||
!Number.isFinite(value) ||
value <= 0
) {
throw new Error(
`MERGE_CONFLICT_SWEEP_BOUNDS ${field} is not a positive number`,
);
} else {
merged[name] = value;
}
}
return merged;
};
return merge(BOUNDS, override, '');
}
export const realClock = {
now: () => Date.now(),
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
};