The Software Factory — 22 deterministic-first SDLC agents & skills for pi, Claude Code, and Antigravity
Overview
A schedulable fleet of software-development-lifecycle (SDLC) agents that run under your existing developer session auth (antigravity, claude, pi) and on GitHub Actions. Deterministic scanners enumerate; AI models triage, synthesize, and judge. Findings flow directly into whatever issue tracker your target repository already uses (beads or local reports; public GitHub issues are triaged separately as public input). 1. : Scanners (gitleaks, npm audit, HTMLParser, custom harnesses) enumerate candidate files and metrics. Models never do work a static tool does faster and cheaper. 2. : The same portable agent definition (SKILL.md) runs locally with your subscription session auth and in CI with repository API keys. 3. : Discovery and verification are separate agents. Discovery optimizes for recall; verification runs in a fresh session prompted strictly to disprove the finding. 4. : Agents open PRs, draft wisps/issues, and generate delta reports. Humans retain the merge control point. 5.
README
The Software Factory
A schedulable fleet of software-development-lifecycle (SDLC) agents that run locally under your existing developer session auth (antigravity, claude, pi) and in CI on GitHub Actions.
Deterministic scanners enumerate; AI models triage, synthesize, and judge. Findings flow directly into whatever issue tracker your target repository already uses (beads or local reports; public GitHub issues are triaged separately as public input).
How It Works
flowchart TD
subgraph Triggers["Triggers"]
CLI["CLI (manual)"]
SCHED["launchd (schedule)"]
GHA["GitHub Actions (CI)"]
end
subgraph Factory["The Software Factory"]
DISP["factory dispatcher"]
PRE["Deterministic Pre-pass(scan.py, mine_history.py, etc.)"]
ENG["Engine Adapter(pi · claude · antigravity)"]
STORE["Findings Store & State Machine(fingerprint · dedupe · lifecycle)"]
end
subgraph Targets["Target Codebase"]
TGT["Target Repo (/path/to/project)"]
TM["THREAT_MODEL.md"]
end
subgraph Sinks["Pluggable Sinks"]
BD["Beads (.beads)"]
GH["GitHub Issues"]
FILE["Local Delta Reports"]
end
CLI --> DISP
SCHED --> DISP
GHA --> DISP
DISP -->|"1. Enumerate"| PRE
PRE -->|"Inspect"| TGT
PRE -->|"Read scope"| TM
PRE -->|"Candidates JSON"| DISP
DISP -->|"2. Triage & Judge"| ENG
ENG -->|"Execute SKILL.md"| TGT
ENG -->|"Structured Findings"| DISP
DISP -->|"3. Dedupe & State Transition"| STORE
STORE -->|"Active Beads"| BD
STORE -->|"Verified public issues (all real severities)"| GH
STORE -->|"Markdown Delta"| FILE
Core Principles
- Deterministic-first: Scanners (
gitleaks,npm audit,HTMLParser, custom harnesses) enumerate candidate files and metrics. Models never do work a static tool does faster and cheaper. - Two-plane architecture: The same portable agent definition (
SKILL.md) runs locally with your subscription session auth and in CI with repository API keys. - Clean session isolation: Discovery and verification are separate agents. Discovery optimizes for recall; verification runs in a fresh session prompted strictly to disprove the finding.
- Propose, don’t apply: Agents open PRs, draft wisps/issues, and generate delta reports. Humans retain the merge control point.
- Noise control & stable fingerprinting: Findings are keyed by
sha256(agent:rule_id:normalized_path:normalized_snippet)—deliberately excluding line numbers so refactors don’t trigger duplicate alerts. - Public disclosure guard: An explicitly-public target publishes redacted public issues for every real finding, including high/critical; missing visibility fails closed. Human approval is required before creating a linked bead.
Quick Start
Prerequisites
- Python 3.9+
- At least one supported AI CLI installed and authenticated:
pi(Google / Gemini / Anthropic session)claude(Claude Code CLI)antigravity(Headless session API)
- Optional tools depending on target sink:
0. Agent Integration Guidance (--agent / integrate)
AI coding agents (or developers) can print machine-actionable integration instructions and copy-pasteable workflow recipes directly from the CLI:
# Print full machine-actionable integration guide for AI agents
./factory --agent
# Or print specific integration recipes directly to stdout
./factory integrate --section github-actions # GitHub Actions workflows & composite action
./factory integrate --section pre-commit # Sub-150ms deterministic pre-commit gate
./factory integrate --section target # targets/.yaml configuration
1. List Available Agents
./factory list
2. Run an Agent Against a Target
You can run any agent against a registered target name (from targets/) or against an arbitrary local directory path:
# Run secret-scan against a registered target
./factory run secret-scan --target fauxmium
# Run against an arbitrary local repository path
./factory run secret-scan --target /path/to/my-web-app
# Choose a specific AI engine (auto-detects by default)
./factory run threat-model --target fauxmium --engine pi
# Override the issue sink
./factory run vuln-discovery --target fauxmium --sink file
3. Run a Factory Line (Composition & Andon Cord)
A Factory Line orchestrates multiple agents in sequence across the SDLC. The Andon Cord immediately halts the line — skipping every downstream station — when a station hits a genuine failure (its engine or pre-pass could not run) or a critical defect (e.g. an exposed secret). A station whose engine ran but produced no usable verdict (unparseable or schema-violating output) is a plumbing failure, not a genuine one: it gets one bounded repair-retry, then degrades to ERROR and the line continues as INCOMPLETE rather than halting.
Adapter Auth & Environment Failures (ENV_FAILURE)
When an engine adapter fails due to missing, invalid, or inaccessible credentials (e.g. pi exit 1 with "No API key found", claude exit 1 with "no Claude credentials", or missing DEEPSEEK_API_KEY):
- It is classified and reported as a named
ENV_FAILURE(Environment Failure), distinct from a code defect or absent verdict. - Never rendered as clean or PASS: findings count is marked
UNKNOWN(—), not zero. A reader can immediately see that the station could not authenticate and did not run. - Single-station runs:
factory runprints[environment failure]to stderr, writes anENVIRONMENT FAILUREdelta report trio, and exits 2. - Factory Line behaviour:
- If
andon_halt_on_failure: true(e.g.project-audit): the line halts immediately, pulls the Andon cord, skips downstream stations, and finishes in stateHALTED(exit 1). - If
andon_halt_on_failure: false(e.g.web-excellence): the failed station is recorded on the scorecard asENV_FAILURE(-findings,-criticals), downstream stations continue running, and the overall line finishes in stateINCOMPLETE(exit 1). The delta report displays a prominentENVIRONMENT FAILUREcallout and never produces a “Clean Delta” banner.
- If
# Run the full SDLC project audit line against a target
./factory line project-audit --target fauxmium
# Run the Web Platform, UI/UX, Resilience, Perf & Memory excellence line
./factory line web-excellence --target fauxmium
3b. Run the Class C Performance Hill-Climber (./factory hillclimb)
The hillclimb command runs an iterative optimization loop (lib/bench/runner.py) toward a concrete numeric goal (perf_hazard_score, total_gzip_bytes, or custom_bench_ms), recording every kept win and reverted dead-end in findings/-hillclimb-ledger.jsonl:
# Propose hill-climb steps toward 0 performance hazards
./factory hillclimb --target fauxmium --metric perf_hazard_score --goal 0
# Run 3 iterations applying edits in-worktree, re-measuring, keeping wins & reverting regressions
./factory hillclimb --target fauxmium --metric total_gzip_bytes --iterations 3 --apply
3c. Wire Up Your Daily Workflow (3 Layers)
You can wire the factory into your daily development loop in one command per layer:
# Layer 1: Sub-second (<150ms) deterministic secret-scan git pre-commit hook (Rule 8 compliant)
./factory hook install --all
# Layer 2: Symlink all 22 agent SKILL.md manifests into ~/.pi/agent/skills, ~/.claude/skills, and ~/.gemini
./factory skills install
# Layer 3: Install macOS launchd daily morning briefing schedules (07:30 / 07:35 / 07:40 AM)
./factory schedule generate && ./factory schedule install --all
Installing the Skills on Another Machine (install.sh / pi / npx skills)
You can install all 22 skills (plus the factory CLI in ~/.local/bin/factory) onto any machine in a single command:
# Option 1: Zero-dependency universal installer (installs into pi, Claude, Antigravity & ~/.local/bin/factory)
curl -fsSL https://raw.githubusercontent.com/PaulKinlan/agents/main/install.sh | bash
# Or install a single skill only:
curl -fsSL https://raw.githubusercontent.com/PaulKinlan/agents/main/install.sh | bash -s -- --skill modern-web
# Option 2: Native pi package manager (auto-registers in ~/.pi/agent/settings.json)
pi install git:github.com/PaulKinlan/agents
# Option 3: skills.sh CLI (pass -a to target pi/claude-code; omitting -a with -y tries PromptScript which lacks -g support)
npx skills add PaulKinlan/agents -g -a pi claude-code -y
# Or install a single station via skills.sh:
npx skills add PaulKinlan/agents@modern-web -g -a pi -y
4. Run in CI (GitHub Actions Composite Action)
Any repository can invoke Factory agents directly in GitHub Actions using the reusable composite action:
# .github/workflows/security.yml in consumer repositories
name: Security Audit
on: [push, pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Pin the action itself too: `@main` moves under the consumer, and this action runs with
# your model key and GitHub token.
- uses: paulkinlan/agents/.github/actions/factory@35ae9c5a428fbcab2c5bf3e1c2ced586db844f6c
with:
agent: secret-scan
target: .
model_api_key: ${{ secrets.GEMINI_API_KEY }}
For complete workflow recipes (PR quality gates, scheduled deep audits, automated issue triage, and matrix runners), see docs/INTEGRATION.md.
Setting Up Targets
Targets represent repositories the factory audits. Targets are defined as simple YAML files in targets/.yaml:
# targets/my-service.yaml
name: my-service
path: /Users/username/Code/my-service
sink: file # Sink: file | beads
visibility: public # Visibility: public | private (guards disclosure)
trusted: false # agents-bp0: `true` (+ visibility: private) is the attestation
# FACTORY_ALLOW_UNSANDBOXED=1 requires on a host without
# bubblewrap; never set it for a public or untrusted repo
agents:
- secret-scan
- threat-model
- vuln-discovery
- vuln-verify
- modern-web
- ui-ux-audit
- resilience
- perf-review
- perf-hillclimb
- memory-profile
- bundle-size
schedule:
secret-scan:
interval: 86400 # Run daily (in seconds)
deps-supply-chain:
interval: 604800 # Run weekly
Target Fields
| Field | Description | Values |
|---|---|---|
name |
Unique project identifier used in findings and logs | String (e.g. my-service) |
path |
Absolute path to the repository on your machine | /path/to/repo |
sink |
Findings destination | file (default) · beads; --sink github-issues is rejected for findings |
visibility |
Explicit publication policy; missing/invalid withholds high/critical from beads | public · private (beads sink is private/owner-visible; missing/invalid withholds high/critical) |
repo |
Explicit public GitHub issue destination (never inferred from origin) |
OWNER/REPO on github.com |
beads_path |
Explicit initialized project Beads DB for approved promotion | /path/to/project (contains .beads/) |
agents |
List of agents enabled for this target | Array of agent names |
schedule |
Optional schedule overrides per agent (seconds or clock time) | interval: 86400 or hour: 3, minute: 0 |
[!TIP] Ad-hoc targets: You don’t have to create a YAML file to audit a project. Passing
./factory run --target /path/to/repoautomatically resolves the target name from the directory and selectsfilesink as a safe default. A raw path carries no manifest, so an ad-hoc run that files tobeadsalso needs--visibility public|private: without it the fail-closed embargo holds every high/critical finding locally. A registered target’s ownvisibilityis authoritative and is never overridden by the flag. The canonical version of this, including the two declarations, the raw-path sink default and the log line a withheld finding prints, lives in targets/README.md — edit there first.
The Agent Fleet
The factory includes 22 implemented agents across 7 SDLC lanes:
1. Security & Vulnerability Lane
| Agent | Class | Plane | Description |
|---|---|---|---|
threat-model |
Observer (A) | Local | Mines git history, commit diffs, and issue records to generate a comprehensive THREAT_MODEL.md (trust boundaries, attack surfaces, bug-shape hints, and explicit non-threats). |
secret-scan |
Observer (A) | Both | Deterministic regex/gitleaks scan + model triage for high-entropy tokens, API keys, private keys, and cloud credentials. |
vuln-discovery |
Observer (A) | Local | Semantic attack-surface scanner guided by THREAT_MODEL.md. Identifies candidate vulnerabilities and models multi-hop exploit chains with high recall. |
vuln-verify |
Observer (A) | Local | Adversarial verifier with zero shared context. Prompted strictly to disprove findings by hunting for upstream sanitizers, auth gates, and framework protections. |
vuln-triage |
Observer (A) | Both | Deduplicates findings by root cause rather than symptom, evaluates reachability against THREAT_MODEL.md, and prepares findings for tracker promotion. |
2. Modern Web, UI/UX, Accessibility & Resilience Lane
| Agent | Class | Plane | Description |
|---|---|---|---|
modern-web |
Proposer (B) | Both | Audits HTML, CSS, and JS against modern-web-guidance to replace legacy JS/CSS workarounds with native Baseline Web APIs (``, Popover, Anchor Positioning, Container Queries, :has(), :user-valid, View Transitions, Scroll-Driven Animations). |
ui-ux-audit |
Proposer (B) | Both | Holistic UI & UX analyzer checking design token consistency, interactive states (:focus-visible, :active, :disabled), async loading/empty/error states, tap targets, dark mode (light-dark()), and typography. |
accessibility |
Observer (A) | Local | Scans HTML files and templates for 6 core WCAG 2.1 AA violation types (missing alt text, unlabelled buttons, missing lang attributes, tabindex > 0). |
resilience |
Proposer (B) | Both | Wraps web-resilience-audit and web-resilience-fix against 46 failure states (offline, AbortSignal.timeout(), storage quota errors, FOIT fonts, 3P script SPOFs, tab discard). |
3. Performance & Memory Optimizers Lane (Class B & C)
| Agent | Class | Plane | Description |
|---|---|---|---|
perf-review |
Proposer (B) | Both | Inspects recent git commits and diffs for performance regressions (layout thrashing, sequential await waterfalls, render-blocking head assets, LCP/CLS media hazards) and outputs ready-to-apply fix diffs. |
perf-hillclimb |
Optimizer © | Local | Goal-directed optimizer paired with lib/bench/runner.py. Iteratively measures metrics, proposes single-step optimizations, verifies improvements, and records reverted dead ends in an append-only ledger. |
memory-profile |
Optimizer © | Local | Wraps memory-leak-debugging + DevTools MCP. Detects unbounded Map/Set caches, uncleaned event listeners, setInterval leaks, and undisconnected DOM observers. |
bundle-size |
Optimizer © | CI/Local | Measures countable raw/gzipped asset bytes against an append-only baseline. Recommends dynamic import() opportunities and budget caps. |
4. Inner-Loop Testing & Automated Patching Lane
| Agent | Class | Plane | Description |
|---|---|---|---|
test-gap |
Proposer (B) | Local | Measures test coverage deficit across source modules and synthesizes executable unit test skeletons using standard runners (node:test, pytest). |
pr-fixer |
Proposer (B) | Both | Synthesizes minimal, verified unified diff patches (proposed_patches) and branch proposals for active findings or failing CI checks without pushing to default branches. |
5. CI, Supply Chain & Repo Triage Lane
| Agent | Class | Plane | Description |
|---|---|---|---|
issue-triage |
Proposer (B) | CI/Local | Interfaces with GitHub Issues (gh) or Beads (.beads/). Verifies reproduction steps, flags duplicates, calculates severity, and proposes labels and maintainer triage comments. |
deps-supply-chain |
Observer (A) | Both | Combines npm audit with static import analysis to verify if reported CVEs are actually reachable in runtime code, filtering out dormant devDependencies. |
6. Ops, Documentation & Release Lane
| Agent | Class | Plane | Description |
|---|---|---|---|
log-check |
Observer (A) | Both | Inspects application, test, and server error logs, extracts stack traces, correlates them to source files, identifies root causes, and proposes defensive fixes. |
docs-drift |
Observer (A) | Both | Parses markdown documentation and cross-references actual code symbols, directory layouts, and CLI flags to detect stale docs. |
docs-write |
Proposer (B) | Both | Companion to docs-drift that generates exact markdown replacement patches (proposed_markdown_patch) to synchronize documentation. |
release-notes |
Proposer (B) | CI/Local | Mines commit history and merged PRs since the last release, synthesizing customer-facing release notes grouped into Features, Fixes, and Breaking Changes. |
7. Factory Meta-Assurance Lane
| Agent | Class | Plane | Description |
|---|---|---|---|
qa-station |
Observer (A) | Both | Meta-agent auditing the precision (1 - wontfix_rate), duplicate clusters, remediation completeness, and anatomy contract compliance of all other agents in the fleet. |
Anatomy of an Agent
Every agent lives in agents// and adheres to a strict contract:
agents//
├── agent.yaml # Metadata: class, plane, containment, capabilities, budget
├── SKILL.md # Portable instructions (runs on pi, claude, and antigravity)
├── scripts/ # Deterministic pre-pass tool (Python / shell)
└── report.schema.json # Output contract enforced on the model's response
agent.yaml: Declares agent behavior:class:observer(read-only),proposer(opens PRs/issues),optimizer(measure-change-remeasure).containment:t0-readonly,t1-fetch(outbound network),t2-local(file modifications), ort3-sandbox(refused: no gVisor-class runner exists yet). The tier is a ceiling on thecapabilitiesan agent may declare, and every run checks it. The model session runs read-only, except that an agent declaringwriteatt2-localis granted a disposable per-session git worktree (agents-6ce) to edit files in — but only when the engine is OS-sandboxed (justpiunder a working bubblewrap;claudeis not kernel-confined, so awritedeclaration there always downgrades toread-only, review P1-2). The target checkout stays read-only and the collected session diff is the proposal. On Linux hosts with bubblewrap it also runs inside an OS sandbox that confines it to the target (lib/sandbox.py), and — when every model-provider key in play is brokerable — with the network namespace isolated (--unshare-net, agents-2x6): the only egress is the per-run egress-allowlist proxy allowlisted to the agent’s ownrequireshosts, and the model API calls go through the credential broker via an in-sandbox relay (lib/net_forward.py,lib/egress_proxy.py). A run that cannot isolate the netns honestly re-withholds a declarednetworkand reportsnetwork-egressas not enforced. On a host where bubblewrap cannot run, a sandbox-confined engine refuses instead of degrading —FACTORY_ALLOW_UNSANDBOXED=1opts out only for a target that atteststrusted: truewithvisibility: privatein its manifest (agents-bp0), and the banner/policy.jsonthen name the opt-in. See THREAT_MODEL.md §6.1 and §7.short_circuit_empty:trueif model invocation should be bypassed when the pre-pass finds zero candidates.
SKILL.md: Contains no engine-specific logic. Engine-specific flags belong inlib/adapters/.scripts/: Executable pre-pass script (scan.py,mine_history.py, etc.). Its stdout is the human/CI channel and is redacted by default: it carries only the keys a reader consumes (rule_id,path,line_number,severity), thecandidatescontainer, and small scalars such as counts — so a station whose payload is not acandidateslist prints counts only.--outputwrites the raw local record.report.schema.json: JSON Schema validating the model’s triage output.
Sinks & Noise Control
Sinks
When a run completes, findings are dispatched based on target configuration:
-
file(Default): Writes formatted markdown delta reports tofindings/-latest.mdand appends metrics tofindings/-history.jsonl. In afactory line, each station writesfindings/--delta.mdand the line writes the run’sfindings/-delta.md(plus machine-readablefindings/-line.json) once, from every station. A station that produced no verdict (pre-pass/engine failure, timeout, unparseable or schema-violating output) isERROR, the run isINCOMPLETE, never “Clean Delta”, and the command exits non-zero. -
Severity: reports, the store and the andon count the triaged severity (
unclassifiedwhen missing; triaged false positives are evidence, not public work).routing_severityremains fail-closed when visibility is undeclared. The beads sink files medium and above (low and info are skipped; missing/invalid visibility withholds high/critical); sensitive values are redacted from tracker prose. The sink reports published / skipped / duplicate / failed counts. -
beads: Findings dispatch to beads automatically (agents-eyo): internal findings file directly into the target’s configuredbeads_pathwithout a public GitHub issue and without a human triage gate. Medium and above severities file (low and info are skipped); a target without explicitvisibilitywithholds high/critical from the synced tracker (embargo_reason). Bead creation is deduped by fingerprint (external_ref: factory:), so re-runs do not duplicate open beads; a finding that was fixed and reappears (regressed) with its prior bead closed files a new bead. -
Public GitHub issues & Promotion: Public GitHub issues are exclusively for public input, not a findings sink;
--sink github-issuesis rejected for findings. Theissue-triagestation reads public issues, and after human triage applies thefactory-approvedlabel,./factory promote --target NAME --issue https://github.com/OWNER/REPO/issues/NUMBERcreates at most one linked bead using a repo-scoped fingerprint external ref (factory:github.com/OWNER/REPO:), links the issue URL on the bead, and comments the bead ID on the issue. Promotion must run from the same factory checkout whosefindings/.jsonproduced the issue; stores are not shared between worktrees. If the configured project Beads DB is missing or unreadable, promotion refuses instead of guessing a DB. -
command: For other trackers, setsink: commandandsink_command: ""in the target manifest, with optionalsink_timeoutandsink_env: [NAMES]. The command reads redacted, embargo-filtered JSON Lines (factory-sink/2), reports per-finding receipts, and writes redacted stderr privately in the run directory. Seelib/sinks/command.py.
Sink implementations use the registry and Sink/SinkContext interface in lib/sinks/; the local report remains the complete evidence trail.
QA precision needs lifecycle observations. qa-station aggregates valid records
across all readable findings/*.json stores in the same factory checkout that runs it;
these stores are gitignored, not shared between worktrees. --target validates and labels
the audit target; it does not select a single findings store. If there are no valid
lifecycle records across those stores (including when none exist or all are empty),
qa-no-findings-store marks a measurement gap, not evidence of a clean fleet. After the
owner-managed project-audit line has populated that checkout’s store, inspect
its per-agent volume, wontfix_rate and estimated_precision without a model call:
python3 agents/qa-station/scripts/audit_factory_quality.py --target . --output /tmp/qa-audit.json.
The estimates describe recorded lifecycle states, not human-validated precision;
new findings with no wontfix dispositions do not certify 100% precision.
Do not copy a private findings store into Git to make a QA scorecard appear.
Public Disclosure Policy
[!IMPORTANT] Findings file to the target’s Beads DB automatically without a public issue or human gate (agents-eyo). Public GitHub issues are exclusively for public-input triage (
issue-triage), never a direct findings sink;--sink github-issuesis rejected for findings. Missing or invalid targetvisibilitywithholds high/critical findings from the synced beads tracker (embargo_reason)..gitignorekeeps rawfindings/*andruns/out of Git; tracker prose is redacted, but reviewers must still consider the disclosure policy before opting a target in.
Noise Control & State Machine
Findings follow a deterministic lifecycle across runs:
$$\text{new} \longrightarrow \text{accepted} \mid \text{wontfix} \longrightarrow \text{fixed} \longrightarrow \text{regressed}$$
- Stable Fingerprint:
sha256(agent:rule_id:normalized_path:normalized_snippet). Line numbers are omitted so reformatting or refactoring does not re-alert on existing issues. - Delta Reports: Output reports only emphasize the delta (
X new, Y regressed, Z fixed). Clean runs produce zero alert noise. - Suppressions: Committed rationales in
findings/suppressions.yamltransition findings towontfix.
Local Scheduling (macOS launchd)
The factory integrates with macOS launchd User Agents to run recurring background audits using your local login session authentication.
# List all schedulable agents and current launchd status
./factory schedule list
# Generate a launchd plist for an agent
./factory schedule generate --target fauxmium --agent secret-scan
# Install and load into ~/Library/LaunchAgents/
./factory schedule install --target fauxmium --agent secret-scan
# Trigger an immediate background run via launchctl
./factory schedule trigger --target fauxmium --agent secret-scan
# Uninstall and unload
./factory schedule uninstall --target fauxmium --agent secret-scan
Scheduled jobs log their output to runs/schedule--.stdout.log and automatically update the target’s delta report.
Run Artifact Retention
Every factory run writes a run directory under runs/--/
holding the run record (policy.json), raw scanner matches (candidates.json,
prompt.txt) and model output (model_output.txt, rejected_report.json). Those
artifacts can embed matched secret values and raw model output, so the factory applies
a bounded retention policy whenever a new run directory is created:
- Count bound — keep the
FACTORY_RUN_RETENTIONmost-recent run directories (default20). This is the hard guarantee: however fast runs are produced, at most this many run directories survive. - Age bound (TTL) — prune any run directory older than
FACTORY_RUN_RETENTION_AGE_DAYSwhole days (default30), even if it is within the count bound.
# Keep 50 runs instead of 20, and expire anything older than 7 days
FACTORY_RUN_RETENTION=50 FACTORY_RUN_RETENTION_AGE_DAYS=7 ./factory run secret-scan --target voicebox
Pruning is explicit and safe: the run directory being written is never removed;
symlinks are never followed (and are left in place); non-directory files such as the
scheduler’s schedule-*.stdout.log and the hill-climb runs/hillclimb--/
proposal directories are never swept (they are proposals, not run records). If a run
crashes mid-way, its partial directory is still the newest entry and survives that pass;
it is pruned on a later run once it is old enough or falls past the count bound.
Every removal under runs/ is tombstoned, at file granularity, beside the runs root.
All removals of citation-bearing directories — the prune’s, and the failed --apply
cleanup that deletes runs/hillclimb--/ when the disposable worktree
cannot be created — go through one recorded-removal choke point
(lib/retention.py::remove_recorded), which appends one JSON tombstone line per removal
to retention-ledger.jsonl at the factory root (never inside runs/): directory
name, absolute path, reason (age/count/apply-worktree-failure), UTC time, outcome (removed, moved or partial),
every disappeared symlink with its target (symlinks, for removed and partial; null for moved),
and the list of files that disappeared (for removed and partial; null for moved, where moved_files records the pre-removal snapshot). A citation is to a file — e.g. a bead
citing runs/--/candidates.json as evidence — so the record counts
files: a removal that fails part way (the directory survives but contents are destroyed)
is tombstoned with outcome: "partial" and the lost files named, rather than passing a
directory-level check while the evidence is gone. The ledger lives outside the prunable
subtree because the record must outlive the thing it explains: it survives the automatic
prune and a human clearing runs/ to reclaim disk, and beads — which ask the question a
tombstone answers — outlive the run root. runs/README.md (written by the prune, never
swept) points a reader standing on a dead citation at the ledger; if runs/ itself was
cleared, this section is the fallback. The ledger records only removals made
through it: beads sync across VMs and containers, the ledger does not — so a cited
directory with no tombstone in this ledger was not removed by a prune writing to it,
not that the run never existed. The ledger names no machine: inside an ephemeral
container a hostname is a random ID that reads as a stable machine identity while
meaning none, so the honest noun is this ledger — all the record can stand behind.
A cited directory with no tombstone in this ledger admits three readings the
record cannot distinguish: it was never pruned at all; it was pruned through
another ledger (ledgers are local and do not sync); or it was pruned before this
ledger existed — absence is not evidence of absence. The
record also reads as best-effort where it is best-effort: for removed and
partial, files is the pre-removal snapshot (stated on every line as record_scope),
and a file created inside the directory during the removal window may be destroyed
without being listed — the mechanism cannot see it, so the record does not claim
completeness there (for moved, files is null and moved_files records the
pre-removal snapshot). Equally, a listed file is one the removal saw disappear between
its two observations: destroyed by the removal, or moved or renamed out of the snapshot
by a concurrent writer — the record cannot tell which, so a listed file must never be
read as a destroyed file. A symlinks
entry (for removed and partial) records the link and its raw target, and outside_tree is three-valued: true
means the target resolved outside the removed tree — its contents were NOT removed and
are NOT covered by the record (the difference between “this evidence is gone” and “this
evidence moved or was never in this tree”); false means it resolved inside, so the
evidence is covered by files under the target’s real paths; null means the target
could not be fully resolved — a symlink loop, a permission boundary, or more links than
the bounded resolver follows — so the record claims neither survival nor destruction,
and null must never be read as “it survived”.
The deletion-inventory guard (tests/test_retention.py::TestRunRootDeletionInventory)
covers the deletion primitives it knows (shutil.rmtree(, os.unlink(, os.remove(,
.unlink(, rm -rf/rm -fr); a deletion through a construct the scan does not match
— e.g. Path.rmdir() or a subprocess — will NOT be recorded. That boundary is stated
rather than scanned harder: the registry cannot be made complete, and pretending
otherwise is the defect. Note plainly: the ledger explains removals from the
moment it exists — it cannot recover what was already lost before that, and a
tombstone must never be read as having reclaimed an older citation. See
lib/retention.py for the exact policy.
Active-run guard — a concurrently running factory (e.g. a scheduled scan) must
never sweep another process’s still-running run directory, even when many fast runs
overflow the count bound. Each run directory carries a .active marker created at
allocation; pruning skips any directory whose marker is fresher than
FACTORY_RUN_ACTIVE_GRACE_SECONDS (default 3600, one hour). The marker is removed
when the run completes successfully; a run that crashes or is hard-killed leaves its
marker behind, and the grace bounds how long that stale marker protects the directory
before it is swept. Set FACTORY_RUN_ACTIVE_GRACE_SECONDS larger than your longest
station budget.max_minutes if you override budgets.
Repository Structure
agents/ # Fleet of 22 SDLC agents
├── secret-scan/
├── threat-model/
├── vuln-discovery/
├── vuln-verify/
├── vuln-triage/
├── modern-web/
├── ui-ux-audit/
├── accessibility/
├── resilience/
├── perf-review/
├── perf-hillclimb/
├── memory-profile/
├── bundle-size/
├── test-gap/
├── pr-fixer/
├── issue-triage/
├── deps-supply-chain/
├── log-check/
├── docs-drift/
├── docs-write/
├── release-notes/
└── qa-station/
lines/ # Composed Factory Lines (project-audit.yaml, web-excellence.yaml)
.github/
├── actions/factory/ # Reusable Composite Action for CI
└── workflows/ # Factory self-audit and test workflows
lib/
├── adapters/ # Engine bridges: pi.sh, claude.sh, antigravity.sh
├── bench/ # Class C benchmark harness & hill-climb ledger (runner.py)
├── findings.py # Fingerprinting, dedupe, state machine, sink dispatch
└── scheduler.py # macOS launchd plist generation and lifecycle
targets/ # Target project configurations (YAML)
findings/ # Local findings store, latest delta reports, history, ledgers
runs/ # Detailed run transcripts and raw model logs (gitignored)
retention-ledger.jsonl # Tombstone of every removal under runs/ (gitignored; survives clearing runs/)
schedules/ # Generated launchd plists (gitignored)
docs/
├── INTEGRATION.md # Agent and CI/CD integration guide
└── PLAN.md # Master architectural design and research notes
AGENTS.md # Repository rules and agent conventions
factory # Central CLI dispatcher
Reading Order & Further Documentation
docs/PLAN.md: Complete architecture specification, threat model research, Mythos analysis, and 9-phase build plan.docs/INTEGRATION.md: Machine-actionable integration guide for AI agents and CI/CD pipelines.AGENTS.md: Factory conventions, containment tiers, and non-negotiables.THREAT_MODEL.md: Self-hosting threat model for the factory repository itself.
Recommended Tools
Try a different keyword or remove a filter.
Install
npx skillfish add paulkinlan/agents