A small script implementing a tiny software factory A developer has created a small script that acts as a software factory, automatically finding Linear issues labeled 'factory' and using Claude and Polygraph to classify and execute them end-to-end. The script queries Linear for Todo issues, asks Claude whether an agent can handle each, and then launches a detached Polygraph session to do the work, updating the issue status accordingly. | /usr/bin/env bun | | | // The factory trigger: finds workable Linear issues and hands each one to a | | | // detached Polygraph session that does the work end to end. | | | // | | | // FIND WORK here query Linear for Todo issues project + label | | | // CLASSIFY here claude -p: can an agent run this issue? | | | // DO THE WORK Polygraph one detached session, end to end | | | // COMPLETE here attach the session to the issue, move to In Progress | | | import { mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs"; | | | import { homedir } from "node:os"; | | | import { join } from "node:path"; | | | const LINEAR API URL = "https://api.linear.app/graphql"; | | | const LOGS DIR = join import.meta.dir, "logs" ; | | | const PROJECT NAME = process.env.LINEAR PROJECT NAME; | | | const LABEL NAME = "factory"; | | | const START TIMEOUT MS = 30 60 000; | | | const apiKey = process.env.LINEAR API KEY; | | | if apiKey { | | | console.error "LINEAR API KEY is not set." ; | | | console.error "Create a personal API key at https://linear.app/settings/api and export it:" ; | | | console.error " set -x LINEAR API KEY lin api ... fish" ; | | | process.exit 1 ; | | | } | | | if PROJECT NAME { | | | console.error "LINEAR PROJECT NAME is not set." ; | | | console.error 'Add it to .env, e.g.: LINEAR PROJECT NAME="My Project"' ; | | | process.exit 1 ; | | | } | | | // --------------------------------------------------------------------------- | | | // The factory | | | // --------------------------------------------------------------------------- | | | async function main { | | | const runStartedAt = Date.now ; | | | if process.argv.includes "--scheduled" { | | | if shouldSkipScheduledRun { | | | log "scheduled run: already ran today — exiting max one scheduled run per day " ; | | | return; | | | } | | | log "scheduled run: first invocation today" ; | | | } | | | log factory starting — project "${PROJECT NAME}", label "${LABEL NAME}", state Todo ; | | | const viewer = await findWork ; | | | const issues = viewer.assignedIssues.nodes; | | | log ${issues.length} issue s to process for ${viewer.name} ; | | | for const issue of issues { | | | log - ${issue.identifier} ${issue.title} ; | | | } | | | if issues.length === 0 { | | | log nothing to do — run finished in ${elapsed runStartedAt } ; | | | return; | | | } | | | log "loading account info ..." ; | | | const account = await whoami ; | | | log account: ${account.selectedOrganization?.name} ${account.selectedOrgId} ; | | | const counts = { started: 0, skipped: 0, error: 0 }; | | | for const issue of issues { | | | log processing ${issue.identifier} ${issue.title} ; | | | try { | | | const outcome = await processIssue issue, account ; | | | counts outcome ++; | | | } catch error { | | | log error: ${error.message} — continuing with next issue ; | | | counts.error++; | | | } | | | } | | | log | | | run finished in ${elapsed runStartedAt } — ${counts.started} started, ${counts.skipped} skipped, ${counts.error} error s , | | | ; | | | } | | | // Returns "started", "skipped", or "error". | | | async function processIssue issue, account { | | | const classification = await canRunByAgent issue ; | | | if classification.verdict == "yes" { | | | log skipped: ${classification.reason} ; | | | return "skipped"; | | | } | | | log "verdict: yes" ; | | | // The session agent does the rest: repos, implementation, review, PRs. | | | const session = await launchSession issue, account ; | | | if session { | | | log "warning: could not determine session id — leaving issue in Todo so the next run retries" ; | | | return "error"; | | | } | | | await complete issue, session ; | | | log ${issue.identifier} done: session running, Linear updated ; | | | return "started"; | | | } | | | // --------------------------------------------------------------------------- | | | // The steps | | | // --------------------------------------------------------------------------- | | | // Todo issues assigned to the current user, filtered by project and label. | | | async function findWork { | | | const data = await linearQuery | | | | | | query MyTodoIssues $project: String , $label: String { | | | viewer { | | | name | | | assignedIssues | | | first: 50 | | | filter: { | | | state: { name: { eq: "Todo" } } | | | project: { name: { eq: $project } } | | | labels: { name: { eq: $label } } | | | } | | | { | | | nodes { | | | id | | | identifier | | | title | | | description | | | url | | | team { | | | id | | | } | | | state { | | | name | | | type | | | } | | | } | | | } | | | } | | | } | | | , | | | { project: PROJECT NAME, label: LABEL NAME }, | | | ; | | | return data.viewer; | | | } | | | // Ask Claude Code headless whether an agent can solve the issue | | | // autonomously. Returns { verdict: "yes" } or { verdict: "skip", reason }. | | | async function canRunByAgent issue { | | | const prompt = You are screening Linear issues to decide whether a coding agent can pick them up and solve them autonomously. | | | Issue ${issue.identifier}: ${issue.title} | | | Description: | | | ${issue.description?.trim || " no description provided "} | | | Answer "yes" only if all of these hold: | | | 1. Actionable: it describes a concrete task or change, not a vague idea or open discussion. | | | 2. Somewhat straightforward: it does not require major open-ended design or product decisions. | | | 3. Enough context: the description gives enough detail what to do, where, or how to tell it's done to start without asking clarifying questions. | | | Respond with ONLY a single line of JSON, no markdown, in one of these forms: | | | {"verdict":"yes"} | | | {"verdict":"skip","reason":"