Skip to content

checkpoint_push_remote: reject pushurl-only remotes during election - #5

Draft
entire[bot] wants to merge 341 commits into
mainfrom
fix-checkpoint-push-remote-election-2360
Draft

entire[bot] wants to merge 341 commits into
mainfrom
fix-checkpoint-push-remote-election-2360

Conversation

@entire

@entire entire Bot commented Sep 11, 2026

Copy link
Copy Markdown

https://entire.io/gh/MuskanPaliwal/cli/trails/4

This draft pull request was opened by Entire after CI was requested for the linked trail. Feel free to edit the title or body — the link above is what keeps the trail and PR connected.

ashtom and others added 30 commits September 13, 2026 23:58
Entire-Checkpoint: 01M2ECEB687V1RPFCT12TJ06J1
Entire-Checkpoint: 01M2EE0S4GJ8TPCXQ6D8KTBGWP
Entire-Checkpoint: 01M2EFKAY90V79JV8S53QXATCY
Entire-Checkpoint: 01M2EHA4WBB9J2YSNKDB2PPPQE
Entire-Checkpoint: 01M2EK27T5WEXV83TC3SC3XDBF
Entire-Checkpoint: 01M2ENTH8GCT0EWB4B32MER546
The session-ID fix does not need this. The store's location comes from
the agent's own GetSessionDir, not from checkpoint metadata or a hook
payload, so it is not the untrusted input this PR is about. It arrived
as a bypass of the ValidateWritePath preflight added here, and that
preflight is itself point-in-time defense-in-depth against an external
subprocess that can race it.

The cost was real and had no opt-out: a ~/.claude or ~/.codex managed by
chezmoi/stow/yadm failed whenever the link was the deepest component
that existed yet, and a session directory symlinked onto another volume
failed outright, on reads as well as writes. allow_symlinked_agent_dirs
covers worktree-relative config dirs only, never the home session store.

openRoot and openRootForWrite go back to what main does. Everything that
fixes the reported vulnerability stays: ValidateSessionID in SessionFile,
Name containment, the write-name component check, the resume guards, the
external ref preflight, and every symlink refusal inside the store. Two
tests pin the decision in the other direction so it is not re-added by
the next review round.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2JMBPGYQBDGMVPKP4N3Q0C5
Entire-Checkpoint: 01M2P2CE7CZJHX19NGX6HMHQNG
Decode the cell snake_case schema while preserving CLI JSON keys. Follow opaque cursors for finding pages, summaries, lookups, and snapshots without overriding their filters with CLI defaults.

Entire-Checkpoint: 01M2P3G14M44M5A5FDS9ZY93SS
Decode snake_case stream fields and payloads, recognize review.started, and preserve dotted event names. Cover the documented public events, raw JSON envelopes, and reconnect cursors on both cell route forms.

Entire-Checkpoint: 01M2P45TD5Z6B66322N98392D7
Use discussion routes, envelopes and review linkage in the cell client
and CLI JSON. Update help and output while preserving command verbs,
resource IDs, user-authored text and unrelated output fields.

Keep raw watch events in the public cell form through reconnects and
remove obsolete thread event rendering.

Entire-Checkpoint: 01M2P6JYP8KPGE95JEEB3XVP7S
Entire-Checkpoint: 01M2PA4G6BX7ABH4Q5PXP9KMG6
Entire-Checkpoint: 01M2PCMGZ36EHNNVJVYS4SF1GV
`project grant add` and `repo grant add` now take the grantee as an optional
argument. Omitted on a terminal, they offer the members of the owning org who
do not hold the target yet, then collect a role per grantee — so one run can
add a reader and an admin together. `--role` given fixes every row, rendered as
a huh note so the pairing is shown but not editable.

The pool is the owning org's membership minus whoever already holds the target,
and both halves matter. Org membership grants no data access: the authz schema
gives an org member project `view` but neither `read` nor `write`, and
entiredb's TestPermissionCascade pins that an org owner can manage a project yet
neither push nor pull its repos. So every org member is a real candidate until
they hold a grant. Project access DOES reach the project's repos, which is why
the repo pool also subtracts the `project:<name>` rows ListRepoGrants returns
beside the direct ones — offering access someone already holds through the
project would grant a no-op.

