Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a710441
feat: add declarative review loop runtime
apresmoi Jul 24, 2026
56c147c
feat: connect loop configuration and lifecycle CLI
apresmoi Jul 24, 2026
48cb673
feat: show live loop runs in the checklist dashboard
apresmoi Jul 24, 2026
61c6802
feat: expose loop commands from the main CLI
apresmoi Jul 24, 2026
9f2b8dd
test: verify the installed loop product
apresmoi Jul 24, 2026
c10dcad
docs: document review loop setup and recovery
apresmoi Jul 24, 2026
75912df
feat: render item-scoped loops in checklist oven
apresmoi Jul 24, 2026
5c564d8
fix: simplify checklist loop workspace
apresmoi Jul 24, 2026
1a8e934
fix: harden loop evidence boundaries
apresmoi Jul 24, 2026
d5f65a4
feat: complete host-orchestrated review loops (H10)
apresmoi Jul 25, 2026
ead095c
test: make project loop strategies hermetic (H10)
apresmoi Jul 25, 2026
13010d7
fix: refresh dashboard package manifest (H10)
apresmoi Jul 25, 2026
4f7d631
refactor: make loop providers host-owned
apresmoi Jul 25, 2026
194b3aa
fix: render loops for completed checklist items
apresmoi Jul 25, 2026
adbb58d
feat: streamline host loop execution
apresmoi Jul 25, 2026
83d2569
fix: compact serial loop feedback rails
apresmoi Jul 25, 2026
cedb629
feat: add responsive layered loop layout
apresmoi Jul 25, 2026
28ab6ca
fix: balance loop layouts across levels
apresmoi Jul 25, 2026
c8311e6
fix: pad loop edge labels
apresmoi Jul 25, 2026
265bde0
feat: simplify built-in loop catalog
apresmoi Jul 25, 2026
c342662
fix: refine loop host usability
apresmoi Jul 25, 2026
deb5599
fix: compact loop feedback routing
apresmoi Jul 25, 2026
8fca071
fix: keep loop labels clear of return paths
apresmoi Jul 25, 2026
1d076c3
feat: simplify host loop protocol (E1)
apresmoi Jul 25, 2026
bb0988d
feat: correlate native loop observations (E2)
apresmoi Jul 25, 2026
e86208d
feat: add local loop forecasts (E3)
apresmoi Jul 25, 2026
964406d
feat: expose live loop forecasts (E4)
apresmoi Jul 25, 2026
105d612
fix: harden minimal loop orchestration (E5)
apresmoi Jul 25, 2026
0bc50cd
feat: optimize Burnlist operational usability (E6)
apresmoi Jul 25, 2026
96c5e90
fix: simplify Loop Progress status view (E6)
apresmoi Jul 25, 2026
feb9656
docs: publish operational UX guidance (E6)
apresmoi Jul 25, 2026
6cfe9f7
Merge remote-tracking branch 'origin/main' into feat/review-loops
apresmoi Jul 25, 2026
cbc25d6
fix: close review-loop correctness gaps (E6)
apresmoi Jul 26, 2026
ddbc355
fix: refresh package asset manifest (E6)
apresmoi Jul 26, 2026
a3982ca
fix: keep retained Runs and native hooks operational (E6)
apresmoi Jul 26, 2026
3ab3fc2
Merge remote-tracking branch 'origin/main' into feat/review-loops
apresmoi Jul 26, 2026
1e34e57
fix: refresh checklist loop goldens after main merge (E6)
apresmoi Jul 26, 2026
fb70e78
Merge remote-tracking branch 'origin/main' into feat/review-loops
apresmoi Jul 26, 2026
c84b358
fix: preserve accepted Loop report retries during prune (E6)
apresmoi Jul 26, 2026
550d675
fix: refresh merged dashboard package assets (E6)
apresmoi Jul 26, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ dashboard/dist/
node_modules/
.oven-test-*/
.playwright-cli/
.worktrees/
output/
build/
*.zip
Expand Down
109 changes: 105 additions & 4 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 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).
The global package installs the `burnlist` command and registers the bundled Burnlist skill for Claude Code under `~/.claude/skills` and Codex under `~/.agents/skills`. Native observability 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 @@ -42,6 +42,38 @@ One skill owns Burnlist creation, hardening, execution, and maintenance. The pro

