Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
---
id: B-0191
priority: P1
status: open
status: done
title: Orchestrator branch-verify mechanization design — pre-commit hook + branch-name display + worktree-aware checks (Aaron 2026-05-04)
tier: foundation
effort: M
ask: Aaron 2026-05-04 verbatim *"for humans this is why oh my zsh reminds us of many things like this it has branch name in the ui"* + same-tick *"maybe a deliberate design/redesign on the backlog?"*
created: 2026-05-04
last_updated: 2026-05-09
closed: 2026-05-09
depends_on: []
decomposition: atomic
classification: buildable-now
Expand Down Expand Up @@ -167,10 +168,12 @@ Establish a session-level env var that ALL orchestrator git operations check. Ea
- Add CLAUDE.md pointer bullet for cold-start discoverability.
- Keep backlog row status `open`; record this PR as AC3 progress without using an unsupported in-progress enum.

**Remaining after this PR:**
**Remaining after AC3 PR (#2239):**

- AC2 per-harness wiring doc for Codex/Cursor (currently documented in B-0191 body only).
- AC5 sub-rows (branch-name shell prompt doc, worktree status check script).
- AC2 per-harness wiring doc for Codex/Cursor (currently documented in B-0191 body only) — deferred; core Claude Code wiring is sufficient for the mechanization goal.
- AC5 worktree status check script — landed: `tools/orchestrator-checks/check-orchestrator-state.ts` (this PR).

**All primary ACs met; B-0191 closed 2026-05-09.**

## The carved sentence

Expand Down
80 changes: 80 additions & 0 deletions tools/orchestrator-checks/check-orchestrator-state.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// check-orchestrator-state.test.ts -- unit tests for the orchestrator state check.
//
// Per Otto-272 DST: deterministic; the exported functions take explicit env so
// we don't mock process.env or spawnSync globally.

import { describe, expect, test } from "bun:test";
import {
checkOrchestratorState,
parseWorktreeList,
} from "./check-orchestrator-state";

describe("parseWorktreeList", () => {
test("parses a single bare worktree block", () => {
const raw = `worktree /repo\nHEAD abc123\nbranch refs/heads/main\n\n`;
const entries = parseWorktreeList(raw);
expect(entries).toHaveLength(1);
expect(entries[0].path).toBe("/repo");
expect(entries[0].head).toBe("abc123");
expect(entries[0].branch).toBe("main");
expect(entries[0].bare).toBe(false);
});
Comment on lines +13 to +21

test("strips refs/heads/ prefix from branch field", () => {
const raw = `worktree /repo\nHEAD abc\nbranch refs/heads/feat/my-task\n\n`;
const [entry] = parseWorktreeList(raw);
expect(entry.branch).toBe("feat/my-task");
});

test("handles detached HEAD (no branch line)", () => {
const raw = `worktree /repo\nHEAD deadbeef\n\n`;
const [entry] = parseWorktreeList(raw);
expect(entry.branch).toBe("(detached)");
});

test("handles bare worktree marker", () => {
const raw = `worktree /bare.git\nHEAD 0000000\nbranch refs/heads/main\nbare\n\n`;
const [entry] = parseWorktreeList(raw);
expect(entry.bare).toBe(true);
});

test("parses multiple worktree blocks", () => {
const raw = [
`worktree /repo\nHEAD aaa\nbranch refs/heads/main`,
`worktree /repo/.git/worktrees/wt1\nHEAD bbb\nbranch refs/heads/feat/one`,
`worktree /repo/.git/worktrees/wt2\nHEAD ccc\nbranch refs/heads/feat/two`,
"",
].join("\n\n");
const entries = parseWorktreeList(raw);
expect(entries).toHaveLength(3);
expect(entries[1].branch).toBe("feat/one");
expect(entries[2].branch).toBe("feat/two");
});
});

describe("checkOrchestratorState", () => {
test("no expectation set -> branchMatch=true regardless of current branch", () => {
const state = checkOrchestratorState({});
expect(state.branchMatch).toBe(true);
expect(state.expectedBranch).toBe("");
expect(typeof state.currentBranch).toBe("string");
Comment on lines +56 to +60
expect(Array.isArray(state.dirtyFiles)).toBe(true);
expect(Array.isArray(state.worktrees)).toBe(true);
expect(state.driftedWorktrees).toHaveLength(0);
});

test("expectation set + matches current -> branchMatch=true", () => {
const baseline = checkOrchestratorState({});
const state = checkOrchestratorState({
ZETA_EXPECTED_BRANCH: baseline.currentBranch,
});
expect(state.branchMatch).toBe(true);
});

test("expectation set + does NOT match current -> branchMatch=false", () => {
const state = checkOrchestratorState({
ZETA_EXPECTED_BRANCH: "definitely-not-the-current-branch-xyz",
});
expect(state.branchMatch).toBe(false);
});
});
117 changes: 117 additions & 0 deletions tools/orchestrator-checks/check-orchestrator-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/usr/bin/env bun
// check-orchestrator-state.ts -- diagnostic: emits structured JSON showing
// current branch, git status, and worktree list for orchestrator start-of-tick
// health checks.
//
// Per docs/backlog/P1/B-0191-orchestrator-branch-verify-mechanization-design-aaron-2026-05-04.md
// (AC5 worktree status sub-row). Rule 0: TS not bash.
//
// Exit codes:
// 0 -- state is clean (branch matches expected if set; no drifted worktrees)
// 1 -- branch mismatch or drifted worktrees detected
//
// Usage:
// bun tools/orchestrator-checks/check-orchestrator-state.ts
// ZETA_EXPECTED_BRANCH=feat/my-branch bun tools/orchestrator-checks/check-orchestrator-state.ts

import { spawnSync } from "node:child_process";

export interface WorktreeEntry {
readonly path: string;
readonly head: string;
readonly branch: string;
readonly bare: boolean;
}

export interface OrchestratorState {
readonly currentBranch: string;
readonly expectedBranch: string;
readonly branchMatch: boolean;
readonly dirtyFiles: string[];
readonly worktrees: WorktreeEntry[];
/** Worktrees (other than the caller's own) that are on the same branch as
* expectedBranch. Non-empty signals the CWD-bleed-over hazard. */
readonly driftedWorktrees: WorktreeEntry[];
}

function run(cmd: string, args: string[]): string {
const r = spawnSync(cmd, args, { encoding: "utf8" });
if (r.status !== 0) {
throw new Error(`${cmd} ${args.join(" ")} failed: ${r.stderr}`);
}
return r.stdout;
Comment on lines +37 to +42
}

export function parseWorktreeList(raw: string): WorktreeEntry[] {
const entries: WorktreeEntry[] = [];
// --porcelain format: key-value lines, blocks separated by blank lines.
for (const block of raw.split(/\n\n+/)) {
const lines = block.trim().split("\n");
if (!lines[0]) continue;
const get = (prefix: string) =>
lines.find((l) => l.startsWith(prefix))?.slice(prefix.length) ?? "";
const branchRef = get("branch ");
entries.push({
path: get("worktree "),
head: get("HEAD "),
// strip refs/heads/ prefix; keep "(detached)" or bare as-is.
branch: branchRef.startsWith("refs/heads/")
? branchRef.slice("refs/heads/".length)
: branchRef || "(detached)",
bare: lines.includes("bare"),
});
}
return entries;
}

export function checkOrchestratorState(
env: NodeJS.ProcessEnv = process.env,
): OrchestratorState {
const currentBranch = run("git", ["branch", "--show-current"]).trim();
const expectedBranch = env.ZETA_EXPECTED_BRANCH ?? "";
const dirtyFiles = run("git", ["status", "--short"])
.split("\n")
.filter(Boolean);
const worktrees = parseWorktreeList(
run("git", ["worktree", "list", "--porcelain"]),
);
// Own worktree is the first entry; exclude it from drift check.
const driftedWorktrees =
expectedBranch && worktrees.length > 1
? worktrees.slice(1).filter((w) => w.branch === expectedBranch)
Comment thread
AceHack marked this conversation as resolved.
: [];
Comment on lines +78 to +82
return {
currentBranch,
expectedBranch,
branchMatch: !expectedBranch || currentBranch === expectedBranch,
dirtyFiles,
worktrees,
driftedWorktrees,
};
}

function main(): number {
const state = checkOrchestratorState();
console.log(JSON.stringify(state, null, 2));

if (!state.branchMatch) {
console.error(
`\nWARNING: current branch '${state.currentBranch}' != expected '${state.expectedBranch}'`,
);
return 1;
}
if (state.driftedWorktrees.length > 0) {
console.error(
`\nWARNING: ${state.driftedWorktrees.length} other worktree(s) are on branch '${state.expectedBranch}' -- CWD-bleed-over risk.`,
);
for (const w of state.driftedWorktrees) {
console.error(` ${w.path}`);
}
return 1;
}
return 0;
}

if (import.meta.main) {
process.exit(main());
}
Loading