Skip to content

Building: follow the session to its result line - #51

Merged
tomkaczocha merged 18 commits into
mainfrom
t3/factory-issue-17
Oct 16, 2026
Merged

tomkaczocha merged 18 commits into
mainfrom
t3/factory-issue-17

Conversation

@rawphp

@rawphp rawphp commented Oct 16, 2026

Copy link
Copy Markdown

Closes #17

Contract

  • done: a building run is followed on every pass and within 5 s of an events wake: the thread's status is read. FACTORY-DONE → the pull request is checked (same repo, not a fork, base = project.base, head = the session's branch; a ready pull request goes back to draft with a pr_set_draft line, a draft call that fails is stuck unless rate-limited) → pr_opened once → outcome ready-for-gate, label stays building. FACTORY-STOP: needs-info → questions comment, factory:needs-info, unassign, status comment, state_changed, archive + worktree removed, finished needs-info. FACTORY-STOP: stuck → archive, comment (session text fenced), factory:stuck, unassign, status, finished. No result line → one reminder per session (result-line-reminding <k> / result-line-reminded), a second miss is stuck. failed/cancelled/rolled_back → stuck "the session run ended as "; interrupted → paused (session_paused line only); thread gone or archived → stuck "the T3 thread no longer exists"; wrong gh account → session-wrong-account. Issue text changed during the build (watch) → stuck. Worked time in triage+building past limits.minutes → stuck "the run passed its time limit", branch named when the thread has one.
  • out: the gate and merged/closed pull request ends (Gating: a passing gate opens the PR for review, a failing one goes back to the session #19); closed issue, stop by hand (End of a run: merge, close, abandon and stop by hand #20); forwarding (Forward instructor comments and edits into a running session #22); interruption evidence, resume and relaunch; paused-run feedback; pr_adopted/branch_bound; commits_ahead counting (always null, so the comment's sentence has no parenthesis, as softfac prints when its clone cannot count).
  • risk: none
  • test: vp test run apps/server/src/factory → pass (33 files, 1087 tests); vp run --filter t3 typecheck → pass

Acceptance criteria

  • On every pass, and within 5 s of the events stream naming a thread the factory owns, the factory reads the thread's status — pass.test.ts "following a building run" (time_limit → watch → advance per run; needs-info/stuck runs advanced too); index.test.ts "waking the pass when a factory thread's run changes status" (ownsThread, a non-owned thread never wakes, an owned thread starts the next pass and a failed stream is re-run). FactoryEvents.layerLive shares the server's EventSinkV2 instance via RuntimeLayer.layerEventSink (verified by the correctness reviewer from runtimeLayer.ts/server.ts).
  • FACTORY-DONE: <url>: same repo, not a fork, configured base, session's branch as head; ready → draft; pr_opened written; moves on to the gate — building.test.ts "advance: done" (23 tests: fork, base, branch, no branch, not a draft → drafted, draft call fails → stuck, rate limit → error, pr_opened once).
  • FACTORY-STOP: needs-info: questions posted, factory:needs-info, unassigned, archived, worktree removed, run ends — "advance: questions" and "advance: a run left in needs-info".
  • FACTORY-STOP: stuck: archived, comment with the reason, factory:stuck — "advance: stuck" (session text fenced, wrong account, ended badly, thread gone/archived, decision already in the ledger).
  • One reminder per session; a second miss is stuck — "advance: the result-line reminder" (18 tests, incl. crash between send and step, same command id softfac:<run>:remind:<k>).
  • A run older than limits.minutes is stuck with softfac's reason — "timeLimit" (boundary at the last millisecond, branch named without a count, no branch, status read fails, never-launched run with the session-may-exist detail).
  • The matching cases in softfac's "advance" and "watching a build" sections are ported and pass — building.test.ts names the skipped ones in its header comment (merged/closed ends, closed-issue abandon, interruptions, gate-pass skips, the real-gh watch cases, apply internals).

Gate

  • vp test run apps/server/src/factory → exit 0 (.factory/gate-0.log)
  • vp run --filter t3 typecheck → exit 0 (.factory/gate-1.log)

Models

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

Review

  • cycles: 2 (cycle 1: correctness, security, reliability, maintainability, read-only; cycle 2: testing, mutating, 12 named mutants, all reached, 10 killed, 1 equivalent, 1 survivor fixed)
  • fixed: the pass never re-advanced a run left in needs-info/stuck after a failed archive, leaking the session and its slot (correctness-1 = reliability-1, blocker); a reminder T3 refuses looped every pass until the time limit (reliability-2) — now stuck at once with softfac's "no result line" reason and T3's refusal in the ledger detail; four hand-written lifecycle action loops → one applyActions ported from softfac's apply (maintainability-1); a second "unsafe text" character class → one sanitize in text.ts (maintainability-2); progressFrom shared with start.ts, Progress.pr required (nit); the pass passes the ledger CauseStep explicitly (nit); a thread the factory does not own is proven never to wake the loop (testing-1, the survivor); bounded wait in the wake test (nit); dead !endedBadly term dropped (nit).
  • leftovers: none
  • notes: the worktree the factory made was at e4bcce9f6, behind origin/main; fast-forwarded to 9e8095616 before provisioning (field lesson 18). All seven factory:* labels already existed on the repo; none created. Red-then-green verified at the pair b4e6422a6 (6 failures) → c4ac52d58 (202 pass) in a detached worktree.

Lessons

  • In a pnpm monorepo, a detached red/green worktree runs tests through the root node_modules/.bin/vp. Symptom: npx vitest and apps/server/../../node_modules/.bin/vitest both print nothing or "no such file" in the detached worktree, so the red check looks like it ran and found nothing. Cause: pnpm does not hoist vitest's bin to the root .bin, and npx resolves against the wrong tree. Default: symlink the root node_modules and every workspace package's node_modules into the detached worktree, then call ./node_modules/.bin/vp test run <files> from its root; grep the output for Test Files to prove the run happened.
  • Name the repo's existing helper in the packet before asking the maker to write one. Symptom: the maker wrote a second sanitiser character class in messages.ts while gate.ts already had sanitize, and a reviewer had to find it. Cause: the packet described the helper's behaviour but did not grep for an existing one. Default: for every utility a packet asks for (sanitiser, fence, formatter, id builder), grep the repo first and write "use <path>:<name>" or "none exists" into the packet.
  • Copy the reference's per-state dispatch into the packet, not just the happy state. Symptom: the pass called the new step only for building runs, so a run left in needs-info/stuck was never finished; two reviewers flagged it as the one blocker. Cause: the packet said "building runs get watch → advance" and "other states are left alone", while the reference's match info.state sends every non-gate state to advance. Default: when porting a step the reference dispatches by state, paste that match arm by arm into the packet and mark each arm in or out of scope.

Commits

  • 46d9e3e9b test(factory): session text, result-line reminder and progress of a building run — Make slice 1 (red)
  • 09915e03f feat(factory): prepare session text and read a building run's progress — Make slice 1
  • e0687d155 test(factory): advance a building run to its result line — Make slice 1 (red)
  • 8d6ef7830 feat(factory): advance a building run to its result line — Make slice 1
  • a36408d07 test(factory): watch a building run's text and stop it at its time limit — Make slice 1 (red)
  • fec84c408 feat(factory): watch a building run's text and stop it at its time limit — Make slice 1
  • a2f42fe31 test(factory): the pass works a building run and the loop wakes on its thread — Make slice 2 (red)
  • cd2b0089a feat(factory): the pass follows a building run and wakes on its thread's run changes — Make slice 2
  • b4e6422a6 test(factory): the pass advances a run left in needs-info or stuck, and a refused reminder is final — Check cycle 1 (red)
  • a56eabc03 fix(factory): the pass advances a run left in needs-info or stuck — Check cycle 1
  • c4ac52d58 fix(factory): a reminder T3 refuses ends the run as stuck — Check cycle 1
  • 732955429 refactor(factory): one interpreter for lifecycle actions, one text sanitizer — Check cycle 1
  • 098c9e363 test(factory): a thread the factory does not own never wakes the pass — Check cycle 2 (red)
  • 71fb342a4 refactor(factory): the events wake is testable over a finite stream — Check cycle 2

Files

  • apps/server/src/factory/runner/building.ts — advance, watch, timeLimit and their outcomes, ported from softfac's building.rs
  • apps/server/src/factory/runner/building.test.ts — the ported "advance", reminder, "watching a build" and time-limit cases
  • apps/server/src/factory/runner/run.ts — Stop with session text and branch, applyActions (softfac's apply), progressFrom, stopSession exported
  • apps/server/src/factory/runner/progress.ts (+test) — Seen gains the pull request, questions, reminder, wait marker, pause and worked-time fields
  • apps/server/src/factory/runner/messages.ts (+test) — prepareSessionText, fenced, questionsComment, stuckComment with session text and work left, the reminder and the fixed reasons
  • apps/server/src/factory/runner/start.ts — uses progressFrom; finishBuilding reads the thread from Progress
  • apps/server/src/factory/text.ts — sanitize, moved from gate.ts (which re-exports it)
  • apps/server/src/factory/gate.ts — imports sanitize from text.ts
  • apps/server/src/factory/pass.ts (+test) — the per-run flow: time limit, then resume / watch → advance / advance by state, with softfac's lines and error steps
  • apps/server/src/factory/index.ts (+test) — BuildingContext in the live world; ownsThread, wakeOn, the events consumer with restart; FactoryLayer provides Events.layerLive
  • apps/server/src/factory/t3/events.ts — layerLive over the server's shared EventSinkV2

@rawphp
rawphp marked this pull request as ready for review October 16, 2026 03:32
@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 71fb342 merged with the base branch tip 9e80956)

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

@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 71fb342 merged with the base branch tip 11492a7)

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