`org grant add` gets no picker and keeps its two required arguments: everyone
eligible is by definition absent from the only list there is. There is no user
search or global account listing anywhere in the API, so the subtraction is
client-side. Org membership is also the only pool whose entries are directly
grantable — project and repo grant rows carry a grantee ULID and no provider
identity, with no reverse lookup, while a Membership carries the
provider:handle resolveGranteeProvider already takes.

--role stops being a cobra-required flag, because cobra enforces those before
RunE and a role that cannot reach RunE cannot be prompted for. The guarantee it
provided — an omitted role never reaching validation, a lookup, or the API —
moves into the RunE, which settles both non-interactive refusals from the
command line before any request: an unanswerable prompt must not cost a lookup.
Four no-candidate conditions get four messages, since "every member already has
access" is a claim about members that must not be printed when there are none,
and an account-owned project is the absence of a pool rather than an empty one.
Each still names the explicit provider:handle form, because a grantee never has
to be an org member.

No candidate is auto-picked even when only one is eligible, unlike
selectPlacement, which returns a lone cluster without prompting: this writes
access. A failure part-way through a batch stops and reports what landed, in
--json as well as text, since those grants are real and cannot be undone here.

Verified against a throwaway org rather than a production one: all four
refusals, and the pool tracking a project grant being added and revoked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2N9T1MNH9Z7FQQGHPMCM3G0
huh writes a form to stderr in its TUI mode but to stdout in accessible mode,
and `grant add` can be asked for --json, so under ACCESSIBLE the prompts landed
in the middle of the JSON a caller was parsing. The pickers now build their
forms through promptForm, which pins output to the command's stderr — where a
prompt belongs whenever stdout carries data, and what the TUI mode already did.

TestPickRoles_PromptsStayOffStdout fails if a form is ever built without it,
checking both the process's stdout and the command's.

The form tests get simpler as a result: they capture the command's stderr
buffer instead of swapping os.Stdout, leaving stdin as the only global swap.

Verified live in accessible mode: selecting a grantee and taking a role emits
only clean JSON on stdout, with every prompt on stderr.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2NAGZ2CGRE7WKCPZTEQJ5QC
`remove` now takes the grantee optionally on all three targets, offering who
holds the target now. Unlike `add` this includes org: everyone eligible to be
added to an org is by definition absent from the only list there is, but the
members to remove ARE that list, so the asymmetry belongs to add rather than to
the org.

The pool is filtered to rows revoking would actually remove, both filters for a
condition a real listing carries. The `owner` row is the owning org, which holds
the target through the authz schema rather than a grant, so there is nothing to
revoke and the typed route could not address it. A repo listing also carries the
project's grants as `project:<name>` rows; revoking one of those at the repo
level answers "no such grant; nothing to revoke", verified against a live repo
before writing the filter, so offering it would be offering a no-op. Removing
that access means removing the project grant.

Project and repo rows are revoked by account ULID through the typed-id route,
which needs no handle lookup and cannot be defeated by a handle renamed since
the listing. So grantCandidate now carries a `ref` to act on and a `label` to
show, and the confirmation names the label: picking github:alice off a list and
being told an opaque id was revoked is not an answer to what was asked. A ULID
the user typed keeps its existing "account <id>" wording. Org membership has no
typed-id route, so its rows are addressed by handle, and a member whose handle
the server did not resolve is left out rather than offered and then refused.

Picked and typed grantees share one revoke path, so the routing cannot drift
between them. The non-interactive refusal is settled from the command line
before any request, and names the forms that target accepts — only project and
repo take a ULID.

Verified live against the fixture org: the repo pool offering only the direct
grant while excluding the inherited holder and the owner row, the org pool
listing all five members, and a revoke reporting the handle it showed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2NEHJAXNTW97TW6EDWHH06D
`grant remove` now asks before revoking. The prompt comes after the picker, so
it names what was actually chosen, and it is one prompt for the whole set: a
single grantee reads as a sentence, several are counted in the title and listed
under it. A prompt that summarises a set as a bare number is not a confirmation
of anything.

