Skip to content
Closed
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
18 changes: 13 additions & 5 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
#!/bin/sh
set -eu

repo_root=$(git rev-parse --show-toplevel)
cd "$repo_root"

if ! command -v bun >/dev/null 2>&1; then
echo "error: Bun is required to run the pre-commit lint check" >&2
echo "error: Bun is required to run the pre-commit checks" >&2
exit 1
fi

bun run lint:staged

staged_root=$(mktemp -d)
trap 'rm -rf "$staged_root"' EXIT HUP INT TERM
git checkout-index --all --prefix="$staged_root/"

if [ ! -f "$staged_root/scripts/plugin-projections.ts" ]; then
echo "error: staged projection checker is missing" >&2
exit 1
fi

exec bun run lint:staged
bun "$staged_root/scripts/plugin-projections.ts" check-staged "$PWD"
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: CI

on:
pull_request:
push:
branches: [main]

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.18
- run: bun install --frozen-lockfile
- run: bun run plugins:check
- run: bun run lint
- run: bun run typecheck
- run: bun run test:projections
- run: bun run test:cli
4 changes: 3 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
docs/research/
evals/experiments/
plugins/*/skills/*/SKILL.md
plugins/*/source/*/SKILL.md
plugins/*/claude-skills/*/SKILL.md
plugins/*/codex-skills/*/SKILL.md
27 changes: 17 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,22 @@ contract.

- `cli/` — global TypeScript/Bun workflow CLI and Temporal worker (M1 onward),
versioned independently from plugins.
- `plugins/<name>/` — independently adoptable plugins. Skills live in
`skills/<skill>/SKILL.md` with colocated `scripts/` and `evals/`. Plugins are
self-contained: never reference files outside the plugin directory or assume
a sibling plugin is installed. Workflows invoke command skills by canonical
`<plugin>:<skill>` ID; capability composition uses intent and portable
contracts.
- `plugins/<name>/` — independently adoptable plugins. Canonical skills live in
`source/<skill>/SKILL.md` with colocated `scripts/` and `evals/`; optional
harness overlays live under `overlays/<harness>/`. `claude-skills/` and
`codex-skills/` are generated, committed projections and must never be edited
directly. Plugins are self-contained: never reference files outside the
plugin directory or assume a sibling plugin is installed. Workflows invoke
command skills by canonical `<plugin>:<skill>` ID; capability composition uses
intent and portable contracts.
- `docs/specs/` — normative invariants. Runtime contracts live in
`workflow-runtime.md`, `workspaces-artifacts.md`, `compatibility.md`, and
`observability.md`; capability contracts live in their named files. Tests and
evals trace to stable invariant IDs (for example, WR-20 or GW-C1).
- `docs/decisions/` — accepted architecture choices. Read the directly linked
ADR before revisiting a selected technology or distribution boundary.
- `evals/` — shared runner (`runner/`) and results (`results/`, gitignored).
The runner discovers cases via `plugins/*/skills/*/evals/*.yaml`.
The runner discovers cases via `plugins/*/source/*/evals/*.yaml`.

## Skill development loop (mandatory, in order)

Expand Down Expand Up @@ -56,7 +58,7 @@ Review agents must never run git/gh against this repo — temp dirs via

## Tests & evals

- Script tests: `bash plugins/darrow-git/skills/<skill>/scripts/<name>.test.sh`
- Script tests: `bash plugins/darrow-git/source/<skill>/scripts/<name>.test.sh`
(also with `/bin/bash`).
- Evals: `cd evals && bun runner/run.ts --case <substring> [--dry]`.
5 trials/case, pass-rate threshold 0.8, ~$0.5/case — use `--case` to scope.
Expand Down Expand Up @@ -84,11 +86,16 @@ Review agents must never run git/gh against this repo — temp dirs via

