Skip to content

Start: launch the /factory session for a claimed issue - #47

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

tomkaczocha merged 16 commits into
mainfrom
t3/factory-issue-16

Conversation

@rawphp

@rawphp rawphp commented Oct 16, 2026

Copy link
Copy Markdown

Closes #16

Contract

  • done: start() takes a claimed issue to a launched /factory session as softfac's runner/start.rs does: findProject(repo.path) before the claim; the launch-sending step once; the launch under softfac:<run>:launch with softfac's title, base, first message and the machine's model; the session_launched line; the issue text checked again once the session exists (a change archives the session, removes its worktree and goes stuck); the approval-rechecked step; the building label, the status comment and the state_changed line; outcome { started, run, threadId }. Every resume path replays from the ledger, and the pass calls start again for a confirmed run still in triage. A refused launch goes stuck with the detail and the session-may-exist note in the ledger only.
  • out: resume from an earlier run's branch, owed feedback, abandoning a closed issue, archiving older runs in finishOlderRuns (Building: follow the session to its result line #17–End of a run: merge, close, abandon and stop by hand #20), Linear.
  • risk: none
  • test: vp test run apps/server/src/factory → pass; vp run --filter t3 typecheck → pass

Acceptance criteria

  • A run in factory:triage finds the T3 project by the repo's path; a missing project makes the run stuck with softfac's reason, and no project is created — start.test.ts "a repo with no T3 project goes stuck before any launch is sent" (reason no T3 project for this repo on this machine, public comment the factory could not launch a session) and "a T3 that cannot be reached when the project is looked up claims nothing".
  • It launches with command id softfac:<run>:launch, the base from the factory block, softfac's thread title and first message, and the model instance and model from the machine config — start.test.ts "claims, launches the session and moves the issue to building" (asserts the whole LaunchRequest), "records the model the launch call sent"; messages.test.ts pins the first message byte for byte against softfac's launch_message.
  • It checks the issue text again and acts on a change as softfac does — start.test.ts "a text that changed before the second look archives the session and goes stuck" (changed and unjudged), "the second look comes after the launch is recorded and before the label", "an archive that fails is an error before anything is written, and the next start retries".
  • It sets factory:building, upserts the status comment and writes session_launched with the thread id — the happy-path test's tracker calls and ledger lines (startedEvents()).
  • A crash between launch and record is safe: the next pass launches with the same command id and gets the same thread — start.test.ts "writes the launch-sending step once and sends under the same command id", "with no session launches and then looks at the text once"; pass.test.ts "a confirmed triage run gets start on the next pass, and the line says the session started".
  • softfac's runner tests "start" and "start: a claim that errors after GitHub applied it" are ported and pass — start.test.ts describes "start: a fresh ready issue", "start: a claim call that fails", "start: a launch that an earlier pass may have sent", "start: a resume", "start: older runs of the issue".

Decisions

  • The issue body says the text is checked "before launching"; softfac checks it right after the launch is recorded and before the label moves to building, and never before the launch (a resume that skipped the launch for a changed text would leave an untracked thread running). The port follows softfac and the ported tests.
  • In-process there is no transient launch failure class (no connection, no timeout, no unreadable reply): every failure of FactorySessions.launch is T3's answer after the request went out, so it is final and recorded with the session-may-exist note. A findProject failure still fails the start with nothing claimed, as in softfac.
  • FactorySessions.layerLive (adapter) provides the projection store the server does not have in scope where the factory is merged in; nothing outside factory/ changed.
  • The factory's worktree was cut from a stale base (e4bcce9f6); it was fast-forwarded to origin/main (a8a24e261) before provisioning. No factory:* labels were created; all seven existed.

Gate

  • vp test run apps/server/src/factory → exit 0 (31 files, 921 tests)
  • vp run --filter t3 typecheck → exit 0

Models

  • maker: claude-sonnet-5-5
  • checker: claude-opus-5-5

Review