It asks only where there is a terminal, and there is no flag to bypass it. This
is deliberately not what `delete` does — that refuses without --force rather
than acting unprompted — and the difference is blast radius: a deleted repo is
gone, while a revoked grant is one command from being restored. So a script that
has always revoked without being prompted keeps working, and no --force has to
exist to let it. The prompt is for the hand on the keyboard, above all for a set
just picked off a list, and that hand is by definition at a terminal.

The gate is shared rather than cloned: confirmDestructiveAction holds the logic
and a destructiveAction supplies the three words that differ, with
confirmControlPlaneDeletion reduced to its delete-worded wrapper. The two verbs
therefore cannot drift, and a third would be a value rather than a copy.

revokeConfirmed joins removePicker as a seam, since forcing a terminal in tests
puts the confirmation in play too. Both halves of the rule are covered:
declining revokes nothing and exits cleanly, and a non-terminal run is never
asked a question it cannot answer.

Verified live: a scripted remove still revoking unprompted, the prompt rendering
after a pick, "y" revoking, and anything else leaving the grant in place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`entire repo grant add /et/pgrant/rgrant` refused with "every member of the org
owning repo ... already has access to it" on a repo whose org showed three
members, none of which could be picked — and exited 1 while doing it.

**The pool rule.** It subtracted everyone holding the target, and a repo
inherits its project's grants, so a project covering the org emptied it. The
rule is one step narrower: subtract members with a DIRECT grant, keep the ones
who hold it only through the project. A direct holder has nothing to add here
(changing their role is the typed form's job, `grant add` being an upsert —
verified live, where re-granting a writer as reader moves them to reader). An
inherited holder has no grant on THIS target, so granting one pins the role on
this repo instead of following the project's, which is a real action.
`directHolders` is the single place `source` is read for this.

**Exit codes.** Having nobody to add is not a failure. Nothing went wrong,
nothing is left for the user to fix, and in the common case the state they
wanted already holds — the same reasoning that already makes revoking an
already-revoked grant a success rather than a 404. The three empty-pool cases
report and exit 0, as does confirming an empty selection, which used to exit 1
with no message at all. What stays an error is a command that cannot run as
asked: an account-owned target, which has no membership list anywhere, and a
non-interactive run with no grantee.

**Messages.** Those two errors spell out the provider:handle form because the
user is stuck without it. The empty-pool messages state the fact and stop: there
is nobody left to add, so a worked example with an invented handle would answer
a question nobody asked. Under --json the reason moves to stderr and stdout gets
the empty array, so a caller parsing stdout is never handed a sentence.

Candidates are plain handles and every role row defaults to the least-privileged
role. An intermediate version labelled inherited rows with the role and project
they came from and defaulted their row to it; both are detail about a grant the
user is not editing, in a list whose question is only "who".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR review found four things, all real.

**Nobody selected crashed the add picker.** Confirming an empty multi-select
fell through to the role screen, and `huh` indexes a group's first field
unguarded, so a group with no fields panics rather than merely looking odd —
verified, not inferred. The remove side had the same shape and asked "Revoke 0
grants on …?" before revoking nothing. Both now stop where the selection is
made. My tests missed both because they stub the picker seam, so the real
`runGrantPicker` never ran; the new ones go through it.

**A grantee is a provider-qualified handle and nothing else.** Account ULIDs are
internal ids the interface should not ask anyone to copy, so `remove` no longer
takes one and its help no longer offers one. `ensureGranteeIsHandle` is the
single check, run before the target is resolved so a grantee that cannot work
costs no lookup, and shared with `resolveGranteeProvider` so there is one
message. The typed-id revoke route stays, reached only by the picker, which
reads the id off a listing: candidates now carry `byID` instead of the route
being sniffed from the ref's shape. That sniffing was also latently wrong — a
non-ULID grantee id would have taken the handle path — and it is what let a
pasted ULID reach the route at all. The "account <id>" wording goes with it,
since every label is now a handle.

