Forgejo: implementer failure and reset (plan step 22) #147
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#147
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
An implementer session can today only end through success (merge → post-merge CI → archive) or a human closing its PR, which merely archives ("archived: closed without merge",
handleLifecycleinapps/server/src/forgejo/dispatcher.ts). Nothing resets the issue's labels and nobody records what was tried. Failure must be a regular outcome: the PR is closed unmerged, the issue goes back to planning with a summary of what was attempted, what failed and why, and a later implementer run on the same issue starts cleanly.Decisions:
forgejo_abandon_implementation; a human by closing the agent's PR without merging, which gives the agent one final turn to write the summary.implementation.planning_label; null means the progress label is only removed.Status/Blockedis not used (it makescheckImplementablerefuse restarts).runImplementerTurn) keep their current notice and do not reset.implementation.max_concurrentneeds no change:intakeRunningcounts only the first turn and is released inrunIntake'sfinally(releaseIntakeSlot), so a failed session holds no slot.Plan
Context
Add an explicit failure path for implementer sessions: agent tool + human PR close → close PR unmerged, swap progress label for planning label, post failure summary on the issue (issue stays open), archive the session. Re-running an implementer on the issue afterwards must succeed.
Approach
Config key (
apps/server/src/forgejo/repo-config.ts)RepoConfig["implementation"]to{ intakeLabel; progressLabel; planningLabel: string | null; maxConcurrent }; defaultplanningLabel: nullinDEFAULT_REPO_CONFIG.parseImplementation, add"planning_label"to the key loop (same non-empty-string validation) and return it.parseRepoConfig, after the existing distinctness checks, rejectplanningLabelequal toprogressLabel→ error`implementation.planning_label` must differ from `implementation.progress_label`; equal tointakeLabelis allowed (re-queueing straight to intake is a legitimate setup).repo-config.test.ts,dispatcher.test.ts).Close-PR API (
apps/server/src/forgejo/dispatch-api.ts)closePull(owner, repo, number, signal?): Promise<ApiResult<unknown>>toDispatchApi=PATCH /repos/{owner}/{repo}/pulls/{number}with{ state: "closed" }. Reuse the client call thatforgejo_update_pr(apps/server/src/forgejo/tools/prs.ts,updatePr) uses for itsstatefield; add the method to the fake API indispatcher.test.tsrecording calls inclosedPulls: number[].Agent tool
forgejo_abandon_implementation(apps/server/src/forgejo/tools/issues.ts, viadefineForgejoTool,mutating: true, added to the exported tool list next tocloseIssue)number(int, the issue),summary(non-empty string, markdown). Description: "Forgejo: end this implementation as failed. Postssummaryon the issue; omp-deck then closes your pull request without merging, moves the issue back to planning and ends this session. Use only when the work cannot be completed: CI you cannot fix, a plan that does not hold, or a rejected approach."run: post issue comment onnumberwith body## Implementation abandoned\n\n${summary}and returntextResult("Posted the failure summary on #N. omp-deck will close the pull request and end this session; end your turn now."). Errors viaapiError.apps/server/src/forgejo/tools/index.tslists thatindex.test.ts/permission-filter.test.tsenumerate (mutating list).Implementer policy (
apps/server/src/bridge/session-policy.ts)"forgejo_abandon_implementation"toIMPLEMENTER_TOOL_NAMESonly (not conversation/PR policies).forgejoImplementerPolicy.decide: allow only whenparamNumber(params) === scope.number, else{ deny: "only #${scope.number} can be abandoned in this session" }.systemContextrules add bullet: "- If the work cannot be completed (CI you cannot fix after real attempts, a plan that falls apart, or reviewers rejecting the approach), callforgejo_abandon_implementationon #N with a summary: what you attempted, what failed and why, the pull request and branch, and what re-planning should consider. Do not close the pull request or change labels yourself."agent/issue-${number}-<slug>), append: "pick a name not already on origin (earlier attempts' branches are kept)".Record the abandon (
apps/server/src/bridge/in-process.ts,tool_execution_endblock next to theforgejo_create_prmapping ~line 986)toolName === "forgejo_abandon_implementation",trigger?.kind === "forgejo_implementer"and the result is not an error: callenqueueAbandon(\${scope.owner}/${scope.repo}`, scope.number, sessionId, true)(below). The row follows thequeuePostMergeFixsynthetic-row pattern withevent: "issues",action: "implementation_abandoned",isPull: false,commentAuthor: "",deliveryId: "deck:abandon",dedupeKey: sha256(JSON.stringify(["implementation_abandoned", repoLower, number, sessionId])),payloadJson: JSON.stringify({ sessionId, summaryPosted }). Then wake the dispatcher the way other out-of-dispatcher inserts do (unverified — confirm first; if no hook exists, pass awake` callback into the bridge from where the dispatcher is constructed).enqueueAbandon(repo, number, sessionId, summaryPosted)indispatcher.ts(module-level function, no class state) so step 7 reuses it.Abandon handler (
dispatcher.ts): route rows withaction === "implementation_abandoned"(add next to wherepost_merge_ci_failedis routed) to newprivate async handleAbandon(row):sessionIdfrom payload; if the session is already archived (getArchivedMap) →finishForgejoTrigger(row.id, "dropped", "session already archived").await this.waitUntilIdle(sessionId, SESSION_POLICIES.forgejo_implementer.turnTimeoutMs)(lets the agent's turn end).listForgejoSessionNumbers(repo, sessionId)except the issue:readIssue; ifisPullandstate === "open"→api.closePull. Failures are collected into a reason list, not thrown.readRepoConfig-style logic (api.readRepoFile+parseRepoConfig; invalid/missing → defaults, log.warn). NewswapPlanningLabels(api, issue, labels)mirroringswapInProgressLabels:remove = [progressLabel],add = [planningLabel](each only if non-null), sameplanLabelDelta+replaceIssueLabelsflow, returns "" or reason.summaryPostedis false, postomp-deck: the implementation of #N was abandoned after @<closer> closed pull request #P without merging; the agent did not write a summary. Details are in omp-deck session \`.` on the issue.omp-deck could not fully reset this issue after abandoning the implementation: <reasons>(fallbacknotificationService.notifylikemarkInProgress).archiveSessionviathis.archiveAfterIdle(sessionId, "forgejo_implementer"); finish row"done","implementation abandoned".this.running/intakeRunning.Human scrap via PR close (
handleLifecycle, therow.isPullbranch)!merged,trigger.kind === "forgejo_implementer", PR author (payload.pull_request.user.login) is the agent, andpayload.sender.loginis not the agent: queue a turn instead of archiving. Insert a synthetic row (queuePostMergeFixpattern) withaction: "implementation_scrapped",isPull: true, number = PR,commentAuthor= sender, dedupe["implementation_scrapped", repoLower, prNumber, sessionId]; finish the lifecycle row"done","scrapped by @sender: final turn queued"; return.!mergedand the sender is the agent (the abandon handler closed it): finish"dropped","closed by the agent while abandoning"without archiving (step 6 archives).implementation_scrappedrows by resuming the session through the same path existing turn rows use (ensureSessionLiveregion ~line 1496), with prompt from newbuildScrappedTurninconversation-detect.ts: "@ closed your pull request #P without merging. The implementation is over. Callforgejo_abandon_implementationon # with a summary of what you attempted, what failed and why, the pull request and its branch, and what re-planning should consider. Make no other changes." After the turn settles (any outcome), callenqueueAbandon(repo, issue, sessionId, false): if the agent's tool call already inserted the row the dedupe key makes this a no-op (confirminsertForgejoTriggerignores duplicate dedupe keys — unverified; if it throws instead, catch the unique-constraint error), so the fixed notice of step 6.5 is posted only when the agent did not summarize.trigger.scope.number.Re-run on the same issue: no code change beyond the above. After archive,
checkImplementableno longer sees a usable mapped session,createCheckoutre-createsissue-N,setForgejoSessionoverwrites the mapping, and the old PR is closed soimplementerPrNumbersof the new session is empty. Verified by test in Verification.Critical files & anchors
apps/server/src/forgejo/dispatcher.ts—handleLifecycle(~1650),swapInProgressLabels(~2043),queuePostMergeFix(~1273) as the synthetic-row pattern.apps/server/src/bridge/session-policy.ts—IMPLEMENTER_TOOL_NAMES(152),forgejoImplementerPolicy(350).apps/server/src/bridge/in-process.ts—tool_execution_endhook (~986).apps/server/src/forgejo/repo-config.ts—parseImplementation.Verification
Run from
/workspace/omp-deck:bun test apps/server/src/forgejo apps/server/src/bridgeand the repo typecheck.New tests:
repo-config.test.ts:implementation: { planning_label: "Status/Planning" }parses;planning_labelequal toprogress_label→ the exact error string.session-policy.test.ts: implementerdecidePolicy(IMPL, "forgejo_abandon_implementation", { number: 7 }, "s1")→"allow";{ number: 8 }→ deny; conversation policy denies it.dispatcher.test.ts(existing fake API/harness):progress_label: "agent/in progress",planning_label: "agent/planning"→closedPulls == [12], label PUT removes progress and adds planning, session archived, no extra comment posted.alice→ no archive, animplementation_scrappedrow queued; running it with an agent that does not call the tool → fixed "did not write a summary" comment on #7, labels reset, archived.startImplementeron #7 succeeds (no 409).Assumptions & contingencies
{ state: "closed" }(PRs share issue routes).