Allow conversation sessions to relabel their own issue or pull request #241
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#241
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
Unattended Forgejo conversation sessions — both @mention turns and triage
(issue-handler) turns, which are
forgejo_conversationsessions carrying a markin
forgejo_issue_handler_sessions— cannot change labels.forgejo_update_issue_labelsis absent from
CONVERSATION_TOOL_NAMES(apps/server/src/bridge/session-policy.ts:158-186),so the tool is never even constructed for them, and their system prompt states
You cannot change labels, state, descriptions, branches or code.This blocks the documented triage use case:
issue_handler.promptin.omp/forgejo.yamlis "the steering half of the triage task — what to analyse,label or assign", and the commented example in
.omp/forgejo.yaml:7-8reads"Apply a severity label (sev/1..sev/4) and assign the owner …". A repository
can ask for labeling, and the session then has no tool to do it.
Give every
forgejo_conversationsession the label tool, restricted to theissue or PR the session is about, with the repository's automation-steering
labels off limits.
Plan
1. Put the tool on the conversation surface, keep it off the watchdog
In
apps/server/src/bridge/session-policy.ts:"forgejo_update_issue_labels"toCONVERSATION_TOOL_NAMES(after"forgejo_create_issue", line 185).CONVERSATION_NO_CHECKOUT_TOOL_NAMES(line 188) filters out only
git_-prefixed names, so the no-checkout variantpicks it up automatically — intended, since labeling needs no checkout.
WATCHDOG_TOOL_NAMESfilter (lines 292-294) with&& name !== "forgejo_update_issue_labels". The CI watchdog's system prompt(line 328) says "You cannot change code, branches, labels or issue state"
and stays true.
IMPLEMENTER_TOOL_NAMESandforgejoPullRequestPolicykeep their own listsand are not touched.
Why an explicit
"allow"is required in step 3:forgejo_update_issue_labelsis in
FORGEJO_MUTATING_TOOLS, sodefaultAsk(permission-filter.ts:55-58)turns it into an approval prompt, and under a trigger an
askbecomes a denial.Returning
"default"from the policy would make the tool unusable. Returning"allow"skips user allow/ask rules but not user deny rules, whichpermission-filter.tschecks before consulting the policy.2. Make
Status/BlockedshareableBLOCKED_LABEL = "Status/Blocked"is currently a module-local constant inapps/server/src/forgejo/dispatcher.ts:212.dispatcher.tsimportssession-policy.ts, so importing the constant from there would create a cycle.Move it instead: add
export const BLOCKED_LABEL = "Status/Blocked";toapps/server/src/forgejo/repo-config.ts(next toREPO_CONFIG_PATH, line 37),delete the local constant in
dispatcher.ts:212, and import it there from./repo-config.ts— a moduledispatcher.tsalready imports (parseRepoConfig,sameLabel).repo-config.tsimports only theyamlpackage, sosession-policy.tsimporting it introduces no cycle.grep -n "BLOCKED_LABEL" apps/server/srcmust return onlyrepo-config.ts,dispatcher.tsand the newsession-policy.tsuse.3. Protected-label check in the conversation policy
In
session-policy.ts, add the import:Add the fallback constant and the helper above
forgejoConversationPolicy:Behavior, in order:
addandremovefromparams, keeping onlynon-empty strings. Empty set → return null (the tool itself rejects a call
with neither,
forgejo/tools/issues.ts:208-212).const client = api(); when non-null,await client.readRepoFile(scope.owner, scope.repo, REPO_CONFIG_PATH, signal),then
parseRepoConfig(file.data)whenfile.ok. A 404 is not a failure —readRepoFilemaps it to{ ok: true, data: null }andparseRepoConfig(null)returns
DEFAULT_REPO_CONFIG, whose label fields are all null, so onlyBLOCKED_LABELis protected.parsed.ok):BLOCKED_LABEL,issueHandler?.label,implementation.intakeLabel,implementation.progressLabel,implementation.planningLabel,review?.label,ciWatchdogOthersPrs?.label, and every entry ofreview?.excludeLabelsand
ciWatchdogOthersPrs?.excludeLabels. Drop nulls.api()null,!file.ok, or!parsed.ok): justBLOCKED_LABEL, plus the rule that any name withlabelKey(name).startsWith(AGENT_LABEL_PREFIX)is protected. Thisagent/rule applies only on this fallback path, never when theconfig was read.
sameLabel(name, protectedName)(repo-config.ts:413), whichfolds case and
-/_/space;/is preserved, soAgent/Ready,agent readyandAGENT-Readyall collide, whileagentic-uxdoes notmatch the
agent/prefix.{ deny: \the label "${name}" steers this repository's automation; propose the change in your comment instead` }. The tool appliesaddandremove` in one atomic update, so one protectedname denies the whole call.
Then rewrite
forgejoConversationPolicy.decide(lines 255-272) asasync decide(the
SessionPolicy.decidesignature already permits a promise, line 122, andforgejoImplementerPolicyis async, line 462). Keep every existing branch inplace and insert the new one after the
forgejo_create_issuebranch (line 268),before the
noCheckoutgit check:The signature of
decidegains theapiandsignalparameters it currentlydrops;
sessionIdandcwdkeep their_-prefixed names (still unused).Note the deliberate asymmetry with the comment tools directly above, which allow
any number in the repository: labels are state, comments are not.
4. System-prompt orientation
In
forgejoConversationPolicy.systemContext, replace the single bullet at line285 with two bullets (keep every other line of the array as is):
The per-turn triage task text in
conversation-detect.ts:765-767("Do notchange labels or anything else; do not implement") is left unchanged on
purpose: the built-in triage steering stays passive, and a repository opts
into labeling by setting
issue_handler.prompt, whose branch (lines 768-772)replaces that text entirely. Consequently
conversation-detect.ts, its tests(
conversation-detect.test.ts:527-547) and therepo-config.ts:54-57doccomment are out of scope for this change.
5. Tests
In
apps/server/src/bridge/session-policy.test.ts, insidedescribe("decidePolicy: forgejo_conversation")(line 42). The existingTRIGGER(line 22) hasscope: { owner: "o", repo: "r", number: 7, isPull: true }and
NO_API(line 27) returns null — reuse both. Add a stub next topullsApi(line 30):
Cases:
policyToolNames(TRIGGER)containsforgejo_update_issue_labels;so does
policyToolNamesfor anoCheckout: trueconversation scope (see theexisting no-checkout trigger at line 80); the
forgejo_ci_watchdogtrigger'snames do not contain it.
decidePolicy(TRIGGER, "forgejo_update_issue_labels", { number: 7, add: ["kind/bug"] }, "s", process.cwd(), configApi(null))is
"allow"; the same call withnumber: 8denies withmay only change labels on #7.configApi("issue_handler:\n label: needs-triage\nimplementation:\n intake_label: Agent/Ready\n progress_label: Agent/In Progress\n"):{ number: 7, add: ["Agent/Ready"] }and{ number: 7, remove: ["needs triage"] }(normalization) both deny with
steers this repository's automation, while{ number: 7, add: ["kind/bug"] }is"allow".Status/Blockedwith a config present — denied even though the file namesno labels (
configApi(null)).configApi(null, { status: 500, message: "boom" })and
NO_API:add: ["kind/bug"]is"allow", whileadd: ["Agent/Ready"],add: ["agent/anything"]andremove: ["Status/Blocked"]deny. Also assertadd: ["agentic-ux"]is"allow", pinning that the fallback matches theagent/namespace and not a bareagentprefix.agent/is fallback-only — withconfigApi("issue_handler:\n label: needs-triage\n"),add: ["Agent/Ready"]is"allow"(the config does not list it).The cross-kind tests at lines 359, 416, 430 and 442 iterate every trigger kind
including
TRIGGER; none asserts an exhaustive tool list, so they need nochange. Re-read them before editing to confirm.
6. Documentation of the fixed tool set
In
docs/configuration.md:250, extend the trigger tool-set sentence after theforgejo_create_issueclause with:Leave line 221 alone: it describes the attended default-ask behavior, which is
unchanged — trigger sessions never reach
defaultAskfor this tool now.Verification
Run from the repository root.
bun test apps/server/src/bridge/session-policy.test.ts— all tests pass,including the new cases from step 5. This is the end-to-end proof of the new
behavior:
decidePolicywith{ number: 7, add: ["kind/bug"] }returns"allow"(previously{ deny: "not available in unattended Forgejo conversation sessions" }), and{ number: 7, add: ["Agent/Ready"] }againsta config naming that intake label returns a deny mentioning the label.
bun test apps/server/src/forgejo/dispatcher.test.ts— guards theBLOCKED_LABELmove in step 2.bun run typecheck— catches theasync decidesignature change and anymissed
BLOCKED_LABELimport.Assumptions & contingencies
calls are rare (at most a few per session) and
.omp/forgejo.yamlis readlive everywhere else in the dispatcher so edits apply without a restart. If a
turn is observed issuing many label calls, cache per
(owner, repo)for theturn — do not introduce a process-wide cache, which would defeat the live-edit
property.
DispatchApi.readRepoFileis reachable from the policy through the existingapi: () => DispatchApi | nullparameter, exactly asopenPrDenial(
session-policy.ts:439-457) reachesreadPull. Ifapi()is null inpractice for conversation sessions, the fallback path in step 3 covers it and
no further work is needed.