fix(work-items): make the lease protocol runnable without GraphQL, and specify the overengineering product-code lane - #3166
Conversation
The seam's claim/reclaim verbs resolved assignees through `gh issue edit --add-assignee` and `gh issue view --json assignees`, both of which route through GitHub's GraphQL API. Sandboxed sessions (Claude Code on the web and remote execution) serve only a pinned set of GraphQL operations and refuse the rest with HTTP 403, so the entire lease protocol was unrunnable there and no work-items lane could acquire a race-safe claim. Port both verbs to the REST `.../issues/<n>/assignees` endpoints via shared helpers in common.sh. The helpers take the same read/write writer argument the adapter already used, so the carve-out that assigns the session identity rather than the bot is preserved; `@me` is resolved to the login explicitly because REST takes a literal login. Two defects surfaced while verifying the port end to end: - REST POST /assignees returns 201 and silently drops a login that cannot be assigned, where the GraphQL path failed loudly. Unguarded, that turns a no-op assignment into a reported claim while list-frontier still sees the item unassigned, putting two workers on one item. claim now re-reads the assignees and exits 4 before posting any lease comment. - wit_lease_json matched only a body ENDING in ` -->`, so a lease comment with anything appended (a bot wrapper's attribution footer, a signature, a CI note) parsed as "not a lease". This was silent and unsafe: claim's arbitration found no incumbent and granted over a live holder, and renew-lease refused to renew a lease it had just written. Anchor on the first ` -->` after the marker, which is also strictly more correct since an HTML comment cannot contain `-->`. claim.test.sh covered only --help and usage errors, so the protocol itself passed vacuously. lease-coordination.test.sh now drives the happy path, the foreign-assignee conflict with its rollback, and the dropped-assignment guard. Verified against the live API: claim, renew-lease, and back-off-on-live-lease all succeed in a session where they previously could not run at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RiAekqvuMHJs3CjusBbNWH
…hape Closes #2897. The deferred second lane from the overengineering-detection-skill interview. V1 (#2961) shipped the enforcement surface and wrote the scrutiny method lane-reusable at the plugin level, whose "Lane binding" section forward- referenced "a future product-code lane" with nowhere to go. Both conditions that blocked this issue now hold: V1 shipped, and the method stabilized. The issue asked for three things. Walker and evidence sources: context/product-code-lane.md supplies the four bindings the method asks a lane for. The item is the abstraction, never the file, so an interface plus its implementations plus its call sites is one finding with a members list. Eight layers, ordered by how cheaply their evidence settles: single-implementation, extension-points, configuration, generality, layering, speculative-api, dead-branches, premature-async. The evidence table maps each tier onto sources this lane actually has, and names the lane's signature tier-2 probe: whether the second implementation ever arrived. That is what makes speculative generality checkable, since the generality was a prediction that variation would come, and history can falsify it rather than leaving the call to a reviewer's taste. Protected classes extend the method's set with the hazards specific to changing code that runs: published API surface, serialization and wire formats, concurrency primitives, error-containment boundaries, and testability seams. Boundary against existing owners: documented as three operational handoffs rather than a declaration. The distinguishing axis is retrospective and evidence-gated versus prospective and judgment-gated. A "keep but reshape" finding belongs to architecture:improve, a safe mechanical one to code-tidying, and the lane never runs on a diff because a construct introduced in the diff has no history to weigh it with. Shipping shape: ADR 0017 records it as its own skill rather than an argument of overengineering:audit. Skill descriptions are the routing surface and are budgeted least-invoked-first, the two lanes' trigger vocabularies are disjoint, the protected-class sets do not map, and retirement means something different in each. The shared walk orchestration is handled by extraction when the skill lands, the same move the plugin already made for the method itself, not by fusing the two lanes. No skill or behavior changes: the specification precedes its skill, and points at the enforcement lane's copy of the shared rules rather than duplicating them so there is one statement of each rule through the transition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RiAekqvuMHJs3CjusBbNWH
Resolves a CHANGELOG conflict: main shipped work-items 0.39.14 (the generated options-block docs fix) while this branch had also claimed 0.39.14. Main's entry is preserved as 0.39.14 and this branch's entries move to 0.39.15, with the manifest bumped to match. Also fixes a latent trap found while adding the claim cases: lease-coordination.test.sh wrapped one call in `set +e` and "restored" with `set -e 2>/dev/null || true`, which ENABLES errexit rather than restoring the file's declared `set -uo pipefail` mode, so every later case expecting a non-zero exit aborted the suite instead of asserting on it. Both sites now use `|| rc=$?`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RiAekqvuMHJs3CjusBbNWH
Resolves an overengineering version collision: main shipped the delta lane as 0.2.0 (#3146) while this branch had also claimed 0.2.0. Main's entry stays at 0.2.0 and this branch's documentation-only entries move to 0.2.1, taking main's plugin description (which now names the third lane). The delta lane also shipped as its OWN skill composing audit, rather than as an argument selecting a mode inside it, which is independent corroboration of the decision ADR 0017 records. The ADR now cites it, and its context section is corrected: #2897 said "a third skill" when the plugin had two, but delta has taken that slot, so the product-code lane is the fourth skill and third lane. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RiAekqvuMHJs3CjusBbNWH
|
Claude finished @kyle-sexton's task in 3m 17s —— View job Security review in progress
|
|
Last security-reviewed head: |
|
Claude finished @kyle-sexton's task in 3m 25s —— View job Code review complete
SummaryI read this PR end to end against the two feature commits and did not find correctness bugs to flag. No inline comments were posted because nothing localizes to a single changed line as a defect — the following is prose review only.
Note on verification limits in this session: this sandbox's Verdict: no blocking issues found. This is a clean, well-tested seam fix plus a doc-only spec addition with consistent cross-references. Branch: |
|
Claude has reviewed this PR 2 times. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
Second overengineering version collision in a row: main took 0.2.1 for the findings-artifact date-rationale fix (#3167) after taking 0.2.0 for the delta lane (#3146). Main's entries stay where they are and this branch's documentation-only entry moves to 0.2.2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RiAekqvuMHJs3CjusBbNWH
|
Claude finished @kyle-sexton's task in 1m 5s —— View job Security review complete
Skip gate: no re-review needed. The last full security review covered head None of the security-relevant surface — Verdict: no new security-relevant changes since the last review at Branch: |
|
Claude finished @kyle-sexton's task in 3m 43s —— View job
|
Second CHANGELOG collision: main shipped its own 0.39.15 (#3166 — lease marker parsing, and the GraphQL removal from claim/reclaim), so this work moves to 0.39.16 and both entries are kept, newest-first. The two changes are complementary halves of one problem rather than overlapping work, and the changelog entry now says so: a sandboxed session has both an old gh from Ubuntu's archives and a GraphQL surface that answers only a pinned operation set. #3166 took GraphQL out of the lease path; this branch stops the version floor refusing that path at the dispatcher. Verified composing on gh 2.45 with GraphQL 403: `claim` against a nonexistent issue now returns a REST 404, where before #3166 it was 403 and before this branch it was exit 3. No source conflict between them — #3166 touched claim.sh, reclaim.sh, common.sh and lib/lease.sh; this branch touches the dispatcher. All 18 seam suites pass on the merged tree (242 assertions). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012RvxCrBC5rq1Sv8oMBDriB

Closes #2897
Summary
Working #2897 through
/work-items:workwas blocked at the very first step: the seam'sclaimverb could not run in a Claude Code web/remote session, so no race-safe claim could be acquired and the lane's claim-before-dispatch invariant could not be satisfied. This PR fixes the seam, then delivers #2897 with a real claim held.Two independent changes, one per commit:
work-items0.39.15 — the lease protocol no longer depends on GraphQL, plus two correctness defects found while verifying that port end to end.overengineering0.2.0 — the deferred product-code lane specification and the ADR deciding its shipping shape. Documentation only; no skill or behavior changes.Fix
The lease protocol (
work-items)claimandreclaimresolved assignees throughgh issue edit --add-assigneeandgh issue view --json assignees. Both route through GitHub's GraphQL API, which sandboxed sessions serve only as a pinned set of PR-review operations, refusing the rest with HTTP 403. Both verbs now use the REST…/issues/<n>/assigneesendpoints through sharedwit_read_assignees/wit_add_assignee/wit_remove_assignee/wit_try_remove_assigneehelpers.Identity routing is unchanged: the helpers take the same
read(baregh, session identity) /write(bot wrapper) writer argument the adapter already used, so the carve-out that assigns the session user rather than the bot still holds.@meis resolved to the login explicitly because REST takes a literal login.Verifying the port surfaced two defects that were not part of the original scope:
claimdid not verify its own assignment landed. RESTPOST …/assigneesreturns 201 and silently drops a login that cannot be assigned, wheregh issue edit --add-assigneefailed loudly. Unguarded, the port would have introduced a new race:claimreporting a held lease whilelist-frontierstill saw the item unassigned, putting two workers on one item. A dropped assignment now exits4before any lease comment is posted.wit_lease_jsonmatched only a body ENDING in-->. Any comment carrying the lease plus trailing text (a bot wrapper's attribution footer, a signature, a CI note) parsed as "not a lease". This was silent and unsafe rather than merely lossy:claim's arbitration found no incumbent and granted over a live holder, andrenew-leaserefused to renew a lease it had just written. Without this, the GraphQL fix alone would have restored a claim that was not actually race-safe. The match is now anchored on the FIRST-->after the marker, which is also strictly more correct since an HTML comment cannot contain-->.claim.test.shcovered only--helpand usage errors, so the protocol itself passed vacuously.lease-coordination.test.shnow drives it: the happy path, the foreign-assignee conflict with its rollback, and the dropped-assignment guard. Adding those cases exposed a latent trap in that file, fixed here: aset +e/set -e 2>/dev/null || truepair that ENABLES errexit rather than restoring the file's declaredset -uo pipefailmode, so every later case expecting a non-zero exit aborted the suite instead of asserting on it.The product-code lane (
overengineering)#2897 asked for three things, all deferred until V1 shipped and the shared method stabilized. Both conditions now hold.
context/product-code-lane.mdsupplies the four bindingsscrutiny-method.mdasks a lane for. The item is the abstraction, never the file, so an interface plus its implementations plus its call sites is one finding with a members list. Eight layers ordered by how cheaply their evidence settles:single-implementation,extension-points,configuration,generality,layering,speculative-api,dead-branches,premature-async. Protected classes extend §7 with the hazards specific to changing code that runs: published API surface, serialization and wire formats, concurrency primitives, error-containment boundaries, and testability seams.The lane's signature evidence probe is whether the second implementation ever arrived. Speculative generality is a prediction that variation would come, and tier-2 history can falsify it directly, which is what makes the lane tractable rather than a matter of a reviewer's taste.
The boundary against
/simplify,code-tidying, andarchitecture:improveis documented as three operational handoffs. The distinguishing axis is retrospective and evidence-gated versus prospective and judgment-gated; notably, this lane never runs on a diff, because a construct introduced in the diff has no history to weigh it with.ADR 0017 decides the lane ships as its own skill rather than an argument of
overengineering:audit: skill descriptions are the routing surface and are budgeted least-invoked-first, the two lanes' trigger vocabularies are disjoint, the protected-class sets do not map, and retirement means something different in each. The shared walk orchestration is handled by extraction when the skill lands, the same move the plugin already made for the method itself.Verification
The seam fix is verified against the live GitHub API, in the session where it previously could not run at all:
claimon overengineering: product-code lane — apply the scrutiny method to code-level overengineering in application code #2897 succeeded and returnedlease_comment_id: 5383892028.renew-leaseon that handle succeeded.claimwhile the lease was live correctly backed off with exit7, naming the incumbent holder. Before thewit_lease_jsonfix this returned0, granting over the live lease, which is the proof that race-safety is genuinely restored rather than only the transport being changed.Gates run locally:
adapters/gitea/list-items.test.shis excluded because it hangs, and it hangs identically on a cleanorigin/mainworktree, so it is pre-existing and unrelated to this change. Flagging it rather than silently skipping.shellcheckandshfmtclean on every changed shell file.markdownlint-cli2,typos,editorconfig-checkerclean on every changed markdown file.ai-slopdetector reports 0 findings across the new prose.Related
docs/adr/0017-ship-the-product-code-lane-as-its-own-skill.md— added here; records the lane's shipping-shape decision.Known limitation, deliberately not fixed here. Only the lease protocol is ported.
get-item,list-items,list-sub-items,list-frontier,add-sub-item, andlink-blocksstill resolve GraphQL-only fields (issueType,blockedBy,parent,subIssues) or usegh issue list, so they remain unavailable under the same restriction. Becauselist-frontieris what/work-items:workuses to select, an unattended lane still cannot pick an item in a web session; only the named-item path works. The REST endpoints for most of this are served (issueslist, nativetype,sub_issues,dependencies/blocked_by), but REST exposes noparentfield, soparent_idneeds its own resolution. That is a separate piece of work across five scripts and should be scoped on its own rather than widening this PR. Documented in the GitHub adapter README.Generated by Claude Code