Workspace-less Forgejo conversation sessions for repos without a workspace #208

Closed
opened 2026-11-03 01:06:15 +01:00 by omp-deck · 0 comments
Collaborator

Problem

When a Forgejo webhook arrives for a repository that matches no omp-deck workspace, the dispatcher posts NOT_CONNECTED ("This repository isn't connected to an omp-deck workspace…") and drops the trigger (apps/server/src/forgejo/dispatcher.ts, gate after matchWorkspaces at ~652). That rules out repos that are only discussion spaces and have no code: nobody can talk to the agent there.

Wanted: sessions without a workspace. A trigger from an unmatched repo starts a session that:

  • is restricted to the conversation kind (forgejo_conversation, read-only plus comment and create-issue tools);
  • runs in an empty directory, so there is no code access and nothing on disk to read.

Scope:

  • Only conversation-message triggers fall back to a workspace-less session: mentions, edited mentions, quoted replies, thread replies, follows-agent and agent-artifact (CONVERSATION_REASONS), on issues and PRs alike.
  • Every other event on an unmatched repo keeps posting NOT_CONNECTED: review requests, auto-review, CI, pushes, labels, intake, merges, and anything on a PR the agent authored.
  • The fallback is always on, with no setting.
  • Sessions start only from webhooks; there is no UI or API entry point for creating them.
  • Once the repo is connected to exactly one workspace, an existing workspace-less session for an issue/PR is treated as unusable and replaced by a normal session in a real checkout on the next trigger.
  • More than one matching workspace still gives AMBIGUOUS, unchanged.
  • Remote-cloned checkouts for unmatched repos (enabling reviews/implementer without a workspace) are deliberately left to a separate issue.

Plan

Context

Today a trigger session's Forgejo repo binding comes from its cwd's git origin:

  • buildForgejoTools(cwd) (apps/server/src/forgejo/tools/index.ts:66) derives the repo that way.
  • assertTriggerRepo (apps/server/src/bridge/session-policy.ts:633) refuses the session if the derived repo doesn't match the trigger scope.

An empty dir has no origin, so workspace-less sessions must bind Forgejo tools to the trigger scope's owner/repo explicitly. They also need:

  • a persisted marker so resume and the policy know the session has no checkout;
  • a tool surface without git_* tools.

A repo with no .omp/forgejo.yaml already works: readRepoFile returns data: null on 404 (apps/server/src/forgejo/dispatch-api.ts:455).

Approach

