Forgejo: comments on agent-opened issues/PRs and replies to the opening post must trigger #82

Closed
opened 2026-10-26 15:38:11 +01:00 by omp-deck · 0 comments
Collaborator

Problem

Trigger dad12e80-198b-4faa-be10-daee1cf9659c was dropped as "not addressed to the agent". It was lucas's first comment on PR #79 ("Give me an example for the repo config file"), and the agent (omp-deck) had opened that PR.

detectAddressed (apps/server/src/forgejo/conversation-detect.ts) only has three rules: mention, quoted-reply and follows-agent. The thread it gets from dispatcher.ts detect() is listComments only. As a result:

  • the author of the issue/PR is never considered;
  • the opening post (the description) is not part of the conversation. A first comment that replies to a description the agent wrote has no previous comment, so detection returns null.

Two rules are needed:

  1. A comment on an issue/PR the agent opened always triggers, but as a soft trigger. Two humans may be talking to each other on an agent-opened issue/PR, so this is not a hard "addressed to you" verdict. The agent is told to use the content and context to decide whether it is being asked to comment or answer, and to end without posting if it isn't.
  2. The opening post counts as a conversation entry. Replies to an agent-written description, and quotes of it, match the existing hard rules (follows-agent, quoted-reply).

The hard rules take precedence over the soft one. The "decide from context" steering is only added when no explicit rule matched.

Plan

Approach

Steps 1 and 2 must land together. The new parameter is required, so the dispatcher stops compiling after step 1 until step 2 is done. Step 3 is independent.

1. Detection rules (apps/server/src/forgejo/conversation-detect.ts)

  • Extend TriggerReason to "mention" | "edited-mention" | "quoted-reply" | "follows-agent" | "agent-artifact".
  • Add a required input field opening: ThreadComment | null to detectAddressed. It is the issue/PR description as a synthetic thread entry; null when the author is unknown.
  • New order of rules in detectAddressed, first match wins:
    1. action === "edited" → unchanged: edited-mention or null. Edits never use the new rules.
    2. mentions(body, agent) → "mention" (unchanged).
    3. thread === null → return null (unchanged; the dispatcher then fetches the thread).
    4. Build const entries = opening ? [opening, ...thread] : thread; and run the existing quoted-reply and follows-agent logic over entries instead of thread. The opening entry has id 0, so it is "previous" exactly when no real comment with a lower id exists.
    5. New: if (opening && isAgent(opening)) return "agent-artifact";
    6. Otherwise null.
  • Update the doc comment of detectAddressed: opening is the description as entry 0, and agent-artifact is the soft last-resort rule for artifacts the agent authored.
  • Add to REASON_TEXT: "agent-artifact": "a comment on an issue or pull request you opened".
  • In buildConversationSeed, when t.reason === "agent-artifact", append these lines after the fenced comment body:
    
    This comment does not mention, quote or directly follow you; it triggered only because you opened this ${t.isPull ? "pull request" : "issue"}. Read the conversation and decide from its content and context whether you are being asked to answer or act. Participants may be talking to each other. If it is not for you, end the turn without posting.
    
    For every other reason the seed is unchanged.

2. Dispatcher wiring (apps/server/src/forgejo/dispatcher.ts detect(), around line 618)

  • Build opening from the webhook payload's issue field. Comment payloads on PRs also carry issue; gate() already reads payload.issue?.user?.login for them at line 333. Change detect's parameter type from PayloadFields to Payload (both are defined in the same file) so it can read payload.issue. The call site at line 309 already passes a Payload.
    const login = payload.issue?.user?.login;
    const opening: ThreadComment | null =
    	typeof login === "string" && login !== ""
    		? { id: 0, author: login, body: payload.issue?.body ?? "", createdAt: "" }
    		: null;
    
  • Pass opening in input, so both detectAddressed calls (explicit with thread: null, and with the fetched thread) receive it. Leave job.thread = thread.data as is: the seed and other consumers keep the real comments only, without the synthetic entry.
  • The early return if (explicit !== null || action === "edited") return explicit; stays. Agent-artifact comments always fetch the thread, because the hard rules have to be checked first.
  • In runReady, buildConversationSeed already receives every TriggerReason, so it needs no change. Permission and workspace gates are unchanged.

3. Implementer policy wording (apps/server/src/bridge/session-policy.ts, forgejo_implementer systemContext, rules array around line 323)

  • Insert as the first element of rules:
    "- Comments on the issue or pull request arrive as turns. If one is not addressed to you, end the turn without posting. If you cannot tell, ask in a reply."
  • The rules array is shared by both the PR-started and the issue-started branches, so both get the line. The conversation and pull-request policies already have the equivalent line and stay unchanged.

