Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
820ac08
feat: scaffold Astro+Starlight website (W2)
apresmoi Jul 17, 2026
8c0d867
feat: build Burnlist landing page (W3)
apresmoi Jul 17, 2026
00e3902
feat: register skills for both Claude and Codex, per-repo and global …
apresmoi Jul 17, 2026
664f225
docs: add core docs — install, CLI, ovens, dashboard (W4)
apresmoi Jul 17, 2026
05dfadb
feat: add burnlist install/uninstall CLI verbs for both agents (S3)
apresmoi Jul 17, 2026
3e8ab3b
docs: add lifecycle and four oven deep-dive pages (W5)
apresmoi Jul 17, 2026
623b355
fix: honest positioning, atomic llms output, accurate CLI/oven docs (W7)
apresmoi Jul 17, 2026
4626761
fix: reinstate theme toggle, a11y and contrast fixes, add favicon (W7)
apresmoi Jul 17, 2026
1c7d5f3
feat: honest tracked-vs-untracked per-repo skill install with --commi…
apresmoi Jul 17, 2026
7a3d5d4
docs: document the two independent install systems (skills and hooks)…
apresmoi Jul 17, 2026
13d4504
test: round out skill install coverage with global non-clobber (S7)
apresmoi Jul 17, 2026
26ea7f2
fix: make skill install transactional and honest; harden purge, help,…
apresmoi Jul 17, 2026
8eed38a
docs: document --force, true commit-ignore status, and global auto-re…
apresmoi Jul 17, 2026
cdb2949
fix: close skill install/rollback TOCTOU race class with atomic quara…
apresmoi Jul 17, 2026
ce7f31c
merge: skill install system (both agents, hooks+skills)
apresmoi Jul 17, 2026
dc30d65
merge: public website (Astro+Starlight docs & landing)
apresmoi Jul 17, 2026
43fcbee
feat: reskin site to the dashboard design system + triangle logo
apresmoi Jul 17, 2026
28bda49
merge: website reskin (dashboard design system + logo)
apresmoi Jul 17, 2026
b5a96a2
docs: drop loop framing from the site; reflect the five shipped ovens
apresmoi Jul 17, 2026
b460f18
docs: add Visual Parity oven page and sync Differential Testing engin…
apresmoi Jul 17, 2026
5f6f84a
feat: emit an agent-ready skill.md and wire sitemap into the docs build
apresmoi Jul 17, 2026
f83356b
feat: add two-path install section with corner copy buttons, document…
apresmoi Jul 18, 2026
ea50d4e
fix: make skills-install quarantine identity survive Linux inode reuse
apresmoi Jul 18, 2026
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
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ tiny (≤ 200 lines). `CLAUDE.md` in each folder is a symlink to its `AGENTS.md`
- **Ovens are declarative and non-executable** — data, never code. No `eval`, no
runtime component/renderer injection, no imported UI.

## Agent integrations
- Skills (`burnlist install`) and Streaming Diff hooks (`burnlist hooks install`) are
independent. Keep their docs accurate and separate; see `README.md` and
`skills/burnlist/references/installation.md` before changing either surface.

## Hygiene
- **Conventional commits** (`feat:`/`fix:`/…); reference a burnlist item id when one
applies: `feat: … (auth-07)`.
Expand Down
75 changes: 70 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Burnlist requires Node.js 18 or newer.
npm install --global burnlist
```

The global package installs the `burnlist` command and registers one bundled agent skill under `$HOME/.agents/skills`.
The global package installs the `burnlist` command and registers the bundled Burnlist skill for Claude Code under `~/.claude/skills` and Codex under `~/.agents/skills`. Streaming Diff hooks are a separate, opt-in per-repository integration; see [Agent integrations](#agent-integrations).

Ask your agent to create a Burnlist for a goal or continue an existing one. The skill owns that workflow; the CLI provides the dashboard and protocol helpers.

Expand Down Expand Up @@ -70,19 +70,84 @@ Projects that need worker orchestration can import `createDifferentialTestingWor

See the [Differential Testing data contract](skills/burnlist/references/differential-testing-data.md) and [adapter SDK reference](skills/burnlist/references/differential-testing-adapter-sdk.md) for scenario bundles, exact sessions, telemetry, and worker interfaces.

## Streaming Diff hooks
## Agent integrations

`burnlist hooks install --agent codex,claude` merges local Streaming Diff commands into `.codex/hooks.json` and `.claude/settings.json`; it preserves existing hook entries. The agent remains responsible for any first-run hook trust/consent prompt—Burnlist only writes configuration and never bypasses that review. `burnlist hooks status` reports whether each config is tracked (and therefore shared) or local; an already tracked config cannot be hidden with `.git/info/exclude`.
Burnlist has two independent systems. You can install the skill without hooks, hooks without the skill, or both.