The dashboard scans lifecycle folders and refreshes automatically. Its progress views observe Burnlists without changing them. `New Oven` and `Run Burn` write local controller records under `.local/burnlist/` by default; they do not change canonical task state.

## Operational contract

Start with the thinnest working end-to-end path, prove it, then refine—unless
the repository or user has already chosen another architecture. Burnlist
coordinates task, execution, proof, and observation contracts; it does not
dictate project structure.

Per-item control recommendations are optional:

```sh
burnlist recommend <id>#<item>
burnlist recommend <id>#<item> --json
```

They suggest the lightest fitting direct/gate/review/branch workflow, a
provider-neutral model class and effort, P0-P4 review handling, and an honest
metric or task-fit Oven when one adds value. The user remains authoritative.

The Checklist labels items `PENDING`, `ACTIVE`, `WAITING`, `BLOCKED`, or
`COMPLETED` from canonical Burnlist and Run/claim state—never checklist
position. Recent correlated hooks may only refine an active item to
**progressing** and show bounded observational facts. The Loop Progress lens
separates canonical checks/gates/reviews and blockers from observational
agent/model/effort, file activity, timing, usage, and bounded forecasts.
Missing facts stay unavailable.

Creation, registration, assignment, and Oven commands print the next useful
action and canonical dashboard URL. See the
[operational UX contract](skills/burnlist/references/operational-ux.md) for
coarse-to-fine guidance, P0-P4 policy, provenance, optimization measures, and
the required browser inspection of every task-fit Oven.

## Ovens

An Oven is a declarative recipe for a Burn. Its `instructions.md` defines the outcome, canonical state, required inputs, and evidence rules. Its `<id>.oven` source declares controlled widgets and bindings used to present normalized data.
Expand Down Expand Up @@ -106,14 +138,26 @@ burnlist uninstall --global --purge

