Skip to content

Adapter: find a project, launch a thread and read its status in-process - #36

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

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

Conversation

@rawphp

@rawphp rawphp commented Oct 15, 2026 •

Copy link
Copy Markdown

Closes #5

Contract

  • done: FactoryPaths gives baseDir, worktreesDir and <base>/factory from ServerConfig. FactorySessions ports softfac's Sessions trait in-process: findProject over ProjectStoreV2 (exact root, live projects only); launch over ThreadLaunchService (new worktree from baseRef, or the origin tip when startFromOrigin, full-access, default interaction, the caller's commandId, so the same id again returns the same thread and starts nothing); status and thread over ProjectionStoreV2 with softfac's run selection (highest ordinal, skipping a cancelled run that never started unless every run is one; that run's own last assistant message; branch, worktreePath, archived). thread reads the thread row alone.
  • out: send, archive, events.ts, process.ts, wiring into FactoryLayer, HTTP routes, ledger.
  • risk: none
  • test: vp test run apps/server/src/factory → pass (42 tests); vp run --filter t3 typecheck → pass

Acceptance criteria

  • sessions.ts provides FactorySessions with findProject, launch, status, thread, softfac's inputs and the ten run statuses — sessions.ts (RUN_STATUSES, LaunchRequest, FactorySessions); sessions.test.ts "maps every known run status".
  • status reports the latest run leaving out a cancelled run that never started, that run's last assistant message, and branch / worktreePath / archived — sessions.test.ts threadStatusOf block: 16 cases ported from softfac src/t3.rs plus "reports only the reported run's own last assistant message" and "reports the last of the run's assistant messages, not the first"; "reads a real recorded snapshot" against softfac's t3_thread_snapshot.json.
  • thread reports the thread fields alone and works when the run cannot be read — sessions.test.ts "thread reads the thread's own fields and works when the runs cannot be read" (getThreadProjection fails, getThread answers).
  • launch starts a thread in a new worktree from baseRef / origin tip, full-access, default interaction, given model, title, message, under the caller's commandId; a repeated commandId returns the same thread and starts nothing — sessions.test.ts "starts a thread in a new worktree, full-access, default interaction, under the caller's command id" (asserts every field of the ThreadLaunchInput, including startFromOrigin: true); sessions.contract.test.ts (second launch → same thread id, runOrdinal stays 1, run stays completed).
  • paths.ts gives base dir, worktrees dir and <base>/factory from ServerConfig — paths.test.ts.
  • sessions.contract.test.ts runs the real orchestration layers with the provider-testing replay (Claude simple transcript through ProviderReplayHarness.layerWithRegistry, real ThreadLaunchService, real git worktree through GitVcsDriver): launches, waits for run.updated to reach a terminal status through EventSinkV2.stream, reads "fixture simple ok", checks the worktree's .git exists under the worktrees dir, that git branch --show-current equals status.branch and HEAD equals the base ref, then repeats the commandId. Runs in about 5 s.
  • Boundary test still passes; only factory/t3/ imports T3 internals — boundary.test.ts in the gate run; factory/scripts/check-seams.sh exit 0 (no upstream file touched, no new seam).

Gate

  • vp test run apps/server/src/factory → exit 0 (6 files, 42 tests) — .factory/gate-1.log
  • vp run --filter t3 typecheck → exit 0 — .factory/gate-2.log

