JuliusBrussee/caveman · error · Error
base and head must be full Git object IDs
Error message
base and head must be full Git object IDs
What it means
checkProfileScope requires base and head to be full Git object IDs (40-char hex SHAs) validated by REVISION_RE, and throws 'base and head must be full Git object IDs' otherwise. The gate deliberately rejects symbolic refs, short SHAs, and branch names so its cat-file/diff plumbing commands operate on unambiguous commits.
Solutions
- Resolve refs to full SHAs before calling: git rev-parse <ref>^{commit}
- In CI, use the event's commit SHA variables (e.g. GITHUB_SHA, or actions/checkout's fetch-depth + rev-parse) instead of branch names
- Reject/normalize short SHAs by expanding them with git rev-parse
- Validate input length (40 hex chars, 64 for SHA-256 repos) before invoking the gate
Example fix
// before
checkProfileScope({ base: 'main', head: 'HEAD' })
// after
const base = exec('git rev-parse main^{commit}').trim();
const head = exec('git rev-parse HEAD^{commit}').trim();
checkProfileScope({ base, head }) Defensive patterns
Strategy: validation
Validate before calling
const REVISION_RE = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/;
if (!REVISION_RE.test(base) || !REVISION_RE.test(head)) {
throw new Error(`base/head must be full object IDs, got base=${base} head=${head}`);
} Type guard
const isFullOid = (s) => typeof s === 'string' && /^[0-9a-f]{40}$|^[0-9a-f]{64}$/.test(s); Try / catch
try { checkProfileScope({ base, head }); } catch (e) {
if (e.message.includes('full Git object IDs')) {
base = revParse(base); head = revParse(head); // resolve refs to SHAs and retry
return checkProfileScope({ base, head });
} throw e;
} Prevention
- Always git rev-parse refs to full SHAs before invoking the gate
- In CI use commit-SHA event variables, not branch names
- Never pass ranges, short SHAs, or HEAD~n forms
- Add a pre-call assertion with a clear error naming the offending value
When it happens
Trigger: Calling checkProfileScope({ base, head }) with branch names ('main'), short SHAs ('abc123'), ranges ('main...head'), annotated ref names, or refs like 'HEAD~1'.
Common situations: CI systems exporting branch names instead of commit SHAs into base/head variables, GitHub Actions events providing refs rather than SHAs, or hand-testing with abbreviated hashes.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- at least one report path and the canonical span fixture…
- at least one sink id is required
- cave_live_eval_sandbox_profile_invalid
- caveman agent: entry must export default agent()
- --days must be an integer from 0 to 3660
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/ac5085e6b5b8f06a.
Report an issue: GitHub.
Appendix: source
Thrown at agents/check-profile-scope.mjs:46
}
function requireGit(result, operation) {
if (!result.error && result.status === 0) return result.stdout;
const detail = result.stderr?.toString().trim() || result.error?.message || `exit ${result.status}`;
throw new Error(`${operation} failed: ${detail}`);
}
function isConcreteProfile(path) {
return path !== SCHEMA_PATH && /^agents\/profiles\/[a-z0-9][a-z0-9-]*\.json$/.test(path);
}
function isAllowedConcreteProfileCommitPath(path) {
return isConcreteProfile(path) || GENERATED_PATHS.has(path);
}
export function checkProfileScope({ base, head, cwd = process.cwd() }) {
if (!REVISION_RE.test(base) || !REVISION_RE.test(head)) {
throw new Error("base and head must be full Git object IDs");
}
const baseRegistry = runGit(cwd, ["cat-file", "-e", `${base}:agents/agents.json`]);
if (baseRegistry.error || baseRegistry.status !== 0) {
return { ok: true, skipped: true, message: "profile registry absent at base (initial import); skipping lane-scope gate" };
}
const commitsText = requireGit(runGit(cwd, ["rev-list", "--reverse", `${base}..${head}`]), "git rev-list");
const commits = commitsText.split(/\r?\n/).filter(Boolean);
let contractTouched = false;
for (const commit of commits) {
const changedBuffer = requireGit(
// -m takes the union across merge parents; otherwise a merge commit can hide
// an out-of-scope path from an ordinary single-parent diff-tree view.
runGit(cwd, ["diff-tree", "--root", "-m", "--no-commit-id", "--name-only", "-r", "-z", commit], null),
`git diff-tree ${commit}`,
);View on GitHub (pinned to 3ee70a1026)