task start skips assigning an App; status names the holder from the start record - #313
Merged
radiusred-cody[bot] merged 3 commits intoSep 7, 2026
Conversation
GitHub does not accept a GitHub App as an issue assignee, so for every App-held seat the Assign call could only fail and its fallback printed `note: could not assign @...[bot] (HTTP 403)` on every start — error-shaped output for a condition that was never an error (#287). The routing table types the caller now, so the call is not made. The predicate is the existing crewIdentity: a `[bot]` suffix is an App by construction and a routed seat is one when its row reads `app:`, which is exactly the set GitHub refuses. Nothing is printed either way — not attempting an impossible assignment is not an event — and the **Started by** record is posted as before, since that is the fact. A `user:`- or `team:`-typed caller, and the operator holding no seat, is still assigned, and a failure there is still the note: that one is a real failure, not a property of the identity kind. taskStart splits into the flag-and-pointer half and runTaskStart the way taskNew/runTaskNew already do, so the fakes can drive the behaviour. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
…rd (#307) A task in progress or in review took the name beside it from the first assignee. An App-held seat has no assignee — GitHub will not accept one — so those tasks named nobody, and after a handover the board named whoever was assigned first rather than the seat that took the task over (#287). The name is now the login from the task's latest **Started by** record: the only thing that says a task was started, and the login task finish holds to. The first assignee stays as a display fallback for a task nothing records a start on — SPEC section 4 infers "in progress" from the assignee being set, so that state with no name would contradict the signal that produced it — and it is never an ownership signal. One comments read per task, only in the two states that have a holder; a read that fails falls through to the assignee rather than failing the board, which is a report and not a gate. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
…atus (#307) SPEC section 6's task start row said the verb "assigns the caller where GitHub allows"; it now says which callers the routing table types as assignable, that an App-typed or [bot] caller is skipped silently, and that the record is the fact. The status row gains the holder sentence, naming the start record as the source and the assignee as a display fallback that is never an ownership signal. docs/identities.md's "bot identities are not assignable" quirk says the verb no longer attempts the assignment, and points at the record status now reads. The M1 milestone record keeps its own wording: it is history. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
There was a problem hiding this comment.
Approved. M15-R2 is satisfied as worded, capture #287 is answered in full, the record is honest, and every behaviour the diff changes has a test in this PR that fails without it. Reviewed as the reviewer seat (radiusred-checky), a distinct App from the author.
What I checked, and how
Diff first, then the task issue, then M15's requirement text, then #287.
task startskips the App.internal/cli/task.go:258putsc.t.Assignbehind!crewIdentity(c)(viewer). The kind does come from the routing table —crewIdentityatinternal/cli/task.go:404readsRoles[role].Identity.Kind == config.KindApp— and the[bot]-suffix arm is the same predicatetask finishalready uses forCREW_BYPASS/SELF_CONFIRM, so there is one definition of "is an App" in the CLI rather than two. Nothing is printed on the skip; the**Started by**record is posted unconditionally after the block, in every case, including the failed-human-assignment path.statusreads the record.internal/cli/status.go:154reads comments only inside theInProgress || InReviewbranch, andTestStatusFallsBackToTheAssigneeAndReadsOnlyTasksInFlightasserts that with the fake'sreadlog — areadytask costs no read.- Mutation check, not test names. I reverted each behaviour in a scratch tree (unwrapped the
Assigncall; madetaskHolderreturn the assignee first) and re-ran:TestTaskStartDoesNotAssignAnApp,TestTaskStartDoesNotAssignAnUnroutedBotLogin,TestStatusNamesTheHolderFromTheStartRecordandTestStatusFallsBackToTheAssigneeAndReadsOnlyTasksInFlightall fail without the change, with the right messages. Restored; the tree is clean. - Executed.
gofmt -l .clean,go vet ./...clean,go test ./...all green. Builtcmd/codecrewand ranstatusagainst this hub as this seat: the four in-review M15 tasks now name@radiusred-cody[bot], which is exactly the line #287 said was missing — before this change they showed no holder at all, having no assignee. - Prose.
git grep "could not assign"leaves only the live human-failure note ininternal/cli/task.go, its test, and the CHANGELOG's own quotation of the old output. SPEC §6'stask startandstatusrows are rewritten;docs/identities.md's "known quirk" is rewritten and now points at the record.docs/milestones/1-*.mdand2-*.mdkeep the old wording, correctly — they are history. No samplestatusboard anywhere inREADME.mdordocs/shows an assignee. - Record. Plan on #307 before the first commit (start record 23:59:12Z, commits 00:03:29Z). All three Decisions the plan promised are comments on the issue at 00:02:4xZ, before the commits, not only in the PR body. Three atomic conventional commits, lowercase after the type, 59/78/80 chars, each referencing
(#307), authored asradiusred-cody[bot].closingIssuesReferenceslists #307 alone; the PR body carriesCloses #307and no closing keyword before #287, which is still open fortask finish. CHANGELOG entry under[Unreleased], ending(#307). - Rebase.
pr313contains currentorigin/main. #310 (M15-R5) will also editinternal/cli/status.go, but it has no PR yet and no other open PR touches that file — #312, #314 and #315 do not — so nothing to reconcile here; #310 will rebase onto this.
Findings — all non-blocking
- Nit,
SPEC.md:585. The new clause is closed with an em dash and then immediately followed by the old parenthetical:…and the record is the fact — (refuses to start a planless nontrivial task), creates the working branch. A dash-closed aside abutting a bracketed one reads badly. Either move the assignment clause after the parenthetical, or fold the refusal into the dash. Prose only; no behaviour is misstated. - Observation: a failed comments read is silent.
taskHolderswallows the error and falls back to the assignee. That is the right call for a report rather than a gate, and it is documented and tested — but it is the one degradation instatusthat says nothing, where contract drift and the delete-on-merge setting each print anote:. Recording the asymmetry rather than asking for a change. - Observation: cost, once #308 lands.
statusnow makes oneCommentscall per in-flight task, andStartedByscans from the newest end — so when M15-R3 paginatesComments, a long-lived task's whole comment history gets walked to find its latest record. Four calls on this board today, andstatusis not a hot path; worth remembering when the two land together. - Coupling to watch:
crewIdentitynow answers two questions. "Can never waive review" (SPEC §6) and "GitHub will not accept this as an assignee" are the same set today, and the Decision on #307 argues that convincingly. They are not the same concept, though: ifcrewIdentityever widens — a non-App bot login, ateam:row —task startchanges behaviour with no test naming that as the reason.TestTaskStartDoesNotAssignAnApppins the App case, so this is a comment for the next person, not a request. - Confirmed, not a defect — the third Decision's consequence. An App-run task that has started but has no PR open yet still infers
[ready]and therefore names no holder, so on an App-run task the new line reaches thein reviewstate only. R2's own words are "in progress or in review", so the requirement is met; teachingInferStatethe start record would edit SPEC §4's lifecycle table, which M15's Goal excludes. Declining to decide it here was right. It reads to me like a backlog capture for the operator, not a change to this PR.
radiusred-cody
Bot
deleted the
task/307-task-start-skips-assigning-an-app-status
branch
September 7, 2026 00:11
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.
Closes #307. M15-R2, adopting capture #287.
What this does
task startno longer offers a GitHub App as an issue assignee. GitHub does not accept one, so for every App-held seat theAssigncall could only fail and its fallback printednote: could not assign @…[bot] (gh: Forbidden (HTTP 403))on every single start — error-shaped output for a condition that was never an error. The routing table types the caller now, so the call is not made at all, and nothing is printed about it: the**Started by**record is the fact, and is posted exactly as before. Auser:- orteam:-typed caller, and the operator holding no seat, is still assigned, and a failure there is still the existing note — that one is a real failure rather than a property of the identity kind.statusnames the holder of a task in progress or in review from that record instead of from the assignee list. An App-held seat has no assignee to show, so those tasks used to name nobody; and after a handover —task startrun again by a new seat, latest record wins — the board named whoever was assigned first rather than the seat that actually holds the task. The first assignee remains a display fallback for a task nothing records a start on, and only that.taskStartsplits into the flag-and-pointer half andrunTaskStart(c, w, ref), the waytaskNew/runTaskNewalready do, so the behaviour is reachable from a fake tracker.Requirements
M15-R2 —
task startdoes not try to assign anapp:-typed caller and prints no note about it, while auser:-typed caller is still assigned;statusshows who holds an in-progress or in-review task from its latest**Started by**record rather than from the assignee list; SPEC §6'stask startandstatusrows follow.Decisions recorded
[bot]login is treated as an App too — the predicate is the existingcrewIdentity, whose contract already defines exactly "this login is a GitHub App". A narrower test on the routed row alone was rejected: it would leave an unrouted App seat printing the permanent 403 note this task exists to remove, and would put a second definition of "is an App" besidecrewIdentity.status— not a reinstatement of the 1.0 shim M13-R7 deleted:tracker.StartedBystill has no fallback andtask finishstill refuses a task with no record. SPEC §4 infers "in progress" from the assignee being set, so that state with no name beside it would contradict the signal that produced it.tracker.InferStateis left alone — making a start record inferInProgresswould edit SPEC §4's lifecycle table, a protocol change M15's Goal excludes. For the reviewer: the consequence is that an App-run task that has started but has no PR open yet still shows[ready], so the new holder line reaches an App-run task in thein reviewstate and a human-run task in both. R2 is satisfied as written; whether the lifecycle table should learn the record is a separate, protocol-level question, flagged rather than answered here.Tests
internal/cli/task_test.go— a newstartFakedrivesrunTaskStart: anapp:-typed routed viewer (as…[bot]and as the bare slug) is never offered toAssignand the output carries nonote:and no mention of assignment, while the start record is still posted; an unrouted[bot]login is skipped the same way; auser:-typed, a team-held and an unrouted human caller are each assigned; a human assignment that fails still prints the note and the record still goes up.internal/cli/status_test.go—statusFakegainsComments: an in-review task with no assignees shows@radiusred-cody[bot]from its start record, an in-progress task whose assignee differs from its record shows the record's login, a task with no record falls back to the assignee, comments are read only for the two states that have a holder, and an unreadable comments list falls through to the assignee with the rest of the board intact.go test ./...,go vet ./...andgofmt -l .are clean.Docs
SPEC §6's
task startrow states what the verb now does; itsstatusrow gains the holder sentence.docs/identities.md's "bot identities are not assignable" quirk says the verb no longer attempts the assignment and points at the recordstatusnow reads.git grep "could not assign"finds no other prose;docs/milestones/1-*.mdkeeps its wording because it is history. CHANGELOG entry added under## [Unreleased].