dark-factory
Run the Dark Factory DOT pipeline runner against a goal. Slash command: /factory. Implements StrongDM's Attractor pattern as an external Python runner — .dot files are the versioned artifact, sealed h
SKILL.md.claude/skills/dark-factory/SKILL.md
GitHub/factory — Dark Factory Pipeline Runner
Purpose
Run a goal through the Dark Factory .dot pipeline runner at
~/projects/dark-factory/. The runner is the Python implementation of
StrongDM's Attractor pattern (cf. brynary/attractor, strongdm/attractor).
Versus /h / /goal_harness:
| Aspect | /h (goal_harness) |
/factory (this skill) |
|---|---|---|
| Engine | Claude session dispatches subagents | dark-factory binary (uv install) |
| Artifact | Skill prose + tool calls | .dot graphviz file |
| Recording | Conversation transcript | SQLite CXDB row-per-step |
| Holdouts | Adversarial subagent reads diff | Sealed repo agent never sees |
| Reuse | Tied to this session | Replayable from .dot alone |
| Cost | High (multi-agent context) | Low (one CLI per node) |
Use /factory when you want the goal_harness idea as a reproducible external
pipeline; use /h when you want an interactive in-session loop.
Entry points
| Command | Behavior |
|---|---|
/f |
Full loop, auto-routes PR-mode vs feature-mode (Step 0a below) |
/factory |
Alias for /f, identical behavior |
/f-pr |
Explicit PR-mode entry point (skips auto-route, always PR-mode) |
/fs |
Spec-generation entry point; runs pipelines/slim/spec_gen.dot or a binary-owned dynamic spec graph — run this first when the goal needs a spec |
This skill file is the single source of truth for the binary-first contract;
.claude/commands/f.md is the canonical command entry point, with /factory
and /f-pr as thin aliases so auto-detect behavior stays identical across
entry points.
What "binary-first" bans: an in-Claude prose-only workflow that claims a factory run without a logged binary invocation is not a valid run — see Honesty rules below. Every default run must preserve required default nodes or their graph-level equivalents: plan/spec producer, independent review, bounded fix loop, evidence gates, and exit summary. Dynamic graph generation is valid only when the generated/selected DOT graph is saved or echoed in the run evidence.
Repos
~/projects/dark-factory/— the runner (factory code; agent can see this)~/projects/dark-factory-holdouts/— sealed scenarios (agent must NEVER read)- Pushed:
https://github.com/jleechanorg/dark-factory+dark-factory-holdouts
Available pipelines
.dot file |
Flow | When to use |
|---|---|---|
pipelines/slim/two_node.dot ⭐ default |
worker → fresh Codex reviewer → exit | Default for /f and /factory when no --pipeline is passed. The reviewer is a fresh fully tooled codex exec --ephemeral --yolo process; its response is copied to the worker on failure. |
pipelines/factory/hello.dot |
plan → implement → holdout → fix-loop → exit | Add a new feature with a holdout scenario |
pipelines/factory/gates.dot |
start → holdout → /es → /er → /code_standards → exit | Validate an already-implemented diff (Attractor-style 4-gate harness as .dot) |
pipelines/factory/pr_gates.dot |
start → holdout → /es → /er → /code_standards → exit | Validate an already-implemented in-flight PR diff (Holdout-always policy; requires --feature <name> for the holdout) |
pipelines/slim/minimal_feature.dot |
explore → plan → implement → test → review → holdout → gates → exit | Full production pipeline from scratch with a holdout |
pipelines/slim/minimal_pr.dot |
explore → plan → implement → test → review → holdout → gates → exit | Slim in-flight PR iteration loop with parameterized tests (Holdout-always policy; requires --feature <name> for the holdout) |
| pipelines/factory/level5_feature.dot | full reference pipeline | Full Level-5 reference pipeline with hard-tier gates wired in |
| dynamic DOT via binary | binary-owned graph builder | A static graph can't express the needed phase/fanout; the binary saves/echoes the generated graph in run evidence |
| no pipeline | — | When no available pipeline fits the inspected PR, follow canonical Honesty rules below: report the limitation and return to the parent for authorized work; do not force an inapplicable holdout pipeline |
You can also write your own .dot and pass it via --pipeline.
How to invoke this skill
The user types /f $ARGUMENTS (or /factory $ARGUMENTS). Parse the
arguments and run the steps below. When --pipeline is omitted, /f and
/factory default to two_node (pipelines/slim/two_node.dot). Non-default
pipelines require explicit opt-in via --pipeline <name>.
Step 0a — Auto-route PR-mode vs feature-mode (applies to /f and /factory; /f-pr skips this and forces PR-mode)
Run once before dispatch:
gh pr list --head "$(git rev-parse --abbrev-ref HEAD)" \
--json number,title,state,isDraft,additions,deletions,changedFiles,baseRefName,headRefName,labels
echo "$ARGUMENTS"
Reasoning (LLM, not rules):
- No open PR → feature-mode (new work).
- Open PR exists AND goal relates to it → PR-mode (drive the existing PR to green).
- Open PR exists AND goal is unrelated → ask the user which mode they meant. Do not silently route.
/f-pr — explicit PR-mode entry point
/f-pr skips Step 0a and always runs PR-mode. Use the LLM to read the
PR's actual context and pick the right pipeline — this is a reasoning
task, not a deterministic rule table (no if is_draft then X, no
if labels contains 'bug' then bug_fix.dot, no keyword-routing).
- Resolve the PR number: from
$ARGUMENTSif present, elsegh pr list --head "$(git rev-parse --abbrev-ref HEAD)" --json number --jq '.[0].number', else ask the user. - Read PR context as facts, not routing rules:
gh pr view <N> --json number,title,state,isDraft,additions,deletions,changedFiles,baseRefName,headRefName,body,files,labels gh pr view <N> --json statusCheckRollup gh pr diff <N> --name-only gh pr view <N> --comments # optional ls specs/ roadmap*/ docs/design/ 2>/dev/null # optional - Reason about: what kind of work this is (new feature / bug fix /
refactor / docs / test-only / infra — from diff+body+files, never
pre-bucketed by label alone); whether a spec already covers the
change (if so,
/fsis skippable; if needed, report the prerequisite and carry out its already-authorized preparation or return it to the parent task under active repository role and execution constraints. Do not force a pipeline run before it is ready); holdout eligibility (pass--feature <name>only if~/projects/dark-factory-holdouts/holdouts/<feature>/actually exists — never invent one); and what evidence mix (/es+/er+/code_standardsminimum,holdout_evalfor behavior-grade) the pipeline needs to deliver without over-running. - Execute only after needed preparation is complete and a pipeline
actually fits: pick the pipeline from Available pipelines above
using that reasoning, pick the backend (
echofor wiring smoke;claudeunless the PR's reviewer queue orgate_erpriority queue says otherwise), then construct, show, and run the command — same shape as Step 0c below butcdinto the PR's target repo, notdark-factory. - Report the outcome: when a pipeline ran, report the verdict per Output contract below; otherwise report the unmet prerequisite or no-fitting-pipeline limitation and return to the parent task with no fabricated run ID, exit code, or review verdict, preserving authorized parent continuation.
/f-pr honesty rules (in addition to the shared ones under Honesty
rules): the gate_er priority queue (codex > minimax > agy > claude-sonnet) still resolves the reviewer even when --backend claude
was passed for the run itself — passing --backend claude does not mean
gate_er uses claude by default.
Step 0b — Auto-detect CLI backend
When the user invokes Claude via a ~/.bashrc wrapper (claudem, clauded,
codex, agy), bash exports a BASH_FUNC_<wrapper>%% env var. Read these to
pick the right --backend flag so the LLM that runs the audit is the same
one the user picked from their shell. %% in the var name breaks
${BASH_FUNC_X%%:-default} and ${!key} expansion, so use printenv:
detect_cli_backend() {
local key parent_comm ppid
for key in 'BASH_FUNC_claudem%%' 'BASH_FUNC_clauded%%' 'BASH_FUNC_agy%%' 'BASH_FUNC_codex%%' 'BASH_FUNC_claude%%'; do
if [ -n "$(printenv "$key" 2>/dev/null)" ]; then
case "$key" in
*claudem%%) echo "minimax"; return ;;
*clauded%%) echo "claude"; return ;;
*agy%%) echo "agy"; return ;;
*codex%%) echo "codex"; return ;;
*claude%%) echo "claude"; return ;;
esac
fi
done
case "${ANTHROPIC_BASE_URL:-}" in
*api.minimax.io*) echo "minimax"; return ;;
*anthropic.com*) echo "claude"; return ;;
*openai.com*) echo "codex"; return ;;
esac
ppid="${PPID:-}"
if [ -n "$ppid" ]; then
parent_comm="$(ps -o comm= -p "$ppid" 2>/dev/null | tr -d ' ')"
case "$parent_comm" in
*claude*|*claudem*) echo "claude"; return ;;
*codex*) echo "codex"; return ;;
*agy*|*antigrav*) echo "agy"; return ;;
esac
fi
echo "claude"
}
DETECTED_BACKEND="$(detect_cli_backend)"
Override precedence (highest to lowest): explicit --backend <x> flag >
BASH_FUNC_* wrapper signal > ANTHROPIC_BASE_URL > parent process name >
hardcoded default (claude). Always echo the detected backend + source in
the proof block (see Output contract). If --backend was passed
explicitly, note the override rather than the raw detection.
Step 0c — Pipeline selection
When --pipeline is omitted, the runner dispatches pipelines/slim/two_node.dot by default. To opt into a non-default pipeline (such as gates, minimal_feature, review_slim, etc.), pass explicit --pipeline <name>.
Short-name expansion
When --pipeline is a short name (no / or .dot), expand under
$DARK_FACTORY_HOME:
| Short name | Path |
|---|---|
two_node |
pipelines/slim/two_node.dot |
gates |
pipelines/factory/gates.dot |
hello |
pipelines/factory/hello.dot |
pr_gates |
pipelines/factory/pr_gates.dot |
minimal_feature |
pipelines/slim/minimal_feature.dot |
minimal_pr |
pipelines/slim/minimal_pr.dot |
review_slim |
benchmarks/attractor-spec-review/pipelines/review_slim.dot |
review_full |
benchmarks/attractor-spec-review/pipelines/review_full.dot |
spec_gen |
pipelines/slim/spec_gen.dot |
bug_fix |
pipelines/bug_fix.dot |
level5_feature |
pipelines/factory/level5_feature.dot |
Default graph when --pipeline is omitted
When the user invokes /f or /factory with no --pipeline flag, the
runner dispatches pipelines/slim/two_node.dot by default — exactly two
productive nodes (a generic worker codergen + a cold_reviewer
type="codergen" verdict gate), plus a bounded fix loop. The cold reviewer
runs as a fresh fully tooled Codex process in the worker's target worktree;
the engine copies its exact response back into the worker prompt on failure.
To opt into a richer pipeline, pass an explicit --pipeline <name>. To
roll your own (e.g. a custom slim or feature shape), pass
--pipeline /absolute/path/to/your.dot. Custom dot graphs always win
over the default — there is no scenario where /f will silently fall
through to a heavyweight pipeline.
The default is the user's standing rule (set 2026-08-02): slim two-node
is the default; custom graphs are explicit opt-in. This applies to all
/f invocations across this repo.
Arg parsing
Honor these flags inside $ARGUMENTS:
--pipeline <name>— short name (two_node,gates,hello,pr_gates,minimal_pr,minimal_feature,review_slim,review_full) or path to a.dot. If omitted, the runner dispatches the slim two-node default graph (pipelines/slim/two_node.dot) — a generic worker + fresh, fully tooled Codex reviewer — for every/fand/factoryinvocation. To opt into the heavyweight pipelines (gates / minimal_feature / etc.) pass an explicit--pipeline <name>. The previous "auto-select from the goal" behavior is retired; the slim two-node shape is the new default across the board.--feature <name>— holdout feature. Required when the pipeline includes aholdout_evalnode, but never default it blindly: pass it only after confirming~/projects/dark-factory-holdouts/holdouts/<name>/actually exists (hellois a real, feature-agnostic holdout and a reasonable choice when the goal has no more specific holdout, but confirm the directory exists before passing it rather than defaulting unconditionally — see Honesty rules below on not inventing--featurevalues)--backend echo|claude|codex|ao|agy— defaultclaude. Useechofor cost-free dry-runs that verify wiring without LLM calls.--max-steps <N>— default 100--cxdb <path>— default~/.dark-factory/cxdb.sqlite(shared across runs so the Healer can cluster across history)--state KEY=VALUE— pre-seed context variables; repeatable. Extremely useful for setting parameterized test commands (e.g.--state slim.test_command="pytest tests/...") during PR iteration loops.
Whatever is left after flag parsing is the goal description.
Steps
Before Steps 1–2, define this resolver. It preserves an explicit
DARK_FACTORY_HOME; otherwise it resolves the dark-factory binary selected
from PATH, follows symlinks, derives its repository root, and exports that
same source for pipeline and prompt resolution:
resolve_dark_factory_home() {
if [ -z "${DARK_FACTORY_HOME:-}" ]; then
DARK_FACTORY_BIN="$(command -v dark-factory 2>/dev/null)" || {
echo "ERROR: dark-factory is not on PATH; run ./install.sh first"
return 1
}
DARK_FACTORY_BIN="$(python3 -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$DARK_FACTORY_BIN")" || {
echo "ERROR: unable to resolve the installed dark-factory binary"
return 1
}
DARK_FACTORY_HOME="$(cd "$(dirname "$DARK_FACTORY_BIN")/.." && pwd -P)" || return 1
fi
export DARK_FACTORY_HOME
}
Verify binary install. The factory runs via the
dark-factorybinary (notpython -m runnerfrom source). If missing, report the unmet prerequisite and inspect the existing installation before cloning or reinstalling. Complete setup within the current authorized scope and repository execution rules, or return the limitation to the parent for other authorized work. Ask only for missing authority or access; an unavailable binary is not a completed run. The following check runs as a tool command; its failure ends that command, then the parent follows the prerequisite guidance above:export PATH="$HOME/.local/bin:$PATH" resolve_dark_factory_home || exit 1 command -v dark-factory || exit 1 dark-factory --help || exit 1 test -x "$DARK_FACTORY_HOME/bin/dark-factory" || { echo "ERROR: run $DARK_FACTORY_HOME/install.sh first" exit 1 }Environment:
resolve_dark_factory_home || exit 1 export DARK_FACTORY_HOLDOUTS="${DARK_FACTORY_HOLDOUTS:-$HOME/projects/dark-factory-holdouts}" export PATH="$HOME/.local/bin:$PATH"Run the pipeline from the target repo (implementation workdir = cwd). Pipelines and prompts resolve from
$DARK_FACTORY_HOME; code changes land in cwd:cd "<TARGET_REPO>" # e.g. the product repo being built dark-factory \ --pipeline pipelines/<PATH_TO_DOT>.dot \ --goal "<GOAL>" \ --backend <BACKEND> \ --feature <FEATURE> \ --cxdb <CXDB_PATH> \ --state <KEY>=<VALUE>Surface the verdict. The last line of stdout is a JSON summary with
final_outcome,pipeline,goal,steps,trace. Report:- Pipeline name + final outcome
- Per-node trace (one line each:
node outcome preview) - Exit code of the runner
Run the Healer (only if
final_outcome != "success"):df-healer --cxdb <CXDB_PATH>Render the resulting markdown table inline. Each row is a failure cluster with a prescription template the user can act on.
Sealed holdouts reminder. If the run included a
holdout_evalnode, remind the user that you (and any subagent) did NOT see the scenarios at~/projects/dark-factory-holdouts/holdouts/<feature>/— only the PASS/FAIL verdict per scenario name leaked back.
Reviewer calibration (default on)
--reviewer-calibration=true is the default for every real /f//factory
run. Treat the flag as present unless the user explicitly passes
--reviewer-calibration=false.
Calibration routes the Codex backend through the binary-owned controller command:
dark-factory review \
--workdir <repo> \
--base-sha <full-40-hex-sha> \
--head-sha <full-40-hex-sha> \
--task-file <path> \
--output-dir <dir> \
--backend codex
dark-factory review owns the static prompt, canonical envelope, backend
dispatch, response validation, and controller-receipt.json. Controller v1
accepts only Codex through its tool-free JSONL transport. A valid PASS requires
non-empty evidence_checked and commands_executed: []; the
ReviewTransportReceipt and controller terminal receipt bind the prompt,
envelope, response, reviewed revision/tree, and evidence manifest. Do not
author reviewer instructions, do not inline prompts, do not pass vendor CLI
flags. The controller binds prompt SHA-256, envelope SHA-256, task SHA-256, diff SHA-256, changed-files SHA-256, and
evidence-manifest SHA-256 in the controller receipt so calibration lanes can
hash the receipt itself for provenance.
Each accepted lane must expose a controller receipt digest containing
the prompt SHA-256, envelope SHA-256, task SHA-256, diff SHA-256,
changed-files SHA-256, evidence-manifest SHA-256, head SHA, base SHA,
and the response's own response SHA-256 digest. Hashing the
controller-receipt.json file itself gives a stable SHA-256 digest for
the controller receipt.
prompt.txt is a binary-emitted audit capture, not an authority file.
The static authority lives in prompts/catalog/controller_cold_review_v1.md
and is bound by its SHA-256 of the controller receipt.
evidence/<run-id>/reviewer-calibration/
<backend>/
controller-receipt.json
envelope.json
prompt.txt
reviewer.output.md
findings.json
comparison.json
adjudication.md
Do not claim delegated subagents underperformed raw Codex unless the same
envelope and prompt were reviewed at the same SHA and raw Codex found a
later-confirmed blocker the delegated reviewer missed. If calibration is
disabled, the final response must say Reviewer calibration: disabled and
give the explicit reason.
The gate_er priority queue is codex > minimax > agy > claude-sonnet by
default; passing --backend claude for the run itself does not change how
gate_er resolves its reviewer.
Output contract
When a pipeline ran, end the /f//factory invocation with this proof block
(missing any required line means the run is unproven and must be reported as
such). When no pipeline ran because a prerequisite was unmet or no applicable
pipeline fits, report the unmet prerequisite or no-fitting-pipeline limitation
and return to the parent task without fabricated run metadata (no fabricated
run ID, exit code, or review verdict), preserving authorized parent
continuation.
For executed pipeline runs, provide this proof block:
# CLI backend: <detected-or-override> (source: <BASH_FUNC_X%%|explicit --backend|default>)
# Literal command run:
cd /Users/jleechan/projects/<target-repo>
DARK_FACTORY_HOLDOUTS=~/projects/dark-factory-holdouts \
PATH="$HOME/.local/bin:$PATH" \
dark-factory \
--pipeline <chosen-or-generated-dot> \
--goal "<echo of $ARGUMENTS>" \
--backend <backend> \
--feature <feature-if-any> \
--cxdb ~/.dark-factory/cxdb.sqlite
# Run ID: <id>
# CXDB SHA: <sha>
# Final outcome: <success|failure|exhausted|error>
# Exit code: <integer>
# Wall-clock: <duration>
# Logs: <path>
# Evidence envelope: <path>
# Reviewer calibration: <enabled|disabled> <artifact-path-or-reason>
Plus the per-node trace and, on failure, the Healer report.
Adding a new feature
To add a new sealed-holdout feature foo:
- Write
~/projects/dark-factory/specs/foo.md(agent-visible). - Write
~/projects/dark-factory/prompts/foo/{plan,implement,fix}.md. - Write
~/projects/dark-factory-holdouts/holdouts/foo/scenarios.yamlin the sealed repo (the implementing agent must never read this). - Either reuse
pipelines/factory/hello.dot(which is feature-agnostic) by passing--feature foo, or writepipelines/factory/foo.dotwith custom nodes.
Review Verdict Interpretation Contract (Zero Inversion)
status: "valid"/backend_returncode: 0: Indicates only that the reviewer CLI/transport executed cleanly and emitted valid JSON matching the schema. It does NOT indicate that the code passed.verdict: "fail"/exit_code: 2: Indicates the independent review REJECTED the change.verdict: "pass"/exit_code: 0: Indicates the review ACCEPTED the change.- NEVER treat
status: "valid"or a successful LLM invocation as a passing verdict whenverdict == "fail".
Review Result Surface Requirements
Whenever dark-factory review completes, the agent MUST immediately report:
- If
verdict: fail(exit code 2):VERDICT: REJECTED (FAIL)- The exact reviewed
HEAD_SHAandBASE_SHA. - List of all failing Criteria Gates (
C0–C7) and Evidence Gates (E0–E14). - Full verbatim list of Critical, High, and Blocker findings from
reviewer.output.md. - Explicit statement that the PR is NOT
/readyand CANNOT be merged.
- If
verdict: pass(exit code 0):VERDICT: ACCEPTED (PASS)- The exact reviewed
HEAD_SHAandcontroller-receipt.jsondigest.
Loop Exhaustion Invariant
- Reaching the maximum cycle budget (e.g., 3/3 cycles) without achieving
verdict: passconstitutes a FAILED RUN. - The agent MUST report
STATUS: REVIEW FAILED / EXHAUSTED AFTER N CYCLESand end that factory invocation and return its failure evidence to the parent task. The parent continues authorized work within the mission deadline and the active repository's role and execution rules. Where coding must run through auto-factory, route repairs through a fresh or adjusted factory run. An invocation's cycle budget is not a mission stop. - An agent must NEVER summarize cycle exhaustion as "addressed findings", "all gates passing", or "/ready".
Honesty rules
- Always tell the user the actual command you ran (
dark-factory ...ordf-healer ...). No paraphrasing. - Always quote the runner's exit code and actual review verdict (
pass/fail) verbatim. - Never rewrite or summarize a failed review (
verdict: fail, exit code 2) as a success, pass, or confirmation. - Never claim a PR is
/ready,/green, or "All gates passing" when the review verdict isfailor when CI checks are pending/failing. - Never claim a holdout passed if you read the scenarios — that breaks the
adversarial guarantee. If you read
~/projects/dark-factory-holdouts/, declare the run invalid and rerun the pipeline. - If
--backend echowas used, label the run as a wiring smoke, not a real validation. - Reviewer calibration is default-on. If
--reviewer-calibration=falseis passed, quote the explicit opt-out reason and do not imply raw-Codex-vs- subagent quality was measured. - Do not claim a factory run based on an in-Claude workflow,
Skill()call, or prose summary. The only valid proof is an actualdark-factorybinary invocation plus the proof block above. - If
/fsis needed first, follow/f-pritem 3: report the prerequisite and carry out its already-authorized preparation or return it to the parent task under active repository role and execution constraints. Ask only for missing authority; do not silently fall through togates.dotand pretend the PR is green. - If no pipeline fits (e.g. docs-only PR), report that limitation and return to the parent task for authorized diagnosis or preparation. A materially different requested method needs authorization; do not silently substitute a holdout-bearing pipeline.
- Do not invent
--featurevalues. If there's no holdout directory at~/projects/dark-factory-holdouts/holdouts/<feature>/, don't pass--feature. - When the goal is unrelated to the open PR (Step 0a), ask the user which mode they meant. Do not silently route to PR-mode for unrelated work.
- When the fix loop exhausts (3 attempts), surface its failure diagnosis and return control to the parent as above. Preserve the failed verdict; never auto-merge or retry an unchanged failing approach merely to get a pass.
Known limits
- The
claudebackend shells out toclaude --print --dangerously-skip-permissions— each node is a fresh non-interactive session. Long agentic loops should use/hinstead. _parse_verdictfalls back to scanning the tail for a standalone PASS/FAIL token; if the gate command emits a debug line with just "PASS" the runner will trust it. For high-stakes gates require explicitVERDICT:markers.- CXDB is per-process — don't share one across threads.