- Marketplace manifest: `.claude-plugin/marketplace.json` (Codex reads it too).
- Each plugin needs BOTH `.claude-plugin/plugin.json` and
`.codex-plugin/plugin.json` (Codex variant adds `"skills": "./skills/"`).
`.codex-plugin/plugin.json`; they declare `"skills": "./claude-skills/"` and
`"skills": "./codex-skills/"` respectively and share identity and version.
- Each skill that participates in the Darrow workflow runtime (M1 onward) needs
`skills/<skill>/darrow.json`, validated against
`source/<skill>/darrow.json`, validated against
`docs/specs/darrow-skill-metadata.schema.json`. Native plugin manifests retain
plugin identity and package version; never add arbitrary Darrow fields to
them.
- Run `bun run plugins:generate` after canonical or overlay changes, or
`bun run plugins:generate -- <plugin>` for one plugin. `bun run plugins:check`
validates deterministic provenance without mutation. The pre-commit hook
performs the same check from the staged index for affected plugins only.
- A command skill is invoked explicitly by canonical name. A capability skill is
loaded by harness intent and advertises portable contracts in `darrow.json`.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,10 @@ The marketplace manifest is
[`.claude-plugin/marketplace.json`](.claude-plugin/marketplace.json); Codex uses
the same marketplace and each plugin also ships a Codex-specific manifest.
Darrow-aware skills keep workflow metadata in a colocated `darrow.json` rather
than extending either runtime's native plugin manifest.
than extending either runtime's native plugin manifest. Each plugin directly
authors one canonical `source/` tree plus optional harness overlays; native
manifests select committed `claude-skills/` and `codex-skills/` projections from
the same package and version.

## Development

Expand All @@ -248,4 +251,8 @@ bun run hooks:install