Lessons

  • A command guard can match prose the contract requires. Symptom: the maker stopped at its first edit because the machine's Bash deny hook matched gh auth token inside a heredoc that wrote the launch message, which forbids that very command. Cause: the guard scans command lines, and the contract text carried the phrase as data. Default: when a packet's byte-exact text contains words a command guard watches, say in the packet to write those files through the Write/Edit tools and never put the phrase on a command line.
  • A criterion that says "the next pass does X" names a caller; put the caller in files. Symptom: correctness approved the runner, and reliability found that nothing in the pass ever called it again, so the crash-safety criterion was unreachable in production. Cause: the port split the step (start.rs) from its dispatcher (tick.rs) and the contract named only the step. Default: for each criterion phrased as "the next pass/run/tick does X", list the dispatcher in files: and add a test that drives X through it.

Commits

  • 24d132e34 feat(factory): a sessions layer with its projection store provided — Make slice
  • 844104c4a test: port softfac's start tests for the launch — Make slice (red)
  • cb7c29f67 feat(factory): launch the /factory session for a claimed issue — Make slice
  • d38718702 test(factory): a confirmed triage run is started again by the next pass — Check cycle 1 (red)
  • 76c9b3659 fix(factory): the pass starts a confirmed triage run again — Check cycle 1
  • c20e7a48a test(factory): a start that fails at T3 is logged as a t3 error — Check cycle 1 (red)
  • 13207bed2 fix(factory): a failed T3 call is logged as kind t3 — Check cycle 1
  • 8e61acc4c refactor(factory): move the run's stop and stuck path to runner/run.ts — Check cycle 1 (no red phase: a move)
  • 9f46619cd test(factory): pin the whole outcome in two stuck-path start tests — Check cycle 2 (no red phase: tightened assertions)

Files

  • apps/server/src/factory/runner/start.ts — the launch, the second look and the move to building; findProject before the claim; resume from the ledger
  • apps/server/src/factory/runner/run.ts — new: Progress, goStuck (with the session stop and worktree removal) and finishBuilding, shared by the steps to come
  • apps/server/src/factory/runner/messages.ts — the launch message, LAUNCH_FAILED, TEXT_CHANGED, the session-may-exist note
  • apps/server/src/factory/runner/progress.ts — Seen reads launch-sending, approval-rechecked, the worktree and its removal
  • apps/server/src/factory/pass.ts — workRuns calls start for confirmed triage runs; started outcome; T3 errors are kind t3
  • apps/server/src/factory/index.ts — wires FactorySessions into start; FactoryLayer provides layerLive
  • apps/server/src/factory/t3/sessions.ts — layerLive
  • apps/server/src/factory/runner/start.test.ts, messages.test.ts, pass.test.ts, index.test.ts — the ported and added tests

The server has the project store, the launch and management services and
git in scope where the factory is merged in, but not the projection store.
`layerLive` is `layer` with that store provided, for the factory's root layer.

Issue: #16
Fails until the launch lands: start() has no sessions, launch message, text re-check or started outcome yet (type errors and failing assertions in start, messages, pass and index tests).

Issue: #16
Fails: the pass never calls start for a run that is already confirmed in triage.

Issue: #16
Fails: the error is recorded as kind ledger.

Issue: #16
A pure move with no behaviour change, so there is no red phase. Progress gains the step it runs for, so #17, #19 and #20 reuse the stop path under their own cause step. The ledger step names launch-sending and approval-rechecked are exported from progress.ts and used by start.ts.

Issue: #16
Two review nits: the no-note test now asserts the stuck outcome and its
reason, and the failing-unassign test asserts the exact ledger lines. Both
pass already; no behaviour changes.

Issue: #16
@rawphp
rawphp marked this pull request as ready for review October 16, 2026 01:25
@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 9f46619 merged with the base branch tip a8a24e2)

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

The pass's run and untracked-claim steps now call `resume`, which goes on with
a claim already made and never takes the fresh-claim branch: an issue put back
to factory:ready is left to the claim step, which checks the pause, the limit
and the blockers (f1, f2).