Models

  • maker: claude-fable-5-1 (this session; .dw/config.yml has no models block, so Tom's roster applied, and Make ran in-session after the Contract research rather than in a sonnet-5-5 worker)
  • checker: claude-opus-5-5 (host subagents: correctness, testing)

Review

  • cycles: 1 (correctness: approve, 1 action; testing: request changes, 1 action, 10 of 12 mutants killed, 1 equivalent). The cycle-1 fix is a few lines; the orchestrator read the diff and shipped, per the testing review budget.
  • fixed: blank commandId / projectId / instanceId / threadId threw a schema defect from CommandId.make instead of failing as SessionsError / ThreadNotFoundError (softfac returns InvalidInput); findLast vs find for the last assistant message had no test with two assistant messages in one run.
  • leftovers: none. Equivalent mutant: archived with archivedAt === undefined (T3 types it NullOr, never undefined).

Notes

  • Isolate: the launched worktree was one merge behind origin/main (issue Factory skeleton: an empty FactoryLayer wired in through one seam #1 had landed); fast-forwarded e4bcce9f6 → f1dba9a17 before provisioning. The first vp i failed on a transient .git/config lock in the primary checkout; the retry succeeded.
  • Labels: all seven factory:* labels already existed; none created.
  • creationSource is "server" (softfac sent "web" over the wire). The correctness reviewer checked: nothing in T3 branches on it for createdBy: "user", and in-process callers (serverRuntimeStartup, UsageLimitRecoveryWorker) already use "server".
  • The test: commits were red before their fix: f2e3f81f4 (modules missing), 55551d4a5 (2 failures observed before 24a0161ce). The two-assistant-messages case in 55551d4a5 was green already; it pins findLast.
  • Ship: the first gh pr create (no -R) opened the PR on upstream pingdotgg/t3code as Adapter: find a project, launch a thread and read its status in-process pingdotgg/t3code#19865, because gh resolves a fork's parent as the default repository. Closed it at once with a comment; nothing else was written there. The real PR is this one, opened with -R original-solutions/t3code-factory.
  • softfac's RunStatus::Unknown has no counterpart: in-process the projection status is a closed literal union, so a new upstream status fails typecheck instead.

Lessons

  • On a fork, gh pr create needs -R <owner/repo>. Symptom: the draft PR landed on the upstream repository the fork was cut from, with the fork issue's Closes #n pointing at a stranger's issue. Cause: gh picks the parent repository as the default for a fork's remote. Default: always pass -R <factory repo> (and --head <branch>) to gh pr create, gh pr edit and gh pr list, and check the printed URL's owner before reporting it.
  • A contract test over a real runtime needs the live clock and a diagnostic that survives a pass. Symptom: the test hung to the timeout with no error; Effect.timeoutOption never fired; console output was invisible once the test passed. Cause: it.effect runs under a TestClock the runtime never advances, and vitest hides stdout of passing tests. Default: use it.live for runtime contract tests, and when a wait hangs, fork a stored-event logger that appends to a file under .factory/ before the action under test, then compare where the pipeline stopped against where the wait subscribed.
  • Check an unfamiliar stream combinator's signature in the library source before building a wait on it. Symptom: Stream.filterMap returning Option.none() matched nothing in Effect 4 (it takes a Filter), so every variant of the wait returned empty. Default: grep "^export const <name>" node_modules/<lib>/src/<Module>.ts before the first use; prefer Stream.filter + Stream.take(1) + Stream.runCollect for "first matching event".
  • Run the gate's typecheck before the first product commit, not only before Ship. Symptom: six type errors (deterministic service keys, branded ids, mock shapes) in files whose tests were already green. Cause: vitest transpiles without typechecking. Default: typecheck right after the first green test run.
  • Re-read a file after a commit before patching it by exact string. Symptom: three string-replace patches applied to nothing. Cause: the repo's pre-commit hook reformats staged files, so the text on disk no longer matched what the session wrote. Default: after any commit, grep -n the target lines and patch against what is there.

Commits

  • f2e3f81f4 test(factory): sessions and paths adapter cases — Make slice (red)
  • 53d91d5b8 feat(factory): sessions adapter over T3's launch and projection services — Make slice
  • 55551d4a5 test(factory): blank ids fail typed, last assistant message is the last — Check cycle 1 (red)
  • 24a0161ce fix(factory): blank ids fail as the declared sessions errors — Check cycle 1

Files

  • apps/server/src/factory/t3/sessions.ts — the adapter: types, threadStatusOf, FactorySessions service and layer
  • apps/server/src/factory/t3/paths.ts — FactoryPaths from ServerConfig
  • apps/server/src/factory/t3/sessions.test.ts — ported softfac status and find_project cases; launch, status, thread over mocks and a real in-memory ProjectStoreV2
  • apps/server/src/factory/t3/sessions.contract.test.ts — real orchestration layers, Claude replay transcript, real git worktree
  • apps/server/src/factory/t3/paths.test.ts — paths from a ServerConfig
  • apps/server/src/factory/t3/fixtures/t3_thread_snapshot.json — softfac's recorded thread snapshot, copied from tests/fixtures

Ports softfac's find_project and status cases from src/t3.rs, adds launch
and thread cases, and a contract test over the real orchestration layers
with the Claude replay transcript. Red: paths.ts and sessions.ts do not
exist yet, so none of the three files compile.

Issue: #5
FactorySessions ports softfac's Sessions trait in-process: findProject
over ProjectStoreV2, launch over ThreadLaunchService (new worktree from
baseRef or the origin tip, full-access, default interaction, the
caller's command id so a retry is the same launch), status and thread
over ProjectionStoreV2 with softfac's run selection. FactoryPaths gives
the base dir, the worktrees dir and <base>/factory from ServerConfig.

The contract test waits for the run through EventSinkV2.stream with
take(1): Stream.filterMap takes a Filter in Effect 4, not an Option, so
the first draft never matched, and the test runs on the live clock
because the orchestration runtime does not advance a TestClock.

Issue: #5
From review cycle 1. Red: a blank commandId or threadId escapes as a
schema defect instead of SessionsError / ThreadNotFoundError. The
two-assistant-messages case is green already; it pins findLast against
find, which every earlier test let through.

Issue: #5
CommandId.make and ThreadId.make throw on a blank id. Decode the ids
inside the Effect instead: a blank command, project or instance id is a
launch SessionsError, and a blank thread id names no thread.

Issue: #5
@rawphp
rawphp marked this pull request as ready for review October 15, 2026 11:16
@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 24a0161 merged with the base branch tip f1dba9a)

Command Exit Time
vp test run apps/server/src/factory 0 17.1 s
vp run --filter t3 typecheck 0 6.0 s

status is polled per session, so it now reads the runs and assistant
messages through getThreadRecords instead of loading the whole thread
projection. Adds tests for blank project and provider instance ids,
thread read errors and project store failures failing as sessions errors.
@tomkaczocha
tomkaczocha merged commit a50605f into main Oct 15, 2026
@rawphp
rawphp deleted the t3/factory-issue-5 branch October 27, 2026 23:52
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.

Adapter: find a project, launch a thread and read its status in-process

2 participants