Skip to content

feat(cloud): set up a project's machine from the first message of its cloud session - #414

Merged
johnnyelwailer merged 25 commits into
mainfrom
feat/machine-setup
Oct 8, 2026
Merged

johnnyelwailer merged 25 commits into
mainfrom
feat/machine-setup

Conversation

@johnnyelwailer

@johnnyelwailer johnnyelwailer commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

A cloud session for a project with no machine definition now sets the machine up only when the user's task needs one, from their first message, without making them wait or answer a question first.

What it does

  • New cloud session for a project with no definition and nothing rejected starts a setup session (NEXI_FF_MACHINE_SETUP, default off). A rejected definition disables the row and names the file. The old "Set up a machine" offer is gone.
  • Kickoff recipe (t3team-kickoffRecipe.ts, T3CODE_KICKOFF_RECIPE): the first message the user types on the bootstrap thread launches the named pack recipe with { firstMessage }. It never blocks the user's turn, runs once per thread (a live run continues and an ended one stays ended after a restart), and /machine-setup runs it on request.
  • t3team.change_request.publish (MCP t3_change_request_publish, group mutation.change_request): commits only the listed paths to a branch, pushes, and opens a change request through SourceControlProvider. It never touches the user's checkout: the commit is built on a temporary index and the branch ref is moved by compare-and-swap. It refuses a branch checked out in another worktree, being rebased in any worktree, or mid-merge, cherry-pick, revert or git am here. A remote it cannot read is an error. Commits are signed when the user's config signs commits; hooks do not run. Re-running adds to the same branch, local or remote-only, and returns the open change request.
  • waitForAny (runbook-core/handlesAny.ts, t3team-sdk.signalAny.ts, migration 079): a workflow parks on several signals and resumes on whichever arrives first, replay-deterministically. The recipe uses it to follow a change request's merged, closed, checks and review signals together.
  • Team secret names go on the session grant and on a standby claim.

Review

Three refute-first rounds on Nexplore (high); Codex hit its spend cap and Cursor could not start. Round 1 found the wait-any primitive, path validation and idempotency sound, and that the first publish implementation switched the user's checkout. Rounds 2–3 hardened the replacement. Every finding is fixed, apart from these accepted limitations: if the branch moves during a publish the caller gets git's raw error and a re-run fixes it, a moved pack directory can re-fire the kickoff after a restart, and an any-wait shows only its first branch in status. The Codex bot's comments on the first push (flag registry test, contract over 200 lines, "Start another" on a setup session) are fixed in ce934a3c62.

Rollout

The distribution half is https://nexplore.ghe.com/pj/nexi-distribution/pull/637. Merge and publish this first; that PR's session workflow sets T3CODE_KICKOFF_RECIPE and its recipe imports waitForAny, so its submodule pin has to move to this merge before it lands.

Test plan

  • Focused vp test run: publisher (16), kickoff (7), flag registry and layering, cloud session presentation and machine choice, wait-any server delivery, handlesAny, signalAny, offer
  • Guard and signing tests fail with their fix removed
  • tsc clean for server, web, contracts, project-context, SDK, runbook-core; additive guard passes
  • GitVcsDriverCore.test.ts "preserves renames, unusual paths, modes, and binary statistics" fails, identically on base 556812db02, so it predates this branch
  • Live setup session: docs-only first message ends quietly; a build task proves the machine, opens the change request and follows it

🤖 Generated with Claude Code

Phil J and others added 2 commits October 7, 2026 09:32
When a project has no machine and the flag is on, Run on checks the project out on the host instead of building a devcontainer. Team secret names from the definition go with the session, including a claimed standby.

Co-authored-by: Cursor <cursoragent@cursor.com>
The catalog file is camel-cased, so the t3team- prefix rule does not treat it as fork-owned. A new flag has to be registered there.

Co-authored-by: Cursor <cursoragent@cursor.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T07:39:23.228607Z 21a645b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for 9783b94.

