Mark planned issues with a configurable planned_label #248
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#248
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
.omp/forgejo.yamlalready names the labels that steer the issue lifecycle underimplementation:intake_label(adding it starts an implementer),progress_label(added on start, removed on abandon) andplanning_label(added on abandon). Nothing marks the planning stage itself: an issue being planned by a plan-issue session looks identical to one nobody has touched, and an issue whose plan is finished looks identical to one still being written.Add an optional
implementation.planned_label. A plan-issue session started on an issue marks it withplanning_label; when the approved plan is written to the issue,planned_labelreplacesplanning_label. Both labels are opt-in: with neither configured, behavior is exactly as today. An abandoned implementation, which already returns the issue toplanning_label, additionally drops the now-staleplanned_label.Plan
1. Shared label application helper
ForgejoDispatcher.swapLabels(apps/server/src/forgejo/dispatcher.ts:2792) is the only code that applies a label delta through the API, and it is private; plan-issue sessions need the same behavior. Move it out, widening both sides to lists (the abandon path must now remove two labels).Create
apps/server/src/forgejo/issue-labels.ts:canonicalLabelName(dispatcher.ts:223, together with its doc comment at:218-222) into the new file unchanged and export it; delete it fromdispatcher.ts. Its only uses are inside the movedswapLabelsbody.applyLabelDelta's body isswapLabels's body (dispatcher.ts:2797-2813) with the two single-name arrays replaced by(labels.add ?? []).filter((n): n is string => typeof n === "string" && n.trim() !== "")and the same forremove; keep the comment explaining the re-read before the replace-all PUT.planLabelDeltaalready deduplicates names case-insensitively and treats adding a present label or removing an absent one as a no-op, so no extra guarding is needed.swapLabelsfromdispatcher.tsand rewire its two callsites:dispatcher.ts:2764(markInProgress) →applyLabelDelta(api, issue, { add: [labels.progressLabel], remove: [labels.intakeLabel] }).dispatcher.ts:2414(abandon) →applyLabelDelta(api, { owner, repo, number }, { add: [planningLabel], remove: [progressLabel, plannedLabel] }), destructuringplannedLabelalongside the other two atdispatcher.ts:2412. Keep the surroundingtry/catchand theproblems.push("could not update the labels: …")reporting exactly as it is.2. Config field
In
apps/server/src/forgejo/repo-config.ts:plannedLabel: string | nulltoRepoConfig["implementation"](afterplanningLabel) andplannedLabel: nulltoDEFAULT_REPO_CONFIG.implementation.parseImplementation, addplanned_labelto thelabelsobject and to the["intake_label", "progress_label", "planning_label"]loop key list, and map it toplannedLabelin the returned value. The existing loop already produces the right error (`implementation.planned_label` must be a non-empty string) and trims.parseRepoConfig, after the existingplanning_labelcheck (:158-160), add:Only this pair is checked:
planned_labelequal tointake_labelis legal and meaningful ("plan done" and "ready to implement" can be one label), mirroring the existing allowance ofintake_label == planning_label(repo-config.test.ts:190).repo-config.ts:16-22): state that a plan-issue session started on an issue addsimplementation.planning_labelto it, and that writing the approved plan to the issue addsimplementation.planned_labeland removesplanning_label; extend the abandon sentence to say the progress label and the planned label are removed.3. Labels on plan-issue start and publish
In
apps/server/src/forgejo/plan-issue.ts, add two module-private helpers (the file currently never reads the repo config):implementationLabelsreadsREPO_CONFIG_PATHviaapi.readRepoFile, parses withparseRepoConfig, and returnsparsed.config.implementation; on a failed read or parse it logslog.warnand returns null. Copy the structure ofloadRepoSystemPrompt(apps/server/src/forgejo/repo-system-prompt.ts:30-54), including itstry/catcharound the whole body — a broken config must never break planning. Do not callcreateDispatchApi()here: both callers already hold an api.markIssuereturns immediately when every name inadd/removeis null, callsapplyLabelDelta, and logslog.warn(\could not update labels of ${owner}/${repo}#${number}: ${reason}`)when it returns a non-empty reason or throws. Nothing is posted on the issue and no notification is raised: unlike the dispatcher'smarkInProgress`, these sites run with a human attached to the session.In
startPlanIssue:let api: DispatchApi | null = null;before theif (seed.kind === "issue")branch and assign it there instead of the current localconst api.await handle.setPlanMode(true)and before the kickoffvoid handle.prompt(...), add:Placing it after session creation means a start that is refused (
PlanIssueStartError) never labels the issue. Inbox and free-text seeds have no issue at this point and are skipped.In
publishPlanIssue, after the write succeeded andoutcomeis set, before theb.inboxIdupdate:This covers both seed kinds:
createIssue(dispatch-api.ts:51) accepts only{title, body}, so a newly created issue is labelled by the same call; removingplanning_labelfrom it is a no-op. The publish outcome is unaffected by a label failure — the plan is on the issue either way, andPublishOutcome/plan-mode-bridge.tsstay unchanged.publishPlanIssuedoc comment (plan-issue.ts:58-62) and the module doc comment to mention the label changes.4. Protect the new label from the agent
In
apps/server/src/bridge/session-policy.ts, addimplementation.plannedLabelto theprotectedNameslist (:289-292, next toimplementation.planningLabel). It steers automation exactly like its siblings, so the agent'sforgejo_update_issue_labelsmust not set or clear it by hand.5. This repository's config
In
.omp/forgejo.yaml, underimplementation(afterprogress_label, beforemax_concurrent):Whether labels named
agent/planningandagent/plannedexist inlucas/omp-deckon the Forgejo instance is unverified — confirm first; if they do not, every plan-issue start and publish logs a label warning until they are created, and nothing else breaks.6. Tests
apps/server/src/forgejo/repo-config.test.ts: addplannedLabel: nullto the defaults assertion (:150-153) andplanned_labelto the full-parse case (:158-168); addexpect(error("implementation: {planning_label: x, planned_label: X}")).toBe("implementation.planned_labelmust differ fromimplementation.planning_label")andexpect(parseRepoConfig("implementation: {intake_label: x, planned_label: x}").ok).toBe(true)to the validation test.apps/server/src/forgejo/plan-issue.test.ts: extendfakeApi(:59) withreadRepoFile(returning configurable YAML text),readIssue(returning the issue's current labels),listLabels(the repo's label pool) andreplaceIssueLabels(recording the id list). Then assert, for a config withplanning_label: agent/planningandplanned_label: agent/planned:agent/planning→ afterpublishPlanIssuethe recorded PUT holds the id ofagent/plannedand not that ofagent/planning;agent/plannedid, and the inbox item is still marked processed;replaceIssueLabels(or an unknownplanned_labelname) still returns{ok: true, …}and still marks the inbox item processed;readRepoFile→{ok:true,data:null}) → noreplaceIssueLabelscall at all.apps/server/src/forgejo/plan-issue.test.ts: add astartPlanIssuecase with a stub bridge —createSessionreturning{ sessionId: "s1", setName: async () => {}, setPlanMode: async () => {}, prompt: async () => {} }castas unknown as AgentBridge, matching the file's existingas unknown as DispatchApistyle — asserting that an issue seed triggers a PUT addingagent/planning, and that an inbox seed triggers none.apps/server/src/forgejo/dispatcher.test.ts: in the abandon suite (:3494), addplanned_label: Agent/PlannedtoCONFIGand assert the replace-all PUT after an abandon drops both the progress and planned label ids while adding the planning one.Verification
From the repository root:
bun test apps/server/src/forgejo/repo-config.test.ts apps/server/src/forgejo/plan-issue.test.ts apps/server/src/forgejo/dispatcher.test.ts apps/server/src/bridge/session-policy.test.tsbun run typecheckEnd-to-end check of the new behavior without a live Forgejo, as a throwaway script (delete it afterwards) under
apps/server:DispatchApiwhosereadRepoFilereturns"implementation:\n planning_label: agent/planning\n planned_label: agent/planned\n", whoselistLabelsreturns[{id:1,name:"agent/planning"},{id:2,name:"agent/planned"}], whosereadIssuereturns issue #7 with labels[{id:1,name:"agent/planning"}], and which recordsreplaceIssueLabelscalls.publishPlanIssue({workspaceId:"w",owner:"o",repo:"r",issueNumber:7,inboxId:null}, "# T\n\n## Problem\nx\n\n## Plan\ny\n", api).{ok:true, url:…, number:7}, exactly oneupdateIssueBodycall, and exactly onereplaceIssueLabelscall with[2]—agent/plannedadded,agent/planninggone.Assumptions & contingencies
agent/planningandagent/plannedare expected to exist inlucas/omp-deckon the Forgejo instance (unverified). If they do not, the code is still correct — the label call fails and is logged — but to keep the repo's own config clean, create the labels in Forgejo rather than reverting step 5.dispatcher.test.ts's abandon suite turns out to assert label ids through a path that the list-valuedremovechanges beyond theplanned_labelid (for example an ordering assertion), keepapplyLabelDelta's output ordering identical to today'sswapLabels(planLabelDeltapreserves the current order and appends additions) and adjust only the expected id set.