Hooks use the portable `burnlist` command from `PATH`; the host resolves the platform-specific launcher.
### Skills: make Burnlist discoverable

`burnlist install` registers the bundled skill for both Claude Code and Codex. Its default scope is the current repository and creates managed, untracked-local skill registrations via `.git/info/exclude`:

| Agent | Per-repository target | Global target (`--global`) |
| --- | --- | --- |
| Claude Code | `<repo>/.claude/skills/burnlist` | `~/.claude/skills/burnlist` |
| Codex | `<repo>/.agents/skills/burnlist` | `~/.agents/skills/burnlist` |

```sh
# Per-repository skill only (both agents by default)
burnlist install

# Limit to one agent, or preview without writing
burnlist install --agent codex
burnlist install --dry-run

# Global skill only
burnlist install --global

# Portable per-repository copy that the team can commit
burnlist install --commit

# Remove the matching per-repository or global registration
burnlist uninstall
burnlist uninstall --global

# Also remove the global npm package (global scope only)
burnlist uninstall --global --purge
```

`--agent codex,claude` restricts either install or uninstall to the selected agents. `--commit` is per-repository only; it makes a portable copy instead of the default local registration.

### Hooks: capture Streaming Diff edits

`burnlist hooks install` is separate from skill installation. It is per-repository only (there is no global hooks mode) and merges Burnlist's edit-capture commands without replacing unrelated hooks:

| Agent that consumes the hook | Worktree-root config |
| --- | --- |
| Codex | `<repo>/.codex/hooks.json` |
| Claude Code | `<repo>/.claude/settings.json` |

Codex receives `SessionStart`, `PreToolUse`, and `PostToolUse` hooks; Claude Code also receives `PostToolUseFailure`. Edit events are limited to each agent's edit/write tools and invoke `burnlist streaming-diff hook`. Codex needs CLI version 0.124.0 or newer to run these hooks. The host needs `burnlist` on `PATH`, and the agent remains responsible for any hook trust or consent prompt.

```sh
# Hooks only, for both agents by default
burnlist hooks install

# Limit to one agent; --untracked requests a local exclude entry
burnlist hooks install --agent claude
burnlist hooks install --untracked

# Inspect or remove Burnlist-managed hooks
burnlist hooks status
burnlist hooks uninstall

# Install or remove both independent per-repository systems
burnlist install && burnlist hooks install
burnlist uninstall && burnlist hooks uninstall

# A global skill can be combined with hooks in the current repository
burnlist install --global && burnlist hooks install
```

Untracked hook configs are added to `.git/info/exclude` by default; tracked configs remain shared with the team, and `--untracked` cannot hide one. `burnlist hooks uninstall` removes only Burnlist's own hook entries and preserves the rest of the config. See the [installation reference](skills/burnlist/references/installation.md) for the full CLI surface.

## Command Line

- `burnlist --plan <burnlist.md> --check` validates the active queue and completed ledger.
- `burnlist --plan <burnlist.md> --digest` prints a completion digest after the active queue is empty.
- `burnlist --close-completed` adds a digest when needed and moves empty in-progress Burnlists to `completed`.
- `burnlist --stamp` prints a local ISO timestamp for completion records.
- `burnlist uninstall` removes the command and its registered skill.
- `burnlist install` / `burnlist uninstall` manage the independent agent-skill registrations.
- `burnlist hooks install|uninstall|status` manages the independent per-repository Streaming Diff hooks.

Use `burnlist --help` for dashboard ports, scan roots, local state paths, and Oven data bindings.

Expand Down
69 changes: 24 additions & 45 deletions bin/burnlist.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { basename, dirname, resolve } from "node:path";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

import { runSkillsInstallCli } from "../src/cli/skills-install-cli.mjs";