**`add --json` is always an array.** It was an object or an array depending on
how many grants landed, so a caller had to inspect the result to learn which
shape the run had chosen. One entry per grant, including none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Always emitting an array made `grant add` the only mutation in the CLI
answering `--json` with one. `org create`, `project create` and every other
single mutation emit the bare wire object, and so did `grant add` itself
through runCoreMutation before the picker existed, so a script parsing
`.status` broke silently for no gain.

The shape now follows the INVOCATION rather than the outcome. A grantee named
on the command line is one mutation and emits the object. The picker grants a
set and emits an array, one entry per grant that landed including none, so a
caller reading that form never has to branch.

Keying on the outcome is the version worth naming as wrong, since it is the
tempting middle: an array only once more than one grant landed makes a picker
run that granted one person indistinguishable from a typed one, so the shape
depends on what the user happened to click.

Found by Entire Gates on the PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four review fixes against the entire-api RFD-026 branch (ENT-2466).

trail watch dropped thread.created/message_added/message_edited instead
of following their rename. entire-api snake-cases each event segment and
maps the thread domain to discussion on read, so those rows now arrive as
discussion.*; without the cases they fall through to the generic default
line. Two of the three never matched before either: stored rows spell them
thread.messageAdded, so this is the first release where they can work.

trail_review_json.go leaked discussion_id/discussion_message_count into
CLI output, where every sibling key is camelCase. The presenter exists to
keep the CLI's JSON independent of the cell schema, so these become
discussionId/discussionMessageCount.

The camelCase request test was renamed to assert snake_case but lost its
negative guard; restore it in the new direction.

CheckResponse now carries the RFC 9457 request_id through to HTTPError and
falls back to title when a problem body has no detail, so an API failure
names the id support needs instead of echoing raw JSON.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2QD7EXBTJ9N2N0QHN99MAMX
An unqualified grantee was answered with `account "asdasdasd" must be a
qualified handle like "github:alice" (or a ULID)` — pointing at a form the
command had just stopped accepting, and calling the grantee an account.

That message belongs to parseQualifiedHandle, which is shared with `project
create --owner`, where a ULID genuinely is accepted and the sentence is true.
So ensureGranteeIsHandle borrows the split RULE and not the sentence: a grantee
that is not provider-qualified now gets a grantee-worded error naming the one
form it takes, and the owner ref keeps its own.

Both halves are pinned, including that no grantee error may end up offering the
ULID form again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	docs/development/cli-conventions.md
Soph and others added 30 commits September 24, 2026 09:55
Antigravity's two truncated-step warnings logged through
context.Background(). logging.log resolves its logger from the context and
falls back to slog.Default() when there is none, and nothing outside tests
calls slog.SetDefault — so those warnings went to the stdlib text handler on
stderr, not to .entire/logs/entire.log where the integration says they are
recorded. ExtractModifiedFilesFromOffset runs inside prepare-commit-msg and
post-commit condensation, whose stderr the user sees, so a transcript with
truncated replace_file_content steps (measured at roughly 0.8% of such calls)
printed unstructured `level=WARN msg=...` lines into `git commit` output,
without session_id.

The method had no context to log through, so the interface gains one. The
other ten implementations ignore it; only the call sites, which all had a
context in scope, and the antigravity implementation change behaviour.

Mechanical apart from those two log calls, and separable from the rest of
this branch if it is better carried by the prompt-resolution follow-up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M396KXK11H1DRDMZ9R131KPR
The import guard built its per-package allowlist with
`append(allowedPrefixes, scopedPrefixes[pkgName]...)` inside the loop. That
is safe only because allowedPrefixes is a composite literal whose cap equals
its len, forcing a copy. Build it any other way — make() with spare capacity,
or one more appended entry — and append writes the antigravity-scoped
internal/flock entry into its backing array, handing every package checked
afterwards the exception scopedPrefixes exists to contain, with no test
failure to signal it.

slices.Clone makes that independent of how the slice is built, and hoisting
it out of the inner loop drops a re-allocation per import.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M396KYVETYG282B0SZZZ0ZSV
Answer repo access with one verb, `repo grant`
…s9oyo

