Skip to content

Gate: run a repo's provision and gate commands on a fresh checkout - #43

Merged
rawphp merged 11 commits into
mainfrom
t3/factory-issue-18
Oct 15, 2026
Merged

rawphp merged 11 commits into
mainfrom
t3/factory-issue-18

Conversation

@rawphp

@rawphp rawphp commented Oct 15, 2026

Copy link
Copy Markdown

Closes #18

Contract

  • done: apps/server/src/factory/gate.ts exposes a FactoryGate service (over FactoryProcess) whose run() makes a standalone checkout at <data>/gate/<run id>/<attempt>/ (git init --template=, fetch of the head and the base tip by sha from <remote>/<owner>/<name>.git, checkout --detach, clean-status check, merge --no-ff as softfac/softfac@localhost, or "contained" when the base is already in the head); a conflict aborts the merge and gives conflict with one gate_command line softfac: merge the base branch into the head, exit 1, duration 0; otherwise the provision then the gate commands run through sh -c via FactoryProcess (cleared environment, 30 minutes each), the first failure ends the attempt, and every command that ran is written as gate_command (cause gate-command-ran, with command and attempt) once the loop has ended in a pass or a fail; gate_checkout is written before the first command; the result is pass, fail or conflict; <data>/gates/<run id>.lock is created O_EXCL at the start and removed at the end (one there already → running; max_concurrent_gates lock files held → queued); clearGateLocks runs at FactoryRuntime start; the checkout is removed after the result is recorded (gate_cleanup_failed when it cannot be); git trouble is a typed error with no attempt used.
  • out: worktree removal and listing (Adapter: send a message into a thread, archive it and remove its worktree #6), commits_touch_path / project-config refusal, gate retries, send-back, state changes, PR comments, required checks (Gating: a passing gate opens the PR for review, a failing one goes back to the session #19), the cross-run checkout sweep, flock-based cross-process locks, process-group record files.
  • risk: none
  • test: vp test run apps/server/src/factory/gate.test.ts apps/server/src/factory/index.test.ts → pass (69 tests + 4), vp run --filter t3 typecheck → pass

Settled in this run, not by the issue: gates run inside the server process, so a lock is a plain file created O_EXCL, not an flock; a dead server leaves files that the next start clears. The merge identity and the conflict line keep the literal word softfac, as the issue asks for ledger wording to be ported as is. Non-fetch git calls get FactoryProcess's environment allowlist where softfac gave them only PATH and HOME; none of the extra names changes git's behaviour (noted in the doc comment).

Acceptance criteria

  • Checkout at <data>/gate/<run>/<attempt>/ from the head and base shas, fetched by sha, base merged in; a conflict gives conflict — gate.test.ts "the gate runs in a fresh checkout of the pr head and never in the session's worktree", "a conflict with the base is a failed attempt with no command run", "the base is merged into the head with the daemon's identity", "a base that is already in the head is not merged", "a head that is behind the base gets a merge commit and not a fast-forward".
  • provision then gate commands with sh -c through the process runner, 30 minutes each, cleared environment; each command's exit code and output tail as gate_command; result pass, fail or conflict — "provision runs before the gate and a failing provision command stops the attempt", "a failing gate command ends the attempt and the later commands do not run", "a command that runs past its limit is killed and counts as a failure", "a gate command that cannot start leaves no gate_command line and uses no attempt". The environment is FactoryProcess's allowlist (process.test.ts); the ledger line keeps exit code and duration, the result object carries the output tail.
  • At most max_concurrent_gates gates at once, a run never has two; lock files under <data>/gates/ cleared at server start — "the lock file exists while a command runs and is gone after", "the lock is released when the gate fails with an error", "a second run for the same run while one is running returns running", "a gate is queued while max_concurrent_gates other locks are held", "clearGateLocks removes every lock file and nothing else"; index.test.ts "clears the gate locks a dead server left and nothing else under gates".
  • The checkout is deleted after the result is recorded — "removing a checkout deletes the directory", "a checkout that cannot be removed leaves a gate_cleanup_failed line", and the gate/<run id>/ directory is empty after every gate-level test.
  • softfac's test sections ported and passing — "LocalWorkspace": error texts and run_commands ported; its four remove_worktree / worktrees tests are not ported because issue Adapter: send a message into a thread, archive it and remove its worktree #6 owns worktree removal. "gate: a fresh checkout of the pull request's head": both tests ported (gateResultsComment exact text; the gate-level run with the ledger lines, the untouched primary clone and the empty gate directory; its state-change, PR-comment and worktree_removed assertions belong to Gating: a passing gate opens the PR for review, a failing one goes back to the session #19). "gate: the head merged with the current base": the conflict helpers and their behaviour are ported at gate level. The "standalone repository holding one commit" section that pins the checkout itself is ported too, except commits_touch_path.

Gate

  • vp i (provision) → exit 0
  • vp test run apps/server/src/factory → exit 0 (20 files, 522 tests)
  • vp run --filter t3 typecheck → exit 0

Models

  • maker: claude-sonnet-5-5 (sonnet, host; no models block in .dw/config.yml, so Tom's roster)
  • checker: claude-opus-5-5 (opus, host)
  • orchestrator: this session (claude-fable-5-1)

Review

  • cycles: 1 (correctness, security, reliability, maintainability; then one mutating testing pass on the shipping tip: 12 mutants, 12 killed)
  • fixed: gate_command lines are written only once the commands have ended in a pass or a fail, never on an error path (correctness-1); git calls parse stdout alone and keep stderr for the failure text (correctness-2); git output past the cap fails the call instead of being parsed from a cut front (correctness-3); checkout and leftover removal are asynchronous so a large tree does not block the server's event loop (reliability-1); doc note on the environment drift (security-1); resultsComment(verdict) shared shape for Gate again when a pr-open pull request's head or base moves #23's "passed again" (maintainability-2); the fast-forward test now asserts the second parent (testing-1).
  • leftovers (non-blockers): maintainability-1 asked for a workspace.ts split; dropped because factory/docs/design.md fixes gate.ts as "checkout + provision + gate commands" and Gating: a passing gate opens the PR for review, a failing one goes back to the session #19's policy lands under runner/. maintainability-3 (export ledgerCall from ledger/errors.ts) is outside this issue's files; Gating: a passing gate opens the PR for review, a failing one goes back to the session #19 can lift it. testing-2 (a test title that claims more than it checks; the other half is covered by the call-order test) and testing-3 (no test of a dirty tree after the merge being refused) are optional and left.

Lessons

  • Merge the base before the first review when sibling issues are in flight. Symptom: the diff stat against origin/main showed hundreds of deleted lines in files the issue never touched. Cause: two sibling PRs merged between Isolate and Check, so the branch was behind the base. Default: git fetch and git merge origin/<base> right after the maker's first green, before dispatching reviewers, so they and the gate see the merged tree (field lesson 5 applied at Check rather than only at Ship).
  • A new dependency's fields should be named in the maker packet when a sibling PR adds them. Symptom: the maker merged stdout and stderr for git calls because the packet said to read only exitCode, timedOut, tail; the reviewer then found that parsed data had warnings in it. Cause: separateStderr landed in a sibling PR after the packet was written. Default: after merging the base, re-read the changed shared modules and send their new fields to the maker as a packet amendment before review.

Commits

  • 5acb4cce6 test(factory): the gate's checkout, merge, commands, locks and cleanup — Make slice (red: gate.ts absent)
  • 7a122b802 feat(factory): run a repo's provision and gate commands on a fresh checkout — Make slice
  • 6cf596e4a Merge remote-tracking branch 'origin/main' into t3/factory-issue-18 — base merged in before Check
  • e764b3543 test(factory): a spawn error leaves no gate_command line, and git output is parsed from stdout alone — Check cycle 1 (red at runtime; does not typecheck until the fix, because it uses the new gitTailBytes option)
  • ce276d1fc fix(factory): write a gate's command lines only when its commands have ended in a pass or a fail — Check cycle 1
  • 06f885e97 fix(factory): git calls parse stdout only and fail when they print past the cap — Check cycle 1
  • e979b8071 fix(factory): remove a gate checkout without blocking the server's event loop — Check cycle 1
  • 101920402 docs(factory): note the git environment drift, and share the results comment body — Check cycle 1
  • 848d2f6df test(factory): a head behind the base is checked for a second parent, not only the first — Check testing nit

Files

  • apps/server/src/factory/gate.ts — the gate: workspace errors, results comment, lock files, makeWorkspace (hardened git checkout, merge, removal), runGate and the FactoryGate service
  • apps/server/src/factory/gate.test.ts — the ported tests, against real git with file:// remotes and a fake git on PATH for argv and environment checks
  • apps/server/src/factory/index.ts — clears gate locks when the factory starts
  • apps/server/src/factory/index.test.ts — the clear-locks test

No seam added: factory/SEAMS is unchanged. Labels: all seven factory:* labels existed on the repo; this run created none. The worktree was fast-forwarded from e4bcce9f6 to origin/main at Isolate.

…eckout

Checks out the pull request head merged with the base tip in a standalone
repository, runs provision then gate commands in it under a per-run lock file,
records each step in the ledger, and removes the checkout on every path out.
Server start clears gate locks a dead server left.

Issue: #18
@rawphp
rawphp marked this pull request as ready for review October 15, 2026 13:05
@rawphp

rawphp commented Oct 15, 2026

Copy link
Copy Markdown
Author

Factory gate: passed (attempt 1, run by the factory in a fresh checkout of the pull request's head 848d2f6 merged with the base branch tip 952129a)

Command Exit Time
factory/scripts/check.sh 0 30.2 s

A merge of a large base update prints a line per file and could pass the
1 MiB cap, which failed the gate on every pass though the merge had worked.
Only calls whose stdout is parsed (rev-parse, status, diff) refuse output cut
at the cap, after the exit code is checked, and the error no longer says the
call was killed.

Tests: a merge that exits 1 with no conflicted file is an error; a failing
merge-base stops before any merge; a loud merge still merges; gateCount on a
failing gate command; GH_CONFIG_DIR from the server's environment reaches only
the fetch. The global-config test's own plain clone drops the suite's pinned
GIT_CONFIG_* settings, which outranked its HOME and made it fail. The checkout
and gate results are checked with `satisfies` instead of casts.
…leaves the first of them

The ledger has no batched append, so the lines go in one at a time. GateError
now says that such a failure counts the attempt (the commands ran) and that a
retry under the same attempt number is not counted twice.
@rawphp

rawphp commented Oct 15, 2026

Copy link
Copy Markdown
Author

Factory gate: passed again (attempt 2, run by the factory in a fresh checkout of the pull request's head 848d2f6 merged with the base branch tip dd768b6)

Command Exit Time
factory/scripts/check.sh 0 39.9 s

@rawphp

rawphp commented Oct 15, 2026

Copy link
Copy Markdown
Author

Factory gate: passed again (attempt 3, run by the factory in a fresh checkout of the pull request's head 5ec8efc merged with the base branch tip 465a1e5)

Command Exit Time
factory/scripts/check.sh 0 30.3 s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gate: run a repo's provision and gate commands on a fresh checkout

1 participant