Skip to content

ISS-13963: Place every fleet writer and prove the cloud path - #424

Merged
shafty023 merged 9 commits into
mainfrom
feat/iss-13963-placement-gate
Nov 11, 2026
Merged

shafty023 merged 9 commits into
mainfrom
feat/iss-13963-placement-gate

Conversation

@shafty023

@shafty023 shafty023 commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • ISS-13963, all criteria. AC6 shipped in ISS-13963: Validate a pushed commit in a cloud worker #423; this PR adds AC1 to AC5, AC7 and their AC8 tests, and fixes the AC6 validate path its first real run exposed.
  • AC1: board.mjs <orchDir> placement <ticket> --evidence <file> runs the existing check (vibe-core decideCloudPlacement) and records its decision and reason. board.mjs writer --tickets <a,b> (or an open unit naming the writer) refuses a ticket with no decision, and so does unit --writer when it folds a ticket into a running writer or hands a unit to one. Unreadable evidence is refused.
  • AC2: a cloud-placed ticket is refused for any board writer until placement <ticket> --keep-local <reason> records the operator's reason. New evidence drops an old reason. The board page's Placement section shows each decision and reason.
  • AC3: board.mjs status prints PLACEMENT <writer> <ticket> <cloud|local|none>: <reason> per writer ticket (informational, exit unchanged).
  • AC4: Cloud workers states one rule: record the placement before every writer start or fold, whatever the off-by-default routing says. An imported cloud task given a local writer row records --keep-local "cloud import <task id>". The check's and cloud worker's vibe-core locations are named there and in the resume (state.md) and setup steps.
  • AC7: the hard-coded environment id is gone. New vibe-core cloud-environment.mjs resolves the request's claudeEnvironment, then VIBE_CLOUD_CLAUDE_ENV, then CLOUD_CLAUDE_ENV in the fleet config.env. The RemoteTrigger API has no environment list or GitHub endpoint, so neither is discovered. Named errors, each with its fix:
    • CLOUD_ENV_MISSING, raised before any API call.
    • CLOUD_ENV_INVALID.
    • CLOUD_ENV_UNKNOWN: create refused (401/403) while the routine list answers, or 404.
    • CLOUD_AUTH_REFUSED: the list is refused too.
    • CLOUD_RUN_NOT_CREATED: a fired run never appears. It quotes the routine's last run and names connecting GitHub.
  • New cloud-worker.mjs check reports the environment and its source, API reachability, whether an account routine already runs in that environment, and that GitHub cannot be checked before a run. It exits 1 on a failure.
  • No hard-coded repository (Daniel: support "whatever the dev has authorized with claude.ai", not two repos):
    • The repository is the request's repository, else the worktree's HTTPS or SSH github.com origin. Errors: CLOUD_REPO_NOT_GITHUB (non-GitHub or no origin), CLOUD_REPO_INVALID, CLOUD_REPO_MISMATCH.
    • The allow-list and every closedloop-ai/claude-plugins default are gone. Access is the claude.ai GitHub connection; an unreachable repo surfaces as CLOUD_RUN_NOT_CREATED, naming the Claude GitHub App or /web-setup.
    • validate installs from the checked-out lockfile (pnpm, uv with --group dev only when declared, npm, yarn, or nothing) unless the request names install. The worker is no longer asked to report its install: one real run reported the wrong one.
    • Codex needs the request's codexEnvironment (CLOUD_CODEX_ENV_MISSING). Dispatch still needs an ISS ticket; validate does not.
  • Cloud provenance (an Agent launched with isolation: remote once ran locally and reported cloud):
    • The validate brief prints the host, the CLAUDE_CODE_REMOTE marker (confirmed by a cloud probe), git remote get-url origin and the commit.
    • parseValidationResult refuses a result with missing provenance, no marker, or another repository or commit.
    • Cloud workers now says cloud work never goes through isolation: remote, and that every cloud report states its host, repository and commit.
  • pr-facts.mjs takes collision PRs from the session's own repository (its origin), not always symphony-alpha. One github.com remote parser (github-remote.mjs) replaces the copies, including pr-create-guard.mjs's. vibe needs vibe-core 1.0.4, and the dist bundles are rebuilt.
  • Out of scope: vibe-preflight.sh still runs vibe only in symphony-alpha or claude-plugins checkouts. ISS-13446 covers that. env-ctl, prototype-share and vibe-sessions keep overridable symphony-alpha defaults for workflows that exist only there.
  • AC6 fix: validate could never read a result. remoteTrigger kept only the get_run_log header line, and the result pattern required the marker to start a line, but the condensed log folds the final message onto one line. The log is now returned, and the marker is matched anywhere on its line; the commit, lane and clean-tree checks are unchanged.
  • AC5 proof: details in the first PR comment.
  • Versions: vibe 1.1.80, vibe-core 1.0.4, VIBE_CORE_MIN 1.0.4.

Breaking changes

  • board.mjs writer and unit --writer refuse a ticket without a recorded placement. A Claude cloud dispatch with no configured environment is refused (CLOUD_ENV_MISSING) instead of sending a hard-coded id. Both are the ticket's intent.

