CtrlK
BlogDocsLog inGet started
Tessl Logo
PluginPublic

tessl/factory-setup

Set up, understand, and change a Tessl factory on a repository: pick automations that fit how you work, such as shipping tickets as pull requests, answering reviews and fixing checks on those pull requests, code review, and scheduled jobs; prepare the repository for agents; write issues for the factory to build; then see what the factory has done, find out why a run failed or never started, and change or extend it.

Quality

91%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Medium

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

context.mjsskills/prepare-repo/scripts/sources/

// Agent context inventory: the files agents read in this repository, found
// among the files git tracks so ignored and generated trees stay out.

import { readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
import { isAbsolute, join, relative } from 'node:path';

import { failed, run } from '../lib/run.mjs';

const SKIP_DIRS = new Set([
  'node_modules',
  '.git',
  'dist',
  'build',
  '.next',
  '.turbo',
  '.venv',
]);

/**
 * The kind of agent context a repository-relative path is, or null. Skills
 * and rules that `tessl install` vendored (any `tessl__` segment, or under
 * `.tessl/`) belong to their plugin, not to the repository, so they are null.
 */
export function contextKind(rel) {
  const segs = rel.split('/');
  if (segs[0] === '.tessl' || segs.some((s) => s.startsWith('tessl__')))
    return null;
  const base = segs.at(-1);
  const has = (s) => segs.includes(s);
  if (base === 'AGENTS.md' || base === 'CLAUDE.md' || base === 'GEMINI.md')
    return 'agent-instructions';
  if (base === '.cursorrules') return 'rules';
  if (has('.cursor') && has('rules') && /\.mdc?$/.test(base)) return 'rules';
  if (has('.github') && base === 'copilot-instructions.md') return 'rules';
  if (has('.github') && has('instructions') && base.endsWith('.md'))
    return 'rules';
  if (has('.claude') && has('rules') && base.endsWith('.md')) return 'rules';
  if (base === 'SKILL.md') return 'skill';
  if (has('.claude') && has('commands') && base.endsWith('.md'))
    return 'command';
  if (has('.claude') && has('agents') && base.endsWith('.md'))
    return 'subagent';
  if (/^CONTRIBUTING(\.md)?$/i.test(base)) return 'contributing';
  if (base === '.mcp.json') return 'mcp';
  if ((has('.cursor') || has('.vscode')) && base === 'mcp.json') return 'mcp';
  if (base === 'tessl.json') return 'tessl-manifest';
  if (has('.claude') && /^settings(\.local)?\.json$/.test(base))
    return 'agent-settings';
  return null;
}

function walk(root, dir, out) {
  let entries;
  try {
    entries = readdirSync(dir, { withFileTypes: true });
  } catch {
    return;
  }
  for (const e of entries) {
    if (SKIP_DIRS.has(e.name)) continue;
    const full = join(dir, e.name);
    if (e.isDirectory()) walk(root, full, out);
    else if (e.isFile()) out.push(relative(root, full));
  }
}

async function candidatePaths(root) {
  const listed = await run('git', ['ls-files', '-z'], { cwd: root });
  if (listed.ok) return listed.stdout.split('\0').filter(Boolean);
  const out = [];
  walk(root, root, out);
  return out;
}

/**
 * Where `rel` really lands, or why it is not read. A tracked symlink or a `..`
 * path can point outside the repository, and the repository's content must not
 * pull the caller's other files into evidence.json.
 */
function resolveInside(realRoot, rel) {
  const real = realpathSync(join(realRoot, rel));
  const inside = relative(realRoot, real);
  if (inside === '' || inside.startsWith('..') || isAbsolute(inside))
    return { reason: 'outside root' };
  // A symlink to a FIFO would block the read forever.
  if (!statSync(real).isFile()) return { reason: 'not a regular file' };
  return { real };
}

function describe(realRoot, rel, kind) {
  let text = '';
  try {
    const target = resolveInside(realRoot, rel);
    if (!target.real)
      return { path: rel, kind, readable: false, reason: target.reason };
    text = readFileSync(target.real, 'utf8');
  } catch {
    return { path: rel, kind, readable: false };
  }
  const entry = {
    path: rel,
    kind,
    lines: text.split('\n').length,
    bytes: Buffer.byteLength(text),
  };
  if (kind === 'skill') {
    const name = /^name:\s*(.+)$/m.exec(text);
    if (name) entry.name = name[1].trim();
  }
  if (kind === 'mcp') {
    try {
      const parsed = JSON.parse(text);
      const servers = parsed.mcpServers ?? parsed.servers ?? {};
      entry.servers = Object.keys(servers);
    } catch {
      entry.servers = null;
    }
  }
  if (kind === 'tessl-manifest') {
    try {
      entry.dependencies = Object.keys(
        JSON.parse(text).dependencies ?? {},
      ).length;
    } catch {
      entry.dependencies = null;
    }
  }
  return entry;
}

/** Inventory of agent context files, bounded to `maxFiles` entries. */
export async function collectContext(ctx) {
  try {
    const realRoot = realpathSync(ctx.root);
    const paths = await candidatePaths(ctx.root);
    const matches = paths
      .map((rel) => ({ rel, kind: contextKind(rel) }))
      .filter((m) => m.kind)
      .sort(
        (a, b) =>
          a.rel.split('/').length - b.rel.split('/').length ||
          a.rel.localeCompare(b.rel),
      );
    const counts = {};
    for (const m of matches) counts[m.kind] = (counts[m.kind] ?? 0) + 1;
    return {
      status: 'ok',
      counts,
      truncated: matches.length > ctx.limits.maxFiles,
      files: matches
        .slice(0, ctx.limits.maxFiles)
        .map((m) => describe(realRoot, m.rel, m.kind)),
    };
  } catch (error) {
    return failed(`could not list files: ${error.message}`);
  }
}

README.md

tile.json