Steps 1–3 are independent of each other. Step 4 depends on 1–3, and step 5 depends on 4.

  1. Mark workspace-less conversation scopes (apps/server/src/bridge/session-policy.ts)

    • Add an optional field to ForgejoConversationScope. Its doc comment should say: "Set for sessions of a repository that matches no workspace: the session runs in an empty directory (no code, no git) and its Forgejo tools are bound to owner/repo instead of the cwd's origin. Absent on every other session."

      noCheckout?: true;
      
    • The field is persisted automatically, because insertSessionTrigger stores JSON.stringify(trigger.scope) and getSessionTrigger returns the parsed object as-is (apps/server/src/db/session-triggers.ts). No migration is needed.

    • Add const CONVERSATION_NO_CHECKOUT_TOOL_NAMES = CONVERSATION_TOOL_NAMES.filter((n) => !n.startsWith("git_"));.

    • policyToolNames, forgejo_conversation case: return CONVERSATION_NO_CHECKOUT_TOOL_NAMES when trigger.scope.noCheckout, else toolNames as today.

    • forgejoConversationPolicy.decide: before the final this.toolNames.includes check, add

      if (scope.noCheckout && toolName.startsWith("git_")) return { deny: "this session has no checkout" };
      

      The read-tool confinement stays as is: with cwd being the empty dir, read still reaches http(s)/local:///artifact://.

    • forgejoConversationPolicy.systemContext: when scope.noCheckout, replace READ_ACCESS_BULLET with a new constant NO_CHECKOUT_BULLET:

      "- This repository is not connected to an omp-deck workspace, so you have no checkout and no git tools. Use forgejo_read_file_at_ref (then read on the snapshot), forgejo_read_pr and forgejo_get_pr_diff when you need repository content."

      Also change the opening sentence's "comment or a label change" to "comment" in that case, since labels never start these sessions.

    • repoMismatch needs no change. Step 2 makes the bound repo equal to the scope.

  2. Bind Forgejo tools to an explicit repo (apps/server/src/forgejo/tools/index.ts)

    • Extract the body after repo resolution in buildForgejoTools (building ctx, visibility, tools, system context) into:
      export async function buildForgejoToolsForRepo(repo: RepoRef, cwd: string, signal?: AbortSignal): Promise<ForgejoToolsResult>
      
      It reads readForgejoConfig() itself and returns EMPTY when that is unset, with the same debug log and try/catch → EMPTY.
    • buildForgejoTools(cwd, signal) becomes: config check, resolveRepoFromCwd, then buildForgejoToolsForRepo(repo, cwd, signal).
    • No equivalent exists today. Every caller resolves from the cwd.
  3. Empty checkout dir (apps/server/src/forgejo/checkout.ts)

    • Add:
      export async function createEmptyCheckout(opts: { owner: string; repo: string; kind: CheckoutKind; number: number }): Promise<string>
      
      • dest = checkoutPath(...).
      • fs.promises.rm(dest, { recursive: true, force: true }), then mkdir(dest, { recursive: true }), then return dest. This replaces a leftover dir, mirroring createCheckout.
    • Add:
      export async function restoreEmptyCheckout(dir: string, opts: { owner; repo; kind; number }): Promise<void>
      
      • Throw checkout ${dir} is not at ${expected} when path.resolve(dir) !== checkoutPath(...), like restoreCheckout.
      • Otherwise run mkdir(dir, { recursive: true }), leaving existing contents alone.
    • Using the checkout root means removeCheckout (on session delete in routes.ts:216 and archive in session-archive.ts:20) cleans these up with no changes.
  4. Bridge create/resume (apps/server/src/bridge/in-process.ts)

    • Add a local helper:
      function noCheckoutRepo(trigger: SessionTrigger | null): RepoRef | null
      
      It returns { owner, repo } from the scope when trigger?.kind === "forgejo_conversation" && trigger.scope.noCheckout, else null.
    • createSession (~280): if noCheckoutRepo(opts.trigger ?? null) is non-null, call buildForgejoToolsForRepo(thatRepo, opts.cwd); otherwise call buildForgejoTools(opts.cwd).
      • assertTriggerRepo stays and now passes.
      • buildGitTools(opts.cwd, forgejoCtx) is still constructed. The restricted tool list from policyToolNames excludes git_*, so they are never registered.
    • resumeSession (~385): before the existing if (trigger && placement?.checkoutPath) block, handle workspace-less sessions:
      • Condition: trigger?.kind === "forgejo_conversation" && trigger.scope.noCheckout && placement?.checkoutPath.
      • Action: call restoreEmptyCheckout(placement.checkoutPath, { owner, repo, number: trigger.scope.number, kind: trigger.scope.isPull ? "pr" : "issue" }) and skip the workspace lookup, which today throws "belongs to a workspace that no longer exists".
      • Structure: turn the existing block into the else if.
    • Tool binding on resume (~418): same noCheckoutRepo switch as create.
    • Portainer: placement.workspaceId is null, so buildPortainerTools(null) already yields no tools. Nothing to change.
  5. Dispatcher fallback (apps/server/src/forgejo/dispatcher.ts)

    • Ready.workspace: change it to Workspace | null. Update its doc comment ("matched to one workspace, or to none for a workspace-less conversation").
    • Gate (~652):
      • Compute agentPr as today.
      • When workspaces.length === 0, continue only if CONVERSATION_REASONS.has(reason) && !(row.isPull && agentPr).
      • Otherwise keep the existing NOT_CONNECTED notice and the "repository is not connected to a workspace" drop.
      • Pass workspace: workspaces[0] ?? null into the returned Ready (~725).
    • The othersPrHead block (~700): skip it when workspaces.length === 0, leaving othersPrHead = null. With no checkout there is nothing to push from, and skipping also avoids failing on config reads.
    • resolveSession (~1700), when ready.workspace === null:
      • Trigger:
        { kind: "forgejo_conversation", scope: { owner, repo, number: row.number, isPull: row.isPull, noCheckout: true } }
        
        Use this for both issues and PRs. prHead is always null here because agent PRs were excluded at the gate.
      • Dir: cwd = await createEmptyCheckout({ owner, repo, kind: row.isPull ? "pr" : "issue", number: row.number }), keeping the same could not prepare a checkout: error wrapping.
      • Session: createSession({ cwd, trigger, placement: { workspaceId: null, checkoutPath: cwd } }). Run removeCheckout(cwd) on failure, as today.
      • The rest (mapping, naming Forgejo: owner/repo#n) is unchanged.
      • The workspace path keeps the current code.
    • resolveBranchWatchSession: it only runs for branch reasons, which never reach it with a null workspace. Add a guard that throws "branch watch needs a workspace" when ready.workspace === null, to satisfy the type.
    • reuseMapped (~1820), after loading trigger:
      • If trigger.kind === "forgejo_conversation" && trigger.scope.noCheckout && ready.workspace !== null, return "the repository is now connected to a workspace". The caller logs it and creates a normal session.
      • The cwd check at ~1863 becomes getSessionPlacement(sessionId)?.checkoutPath ?? ready.workspace?.path. If both are null, return "it has no checkout".
    • Fix any other ready.workspace uses the compiler flags the same way: null means the workspace-less path.

Critical files & anchors

  • apps/server/src/forgejo/dispatcher.ts: the matchWorkspaces gate (~652), Ready (~356), resolveSession (~1677), reuseMapped (~1816).
  • apps/server/src/bridge/in-process.ts: createSession (~273) and resumeSession (~370–430), the tool binding and checkout restore.
  • apps/server/src/bridge/session-policy.ts: ForgejoConversationScope, forgejoConversationPolicy, policyToolNames.
  • apps/server/src/forgejo/tools/index.ts: buildForgejoTools.
  • apps/server/src/forgejo/dispatcher.test.ts: the "no workspace: the notice is posted once…" test (~1194) encodes the old behaviour for a mention and must be rewritten.

Verification

Run from the repo root with bun test (or the package's test script).

apps/server/src/forgejo/dispatcher.test.ts

  • Replace the ~1194 test with one where h.workspaces = [] and a mention on issue #4 is enqueued. Expect:
    • the row is not dropped and no NOT_CONNECTED is posted;
    • h.sessions[0].opts has cwd === checkoutPath("lucas","omp-deck","issue",4);
    • the directory exists and is empty;
    • placement is { workspaceId: null, checkoutPath: cwd };
    • trigger is { kind: "forgejo_conversation", scope: { owner: "lucas", repo: "omp-deck", number: 4, isPull: false, noCheckout: true } }.
  • Same setup for a mention on a PR not authored by the agent → kind: "forgejo_conversation", isPull: true, noCheckout: true, cwd at pr-N.
  • With no workspace, a non-conversation trigger (e.g. a review request or label intake, using existing enqueue helpers) is still dropped with "repository is not connected to a workspace" and NOT_CONNECTED posted once. Keep the "posted once" assertion there.
  • Upgrade: insert a workspace-less session mapped to #4 (insertSessionTrigger with noCheckout: true, placement { workspaceId: null, checkoutPath }, plus setForgejoSession). Then set h.workspaces = [workspace("deck")] and enqueue a mention on #4. Expect:
    • a new session created via createCheckout with placement.workspaceId === ws.id;
    • the trigger has no noCheckout;
    • the mapping points to the new session.

Session-policy tests (apps/server/src/bridge/session-policy.test.ts, if present; else add cases next to existing policy tests)

  • policyToolNames for a noCheckout conversation trigger contains no git_* name and still contains forgejo_create_issue_comment.
  • decidePolicy(trigger, "git_status", {}, id, cwd) → { deny: "this session has no checkout" }.
  • policySystemContext contains NO_CHECKOUT_BULLET and not READ_ACCESS_BULLET.

checkout.test.ts

  • createEmptyCheckout replaces a leftover dir that has files with an empty dir.
  • restoreEmptyCheckout recreates a missing dir and throws for a path not at checkoutPath(...).

Manual (needs a running deck with Forgejo configured; the user's environment)

  • On a Forgejo repo with no matching workspace, mention the agent in an issue. Expect a reply comment.
  • The session appears in the global sidebar flagged as orphan. The existing orphan rendering in apps/web/src/components/Sidebar.tsx handles a cwd that matches no workspace, so no UI change is needed.
  • Restart the deck, comment again, and expect the same session to resume.

Assumptions & contingencies

  • If apps/server/src/bridge/session-policy.test.ts does not exist, put the policy cases in whichever test file currently covers policyToolNames/decidePolicy (find it with grep -l decidePolicy apps/server/src). If none exists, create session-policy.test.ts.
  • If a test fixture's PR mention is classified with a reason outside CONVERSATION_REASONS (e.g. as a review submission), choose a fixture whose detect result is "mention". The rule stays: only CONVERSATION_REASONS fall back.
## Problem When a Forgejo webhook arrives for a repository that matches no omp-deck workspace, the dispatcher posts `NOT_CONNECTED` ("This repository isn't connected to an omp-deck workspace…") and drops the trigger (`apps/server/src/forgejo/dispatcher.ts`, gate after `matchWorkspaces` at ~652). That rules out repos that are only discussion spaces and have no code: nobody can talk to the agent there. Wanted: sessions without a workspace. A trigger from an unmatched repo starts a session that: - is restricted to the conversation kind (`forgejo_conversation`, read-only plus comment and create-issue tools); - runs in an empty directory, so there is no code access and nothing on disk to read. Scope: - Only conversation-message triggers fall back to a workspace-less session: mentions, edited mentions, quoted replies, thread replies, follows-agent and agent-artifact (`CONVERSATION_REASONS`), on issues and PRs alike. - Every other event on an unmatched repo keeps posting `NOT_CONNECTED`: review requests, auto-review, CI, pushes, labels, intake, merges, and anything on a PR the agent authored. - The fallback is always on, with no setting. - Sessions start only from webhooks; there is no UI or API entry point for creating them. - Once the repo is connected to exactly one workspace, an existing workspace-less session for an issue/PR is treated as unusable and replaced by a normal session in a real checkout on the next trigger. - More than one matching workspace still gives `AMBIGUOUS`, unchanged. - Remote-cloned checkouts for unmatched repos (enabling reviews/implementer without a workspace) are deliberately left to a separate issue. ## Plan ### Context Today a trigger session's Forgejo repo binding comes from its cwd's git `origin`: - `buildForgejoTools(cwd)` (`apps/server/src/forgejo/tools/index.ts:66`) derives the repo that way. - `assertTriggerRepo` (`apps/server/src/bridge/session-policy.ts:633`) refuses the session if the derived repo doesn't match the trigger scope. An empty dir has no origin, so workspace-less sessions must bind Forgejo tools to the trigger scope's `owner/repo` explicitly. They also need: - a persisted marker so resume and the policy know the session has no checkout; - a tool surface without `git_*` tools. A repo with no `.omp/forgejo.yaml` already works: `readRepoFile` returns `data: null` on 404 (`apps/server/src/forgejo/dispatch-api.ts:455`). ### Approach Steps 1–3 are independent of each other. Step 4 depends on 1–3, and step 5 depends on 4. 1. **Mark workspace-less conversation scopes** (`apps/server/src/bridge/session-policy.ts`) - Add an optional field to `ForgejoConversationScope`. Its doc comment should say: *"Set for sessions of a repository that matches no workspace: the session runs in an empty directory (no code, no git) and its Forgejo tools are bound to `owner/repo` instead of the cwd's origin. Absent on every other session."* ```ts noCheckout?: true; ``` - The field is persisted automatically, because `insertSessionTrigger` stores `JSON.stringify(trigger.scope)` and `getSessionTrigger` returns the parsed object as-is (`apps/server/src/db/session-triggers.ts`). No migration is needed. - Add `const CONVERSATION_NO_CHECKOUT_TOOL_NAMES = CONVERSATION_TOOL_NAMES.filter((n) => !n.startsWith("git_"));`. - `policyToolNames`, `forgejo_conversation` case: return `CONVERSATION_NO_CHECKOUT_TOOL_NAMES` when `trigger.scope.noCheckout`, else `toolNames` as today. - `forgejoConversationPolicy.decide`: before the final `this.toolNames.includes` check, add ```ts if (scope.noCheckout && toolName.startsWith("git_")) return { deny: "this session has no checkout" }; ``` The read-tool confinement stays as is: with cwd being the empty dir, `read` still reaches http(s)/`local://`/`artifact://`. - `forgejoConversationPolicy.systemContext`: when `scope.noCheckout`, replace `READ_ACCESS_BULLET` with a new constant `NO_CHECKOUT_BULLET`: > "- This repository is not connected to an omp-deck workspace, so you have no checkout and no git tools. Use `forgejo_read_file_at_ref` (then `read` on the snapshot), `forgejo_read_pr` and `forgejo_get_pr_diff` when you need repository content." Also change the opening sentence's "comment or a label change" to "comment" in that case, since labels never start these sessions. - `repoMismatch` needs no change. Step 2 makes the bound repo equal to the scope. 2. **Bind Forgejo tools to an explicit repo** (`apps/server/src/forgejo/tools/index.ts`) - Extract the body after repo resolution in `buildForgejoTools` (building `ctx`, visibility, tools, system context) into: ```ts export async function buildForgejoToolsForRepo(repo: RepoRef, cwd: string, signal?: AbortSignal): Promise<ForgejoToolsResult> ``` It reads `readForgejoConfig()` itself and returns `EMPTY` when that is unset, with the same debug log and try/catch → `EMPTY`. - `buildForgejoTools(cwd, signal)` becomes: config check, `resolveRepoFromCwd`, then `buildForgejoToolsForRepo(repo, cwd, signal)`. - No equivalent exists today. Every caller resolves from the cwd. 3. **Empty checkout dir** (`apps/server/src/forgejo/checkout.ts`) - Add: ```ts export async function createEmptyCheckout(opts: { owner: string; repo: string; kind: CheckoutKind; number: number }): Promise<string> ``` - `dest = checkoutPath(...)`. - `fs.promises.rm(dest, { recursive: true, force: true })`, then `mkdir(dest, { recursive: true })`, then return `dest`. This replaces a leftover dir, mirroring `createCheckout`. - Add: ```ts export async function restoreEmptyCheckout(dir: string, opts: { owner; repo; kind; number }): Promise<void> ``` - Throw `checkout ${dir} is not at ${expected}` when `path.resolve(dir) !== checkoutPath(...)`, like `restoreCheckout`. - Otherwise run `mkdir(dir, { recursive: true })`, leaving existing contents alone. - Using the checkout root means `removeCheckout` (on session delete in `routes.ts:216` and archive in `session-archive.ts:20`) cleans these up with no changes. 4. **Bridge create/resume** (`apps/server/src/bridge/in-process.ts`) - Add a local helper: ```ts function noCheckoutRepo(trigger: SessionTrigger | null): RepoRef | null ``` It returns `{ owner, repo }` from the scope when `trigger?.kind === "forgejo_conversation" && trigger.scope.noCheckout`, else `null`. - `createSession` (~280): if `noCheckoutRepo(opts.trigger ?? null)` is non-null, call `buildForgejoToolsForRepo(thatRepo, opts.cwd)`; otherwise call `buildForgejoTools(opts.cwd)`. - `assertTriggerRepo` stays and now passes. - `buildGitTools(opts.cwd, forgejoCtx)` is still constructed. The restricted tool list from `policyToolNames` excludes `git_*`, so they are never registered. - `resumeSession` (~385): before the existing `if (trigger && placement?.checkoutPath)` block, handle workspace-less sessions: - **Condition:** `trigger?.kind === "forgejo_conversation" && trigger.scope.noCheckout && placement?.checkoutPath`. - **Action:** call `restoreEmptyCheckout(placement.checkoutPath, { owner, repo, number: trigger.scope.number, kind: trigger.scope.isPull ? "pr" : "issue" })` and skip the workspace lookup, which today throws "belongs to a workspace that no longer exists". - **Structure:** turn the existing block into the `else if`. - Tool binding on resume (~418): same `noCheckoutRepo` switch as create. - Portainer: `placement.workspaceId` is `null`, so `buildPortainerTools(null)` already yields no tools. Nothing to change. 5. **Dispatcher fallback** (`apps/server/src/forgejo/dispatcher.ts`) - `Ready.workspace`: change it to `Workspace | null`. Update its doc comment ("matched to one workspace, or to none for a workspace-less conversation"). - Gate (~652): - Compute `agentPr` as today. - When `workspaces.length === 0`, continue only if `CONVERSATION_REASONS.has(reason) && !(row.isPull && agentPr)`. - Otherwise keep the existing `NOT_CONNECTED` notice and the `"repository is not connected to a workspace"` drop. - Pass `workspace: workspaces[0] ?? null` into the returned `Ready` (~725). - The `othersPrHead` block (~700): skip it when `workspaces.length === 0`, leaving `othersPrHead = null`. With no checkout there is nothing to push from, and skipping also avoids failing on config reads. - `resolveSession` (~1700), when `ready.workspace === null`: - Trigger: ```ts { kind: "forgejo_conversation", scope: { owner, repo, number: row.number, isPull: row.isPull, noCheckout: true } } ``` Use this for both issues and PRs. `prHead` is always null here because agent PRs were excluded at the gate. - Dir: `cwd = await createEmptyCheckout({ owner, repo, kind: row.isPull ? "pr" : "issue", number: row.number })`, keeping the same `could not prepare a checkout:` error wrapping. - Session: `createSession({ cwd, trigger, placement: { workspaceId: null, checkoutPath: cwd } })`. Run `removeCheckout(cwd)` on failure, as today. - The rest (mapping, naming `Forgejo: owner/repo#n`) is unchanged. - The workspace path keeps the current code. - `resolveBranchWatchSession`: it only runs for branch reasons, which never reach it with a null workspace. Add a guard that throws `"branch watch needs a workspace"` when `ready.workspace === null`, to satisfy the type. - `reuseMapped` (~1820), after loading `trigger`: - If `trigger.kind === "forgejo_conversation" && trigger.scope.noCheckout && ready.workspace !== null`, return `"the repository is now connected to a workspace"`. The caller logs it and creates a normal session. - The cwd check at ~1863 becomes `getSessionPlacement(sessionId)?.checkoutPath ?? ready.workspace?.path`. If both are null, return `"it has no checkout"`. - Fix any other `ready.workspace` uses the compiler flags the same way: null means the workspace-less path. ### Critical files & anchors - `apps/server/src/forgejo/dispatcher.ts`: the `matchWorkspaces` gate (~652), `Ready` (~356), `resolveSession` (~1677), `reuseMapped` (~1816). - `apps/server/src/bridge/in-process.ts`: `createSession` (~273) and `resumeSession` (~370–430), the tool binding and checkout restore. - `apps/server/src/bridge/session-policy.ts`: `ForgejoConversationScope`, `forgejoConversationPolicy`, `policyToolNames`. - `apps/server/src/forgejo/tools/index.ts`: `buildForgejoTools`. - `apps/server/src/forgejo/dispatcher.test.ts`: the `"no workspace: the notice is posted once…"` test (~1194) encodes the old behaviour for a mention and must be rewritten. ### Verification Run from the repo root with `bun test` (or the package's test script). **`apps/server/src/forgejo/dispatcher.test.ts`** - Replace the ~1194 test with one where `h.workspaces = []` and a mention on issue #4 is enqueued. Expect: - the row is not dropped and no `NOT_CONNECTED` is posted; - `h.sessions[0].opts` has `cwd === checkoutPath("lucas","omp-deck","issue",4)`; - the directory exists and is empty; - `placement` is `{ workspaceId: null, checkoutPath: cwd }`; - `trigger` is `{ kind: "forgejo_conversation", scope: { owner: "lucas", repo: "omp-deck", number: 4, isPull: false, noCheckout: true } }`. - Same setup for a mention on a PR not authored by the agent → `kind: "forgejo_conversation"`, `isPull: true`, `noCheckout: true`, cwd at `pr-N`. - With no workspace, a non-conversation trigger (e.g. a review request or label intake, using existing enqueue helpers) is still dropped with `"repository is not connected to a workspace"` and `NOT_CONNECTED` posted once. Keep the "posted once" assertion there. - Upgrade: insert a workspace-less session mapped to `#4` (`insertSessionTrigger` with `noCheckout: true`, placement `{ workspaceId: null, checkoutPath }`, plus `setForgejoSession`). Then set `h.workspaces = [workspace("deck")]` and enqueue a mention on #4. Expect: - a new session created via `createCheckout` with `placement.workspaceId === ws.id`; - the trigger has no `noCheckout`; - the mapping points to the new session. **Session-policy tests** (`apps/server/src/bridge/session-policy.test.ts`, if present; else add cases next to existing policy tests) - `policyToolNames` for a `noCheckout` conversation trigger contains no `git_*` name and still contains `forgejo_create_issue_comment`. - `decidePolicy(trigger, "git_status", {}, id, cwd)` → `{ deny: "this session has no checkout" }`. - `policySystemContext` contains `NO_CHECKOUT_BULLET` and not `READ_ACCESS_BULLET`. **`checkout.test.ts`** - `createEmptyCheckout` replaces a leftover dir that has files with an empty dir. - `restoreEmptyCheckout` recreates a missing dir and throws for a path not at `checkoutPath(...)`. **Manual (needs a running deck with Forgejo configured; the user's environment)** - On a Forgejo repo with no matching workspace, mention the agent in an issue. Expect a reply comment. - The session appears in the global sidebar flagged as orphan. The existing `orphan` rendering in `apps/web/src/components/Sidebar.tsx` handles a cwd that matches no workspace, so no UI change is needed. - Restart the deck, comment again, and expect the same session to resume. ### Assumptions & contingencies - If `apps/server/src/bridge/session-policy.test.ts` does not exist, put the policy cases in whichever test file currently covers `policyToolNames`/`decidePolicy` (find it with `grep -l decidePolicy apps/server/src`). If none exists, create `session-policy.test.ts`. - If a test fixture's PR mention is classified with a reason outside `CONVERSATION_REASONS` (e.g. as a review submission), choose a fixture whose `detect` result is `"mention"`. The rule stays: only `CONVERSATION_REASONS` fall back.
lucas closed this issue 2026-11-03 10:08:31 +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#208
No description provided.