Skip to content
Open
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
11 changes: 10 additions & 1 deletion src/core/worktree-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,11 +383,19 @@ export function createWorktreeWorkflowService({ directory, git, stateStore, logg
return (await git(["rev-parse", "--show-toplevel"], { cwd: directory })).stdout;
} catch (error) {
if (isMissingGitRepositoryError(error.message || "")) {
throw new Error("This command must run inside a git repository. Initialize a repository first or run it from an existing repo root.");
throw new Error("This command must run inside a git repository. Initialize a repository first or run it from an existing repo root.", { cause: error });
}
throw error;
}
}
async function tryGetRepoRoot() {
try {
return await getRepoRoot();
} catch (error) {
if (isMissingGitRepositoryError(error.message || "") || isMissingGitRepositoryError(error.cause?.message || "")) return null;
throw error;
}
}
async function loadWorkflowConfig(repoRoot) {
const [projectConfig, projectConfigC, sidecarConfig] = await Promise.all([
readJsonFile(path.join(repoRoot, "opencode.json")),
Expand Down Expand Up @@ -668,6 +676,7 @@ export function createWorktreeWorkflowService({ directory, git, stateStore, logg
prepare,
cleanup,
getRepoRoot,
tryGetRepoRoot,
getSessionBinding,
ensureActiveWorktree,
recordToolUsage,
Expand Down
11 changes: 6 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,11 @@ export const WorktreeWorkflowPlugin = async ({ $, directory, logger: providedLog
const sessionID = input?.sessionID;
let binding = null;
let repoRootForLogging = null;
const repoRoot = await service.tryGetRepoRoot();
if (!repoRoot) return;

if (classification.requiresIsolation) {
if (!sessionID) throw new Error(`Isolation required for ${toolName || "tool"} but sessionID is missing.`);
const repoRoot = await service.getRepoRoot();
repoRootForLogging = repoRoot;
for (const key of rewritePolicy.opaqueArgKeys) {
if (hasOpaqueRepoRootAbsoluteReference({ value: args[key], repoRoot })) {
Expand All @@ -202,7 +203,6 @@ export const WorktreeWorkflowPlugin = async ({ $, directory, logger: providedLog
workspaceRole: deriveWorkspaceRole({ subagentType: args.subagent_type }),
});
} else if (sessionID) {
const repoRoot = await service.getRepoRoot();
repoRootForLogging = repoRoot;
const { activeTask } = await service.getSessionBinding({ repoRoot, sessionID });
if (activeTask?.worktree_path) binding = { repoRoot, task: activeTask };
Expand Down Expand Up @@ -290,15 +290,15 @@ export const WorktreeWorkflowPlugin = async ({ $, directory, logger: providedLog
const toolName = input?.tool;
const args = input?.args || {};
const sessionID = input?.sessionID;
const repoRoot = await service.tryGetRepoRoot();
if (!repoRoot) return;
if (toolName === "worktree_prepare" && sessionID) {
const result = output?.metadata?.result;
if (result?.branch && result?.worktree_path) {
const repoRoot = await service.getRepoRoot();
await service.updateStateForPrepare(repoRoot, sessionID, result, "manual");
}
}
if (toolName === "task" && sessionID) {
const repoRoot = await service.getRepoRoot();
const handoffPath = resolveSafeHandoffPath({
rawPath: extractHandoffArtifactPath(args.prompt),
repoRoot,
Expand Down Expand Up @@ -354,7 +354,8 @@ export const WorktreeWorkflowPlugin = async ({ $, directory, logger: providedLog
const existingSystem = Array.isArray(output?.system) ? output.system : [];
if (!sessionID || existingSystem.some((entry) => entry.includes(WORKSPACE_SYSTEM_CONTEXT_MARKER))) return;

const repoRoot = await service.getRepoRoot();
const repoRoot = await service.tryGetRepoRoot();
if (!repoRoot) return;
const { activeTask } = await service.getSessionBinding({ repoRoot, sessionID });
if (!activeTask?.worktree_path) return;

Expand Down
16 changes: 16 additions & 0 deletions test/hook-enforcement.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ import path from "node:path";
import { createRuntimeStateStore } from "../src/runtime/state-store.js";
import { createPlugin, createRemoteRepo, executeToolWithMetadata, git, runCommandExecuteBeforeHook, runExperimentalChatSystemTransformHook, runToolExecuteAfterHook, runToolExecuteBeforeHook } from "../test-support/helpers.js";

test("hooks are no-op outside git repository", async () => {
const fixture = await fs.mkdtemp(path.join(process.env.TMPDIR || "/tmp", "worktree-plugin-no-repo-"));
try {
const plugin = await createPlugin(fixture);
const output = { args: { filePath: "tracked.txt" } };
await plugin["tool.execute.before"]({ tool: "write", sessionID: "outside-repo" }, output);
assert.equal(output.args.filePath, "tracked.txt");
await plugin["tool.execute.after"]({ tool: "write", sessionID: "outside-repo" }, {});
const system = { system: ["Base system prompt"] };
await plugin["experimental.chat.system.transform"]({ sessionID: "outside-repo" }, system);
assert.deepEqual(system.system, ["Base system prompt"]);
} finally {
await fs.rm(fixture, { recursive: true, force: true });
}
});

test("tool.execute.before provisions worktree and rewrites mutating filePath", async () => {
const fixture = await createRemoteRepo();
const logs = [];
Expand Down