Add --nearest flag to measure latency and auto-select closest cluster
stderrURLRe ended the match on a quote as well as whitespace, so that
git's own `to 'https://…'` quoting read cleanly. A password containing a
quote then split the match inside the credential:

  in     error: cannot fetch https://user:pa'ss@github.com/o/r now
  out    error: cannot fetch <unparseable>'ss@github.com/o/r now

leaving the tail of the password in text that reaches stderr, logs and
pasted transcripts — the one thing this redaction exists to prevent.
Only whitespace ends the match now; a trailing quote swept into the path
is the harmless side of that trade.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M39ADA184XW2WXHQB3B7PPT2
main removed `repo access list` in favour of `repo grant list` and added
`repo clone --nearest`, so the conflicts are mostly this branch's rename
meeting those two changes:

  - cluster_list, the --json ledger and the deprecated-spelling ban take
    main's removal of `access list` alongside this branch's rename and
    its two new banned spellings.
  - repo clone keeps main's --nearest/--cluster refusal and latency
    picker, called through the resolver signature this branch trimmed.
  - repo_mirror_ref_test drops the access-list test main deleted with the
    command it covered.
  - the conventions doc keeps this branch's rewritten placement
    paragraph, minus the `access list` sentence.

main's new code also names the deleted verbs in four comments
(placement_latency, repo_clone, repo_grant, the doc); those now name the
commands that exist.

The e2e lifecycle test crossed the maintidx threshold with this branch's
three `remote add` phases on top of main's additions, so those phases
move to remoteAddPhases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four conflicts, all from main reworking `repo grant` under this branch's
picker. Both sides kept everywhere; nothing was dropped.

grantTarget: main added unwritableRef and listBranch alongside this branch's
candidates/holders/ownerNotOrg, so the struct takes all of them and main's
grantListBranch type comes with it.

Both `add` and `remove` leaves: main set Args back to ExactArgs(2), which is
the pre-picker shape, and added PreRunE: refuseUnwritableRef(t). Kept the
picker's optional grantee (RangeArgs, and the variable Args on add) with
main's PreRunE — refuseUnwritableRef reads args[0], which RangeArgs(1, 2)
still guarantees. A /gh/ mirror ref is now refused before the picker opens,
which is the right order: the answer is about the repo the user named.

repoGrantTarget: same field list on both sides; leastRole joins listBranch and
unwritableRef.

repo_clone.go: main added withLatencyProbe and probeableHosts in the block
this branch emptied by moving placementPromptTerminal into the shared
promptTerminal. Kept main's two helpers and dropped the re-introduced pair —
selectPlacement auto-merged onto runPromptForm and has no other caller.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M39C17YX1WNYTB4D22TSRF37
revokeConfirmed returned (false, nil) when the command context was already
cancelled, and the caller exits 0 on that pair — so a signal landing just
before the confirmation revoked nothing, said nothing, and reported success.
The same silent no-op the confirmation gate was reworked to avoid.

It now returns an error wrapping ctx.Err(), which is what the rest of the CLI
does: plugin_confirm.go checks either side of its form and login.go goes out
of its way to record the signal before returning context.Canceled, because
main.go matches on it to re-raise the signal — that is where Ctrl+C's quiet
130 and a broken enclosing shell loop come from. confirmControlPlaneDeletion's
nilerr skip is the outlier this had copied, and still carries the bug for
`delete`.

Checked on both sides of the form, and the far-side check comes before the
form error is inspected: handleFormCancellation treats context.Canceled as a
clean abort and would otherwise report a signal as an answer. An abort from
inside the form stays an answer — Esc or Ctrl+C at the prompt is the user
saying no.

The caller's `if err != nil || !proceed { return err }` is split in two. The
compressed form returned a known-nil err to mean success, which is what hid
this; runControlPlaneDelete, whose gate this copied, already kept the branches
apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M39DF985VCE97GT55ERBWZCM
Interactive grantee pickers for grant add and grant remove
main moved the placement picker's terminal plumbing into a shared
runPromptForm (from the grant-picker work), deleting
placementPromptTerminal and openPlacementPromptTerminal. Taking that
side leaves this branch's picker-routing test driving a seam that no
longer exists, so it moves to openPromptTerminal — that test is the only
coverage of the routing, which is why it was ported here in the first
place.

