feat(ownership): make the native delegate the only agent ownership model - #31
Merged
Conversation
`linear tasks --agent <name>` filtered on an `agent:<name>` label and `linear tasks --board` grouped its columns by the same label. An issue delegated through Linear's own delegation UI never showed up in that agent's queue, and an issue merely tagged `agent:claude` did. Both now read the native `delegate` field; unowned means exactly `delegate` is null, and a leftover `agent:*` label is an inert ordinary label. Fallout of dropping the label model: - An unknown `--agent` now resolves against the workspace agent roster and exits non-zero instead of printing an empty queue. An unattended drain read a silent empty list as 'queue clear'. - `--label` composes with `--agent` again. It was suppressed only because ownership was itself a label and the two label filters fought. - `save_config` no longer kills a read command when the config dir is unwritable; a roster cache refresh is not the operation. `setup` still exits non-zero, because there persistence IS the operation. Adds `linear migrate-agent-labels` for the one-time move off the labels: dry-run by default, never overwrites an existing delegate (reports CONFLICT), never guesses at a suffix that is not a delegatable agent (reports UNRESOLVED), deletes each `agent:*` label once nothing carries it, and exits non-zero if anything was left behind. RUSH-2272
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
… counters Non-author review found four blockers in migrate-agent-labels, all confirmed with probes against the shipping code: - An issue carrying two agent:* labels was written twice from the same pre-mutation snapshot. Because labelIds is a full replacement, the second write resurrected the first's stripped label and overwrote the delegate with no CONFLICT and exit 0. Decisions are per issue now: labels that disagree are a conflict, labels that agree produce one write. - Label deletion was gated on a team-scoped issue scan, but list_team_labels also returns workspace-scoped labels. A label carrying ownership on another team's issues was deleted silently. Gated on a workspace-wide use count. - A failed issueUpdate was appended to conflicts but left in planned, so one issue reported as both migrated and conflicted. Counters now report writes that landed. - A failed issueLabelDelete exited 0, contradicting the fail-loud claim in the docstring, README, and CHANGELOG. It is a failure now. Two design corrections from the same review: - A dry run that found a blocker exited 1 and swallowed its own hint, and unresolved labels can never be cleared by this tool — so --apply exited 1 forever with no path to green. A dry run is an inspection: it exits 0 and names what needs a human. --apply still exits non-zero when anything is left. - The config-derived default agent no longer hard-exits when the roster does not resolve. get_agents degrades to [] on a transient error, so a users-query blip bricked the most-used command. A typed --agent still exits 1; the default warns and filters on the name as given, which matches anyway. Nine new tests cover every branch above.
…l guard Follow-up review caught a regression the previous round introduced, plus a hazard neither round had checked: - count_label_uses measured the LIVE state, but a dry run has written nothing yet — so every label the run planned to strip still had a carrier and was reported 'still carried by an issue outside this team'. Both halves were wrong: the preview documented in README went dead, and the reason named another team when the carrier was the issue listed three lines above. The gate now subtracts what this run clears, and the message distinguishes an unmigrated issue from a genuine outside carrier. - The carrier query omitted includeArchived. Verified against the live API that Linear's issues connection excludes archived issues by default, so a label carried only by archived work counted 0 and would have been deleted. - classify_agent_labels([]) raised IndexError. Unreachable from its one call site, but the docstring sells it as pure testable logic. - The config-default fallback assigned the raw name where resolve_agent_name would have stripped it, so a config of ' claude ' matched nothing. - A dry run whose carrier lookup failed still printed the resolve-the-conflicts hint, which named neither the API error nor anything a human could fix. The test harness hardcoded label_uses=0 against a fixture whose issue still carried the label — a state the real API cannot produce, and what let the regression pass green. Carriers are now derived from the fixture the way the API answers, and four tests cover the preview, the outside-carrier case, the unmigrated-carrier case, and a lookup failure. 73 tests.
Final review found a blocker the previous fix introduced. label_carrier_ids returned at most 200 ids with no truncation signal. That was safe while the caller asked 'is anything left?' — 1 > 0 kept the label. It stopped being safe when the gate became set subtraction: a PARTIAL page whose every entry happens to be one this run clears subtracts to empty and reads as 'nothing carries it'. Reproduced against the previous commit — 200 carriers in the scanned team, all migrating, plus 50 on another team; the API returns the first 200; the label is deleted, exit 0, no warning. Exactly the silent cross-team strip the function's own docstring says it exists to prevent, and includeArchived made it easier to hit by letting archived issues fill the page. It now asks for one more than the page size and keeps any label whose carrier list came back full, saying so. Also from that review: - The 'outside the scanned team' vs 'did not migrate' distinction was one dead branch and one wrong message: blocked_labels short-circuits every case that could reach the first, and an archived carrier INSIDE the scanned team was reported as outside it, because the team scan does not pass includeArchived. Both collapse into one statement that is always true. - The dry-run hint made a lookup failure swallow the resolve-your-conflicts guidance. They are independent conditions and both print now. 74 tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Behavior change —
delegatebecomes the only thing that owns an issue;agent:<name>labels own nothing.What was broken
linear tasks --agent <name>filtered on anagent:<name>label, andlinear tasks --boardgrouped its columns by the same label. So an issue delegated to Claude through Linear's own delegation UI never appeared in Claude's queue, and an issue merely taggedagent:claudedid — the tool disagreed with Linear about who owns the work.What changed
tasks --agent Xagent:Xdelegateis Xagent:*labeldelegateis nulltasks --boardcolumnsagent:*label suffixes--agent--labelwith--agentThe unknown-agent change matters most for unattended callers: a drain read the silent empty list as "queue clear".
--labelwas suppressed only because ownership was itself a label and the two label filters fought. Ownership is a field now, so the two are orthogonal.Also hardened
save_config: a roster-cache refresh on a read path no longer kills the command when the config dir is unwritable (it warns and continues).setupstill exits non-zero, because there persistence is the operation. This became reachable because a typo'd--agentforces a roster refresh.New:
linear migrate-agent-labelsOne-time move off the labels. Dry run by default,
--applywrites.agent:<name>label, then strips that label (other labels survive).CONFLICTand left byte-for-byte alone.UNRESOLVEDand the label is kept.agent:*label only once nothing carries it.Run result
Run from this branch against the live workspace. Titles redacted — this repo is public. Full unredacted capture:
yosemite-s0:/home/muqsit/.agents/artifacts/rush2272-cli-run.txtThat dry run is the migration doing its job on a real workspace: every remaining
agent:*label there is a machine name or a workflow flag, not an agent, so it refuses to guess and exits 1.Tests
22 new tests, 60 total, all stdlib
unittestlike the rest of the file. They drive the reallist_tasks,show_board, andcmd_migrate_agent_labels— only the two network edges (cycle resolution, the issue page) are substituted, so every line of ownership logic under test is shipping code. Covered: delegate filter, unowned rule, roster casing,--all, unknown-agent abort,--labelcomposition, board grouping with an inertagent:*label present, and the migrate/strip/conflict/unresolved/dry-run/label-delete paths.Ticket: RUSH-2272
Review round 2 (commit
d54bdd9)An independent non-author review found four blockers in
migrate-agent-labels, each reproduced with a probe. All fixed:agent:*labels on one issue → two writes from one snapshot; the second resurrected the first's stripped label and silently overwrote the delegate, exit 0CONFLICTlist_team_labelsalso returns workspace-scoped labels → a label still owning another team's issues was destroyedcount_label_uses)issueUpdatewas counted as both migrated and conflictedfailures=addedissueLabelDeleteexited 0, contradicting the fail-loud claim in the docstring, README, and CHANGELOGTwo design corrections from the same review:
unresolvedlabels can never be cleared by this tool,--applywould have exited 1 forever with no path to a green run. A dry run is an inspection: exit 0, and name what needs a human.--applystill exits non-zero when anything is left behind.get_agentsdegrades to[]on a transient API error, so a users-query blip or an app rename bricked the most-used command. A typed--agentstill exits 1. The filter compares delegate names case-insensitively either way, so the raw name still matches.Nine new tests cover every branch above — 69 total, green:
The workspace now reports 0
agent:*labels — the migration ran for real (see the RUSH-2272 ticket for the full--applytranscript, including aCONFLICTthat was correctly refused).