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
8 changes: 7 additions & 1 deletion docs/installing-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,17 @@ the updated environment.
Each plugin ships its own `pyproject.toml` and `uv.lock`. Its documented
bootstrap commands prepare an isolated environment from that lock on first use
without writing below the installed plugin. By default, environments live under
`${XDG_CACHE_HOME:-$HOME/.cache}/darrow` on Linux and macOS and
`$HOME/.darrow/cache` on Linux and macOS and
`%LOCALAPPDATA%\Darrow\Cache` on native Windows. Set `DARROW_CACHE_DIR` to a
non-empty absolute path to override that root. The selected root must be
readable and writable; invalid configuration fails without falling back to the
plugin, working directory, repository, or temporary storage.
These UV project environments are disposable cache files. The new default
does not move or delete files under the former cache location; remove those
manually when no older plugin process needs them. `UV_CACHE_DIR` still belongs
to UV or the caller. Review evidence uses a separate retained state directory
under `$HOME/.darrow/reviews` or `%LOCALAPPDATA%\Darrow\Reviews` and follows
the cleanup rules documented by `darrow-review`.

Allow network access for Python, build requirements, and runtime dependency
downloads, plus write access to the Darrow and UV caches. Subsequent runs reuse
Expand Down
27 changes: 27 additions & 0 deletions docs/specs/code-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,27 @@ axis and report `not_available`. Do not invent requirements.
result. An unavailable-route result MUST retain zero native reader-launch
attempts, including inherited, substituted, background, generic, or
otherwise unbound attempts.
22. **CR-C22 — User-level review state and bounded retention.** Generated review
packets and evidence MUST live in a per-user review-state root, never in the
repository Git directory or product tree. The default is `~/.darrow/reviews`
on Unix and `%LOCALAPPDATA%\Darrow\Reviews` on native Windows; an explicit
override MUST be absolute. Each run is isolated by canonical repository and
worktree identity and a unique run directory. Bundled commands return
absolute paths and provide exact-target discovery; callers MUST NOT scan
`.git` or infer an artifact from a filename. The state root and run
directories MUST restrict access to the current user on Unix and use the
user's local application-data area on Windows. Successful evidence remains
available for fix verification. A terminal scope failure MUST still be able
to allocate a private run and manifest for its canonical blocked result,
so the same pinning and retention rules apply. On review
invocation, bundled mechanics prune unpinned runs whose directories have
not changed for 30 days, retaining every prior scope and verification
artifact referenced by a retained run. An explicit pin keeps a run and its
dependencies; an explicit prune can remove unpinned expired
evidence. Cleanup MUST stay inside the review-state root, reject unsafe
paths, and report expired or missing follow-up evidence clearly. Temporary
staging files are removed as soon as their canonical record is installed.
This location change does not migrate or delete older `.git` review files.

## Result shape and presentation

Expand Down Expand Up @@ -425,6 +446,12 @@ the prior-to-current repair delta remains nonempty and exact-target-bound.
absolute paths. The plugin MAY share the `.darrow/config.json` envelope with
other independently installed plugins, but MUST NOT call or reference their
helpers or files.
9. **CR-P9 — Review-state portability.** State-root selection, allocation,
exact-target lookup, pinning, and pruning belong to the review plugin's
bundled mechanics. They must work across supported native hosts without a
daemon or a shared Darrow runtime. A missing or unusable state root refuses
review rather than falling back to `.git`, the repository, or a temporary
location.

## Evaluation requirements

Expand Down
5 changes: 4 additions & 1 deletion docs/specs/python-quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ set `UV_PROJECT_ENVIRONMENT` only for its `uv` child and must not override
The bootstrap resolves the Darrow cache root in this order:

1. a non-empty absolute `DARROW_CACHE_DIR`;
2. on Linux and macOS, `${XDG_CACHE_HOME:-$HOME/.cache}/darrow`; and
2. on Linux and macOS, `$HOME/.darrow/cache`; and
3. on native Windows, `%LOCALAPPDATA%\Darrow\Cache`.

Missing path inputs and relative, unreadable, or unwritable roots fail clearly.
There is no fallback to the plugin tree, repository, working directory, or
temporary storage. Environments live below
`<cache-root>/uv-environments/<plugin-name>/<environment-key>/`.
The cache is disposable. A change to this default does not migrate or remove
files at the previous location. `UV_CACHE_DIR` remains under UV or caller
control; it does not select Darrow's project-environment cache.