repo_remote_url_test.go stays deleted; main keeps editing the tests of a
command this branch removes.

main's new text also uses `repo remote url` as the motivating example
for "stdout is captured by design, so prompts go to the terminal", in
utils.go and in the conventions doc. The rule outlives the command, so
both now name a --json read piped into a parser instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
repo remote: one `add <remote-name>`, and a primary default for scripts
…06ac

test: characterize Git CLI behavior before go-git migration
Entire-Checkpoint: 01M39JB34RP7Q4EFVK1ADWYVBD
entireio#2548 removed `entire repo remote url`, but a native-mirror lifecycle
phase still called it, so the control-plane suite failed on main. The
primary-default half was already covered by the `remote add` phase; the
unknown-cluster refusal moves there, run inside the clone.

The suite only ran on pushes to main, which is how this slipped through.
It now also runs on same-repo PRs to main (forks get no secrets). Slack
still notifies only for main.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M39N8CRAJM2WR38XJ0K4M862
Entire-Checkpoint: 01M39NPY3D7VYE65CZAJW5X33S
…-split

coreapi: land vendored Core spec refresh before entireio#2561
Dependabot PRs come from same-repo branches, so the fork guard let them
through, but their runs get only Dependabot secrets and would fail at
login while holding the shared account's queue.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M39PGZS1S9FK09YTYYNN84GV
e2e controlplane: drop removed `repo remote url`, run suite on PRs
…ion-2360' into fix-checkpoint-push-remote-election-2360

# Conflicts:
#	cmd/entire/cli/strategy/checkpoint_sync_remote.go
main removed Gemini CLI support while this branch was adding Antigravity, so
every conflict was the same collision: both sides edit the lists of supported
agents. Resolved by taking main's list and restoring Antigravity to it,
keeping main's other additions (Codex, where it had been added alongside).

Substantive resolutions:

- generateSummary: kept this branch's sliceByAgentMetric delegation and took
  main's geminilegacy call inside the fallback switch. The branch's inner
  Gemini case still named the geminicli package, which main deleted, so it
  could not have compiled either way.
- agent/geminicli: accepted main's deletion of the package. This branch had
  only touched it to thread ctx through ExtractModifiedFilesFromOffset.
- e2e/testutil/repo.go: kept RepoPreparer/RepoCleaner, dropped the gemini-cli
  branch — setupGeminiTestHome no longer exists.
- antigravity_test.go: the "wrong agent" sentinel named AgentNameGemini, a
  constant main deleted. Now AgentNameClaudeCode. This one is a semantic
  conflict git resolved cleanly and only vet caught.

Verified on the merged tree: go build, go vet (including GOOS=windows),
gofmt, mise run lint (0 issues) and mise run test:ci, all green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M39RV7DGXVXPKTJRE1KGKEZ8
Legacy records normalize to IDLE on list and isOrphanedSessionState never
deletes IDLE, so they now remain until StaleSessionThreshold expiry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M39S21QM1G5K5G7EGNG5AKAZ
Both comments described the secret as shared with the gemini-cli E2E leg.
That leg no longer exists — main removed Gemini CLI support — so the only
consumer is agy's API-key mode, and "shared" now points a reader at an agent
that is not there.

Comment-only; the secret, the workflow and the fallback behaviour are
unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M39S2GZKTWB0KJFM3TQ86THA
isOrphanedSessionState re-spelled "ended" as a phase check, so an IDLE
state with EndedAt stamped was kept for 7 days (the finalizer skips it
via IsEnded). Use the canonical predicate and drop test leftovers from
the owner-liveness version.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M39SKJBAWGZCRTKYPYZ8ZHP2
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M39SWN2NJNA43TF5Z9P5457V
feat(agent): add Antigravity (agy) CLI agent
…urvive-hooks

Hooks stop deleting live idle sessions
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.