Critical files & anchors

  • apps/server/src/forgejo/conversation-detect.ts: TriggerReason, detectAddressed, REASON_TEXT, buildConversationSeed.
  • apps/server/src/forgejo/dispatcher.ts: detect() (around line 618) and the PayloadFields / Payload types (lines 105–112).
  • apps/server/src/bridge/session-policy.ts: implementer systemContext rules.
  • Tests: apps/server/src/forgejo/conversation-detect.test.ts (base at line 33 needs opening: null), apps/server/src/forgejo/dispatcher.test.ts (the enqueue helper at line 325 already has an issueAuthor option; add an optional issueBody → issue.body).

Verification

Run from apps/server.

Add to conversation-detect.test.ts (detectAddressed). Add opening: null to base so the existing cases still compile and pass unchanged:

  • opening = comment(0, AGENT, "desc") with an empty thread and body "Give me an example" → "follows-agent". This is the PR #79 case: the description counts as the previous entry.
  • opening authored by the agent, thread [comment(5, "alice", "q")], body "@carol thoughts?" (comment id 10) → "agent-artifact".
  • opening authored by "carol", thread [comment(5, "alice", "q")] → null. A human-authored artifact without agent involvement still drops.
  • opening = comment(0, AGENT, "The parser lives in repo.ts") with body "> the parser lives in repo.ts\n\nwhy?" and thread [comment(5, "alice", "x")] → "quoted-reply".
  • An edited comment on an agent artifact with no added mention → null.
  • buildConversationSeed with reason: "agent-artifact" contains "Participants may be talking to each other"; with reason: "mention" it does not.

Add to dispatcher.test.ts:

  • enqueue({ isPull: true, number: 9, issueAuthor: AGENT, body: "just chatting" }), with h.api.threads.set(9, [{ id: 1, author: "bob", body: "hi", createdAt: "" }]) and the same agent-PR fixtures as the existing test at line 455 (h.api.pull etc.) → the trigger runs (not dropped) and the prompted seed contains "Participants may be talking to each other".
  • The existing test "a comment not addressed to the agent is dropped without a post" (line 606) must still pass. Its issueAuthor defaults to carol.

Commands: bun test src/forgejo src/bridge, then bun run typecheck in apps/server. The apps/web typecheck is also needed if it imports TriggerReason; currently nothing outside apps/server/src/forgejo matches the reason strings.

