// lib/stats/claimed-order.mjs // Which coord-queue order is this Voyage run working on? Read from the queue // itself, never typed by the model and never guessed. // // Why this exists: the yardstick's `use` number joins a new-work order to a // Voyage run. A key declared on the order before any run (a `slug:`) is not // proof a run happened, and no order carried one. The run itself has to say // which order it ran, so /trekbrief's stats record carries the id this module // finds (lib/stats/stats-append.mjs adds it; the model never copies it). // // The contract it mirrors is repo-mailbox's claim verb (coord-order-claim.sh, // 0.37.0), read here, never called: // - coord root ${CLAUDE_COORD_DIR:-$HOME/.claude/coord} // - mailbox basename of `git rev-parse --show-toplevel`; `_*` is reserved // - claim `mv orders/.md orders/claimed/.md`, so the id is the // file stem, plus a best-effort sidecar `claimed/.claim` // holding `claimed-at: ` // - close coord-order-done.sh deletes the `.claim`, so the claim time // survives only in the record this module feeds. // A worktree's toplevel basename is the worktree's name, so the main repo's // name (the parent of the common git dir) is searched too. // // Exactly one claimed order -> { order_id, order_claimed_at, order_reason: null }. // Anything else -> order_id null (or the id with `no-claim-time`) and a reason: // mailbox-unresolved · coord-missing · coord-unreadable · none-claimed · // several-claimed:N · to-mismatch · no-claim-time. It only reads, never throws, // and starts nothing. Zero npm dependencies. import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'; import { homedir } from 'node:os'; import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'; import { execFileSync } from 'node:child_process'; const GIT_TIMEOUT_MS = 5000; const unattributed = (order_reason) => ({ order_id: null, order_claimed_at: null, order_reason }); /** The coord root the claim script uses: CLAUDE_COORD_DIR, else $HOME/.claude/coord. */ export function resolveCoordDir(env = process.env) { if (env.CLAUDE_COORD_DIR) return env.CLAUDE_COORD_DIR; const home = env.HOME && env.HOME.length > 0 ? env.HOME : homedir(); return join(home, '.claude', 'coord'); } function gitLine(dir, args, env) { try { return execFileSync('git', ['-C', dir, 'rev-parse', ...args], { encoding: 'utf8', timeout: GIT_TIMEOUT_MS, env, stdio: ['ignore', 'pipe', 'ignore'], }).trim(); } catch { return ''; } } /** * The mailbox names `dir` can be filed under: the toplevel's basename (the * claim script's rule) and, in a linked worktree, the main repo's name. Two * independent git calls, so one failing never loses the other. An inherited * GIT_DIR / GIT_WORK_TREE would point git at another repo, so both are dropped. */ export function resolveMailboxes(dir, { env = {} } = {}) { const childEnv = { ...process.env, ...env }; for (const key of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE']) delete childEnv[key]; const names = []; const top = gitLine(dir, ['--show-toplevel'], childEnv); if (top) names.push(basename(top)); const common = gitLine(dir, ['--path-format=absolute', '--git-common-dir'], childEnv); // Only `/.git` names a repo; a submodule's `.git/modules/` does not. if (common && basename(common) === '.git') names.push(basename(dirname(common))); return [...new Set(names)]; } const isDir = (p) => existsSync(p) && statSync(p).isDirectory(); /** The one claimed order across `mailboxes`, or null and the reason it is not one. */ export function findClaimedOrder({ coordDir, mailboxes }) { try { if (!isDir(coordDir)) return unattributed('coord-missing'); const names = (mailboxes ?? []).filter((m) => typeof m === 'string' && m !== '' && !m.startsWith('_')); if (names.length === 0) return unattributed('mailbox-unresolved'); const found = []; for (const mailbox of names) { const dir = join(coordDir, mailbox, 'orders', 'claimed'); if (!isDir(dir)) continue; for (const name of readdirSync(dir).sort()) { if (name.endsWith('.md')) found.push({ mailbox, dir, id: name.slice(0, -3) }); } } if (found.length === 0) return unattributed('none-claimed'); if (found.length > 1) return unattributed(`several-claimed:${found.length}`); const [{ mailbox, dir, id }] = found; const text = readFileSync(join(dir, `${id}.md`), 'utf8'); const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? ''; const to = fm.match(/^to:[ \t]*(.*?)[ \t]*$/m)?.[1]; if (to !== undefined && to !== mailbox) return unattributed('to-mismatch'); const claimPath = join(dir, `${id}.claim`); const at = existsSync(claimPath) ? readFileSync(claimPath, 'utf8').match(/^claimed-at:[ \t]*(\S+)[ \t]*$/m)?.[1] : undefined; if (!at) return { order_id: id, order_claimed_at: null, order_reason: 'no-claim-time' }; return { order_id: id, order_claimed_at: at, order_reason: null }; } catch { return unattributed('coord-unreadable'); } } // The directory to resolve the mailbox from: `projectDir` when it is a directory // inside `cwd` (a brief's project dir), else `cwd`. A path that walks out of cwd // cannot move the lookup to another repo. function baseDir(cwd, projectDir) { if (typeof projectDir !== 'string' || projectDir === '') return cwd; try { const abs = resolve(cwd, projectDir); if (!isDir(abs)) return cwd; const rel = relative(realpathSync(cwd), realpathSync(abs)); return rel.startsWith('..') || isAbsolute(rel) ? cwd : abs; } catch { return cwd; } } /** The attribution a /trekbrief record carries: always the three keys. */ export function attributeOrder({ cwd = process.cwd(), env = process.env, projectDir } = {}) { const mailboxes = resolveMailboxes(baseDir(cwd, projectDir), { env }); if (mailboxes.length === 0) return unattributed('mailbox-unresolved'); return findClaimedOrder({ coordDir: resolveCoordDir(env), mailboxes }); }