Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 30 additions & 19 deletions agent-support/opencode/git-ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* @see https://opencode.ai/docs/plugins/
*/

import type { Plugin } from "@opencode-ai/plugin"
import { Plugin } from "@opencode-ai/plugin"
import { spawn } from "child_process"
import { readFile, stat } from "fs/promises"
import { dirname, isAbsolute, join, resolve } from "path"
Expand Down Expand Up @@ -146,10 +146,18 @@ const extractFilePaths = (args: unknown, cwd?: string): string[] => {
type ToolHookInput = {
tool?: unknown
sessionID?: unknown
callID?: unknown
args?: unknown
id?: unknown
input?: unknown
}

type ToolHookOutput = {
status?: "completed" | "error"
result?: { metadata?: unknown }
error?: unknown
}

type PluginContext = Parameters<NonNullable<Plugin.Plugin["setup"]>>[0]

const asRecord = (value: unknown): Record<string, unknown> | undefined => {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined
Expand Down Expand Up @@ -258,18 +266,21 @@ const runCheckpoint = (hookInput: string): Promise<void> => {
})
}

export const GitAiPlugin: Plugin = async (ctx) => {
export const GitAiPlugin = Plugin.define({
id: "git-ai",
setup: async (ctx) => {
try {
return createGitAiPlugin(ctx)
const hooks = createGitAiPlugin(ctx)
await ctx.tool.hook("execute.before", hooks.before)
await ctx.tool.hook("execute.after", hooks.after)
} catch (error) {
debugLog("failed to initialize plugin", error)
return {}
}
}
},
})

const createGitAiPlugin = (ctx: Parameters<Plugin>[0]): Awaited<ReturnType<Plugin>> => {
const { worktree, directory } = ctx
const defaultCwd = worktree || directory || process.cwd()
const createGitAiPlugin = (ctx: PluginContext) => {
const defaultCwd = process.cwd()
Comment on lines +282 to +283

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Repo resolution drops project worktree fallback

defaultCwd is now set to process.cwd() and the plugin context's directory/worktree are never read, unlike the prior worktree || directory || process.cwd(). When the OpenCode process runs outside the project root, shell-tool checkpoints have no file path to resolve from, so resolveRepoDir fails to locate the repo and the checkpoint is silently skipped.

Prompt for agents
In createGitAiPlugin (agent-support/opencode/git-ai.ts), defaultCwd was changed from `worktree || directory || process.cwd()` to just `process.cwd()`, and the ctx parameter is now unused. If the V2 setup context still exposes the project directory/worktree (verify against the @opencode-ai/plugin beta types for the setup input, e.g. fields like `directory`, `worktree`, or `project`), restore the fallback so repo resolution prefers the project root. This matters for shell/bash tool checkpoints, which have no file path and otherwise fall back to process.cwd(), silently skipping the checkpoint when the OpenCode process cwd differs from the project root.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed this remains a valid issue in the current head 238561af: defaultCwd still uses only process.cwd(), so shell-tool checkpoints can miss the OpenCode project when the process cwd differs from the project worktree. This needs a follow-up change to restore the V2 context project-directory/worktree fallback and add coverage for that launch layout.


// Track pending calls by callID so we can reference them in the after hook
const pendingCalls = new Map<string, { repoDir: string; sessionID: string; toolInput: unknown }>()
Expand Down Expand Up @@ -443,19 +454,19 @@ const createGitAiPlugin = (ctx: Parameters<Plugin>[0]): Awaited<ReturnType<Plugi
}

return {
"tool.execute.before": swallowHookErrors(
before: swallowHookErrors(
"pre-tool checkpoint failed",
async (input: ToolHookInput, output?: { args?: unknown }) => {
async (input: ToolHookInput) => {
const toolName = hookString(input.tool)
const isTrackedEdit = isEditTool(toolName)
const isTrackedBash = isBashTool(toolName)
if (!isTrackedEdit && !isTrackedBash) {
return
}

const callID = hookString(input.callID)
const callID = hookString(input.id)
const sessionID = hookString(input.sessionID)
const toolInput = output?.args ?? input.args
const toolInput = input.input
const toolCwd = resolveCwd(extractToolCwd(asRecord(toolInput)))
const filePaths = isTrackedEdit ? extractFilePaths(toolInput, toolCwd) : []
const repoDir = await resolveRepoDir(filePaths, toolCwd)
Expand All @@ -477,15 +488,15 @@ const createGitAiPlugin = (ctx: Parameters<Plugin>[0]): Awaited<ReturnType<Plugi
},
),

"tool.execute.after": swallowHookErrors(
after: swallowHookErrors(
"post-tool checkpoint failed",
async (input: ToolHookInput, output?: { metadata?: unknown }) => {
async (input: ToolHookInput & ToolHookOutput) => {
const toolName = hookString(input.tool)
if (!isEditTool(toolName) && !isBashTool(toolName)) {
return
}

const callID = hookString(input.callID)
const callID = hookString(input.id)
const callInfo = pendingCalls.get(callID)
pendingCalls.delete(callID)

Expand All @@ -494,8 +505,8 @@ const createGitAiPlugin = (ctx: Parameters<Plugin>[0]): Awaited<ReturnType<Plugi
return
}

const toolCwd = resolveCwd(extractToolCwd(asRecord(input.args)))
const metadataFilePaths = extractMetadataFilePaths(output?.metadata, toolCwd)
const toolCwd = resolveCwd(extractToolCwd(asRecord(input.input)))
const metadataFilePaths = extractMetadataFilePaths(input.result?.metadata, toolCwd)
const toolInput = withMetadataFilePaths(callInfo.toolInput, metadataFilePaths)

const hookInput = JSON.stringify({
Expand Down
Loading