use labels in triage #232
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#232
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
Implement tools to query and set labels. Give the issue handler access so that triage can set labels. Explicitly deny all labels set in omp config, so the agent cannot trigger itself.
Most of this landed after the issue was filed, in
7c59d3c feat(forgejo): let conversation sessions relabel their own issue(2026-11-09):forgejo_update_issue_labelsexists (apps/server/src/forgejo/tools/issues.ts:208-262): add/remove by name, resolved against repo + org labels, applied as one replace-allPUT.forgejo_conversationtrigger withissueHandler: true(dispatcher.ts:1862-1892), and the tool is inCONVERSATION_TOOL_NAMES(session-policy.ts:188), restricted to the session's own issue number (:337-342).protectedLabelDenial(session-policy.ts:266-315) re-reads.omp/forgejo.yamllive and deniesissue_handler.label,implementation.{intake,progress,planning,planned}_label,review.label,ci_watchdog.others_prs.label, bothexclude_labelslists andStatus/Blocked, matching names throughlabelKey(trimmed, lowercased,-_folded to a space). When the config cannot be read it falls back toStatus/Blockedplus theagent/namespace.What is still missing or wrong:
fetchAvailableLabels(tools/labels.ts:124-141), which drops every field exceptidandname. An agent learns valid names only by making a failingforgejo_update_issue_labelscall and reading the error, and has no way at all to learn which labels are off-limits before being denied.buildIssueHandlerSeed(conversation-detect.ts:764-767) ends its built-in branch with "Do not change labels or anything else; do not implement.", while the same session's system context (session-policy.ts:359) says it can label. Only a repo that sets a customissue_handler.promptgets labeling behaviour; the deck's own.omp/forgejo.yaml:1-8leavespromptcommented out.forgejo_create_issueescapes the rule. It takeslabelsby name (issues.ts:132) and both the conversation policy (session-policy.ts:333) and the pull-request policy (:457) allow it unconditionally, with no protected-label check.forgejoPullRequestPolicy.toolNamesspreadsCONVERSATION_TOOL_NAMES(:443), so PR sessions seeforgejo_update_issue_labels, butdecidehas no rule for it: it falls through to"default", and trigger sessions turn every ask into an unattended deny (permission-filter.ts:14). Their system context meanwhile states "You cannot change labels" (:496).Note on the threat model:
classifyLabelignores any label event whosesender.loginis the agent's own account (webhook.ts:449), andresolveLabelTriggerrequires the configured label to be both present and just added (dispatcher.ts:950-962). The agent therefore cannot re-trigger itself through labels today. The denial is defence in depth and protection of the dispatcher's own bookkeeping labels (agent/in progress,agent/planned,Status/Blocked); that is why removals stay denied too even though a removal can never fire a trigger.Plan
Context
Add a read-only
forgejo_list_labelstool that shows the repository's label vocabulary and marks the labels that steer automation; make labeling part of the default triage turn; close theforgejo_create_issuehole in the protected-label rule; and remove the label tool from the pull-request policy's tool list. No new config keys. No web client work:ForgejoTool.tsxalready renders unknown Forgejo tools with the generic result view andsummarizeForgejoArgsfalls through for a parameterless tool, and read tools likeforgejo_search_issuescarry no protocoldetailspayload, sopackages/protocol/src/forgejo-tools.tsis untouched.Step 1 — one shared definition of "automation label"
The protected-name derivation is inline in
protectedLabelDenialtoday; the new tool must mark exactly the same names, so it moves into the module that owns the config.In
apps/server/src/forgejo/repo-config.ts, next tolabelKey/sameLabel(around line 426-434), add:Then rewrite
protectedLabelDenial(apps/server/src/bridge/session-policy.ts:266-315) to use it, and delete the localAGENT_LABEL_PREFIXconstant at:255-257and the inlineprotectedNamesarray. Two further changes in the same function, both load-bearing for later steps:forgejo_create_issue, whose parameter islabels, notadd/remove. Change the destructure toconst p = (params as { add?: unknown; remove?: unknown; labels?: unknown } | null) ?? {};and flatten[p.add, p.remove, p.labels].advice: string = "propose the change in your comment instead"and build the denial as`the label "${name}" steers this repository's automation; ${advice}`, so the existing message stays byte-identical for the update tool.The
parsedhandling stays as it is:file?.ok ? parseRepoConfig(file.data) : null, a 404 yieldingparseRepoConfig(null)and therefore the defaults, and the call site now passesparsed?.ok ? parsed.config : nulltoisAutomationLabel.Independent of steps 2-6; do it first because both the tool and the policy change depend on it.
Step 2 — carry descriptions out of
fetchAvailableLabelsapps/server/src/forgejo/tools/labels.tscurrently narrows every label to{ id, name }(:137), discarding thedescriptionthe API returns (ApiLabel.description?: string | null,tools/api-types.ts:11-14), and loses whether the label came from the repo or the owning org.Add, next to
LabelRef(:12-15):Change
fetchAvailableLabelstoPromise<ApiResult<AvailableLabel[]>>: iterate the repo page and the org page separately instead of concatenating them, pushing{ id: l.id, name: l.name, description: l.description ?? "", org }. Keep the existingseende-duplication by id and the existing error handling verbatim (repo failure surfaced; org failure surfaced only forstatus === 0or>= 500).resolveLabelNamesandplanLabelDeltakeep takingreadonly LabelRef[];AvailableLabel[]is assignable, soforgejo_create_issue(issues.ts:138-142) andforgejo_update_issue_labels(issues.ts:222-229) need no change.Step 3 — read the repo config from a tool, and format the listing
Still in
apps/server/src/forgejo/tools/labels.ts. Tools hold aForgejoClient(shared.ts:30-36) but noDispatchApi; the raw-file read is three lines, exactly asdispatch-api.ts:505-508does it, so no plumbing is added.encodePathandrepoApiPathare exported from../client.ts(client.ts:176-182);REPO_CONFIG_PATH,DEFAULT_REPO_CONFIG,parseRepoConfig,isAutomationLabelandRepoConfigfrom../repo-config.ts.Add the pure formatter beside it, so the output is unit-testable without HTTP (the convention in
tools/prs.test.ts, which testsbuildReviewPayloadrather than the tool):Output, in the order
fetchAvailableLabelsreturns (repo labels first, then org labels):`${labels.length} labels in ${owner}/${repo}:`, orno labels in ${owner}/${repo}when the list is empty (return that line alone).— <description>when the description is non-empty, then[org]whenorg, then[automation]whenisAutomationLabel(name, config).[automation] labels steer this repository's automation; adding or removing them is refused in unattended sessions.configis null, a further line:.omp/forgejo.yaml could not be read, so the [automation] marks are a fallback: Status/Blocked and the agent/ namespace.Step 4 — the
forgejo_list_labelstoolDefine it in
apps/server/src/forgejo/tools/issues.tsamong the read tools (above the// ─── Mutating tools ───divider at:122), whereforgejo_update_issue_labelsalready lives, and append it toISSUE_TOOLS(:332-342). This keeps the registry at the three existing groups.No
detailspayload, matching the other read tools. Update the registry header comment intools/index.ts:1-8, which says "the 23 tools" whileindex.test.ts:80-82already asserts 24; it becomes 25.Step 5 — session tool lists
All in
apps/server/src/bridge/session-policy.ts:"forgejo_list_labels"toCONVERSATION_TOOL_NAMES(:160-189), in the Forgejo read block after"forgejo_get_run_logs"(:183).CONVERSATION_NO_CHECKOUT_TOOL_NAMES(:191) andWATCHDOG_TOOL_NAMES(:367-372) are derived by filter and pick it up automatically."forgejo_list_labels"toIMPLEMENTER_TOOL_NAMES(:196-237), after"forgejo_get_run_logs"(:229).forgejoPullRequestPolicy.toolNames(:443) to drop the label tool it never allows:Idea mode needs no change:
decideIdeaModeForgejogates Forgejo write tools, and the new tool ismutating: false, soFORGEJO_MUTATING_TOOLS(tools/index.ts:34-36) anddefaultAsk(permission-filter.ts:58-61) leave it alone — it never prompts in attended sessions either.Step 6 — protected labels on
forgejo_create_issue, and the promptsIn
forgejoConversationPolicy.decide(session-policy.ts:333), replace the unconditional allow:Do the same at
forgejoPullRequestPolicy.decide:457. That policy'sdecideis currently synchronous and does not receiveapi/signal; widen its signature toasync decide(toolName, params, scope, sessionId, cwd, api, signal), which theSessionPolicyinterface already permits (:116-124, returningPolicyDecision | Promise<PolicyDecision>). Every other branch of that function stays as it is.Prompt text, same file:
:359) becomes::496) keeps "You cannot change labels, state or descriptions." and gains, after theforgejo_create_issuesentence:`\`forgejo_list_labels\` shows the labels you can put on a new issue; the ones it marks \`[automation]\` are refused.`Step 7 — labeling becomes part of the default triage turn
In
apps/server/src/forgejo/conversation-detect.ts, theprompt === nullbranch ofbuildIssueHandlerSeed(:764-767) becomes exactly:The custom-prompt branch (
:768-771) is unchanged: a repository that setsissue_handler.promptowns its own steering.Critical files & anchors
apps/server/src/bridge/session-policy.ts—protectedLabelDenial(~266),CONVERSATION_TOOL_NAMES(~160),forgejoConversationPolicy.decide(~320),forgejoPullRequestPolicy(~442). Every policy change lands here.apps/server/src/forgejo/tools/labels.ts—fetchAvailableLabels(~124) and the newreadRepoConfigForTools/formatLabelList.apps/server/src/forgejo/repo-config.ts—labelKey/sameLabel(~426),BLOCKED_LABEL(43),REPO_CONFIG_PATH(40); the new shared helpers go here so the tool and the policy cannot drift.apps/server/src/forgejo/conversation-detect.ts—buildIssueHandlerSeed(742-778); its two branches are pinned byconversation-detect.test.ts:527-546.apps/server/src/forgejo/tools/issues.ts— read tools end at the divider (~122),ISSUE_TOOLS(332-342).Verification
From the repository root.
bun run typecheck— thefetchAvailableLabelsreturn-type change and the PR policy's now-asyncdecidemust both compile.formatLabelList, inapps/server/src/forgejo/tools/labels.test.ts:[{id:1,name:"kind/bug",description:"Something is broken",org:false},{id:2,name:"agent/ready",description:"",org:false},{id:3,name:"area/ui",description:"",org:true}]with a config whoseimplementation.intake_labelisAgent/Ready. Expected: header3 labels in o/r:, thenkind/bug — Something is broken,agent/ready [automation],area/ui [org], then the[automation]explanation line and no fallback line.config: null. Expected:agent/readystill marked (prefix fallback),kind/bugandarea/uiunmarked, and the.omp/forgejo.yaml could not be readline present.no labels in o/rline.apps/server/src/bridge/session-policy.test.ts(extend the existing label describe at:61-99, reusing itsconfigApihelper from:43-49):decidePolicy(TRIGGER, "forgejo_create_issue", { title: "x", labels: ["Agent/Ready"] }, …, configApi("issue_handler:\n label: needs-triage\nimplementation:\n intake_label: Agent/Ready\n"))→{ deny: 'the label "Agent/Ready" steers this repository\'s automation; create the issue without it' }.labels: ["kind/bug"], and one with nolabelsat all →"allow".policyToolNames({ kind: "forgejo_pull_request", … })containsforgejo_list_labelsand does not containforgejo_update_issue_labels;policyToolNames(TRIGGER)and the implementer trigger both containforgejo_list_labels.:69-97must still pass unchanged — that is the regression check on the refactor intoisAutomationLabel.bun test apps/server/src/forgejo/tools/index.test.ts— update the inventory test at:80-85to 25 tools and addlist_labelsto the read list and to the self-identifyingbarelist at:90-95.bun test apps/server/src/forgejo/conversation-detect.test.ts— update the default-branch assertion at:527-546to the new seed text; the custom-prompt assertion must be untouched.bun test apps/server/src/bridge apps/server/src/forgejofor the surrounding suites (permission-filter.test.ts,repo-config.test.ts,dispatcher.test.ts,plan-issue.test.ts).Assumptions & contingencies
GET /repos/{owner}/{repo}/labelsreturnsdescriptionfor repo labels;ApiLabelalready declares it optional and the formatter treats a missing description as empty, so nothing breaks if an instance omits it.[automation]marks are advisory in attended deck sessions, where no policy denies anything; the wording ("refused in unattended sessions") is written to stay true in both cases.issue_handler.promptrepositories keep their current behaviour by design; if a repo wants the new labeling steering it drops its custom prompt.