The deterministic environment key is the SHA-256 digest of a versioned,
sorted-key JSON value containing the exact plugin name and synchronized plugin
Expand Down
3 changes: 2 additions & 1 deletion evals/runner/environment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { isolatedHarnessEnvironment } from "./environment";
import { isolatedHarnessEnvironment, trialReviewStateDir } from "./environment";
import { codexAgentConcurrency } from "./codex-config";

const cleanup: string[] = [];
Expand Down Expand Up @@ -55,6 +55,7 @@ describe("isolated harness environment", () => {
expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBe("claude-test-token");
expect(env.DARROW_ADAPTIVE_DELIVERY_EXTERNAL_SANDBOX).toBe("1");
expect(env.DARROW_CACHE_DIR).toBe(join(env.TMPDIR!, "darrow-cache"));
expect(env.DARROW_REVIEW_STATE_DIR).toBe(trialReviewStateDir(repo));
expect(env.UV_PROJECT_ENVIRONMENT).toBeUndefined();
expect(env.UV_CACHE_DIR).toBe(join(env.TMPDIR!, "uv-cache"));
expect(env.UNRELATED_EVAL_SECRET).toBeUndefined();
Expand Down
9 changes: 8 additions & 1 deletion evals/runner/environment.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { cp, mkdir, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { basename, dirname, join, resolve } from "node:path";
import { codexAgentConcurrency } from "./codex-config";

type Harness = "claude" | "codex";

/** Review evidence belongs beside the isolated repository, never within it. */
export function trialReviewStateDir(repoDir: string): string {
return join(dirname(repoDir), `${basename(repoDir)}-reviews`);
}

/** Checks need fixture tools, not the candidate harness's authentication. */
export async function isolatedCheckEnvironment(
repoDir: string,
Expand Down Expand Up @@ -117,6 +122,7 @@ export async function isolatedHarnessEnvironment(
await mkdir(configRoot, { recursive: true });
await mkdir(shellRoot, { recursive: true });
await mkdir(tempRoot, { recursive: true });
await mkdir(trialReviewStateDir(repoDir), { recursive: true, mode: 0o700 });

if (harness === "codex") {
const limit = codexAgentConcurrency();
Expand Down Expand Up @@ -157,6 +163,7 @@ export async function isolatedHarnessEnvironment(
// environments belong in writable, per-trial scratch, outside that tree.
// Each bootstrap derives a distinct environment from its locked backend.
env.DARROW_CACHE_DIR = join(tempRoot, "darrow-cache");
env.DARROW_REVIEW_STATE_DIR = trialReviewStateDir(repoDir);
env.UV_CACHE_DIR = join(tempRoot, "uv-cache");
env.ZDOTDIR = shellRoot;
// The runner wraps the evaluated agent in sandboxedAgentCommand. Nested
Expand Down
12 changes: 10 additions & 2 deletions evals/runner/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { existsSync, realpathSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, join, relative, sep } from "node:path";
import type { Fixture } from "./types";
import { trialReviewStateDir } from "./environment";

const TICKETCTL = `#!/bin/bash
set -euo pipefail
Expand Down Expand Up @@ -602,7 +603,11 @@ export async function buildFixture(
}

export async function destroyFixture(repoDir: string): Promise<void> {
if (!existsSync(repoDir)) return;
const reviewStateDir = trialReviewStateDir(repoDir);
if (!existsSync(repoDir)) {
await rm(reviewStateDir, { recursive: true, force: true });
return;
}
let removalError: unknown;
for (let attempt = 0; attempt < 8; attempt += 1) {
const writable = Bun.spawn(["chmod", "-R", "u+rwX", repoDir], {
Expand All @@ -626,7 +631,10 @@ export async function destroyFixture(repoDir: string): Promise<void> {
} catch (error) {
removalError = error;
}
if (!existsSync(repoDir)) return;
if (!existsSync(repoDir)) {
await rm(reviewStateDir, { recursive: true, force: true });
return;
}
await Bun.sleep(Math.min(100 * 2 ** attempt, 1_000));
}
throw removalError ?? new Error(`cannot remove eval fixture: ${repoDir}`);
Expand Down
7 changes: 5 additions & 2 deletions evals/runner/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from "./owner-evidence";
import { parse as parseYaml } from "yaml";
import { buildFixture, destroyFixture } from "./fixture";
import { trialReviewStateDir } from "./environment";
import { resolveCorpusSource } from "./corpus";
import { runQualityJudge } from "./judge";
import {
Expand Down Expand Up @@ -406,11 +407,13 @@ function trialFollowUpPrompt(

function trialCheckEnvironment(
options: RunCaseOptions,
repoDir: string,
): Record<string, string> {
return {
DARROW_EVAL_HARNESS: options.adapter.name,
DARROW_EVAL_MODEL: options.model,
DARROW_EVAL_EFFORT: options.effort,
DARROW_REVIEW_STATE_DIR: trialReviewStateDir(repoDir),
};
}

Expand Down Expand Up @@ -693,7 +696,7 @@ async function trialChecks(
...(await runChecks(
repoDir,
evalCase.checks,
trialCheckEnvironment(options),
trialCheckEnvironment(options, repoDir),
)),
...headChecks,
...orchestrationChecks,
Expand Down Expand Up @@ -1045,7 +1048,7 @@ async function evaluateDryTrial(
await runChecks(
repoDir,
options.evalCase.checks,
trialCheckEnvironment(options),
trialCheckEnvironment(options, repoDir),
),
);
}
Expand Down
11 changes: 11 additions & 0 deletions evals/runner/sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { trialReviewStateDir } from "./environment";
import {
sandboxedAgentCommand,
sandboxedCommand,
Expand Down Expand Up @@ -83,6 +84,12 @@ describe("eval outer sandbox", () => {
cleanup.push(repoDir);
cleanup.push(unrelatedDir);
await mkdir(join(repoDir, ".git"));
const ownReviewState = trialReviewStateDir(repoDir);
const siblingReviewState = await mkdtemp(
join(tmpdir(), "darrow-eval-other-reviews-"),
);
await mkdir(ownReviewState);
cleanup.push(ownReviewState, siblingReviewState);

if (process.platform === "darwin") {
const protectedPath = join(repoDir, ".git", "eval-plugin");
Expand All @@ -105,6 +112,10 @@ describe("eval outer sandbox", () => {
`(deny file-read* (subpath "${await realpath(main!)}"))`,
);
expect(profile).not.toContain(await realpath(unrelatedDir));
expect(profile).not.toContain(await realpath(ownReviewState));
expect(profile).toContain(
`(deny file-read* (subpath "${await realpath(siblingReviewState)}"))`,
);
expect(profile).toContain(
`(deny file-write* (subpath "${protectedPath}"))`,
);
Expand Down
5 changes: 4 additions & 1 deletion evals/runner/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
import { mkdir, readdir, realpath, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, join, resolve } from "node:path";
import { trialReviewStateDir } from "./environment";

const SOURCE_ROOT = resolve(import.meta.dir, "..", "..");
const SANDBOX_EXEC = "/usr/bin/sandbox-exec";
Expand Down Expand Up @@ -40,6 +41,7 @@ export function sandboxProfile(

async function siblingFixtures(repoDir: string): Promise<string[]> {
const repoReal = await realpath(repoDir);
const ownReviewState = trialReviewStateDir(repoReal);
const entries = await readdir(tmpdir(), { withFileTypes: true });
const paths: string[] = [];
for (const entry of entries) {
Expand All @@ -48,7 +50,8 @@ async function siblingFixtures(repoDir: string): Promise<string[]> {
const candidate = join(tmpdir(), entry.name);
try {
const candidateReal = await realpath(candidate);
if (candidateReal !== repoReal) paths.push(candidateReal);
if (candidateReal !== repoReal && candidateReal !== ownReviewState)
paths.push(candidateReal);
} catch {
// A concurrently removed fixture is already inaccessible.
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "darrow-artificer",
"version": "0.1.2",
"version": "0.1.3",
"description": "Explicit local admission and native Codex continuation for nominated GitHub tickets",
"license": "BUSL-1.1",
"author": { "name": "Björn Rochel", "email": "bjoern@bjro.de" }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "darrow-artificer",
"version": "0.1.2",
"version": "0.1.3",
"description": "Explicit local admission and native Codex continuation for nominated GitHub tickets",
"skills": "./skills/",
"interface": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,10 @@ def default_cache_root(environment: Mapping[str, str], platform: str) -> str:
"LOCALAPPDATA is required when DARROW_CACHE_DIR is not set"
)
return join_path(platform, local, "Darrow", "Cache")
xdg = environment.get("XDG_CACHE_HOME")
if xdg:
return join_path(platform, xdg, "darrow")
home = environment.get("HOME")
if not home:
raise LauncherError(
"HOME is required when DARROW_CACHE_DIR and XDG_CACHE_HOME are not set"
)
return join_path(platform, home, ".cache", "darrow")
raise LauncherError("HOME is required when DARROW_CACHE_DIR is not set")
return join_path(platform, home, ".darrow", "cache")


def resolve_cache_root(environment: Mapping[str, str], platform: str) -> str:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "darrow-discovery",
"description": "Explicit grilling, feature discovery, and implementation planning",
"version": "0.2.6",
"version": "0.2.7",
"license": "BUSL-1.1",
"author": {
"name": "Björn Rochel",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "darrow-discovery",
"version": "0.2.6",
"version": "0.2.7",
"description": "Explicit grilling, feature discovery, and implementation planning",
"author": {
"name": "Björn Rochel",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,10 @@ def default_cache_root(environment: Mapping[str, str], platform: str) -> str:
"LOCALAPPDATA is required when DARROW_CACHE_DIR is not set"
)
return join_path(platform, local, "Darrow", "Cache")
xdg = environment.get("XDG_CACHE_HOME")
if xdg:
return join_path(platform, xdg, "darrow")
home = environment.get("HOME")
if not home:
raise LauncherError(
"HOME is required when DARROW_CACHE_DIR and XDG_CACHE_HOME are not set"
)
return join_path(platform, home, ".cache", "darrow")
raise LauncherError("HOME is required when DARROW_CACHE_DIR is not set")
return join_path(platform, home, ".darrow", "cache")


def resolve_cache_root(environment: Mapping[str, str], platform: str) -> str:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ def test_cache_root_resolution_is_native_and_fail_closed() -> None:
assert resolve({"DARROW_CACHE_DIR": "/var/cache/darrow-test"}, "posix") == (
"/var/cache/darrow-test"
)
assert resolve({"XDG_CACHE_HOME": "/var/cache/user"}, "posix") == (
"/var/cache/user/darrow"
assert (
resolve({"HOME": "/home/test", "XDG_CACHE_HOME": "/var/cache/user"}, "posix")
== "/home/test/.darrow/cache"
)
assert resolve({"HOME": "/home/test"}, "posix") == "/home/test/.cache/darrow"
assert resolve({"HOME": "/home/test"}, "posix") == "/home/test/.darrow/cache"
assert (
resolve({"LOCALAPPDATA": r"C:\Users\Test\AppData\Local"}, "nt")
== r"C:\Users\Test\AppData\Local\Darrow\Cache"
Expand Down
2 changes: 1 addition & 1 deletion plugins/capability/darrow-git/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "darrow-git",
"description": "Intent-triggered Git skills for branches, commits, PRs, and candidate-bound PR evidence",
"version": "0.8.5",
"version": "0.8.6",
"hooks": "./.claude-plugin/hooks.json",
"license": "BUSL-1.1"
}
2 changes: 1 addition & 1 deletion plugins/capability/darrow-git/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "darrow-git",
"version": "0.8.5",
"version": "0.8.6",
"description": "Intent-triggered Git skills for branches, commits, PRs, and candidate-bound PR evidence",
"author": {
"name": "Björn Rochel"
Expand Down
9 changes: 2 additions & 7 deletions plugins/capability/darrow-git/backend/scripts/run_locked.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,10 @@ def default_cache_root(environment: Mapping[str, str], platform: str) -> str:
"LOCALAPPDATA is required when DARROW_CACHE_DIR is not set"
)
return join_path(platform, local, "Darrow", "Cache")
xdg = environment.get("XDG_CACHE_HOME")
if xdg:
return join_path(platform, xdg, "darrow")
home = environment.get("HOME")
if not home:
raise LauncherError(
"HOME is required when DARROW_CACHE_DIR and XDG_CACHE_HOME are not set"
)
return join_path(platform, home, ".cache", "darrow")
raise LauncherError("HOME is required when DARROW_CACHE_DIR is not set")
return join_path(platform, home, ".darrow", "cache")


def resolve_cache_root(environment: Mapping[str, str], platform: str) -> str:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "darrow-observability-langfuse",
"description": "Export Codex rollout turns to Langfuse with attribution epochs",
"version": "0.6.2",
"version": "0.6.3",
"license": "BUSL-1.1",
"author": {
"name": "Björn Rochel",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "darrow-observability-langfuse",
"version": "0.6.2",
"version": "0.6.3",
"description": "Export Codex rollout turns to Langfuse with attribution epochs",
"author": {
"name": "Björn Rochel",
Expand Down
Loading
Loading