Problem
Two code paths handle the same event — a human comments on an unassigned issue asking to work on it — and they now disagree on what forge state to write.
server/services/taskPromptDefaults/prompts.js (claim-issue agent, Phase 1 handoff) instructs verbatim:
remove $ME as an assignee if it was present and differs from the claimant, leave contributor-invitation labels intact, do NOT create a worktree, do NOT add in-progress
server/services/issueWatcher.js#assignVolunteer (deterministic gather pass, shipped in PR #6111) now sets the assignee and stamps in-progress, and does not release good first issue / help wanted.
Which one ran decides the resulting forge state. That is exactly the drift the shared IN_PROGRESS_LABEL constant was moved to lib/dispatchLabels.js to prevent, one level up: the spelling is shared, the policy is not.
Consequences currently shipped
STALLED on every reconcile pass. issueReconcile.js#classifyIssue returns STALLED for an issue that is open + in-progress with no merged PR and no live claim. A volunteer who has been assigned but has not opened a PR matches exactly, on every pass, indefinitely if they abandon it. Today this is log-only (cosTaskPreStepBlocks.js emits one info line; it does not dispatch an agent or block the park), so it is noise rather than spend — but it is an applier with no releaser, and every other flow that stamps in-progress owns its release (claim Phase 6/7, the zombie heal).
- Stale contributor invitations.
formatContributorLabelReleaseCommands exists because "this issue is taken now, so it must stop advertising itself to a human." The watcher marks it taken and leaves both invitations up.
Decision to make
Pick one policy and make both paths implement it. The two coherent options:
- (A) A volunteer claim is a claim. Both paths set the assignee, stamp
in-progress, and release the contributor labels. Requires: editing the claim prompt (PROMPT_VERSIONS bump + prior default preserved in PREVIOUS_DEFAULT_PROMPTS + a scripts/migrations/ entry), adding a contributor-label release to assignVolunteer, and extending the reconcile release path to cover "in-progress + non-owner assignee + no PR + stale N days" so the marker has a releaser.
- (B) A volunteer claim is only an assignee. Revert the
in-progress stamp from assignVolunteer and leave the prompt as-is. Cheapest, and perpetualWork.js#isActionableIssue already excludes any issue held by a non-self assignee — so queue exclusion does not depend on the label. Costs the Issues-tab hide the user asked for.
Recommendation: (A). The user's own manual practice on issue #6072 was assign + in-progress, and the repo's label description is literally "Claimed and being worked" — not "autonomously claimed". (B) is a revert of a deliberate request.
Encode the resulting policy once — a volunteerClaimLabels() helper in lib/dispatchLabels.js beside formatContributorLabelReleaseCommands — and render the prompt's handoff step from it, the way CONTRIBUTOR_RELEASE_GH is already interpolated. Note formatContributorLabelReleaseCommands emits shell text; the programmatically reusable piece is the already-exported CONTRIBUTOR_LABELS.
Related
markIssueInProgress's lazy label-create is the third bespoke one in the tree (layeredIntelligence/forgeFiler.js#ensureForgeLabels, autonomousJobs/selfDiagnostics.js#ensureDiagnosticsLabels). forgeFiler.js#applyBlockingLabel is the same "add one label to an existing issue" shape with no lazy create, so it carries the same latent 422. A shared applyForgeLabel({ exec, repoSpec, number, label }) driven by dispatchLabelSpec would be ~10 lines and cover all four — worth folding in if (A) is chosen, since (A) touches this code anyway.
Context: PR #6111, issue #6072.
Problem
Two code paths handle the same event — a human comments on an unassigned issue asking to work on it — and they now disagree on what forge state to write.
server/services/taskPromptDefaults/prompts.js(claim-issue agent, Phase 1 handoff) instructs verbatim:server/services/issueWatcher.js#assignVolunteer(deterministic gather pass, shipped in PR #6111) now sets the assignee and stampsin-progress, and does not releasegood first issue/help wanted.Which one ran decides the resulting forge state. That is exactly the drift the shared
IN_PROGRESS_LABELconstant was moved tolib/dispatchLabels.jsto prevent, one level up: the spelling is shared, the policy is not.Consequences currently shipped
STALLEDon every reconcile pass.issueReconcile.js#classifyIssuereturnsSTALLEDfor an issue that is open +in-progresswith no merged PR and no live claim. A volunteer who has been assigned but has not opened a PR matches exactly, on every pass, indefinitely if they abandon it. Today this is log-only (cosTaskPreStepBlocks.jsemits oneinfoline; it does not dispatch an agent or block the park), so it is noise rather than spend — but it is an applier with no releaser, and every other flow that stampsin-progressowns its release (claim Phase 6/7, the zombie heal).formatContributorLabelReleaseCommandsexists because "this issue is taken now, so it must stop advertising itself to a human." The watcher marks it taken and leaves both invitations up.Decision to make
Pick one policy and make both paths implement it. The two coherent options:
in-progress, and release the contributor labels. Requires: editing the claim prompt (PROMPT_VERSIONSbump + prior default preserved inPREVIOUS_DEFAULT_PROMPTS+ ascripts/migrations/entry), adding a contributor-label release toassignVolunteer, and extending the reconcile release path to cover "in-progress+ non-owner assignee + no PR + stale N days" so the marker has a releaser.in-progressstamp fromassignVolunteerand leave the prompt as-is. Cheapest, andperpetualWork.js#isActionableIssuealready excludes any issue held by a non-self assignee — so queue exclusion does not depend on the label. Costs the Issues-tab hide the user asked for.Recommendation: (A). The user's own manual practice on issue #6072 was assign +
in-progress, and the repo's label description is literally "Claimed and being worked" — not "autonomously claimed". (B) is a revert of a deliberate request.Encode the resulting policy once — a
volunteerClaimLabels()helper inlib/dispatchLabels.jsbesideformatContributorLabelReleaseCommands— and render the prompt's handoff step from it, the wayCONTRIBUTOR_RELEASE_GHis already interpolated. NoteformatContributorLabelReleaseCommandsemits shell text; the programmatically reusable piece is the already-exportedCONTRIBUTOR_LABELS.Related
markIssueInProgress's lazy label-create is the third bespoke one in the tree (layeredIntelligence/forgeFiler.js#ensureForgeLabels,autonomousJobs/selfDiagnostics.js#ensureDiagnosticsLabels).forgeFiler.js#applyBlockingLabelis the same "add one label to an existing issue" shape with no lazy create, so it carries the same latent 422. A sharedapplyForgeLabel({ exec, repoSpec, number, label })driven bydispatchLabelSpecwould be ~10 lines and cover all four — worth folding in if (A) is chosen, since (A) touches this code anyway.Context: PR #6111, issue #6072.