UI Feature Flag

  • None. Plugin scripts and instructions only.

Test plan

  • Local, one file at a time:
    • writer-placement 6/6, board 22/22, generic 2/2, instruction-words 5/5, vibe-core fleet-home 11/11, shared/vibe-core 5/5.
    • cloud-environment 6/6 and cloud-repository 8/8 (new), cloud-worker 6/6, vibe-triage 35/35, pr-facts 61/61, handoff-inventory 56/56, pr-create-guard 8/8, plugin-version-bootstrap 4/4, review-class-guards 6/6 (VIBE_CLOUD_CLAUDE_ENV listed as configuration). Python pin and SKILL contract tests: 54 pass.
  • AC5 on the final code, real account, main 397210d: exit 0. Repository resolved from the SSH origin; provenance host vm, CLAUDE_CODE_REMOTE true; agent-guard 6/6; clean tree; 101 s wall.
  • Mutation checks, each reverted guard turned a named test red:
    • 10 placement guards (writer gate, cloud refusal, keep-local, status, the check as source, page section, added ticket, stale reason, unit fold, unit hand-off);
    • 10 environment guards (hard-coded fallback, precedence, malformed id, create naming, 401 split, run-not-created, check 401, environment use, id sent, HTTP status);
    • 2 log-parsing fixes;
    • 9 repository guards (SSH remotes, non-GitHub refusal, allow-list, dev group, uv rule, override, Codex environment, no-ticket validate, clone URL);
    • 5 provenance guards (check, marker, repository, commit, brief), and pr-facts' fixed repository.
  • Not run locally: vibe-bench reply-checker.test.ts (version literal); CI runs it.

board.mjs placement records the existing check's decision (vibe-core's
decideCloudPlacement) for a ticket from its placement evidence. A new board
writer names its tickets and is refused for one with no decision, or one
placed cloud until the operator's keep-local reason is recorded. Status
lists each writer ticket's placement and reason, and the board page shows
every decision. The fleet-orchestrator Cloud workers rule now says the
placement is recorded for every writer start, independent of the
off-by-default runtime routing, and the resume and setup steps name the
check's location in vibe-core.
…ent-gate

# Conflicts:
#	CHANGELOG.md
#	plugins/vibe/.claude-plugin/plugin.json
#	plugins/vibe/.codex-plugin/plugin.json
#	plugins/vibe/README.md
#	plugins/vibe/tools/python/test_pr_takeover_skill_contract.py
#	plugins/vibe/tools/python/test_vibe_ws8_load_contract.py
#	tools/vibe-bench/src/reply-checker.test.ts
…ent-gate

# Conflicts:
#	CHANGELOG.md
#	plugins/vibe-core/.claude-plugin/plugin.json
#	plugins/vibe-core/.codex-plugin/plugin.json
#	plugins/vibe/.claude-plugin/plugin.json
#	plugins/vibe/.codex-plugin/plugin.json
#	plugins/vibe/README.md
#	plugins/vibe/skills/fleet-orchestrator/SKILL.md
#	plugins/vibe/tools/python/test_pr_takeover_skill_contract.py
#	plugins/vibe/tools/python/test_vibe_ws8_load_contract.py
#	tools/vibe-bench/src/reply-checker.test.ts
The cloud worker no longer sends a hard-coded environment id. It takes the
request's claudeEnvironment, then VIBE_CLOUD_CLAUDE_ENV, then
CLOUD_CLAUDE_ENV in the fleet config, and refuses with a named error and
its fix when none is set, the id is malformed or not the account's, the
sign-in is refused, or a fired run never appears. cloud-worker.mjs check
reports each of these. validate now reads the worker's result from the
condensed run log, which its first real run could not.
@shafty023 shafty023 changed the title ISS-13963: Require a placement decision before a fleet writer starts ISS-13963: Place every fleet writer and prove the cloud path Nov 10, 2026
@shafty023

Copy link
Copy Markdown
Collaborator Author

AC5 proof: cloud-worker.mjs validate run end to end against the operator's account, with no local fallback.

Command: VIBE_CLOUD_CLAUDE_ENV=env_01ALDURMXf2BZUpvBm512QbA node plugins/vibe/skills/vibe/scripts/cloud-worker.mjs validate --request <file>
Request: repository closedloop-ai/claude-plugins, branch main, commit 397210d, model claude-sonnet-5-5, one lane agent-guard: node --test plugins/vibe-core/hooks/agent-guard.test.mjs.

Setup check before the run (cloud-worker.mjs check):

  • With no environment set: exit 1, CLOUD_ENV_MISSING with its fix; the API answers (3 routines).
  • With env_01ALDURMXf2BZUpvBm512QbA: exit 0, confirmed by an existing routine in that environment.
  • With the old hard-coded id: unverified; the check lists env_01ALDURMXf2BZUpvBm512QbA as the environment this account's routines use.

