CtrlK
BlogDocsLog inGet started
Tessl Logo
PluginPublic

tessl-labs/factory

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.

Quality

88%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

sweep-common.mjsskills/merge-conflict-sweep/scripts/

// 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)),
};

README.md

tile.json