feat(forgejo): let repositories steer the issue handler #237

Merged
lucas merged 1 commit from agent/issue-235-issue-handler-prompts into main 2026-11-09 11:17:01 +01:00
Collaborator

Closes: #235

Adds the two config fields the issue asks for, plus the session plumbing that makes them apply to the triage turn itself.

issue_handler.prompt

Replaces the steering half of the triage task. The surviving built-in core is one line: always finish by posting one comment on the issue, never implement (no code, no branch, no pull request). Label and assignee changes become available to the custom prompt. With prompt unset the task line is byte-identical to before, so unconfigured repositories see no change. Both the label and the title_pattern route use the same field.

A section carrying only prompt is still rejected by the existing `issue_handler` needs `label` or `title_pattern` check — a steering text for a handler that never fires is a config error.

system_prompts.issue_handler

A triage session receives global + issue_handler, and not conversation — the same one-block-per-kind rule every other kind follows.

Triage-ness is sticky and persisted (forgejo_issue_handler_sessions, migration 024, modelled on plan_issue_sessions): a session becomes a triage session the first time a triage turn runs on it, whether triage created it or someone @mentioned the agent on the issue first, and stays one for the rest of its life. The mark wins over the trigger kind in promptSessionKind, so it survives dispose, resume and restart. The row is deleted with the session.

Timing

Repository instructions are baked in when a session is created or resumed. A session created for a triage trigger is marked by the bridge as part of createSession, before its first turn. When a triage trigger lands on a session that already exists, the dispatcher lets the running turn finish, marks the session, disposes the handle and resumes it from the persisted JSONL (no conversation history is lost), and only then dispatches the triage turn — so the turn that introduces the triage instructions already runs under them. If the resume fails, the mark is already persisted, the instructions apply from the session's next wake, and the turn runs on the existing handle rather than failing the whole trigger.

An implementer session is never marked: it keeps system_prompts.implementer.

Verification

  • bun run typecheck — clean across all packages.
  • bun test apps/server — 1249 pass, 0 fail.
  • New tests: config parsing and trimming of both keys plus their error cases; repoSystemPrompt(config, "issue_handler") emits global + issue_handler and not conversation; the seed with and without a custom prompt; and three dispatcher end-to-end tests — the custom task text reaches the turn, a created triage session carries issueHandler and is not recycled, and an existing conversation session is marked and recycled (resumed a second time) before its triage turn.
  • .omp/forgejo.yaml: the two new keys are documented as commented-out examples following the file's existing convention; parsing the live file yields the same config as before, with prompt: null / issueHandler: null.
Closes: #235 Adds the two config fields the issue asks for, plus the session plumbing that makes them apply to the triage turn itself. ## `issue_handler.prompt` Replaces the *steering* half of the triage task. The surviving built-in core is one line: always finish by posting one comment on the issue, never implement (no code, no branch, no pull request). Label and assignee changes become available to the custom prompt. With `prompt` unset the task line is byte-identical to before, so unconfigured repositories see no change. Both the `label` and the `title_pattern` route use the same field. A section carrying only `prompt` is still rejected by the existing `` `issue_handler` needs `label` or `title_pattern` `` check — a steering text for a handler that never fires is a config error. ## `system_prompts.issue_handler` A triage session receives `global` + `issue_handler`, and *not* `conversation` — the same one-block-per-kind rule every other kind follows. Triage-ness is **sticky and persisted** (`forgejo_issue_handler_sessions`, migration 024, modelled on `plan_issue_sessions`): a session becomes a triage session the first time a triage turn runs on it, whether triage created it or someone @mentioned the agent on the issue first, and stays one for the rest of its life. The mark wins over the trigger kind in `promptSessionKind`, so it survives dispose, resume and restart. The row is deleted with the session. ## Timing Repository instructions are baked in when a session is created or resumed. A session created for a triage trigger is marked by the bridge as part of `createSession`, before its first turn. When a triage trigger lands on a session that already exists, the dispatcher lets the running turn finish, marks the session, disposes the handle and resumes it from the persisted JSONL (no conversation history is lost), and only then dispatches the triage turn — so the turn that introduces the triage instructions already runs under them. If the resume fails, the mark is already persisted, the instructions apply from the session's next wake, and the turn runs on the existing handle rather than failing the whole trigger. An implementer session is never marked: it keeps `system_prompts.implementer`. ## Verification - `bun run typecheck` — clean across all packages. - `bun test apps/server` — 1249 pass, 0 fail. - New tests: config parsing and trimming of both keys plus their error cases; `repoSystemPrompt(config, "issue_handler")` emits `global` + `issue_handler` and not `conversation`; the seed with and without a custom prompt; and three dispatcher end-to-end tests — the custom task text reaches the turn, a created triage session carries `issueHandler` and is not recycled, and an existing conversation session is marked and recycled (resumed a second time) before its triage turn. - `.omp/forgejo.yaml`: the two new keys are documented as commented-out examples following the file's existing convention; parsing the live file yields the same config as before, with `prompt: null` / `issueHandler: null`.
`.omp/forgejo.yaml` gains `issue_handler.prompt`, which replaces the
steering half of the triage task — what to analyse, which labels or
assignees to set — while the agent still always finishes by posting one
comment on the issue and never implements. Both the label and the
title-pattern route use it.

`system_prompts.issue_handler` adds standing repository instructions to
triage sessions. A session becomes a triage session the first time a
triage turn runs on it, whether triage created it or someone @mentioned
the agent on that issue first, and stays one for the rest of its life;
it then receives `global` + `issue_handler` and no longer
`conversation`. The mark is persisted so it survives dispose and
restart.

Repository instructions are baked in when a session is built, so a
triage trigger landing on an existing session lets the running turn
finish, marks the session and recycles it from its JSONL — no
conversation history is lost — before dispatching the triage turn. That
way the turn that introduces the triage instructions already runs under
them.

Co-Authored-By: Claude Opus 5 via omp-deck
lucas approved these changes 2026-11-09 11:16:57 +01:00
lucas merged commit 48a0472480 into main 2026-11-09 11:17:01 +01:00
lucas deleted branch agent/issue-235-issue-handler-prompts 2026-11-09 11:17:02 +01:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
lucas/omp-deck!237
No description provided.