const args = process.argv.slice(2);
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const knownSubcommands = new Set([
"install",
"uninstall",
"differential-testing",
"streaming-diff",
Expand All @@ -24,50 +26,20 @@ const knownSubcommands = new Set([
"init",
]);

function npmGlobalPrefix() {
let current = packageRoot;
while (dirname(current) !== current) {
if (basename(current) === "node_modules") {
const parent = dirname(current);
return basename(parent) === "lib" ? dirname(parent) : parent;
}
current = dirname(current);
}
throw new Error("Burnlist is not running from a global npm installation.");
}

function runNodeScript(path, scriptArgs) {
return spawnSync(process.execPath, [path, ...scriptArgs], {
env: process.env,
shell: false,
stdio: "inherit",
});
function printSkillUsage(command) {
const usage = command === "install"
? "Usage: burnlist install [--global] [--commit] [--force] [--agent codex,claude] [--dry-run]"
: "Usage: burnlist uninstall [--global] [--agent codex,claude] [--dry-run] [--purge]";
console.log(`${usage}\n\nInstall and remove Burnlist-managed agent skills for Codex and Claude.`);
}

async function main() {
if (args[0] === "uninstall") {
let prefix;
try {
prefix = npmGlobalPrefix();
} catch (error) {
console.error(error.message);
process.exit(1);
}
const unregisterPath = resolve(packageRoot, "scripts", "unregister-skills.mjs");
const unregister = runNodeScript(unregisterPath, ["--force-global"]);
if (unregister.status !== 0) process.exit(unregister.status || 1);

const npm = process.platform === "win32" ? "npm.cmd" : "npm";
const removal = spawnSync(npm, ["uninstall", "--global", "--prefix", prefix, "burnlist"], {
env: process.env,
shell: false,
stdio: "inherit",
});
if (removal.status !== 0) {
console.error("Burnlist: npm uninstall failed; restoring agent skill registrations.");
runNodeScript(resolve(packageRoot, "scripts", "register-skills.mjs"), ["--force-global"]);
if (args[0] === "install" || args[0] === "uninstall") {
if (args.includes("--help") || args.includes("-h")) {
printSkillUsage(args[0]);
return;
}
process.exitCode = removal.status || 0;
process.exitCode = runSkillsInstallCli({ args, packageRoot });
return;
}

Expand Down Expand Up @@ -111,7 +83,7 @@ if (args[0] && !args[0].startsWith("--") && !["-h", "-v"].includes(args[0]) && !
process.exit(2);
}

if (args[0] !== "oven" && (args.includes("--help") || args.includes("-h"))) {
if (!["oven", "hooks"].includes(args[0]) && (args.includes("--help") || args.includes("-h"))) {
console.log(`Burnlist

Usage:
Expand All @@ -125,7 +97,7 @@ Usage:
burnlist differential-testing schema
burnlist differential-testing sdk
burnlist streaming-diff <ensure-feed|capture|url|hook> ...
burnlist hooks <install|uninstall|status> [--agent codex,claude] [--untracked]
burnlist hooks [install|uninstall|status] [--agent codex,claude] [--untracked] (bare defaults to status)
burnlist oven <list|view|bind|unbind|bindings|create|update> ...
burnlist new [--repo <path>]
burnlist show <id>[#<item>] [--repo <path>]
Expand All @@ -137,7 +109,8 @@ Usage:
burnlist unregister [path]
burnlist roots [--prune]
burnlist init [path] [--track]
burnlist uninstall
burnlist install [--global] [--commit] [--force] [--agent codex,claude] [--dry-run]
burnlist uninstall [--global] [--agent codex,claude] [--dry-run] [--purge]

Options:
--auto-port Try the next available loopback port.
Expand All @@ -146,6 +119,12 @@ Options:
--ovens-dir <path> Override launch-repository custom Oven storage only.
--runs-dir <path> Override Run snapshot storage.
--oven-data <id=path> Bind one Oven to a read-only normalized JSON payload.
--global Install or uninstall skills in the user home directory.
--commit Per-repository install: copy portable skills for git commit.
--force Permit install to replace a Burnlist-managed portable copy with a symlink.
--agent <agents> Restrict skill install or uninstall to codex, claude, or both.
--dry-run Print skill link or portable-copy plans without writing them.
--purge With uninstall --global only, also remove the global npm package.
--version, -v Print the installed Burnlist version.
--help, -h Show this help.`);
return;
Expand Down
86 changes: 13 additions & 73 deletions scripts/register-skills.mjs
Original file line number Diff line number Diff line change
@@ -1,83 +1,23 @@
#!/usr/bin/env node
import {
existsSync,
lstatSync,
mkdirSync,
readlinkSync,
readdirSync,
symlinkSync,
} from "node:fs";
import { dirname, join, resolve } from "node:path";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

import { registerSkills, registrationScope } from "../src/cli/skills-register.mjs";

const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const args = new Set(process.argv.slice(2));
const dryRun = args.has("--dry-run");
const globalInstall = process.env.npm_config_global === "true" || args.has("--force-global");
const args = process.argv.slice(2);

if (!globalInstall) {
if (process.env.npm_lifecycle_event === "postinstall" && process.env.npm_config_global !== "true" && args.length === 0) {
console.log("Burnlist: local npm install detected; agent skill registration is only performed for global installs.");
process.exit(0);
}

const home = process.env.HOME || process.env.USERPROFILE;
if (!home) {
console.error("Burnlist: cannot register agent skills because no user home directory is available.");
process.exit(1);
}

const sourceRoot = resolve(packageRoot, "skills");
const targetRoot = resolve(process.env.BURNLIST_SKILLS_DIR || join(home, ".agents", "skills"));
const skills = readdirSync(sourceRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();

function lstatOrNull(path) {
} else {
try {
return lstatSync(path);
registerSkills({
sourceRoot: resolve(packageRoot, "skills"),
scope: registrationScope(args),
dryRun: args.includes("--dry-run"),
});
} catch (error) {
if (error.code === "ENOENT") return null;
throw error;
}
}

function linkedSource(path) {
return resolve(dirname(path), readlinkSync(path));
}

const registrations = skills.map((name) => {
if (!/^[a-z0-9][a-z0-9-]*$/u.test(name)) {
throw new Error(`unsafe skill folder name: ${name}`);
}
const source = resolve(sourceRoot, name);
if (!existsSync(join(source, "SKILL.md"))) {
throw new Error(`skill ${name} is missing SKILL.md`);
}
const target = resolve(targetRoot, name);
const stat = lstatOrNull(target);
if (!stat) return { action: "link", name, source, target };
if (!stat.isSymbolicLink()) {
throw new Error(`${target} already exists and is not a Burnlist-managed symlink`);
}
if (linkedSource(target) !== source) {
throw new Error(`${target} already links to a different skill source`);
console.error(`Burnlist: ${error.message}`);
process.exitCode = 1;
}
return { action: "keep", name, source, target };
});

if (!dryRun) mkdirSync(targetRoot, { recursive: true });

for (const registration of registrations) {
const verb = registration.action === "keep" ? "kept" : dryRun ? "would link" : "linked";
if (registration.action === "link" && !dryRun) {
symlinkSync(
registration.source,
registration.target,
process.platform === "win32" ? "junction" : "dir",
);
}
console.log(`Burnlist: ${verb} ${registration.name} -> ${registration.target}`);
}

console.log(`Burnlist: agent skills are registered under ${targetRoot}.`);
30 changes: 19 additions & 11 deletions scripts/smoke-global-install.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ const home = join(tmpRoot, "home");
const prefix = join(tmpRoot, "prefix");
const packRoot = join(tmpRoot, "pack");
const npmCache = join(tmpRoot, "npm-cache");
const {
BURNLIST_CLAUDE_SKILLS_DIR: ignoredClaudeSkillsDir,
BURNLIST_SKILLS_DIR: ignoredCodexSkillsDir,
...smokeEnv
} = process.env;
const env = {
...process.env,
...smokeEnv,
HOME: home,
USERPROFILE: home,
npm_config_cache: npmCache,
Expand All @@ -41,8 +46,8 @@ function run(command, args, options = {}) {
return options.capture ? result.stdout.trim() : "";
}

function assertManagedLink(name, packageRoot) {
const target = join(home, ".agents", "skills", name);
function assertManagedLink(agentDirectory, name, packageRoot) {
const target = join(home, agentDirectory, "skills", name);
const stat = lstatSync(target);
if (!stat.isSymbolicLink()) throw new Error(`${target} is not a symlink`);
const actual = realpathSync(resolve(dirname(target), readlinkSync(target)));
Expand All @@ -68,7 +73,8 @@ try {
run("npm", ["install", "--global", "--prefix", prefix, tarball]);
const globalRoot = run("npm", ["root", "--global", "--prefix", prefix], { capture: true });
const packageRoot = resolve(globalRoot, "burnlist");
assertManagedLink("burnlist", packageRoot);
assertManagedLink(".claude", "burnlist", packageRoot);
assertManagedLink(".agents", "burnlist", packageRoot);

const cli = process.platform === "win32"
? join(prefix, "burnlist.cmd")
Expand All @@ -95,13 +101,15 @@ try {
|| JSON.stringify(Object.keys(sdk).sort()) !== JSON.stringify(expected.sort())) process.exit(1);
`]);

run(cli, ["uninstall"]);
for (const name of ["burnlist"]) {
try {
lstatSync(join(home, ".agents", "skills", name));
throw new Error(`uninstall left the ${name} skill registration behind`);
} catch (error) {
if (error.code !== "ENOENT") throw error;
run(cli, ["uninstall", "--global", "--purge"]);
for (const agentDirectory of [".claude", ".agents"]) {
for (const name of ["burnlist"]) {
try {
lstatSync(join(home, agentDirectory, "skills", name));
throw new Error(`uninstall left the ${agentDirectory} ${name} skill registration behind`);
} catch (error) {
if (error.code !== "ENOENT") throw error;
}
}
}
console.log("Global npm install smoke test passed.");
Expand Down
Loading
Loading