Plan-issue label failures are silent: report them #271
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#271
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
Starting a "Plan issue" session on a Forgejo issue does not apply
implementation.planning_labelfrom.omp/forgejo.yaml, even though therepository has that label and the config is on the default branch. The deck
reports nothing: no toast, no message in the session, no comment, nothing in
the UI. The only trace is a
log.warnon the server's stdout.The reported case is
lucas/nanoKontroller2(labelsAgent/Ready,Agent/In Progress,Agent/Planning,Agent/Plannedall exist on therepository; the issue carried
Agent/Triageat the time). Why it failed thereis unverified — the repository and the deck's stdout log are both outside
what this session can read. Inspection of the code path leaves exactly these
real failure modes, and they are indistinguishable from the outside:
implementationLabels→api.readRepoFilefails with a non-404 status:log.warn, returnsnull, no label is attempted.implementationLabels→parseRepoConfigfails:log.warn, returnsnull. This is all-or-nothing — an error inreview:,ci_watchdog:,system_prompts:orcompaction:disables the labels too, while theimplementation:block is perfectly valid.markIssue→applyLabelDeltareturns a non-empty reason (unknown label,ambiguous label, failed
PUT):log.warn, nothing else.(A 404 on the config file is a fourth exit, but a legitimate one:
parseRepoConfig(null)means "no config, defaults apply", and a repositorythat was never onboarded must stay quiet. So must the no-op case where the
label is simply not configured.)
The same silence covers the publish side: approving the plan adds
planned_labeland removesplanning_labelthrough the same two helpers, andreports "Plan published to Forgejo" regardless of whether the labels moved.
The dispatcher already solves this for its own label updates —
markInProgresscomments on the issue and falls back to
notificationService.notify(
dispatcher.ts:2988-3006), and the abandon path collectsproblemsinto acomment (
dispatcher.ts:2604-2618). The plan-issue path is the outlier.The fix is the observability, not a named root cause: after it, the next
occurrence names its own reason. Scope boundary: no Settings-side
config-vs-labels health check, and no change to when a planning session starts
or when an issue is published.
Plan
1. Make
markIssuereport its reason instead of swallowing itIn
apps/server/src/forgejo/plan-issue.ts, change the private helperto return the failure reason:
Keep the existing
try/catcharoundapplyLabelDeltaand the existinglog.warnline verbatim (could not update labels of ${ref.owner}/${ref.repo}#${ref.number}: ${reason});return
reasonat the end.applyLabelDelta(apps/server/src/forgejo/issue-labels.ts:29) already returns""for success, for a no-op delta, and for an all-null/empty delta, so the"nothing configured" case needs no extra branch.
2. Add the notification helper
Still in
plan-issue.ts, next to the existingconst log = logger("forgejo:plan-issue");:Import the singleton the dispatcher uses:
import { notificationService } from "../notifications/index.ts";Add one private helper used by both moments:
level: "error"is deliberate:NotificationToastauto-dismissesinfoandwarnafter 6s and keepserror/criticaluntil dismissed(
apps/web/src/components/NotificationToast.tsx:16, 42-50), and the user isnavigating into the new session at exactly that moment. No
actionUrl, matchingthe dispatcher's label notification. The reason string is passed through
verbatim — a long "unknown label … Valid labels: …" clips at
line-clamp-3inthe toast (
NotificationToast.tsx:71) and stays complete in the notificationslog in Settings.
No web-side change is needed:
notify→BrowserNotificationChannel(registered in
apps/server/src/index.ts) →broadcastBus→ the store's"notification"case → existing toast and Settings log.3. Report the two config failures
In
implementationLabels(plan-issue.ts:25-47), both failure branchescurrently do
log.warnandreturn null. Keep the logs, add a report beforeeach return. The function must learn nothing new — it already has
owner,repoandname:!file.ok):`Could not read ${REPO_CONFIG_PATH} of ${name}``${file.status} ${file.message}\n\nThe plan-issue labels of this repository were not applied.`!parsed.ok):`Invalid ${REPO_CONFIG_PATH} in ${name}``${parsed.error}\n\nThe whole file is rejected, so the plan-issue labels of this repository were not applied.`catchbranch (err): reuse the read-failure title with the errormessage as the body.
The 404 path (
file.okwithdata === null) stays silent:parseRepoConfigturns it into
DEFAULT_REPO_CONFIG, whose label fields are allnull, andmarkIssuethen returns"".4. Report the label failure at session start
In
startPlanIssue(plan-issue.ts:211-214), the call is currentlyif (labels) await markIssue(...). Capture the reason and report it:The session still starts whatever happens; the report is awaited only because
notifyis async and the surrounding function already awaits label work.5. Carry the publish-time problem into the approval message
publishPlanIssue(plan-issue.ts:110-138) applies{ add: [labels.plannedLabel], remove: [labels.planningLabel] }after the issuewrite succeeded. A label failure there is not a publish failure: the issue is
written and correct.
plan-issue.ts, widen the ok-variant:In
publishPlanIssue, capturemarkIssue's reason. When it is non-empty,call
reportProblemwith title`Could not label ${b.owner}/${b.repo}#${outcome.number} as planned`andbody
`${reason}\n\nThe issue was published; only the label is missing.`,and include
labelProblem: reasonin the returned ok-outcome. When it isempty, return the outcome exactly as today (no
labelProblemkey).The inbox update (
if (b.inboxId !== null) updateInbox(b.inboxId, { processed: true });)is unaffected and still runs.
In
apps/server/src/bridge/plan-mode-bridge.ts, extend the message builder at:872-874:Without a problem the string is byte-identical to today's.
Pass the field at both approve sites, which already hold
published:plan-mode-bridge.ts:578-580(detached approve,session.prompt(publishedText(where), …))and
:773-778(tool-result approve). Both becomepublishedText(where, published.labelProblem).publishedis narrowed to the ok-variant at both sites (the!published.okbranch returned earlier), so the field is available without a cast.
These are the only two
publishedTextcallers;grep -n "publishedText" apps/server/srcmust return exactly the definition and those two sites (plus any match in
plan-mode-bridge.test.ts, which asserts on the produced text).6. Tests
apps/server/src/forgejo/plan-issue.test.tsalready hasfakeApi({ config }),LABEL_CONFIGandLABEL_POOL, and astartPlanIssuedescribe block(
:208-240). Capture notifications with the pattern fromdispatcher.test.ts:600-608:always paired with
notificationService.unregister("test-recorder")in afinally.Add, as behavior checks:
fakeApiwithLABEL_CONFIGbut a label pool thatlacks
agent/planning→startPlanIssueresolves with a session id, no labelPUTis recorded, and exactly one notification whose title containsas planningand whose body containsunknown label.fakeApi({ config: "implementation: {planning_label: x, planned_label: X}" })(a pair
parseRepoConfigrejects,repo-config.test.ts:196-198) → exactly onenotification titled
Invalid .omp/forgejo.yaml in o/r, and the session stillstarts.
fakeApi({ config: "" })→ no notification atall and no label
PUT(guards against turning the legitimate no-op intonoise).
publishPlanIssueagainst a pool lackingagent/planned→ outcome staysok: truewith the issue written, carrieslabelProblemcontainingunknown label, and raises one notification.ok: truewith nolabelProblemkeyand no notification.
In
apps/server/src/bridge/plan-mode-bridge.test.ts, the existing"plan-issue publish on approve" block (
:815) asserts the published text. Addone case whose publish hook returns
{ ok: true, url: "…", number: 7, labelProblem: "unknown label: agent/planned" }and assert the prompt text contains
planned label could not be applied: unknown label: agent/planned,and one asserting that an outcome without
labelProblemproduces the currenttext unchanged.
Verification
Working directory
/workspace/omp-deck; no env vars or fixtures needed.bun test apps/server/src/forgejo/plan-issue.test.ts apps/server/src/bridge/plan-mode-bridge.test.ts— all pass, including the new cases above.
bun test apps/server/src/forgejo/dispatcher.test.ts— unchanged; proves the shared
applyLabelDelta/notification paths were notdisturbed.
bun run typecheck—PublishOutcomeandpublishedTextchanged shape, sothis is the check that every caller was migrated.
on the configured Forgejo instance, temporarily point
implementation.planning_labelin that repository's.omp/forgejo.yaml(default branch) at a label that does not exist, e.g.
planning_label: "Agent/DoesNotExist", then start New session ▾ → Planissue on an open issue of that repo. Expected: the session starts and
navigates as before, and a red toast appears bottom-right titled
Could not label <owner>/<repo>#<n> as planningwhose body beginsunknown label: Agent/DoesNotExist. Valid labels: …; it stays untildismissed and is listed in Settings → Notifications. Restore the config
afterwards. On
lucas/omp-deckitself this reproduces with no config changeat all, since its configured
agent/planningandagent/plannedlabels donot exist on the instance — which is a separate, repository-side fact, not
part of this change.
Assumptions & contingencies
lucas/nanoKontroller2failure is assumed to be one of the threemodes above. If, after this change, starting a plan-issue session there raises
no notification and still applies no label, the labelling code did not run
at all: check that the running server includes commit
a690585(2026-11-10,
feat(server): mark planned issues with a configurable planned_label),and that the session was started through New session ▾ → Plan issue with
an issue seed, which is the only path that calls
startPlanIssue.notificationService.notifyis assumed safe to call fromstartPlanIssueandpublishPlanIssue. It swallows channel errors itself(
notifications/service.ts:72-85); the extratry/catchinreportProblemexists so a future throwing implementation can never break planning.