This comment will update automatically after the next completed run.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21a645ba2e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +14 to +18
MACHINE_SETUP: {
defaultEnabled: false,
requiresRestart: false,
description:
"Offer to set up a project machine when a project has none. Changes apply immediately.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the feature-flag registry test

Adding this third definition makes apps/server/src/t3team-featureFlagStore.test.ts fail: its exact registeredFeatureFlags assertion still expects only MAIN_REPOSITORY and NEXI_STATE_DIR. Update that expectation and add the new flag's focused layering coverage so the backend test suite can pass.

AGENTS.md reference: AGENTS.md:L18-L21

Useful? React with 👍 / 👎.

Comment on lines +140 to +144
/**
* Start a host checkout of the project so an agent can write its machine definition.
* Refused when the flag is off, or when the project already has a definition.
*/
machineSetup: Schema.optional(Schema.Boolean),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Split the contract before crossing the line cap

This addition takes t3team-cloudSession.ts from 192 to 202 non-empty lines, newly exceeding the repository's hard 200-line limit for prefixed production files. Extract a focused portion of the cloud-session contract into a sibling module rather than landing the file over the blocking design cap.

AGENTS.md reference: AGENTS.md:L29-L33

Useful? React with 👍 / 👎.

Comment on lines +105 to +109
/**
* True when the session checked the project out on the host so an agent can write its machine
* definition. Absent means not.
*/
machineSetup: Schema.optional(Schema.Boolean),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve setup intent when starting another session

When a setup run reaches failed, the UI exposes its existing “Start another” action, but useCloudSessionController handles every terminal session with onCreate() and therefore sends neither projectId nor { machineSetup: true }. Since the new session record retains only this boolean and not the project identity, clicking the action provisions an unrelated plain cloud session instead of retrying setup; retain enough context to recreate the setup request or suppress that generic action for setup failures.

AGENTS.md reference: AGENTS.md:L41-L47

Useful? React with 👍 / 👎.

Phil J and others added 18 commits October 7, 2026 16:52
…sion

New cloud session now reads the project's machine discovery and does one of three things: start (the server pins a definition when there is one), refuse while a committed definition needs fixing, or ask once whether to set up a machine. "No" starts a plain session and is remembered per project, with a quiet way back. The separate setup row is gone.

A server can start a pack recipe on its bootstrap thread by itself (T3CODE_STARTUP_RECIPE), so a setup session begins its work without a client attached. The recipe launch moved out of the HTTP route into launchRecipeWorkflow, which the route and the startup path share.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
New cloud session no longer asks. A project with no machine starts a setup session, and that session's server launches the kickoff recipe (T3CODE_KICKOFF_RECIPE, was T3CODE_STARTUP_RECIPE) on the first message the user types on its bootstrap thread, with that message as an argument. Nothing runs at boot, so a quick question never waits on a devcontainer build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The recipe reads the project id from the change request it opens, so a /machine-setup launch from any thread works the same.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
awaitAny parks on several branches through ONE composite handle: the sent
entry pins the branch list, and the first-write-wins resolved line
{ index, reply } is the selection, so a replay returns the same winner no
matter how many branches fire later.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…trol providers

The `draft` flag has to pass through the provider interface, every provider and their CLIs;
GitManager owns the open-change-request lookup the publish tool reuses; the tool catalog core
gains the `mutation` kind.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…onally as a draft

GitManager.openChangeRequest returns the change request already open for a head branch, or
creates one against the given base (default: the repository's default branch) and reads it
back. runPrStep's body-file handling moves into a shared helper; its behaviour is unchanged.

createChangeRequest gains an optional `draft`, honoured the way each host marks drafts:
`gh pr create --draft`, `az repos pr create --draft true`, Bitbucket's `draft` field, and the
`Draft:` (GitLab) / `WIP:` (Forgejo) title prefixes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…_request.publish)

A neutral host tool, in the `mutation.change_request` group and off by default. In the calling
thread's checkout it switches to or creates the branch from HEAD, commits ONLY the listed
repository-relative paths with the user's git identity, pushes to origin, and opens the change
request through the repository's source-control provider. When one is already open for the
branch, it pushes and returns that one instead. Result: {url, number, repository, provider,
branch, commit, projectId}; projectId is the thread's project, for watching the change
request's signals.

The logic lives in the T3TeamChangeRequestPublisher Effect service. The broker binding (MCP
`t3_change_request_publish`) and a workflow body's getTools() bridge only decode, call it and
map errors. A body reaches it as `getTools().t3team.changeRequest.publish(...)` (the SDK
camelCases id segments) when both its meta.capabilities and the recipe's allowedToolGroups
name the group.

Failures are specific sentences: invalid paths (empty, absolute, `..`, `.git`, outside the
repository, unmatched), nothing to commit, a branch with diverging history, a rejected or
unreachable push, and not being signed in to the remote or the host.

t3team-workflowHostDraftTools.ts becomes t3team-workflowHostTools.ts now that it carries more
than drafts; every importer is updated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	apps/server/src/t3team-thread-recipe-workflow-routes.ts
handle.on(signal, { key }) describes a wait without waiting; waitForAny
journals one signal.waitAny handle over every branch and resolves with
{ index, signal, payload }, the payload narrowed per index. Only branches
minted by handle.on in the same run are accepted, so each keeps its
source:<name> gate. The verb is bound into workflow bodies, so a recipe can
import it from @t3team/sdk and pass the static audit.

The run host gains offer(): a wake that waits out in-flight drives instead
of being dropped, then decides against the state they left. Event delivery
needs it once several sources feed one parked run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A waitForAny park records its whole branch list on the run row
(watch_any_json); the broker drains the oldest open inbox entry across
the branches before parking, and delivery and the park drain match an
event against every branch through one shared rule, replying with the
winning branch's index.

Delivery now wakes through the run host's offer(), so an event that
races another wake answers the park the run is actually on, and an
event nobody claims is bridged to the inbox for the next wait instead
of being dropped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Over the real run repository, journal, signal store and delivery port: a
parked waitForAny resumes when a non-first branch fires and journals that
branch as the winner; the event of a branch that lost answers the next
iteration's wait, never the settled one; boot rehydration drains the
inbox across branches by arrival order. Delivery-port unit cases cover
the re-decide and decline paths.

Also extends the two migration tests that pin the run table's columns and
the migration ledger, and formats the new code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…arks on

Explains why waitForAny is one handle whose resolved line is the winner,
why that winner must be journaled rather than chosen at replay, and what
happens to the event of a branch that lost.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The replay slot flag is now set and cleared inside the tracked drive, so
an offer that sees no drive in flight can never find the slot still held
by one that just finished. A test races two offers woken by the same
drive: each claims the slot in turn and answers its own park.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Register MACHINE_SETUP in the feature-flag registry test and cover its
  layering: off by default, on from the DB, the env wins either way.
- Move the cloud-session failure reasons and error to a sibling module, so
  the contract stays under the 200-line cap; the old module re-exports them.
- An ended setup session no longer offers "Start another": its record keeps
  no project, so the action would have started an unrelated plain session.
- Say what a setup session does now: the first message decides whether the
  machine is set up.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ckout

The setup recipe publishes from the checkout the user's own agent works in.
Switching that checkout to the change-request branch and resetting its index
would unstage the user's work and send their next commit onto the machine
branch. The commit is now built on a temporary index from the branch's tip
(or HEAD for a new branch) and the branch ref is moved by compare-and-swap;
the checkout's branch, index and working tree stay as they were. A branch
only the remote has is continued, and an update after the checkout moved on
lands on the change request's branch instead of being refused as diverged.

The kickoff recipe now runs once per thread: after a server restart a live
run continues and an ended one stays ended (listLiveByLaunchThread gains
includeEnded).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…uess the remote

Review round 2 of the plumbing publish:
- A branch checked out in another worktree, or the checkout's own branch
  mid-merge, cherry-pick or revert, or being rebased, is refused: update-ref
  skips the checks git branch -f makes. Reuses the driver's worktree parser,
  now exported.
- Whether the remote has the branch is read with ls-remote; a remote that
  cannot be read is an error instead of "no such branch", so a failed fetch
  no longer invents a local branch that every later publish trips on. The
  remote branch is fetched straight into the local ref, without touching
  FETCH_HEAD.
- The commit is signed when the user's config signs commits. Hooks do not
  run; the module says so.
- The kickoff's "ran before" check is a pure function tested both ways.
- The tool says a directory path commits everything under it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A rebase detaches HEAD, so the worktree list shows no branch for it, and
update-ref moved the branch under the rebase; its --continue then failed.
Each worktree's own rebase state is read now. git am on the checkout's own
branch is refused too, and the remote probe and fetch never wait on a
credential prompt.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@johnnyelwailer johnnyelwailer changed the title feat(cloud): offer to set up a project machine feat(cloud): set up a project's machine from the first message of its cloud session Oct 8, 2026
@github-actions github-actions Bot added size:XXL and removed size:L labels Oct 8, 2026
Phil J and others added 2 commits October 8, 2026 16:29
Keeps both MACHINE_SETUP and WORK_PROFILE_CHOOSER, renumbers the
waitForAny migration to 106 (main took 105 for PackDocuments), wraps the
publish tool in McpToolAccess.actsAsCaller, and ports main's script host
into the recipe workflow launch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Phil J and others added 3 commits October 8, 2026 16:35
Tests load it by path, which knip cannot follow, like the other host-tool fixtures.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Knip's exports check flags them; MACHINE_SETUP_FLAG_ENV had no reader at all.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@johnnyelwailer
johnnyelwailer merged commit 322c725 into main Oct 8, 2026
20 of 21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant