Skip to content

Pass loop: claim ready issues fairly - #46

Merged
tomkaczocha merged 15 commits into
mainfrom
t3/placeholder-1
Oct 16, 2026
Merged

tomkaczocha merged 15 commits into
mainfrom
t3/placeholder-1

Conversation

@rawphp

@rawphp rawphp commented Oct 15, 2026 •

Copy link
Copy Markdown

Closes #15

Contract

  • done: FactoryLayer runs a pass at start and every poll_seconds, never overlapping, with wake(); a pass records pass_started, loads each repo's factory block from its default branch and the gh login, lists unfinished runs, handles untracked claims (triage, ours, no run: stuck "the factory could not confirm the issue text was approved", as softfac) and unassigned claims (triage/building, nobody, quiet 15 min, our label: proposed with the cut-short comment, claim_repaired reconciled), then claims ready issues oldest first, fewest-held repo first, recounting after each claim, under max_concurrent_runs, with blocked/limit/unapproved/paused claim_skipped events, the limit and pause lines, and pass_finished. A claim applies the approval rule, assigns and swaps ready→triage in one edit (repairing a half-applied edit), and writes approved/claimed/state_changed to a new run file; a refusal posts softfac's comment once and moves the issue to proposed.
  • out: launching sessions (Start: launch the /factory session for a claimed issue #16), advancing runs, gates and the end of a run (Building: follow the session to its result line #17–End of a run: merge, close, abandon and stop by hand #20), forwarding (Forward instructor comments and edits into a running session #22), the dry run, the disk floor, the worktree sweep, the retro, the T3 GH_CONFIG_DIR check, the repo clone and project checks, Linear.
  • risk: none (writes labels, assignees and comments on watched repos, as softfac does)
  • test: vp test run apps/server/src/factory → pass (30 files, 867 tests)

Acceptance criteria

  • FactoryLayer runs a pass at start and every poll_seconds; passes never overlap; wake() starts the next pass now — loop.test.ts (the pass loop: 5 tests under TestClock), index.test.ts "logs factory started once for a valid machine file, then runs the first pass". A wake during a pass makes the next pass follow it at once, however many wakes arrived.
  • Per repo: factory block from the default branch, ready issues oldest first, approval and blockers, claims while slots are free; max_concurrent_runs slots; a slot is held from claim until pr-open, needs-info or stuck; fewest-runs repo first — pass.test.ts "claiming new issues", "which repo gets a free slot", "issues blocked by an open issue", "issues whose Depends on text names an open issue", "the limit, said out loud"; ledger/listRuns.test.ts "holdsSlot"; index.ts projectFromBase (verified live: see below).
  • A claim needs no assignee; it assigns the factory's login, swaps factory:ready for factory:triage in one edit, and writes claimed to a new run file; a refusal moves the issue to factory:proposed with softfac's comment — runner/start.test.ts "checks the approval, claims, and records the approval, the claim and the state", "a refusal posts the comment, moves the issue to proposed and makes no run", "a refusal already explained since the label is not commented again", the claim-repair tests; tracker/github.test.ts "claim views the assignees, then assigns and swaps the labels in one edit".
  • An untracked claim is handled as softfac handles it — runner/start.test.ts "with no run goes stuck: the factory cannot confirm what it was approved on" (softfac sends a triage issue of ours with no run to stuck at once; the 15-minute grace is softfac's rule for the claims with no assignee, ported in tracker/github.test.ts "unassignedClaims keeps only unassigned issues that are old enough" and pass.test.ts "claims with no run and no assignee"). The issue text pairs the 15 minutes with the assigned case; the port follows softfac's code and docs (docs/lifecycle.md, "claims that were cut short").
  • While <data>/claims-paused exists no claims are made, logged once per pass — pass.test.ts "the hand pause"; claims.test.ts.
  • Machine log lines match softfac's — pass.test.ts "what the pass records" (pass_started, pass_finished, repo_skipped, claim_skipped limit/blocked/unapproved/paused, claim_repaired, error in the run ledger with step-failed and in the machine log with run only when the ledger cannot take it); the ledger's event tables are unchanged.
  • softfac's tick tests for ordering, fairness, slots, pause and untracked claims are ported and pass — pass.test.ts holds 64 tests ported from src/tick/tests.rs (claiming new issues, which repo gets a free slot, claims that have no run, claims with no run and no assignee, blocked, Depends on, the limit, the hand pause, what the pass records, text that is printed). The dry-run tests are not ported: there is no dry run.

Verified live before Ship, reads only, through liveWorld with the real gh as rawphp against this repo: the factory block parsed from main, login rawphp, ready issues [16, 17, 19, 20, 21, 22, 23], no triage or unassigned claims, and #16 blocked by #15 (open) both by link and by its Depends on text. No write was made.

Deliberate deviations from softfac, each marked in the code for the next issue: a fresh claim ends at { kind: "claimed" } and the pass line says claimed in run <id> (the launch is #16); finishOlderRuns finishes older runs in the ledger only (archive and PR draft are #16/#20); a closed issue is skipped, not abandoned (#20).

Gate

  • factory/scripts/check.sh → exit 0 (seam check; vp test run apps/server/src/factory 867 passed; vp run --filter t3 typecheck; vp run --filter @t3tools/web typecheck). Log: .factory/gate-1.log.
  • The launch-time config named vp test run apps/server/src/factory and vp run --filter t3 typecheck; both also exit 0.

Models

  • maker: claude-fable-5-1 (this session; the roster's claude-sonnet-5-5 was not used because the port needed the softfac context this session had already read; recorded as fallen back)
  • checker: claude-opus-5-5

Review

  • cycles: 1
  • fixed: correctness-1 (the pause file is opened with O_NONBLOCK, so a FIFO cannot hang the pass), correctness-2 (an error of a start that made a run goes to that run's ledger, with madeSince to find it), correctness-3 (every call start makes is a tracker call: kind tracker, as is the tracker login), correctness-4 (a ready issue whose older run is gating is skipped first with "gate running"), maintainability-1 (BoundGitHub and bindProcess; the claim's Tracker is a Pick), maintainability-2 (one ledgerCall in ledger/errors.ts, used by gate, start and index), maintainability-3 (isState exported once), maintainability-4 (one noteStart for both callers).
  • leftovers (non-blockers): reliability-1 (stop GitHub calls for the rest of a pass after a rate limit): not softfac's behaviour, out of this issue; reliability-2 (nit: make the cut-short comment best-effort in releaseClaim): the port matches softfac's release_claim, which fails on either call; maintainability-5 (split github.ts into transport and operations): the reviewer defers it to the PR that adds the pull request operations.
  • testing: 12/12 mutants killed (slot held by pr-open, the limit, fewest-held, the recount, blockers before the limit, the pause, approval, the repair's "nothing applied" case, the stuck condition, the gate-lock check, the 15-minute boundary, the latch close); no findings.
  • red then green: each test: commit was run before its feat:/fix: commit and failed as its body says (a missing module, a missing method, or the asserted value); the whole suite is green at the tip.

Lessons

  • On a fork, gh pr create without --repo opens the pull request on the parent repository. Symptom: the draft PR for this fork's branch appeared as pingdotgg/t3code#20083 (closed at once with a one-line apology; no other write was made upstream). Cause: gh resolves a fork's default base to its parent. Default: always pass --repo <owner/repo> and --head <branch> to gh pr create and gh pr edit, and read the URL gh prints before reporting it.
  • A reviewer's tree copy under .factory/ is collected by the test runner. Symptom: the gate run reported twice the test files and 16 failures, every one under .factory/testing/. Cause: vitest globs the whole worktree and .factory/.gitignore does not hide it from the runner. Default: on a JS repo, remove the mutating reviewer's copy before running the gate (or tell the reviewer to remove it at the end), and read the gate log's file count against the suite's.
  • Re-read the gate from .dw/config.yml after Isolate's fast-forward. Symptom: the two gate commands from the launch-time config passed, but the base tip's config had moved to one script that also typechecks the web app. Cause: the config was read before git merge --ff-only origin/<base>. Default: read the factory block again after the fast-forward, and run that gate.
  • A test that reproduces a blocking call hangs the whole run. Symptom: the FIFO test for the pause file blocked the test process until the shell timeout. Cause: the bug under test is a blocking open. Default: run such a test file alone with a per-test timeout, commit it red, and expect the first run to hang rather than fail.

Commits

  • 7061a137c test(factory): duration, unfinished run listing and the claims pause file — Make slice 1
  • 31812bedb feat(factory): duration, unfinished run listing and the claims pause file — Make slice 1
  • d31b95ab3 test(factory): gh login, the claim edit, unassigned claims and the newest factory comment — Make slice 2
  • 491322480 feat(factory): gh login, the claim edit, unassigned claims and the newest factory comment — Make slice 2
  • 768a8d53d test(factory): the claim: approval, the claim edit and its repair, refusals, older runs, and the untracked claim — Make slice 3
  • 99efe706c feat(factory): the claim: approval, the claim edit and its repair, refusals, older runs, and the untracked claim — Make slice 3
  • 74a39f315 style(factory): catch the missing-ledger tag with catchTags, as the lint rule asks — Make slice 3
  • 1863af803 test(factory): softfac's tick tests for ordering, fairness, slots, blockers, pause and untracked claims — Make slice 4
  • 0a0686743 feat(factory): one pass: load repos, count runs, handle untracked and unassigned claims, claim fairly — Make slice 4
  • cf3532aee test(factory): the pass loop runs at start, every poll_seconds, on wake, and never overlaps — Make slice 5
  • 1c66f2d36 feat(factory): FactoryLayer runs the pass loop over the live world and exposes wake() — Make slice 5
  • dd396c29b test(factory): a FIFO pause file does not block, a failed start's error goes to its run, a gating issue is skipped first — Check cycle 1
  • 641145bfc fix(factory): review cycle 1: ... — Check cycle 1

Files

  • apps/server/src/factory/index.ts — FactoryLayer builds the live world over gh and the ledger, runs the loop, exposes FactoryLoop (wake, awaitPass)
  • apps/server/src/factory/loop.ts — the pass loop: start, poll, wake, no overlap
  • apps/server/src/factory/pass.ts — one pass over PassWorld: repos, runs, untracked and unassigned claims, the claims
  • apps/server/src/factory/claims.ts — the claims-paused file: load, pause, resume
  • apps/server/src/factory/runner/start.ts — the claim: approval, the edit and its repair, refusals, older runs, the untracked claim's stuck path
  • apps/server/src/factory/runner/progress.ts — what a run's ledger already records
  • apps/server/src/factory/runner/messages.ts — softfac's fixed words: the stuck comment, the status comment, the cut-short comment
  • apps/server/src/factory/ledger/listRuns.ts — unfinished runs oldest first, holdsSlot
  • apps/server/src/factory/ledger/time.ts — duration
  • apps/server/src/factory/ledger/errors.ts — ledgerCall, shared by gate, start and index
  • apps/server/src/factory/domain/lifecycle.ts — isState
  • apps/server/src/factory/gate.ts — uses the shared ledgerCall
  • apps/server/src/factory/tracker/github.ts — login, claim, unassignedClaims, newestFactoryComment, BoundGitHub, bindProcess
  • tests beside each: index.test.ts, loop.test.ts, pass.test.ts, claims.test.ts, runner/start.test.ts, ledger/listRuns.test.ts, ledger/time.test.ts, tracker/github.test.ts

rawphp added 13 commits October 16, 2026 09:04
…file

Red: duration is not exported, and listRuns.ts and claims.ts do not exist.

Issue: #15
…west factory comment

Red: makeGitHub has none of login, claim, unassignedClaims or newestFactoryComment.

Issue: #15
…fusals, older runs, and the untracked claim

Red: runner/start.ts and runner/messages.ts do not exist. Also silences the
global Date diagnostic in claims.test.ts, which sets an mtime.

Issue: #15
…fusals, older runs, and the untracked claim

Issue: #15
…ockers, pause and untracked claims

Red: pass.ts does not exist.

Issue: #15
…ke, and never overlaps

Red: loop.ts does not exist, and FactoryRuntime does not yet need a FactoryProcess or run a pass.

Issue: #15
…or goes to its run, a gating issue is skipped first

Red: loadHandPause blocks on a FIFO (the test hangs), PassWorld has no recordError or
gateRunning, and tracker failures from start are recorded as kind github.

Issue: #15
…d start's error goes to its run ledger, tracker kinds, gating skip, one bound tracker

- claims: open the pause file with O_NONBLOCK, as softfac does, so a FIFO is no pause
- pass: an error of a start that made a run goes to that run's ledger (step-failed), and
  to the machine log with the run only when the ledger cannot take it; a ready issue whose
  older run's gate is running is skipped first with "gate running"; every call start makes
  is a tracker call, so its errors are kind tracker, as is the tracker login
- github: BoundGitHub and bindProcess, so the claim's Tracker is a Pick and index.ts binds
  every method at once; ledgerCall is exported once from ledger/errors and isState from
  domain/lifecycle; one noteStart puts a start's outcome on the line for both callers

Issue: #15
@rawphp
rawphp marked this pull request as ready for review October 16, 2026 00:01
@rawphp

rawphp commented Oct 16, 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 641145b merged with the base branch tip 88c871b)

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

…scan a pass, stranded stuck runs, fail-closed pause, live pass lines

- With no slot free the pass reads no blockers; every issue left counts toward the limit line.
- The ledger is listed once a pass; after each start only that issue's run files are read again.
- A run with no approval and no session is not confirmed: it holds no slot, the untracked-claims
  step starts its issue again, and start goes on from it to stuck in the same run.
- A pause file that exists but cannot be opened is a pause, since its mtime.
- Pass lines are logged as they are decided, so a pass that dies has said what it did.
- Tests for liveWorld's grace window, assignee filters and base-branch rule, and for
  repairClaim's half-moved and someone-else-assigned re-reads.
…before it moves to proposed

A label call that fails after the comment leaves the issue where a later pass finds it again, and
the comment is not posted twice while it is the issue's newest factory comment.
@rawphp

rawphp commented Oct 16, 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 d364cef merged with the base branch tip 88c871b)

Command Exit Time
factory/scripts/check.sh 0 35.2 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.

Pass loop: claim ready issues fairly

2 participants