Repository navigation
feat(cloud): set up a project's machine from the first message of its cloud session - #414
Conversation
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>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Thread transfer impact
This comment will update automatically after the next completed run. |
There was a problem hiding this comment.
💡 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".
| MACHINE_SETUP: { | ||
| defaultEnabled: false, | ||
| requiresRestart: false, | ||
| description: | ||
| "Offer to set up a project machine when a project has none. Changes apply immediately.", |
There was a problem hiding this comment.
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 👍 / 👎.
| /** | ||
| * 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), |
There was a problem hiding this comment.
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 👍 / 👎.
| /** | ||
| * 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), |
There was a problem hiding this comment.
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 👍 / 👎.
…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>
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>
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>
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
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.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-setupruns it on request.t3team.change_request.publish(MCPt3_change_request_publish, groupmutation.change_request): commits only the listed paths to a branch, pushes, and opens a change request throughSourceControlProvider. 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 orgit amhere. 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.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_RECIPEand its recipe importswaitForAny, so its submodule pin has to move to this merge before it lands.Test plan
vp test run: publisher (16), kickoff (7), flag registry and layering, cloud session presentation and machine choice, wait-any server delivery,handlesAny,signalAny,offertscclean for server, web, contracts, project-context, SDK, runbook-core; additive guard passesGitVcsDriverCore.test.ts"preserves renames, unusual paths, modes, and binary statistics" fails, identically on base556812db02, so it predates this branch🤖 Generated with Claude Code