Skip to content

QM-31: Bootstrap the factory source clone separately from the work repo - #1036

Merged
pcapriolo-yc merged 2 commits into
qm-29-port-factory-loopfrom
qm-31-s18516
Sep 10, 2026
Merged

QM-31: Bootstrap the factory source clone separately from the work repo#1036
pcapriolo-yc merged 2 commits into
qm-29-port-factory-loopfrom
qm-31-s18516

Conversation

@pcapriolo-yc

@pcapriolo-yc pcapriolo-yc commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Closes QM-31.

Changes

Why this matters: The factory loop started its coding-agent wrapper from the subject
repository it was about to work on, and pointed the wrapper's factory-source directory at
that same place. The wrapper, its workflows and its factory tools live in a different
repository, and nothing ever put them in the sandbox, so a work run launched a script that
was not there. The failure was silent: the run degraded to a "no pull request" verdict,
burned an attempt, and gave the operator no signal about the real cause.

What changes:

  • Before a work run starts, the factory materialises its own control plane in the sandbox:
    a shallow clone of yc-software/qm-yc at a pinned branch into /workspace/qm-yc. A
    sandbox that already holds that checkout fetches and re-checks-out the pinned branch
    rather than re-cloning, so a warm sandbox converges instead of running a stale copy.
  • The wrapper is launched by absolute path out of that checkout while the working directory
    stays the subject repo, so the wrapper still clones IO_REPO_CLONE_URL and runs
    IO_REPO_SETUP_CMD exactly where it did before.
  • IO_FACTORY_SOURCE_DIR now names the control-plane checkout and IO_REPO_DIR the subject
    repo — two distinct directories, as the wrapper's contract requires. Every other variable
    in the wrapper's environment is unchanged.
  • A bootstrap that fails or times out now fails the run loudly with
    factory_source_bootstrap_failed and never starts the wrapper. The GitHub token
    authenticates the clone through the process environment only, so it appears in no command
    string and in no checked-out git remote.

Acceptance stories:

  • The factory works a ticket on a cold sandbox: before, the wrapper was started from a path
    that did not exist; now the control plane is cloned into the sandbox first and the wrapper
    runs from it.
  • The factory works a ticket on a sandbox it has used before: the existing checkout is
    fetched and moved onto the pinned branch, so it never re-clones and never runs the control
    plane it first happened to fetch.
  • The wrapper gets the control-plane directory and the subject-repo directory as two
    different values, so its control-plane snapshot step finds the files it copies instead of
    aborting.
  • An operator looking at a run whose clone failed sees factory_source_bootstrap_failed,
    where before the item quietly burned an attempt and reported "no pull request".
  • The factory clones with its GitHub credential: the token travels only in the process
    environment, so no logged command string and no persisted git remote carries it.

Test Plan

  • npm run typecheck
  • npm run lint
  • node --test test/loop-factory-process-work.test.ts test/loop-factory-effects.test.ts test/loop-factory-wiring.test.ts
  • npm run test:all — the three remaining failures reproduce identically on a clean
    origin/main worktree and are unrelated to this change.
  • Manual: drove the bootstrap and the wrapper launch against a real local git remote
    standing in for the control-plane repository — cold clone, warm fetch and checkout, a
    flipped branch pin, and a missing branch all behaved as intended.

Proof it works

Proven at runtime: the pre-fix code was first reproduced failing, then the fixed code was
driven against a real git remote and a faithful wrapper stub, covering all five acceptance
stories plus adversarial probes. Captured output is archived with the run, not in the diff:

  • story-all-before-prefix-baseline.txt — .io-agent-qm-31/screenshots/
  • story-all-after-runtime-proof.txt — .io-agent-qm-31/screenshots/
  • adversarial-probes.txt — .io-agent-qm-31/screenshots/

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@pcapriolo-yc

Copy link
Copy Markdown
Contributor Author

Semantic disposition audit at 06836c6 — 1 acceptance-criterion drift, no code change

Dispositions audited: none exist. At this head the MR carries 0 top-level notes, 0 discussion/review comments and 0 reviews — the timeline holds only committed plus two labeled events. There is no human, Hex, Foreman, Bugbot or agent finding on this MR, and therefore no dismissal, evidence-backed or otherwise, to correct.

Final diff vs. the acceptance stories and the acceptance report. All five stories hold, re-verified against this head:

  • S1src/loops/factory/process-work.ts:40 renders IO_FACTORY_SOURCE_DIR from factorySourceDir (was repoDir); test/loop-factory-process-work.test.ts:279-280 pins the full absolute command bash ${SOURCE_DIR}/${FACTORY_WRAPPER} ${TICKET} with opts.cwd === REPO_DIR; the !== inequality is asserted on its own at test/loop-factory-process-work.test.ts:233 and test/loop-factory-effects.test.ts:524.
  • S2/S3src/loops/factory/effects.ts:35-43 is one startProcess command whose [ -d …/.git ] guard picks clone or fetch --depth 1 + checkout -f FETCH_HEAD; effects.ts:161 runs it on the preflight handle inside the finally { teardownWarm } at :162-163, so there is no third provision round trip, and awaitProcessExit at :110 makes the wrapper wait on it.
  • S4effects.ts:96-101 carries the credential only as GIT_CONFIG_* in opts.env; the command string holds the plain https://github.com/yc-software/qm-yc.git, so the persisted origin is token-free.
  • S5effects.ts:115 throws factory_source_bootstrap_failed: exit <code> before any wrapper start, and :161 gates the bootstrap on preflight.ok so factory_preflight_failed still precedes it. The report's justification for the detail suffix over the story's bare string is concrete — it names the sibling factory_preflight_failed: missing_tools: … in the same module — and stands.
  • Re-run at this head: loop-factory-process-work + loop-factory-effects + loop-fire → 104 pass / 0 fail; loop-factory-wiring → 1 pass / 0 fail.

Drift (1). DONE MEANS states "Only four files change: src/loops/factory/process-work.ts, src/loops/factory/effects.ts and their two test files." The final diff changes five — the fifth is test/loop-fire.test.ts.

Evidence that it is forced, not scope leak, read from the pre-change file: at the merge base, test/loop-fire.test.ts:615 returned the constant processId: "p1" for every startProcess, so the new bootstrap poll and the wrapper share one id and the defaultRead at :598 feeds the bootstrap the wrapper's WORK_STDOUT. That is exactly the hazard the stories raise as prerequisite D5, in a second test file the story text did not enumerate. Line :862 additionally pinned ops.slice(0, 5) to the pre-bootstrap call sequence. The diff re-keys the fake per processId and widens that assertion to ops.slice(0, 7), which now covers the bootstrap's own startProcess/readProcess — strictly more assertion; nothing was weakened or deleted.

Recorded rather than reverted: reverting test/loop-fire.test.ts leaves the suite red, and the change stays inside the same test layer the criterion already admits. No production file outside the two named modules is touched.

Verdict: no code change required; no disposition to re-adjudicate.

Auth failures, a missing branch, and a missing repository all exit 128.
The error now carries the last lines of git's output with the token
masked, so an operator can tell them apart from the run's failure reason.
@pcapriolo-yc
pcapriolo-yc merged commit 8cd8dd7 into qm-29-port-factory-loop Sep 10, 2026
16 checks passed
@pcapriolo-yc
pcapriolo-yc deleted the qm-31-s18516 branch September 10, 2026 22:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant