Repository navigation
Conversation
4dc72aa to
c43cac8
Compare
Opus 5.5 Review (fork) — ✅ no blocking findingsReviewed |
Design Review (Opus 5.5, fork) — ✅ PASSDesign-level review of Design-Verdict: PASS No design-level blocker. The feature solves a real problem, the hermetic one-spawn populate removes the probe-then-populate race, and the prune and refresher costs are stated in the description. Notes
[DESIGN-REVIEWED] 0d3cb9c Description read[DESCRIPTION-READ] 57691abc0644dc256f962259d140a38a90ea5c9b6469bde1c01ed659dcb12ef6 That sha256 names everything this verdict read: the PR description, plus the 1 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with |
UX Review (Opus 5.5, fork) — ✅ PASSUX-level review of UX-Verdict: PASS Nothing blocks. The 14 committed screenshots cover every new row state and control, but no first-time reader has seen them, because this fork lane has no blind read. Pushing the branch to this repository runs one. Notes
[UX-REVIEWED] 0d3cb9c Description read[DESCRIPTION-READ] f6d314467a115dbd57cb8bbcf8571b1dcd69bc341e2a86e9afc66964b99fb930 That sha256 names everything this verdict read: the PR description, plus the 17 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with |
First Principles Review (Opus 5.5, fork) — ✅ PASSPremise-level review of First-Principles-Verdict: PASS Nothing blocks. One note: a rider in the refresher now writes the operator's own Notes
What this change shipsInventory (10 items) — 9 justifiedIntent: from Dev Fleet, check out the newest stable release as a worktree you can pod or make live, in one click. This is an ADDITION.
[FIRST-PRINCIPLES-REVIEWED] 0d3cb9c Description read[DESCRIPTION-READ] 42f55b126506182377e2e1bd9680f671c64ff50caedf3e69c9e266c940691070 That sha256 names the PR description this verdict read. Recompute it with |
GPT 6.1 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
c43cac8 to
9cf097a
Compare
9cf097a to
d7edf2d
Compare
d7edf2d to
c98578d
Compare
|
Rebased: |
52411b8 to
31cf360
Compare
|
Rebased: Fork GPT 6.1 lane, second consecutive infrastructure failure, different step each time: on |
|
Opus — Confirmed against real git before changing anything: in a clone with the default The courtesy fetch now names |
|
GPT F1 — Confirmed: The notice is now its own component, |
|
Rebased: Bundle Size Gate on Local on the rebased tree: 1505 dev-fleet backend tests + ratchets + facade guard, flake8, mypy (30 files), isort, docs-lint, feature-map, tsc, eslint, 248 DevFleet vitest, 15 i18n checks + 1035 i18n tests base-scoped, body 446 words, guard single-commit-on-base. The fork GPT lane reached a verdict on |
|
Opus — Both arms reproduced with real git before any change: against a sha256 repository the sha1-shaped common dir answered Object format — fixed as suggested. Partial clone — refused up front, not backfilled. Of the two remedies offered, refusal is the proportionate one: the populate runs on the no-network tier on purpose (lazy fetching is exactly the kind of implicit network call the hermetic design keeps out), and a promisor remote is checkout state the operator chose and can change. Tests (4 new, two real-git): |
|
Rebased: |
|
Rebased: The round on |
|
GPT F1 — Correct, and a defect in the fix I landed one head earlier. Reproduced with real git: a |
|
GPT F2 — Correct: |
|
Rebased: |
|
GPT F1 — Correct: the previous head fixed the backend half (the failed listing became the row's |
|
GPT F2 — Correct, and this closes the span rather than patching around it: the previous round made the probe read |
|
Rebased: |
|
Rebased: |
|
Rebased:
Local: 1511 dev-fleet + ratchet tests, black/isort/flake8/mypy clean, docs-lint, feature-map gate against |
|
First Principles PASS on Item 6 ( Item 3 (prune withholds |
|
UX PASS on Evidence gap ("version unknown" badge): closed on Raw git stderr after the framing text (06, 07, 12): accepted and deferred, not changed here. The framing sentence ("The stable release channel could not be resolved") is the cause in the user's words; the git text is the detail, kept because it is what the operator pastes or hands to "Ask the agent", which every ErrorNotice on this page carries. Translating each git failure class into a next step ("check your connection") is a per-class table the page does not have for any other fleet error either; adding one only for the channel row would make this row's notices differ from the rest of Dev Fleet. Same disposition as on Frame 11 passes git's own advice ("use 'add -f'…") through: accepted and deferred. That text is git's stderr for a dangling worktree registration, rendered by the same ErrorNotice. The step the user can take IS on the page: Prune merged lists the registration, and the notice's Ask-the-agent button hands the exact text to the agent. Rewriting git's advice would need the create path to classify stderr by message, which the backend deliberately does not do (it classifies only its own refusals: |
|
Design PASS on
Refresher fetches base +
|
|
Opus no blocking findings on
|
|
Thanks for picking this up again. Before reviewing the code, I'd like to understand the user problem better. Who is the user here, and how often do they need a stable checkout from Dev Fleet? Today someone who needs to repro on stable can run I'm asking because the change is large for one row (8k+ lines, two new ~700-line modules, a 530-line spec section), and it changes existing behavior for everyone who uses Dev Fleet, not only people who click Create:
If the need is real, could a smaller version work? For example, a "create worktree at tag" action, without the background tag mirror and without the prune change. |
Dev Fleet gains one row, `release-channel-stable`: a real git worktree detached at the newest published stable release, so a shipped build can be podded or made live beside unreleased branches. Create is the whole job; the pod controls already drive any fleet row. Resolution reads a private, pruned tag mirror (`refs/dev-fleet/release-tags/*`), never `refs/tags/`, orders bare `vX.Y.Z` tags by semver, and refuses any oid the remote does not advertise for that tag. `CHANNEL` is a single `"stable"` constant: nightly tags nothing and insider ordering has no stated rule. The populate is hermetic. The staging worktree is filled by one `git read-tree -u --reset <oid>` spawn whose `GIT_COMMON_DIR` is a fresh, agent-owned common dir (0700; a two-line config; `objects/info/alternates` naming the repository's object store), with `GIT_DIR`, `GIT_WORK_TREE`, `GIT_CONFIG_NOSYSTEM=1` and `GIT_CONFIG_GLOBAL=/dev/null` set. The repository's own `.git/config` and `info/attributes` are never opened by that spawn, so no `filter.<name>.*` driver or `core.worktree` redirect written there, at any instant, can reach it. That retires the probe-then-populate sequence kirodotdev#10066 carried and the race between its two spawns. The row says what it knows: a version badge carrying currency, `n/a` for PR and Behind, a placeholder with Create, `not on a release` for a detached tree on no tag, and the reserved-name explanation when a branch checkout holds it. Prune never offers `empty` for a detached worktree. Closes kirodotdev#23344 Closes kirodotdev#12342
|
Rebased: On |
|
GPT on F1 ( |
|
Opus no findings on |
|
UX PASS on Raw git stderr in the notices (06, 07, 11, 12): accepted and deferred, unchanged from the two previous rounds. The framing sentence is the plain cause ("The stable release channel could not be resolved"); the git text follows as the detail because it is what "Ask the agent", present on every notice, hands over. A per-failure-class rewrite table ("Can't reach GitHub — check your connection") exists for no other Dev Fleet error, and the backend deliberately classifies only its own refusals, never git's prose. Frame 14's notice repeats itself: accepted and deferred. The text is framing + Frame 07 shows a red notice when only the tip lookup failed: rebutted. The tip failing means the row cannot say whether the checkout is current, which is the row's reason to exist; it also means Create (on the placeholder variant, 06) is disabled. "latest unknown" in the badge alone would put the only account of the failure in a pill with no keyboard-reachable detail and no "Ask the agent", which is the Badge-title-vs-ErrorNotice point an earlier UX round made and this design followed. The badge already stops asserting (bare |
|
First Principles PASS on Item 6 (the courtesy |
|
Design PASS on |
Problem / Motivation
Goal: From Dev Fleet, check out the newest published stable release as a worktree you can pod or make live, with one click.
You cannot click through a shipped release from Dev Fleet. The fleet lists feature
worktrees, and every one of them is unreleased code. To see what
stableactuallyshipped you leave the dashboard, find the tag by hand, and make a checkout yourself.
Dev Fleet manages git checkouts, and a git checkout has no release channel.
This was built once as #10066 and never merged: its one open gate was a
security-fenced race between the probe that inspected git config and the populate
that ran after it (#12342). It was closed in favour of #12356, which depended on it
and was itself closed unmerged.
Why it matters
A bug report says "broken in stable". Answering it means running stable. Today that
is manual work outside the tool that exists to manage checkouts, so it mostly does
not happen, and a fix gets tested against
maininstead of against the build thereporter is on.
Not a goal
nightlytags nothing, so its row would bemain;insidertags resolve, but ordering-insider.Nagainst-rc.Nneeds a rule this repository does not state.CHANNELis one"stable"constant.$KIROCREW_HOME/channel.Backwards compatibility
Compatible: the
/fleetpayload gains one key andworktreeskeeps its shape; one route is added; nothing that reads today's payload or routes changes meaning.Removes nothing: the diff adds a module, a route, a row and a prune exemption, and narrows no existing contract.
Two costs operators see: Prune merged no longer preselects any detached worktree, so a hand-detached tree stays until removed by hand; and every refresher cycle also runs a best-effort, unforced, tag-only fetch (
refs/tags/*:refs/tags/*, never--tags, which would drag the branch refspec in) into the repo's ownrefs/tags/, so a release tag's name is one the operator can act on.What changed (motivation → approach → change)
Dev Fleet grows one row,
release-channel-stable: a real worktree detached at thenewest published stable release. Pods and Make Live already drive any fleet row, so
a shipped build boots the same way a feature branch does.
release_channel_pin.pyvX.Y.Ztag by semver, read from a private pruned tag mirror (refs/dev-fleet/release-tags/*), never fromrefs/tags/worktree_ops_release.pyworktree_opsfacade: refuse if the reserved name is held, fetch the mirror, resolve, ask the remote for exactly that tag's oid and refuse any other,worktree add --no-checkout --detachat a staging path, populate hermetically, adopt withworktree movefleet_state.pyrelease_channelkey: adopted, placeholder, unpublished, taken, error; two registered checkouts carrying the reserved name adopt nothing and the row's error says so, since the page binds channel treatment to rows by name; a tombstoned removal of the channel's worktree demotes the key to its placeholder shape in the same reconciliation that drops the rowhttp_api.py,server.pyPOST /api/release-channel/create, audited with the reserved name as its static target. It answers202 {run_id}as soon as the create is running: the fetch, add, populate and move carry timeouts in the minutes and the app proxy drops a request at 30 s (routes_proxy._PROXY_TIMEOUT), so an awaited create could never deliver its result. The page polls/api/run?id=and readsresultoff the record; a refusal decidable without a run (exists,ambiguous_name,busy) is still an immediate 409 with itscode, andbusynames the running create so a reloaded page attaches to it. A second audit entry records how the run endedDevFleetPageReleaseChannel.tsx,DevFleetPageRow.tsx,devFleetPageModel.tsn/afor PR and Behind, a placeholder with Create,not on a releasefor a detached tree on no tag andversion unknownwhen the release could not be read, and the reserved-name explanation when a branch checkout holds itworktree_ops.pyemptyfor a detached worktree, so the pin cannot be auto-reaped; the 60-second refresher fetches the tag mirror beside the base branch every cycle (tags are small refs, transferred once), so the badge follows a new release between Creates instead of only inside oneruntime.py,app.json,appManifest.ts_start_task_run: an in-process coroutine tracked as a run in the same_RUNSregistry and_ACTIVE_RUNSshutdown set as the subprocess runs, with the record's birth shared through_admit_run_locked;_error_detail: the strip-scrub-bound of a git stderr, spelled once in the module already classified for that output path, so the new owners add no redaction call site andsecurity_posture.pyis untouched (every other string in a create's result — a path, a ref — is scrubbed by/api/run, the registered egress, as it leaves the process); and the app's feature lineThe populate is hermetic. Checking a tag out runs whatever
filter.<name>.*driver the repository's
.git/confignames, and that file is writable by anythingwith the agent's filesystem access;
core.worktreefrom the same file redirectswhere the files land. #10066 probed for both and then populated, two spawns with a
writable file between them. Here the staging worktree is filled by one
git read-tree -u --reset <oid>spawn whoseGIT_COMMON_DIRis a fresh,agent-owned common dir:
mkdtemp(0700), a two-lineconfig, aHEAD, andobjects/info/alternatesnaming the repository's object store, withGIT_DIR,GIT_WORK_TREE,GIT_CONFIG_NOSYSTEM=1andGIT_CONFIG_GLOBAL=/dev/nullset. Thatspawn never opens the repository's
.git/configorinfo/attributes, so nothingwritten there, at any instant, can name a driver it executes or move where it
writes. No probe is left to race. Measured on git 2.54 with a
requiredfilter.evil.smudgeand acore.worktreeplanted: a plain populate ran the driver;the hermetic one did not, and wrote only into the staging path. What remains is the
operator's own config acting on later ordinary reads, as on every fleet row.
flowchart LR subgraph Before A1[want to run stable]:::ctx --> B1[leave the dashboard]:::removed B1 --> C1[find the tag, clone by hand]:::removed end subgraph After A2[want to run stable]:::ctx --> B2[Create on the stable row]:::added B2 --> C2[detached worktree at v0.11.0]:::added --> D2[Pod / Make live]:::ctx end classDef added fill:#DCFCE7,stroke:#16A34A,color:#14532D,stroke-width:2px classDef removed fill:#FEE2E2,stroke:#DC2626,color:#7F1D1D,stroke-dasharray:4 3 classDef ctx fill:#E0F2FE,stroke:#0284C7,color:#0C4A6E linkStyle 0,1 stroke:#DC2626,stroke-dasharray:4 3 linkStyle 2,3,4 stroke:#16A34A,stroke-width:2pxThe mirror is the integrity story for what the row shows. The operator writes
refs/tags/too, and a localv99.0.0would sort newest; resolution reads only thepruned mirror (fetched with
--no-prune-tagssaid explicitly, so afetch.pruneTagsin
.git/configcannot turn the prune on the operator's own tags), and Createadditionally refuses an oid the remote does not advertise for that tag. The row's
refnamesrefs/tags/<tag>only while that local tag is the resolved commit;a diverged or absent local tag gets the mirror ref instead. Adoption is bracketed:
worktree moveinto an existing directory nests rather than refuses, so thedestination is re-checked under the lock before the move and the landing's admin dir
is verified after it. A successful adoption bumps a fleet epoch so a rebuild already
in flight re-reads rather than publishing the pre-create snapshot.
The row says less when it knows less. Currency lives on the version badge
(
0.5.0 · latest 0.5.3, or the bare version at the tip); PR and Behind readn/a,so Behind keeps one meaning for every row that has one. While a placeholder shows,
the count reads
N worktrees · 1 to create. The spec(
docs/system-specs/modules/dev-fleet.md§ Release-channel worktrees) carries eachmechanism's reasoning; the feature map gains the row.
website/capture/devfleet-release-channel.*is the screenshot harness;website/src/i18ncarries the copy in every locale (en.manual.jsonthe source,en.context.jsonthe translator notes,pluralKeys.jsonthe plural key).Tests
test/test_dev_fleet_release_channel_pin.py(63): mirror refspec,--pruneand--no-prune-tags, one fetch at a time, bare-tag semver ordering,MirrorOverflow,resolve_tip()states (ok / unpublished / error), the publishedreffor an agreeing, diverged and absent local tag,worktree_state()(detached, on a branch, unreadable HEAD, and asymbolic-refprobe that timed out or failed with a readable HEAD, which stays unknown rather than being adopted as detached).test/test_dev_fleet_release_channel_ops.py(83), real git where the claim is about git: the Create sequence and each refusal code; provenance through an annotated tag's peel line; the hermetic populate with arequiredsmudge driver andcore.worktreeplanted before Create (test_the_hermetic_populate_runs_no_repo_declared_filter_and_writes_only_into_staging); the same populate against a sha256 repository, which the private common dir reads as sha256 because it declares the store's object format (the sha1-shaped control cannot resolve one oid); a--filter=blob:noneclone is refused aspartial_clonebefore the fetch, the add and the populate, while a hydrated clone that keepspromisor = falsebehind is not; a--points-atlisting that fails to read becomes the row's error plusversion_unknown(the badge then reads "version unknown", never "not on a release") and the partial-clone remedy, run verbatim against the fixture clone, clears the refusal; the private common dir is minimal, points at the real object store, and is deleted after success, failure and cancellation; a failed create never force-removes the lane path; a directory appearing at the lane path is refused before the move, and a move that nested into a foreign directory is undone by the nested path and reported; a successful create marks the fleet adopted and a build that predates an adoption reads git again; prune never offers a detached tree and still offers a branch checkout holding the reserved name; two checkouts sharing the reserved name adopt neither and probe neither; every emitted refusal code is classified for the route; the two owners carry no redactor call site, git stderr is scrubbed byruntime._error_detailand every other result string by/api/run; the route answers the run id before the create finishes, the record carries the result, a second start names the run in flight — including while the run body holds the worktree lock — and a tombstoned removal demotes the channel key to its placeholder, the terminal audit entry splits outcomes as the response did, and a task run that raises, is cancelled, or is refused at shutdown ends like a subprocess run.test/test_dev_fleet_component_boundaries.py: the new owner joins the facade's ownership DAG and the new route joins the audited-route list.website/src/test/DevFleetPageReleaseChannels.test.tsx(40): every row state above, then/acells, the count label, the Create click posting{}and refetching, the outcome read from the run record (success toast, a failure the run reports, abusy409 attached to rather than reported, a run the gateway no longer knows reported as lost), refusal kept on the row as anErrorNotice.Determinism proof (testing-conventions § Proving a determinism fix): the two backend files green 20/20 at
-n0, 10/10 at-n 4 --dist loadgroupwithtest_dev_fleet_app_guards.pyandtest_dev_fleet_component_boundaries.pyas co-tenants, and 164/164 (withtest_dev_fleet_app_fleet_cache.py) in a shuffled order (seed=23370). The frontend file green 10/10 repeats and shuffled (--sequence.seed=23344); its fixture clock is pinned (FIXED_NOW). Flake-pattern ratchet: zero new shapes against the merge-base.Manual verification
read-tree -uin a staging worktree ran the plantedfilter.evil.smudge(marker file written); theGIT_COMMON_DIRpopulate did not,core.worktree's redirect dir stayed empty, and the files landed in the staging path with HEAD at the oid.tsc -p tsconfig.app.jsonclean; eslint clean on every changed frontend file;i18n:check19/19 PASS againstorigin/main; bundle-size gate within budget with no ceiling change; docs-lint and the feature-map check at 0.kirodotdev/KiroCrewremote and a pod of the resulting worktree. The real-git tests stand in for the first; the pod path is unchanged.Screenshots / video
Committed under
temp-screenshots/release-channel/(fork PR;--attachis unavailable to a read-access account). Captured bywebsite/scripts/capture-devfleet-release-channel.mjs, which asserts each scene's state before shooting.More states
Related Issues
Closes #23344
Closes #12342
Prior review record: #10066 (closed, unmerged). Its three UX advisory copy items (#12355, closed) are folded into this copy.
Checklist
feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert: ...)