Run 1 (code before the log fix):

  • Routine trig_01NSvK1aJPamRU6waJD8cyri, session cse_01H58iadrhaJ9YNcyUMtmkzr.
  • In the cloud it checked out 397210d, agent-guard passed 6/6 with exit 0, and the tree stayed clean (17 s).
  • validate still exited 2 with "cloud validation returned no result line". This is the AC6 log-parsing defect fixed in this PR.

Run 2 (this PR's code), fresh dispatch:

  • Routine trig_01GCgb2szxfi7Mf6i6dJmWHy, session cse_01H58iaemW597doUgs6K7TnW, environment env_01ALDURMXf2BZUpvBm512QbA from VIBE_CLOUD_CLAUDE_ENV.
  • Reachability: create, connector clearing and run all returned HTTP 200. The sandbox was allocated in about 2 s and the private repository was cloned.
  • Wall time: 57.3 s from dispatch to completion against the 35-minute limit (cloud session 19 s; 141 s locally including the RemoteTrigger client calls).
  • Result: exit 0, {"commit":"397210d7b800e9996fdb3f2a7ac4d66287e0d6af","lanes":[{"name":"agent-guard","exitCode":0,"passed":true}],"passed":true}. The lane log shows 6 of 6 tests passing.
  • Changed nothing: git status --porcelain was empty in the cloud checkout.

The cloud worker takes the repository from the request or the worktree's
GitHub origin and refuses a non-GitHub origin with CLOUD_REPO_NOT_GITHUB,
instead of a fixed two-repository list and a claude-plugins default.
Validate's brief has the worker pick the install from the checked-out
lockfile unless the request names one, and Codex cloud needs the Codex
environment the request names.
@shafty023

Copy link
Copy Markdown
Collaborator Author

AC5, rerun on the final code (c9032c7), after the change that removes hard-coded repositories. Same command and environment; this time the request named no repository.

Run 3:

  • Routine trig_01GmqW5rNQT3256GWh8SicGn, session cse_01H58iagPrt2L5eEvU85AqBG.
  • The repository was resolved from the worktree's SSH origin. agent-guard passed 6/6, exit 0, and the tree stayed clean.
  • The worker rewrote the install-detection command instead of running it as written, piped it through tail, then reported "uv sync --frozen". Run locally on 397210d's tree, the same command picks uv sync --frozen --group dev. The result contract therefore no longer asks for the install, and the brief now says to run the command as one call, exactly as written.

Run 4 (final code):

  • Routine trig_01D7QMtxVMyegcEnZHH98C8s, session cse_01H58iaes5fjzCg772sxXn2x, environment env_01ALDURMXf2BZUpvBm512QbA from VIBE_CLOUD_CLAUDE_ENV.
  • The repository closedloop-ai/claude-plugins was resolved from the SSH origin. The detection command ran verbatim as one call, and uv installed.
  • Exit 0, {commit: 397210d7b800e9996fdb3f2a7ac4d66287e0d6af, repository: closedloop-ai/claude-plugins, lanes: [{agent-guard, exitCode 0, passed}], passed: true}. The log shows 6 of 6 tests passing, and git status --porcelain was empty.
  • Wall time: 56.3 s from dispatch to completion against the 35-minute limit (cloud session 20 s; 142 s locally).

…own PRs

A validate result must carry its host, the CLAUDE_CODE_REMOTE marker a
Claude cloud session sets, its origin and its commit; one without them,
or naming another repository or commit, is refused. pr-facts lists the
open pull requests of the session's own repository instead of always
symphony-alpha's. One github.com remote parser in vibe-core replaces the
copies; vibe needs vibe-core 1.0.4 for it.
@shafty023

Copy link
Copy Markdown
Collaborator Author

AC5 run 5, on b4fff6d's code, with cloud provenance.

  • The probe that found the marker: routine trig_01ThEYnvFwYT1tmrArtJwpPR, session cse_01H58iagkz7gAf7BninCDh7h. It printed variable names only. The cloud host is vm, and the environment sets CLAUDE_CODE_REMOTE, CLAUDE_CODE_REMOTE_SESSION_ID, CLAUDE_CODE_REMOTE_ENVIRONMENT_TYPE and the CCR_* family. A laptop sets none of these.
  • Run 5: routine trig_01DJehjykHBjbVKtZQe7oJSV, session cse_01H58iadg4NQ5RpeExsEcJMB, environment env_01ALDURMXf2BZUpvBm512QbA. Exit 0.
  • Provenance reported and checked: {host: vm, remote: true, origin: https://github.com/closedloop-ai/claude-plugins, commit: 397210d7b800e9996fdb3f2a7ac4d66287e0d6af}.
  • Repository: resolved from the SSH origin, with none named in the request.
  • Lane: agent-guard exit 0 (6/6).
  • Tree: clean.
  • Wall time: 101 s against the 35-minute limit.

@shafty023
shafty023 merged commit f1506e7 into main Nov 11, 2026
12 checks passed
@shafty023
shafty023 deleted the feat/iss-13963-placement-gate branch November 11, 2026 05:34
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.

1 participant