`bun run lint` checks all Prettier-supported project content. `bun run format`
updates it. The pre-commit hook runs the same check against staged content and
refuses commits that are not formatted.
refuses commits that are not formatted. `bun run plugins:generate` regenerates
all harness projections, `bun run plugins:generate -- <plugin>` scopes generation
to one plugin, and `bun run plugins:check` verifies deterministic provenance
without mutation. Pre-commit performs projection checks from the staged index;
CI performs the full check.
4 changes: 2 additions & 2 deletions cli/fixtures/golden/lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"id": "darrow-delivery:implement",
"contractVersion": "0.1.0",
"pluginVersion": "0.1.0",
"source": "/plugins/darrow-delivery/skills/implement",
"source": "/plugins/darrow-delivery/codex-skills/implement",
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
],
Expand All @@ -34,7 +34,7 @@
"providerId": "darrow-git:create-branch",
"pluginVersion": "0.1.1",
"scope": "user",
"source": "/plugins/darrow-git/skills/create-branch",
"source": "/plugins/darrow-git/codex-skills/create-branch",
"digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
}
],
Expand Down
4 changes: 2 additions & 2 deletions cli/fixtures/golden/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"providerId": "darrow-git:create-branch",
"pluginVersion": "0.1.1",
"scope": "user",
"source": "/plugins/darrow-git/skills/create-branch",
"source": "/plugins/darrow-git/codex-skills/create-branch",
"digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
}
],
Expand All @@ -60,7 +60,7 @@
"adapter": { "id": "codex-cli", "version": "0.1.0" },
"selectionSource": "fixed_plan"
},
"source": "/plugins/darrow-delivery/skills/implement",
"source": "/plugins/darrow-delivery/codex-skills/implement",
"digest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
"input": { "change": "make it work" },
"publish": null
Expand Down
58 changes: 51 additions & 7 deletions cli/src/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,51 @@ async function inspectPlugin(
pluginDir: string,
scope: Scope,
harnessEnabled: boolean,
harness: "codex" | "claude",
): Promise<SkillCandidate[]> {
const codexPath = resolve(pluginDir, ".codex-plugin", "plugin.json");
const claudePath = resolve(pluginDir, ".claude-plugin", "plugin.json");
const skillsDir = resolve(pluginDir, "skills");
if (!(await exists(skillsDir))) return [];
const entries = await readdir(skillsDir, { withFileTypes: true });
const selectedPath = harness === "codex" ? codexPath : claudePath;
if (!(await exists(selectedPath))) return [];
const selected = await readJson<{
name: string;
version: string;
skills?: string | string[];
}>(selectedPath);
if (typeof selected.skills !== "string" || !selected.skills.startsWith("./"))
throw new DarrowError(
`${harness} plugin manifest must declare one relative skills projection: ${selectedPath}`,
"catalog",
);
const unresolvedSkills = resolve(pluginDir, selected.skills);
let skillsDir: string;
try {
const [realPlugin, realSkills] = await Promise.all([
realpath(pluginDir),
realpath(unresolvedSkills),
]);
if (!inside(realPlugin, realSkills))
throw new DarrowError(
`${harness} skills projection escapes plugin directory: ${selected.skills}`,
"catalog",
);
skillsDir = realSkills;
} catch (error) {
if (error instanceof DarrowError) throw error;
throw new DarrowError(
`cannot resolve ${harness} skills projection ${unresolvedSkills}: ${String(error)}`,
"catalog",
);
}
let entries;
try {
entries = await readdir(skillsDir, { withFileTypes: true });
} catch (error) {
throw new DarrowError(
`cannot read ${harness} skills projection ${skillsDir}: ${String(error)}`,
"catalog",
);
}
const darrowEntries: typeof entries = [];
for (const entry of entries) {
if (
Expand Down Expand Up @@ -105,9 +144,9 @@ async function inspectPlugin(
}
}
candidates.push({
id: `${codex.name}:${entry.name}`,
pluginName: codex.name,
pluginVersion: codex.version,
id: `${selected.name}:${entry.name}`,
pluginName: selected.name,
pluginVersion: selected.version,
skillName: entry.name,
skillDir,
pluginDir,
Expand Down Expand Up @@ -244,7 +283,12 @@ export async function loadCatalog(
seen.add(key);
for (const pluginDir of await pluginDirectories(path))
candidates.push(
...(await inspectPlugin(pluginDir, root.scope, root.harnessEnabled)),
...(await inspectPlugin(
pluginDir,
root.scope,
root.harnessEnabled,
harness,
)),
);
}
return candidates;
Expand Down
2 changes: 1 addition & 1 deletion cli/tests/claude.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ async function fixture(script?: string): Promise<{
"..",
"plugins",
"darrow-delivery",
"skills",
"claude-skills",
"implement",
),
commandDir,
Expand Down
53 changes: 47 additions & 6 deletions cli/tests/compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,24 +185,24 @@ describe("M1 compiler", () => {
const plugin = resolve(root, "bad-plugin");
await mkdir(resolve(plugin, ".codex-plugin"), { recursive: true });
await mkdir(resolve(plugin, ".claude-plugin"), { recursive: true });
await mkdir(resolve(plugin, "skills", "create-branch"), {
await mkdir(resolve(plugin, "codex-skills", "create-branch"), {
recursive: true,
});
const manifest = { name: "darrow-git", version: "9.0.0" };
await writeFile(
resolve(plugin, ".codex-plugin", "plugin.json"),
JSON.stringify({ ...manifest, skills: "./skills/" }),
JSON.stringify({ ...manifest, skills: "./codex-skills/" }),
);
await writeFile(
resolve(plugin, ".claude-plugin", "plugin.json"),
JSON.stringify(manifest),
JSON.stringify({ ...manifest, skills: "./claude-skills/" }),
);
await writeFile(
resolve(plugin, "skills", "create-branch", "SKILL.md"),
resolve(plugin, "codex-skills", "create-branch", "SKILL.md"),
"# incompatible\n",
);
await writeFile(
resolve(plugin, "skills", "create-branch", "darrow.json"),
resolve(plugin, "codex-skills", "create-branch", "darrow.json"),
JSON.stringify({
schemaVersion: 1,
kind: "capability",
Expand All @@ -224,6 +224,35 @@ describe("M1 compiler", () => {
expect(await readdir(resolve(root, ".darrow", "worktrees"))).toEqual([]);
});

test("fails preflight when the selected native manifest projection is missing", async () => {
const root = await repo();
const plugin = resolve(root, "missing-projection-plugin");
await mkdir(resolve(plugin, ".codex-plugin"), { recursive: true });
await mkdir(resolve(plugin, ".claude-plugin"), { recursive: true });
const manifest = { name: "missing-projection", version: "1.0.0" };
await writeFile(
resolve(plugin, ".codex-plugin", "plugin.json"),
JSON.stringify({ ...manifest, skills: "./codex-skills/" }),
);
await writeFile(
resolve(plugin, ".claude-plugin", "plugin.json"),
JSON.stringify({ ...manifest, skills: "./claude-skills/" }),
);
const previous = process.env.DARROW_PLUGIN_ROOTS;
const previousCodexHome = process.env.CODEX_HOME;
process.env.DARROW_PLUGIN_ROOTS = plugin;
process.env.CODEX_HOME = resolve(root, "codex-home");
try {
await expect(
compile(root, "implement-change", { change: "return hello" }),
).rejects.toThrow("cannot resolve codex skills projection");
} finally {
restoreEnvironment("DARROW_PLUGIN_ROOTS", previous);
restoreEnvironment("CODEX_HOME", previousCodexHome);
}
expect(await readdir(resolve(root, ".darrow", "worktrees"))).toEqual([]);
});

test("resolves a declared ticket publication into the immutable plan", async () => {
const root = await repo();
await writeFile(
Expand Down Expand Up @@ -287,7 +316,7 @@ steps:
"cache",
"darrow",
"darrow-git",
"0.1.1",
"0.1.2",
);
await mkdir(resolve(claudeHome), { recursive: true });
await cp(resolve(SOURCE_PLUGIN_ROOT, "darrow-git"), cachedPlugin, {
Expand Down Expand Up @@ -466,6 +495,18 @@ steps:
"codex",
"claude",
]);
expect(compilation.commands[0]?.candidate.skillDir).toContain(
"/codex-skills/implement",
);
expect(compilation.commands[1]?.candidate.skillDir).toContain(
"/claude-skills/implement",
);
expect(compilation.plan.steps[0]?.source).toContain(
"/codex-skills/implement",
);
expect(compilation.plan.steps[1]?.source).toContain(
"/claude-skills/implement",
);

const runDir = resolve(root, ".darrow", "runs", "mixed-run");
await mkdir(runDir, { recursive: true });
Expand Down
24 changes: 13 additions & 11 deletions cli/tests/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,18 @@ async function deliveryTddPluginFixture(root: string): Promise<string> {
await cp(resolve(CLI_ROOT, "..", "plugins"), pluginRoot, {
recursive: true,
});
const implement = resolve(
pluginRoot,
"darrow-delivery",
"skills",
"implement",
);
await rm(implement, { recursive: true, force: true });
await cp(resolve(CLI_ROOT, "fixtures", "delivery-tdd"), implement, {
recursive: true,
});
for (const projection of ["codex-skills", "claude-skills"]) {
const implement = resolve(
pluginRoot,
"darrow-delivery",
projection,
"implement",
);
await rm(implement, { recursive: true, force: true });
await cp(resolve(CLI_ROOT, "fixtures", "delivery-tdd"), implement, {
recursive: true,
});
}
return pluginRoot;
}

Expand Down Expand Up @@ -825,7 +827,7 @@ steps:
const interruptMetadataPath = resolve(
interruptPlugins,
"darrow-delivery",
"skills",
"codex-skills",
"implement",
"darrow.json",
);
Expand Down
Loading
Loading