This guide replicates the "dark factory" that runs on this repository
(dmtools-dart) on any GitHub repo: a human files an issue, assigns it to the
agent, and the machine takes it from there — development, pull request, code
review, bounded rework, and the final merge — with zero human steps in between.
issue assigned/labeled
│
▼
┌─────────────────────────── ai-teammate.yml ───────────────────────────┐
│ guard (decide) → restore fa session → install fa + dmtools → write input/ │
│ → dmtools run <runner>.json → fa agent works (live-streamed to run log) │
│ → persist session + project memory → hand off to reviewer │
└──────────────────────────────────────────────────────────────────────────────┘
│ │
▼ dev run ▼ review run (agent:review)
PR ai/gh-<n> "Closes #<n>" verdict from outputs/pr_review.json
│ │
│ APPROVE ────────┼──────── REQUEST_CHANGES
│ │ │ (blocking findings)
│ ▼ ▼
│ pr_approved label agent:rework → rework run fixes
│ │ │ the same PR branch
▼ ▼ ▼
merge-trigger.yml: green CI + pr_approved → squash-merge → issue auto-closes
rework rounds capped (MAX_AUTO_REWORK_ROUNDS);
past the cap → needs-human + thread summary
Everything below is the concrete wiring, in the order you would set it up.
| Piece | What it is | Where it lives here |
|---|---|---|
| Trigger workflow | Thin stub: issue events → reusable factory call | .github/workflows/ai-teammate.yml |
| Factory pack | The machine itself (guard, legs, verdict, sessions) | agents/.github/workflows/factory/ (pinned submodule) |
| Merge trigger | Squash-merges pr_approved PRs once CI is green |
.github/workflows/merge-trigger.yml |
| Runner wiring | Per-leg runner configs + the leg → runner mapping | .dmtools/runners/ + .dmtools/config.js (in-repo) |
| agents submodule | Agent instructions, job configs, run-agent.sh (provider layer) |
agents/ → IstiN/dmtools-agents |
| dmtools CLI | Job orchestrator: runs the Teammate/CliAgent lifecycle, JS actions, tool bridge | pinned release bundle (releases) |
| fa CLI | The agent harness itself: sessions, tool calls, --log-file transcript |
flutter_agent_harness releases |
The layering is strictly one-directional: the workflow only knows dmtools;
dmtools only knows run-agent.sh; run-agent.sh only knows the selected
provider (AI_AGENT_PROVIDER=fa); fa talks to the model. Swap any layer
without touching the others.
- A GitHub repo where the machine may push branches and merge PRs.
- fa with model access. Any fa-supported backend works — this factory runs
zai(glm-5.3-flash) for dev/rework and a review-strength model for review. fa is self-contained: every "provider" is anFA_PROVIDER_CONFIGenv blob, no per-provider CLIs are installed. - dmtools-dart release bundle (AOT binary + QuickJS
.so). Pin a version; cut a new release when the CLI gains something the machine needs. - A SOURCE_GITHUB_TOKEN (PAT with repo scope) for the agent's own
git/ghoperations. The workflow'sgithub.tokenis used for labeling/merging; the agent inside the job uses the PAT via a credential helper. - Optional but recommended: a bot account (here:
ai-teammate) that issues get assigned to. A plain user account works identically.
Create these (exact names matter — the decide gate matches them):
| Label | Meaning |
|---|---|
agent:dev |
Run a development pass on the issue (or assign to the bot — same path) |
agent:review |
Run a PR review |
agent:rework |
Fix the findings on the linked PR (same branch) |
agent:skip |
Hard opt-out: the machine never runs on this issue (smoke/verification tickets) |
pr_approved |
Review verdict APPROVE — the merge trigger's gate |
bug |
Routes [BUG]-style tickets to the bug agent config |
needs-human |
Set when the rework cap is exhausted |
The cycle also maintains bookkeeping labels (ai_developed,
ai_pr_reviewed, rework-round-<n>, sm_story_rework_triggered); create
them on first use or let the workflow's gh issue edit calls create them.
gh variable set FA_VERSION --body v0.1.357 # fa pin ('latest' allowed, then uncached)
gh variable set DMTOOLS_VERSION --body v0.1.10 # dmtools release tag
gh secret set SOURCE_GITHUB_TOKEN # agent git/gh operations
gh secret set ZAI_CODE_KEY # named by FA_PROVIDER_CONFIG.apiKeyEnvVar
gh secret set KIMI_REVIEW_KEY # the review runner's key (whatever its config names)
The rule for keys: each runner config names its key via
FA_PROVIDER_CONFIG.apiKeyEnvVar — create repo secrets with exactly those
names. run-agent.sh maps FA_PROVIDER_API_KEY into the named variable only
inside the fa subprocess scope.
.dmtools/wiring — copyrunners/*.json(adapt each provider blob:FA_PROVIDER_TYPE+FA_PROVIDER_CONFIG, i.e. the model choice) andconfig.js(thesm.runnersleg → runner mapping the factory guard reads).agents/submodule — addgit submodule add https://github.com/IstiN/dmtools-agents.git agents. It carries:.github/workflows/factory/— the reusable machine loop itself (teammate.yml,sm.yml) that the stubs call;instructions/— the reviewer/dev prompt material (mermaid flows, severity classification, checklists);<role>.jsonjob configs (story_development.json,pr_review.json,pr_rework.json, …) — these run as-is;scripts/run-agent.sh+scripts/providers/fa.sh— the provider layer;setup/fa-session.sh— deterministic session naming (below).
Copy the thin stubs .github/workflows/ai-teammate.yml and
machine-sm.yml verbatim (they only declare the triggers, the per-issue
concurrency group and the reusable call into the factory pack), plus
merge-trigger.yml (adapt the gate-workflow-name placeholder). What you
adapt is repo-level wiring, not workflow logic:
vars.MAX_AUTO_REWORK_ROUNDS— the rework cap beforeneeds-human(factory default 2)..dmtools/config.js— the leg → runner mapping the factory guard reads:
module.exports = {
sm: {
runners: {
bug: '.dmtools/runners/fa-bug-dev.json',
story: '.dmtools/runners/fa-story-dev.json',
review: '.dmtools/runners/fa-review-kimi.json',
rework: '.dmtools/runners/fa-rework-zai.json'
}
}
};Each runner is a thin child that only overrides the provider env (everything
else is inherited from the parent pipeline, which the engine resolves as an
agent pack from the release registry — no agents checkout is cloned anywhere;
the registry base URL comes from the DMTOOLS_PACK_REGISTRY env var, and
pack: prompt references resolve against the unpacked parent pack):
// .dmtools/runners/fa-story-dev.json
{
"parent": { "path": "story_development@latest" },
"params": {
"envVariables": {
"DEFAULT_TRACKER": "github",
"AI_AGENT_PROVIDER": "fa",
"FA_PROVIDER_TYPE": "zai",
"FA_PROVIDER_CONFIG":
"{\"baseUrl\":\"https://api.z.ai/api/coding/paas/v4\",
\"model\":\"glm-5.3-flash\",
\"apiKeyEnvVar\":\"ZAI_CODE_KEY\"}"
},
"inputJql": "",
"cliPromptsByTracker": {
"github": ["pack:instructions/common/github_comment_format.md"]
}
}
}To run the factory on a different model backend, change FA_PROVIDER_TYPE +
FA_PROVIDER_CONFIG here — nothing else in the stack knows about providers.
Workflow permissions the loop needs:
permissions:
contents: write # branches, commits, merge
pull-requests: write # PRs, review threads
issues: write # labels, comments
actions: write # cache saves
concurrency:
group: ai-teammate-issue-${{ github.event.issue.number }}
cancel-in-progress: false # serialize writers per ticketEach run resumes a deterministic fa session named repo:GH-<n>:group:
- bug/story/rework share the dev-write group — a rework run continues the ticket's dev conversation;
- review has its own dev-review group.
CI runners are cattle, so the session directory (.dmtools/fa-sessions) rides
a dedicated git branch fa-sess/gh-<n>: fetched and checked out before the
run, force-pushed after it (if: always() — a failed run's session is exactly
the context the next rework round needs). Two hardening details that cost us
real incidents — keep them:
# restore: checkout-from-tree STAGES what it restores; unstage so session
# files never leak into the agent's own PR commits
git checkout "refs/remotes/origin/$FA_SESS_BRANCH" -- .dmtools/fa-sessions
git restore --staged .dmtools/fa-sessions
# persist: snapshot around the branch switch — `git switch --orphan` removes
# TRACKED files from the working tree and would otherwise delete the very
# tree you are about to commit
SESS_SNAP="$(mktemp -d)"
cp -a .dmtools/fa-sessions "$SESS_SNAP/"
git switch --orphan "$FA_SESS_BRANCH" ...
cp -a "$SESS_SNAP/fa-sessions" .dmtools/
git add -f .dmtools/fa-sessionsAlso add .dmtools/fa-sessions to .gitignore — belt and suspenders against
the same leak.
Why git branches and not actions/cache? Cache saves are denied on
issues-event runs — the job token is cache-read-only there (verified the hard way). Plaingit fetchworks from any event type.
fa's project memory (.fah/memory) is simpler: it is committed to the
repo by the run's persist step, so knowledge accumulates in the repository
itself and every checkout brings it back.
Three channels, all verified live:
- Run log, live: dmtools mirrors the child process output line-by-line to
its own stderr — you watch
Running: fa …, every[bash]/[read]/[write]harness line, and=== Agent completed ===in the Actions log in real time. fa-trace-*artifact: the workflow exportsFA_LOG_FILE;providers/fa.shforwards it asfa --log-file(fa ≥ 0.1.335), which tees the full untruncated transcript — assistant text and tool calls — to a file uploaded as a run artifact. Post-mortem debugging without rerunning.Calling tool …lines: the dmtools tool bridge logs every MCP tool dispatch (file_read,github_resolve_pr_thread, …) — the machine's own hands.
If the trace artifact comes back 0 bytes, check that the provider script
actually forwards --log-file — an empty file means fa was never told to tee
(our exact bug, fixed in dmtools-agents#416).
The review run writes outputs/pr_review.json; the workflow's Apply the
review verdict step parses it:
- APPROVE →
pr_approvedlabel → merge-trigger squashes once CI is green (a REQUEST_CHANGES with zero BLOCKING findings is honored as approve-with-suggestions — suggestions alone never block). - REQUEST_CHANGES with blockers →
agent:rework→ the rework runner fixes the same PR branch → the labeled event re-fires this same workflow for re-review. - Every automatic rework round is counted via
rework-round-<n>labels; afterMAX_AUTO_REWORK_ROUNDSthe machine stops re-labeling, addsneeds-human, and posts a summary of unresolved threads. Re-labelingagent:reworkby hand clears the counter and re-arms the loop with a fresh cap.
Closing the issue is terminal — no agent runs on a closed ticket, whatever the
labels say. agent:skip is the same hard exit for open tickets.
- Plumbing smoke (no agent): create an issue with
agent:skip+agent:dev. Expect the guard step to printagent:skip — agent bypassedand theteammatejob to be skipped. This proves the workflow, labels and gate wiring without spending a token. - Tiny dev task: file
docs/agent-smoke.txtstyle issue ("create a file with one line"), labelagent:dev. Watch the run log for the live harness lines (§8). Verify: PRai/gh-<n>appears,fa-sess/gh-<n>branch exists, thefa-trace-*artifact is non-empty. - Review pass: the dev run hands off automatically; watch the review run produce a verdict and the label state machine execute it.
- Let the smoke PR merge (or close it and the issue — both are clean exits).
- New task: file an issue, assign to the bot or label
agent:dev([BUG]title orbuglabel routes to the bug config). - CLI feature needed by the machine: merge it, cut a release
(
release-cli.yml, workflow_dispatch — idempotent: re-dispatching an existing version is a clean no-op), bumpDMTOOLS_VERSION. - Runaway ticket: close the issue (terminal) or
agent:skip. - Escalation:
needs-humanmeans the cap ran out — fix the PR yourself or re-arm withagent:rework.
Ground truth for why the odd-looking parts look odd:
- Empty fa-trace artifact — provider script never passed
--log-file(§8; dmtools-agents#416). fatal: pathspec '.dmtools/fa-sessions' did not match any files— restore-step staging leaked session files into the PR branch, thengit switch --orphandeleted the now-tracked tree (§7's two hardenings).- Actions cache denied on issue events — job token is cache-read-only there; sessions moved to git branches (§7).
- Red release run on re-dispatch —
git tagdying on "already exists"; the release flow now treats an existing tag as a clean skip. - Duplicate PRs from the same ticket — in-flight runs re-opened PRs after manual cleanup; harmless, close the duplicate, the closed issue is terminal.
Wiring reference in this repo: .github/workflows/ai-teammate.yml (stub)
and agents/.github/workflows/factory/teammate.yml (the machine),
.dmtools/config.js (leg wiring), agents/AGENTS.md
(agent-side conventions), AGENTS.md §8 (session reuse operating manual).