## Problem Trigger `dad12e80-198b-4faa-be10-daee1cf9659c` was dropped as "not addressed to the agent". It was lucas's first comment on PR #79 ("Give me an example for the repo config file"), and the agent (`omp-deck`) had opened that PR. `detectAddressed` (`apps/server/src/forgejo/conversation-detect.ts`) only has three rules: mention, quoted-reply and follows-agent. The `thread` it gets from `dispatcher.ts` `detect()` is `listComments` only. As a result: - the author of the issue/PR is never considered; - the opening post (the description) is not part of the conversation. A first comment that replies to a description the agent wrote has no previous comment, so detection returns null. Two rules are needed: 1. **A comment on an issue/PR the agent opened always triggers, but as a soft trigger.** Two humans may be talking to each other on an agent-opened issue/PR, so this is not a hard "addressed to you" verdict. The agent is told to use the content and context to decide whether it is being asked to comment or answer, and to end without posting if it isn't. 2. **The opening post counts as a conversation entry.** Replies to an agent-written description, and quotes of it, match the existing hard rules (`follows-agent`, `quoted-reply`). The hard rules take precedence over the soft one. The "decide from context" steering is only added when no explicit rule matched. ## Plan ### Approach Steps 1 and 2 must land together. The new parameter is required, so the dispatcher stops compiling after step 1 until step 2 is done. Step 3 is independent. **1. Detection rules (`apps/server/src/forgejo/conversation-detect.ts`)** - Extend `TriggerReason` to `"mention" | "edited-mention" | "quoted-reply" | "follows-agent" | "agent-artifact"`. - Add a required input field `opening: ThreadComment | null` to `detectAddressed`. It is the issue/PR description as a synthetic thread entry; null when the author is unknown. - New order of rules in `detectAddressed`, first match wins: 1. `action === "edited"` → unchanged: `edited-mention` or null. Edits never use the new rules. 2. `mentions(body, agent)` → `"mention"` (unchanged). 3. `thread === null` → return null (unchanged; the dispatcher then fetches the thread). 4. Build `const entries = opening ? [opening, ...thread] : thread;` and run the existing quoted-reply and follows-agent logic over `entries` instead of `thread`. The opening entry has id `0`, so it is "previous" exactly when no real comment with a lower id exists. 5. New: `if (opening && isAgent(opening)) return "agent-artifact";` 6. Otherwise null. - Update the doc comment of `detectAddressed`: `opening` is the description as entry 0, and `agent-artifact` is the soft last-resort rule for artifacts the agent authored. - Add to `REASON_TEXT`: `"agent-artifact": "a comment on an issue or pull request you opened"`. - In `buildConversationSeed`, when `t.reason === "agent-artifact"`, append these lines after the fenced comment body: ``` This comment does not mention, quote or directly follow you; it triggered only because you opened this ${t.isPull ? "pull request" : "issue"}. Read the conversation and decide from its content and context whether you are being asked to answer or act. Participants may be talking to each other. If it is not for you, end the turn without posting. ``` For every other reason the seed is unchanged. **2. Dispatcher wiring (`apps/server/src/forgejo/dispatcher.ts` `detect()`, around line 618)** - Build `opening` from the webhook payload's `issue` field. Comment payloads on PRs also carry `issue`; `gate()` already reads `payload.issue?.user?.login` for them at line 333. Change `detect`'s parameter type from `PayloadFields` to `Payload` (both are defined in the same file) so it can read `payload.issue`. The call site at line 309 already passes a `Payload`. ```ts const login = payload.issue?.user?.login; const opening: ThreadComment | null = typeof login === "string" && login !== "" ? { id: 0, author: login, body: payload.issue?.body ?? "", createdAt: "" } : null; ``` - Pass `opening` in `input`, so both `detectAddressed` calls (explicit with `thread: null`, and with the fetched thread) receive it. Leave `job.thread = thread.data` as is: the seed and other consumers keep the real comments only, without the synthetic entry. - The early return `if (explicit !== null || action === "edited") return explicit;` stays. Agent-artifact comments always fetch the thread, because the hard rules have to be checked first. - In `runReady`, `buildConversationSeed` already receives every `TriggerReason`, so it needs no change. Permission and workspace gates are unchanged. **3. Implementer policy wording (`apps/server/src/bridge/session-policy.ts`, `forgejo_implementer` `systemContext`, `rules` array around line 323)** - Insert as the first element of `rules`: `"- Comments on the issue or pull request arrive as turns. If one is not addressed to you, end the turn without posting. If you cannot tell, ask in a reply."` - The `rules` array is shared by both the PR-started and the issue-started branches, so both get the line. The conversation and pull-request policies already have the equivalent line and stay unchanged. ### Critical files & anchors - `apps/server/src/forgejo/conversation-detect.ts`: `TriggerReason`, `detectAddressed`, `REASON_TEXT`, `buildConversationSeed`. - `apps/server/src/forgejo/dispatcher.ts`: `detect()` (around line 618) and the `PayloadFields` / `Payload` types (lines 105–112). - `apps/server/src/bridge/session-policy.ts`: implementer `systemContext` `rules`. - Tests: `apps/server/src/forgejo/conversation-detect.test.ts` (`base` at line 33 needs `opening: null`), `apps/server/src/forgejo/dispatcher.test.ts` (the `enqueue` helper at line 325 already has an `issueAuthor` option; add an optional `issueBody` → `issue.body`). ### Verification Run from `apps/server`. Add to `conversation-detect.test.ts` (`detectAddressed`). Add `opening: null` to `base` so the existing cases still compile and pass unchanged: - `opening = comment(0, AGENT, "desc")` with an empty thread and body "Give me an example" → `"follows-agent"`. This is the PR #79 case: the description counts as the previous entry. - `opening` authored by the agent, thread `[comment(5, "alice", "q")]`, body "@carol thoughts?" (comment id 10) → `"agent-artifact"`. - `opening` authored by `"carol"`, thread `[comment(5, "alice", "q")]` → `null`. A human-authored artifact without agent involvement still drops. - `opening = comment(0, AGENT, "The parser lives in repo.ts")` with body "> the parser lives in repo.ts\n\nwhy?" and thread `[comment(5, "alice", "x")]` → `"quoted-reply"`. - An edited comment on an agent artifact with no added mention → `null`. - `buildConversationSeed` with `reason: "agent-artifact"` contains "Participants may be talking to each other"; with `reason: "mention"` it does not. Add to `dispatcher.test.ts`: - `enqueue({ isPull: true, number: 9, issueAuthor: AGENT, body: "just chatting" })`, with `h.api.threads.set(9, [{ id: 1, author: "bob", body: "hi", createdAt: "" }])` and the same agent-PR fixtures as the existing test at line 455 (`h.api.pull` etc.) → the trigger runs (not dropped) and the prompted seed contains "Participants may be talking to each other". - The existing test "a comment not addressed to the agent is dropped without a post" (line 606) must still pass. Its `issueAuthor` defaults to `carol`. Commands: `bun test src/forgejo src/bridge`, then `bun run typecheck` in `apps/server`. The `apps/web` typecheck is also needed if it imports `TriggerReason`; currently nothing outside `apps/server/src/forgejo` matches the reason strings.
lucas closed this issue 2026-10-26 15:41:19 +01:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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#82
No description provided.