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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,8 @@ session lists it, and you can resume with one word. Finished tasks are skipped.
parallel saves; when that is 1 or less (always for a single lane), a direct build is
recommended first, and you can still pick parallel lanes.
2. **Manifest.** Claude writes a JSON manifest covering lanes, model tier per task
(`standard`, `sonnet`, or `light`; security-sensitive tasks are always `standard`),
(`standard` on Opus, `sonnet` on Sonnet, or `light` on Haiku; security-sensitive tasks are
always `standard`),
project commands, commit rules taken from your CLAUDE.md, and budgets.
3. **Dry run and consent.** The workflow validates the manifest without spawning anything,
and Claude shows the table. "Just run it" and auto mode do not skip it.
Expand Down
12 changes: 9 additions & 3 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,9 @@ Fields: reference.md "Manifest fields". In order:
a join task without it waits for every earlier one (reference.md "Join waves").
4. `profile` and tiers: reference.md "Profiles", "Tiers", "Batching". `profile: "lite"` only
for one lane, at most 8 tasks, no security task, and no `hooks.post_integrate`; else
`full`. Tiers `standard`, `sonnet`, `light` (security tasks always `standard`); `batch`
keys for consecutive tiny light tasks. The user's model preference wins when it is stricter
(e.g. "Opus for everything" means all `standard`).
`full`. Tiers `standard` (Opus), `sonnet` (Sonnet), `light` (Haiku); security tasks always
`standard`; `batch` keys for consecutive tiny light tasks. The user's model preference wins
when it is stricter (e.g. "Opus for everything" means all `standard`).
5. `commit_rules`: one string from the user's and project's rules (memory, CLAUDE.md, the
plan's conventions, CONTRIBUTING; reference.md). Every agent prompt carries it.
6. `commands`, `task_checks` and `widen_paths` (from the plan's tests and shared files), paths,
Expand Down Expand Up @@ -282,9 +282,15 @@ The run returns `status`:
"check-verify error" with the exit code and stderr, keep the marker, and recover as reference.md "Verify
evidence" says. Lead with `acceptance` (reference.md "Report"). Only `accepted` is delivered work.
`rejected` or `unverified`: show every reason, notify, keep the marker; never call it done.
`checks_failed_at_start` (the failures fail at the run's start point too) is shown like any
rejection: show its detail, which names the evidence, and suggest the `ledger accept` text
for it (reference.md "Report").
The user fixes and resumes, or explicitly accepts named reasons or warnings: record that
with `<python> <skill_dir>/scripts/ledger accept <ledger_dir> <repo root> <delivered_sha>
"<what>"`. Then rerun archive-run, audit and tool-outcomes, so the override reaches the archive.
Once the accepts cover every reason (and the warnings the user named) and those reruns are
done, remove the marker with `bash <skill_dir>/scripts/active-run remove <run_id>`, so a later
session does not resume the run.

Report: run `<python> <skill_dir>/scripts/run-report <transcript dir> <manifest> --out
<run_dir>/<plan-name>.<run_id>.report.json` (Launch step 4's transcript dir) and append
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.6.7
1.6.8
329 changes: 287 additions & 42 deletions reference.md

Large diffs are not rendered by default.

651 changes: 590 additions & 61 deletions run.workflow.js

Large diffs are not rendered by default.

18 changes: 16 additions & 2 deletions scripts/_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,12 @@ def _counts(audit):
open_work = run["open_work"]
final = open_work.get("final")
findings = final["value"]["findings"] if _is_ref(final) and isinstance(final.get("value"), dict) else []
recorded = run["launches"].get("recorded")
recorded = recorded.get("value") if _is_ref(recorded) and isinstance(recorded.get("value"), dict) else None
return {
"verdict": run["acceptance"]["verdict"].get("value"),
"launches": len(launches),
"recorded": recorded["recorded"] if recorded is not None else None,
"latest": run["launches"].get("latest"),
"order": run["launches"].get("order"),
"tasks": len(tasks),
Expand All @@ -211,10 +214,13 @@ def _counts(audit):
def _summary_lines(audit):
c = _counts(audit)
lines = ["audit of run %s: verdict %s" % (audit.get("run_id") or "unknown", _scalar(c["verdict"]))]
count = "%d" % c["launches"]
if c["recorded"] is not None and c["recorded"] > c["launches"]:
count = "%d archived, %d recorded" % (c["launches"], c["recorded"])
if c["latest"] is not None:
lines.append("launches: %d, latest %s" % (c["launches"], c["latest"]))
lines.append("launches: %s, latest %s" % (count, c["latest"]))
else:
lines.append("launches: %d, latest unknown (%s)" % (c["launches"], c["order"]))
lines.append("launches: %s, latest unknown (%s)" % (count, c["order"]))
lines.append("tasks: %d; committed %d, reviewed %d, verified %d, accepted %d"
% (c["tasks"], c["committed"], c["reviewed"], c["verified"], c["accepted"]))
if c["outside"] is None:
Expand Down Expand Up @@ -297,6 +303,14 @@ def _md_launches(launches, out):
out += _quote("reason", e["reason"], " ")
for gap in e.get("layout") or []:
out.append(_ref("layout", gap, " "))
recorded = launches.get("recorded")
if _is_ref(recorded):
value = recorded.get("value")
text = ("%d recorded, %d archived" % (value["recorded"], value["archived"])
if isinstance(value, dict) else None)
out.append(_ref("launches the ledger records", recorded, value=text))
for gap in launches.get("missing") or []:
out.append(_ref("missing launch", gap))
out.append("")


Expand Down
32 changes: 31 additions & 1 deletion scripts/_audit_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
"verify_head", "accepted_run", "order_known"}) are the run facts the
per-task sections take.

launches also compares the launches the archived ledger records
(_launches.recorded_launches) with the ones archived: recorded is a ref of
{"recorded", "archived", "named"} (unavailable without a ledger), and
missing holds one missing ref per recorded launch the archive lacks when it
records more (the named wf ids with no launch here, then None per unnamed
one); equal counts give none.

Run-level facts come from the latest launch (_audit_core.latest_launch);
when the launch order is unknown they are unavailable, never taken from an
arbitrary launch.
Expand Down Expand Up @@ -59,8 +66,10 @@
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
import _audit_core as c # noqa: E402
import _autopsy # noqa: E402
import _launches as launch_counts # noqa: E402 (the name _launches is the section below)

SCRIPTS = os.path.dirname(os.path.realpath(__file__))
MISSING_LAUNCH = "the ledger records this launch; its transcripts are not archived"

GATES = ("match", "not_required", "missing", "stale", "mismatch", "invalid", "unavailable", "checker_error")
# What check-verify itself prints; checker_error is audit's own.
Expand Down Expand Up @@ -416,6 +425,26 @@ def _launches(run, pairs):
return c.ref(entries, "present", sources)


def _recorded(run):
"""(the recorded ref, the missing refs): the launches the archived ledger
records (_launches.recorded_launches) against the launches archived here.
When it records more, each one missing is a gap: the named wf ids with no
launch here first, then None for each unnamed one."""
if not run.get("ledger"):
return c.ref(None, "unavailable", [], "no ledger in the archive"), []
recorded = launch_counts.recorded_launches(os.path.join(run["folder"], run["ledger"]))
sources = [c.src(run["ledger"], None)]
archived = set(l["wf"] for l in run.get("launches") or [])
count, named = recorded["count"], recorded["named"]
value = {"recorded": count, "archived": len(archived), "named": named}
missing = []
if count > len(archived):
absent = [wf for wf in named if wf not in archived]
absent += [None] * max(0, count - len(archived) - len(absent))
missing = [c.ref(wf, "missing", sources, MISSING_LAUNCH) for wf in absent]
return c.ref(value, "present", sources), missing


# ---- code outside recorded task ranges ----

def _version(run, launch):
Expand Down Expand Up @@ -915,9 +944,10 @@ def run_sections(run, git, head=None, check_verify=run_check_verify, git_note=No
"order_known": order_known,
}
want = expected_checks(manifest)
recorded, missing = _recorded(run)
return {
"launches": {"latest": launch["wf"] if launch is not None else None, "order": why,
"entries": _launches(run, pairs)},
"entries": _launches(run, pairs), "recorded": recorded, "missing": missing},
"code_outside_ranges": _code_outside(run, git, git_note, launch, why, pairs),
"checks": _checks(run, launch, why, gate, evidence, rule, want),
"acceptance": _acceptance(run, launch, why, facts, gate, evidence, rule, accepted_events, pairs),
Expand Down
5 changes: 3 additions & 2 deletions scripts/_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,14 @@

# The keys of an index entry. folder is relative to the history dir; project
# is repo.root, or the shadow's project root; platform is sys.platform;
# first_launch and last_launch are ISO 8601 UTC.
# first_launch and last_launch are ISO 8601 UTC; launches_recorded is the
# launch count the run's ledger recorded (_launches), or "unavailable".
ENTRY_KEYS = (
"run_id", "folder", "ledger_dir",
"project", "plan", "engine_version",
"machine", "platform",
"first_launch", "last_launch",
"launches", "status", "acceptance",
"launches", "launches_recorded", "status", "acceptance",
)

# How often index_lock retries while another process holds the lock.
Expand Down
68 changes: 68 additions & 0 deletions scripts/_launches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""The launches a run's ledger recorded (archive-run and the audit, spec 4.2).

Import with:
sys.dont_write_bytecode = True
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
import _launches

recorded_launches(ledger_dir) gives {"count": int, "named": [wf ids]}:
named the sorted distinct wf ids the ledger names: the 'wf' of every
carried event in <ledger_dir>/_run.jsonl and the <id> of every
doctor-<id>.json file in ledger_dir (1.6.3 and later);
count the larger of the number of run_ended events in _run.jsonl (one
per launch the session recorded, relaunches included, never a dry
run) and len(named) (a launch whose session died has no
run_ended).
Setup run_started events are not counted: a resume may run setup twice in
one launch. A missing or unreadable file counts as none, and a line that is
not a JSON object is skipped. Nothing here writes.
"""
import glob
import json
import os

_DOCTOR_PREFIX = "doctor-"
_DOCTOR_SUFFIX = ".json"


def _events(ledger_dir):
try:
with open(os.path.join(ledger_dir, "_run.jsonl"), "rb") as f:
lines = f.read().splitlines()
except OSError:
return []
out = []
for line in lines:
try:
event = json.loads(line.decode("utf-8"))
except (UnicodeDecodeError, ValueError):
continue
if isinstance(event, dict):
out.append(event)
return out


def _doctor_ids(ledger_dir):
out = []
pattern = os.path.join(glob.escape(ledger_dir), _DOCTOR_PREFIX + "*" + _DOCTOR_SUFFIX)
for path in glob.glob(pattern):
name = os.path.basename(path)
wf = name[len(_DOCTOR_PREFIX):-len(_DOCTOR_SUFFIX)]
if wf and os.path.isfile(path):
out.append(wf)
return out


def recorded_launches(ledger_dir):
"""{"count": int, "named": [wf ids sorted]} for the ledger in ledger_dir."""
ended = 0
named = set()
for event in _events(ledger_dir):
kind = event.get("event")
if kind == "run_ended":
ended += 1
elif kind == "carried" and isinstance(event.get("wf"), str) and event["wf"]:
named.add(event["wf"])
named.update(_doctor_ids(ledger_dir))
named = sorted(named)
return {"count": max(ended, len(named)), "named": named}
Loading
Loading