### 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:
`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 and Loop observation 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.
Codex and Claude receive native session, subagent, tool, and stop lifecycle
hooks; Claude also receives `PostToolUseFailure`. Edit events remain limited to
each agent's edit/write tools and invoke `burnlist streaming-diff hook`.
Advisory `burnlist hooks observe` entries publish bounded, correlated Loop
activity through ignored local event state without reporting outcomes or
changing canonical Run state. A native `PreToolUse` for an exact
`burnlist loop next|claim` command prepares a short-lived session/tool/Run
tuple; the matching `PostToolUse` binds it only after that Run's claim is
durable. An unmatched native session stays unbound, and Burnlist never assigns
the only live claim by process-of-elimination. 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
Expand Down Expand Up @@ -144,10 +188,67 @@ Untracked hook configs are added to `.git/info/exclude` by default; tracked conf
- `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 install` / `burnlist uninstall` manage the independent agent-skill registrations.
- `burnlist hooks install|uninstall|status` manages the independent per-repository Streaming Diff hooks.
- `burnlist hooks install|uninstall|status` manages the independent per-repository native observability hooks.

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

## Built-in Loops (Stage 1)

Items may use direct execution or one of exactly three built-in Loops:

```text
direct Start -> Implement -> Burn
review Start -> Implement -> Check -> Review -> Burn
gate Start -> Implement -> Check -> Burn
branch Start -> Plan -> N host-orchestrated branches -> Merge -> Check -> Review -> Burn
```

Inspect and explicitly trust the repository check with `burnlist loop
capability inspect|trust`, then assign an item with `burnlist loop assign`.
Burnlist stores no agent profile, provider route, subscription, or login. Run
`burnlist loop view <item-ref>`
and paste its complete ASCII output into the work handoff; it is the frozen
graph, pin, and completion-path record for that item. Create a Run with `loop
create`; for each agent node, use `loop next`, invoke a native subagent or
external provider CLI from the host with its small task, and use `loop submit`
for the semantic result. Burnlist automatically
advances its trusted checks and gates to the next agent or terminal node.
Inspect with `loop status|inspect`; `pause` and `stop` are idle-Run controls,
proof-gated `reconcile` handles a demonstrably lost claim, and `loop prune
--retain <count>` archives only non-current failed/stopped/exhausted/needs-human
histories.
`loop list` keeps a bounded newest-history public window even when more history
is retained. Only a
converged Run can be applied by `loop complete`; the
command is idempotent and performs the normal shrinking-list completion.

The host owns every provider invocation. Codex and Claude can use native
subagents; any host can deliberately harness Codex CLI, AGY, Grok, or another
CLI through the installed skill's explicit recipes. Before the first Loop, the
skill inventories available native agents, CLIs, live access, and subscriptions
without reading credentials, shows the result, and asks which providers the
user wants to use or set up. Burnlist never installs, authenticates, configures,
or launches an agent provider. Host execution remains independent from
Streaming Diff hooks.

The Checklist UI is read-only and shows the active node, attempt, results,
transition history, recent correlated hook activity, and a local forecast
range with confidence and provenance. Forecasts are bounded private history
learned only after accepted semantic completion; missing token facts remain
missing and subscription pricing never becomes an invented cost. Burnlist enforces
the graph, claim identity, trusted checks, budgets, closed outcomes, and atomic
CLI writes. Provider permissions remain host-supervised, not an OS sandbox.
Burnlist-native parallel scheduling, Docker isolation, metrics gates,
worktrees, and background execution are deliberately unsupported
in Stage 1. The Branch host uses native or CLI subagents when available and
falls back to the same slices sequentially. Items with no Loop assignment keep the ordinary direct
`burnlist burn` workflow.

See the [Loop CLI reference](website/src/content/docs/loops.mdx) for the exact
setup and recovery commands. Installing the Burnlist skill (`burnlist install`)
does not install Streaming Diff hooks (`burnlist hooks install`), and neither
is required to use the Loop.

## Build and Verify

From a source checkout:
Expand Down
48 changes: 46 additions & 2 deletions bin/burnlist.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ const knownSubcommands = new Set([
"oven",
"new",
"show",
"recommend",
"ready",
"start",
"close",
"burn",
"loop",
"agent",
"route",
"register",
"unregister",
"roots",
Expand All @@ -33,6 +37,19 @@ function printSkillUsage(command) {
console.log(`${usage}\n\nInstall and remove Burnlist-managed agent skills for Codex and Claude.`);
}

function printLifecycleUsage(command) {
const usages = {
new: "Usage: burnlist new [--repo <path>]",
show: "Usage: burnlist show <id>[#<item>] [--repo <path>]",
recommend: "Usage: burnlist recommend <id>#<item> [--json] [--repo <path>]",
ready: "Usage: burnlist ready <id> [--repo <path>]",
start: "Usage: burnlist start <id> [--repo <path>]",
close: "Usage: burnlist close <id> [--repo <path>]",
burn: "Usage: burnlist burn <id> <item> [--check] [--repo <path>]",
};
console.log(usages[command]);
}

async function main() {
if (args[0] === "install" || args[0] === "uninstall") {
if (args.includes("--help") || args.includes("-h")) {
Expand All @@ -43,6 +60,12 @@ if (args[0] === "install" || args[0] === "uninstall") {
return;
}

if (["new", "show", "recommend", "ready", "start", "close", "burn"].includes(args[0])
&& (args.includes("--help") || args.includes("-h"))) {
printLifecycleUsage(args[0]);
return;
}

if (args[0] === "differential-testing" && args[1] === "schema") {
console.log(resolve(packageRoot, "ovens", "differential-testing", "engine", "data.schema.json"));
return;
Expand Down Expand Up @@ -83,7 +106,7 @@ if (args[0] && !args[0].startsWith("--") && !["-h", "-v"].includes(args[0]) && !
process.exit(2);
}

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

Usage:
Expand All @@ -101,10 +124,28 @@ Usage:
burnlist oven <list|view|use|set|bind|unbind|bindings|event|create|update|adopt|upgrade|fork> ...
burnlist new [--repo <path>]
burnlist show <id>[#<item>] [--repo <path>]
burnlist recommend <id>#<item> [--json] [--repo <path>]
burnlist ready <id> [--repo <path>]
burnlist start <id> [--repo <path>]
burnlist close <id> [--repo <path>]
burnlist burn <id> <item> [--check] [--repo <path>]
burnlist loop assign <ItemRef> <LoopRef> [--repo <path>]
burnlist loop unassign <ItemRef> [--repo <path>]
burnlist loop view <LoopRef|ItemRef|review> [--repo <path>]
burnlist loop create <ItemRef> [--repo <path>]
burnlist loop next <RunRef> [--repo <path>]
burnlist loop submit <RunRef> (--outcome <complete|approve|reject|escalate> | --result <file>) [--repo <path>]
burnlist loop claim <RunRef> [--repo <path>] (recovery/diagnostics)
burnlist loop report <ClaimRef> (--outcome <complete|approve> | --result <file>) [--repo <path>]
burnlist loop abandon <ClaimRef> --reason <host-cancelled|host-lost|expired> [--repo <path>]
burnlist loop list [--repo <path>]
burnlist loop prune [--retain <count>] [--repo <path>]
burnlist loop status|inspect <RunRef> [--repo <path>]
burnlist loop pause|stop <RunRef> [--repo <path>] (idle Run only)
burnlist loop reconcile <RunRef> --recovery-proof <hex> [--repo <path>]
burnlist loop complete <RunRef> [--repo <path>]
burnlist loop capability <inspect|trust> <id> ...
burnlist loop setup status [--repo <path>]
burnlist register [path]
burnlist unregister [path]
burnlist roots [--prune]
Expand Down Expand Up @@ -142,8 +183,11 @@ if (args[0] === "oven") {
await import("../src/cli/streaming-diff-cli.mjs");
} else if (args[0] === "hooks") {
await import("../src/cli/hooks-cli.mjs");
} else if (["new", "show", "ready", "start", "close", "burn"].includes(args[0])) {
} else if (["new", "show", "recommend", "ready", "start", "close", "burn"].includes(args[0])) {
await import("../src/cli/lifecycle-cli.mjs");
} else if (args[0] === "loop") {
const { runLoopCliEntry } = await import("../src/cli/loop-cli.mjs");
await runLoopCliEntry(args.slice(1));
} else if (["register", "unregister", "roots", "init"].includes(args[0])) {
await import("../src/cli/registry-cli.mjs");
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ test("checklist detail renders the split progress surface and event card list",
assert.equal(markup.indexOf("Second event") < markup.indexOf("First event"), true);
assert.match(markup, /First proof\./u);
assert.match(markup, /Second proof\./u);
assert.doesNotMatch(markup, /class="event-card-field-label">Outcome|>Outcome</u);
assert.doesNotMatch(markup, /class="event-card-field-label">Outcome/u);
assert.equal((markup.match(/class="event-card-field event-card-field-outcome"/gu) ?? []).length, 2);
assert.equal((markup.match(/class="event-card-summary"/gu) ?? []).length, 2);
assert.equal((markup.match(/class="event-card-description"/gu) ?? []).length, 2);
Expand Down
23 changes: 19 additions & 4 deletions dashboard/src/components/ChecklistDashboard/ChecklistDashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { ClipboardList, Clock3, Gauge, TimerReset } from "lucide-react";
import type { ChecklistProgressData, HistoryPoint } from "@lib";
import { checklistEventDetailFields, compactAge, eventRows, formatDuration, progressHistory, timing } from "@lib/checklist-adapter";
import { checklistEventDetailFields, compactAge, effectiveItemWork, eventRows, formatDuration, progressHistory, timing } from "@lib/checklist-adapter";
export { checklistEventDetailFields } from "@lib/checklist-adapter";
import "./ChecklistDashboard.css";
import { buildChecklistProgressChart, KpiItem, KpiStrip, LogTable, ProgressDonut, SectionHeader } from "@oven";
import { LoopGraph } from "@/components/LoopGraph";
import { ChecklistWorkspace } from "@/oven/ChecklistWorkspace";

function ChecklistKpis({ data }: { data: ChecklistProgressData }) {
const durations = timing(data);
const current = data.active[0];
const current = data.active.find((item) => effectiveItemWork(data, item).state === "ACTIVE")
?? data.active.find((item) => effectiveItemWork(data, item).state === "BLOCKED")
?? data.active.find((item) => effectiveItemWork(data, item).state === "WAITING")
?? data.active[0];
const currentState = current ? effectiveItemWork(data, current).state : "PENDING";
const metrics = [
{ icon: Clock3, heading: "Elapsed", value: formatDuration(durations.elapsed) },
{ icon: Gauge, heading: "Avg pace", value: formatDuration(durations.pace) },
{ icon: TimerReset, heading: "Time left", value: formatDuration(durations.timeLeft) },
];
return <KpiStrip ariaLabel="Burnlist progress KPIs" className="driving-parity-kpi-strip has-burns checklist-kpi-strip">
<KpiItem className="driving-parity-kpi-item driving-parity-kpi-section checklist-kpi-current" title={current?.title ?? "No active task"} visual={<ClipboardList aria-hidden="true" className="driving-parity-kpi-gauge driving-parity-kpi-scenario-icon" />} heading="Current" value={current ? `${current.id} · Active` : "Complete"} />
<KpiItem className="driving-parity-kpi-item driving-parity-kpi-section checklist-kpi-current" title={current?.title ?? "No active task"} visual={<ClipboardList aria-hidden="true" className="driving-parity-kpi-gauge driving-parity-kpi-scenario-icon" />} heading="Current" value={current ? `${current.id} · ${currentState}` : "Complete"} />
<KpiItem className="driving-parity-kpi-item driving-parity-kpi-section driving-parity-kpi-progress" title={`${data.done} of ${data.total} tasks complete`} visual={<ProgressDonut percent={data.percent} />} heading="Progress" value={<><span className="pass">{data.done}</span><span className="separator">·</span><span className="total">{data.total}</span> <span className="pass">({data.percent}%)</span></>} />
{metrics.map(({ icon: Icon, heading, value }) => <KpiItem className="driving-parity-kpi-item driving-parity-kpi-section" heading={heading} key={heading} value={value} visual={<Icon aria-hidden="true" className="driving-parity-kpi-gauge driving-parity-kpi-scenario-icon" />} />)}
</KpiStrip>;
Expand Down Expand Up @@ -101,10 +107,19 @@ export function EventCardList({ data }: { data: ChecklistProgressData }) {
})}{!rows.length && <p className="target-empty">No completed events yet.</p>}</div></section>;
}

export function LoopRunPanel({ data }: { data: ChecklistProgressData }) {
return <LoopGraph
run={data.loopRun}
diagnostic={data.loopRun?.diagnostic ?? data.loopProjectionDiagnostic}
message={data.loopProjectionMessage}
title="Current item Loop"
/>;
}

export function ChecklistDashboard({ data }: { data: ChecklistProgressData }) {
useEffect(() => {
document.body.classList.add("driving-parity-view", "checklist-detail-view");
return () => document.body.classList.remove("driving-parity-view", "checklist-detail-view");
}, []);
return <div className="shell detail-view-shell driving-parity-view checklist-detail-shell"><main className="detail-view" id="burnlist-detail"><section className="differential-overview checklist-overview"><ChecklistKpis data={data} /></section><div className="detail-workspace checklist-progress-workspace" data-detail-tab="dashboard"><ProgressLedger data={data} /><ProgressPanel data={data} /></div><EventCardList data={data} /></main></div>;
return <div className="shell detail-view-shell driving-parity-view checklist-detail-shell"><main className="detail-view" id="burnlist-detail"><section className="differential-overview checklist-overview"><ChecklistKpis data={data} /></section><div className="detail-workspace checklist-progress-workspace" data-detail-tab="dashboard"><ProgressLedger data={data} /><ProgressPanel data={data} /></div><ChecklistWorkspace data={data} /><EventCardList data={data} /></main></div>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ test("adaptChecklist precomputes the checklist oven payload", async () => {
const { adaptChecklist } = await import(`${new URL(`file://${outputPath}`).href}?test=${Date.now()}`);
assert.deepEqual(adaptChecklist(checklistFixture), {
raw: checklistFixture,
items: [],
current: { value: "Complete", title: "No active task" },
progress: { done: 2, total: 2, percent: 100, title: "2 of 2 tasks complete" },
durations: { elapsed: "10m", pace: "5m", timeLeft: "0m" },
Expand Down
Loading
Loading