Add git_repo_info tool (local repo + Forgejo metadata) #29

Closed
opened 2026-10-16 14:22:57 +02:00 by omp-deck · 0 comments
Collaborator

Problem

Forgejo workflows often need the repo's default branch. Today the only code that reads it is the base fallback in forgejo_create_pr (apps/server/src/forgejo/tools/prs.ts, around lines 339-343). Agents have no single call that returns the basic facts about the current project. Add a read-only git_repo_info tool that the agent can call at the start of a session. It always returns local git facts. When the workspace's origin is on the configured Forgejo instance, it also returns Forgejo repo metadata. Auto-injecting this into Forgejo session prompts is deferred to the broader integration work.

Plan

Context

The new git tool is git_repo_info, which takes no parameters. It returns:

  • Local facts: repo root, current branch or detached state, HEAD sha, upstream, ahead/behind.
  • Forgejo metadata, when the workspace is connected: owner/repo, html/clone URLs, description, private/fork/archived/empty flags, and the parent repo if it is a fork.
  • A resolved default branch together with its source.

The tool also gets a dedicated web renderer, and unattended Forgejo trigger sessions are allowed to call it.

Approach

  1. Protocol type: in packages/protocol/src/git-tools.ts, add:

    export interface GitRepoInfoForgejo {
    	owner: string;
    	repo: string;
    	fullName: string;
    	htmlUrl: string;
    	cloneUrl: string;
    	sshUrl: string;
    	description: string;
    	private: boolean;
    	fork: boolean;
    	archived: boolean;
    	empty: boolean;
    	defaultBranch: string;
    	/** `owner/repo` of the fork parent; null when not a fork. */
    	parent: string | null;
    }
    export interface GitRepoInfoResult {
    	root: string;
    	branch: string | null;
    	detached: boolean;
    	/** HEAD sha, or "(initial)" on an unborn branch. */
    	oid: string;
    	upstream: string | null;
    	ahead: number | null;
    	behind: number | null;
    	defaultBranch: string | null;
    	defaultBranchSource: "forgejo" | "origin_head" | null;
    	/** null = origin is not on the configured Forgejo instance (or Forgejo is not configured). */
    	forgejo: GitRepoInfoForgejo | { error: string } | null;
    }
    

    Re-export these from the protocol package index the same way as GitStatusResult (check how git-tools.ts is re-exported and follow that pattern).

  2. Expose the Forgejo context: in apps/server/src/forgejo/tools/index.ts, add context: ForgejoToolContext | null to ForgejoToolsResult:

    • EMPTY sets it to null.
    • The success path returns the existing ctx object.
    • Import the ForgejoToolContext type from ./shared.ts.
  3. Extend ApiRepo in apps/server/src/forgejo/tools/api-types.ts with these fields:

    • html_url: string
    • clone_url: string
    • ssh_url: string
    • description: string
    • private: boolean
    • fork: boolean
    • archived: boolean
    • empty: boolean
    • parent?: { full_name: string } | null

    Keep default_branch and full_name. The existing use in prs.ts only reads default_branch and needs no change.

  4. Tool: create apps/server/src/bridge/git-tools/repo-info.ts exporting buildRepoInfoTool(cwd: string, forgejo: ForgejoToolContext | null): CustomTool<typeof schema>, where schema = z.object({}). Copy the structure of status.ts: guarded("git_repo_info", …), then resolveRepo, then notARepo, then ok(result). Description: "Repository facts for the session directory in one call: work-tree root, current branch, HEAD, upstream and ahead/behind, the default branch, and Forgejo repo metadata (URLs, visibility, fork parent) when origin is on the configured Forgejo instance. Call it at the start of repo work instead of guessing the default branch."

    The body:

    • Local facts: run runGit(repo.root, ["status", "--porcelain=v2", "--branch", "-z", "--untracked-files=no"], { readOnly: true, signal }). Feed the output to the existing parseStatusV2 (./status-parse.ts) and take only branch, detached, oid, upstream, ahead, behind. If git exits non-zero, return fail(gitErrorText(res, "git status failed")).
    • Local default branch: run runGit(repo.root, ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"], { readOnly: true, signal }). On exit code 0, strip the leading origin/ from the trimmed output. Otherwise use null. Add this as originHeadBranch(root, signal): Promise<string | null> in git-tools/repo.ts, next to currentBranch.
    • Forgejo facts, only when forgejo is non-null: call forgejo.client.json<ApiRepo>("GET", repoApiPath(forgejo.owner, forgejo.repo), { signal }), importing repoApiPath from wherever prs.ts imports it.
      • On ok, map the response to GitRepoInfoForgejo, with description ?? "" and parent: data.parent?.full_name ?? null.
      • On failure, set { error: failure.status > 0 ? ${failure.status} ${failure.message} : failure.message }. Do not fail the tool.
      • Run the local git calls and the API call concurrently with Promise.all.
    • Precedence: if the Forgejo fetch succeeded, defaultBranch is its default_branch and defaultBranchSource is "forgejo". Otherwise, if the origin HEAD lookup returned a branch, use that with "origin_head". Otherwise both are null.
  5. Wiring:

    • Change buildGitTools(cwd: string) in git-tools/index.ts to buildGitTools(cwd: string, forgejo: ForgejoToolContext | null), and append buildRepoInfoTool(cwd, forgejo) right after buildStatusTool(cwd).
    • Update both callers in apps/server/src/bridge/in-process.ts (session create, around line 252/271, and resume, around line 334/348). Destructure context: forgejoCtx from buildForgejoTools(...) and pass it as buildGitTools(cwd, forgejoCtx).
    • Run grep -rn "buildGitTools(" apps/server/src and make sure no other callers remain.
  6. Policy and prompt lists: add "git_repo_info"

    • after "git_status" in forgejoConversationPolicy.toolNames (apps/server/src/bridge/session-policy.ts), and
    • to the tool lists in GIT_BASH_RULE_MESSAGE (bridge/git-bash-rule.ts) and in the ## Git section of apps/server/src/orientation-store.ts.

    Leave idea mode alone: its blocklist only names mutating tools.

  7. Web renderer: create apps/web/src/components/tools/git/GitRepoInfoView.tsx, modelled on GitStatusView.tsx.

    • Wrap the view in GitShell, parse the result with extractJson<GitRepoInfoResult>, and add an isRepoInfo guard that checks typeof root === "string" and the presence of the forgejo key. If the guard fails, fall back to <MaybeJsonBlock text={text} />.
    • Row 1: the branch line, rendered the same way as in GitStatusView (detached → "HEAD detached @ ShaChip"; otherwise branch → upstream + AheadBehind, or "no upstream"), followed by a ShaChip for oid unless it is "(initial)", in which case show "no commits yet".
    • Row 2: "default" + the branch in monospace + a Tag showing the source (forgejo / origin/HEAD). If it is null, show "default branch unknown" in text-ink-3.
    • Row 3: root as monospace muted text.
    • Forgejo block, when the object is present:
      • a link to htmlUrl with fullName as the label;
      • Tags for private/public, fork (with "of {parent}"), archived, and empty;
      • the description, if non-empty;
      • a <details> with SUMMARY_CLASS labelled "Clone URLs" that lists cloneUrl and sshUrl.
    • For { error }, render <Warn>Forgejo: {error}</Warn>. For null, render nothing.
    • Add case "git_repo_info": return <GitRepoInfoView {...view} />; to GitTool.tsx. In apps/web/src/lib/git-format.ts, add case "git_repo_info": return ""; to summarizeGitArgs.
    • Add three fixtures to GIT_FIXTURES in apps/web/src/preview-git.ts, following the existing json(...) style:
      • "tc-git-repo-info": full Forgejo data;
      • "tc-git-repo-info-local": forgejo: null, source origin_head;
      • "tc-git-repo-info-forgejo-error".
  8. Tests: add apps/server/src/bridge/git-tools/repo-info.test.ts, using real temp git repos the same way as forgejo/repo.test.ts. Cover:

    • No Forgejo: a repo with a commit and refs/remotes/origin/HEAD → origin/main (create it with git update-ref refs/remotes/origin/main HEAD and git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main). Expect defaultBranch: "main", defaultBranchSource: "origin_head", forgejo: null.
    • Forgejo success: a stub client whose json returns { ok: true, data: { default_branch: "dev", ... } }. Expect "dev"/"forgejo", which shows that Forgejo wins over origin/HEAD, and parent mapped from parent.full_name.
    • Forgejo failure: a stub returning { ok: false, status: 500, message: "boom" }. Expect forgejo: { error: "500 boom" } and the origin_head fallback.
    • Unborn repo without origin: oid: "(initial)", defaultBranch: null, defaultBranchSource: null.
    • Non-repo directory: the result is an error.

    Check the exact ApiResult shape in forgejo/client.ts before building the stubs.

Critical files & anchors

  • apps/server/src/bridge/git-tools/status.ts: the pattern for the new tool.
  • apps/server/src/forgejo/tools/index.ts (buildForgejoTools, ForgejoToolsResult): exposes context.
  • apps/server/src/bridge/in-process.ts (around lines 252-272 and 334-349): the two buildGitTools callers.
  • apps/server/src/bridge/session-policy.ts (toolNames): the trigger allowlist.
  • apps/web/src/components/tools/git/GitStatusView.tsx: the renderer pattern (ui.tsx helpers).

Verification

  • cd apps/server && bun test src/bridge/git-tools/repo-info.test.ts: all cases pass.
  • Repo typecheck: run the workspace's typecheck script (look it up in the root package.json). It must pass, which confirms the protocol type is shared by server and web.
  • Manual: start a deck session in /workspace/omp-deck (origin is git.sclu1034.dev) and call git_repo_info.
    • Expect defaultBranchSource: "forgejo", forgejo.fullName: "lucas/omp-deck", and the card showing the branch, default branch, and a Forgejo link.
    • In a repo whose origin is GitHub, expect forgejo: null, and the card shows no Forgejo block.
  • Open the web preview fixtures tc-git-repo-info* and check all three variants render.

Assumptions

  • Forgejo permissions and merge styles and the full remote list are excluded. The requested field set is local core facts plus Forgejo repo metadata.
  • Auto-injecting into Forgejo session prompts is out of scope for now.
## Problem Forgejo workflows often need the repo's default branch. Today the only code that reads it is the `base` fallback in `forgejo_create_pr` (`apps/server/src/forgejo/tools/prs.ts`, around lines 339-343). Agents have no single call that returns the basic facts about the current project. Add a read-only `git_repo_info` tool that the agent can call at the start of a session. It always returns local git facts. When the workspace's `origin` is on the configured Forgejo instance, it also returns Forgejo repo metadata. Auto-injecting this into Forgejo session prompts is deferred to the broader integration work. ## Plan ### Context The new git tool is `git_repo_info`, which takes no parameters. It returns: - Local facts: repo root, current branch or detached state, HEAD sha, upstream, ahead/behind. - Forgejo metadata, when the workspace is connected: owner/repo, html/clone URLs, description, private/fork/archived/empty flags, and the parent repo if it is a fork. - A resolved default branch together with its source. The tool also gets a dedicated web renderer, and unattended Forgejo trigger sessions are allowed to call it. ### Approach 1. **Protocol type**: in `packages/protocol/src/git-tools.ts`, add: ```ts export interface GitRepoInfoForgejo { owner: string; repo: string; fullName: string; htmlUrl: string; cloneUrl: string; sshUrl: string; description: string; private: boolean; fork: boolean; archived: boolean; empty: boolean; defaultBranch: string; /** `owner/repo` of the fork parent; null when not a fork. */ parent: string | null; } export interface GitRepoInfoResult { root: string; branch: string | null; detached: boolean; /** HEAD sha, or "(initial)" on an unborn branch. */ oid: string; upstream: string | null; ahead: number | null; behind: number | null; defaultBranch: string | null; defaultBranchSource: "forgejo" | "origin_head" | null; /** null = origin is not on the configured Forgejo instance (or Forgejo is not configured). */ forgejo: GitRepoInfoForgejo | { error: string } | null; } ``` Re-export these from the protocol package index the same way as `GitStatusResult` (check how `git-tools.ts` is re-exported and follow that pattern). 2. **Expose the Forgejo context**: in `apps/server/src/forgejo/tools/index.ts`, add `context: ForgejoToolContext | null` to `ForgejoToolsResult`: - `EMPTY` sets it to `null`. - The success path returns the existing `ctx` object. - Import the `ForgejoToolContext` type from `./shared.ts`. 3. **Extend `ApiRepo`** in `apps/server/src/forgejo/tools/api-types.ts` with these fields: - `html_url: string` - `clone_url: string` - `ssh_url: string` - `description: string` - `private: boolean` - `fork: boolean` - `archived: boolean` - `empty: boolean` - `parent?: { full_name: string } | null` Keep `default_branch` and `full_name`. The existing use in prs.ts only reads `default_branch` and needs no change. 4. **Tool**: create `apps/server/src/bridge/git-tools/repo-info.ts` exporting `buildRepoInfoTool(cwd: string, forgejo: ForgejoToolContext | null): CustomTool<typeof schema>`, where `schema = z.object({})`. Copy the structure of `status.ts`: `guarded("git_repo_info", …)`, then `resolveRepo`, then `notARepo`, then `ok(result)`. Description: "Repository facts for the session directory in one call: work-tree root, current branch, HEAD, upstream and ahead/behind, the default branch, and Forgejo repo metadata (URLs, visibility, fork parent) when origin is on the configured Forgejo instance. Call it at the start of repo work instead of guessing the default branch." The body: - **Local facts**: run `runGit(repo.root, ["status", "--porcelain=v2", "--branch", "-z", "--untracked-files=no"], { readOnly: true, signal })`. Feed the output to the existing `parseStatusV2` (`./status-parse.ts`) and take only `branch`, `detached`, `oid`, `upstream`, `ahead`, `behind`. If git exits non-zero, return `fail(gitErrorText(res, "git status failed"))`. - **Local default branch**: run `runGit(repo.root, ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"], { readOnly: true, signal })`. On exit code 0, strip the leading `origin/` from the trimmed output. Otherwise use null. Add this as `originHeadBranch(root, signal): Promise<string | null>` in `git-tools/repo.ts`, next to `currentBranch`. - **Forgejo facts**, only when `forgejo` is non-null: call `forgejo.client.json<ApiRepo>("GET", repoApiPath(forgejo.owner, forgejo.repo), { signal })`, importing `repoApiPath` from wherever prs.ts imports it. - On `ok`, map the response to `GitRepoInfoForgejo`, with `description ?? ""` and `parent: data.parent?.full_name ?? null`. - On failure, set `{ error: failure.status > 0 ? `${failure.status} ${failure.message}` : failure.message }`. Do not fail the tool. - Run the local git calls and the API call concurrently with `Promise.all`. - **Precedence**: if the Forgejo fetch succeeded, `defaultBranch` is its `default_branch` and `defaultBranchSource` is `"forgejo"`. Otherwise, if the origin HEAD lookup returned a branch, use that with `"origin_head"`. Otherwise both are `null`. 5. **Wiring**: - Change `buildGitTools(cwd: string)` in `git-tools/index.ts` to `buildGitTools(cwd: string, forgejo: ForgejoToolContext | null)`, and append `buildRepoInfoTool(cwd, forgejo)` right after `buildStatusTool(cwd)`. - Update both callers in `apps/server/src/bridge/in-process.ts` (session create, around line 252/271, and resume, around line 334/348). Destructure `context: forgejoCtx` from `buildForgejoTools(...)` and pass it as `buildGitTools(cwd, forgejoCtx)`. - Run `grep -rn "buildGitTools(" apps/server/src` and make sure no other callers remain. 6. **Policy and prompt lists**: add `"git_repo_info"` - after `"git_status"` in `forgejoConversationPolicy.toolNames` (`apps/server/src/bridge/session-policy.ts`), and - to the tool lists in `GIT_BASH_RULE_MESSAGE` (`bridge/git-bash-rule.ts`) and in the `## Git` section of `apps/server/src/orientation-store.ts`. Leave idea mode alone: its blocklist only names mutating tools. 7. **Web renderer**: create `apps/web/src/components/tools/git/GitRepoInfoView.tsx`, modelled on `GitStatusView.tsx`. - Wrap the view in `GitShell`, parse the result with `extractJson<GitRepoInfoResult>`, and add an `isRepoInfo` guard that checks `typeof root === "string"` and the presence of the `forgejo` key. If the guard fails, fall back to `<MaybeJsonBlock text={text} />`. - **Row 1**: the branch line, rendered the same way as in GitStatusView (detached → "HEAD detached @ ShaChip"; otherwise branch → upstream + `AheadBehind`, or "no upstream"), followed by a `ShaChip` for `oid` unless it is `"(initial)"`, in which case show "no commits yet". - **Row 2**: "default" + the branch in monospace + a `Tag` showing the source (`forgejo` / `origin/HEAD`). If it is null, show "default branch unknown" in `text-ink-3`. - **Row 3**: `root` as monospace muted text. - **Forgejo block**, when the object is present: - a link to `htmlUrl` with `fullName` as the label; - `Tag`s for private/public, fork (with "of {parent}"), archived, and empty; - the description, if non-empty; - a `<details>` with `SUMMARY_CLASS` labelled "Clone URLs" that lists `cloneUrl` and `sshUrl`. - For `{ error }`, render `<Warn>Forgejo: {error}</Warn>`. For `null`, render nothing. - Add `case "git_repo_info": return <GitRepoInfoView {...view} />;` to `GitTool.tsx`. In `apps/web/src/lib/git-format.ts`, add `case "git_repo_info": return "";` to `summarizeGitArgs`. - Add three fixtures to `GIT_FIXTURES` in `apps/web/src/preview-git.ts`, following the existing `json(...)` style: - `"tc-git-repo-info"`: full Forgejo data; - `"tc-git-repo-info-local"`: `forgejo: null`, source `origin_head`; - `"tc-git-repo-info-forgejo-error"`. 8. **Tests**: add `apps/server/src/bridge/git-tools/repo-info.test.ts`, using real temp git repos the same way as `forgejo/repo.test.ts`. Cover: - **No Forgejo**: a repo with a commit and `refs/remotes/origin/HEAD` → `origin/main` (create it with `git update-ref refs/remotes/origin/main HEAD` and `git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main`). Expect `defaultBranch: "main"`, `defaultBranchSource: "origin_head"`, `forgejo: null`. - **Forgejo success**: a stub client whose `json` returns `{ ok: true, data: { default_branch: "dev", ... } }`. Expect `"dev"`/`"forgejo"`, which shows that Forgejo wins over origin/HEAD, and `parent` mapped from `parent.full_name`. - **Forgejo failure**: a stub returning `{ ok: false, status: 500, message: "boom" }`. Expect `forgejo: { error: "500 boom" }` and the origin_head fallback. - **Unborn repo without origin**: `oid: "(initial)"`, `defaultBranch: null`, `defaultBranchSource: null`. - **Non-repo directory**: the result is an error. Check the exact `ApiResult` shape in `forgejo/client.ts` before building the stubs. ### Critical files & anchors - `apps/server/src/bridge/git-tools/status.ts`: the pattern for the new tool. - `apps/server/src/forgejo/tools/index.ts` (`buildForgejoTools`, `ForgejoToolsResult`): exposes `context`. - `apps/server/src/bridge/in-process.ts` (around lines 252-272 and 334-349): the two `buildGitTools` callers. - `apps/server/src/bridge/session-policy.ts` (`toolNames`): the trigger allowlist. - `apps/web/src/components/tools/git/GitStatusView.tsx`: the renderer pattern (ui.tsx helpers). ### Verification - `cd apps/server && bun test src/bridge/git-tools/repo-info.test.ts`: all cases pass. - Repo typecheck: run the workspace's typecheck script (look it up in the root `package.json`). It must pass, which confirms the protocol type is shared by server and web. - Manual: start a deck session in `/workspace/omp-deck` (origin is git.sclu1034.dev) and call `git_repo_info`. - Expect `defaultBranchSource: "forgejo"`, `forgejo.fullName: "lucas/omp-deck"`, and the card showing the branch, default branch, and a Forgejo link. - In a repo whose origin is GitHub, expect `forgejo: null`, and the card shows no Forgejo block. - Open the web preview fixtures `tc-git-repo-info*` and check all three variants render. ### Assumptions - Forgejo permissions and merge styles and the full remote list are excluded. The requested field set is local core facts plus Forgejo repo metadata. - Auto-injecting into Forgejo session prompts is out of scope for now.
lucas closed this issue 2026-10-20 10:14:30 +02: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#29
No description provided.