`resume` also finishes a stuck path cut short after its label moved: an issue
labelled factory:stuck whose newest run is unfinished in triage with a stuck
line in its ledger. Before, start skipped it every pass and the run held a
slot forever (f10).

A pass that runs past 30 minutes is stopped and logged, so one hung call (an
in-process T3 launch, say) no longer stops every later pass. The bound is above
any single call's own limit (gh 60 s, worktree removal 10 min); a launch cut
short resumes on the next pass under the same command id (f11).

Tests for the stop path's worktree-remove failure and a thread T3 no longer
knows (f5). Drops imports the move to run.ts left unused.
…k by its own run

Since the run steps only resume, a run whose issue an instructor labelled
factory:ready again stayed in triage and held a slot; with every slot taken
the claim step never reached the issue, so its claim never finished the old
run (f10). The claim step now leaves an issue's own runs out of the limit,
since a claim finishes them first. Another issue's run still holds its slot.

PASS_TIMEOUT's comment no longer claims only a hung pass is stopped (f13).
A test pins that resume leaves a stuck issue alone when its run never went
stuck (f14).
…eeps its slot

Round 3 left an issue's own runs out of the claim limit, so an issue put back
to factory:ready while its old session still ran could start a second session
past max_concurrent_runs: finishOlderRuns ends the old run in the ledger only
until #20 ports the stop (f15). That exemption is gone.

As softfac's `stalled` does, a confirmed triage run that `resume` will not take
(a person relabelled, reassigned or closed its issue) now prints an ATTENTION
line saying it is not progressing and holds a slot, and how to let the start
finish, instead of a quiet "left to a later pass" (f10). The run keeps its
slot, as in softfac; abandoning a closed issue's run is #20'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 8265cc9 merged with the base branch tip a8a24e2)

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

…is logged

The stalled ATTENTION line now carries resume's reason, and a skip says what a
person did: a closed issue's line says to reopen it (nothing finishes a closed
issue's run until #20), and an issue put back to ready with nobody assigned
gets a plain note instead, as the claim step takes it when a slot is free
(f10, f16). Each attention line also writes the machine log's `attention`
event with `not-progressing`, as softfac does (f18). Tests pin a re-readied
issue with and without a free slot (f19); comments say its run holds a slot
until a claim finishes it (f17).

A claim_repaired line the machine log cannot take is logged as a warning, not
dropped (f9). `mention` and the sole-assignee check have one definition each,
still case-folded with toLowerCase: a Linear user can be a display name (f8).
…says so

An issue put back to factory:ready while its own unfinished run holds the last
slot can never be claimed: only that claim would finish the run. The claim
step now raises an ATTENTION line and a `not-progressing` attention event for
it, naming the run and how to free it, instead of only a limit line (f10). The
limit still counts the run, so no second session starts past
max_concurrent_runs.

pass.ts matches repos with runId.ts's asciiLower, as run ids do (f8). A test
pins that resume says nothing moved for a ready issue someone is assigned to,
which stalls its run (f19).
…nless run

An issue put back to factory:ready while its own triage run held the last slot
waited forever: only that claim would finish the run (f10). Runs now say
whether a session may be running (a launched session, or a launch that was
sent). The claim step leaves the issue's own confirmed triage runs with no
session out of the limit, since finishOlderRuns ends them and nothing is left
running. One whose session may be running keeps its slot, so no second session
starts past max_concurrent_runs, and only when such runs fill the limit by
themselves does the claim raise its ATTENTION line (f20).
… issue's run

Two pass tests: a re-readied issue whose own run has no session still waits
when other runs fill the limit, and once claimed past its own run it takes the
slot the next ready issue would have had (f21). workRuns picks its runs with
the claim step's `inTriage`, so the two cannot drift, and the docs no longer
say the run holds its slot until the claim (f22).
@tomkaczocha
tomkaczocha merged commit 9e80956 into main Oct 16, 2026
@rawphp
rawphp deleted the t3/factory-issue-16 branch October 27, 2026 23:51
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.

Start: launch the /factory session for a claimed issue

2 participants