…on a set read

A run whose session reported its pull request is no longer limited until it
next enters building: the wait for the gate is not the build's time. A triage
run whose launch was sent with no session recorded is left to resume, which
names its session, so the limit never finishes a run whose session may be
running untracked.

The events wake checks an in-memory set of owned threads, read at start and
after every pass, instead of reading every run file per status change.

The time-limit comment no longer promises that the next attempt continues
from the old branch. sanitize is imported from text.ts everywhere, and
advance's reminder send and ended-badly statuses each have one home.

Tests: an unconfirmed run is never time-limited, watched or advanced; a
hostile branch name reaches the comment and ledger only as prepared text; a
run back in building from pr-open is limited again.
@rawphp

rawphp commented Oct 16, 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 69d4c2d merged with the base branch tip 11492a7)

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

Until the gate is wired into the pass, a building run whose session reported
its pull request stays in building. It has no session at work, so like a
pr-open run it no longer takes one of max_concurrent_runs, and new claims are
not starved by finished builds. The listing carries prReported from the
ledger, cleared when the run enters building again.
@rawphp

rawphp commented Oct 16, 2026

Copy link
Copy Markdown
Author

Factory gate: passed again (attempt 4, run by the factory in a fresh checkout of the pull request's head 5295969 merged with the base branch tip 11492a7)

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

…ad again

Once the session has reported its pull request, the build is done and the run
waits for the gate (#19). The pass no longer watches its text or calls advance
for it, so each pass makes no tracker or T3 call for a finished build and only
says it is ready for the gate.
@rawphp

rawphp commented Oct 16, 2026

Copy link
Copy Markdown
Author

Factory gate: passed again (attempt 5, run by the factory in a fresh checkout of the pull request's head 77b57e0 merged with the base branch tip 11492a7)

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

@tomkaczocha
tomkaczocha merged commit d8e3539 into main Oct 16, 2026
@rawphp
rawphp deleted the t3/factory-issue-17 branch October 28, 2026 04:06
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.

Building: follow the session to its result line

2 participants