acquireWorkspaceRepoWorktree now hardens each sub-repo worktree at acquisition:
(1) installs the identity guard with the executor's settings args
(commitMsgHookEnabled/taskPrefix/taskAttributionTrailerName) for single-repo
parity — it was installing no guard before; (2) captures a per-repo
baseCommitSha local-first against the repo's resolved integration branch via
resolveIntegrationBranch(repoAbsPath, {...settings, integrationBranch: undefined})
— stripping the shared override so each sub-repo falls through to its own
origin/HEAD, not a project-wide branch; (3) persists baseCommitSha into the
workspaceWorktrees[repo] entry (Task type extended); (4) registers same-sub-repo
exclusivity on the sub-repo path via activeSessionRegistry under a distinct
"workspace-repo-acquire" kind (released in finally), so two concurrent workspace
tasks contending for the same sub-repo are serialized (throws
WorkspaceRepoAcquireBusyError). Idempotent re-acquire short-circuits.
resolveCapturedBaseCommitSha gains an optional trailing integrationBranch param
defaulting to "main", so existing single-repo callers + base-commit-capture
real-git tests stay green. New audit events worktree:workspace-repo-acquire-busy
/-failed. 6 new real-fixture tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
72 lines
3.0 KiB
TypeScript
72 lines
3.0 KiB
TypeScript
import { exec } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
|
|
const execAsync = promisify(exec);
|
|
|
|
/**
|
|
* Resolve the fork-point base SHA for a freshly acquired task worktree.
|
|
*
|
|
* Called immediately after worktree acquisition, when the task branch was
|
|
* just created/force-reset from the local integration branch
|
|
* (`prepareForTask` forks from local `main` via `resolveIntegrationBranch`).
|
|
*
|
|
* The merge-base MUST be measured against LOCAL main first (origin/main only
|
|
* as a fallback), matching the contamination-base sites in
|
|
* `worktree-acquisition.ts` and `auto-recovery-handlers/branch-worktree.ts`.
|
|
* The merger lands tasks on local main before pushing, so at fork time local
|
|
* main can be ahead of origin/main by merged-but-unpushed commits. Measuring
|
|
* against origin/main rewinds the base past those commits; once the
|
|
* post-merge rebase-and-push rewrites their SHAs, `baseCommitSha..HEAD`
|
|
* permanently sweeps the predecessors' files into this task's diff (FN-5937:
|
|
* in-review tasks showing 31 "files changed" instead of 12).
|
|
*
|
|
* Returns `undefined` only when every git invocation fails (caller treats a
|
|
* missing base as non-fatal).
|
|
*
|
|
* FNXC:Workspace 2026-06-21-20:10:
|
|
* `integrationBranch` is an OPTIONAL TRAILING param defaulting to the historic
|
|
* "main" literal so the single-repo executor caller and the real-git tests stay
|
|
* green without change. Workspace mode (U2/KTD3) passes each sub-repo's RESOLVED
|
|
* integration branch so per-repo base capture forks against the right branch
|
|
* instead of a hardcoded "main". The local-first ordering (merge-base HEAD
|
|
* <local> then origin/<branch>) is preserved per-branch to keep the
|
|
* inflation-prevention invariant (FN-5937) intact for non-main integration
|
|
* branches too.
|
|
*/
|
|
export async function resolveCapturedBaseCommitSha(
|
|
worktreePath: string,
|
|
logger?: { warn: (msg: string) => void },
|
|
integrationBranch: string = "main",
|
|
): Promise<string | undefined> {
|
|
const branch = integrationBranch.trim() || "main";
|
|
// Shell-quote defensively; integration branch names are normalized upstream
|
|
// but may carry slashes (e.g. "release/2026-06") that are valid in refs.
|
|
const localRef = JSON.stringify(branch);
|
|
const originRef = JSON.stringify(`origin/${branch}`);
|
|
let baseCommitSha: string | undefined;
|
|
try {
|
|
const { stdout } = await execAsync(
|
|
`git merge-base HEAD ${localRef} 2>/dev/null || git merge-base HEAD ${originRef}`,
|
|
{ cwd: worktreePath, encoding: "utf-8" },
|
|
);
|
|
baseCommitSha = stdout.trim() || undefined;
|
|
} catch (err: unknown) {
|
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
logger?.warn(`merge-base failed, falling back to HEAD: ${errorMessage}`);
|
|
}
|
|
|
|
if (!baseCommitSha) {
|
|
try {
|
|
const { stdout } = await execAsync("git rev-parse HEAD", {
|
|
cwd: worktreePath,
|
|
encoding: "utf-8",
|
|
});
|
|
baseCommitSha = stdout.trim() || undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
return baseCommitSha;
|
|
}
|