Implement-issue button: server enforces Agent/Ready gate and moves issue to Agent/In Progress #83
Labels
No labels
Agent
Ignore
Agent
In Progress
Agent
Planned
Agent
Planning
Agent
Ready
Agent
Triage
Compat
Breaking
Kind
Bug
Kind
Documentation
Kind
Enhancement
Kind
Feature
Kind
Security
Kind
Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
lucas/omp-deck#83
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The
Agent/Ready/Status/Blockedlabel check and theAgent/Ready→Agent/In Progresstransition currently exist only as instructions to the agent (.omp/commands/implement.mdsteps 3–4), which agents don't follow reliably. The server-started implementer (the "Implement issue" button in the web UI →POST /api/forgejo/implement→ForgejoDispatcher.startImplementer) does no label handling at all, and its agent has no label tool.When a session is started via the "Implement issue" button, the server itself must perform the checks and the label change:
Agent/Readyand does not haveStatus/Blocked. Label names match case-insensitively after trimming.Agent/Ready→Agent/In Progress. If the swap fails, the session still runs; the failure is logged, posted as a comment on the issue, and — if that comment also fails — raised as a deck notification.The manual
/implementchat command keeps its own instructions unchanged. The implementer's system context is not changed.Plan
Approach
Steps 1–2 keep the tree building and existing tests passing; step 3 adds the behavior.
1. Make label fetching usable outside tools (
apps/server/src/forgejo/tools/labels.ts)fetchAvailableLabels(ctx: ForgejoToolContext, signal?)tofetchAvailableLabels(ctx: Pick<ForgejoToolContext, "client" | "owner" | "repo">, signal?). The body only uses those three fields, so existing callers intools/issues.tsandtools/prs.tsstay valid unchanged.2. Extend
DispatchApi(apps/server/src/forgejo/dispatch-api.ts)IssueSummarygainslabels: LabelRef[](LabelRef={ id: number; name: string }, import the type from./tools/labels.ts). Update its doc comment to "Title, state and labels of issue/PRnumber…".RawIssuegainslabels?: { id: number; name: string }[] | null;readIssuemaps it tolabels: (raw.labels ?? []).map((l) => ({ id: l.id, name: l.name })).createDispatchApi:listLabels(owner: string, repo: string, signal?: AbortSignal): Promise<ApiResult<LabelRef[]>>— doc: "Labels usable on issues of the repo: its own plus its org's." Implementation:fetchAvailableLabels({ client, owner, repo }, signal).replaceIssueLabels(owner: string, repo: string, number: number, ids: number[], signal?: AbortSignal): Promise<ApiResult<unknown>>— doc: "Replace all labels of issuenumberwithidsin one request." Implementation:client.json<unknown>("PUT", repoApiPath(owner, repo, "issues", String(number), "labels"), { body: { labels: ids }, signal })(same endpoint/body theforgejo_update_issue_labelstool uses intools/issues.ts).apps/server/src/forgejo/dispatcher.test.ts, updateFakeApiso it compiles: addavailableLabels: LabelRef[](default:[{ id: 1, name: "Agent/Ready" }, { id: 2, name: "Agent/In Progress" }, { id: 3, name: "Status/Blocked" }]),listLabelsResult: ApiResult<LabelRef[]> | null = null(when set,listLabelsreturns it instead),labelPuts: Array<{ number: number; ids: number[] }>, andputStatus: number | null = null(when set,replaceIssueLabelsfails with that status).replaceIssueLabelsrecords the put and, on success, rewritesissues.get(number).labelsto the matchingavailableLabelsentries. Change theOPENconstant in thestartImplementerdescribe block to includelabels: [{ id: 1, name: "Agent/Ready" }].3. Gate and transition in
ForgejoDispatcher.startImplementer(apps/server/src/forgejo/dispatcher.ts)const READY_LABEL = "Agent/Ready";,const IN_PROGRESS_LABEL = "Agent/In Progress";,const BLOCKED_LABEL = "Status/Blocked";and a private helperhasLabel(labels: readonly LabelRef[], name: string): booleancomparingl.name.trim().toLowerCase() === name.toLowerCase().isPullandstate !== "open"checks (before the secondthis.running.has(key)check andthis.running.add(key)):hasLabel(issue.data.labels, READY_LABEL)→throw new ImplementerStartError(422,#${number} is not labeled ${READY_LABEL}).hasLabel(issue.data.labels, BLOCKED_LABEL)→throw new ImplementerStartError(422,#${number} is labeled ${BLOCKED_LABEL}).private async markInProgress(api: DispatchApi, issue: { owner: string; repo: string; number: number }, sessionId: string): Promise<void>that never throws. InstartImplementer, callawait this.markInProgress(api, { owner, repo, number }, handle.sessionId)after the existingsetNametry/catch and beforethis.track(this.runImplementerTurn(...)). It runs only when session creation succeeded (failed checkout/createSession paths throw earlier and touch no labels). Steps:api.readIssue(fresh labels, so the replace-all PUT does not drop labels added since the precheck) and fetchapi.listLabels(owner, repo). Either failing → reason`${status} ${message}`.planLabelDelta({ current: issue.labels, available, add: [IN_PROGRESS_LABEL], remove: [READY_LABEL] })(reuse from./tools/labels.ts).!ok→ reason =plan.error(e.g. unknownAgent/In Progress).noop→ return.api.replaceIssueLabels(owner, repo, number, plan.nextIds); failure → reason`${status} ${message}`.log.warn(could not mark ${repoFull}#${number} in progress: ${reason}); postapi.postComment(owner, repo, number,omp-deck could not move this issue to ${IN_PROGRESS_LABEL}: ${reason}\n\nThe implementer session `${sessionId}` is running regardless.); if the post fails,log.warnit andawait notificationService.notify({ level: "error", title:Could not mark ${repoFull}#${number} in progress, body:${reason}\n\nDeck session: `${sessionId}`, source: INBOX_SOURCE })— same pattern as the failure branch ofrunImplementerTurn. Wrap the whole method body in try/catch so an unexpected throw is logged and treated as a reason too; it must not reach the caller (therunningkey is already claimed and the session exists).Critical files & anchors
apps/server/src/forgejo/dispatcher.ts—startImplementer(~L826–909): precheck after the#N is closedcheck;markInProgresscall betweensetNameandthis.track(...).runImplementerTurn(~L941–955) is the comment→notification fallback pattern to copy.apps/server/src/forgejo/dispatch-api.ts—IssueSummary,RawIssue,createDispatchApi.apps/server/src/forgejo/tools/labels.ts—planLabelDelta,fetchAvailableLabels,LabelRef.apps/server/src/forgejo/dispatcher.test.ts—FakeApi(~L74) anddescribe("ForgejoDispatcher.startImplementer")(~L865).Verification
Add these tests to
describe("ForgejoDispatcher.startImplementer")inapps/server/src/forgejo/dispatcher.test.ts:Agent/Ready→refusal(...)equals{ status: 422, message: "#4 is not labeled Agent/Ready" }; issue withAgent/Ready+status/blocked(case/whitespace variant) →{ status: 422, message: "#4 is labeled Status/Blocked" }; in both,h.sessionsis empty andh.api.labelPutsis empty.whenIdle,h.api.labelPutsequals[{ number: 4, ids: [2] }]and issue 4's labels are[{ id: 2, name: "Agent/In Progress" }]; an extra label on the issue (e.g.{ id: 9, name: "bug" }) is preserved in the put ids.putStatus = 500):startImplementerstill resolves withsess-1, the turn still runs (handle.promptslength 1), andh.api.postedcontains one comment on #4 starting withomp-deck could not move this issue to Agent/In Progress:.Agent/In Progressmissing fromavailableLabels: same as above, comment containsunknown label.h.api.labelPutsis empty.Run from
/workspace/omp-deck:bun test apps/server/src/forgejoandbun run typecheck.Manual end-to-end (needs a configured Forgejo + registered workspace): in the web UI open "Implement issue" for an issue without
Agent/Ready→ modal shows#N is not labeled Agent/Readyand no session appears; addAgent/Ready, retry → session opens and the issue on Forgejo showsAgent/In Progressinstead ofAgent/Ready.