From 99bca9b17b31af912fba923efe2baee1b31fe4f6 Mon Sep 17 00:00:00 2001 From: Ajay Surya Date: Sun, 28 Jun 2026 00:53:19 +0530 Subject: [PATCH 1/5] feat: add Claude Code claim-warrants scaffolder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `dorian claude-code install-claim-warrants` scaffolds a project-local Claude Code skill at `.claude/skills/dorian-claim-warrants/` (invoked as /dorian-claim-warrants) that DRAFTS Dorian claim warrants for the checkable facts in a coding agent's change summary — config values, signatures/defaults, constants, file/symbol references — which `dorian verify` then proves deterministically. The model only drafts; no model is added to the verification path. - New module src/dorian/claude_code.py mirrors init.py (build_plan/apply, an explicit package-data manifest, _ensure_within + per-pid atomic writes). Writes files only; idempotent; never overwrites without --force; repo-contained. - Nested `claude-code install-claim-warrants` subcommand with --with-hook, --no-hook, --settings-only, --dry-run, --force, --print-next-steps, --target. - Packaged templates (ship as wheel package data via importlib.resources): SKILL.md, bundle README, examples (good/bad claims, final-message walkthrough), change-note + claims.json skeletons, checker-selection + safety-boundary references, review-first and trusted-local settings examples. - Opt-in, reminder-only Stop hook (stdlib only): returns additionalContext, never decision:block (loop-safe), no-ops on stop_hook_active/non-Stop/no-git/no-relevant- change/existing-artifacts/skip-markers, fails open, writes nothing, runs only read-only git status. Not enabled by scaffolding. Co-Authored-By: Claude Opus 4.8 --- src/dorian/claude_code.py | 239 +++++++++++++++++ src/dorian/cli.py | 44 +++ src/dorian/commands.py | 104 ++++++++ .../dorian-claim-warrants/README.md | 65 +++++ .../dorian-claim-warrants/SKILL.md | 114 ++++++++ .../examples/bad-claims.md | 23 ++ .../examples/final-message-example.md | 34 +++ .../examples/good-claims.json | 49 ++++ .../reference/checker-selection.md | 56 ++++ .../reference/safety-boundary.md | 52 ++++ .../templates/change-note.md | 41 +++ .../templates/claims.json | 13 + .../hooks/dorian_claim_warrants_stop.py | 250 ++++++++++++++++++ ...n-claim-warrants.review-first.example.json | 9 + ...-claim-warrants.trusted-local.example.json | 11 + 15 files changed, 1104 insertions(+) create mode 100644 src/dorian/claude_code.py create mode 100644 src/dorian/templates/claude_code/dorian-claim-warrants/README.md create mode 100644 src/dorian/templates/claude_code/dorian-claim-warrants/SKILL.md create mode 100644 src/dorian/templates/claude_code/dorian-claim-warrants/examples/bad-claims.md create mode 100644 src/dorian/templates/claude_code/dorian-claim-warrants/examples/final-message-example.md create mode 100644 src/dorian/templates/claude_code/dorian-claim-warrants/examples/good-claims.json create mode 100644 src/dorian/templates/claude_code/dorian-claim-warrants/reference/checker-selection.md create mode 100644 src/dorian/templates/claude_code/dorian-claim-warrants/reference/safety-boundary.md create mode 100644 src/dorian/templates/claude_code/dorian-claim-warrants/templates/change-note.md create mode 100644 src/dorian/templates/claude_code/dorian-claim-warrants/templates/claims.json create mode 100644 src/dorian/templates/claude_code/hooks/dorian_claim_warrants_stop.py create mode 100644 src/dorian/templates/claude_code/settings/settings.dorian-claim-warrants.review-first.example.json create mode 100644 src/dorian/templates/claude_code/settings/settings.dorian-claim-warrants.trusted-local.example.json diff --git a/src/dorian/claude_code.py b/src/dorian/claude_code.py new file mode 100644 index 0000000..d6b38d2 --- /dev/null +++ b/src/dorian/claude_code.py @@ -0,0 +1,239 @@ +"""`dorian claude-code install-claim-warrants`: scaffold the Claude Code skill. + +Installs a project-local Claude Code **skill** (and an opt-in, reminder-only Stop +hook) that drafts Dorian *claim warrants* for the checkable facts in a coding +agent's change summary. The skill only DRAFTS; ``dorian verify`` proves the claims +deterministically and token-free — no model runs at check time. + +Like ``dorian init``, this module writes files only. It never runs a checker, never +executes code, never writes outside the target repo root, and never overwrites an +existing file without ``--force`` (re-running is idempotent). Template content is +shipped as package data under ``dorian/templates/claude_code/`` and copied verbatim, +so it works from an installed wheel, not just an editable checkout. +""" + +from __future__ import annotations + +import os +from dataclasses import dataclass +from importlib.resources import files +from pathlib import Path + +# Logical groups a caller can select. "skill" and "settings" install by default; +# "hook" is opt-in (--with-hook). --settings-only selects {"settings"} alone. +GROUPS = ("skill", "settings", "hook") +DEFAULT_GROUPS = frozenset({"skill", "settings"}) + +SKILL_DIR = ".claude/skills/dorian-claim-warrants" +HOOK_PATH = ".claude/hooks/dorian_claim_warrants_stop.py" + + +@dataclass(frozen=True) +class _Template: + """One template file: package-relative source, repo-relative dest, group, blurb.""" + + src: str # relative to dorian/templates/claude_code/ + dest: str # repo-relative posix path + group: str # skill | settings | hook + blurb: str + + +# The authoritative install manifest. Every `src` must exist in package data +# (asserted by tests); `dest` is where it lands in the target repo. Deterministic +# order — no directory walking, no surprise files. +MANIFEST: tuple[_Template, ...] = ( + _Template( + "dorian-claim-warrants/SKILL.md", + f"{SKILL_DIR}/SKILL.md", + "skill", + "the skill, invoked as /dorian-claim-warrants", + ), + _Template( + "dorian-claim-warrants/README.md", + f"{SKILL_DIR}/README.md", + "skill", + "what the bundle is and how to use it", + ), + _Template( + "dorian-claim-warrants/examples/good-claims.json", + f"{SKILL_DIR}/examples/good-claims.json", + "skill", + "a worked set of strong claims", + ), + _Template( + "dorian-claim-warrants/examples/bad-claims.md", + f"{SKILL_DIR}/examples/bad-claims.md", + "skill", + "claims to leave as prose, and why", + ), + _Template( + "dorian-claim-warrants/examples/final-message-example.md", + f"{SKILL_DIR}/examples/final-message-example.md", + "skill", + "a summary turned into claim warrants", + ), + _Template( + "dorian-claim-warrants/templates/change-note.md", + f"{SKILL_DIR}/templates/change-note.md", + "skill", + "change-note skeleton the skill fills in", + ), + _Template( + "dorian-claim-warrants/templates/claims.json", + f"{SKILL_DIR}/templates/claims.json", + "skill", + "claims.json skeleton", + ), + _Template( + "dorian-claim-warrants/reference/checker-selection.md", + f"{SKILL_DIR}/reference/checker-selection.md", + "skill", + "match the checker to the claim", + ), + _Template( + "dorian-claim-warrants/reference/safety-boundary.md", + f"{SKILL_DIR}/reference/safety-boundary.md", + "skill", + "model drafts, Dorian proves; not a sandbox", + ), + _Template( + "settings/settings.dorian-claim-warrants.review-first.example.json", + ".claude/settings.dorian-claim-warrants.review-first.example.json", + "settings", + "review-first permissions example (default)", + ), + _Template( + "settings/settings.dorian-claim-warrants.trusted-local.example.json", + ".claude/settings.dorian-claim-warrants.trusted-local.example.json", + "settings", + "trusted-local permissions example (opt-in)", + ), + _Template( + "hooks/dorian_claim_warrants_stop.py", + HOOK_PATH, + "hook", + "opt-in, reminder-only Stop hook (not auto-enabled)", + ), +) + +NEXT_STEPS = ( + "Restart Claude Code (a new skills directory registers only at startup)", + "After a coding change, invoke: /dorian-claim-warrants", + "Review the drafted docs/changes/.md and .claims.json", + "Prove them: dorian verify docs/changes/.md --claims" + " docs/changes/.claims.json --strength-gate=fail --binding-gate=warn", + "Commit the sealed .warrant alongside your change", + "On later PRs the Action runs `dorian revalidate` and REVOKEs a broken claim", +) + +TRUST_BOUNDARY = ( + "The skill drafts claims. Dorian verifies deterministically, token-free. " + "C4 pytest:/C5 shell: checkers can execute code — use trusted repos." +) + + +@dataclass(frozen=True) +class ScaffoldFile: + """One planned write: repo-relative path, content, blurb, group, prior existence.""" + + path: str + content: str + blurb: str + group: str + exists: bool + + +@dataclass(frozen=True) +class ScaffoldPlan: + repo_root: Path + files: tuple[ScaffoldFile, ...] + is_git: bool + + +@dataclass(frozen=True) +class ApplyResult: + created: tuple[str, ...] + overwritten: tuple[str, ...] + skipped: tuple[str, ...] + warnings: tuple[str, ...] + + +def find_repo_root(start: Path) -> tuple[Path, bool]: + """Repo root for ``start``: the nearest ancestor containing ``.git`` (file or + dir), else ``start`` resolved. Returns ``(root, is_git)``. Never raises.""" + start = start.resolve() + for d in (start, *start.parents): + if (d / ".git").exists(): + return d, True + return start, False + + +def _read_template(src: str) -> str: + """Read a template file from package data (works from wheel or editable).""" + node = files("dorian") + for part in ("templates", "claude_code", *src.split("/")): + node = node.joinpath(part) + return node.read_text(encoding="utf-8") + + +def _ensure_within(repo: Path, target: Path) -> None: + """Defense in depth: refuse to write anywhere outside the repo root.""" + resolved = (target if target.is_absolute() else repo / target).resolve() + if not resolved.is_relative_to(repo): + raise ValueError(f"refusing to write outside repo: {target}") + + +def _atomic_write(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_name(f"{path.name}.{os.getpid()}.tmp") # per-process: no shared-.tmp race + tmp.write_text(content, encoding="utf-8") + os.replace(tmp, path) + + +def build_plan(repo: Path, *, groups: frozenset[str] = DEFAULT_GROUPS) -> ScaffoldPlan: + """Compute (without writing) the files the install would scaffold for ``repo``. + + ``groups`` selects which manifest groups to install (subset of ``GROUPS``). + ``repo`` should already be the repo root (see ``find_repo_root``).""" + root, is_git = find_repo_root(repo) + selected = [t for t in MANIFEST if t.group in groups] + files_ = tuple( + ScaffoldFile( + path=t.dest, + content=_read_template(t.src), + blurb=t.blurb, + group=t.group, + exists=(root / t.dest).exists(), + ) + for t in selected + ) + return ScaffoldPlan(repo_root=root, files=files_, is_git=is_git) + + +def apply(plan: ScaffoldPlan, *, force: bool, dry_run: bool) -> ApplyResult: + """Realize the plan. Existing files are skipped unless ``force``; with + ``dry_run`` nothing is written but the same classification is returned.""" + created: list[str] = [] + overwritten: list[str] = [] + skipped: list[str] = [] + warnings: list[str] = [] + if not plan.is_git: + warnings.append( + "target is not a git repository — Dorian is git-native; " + "run `git init` so warrants and revalidation work" + ) + for f in plan.files: + target = plan.repo_root / f.path + _ensure_within(plan.repo_root, target) + if f.exists and not force: + skipped.append(f.path) + continue + if not dry_run: + _atomic_write(target, f.content) + (overwritten if f.exists else created).append(f.path) + return ApplyResult( + created=tuple(created), + overwritten=tuple(overwritten), + skipped=tuple(skipped), + warnings=tuple(warnings), + ) diff --git a/src/dorian/cli.py b/src/dorian/cli.py index 5871bd0..103072a 100644 --- a/src/dorian/cli.py +++ b/src/dorian/cli.py @@ -279,6 +279,50 @@ def build_parser() -> argparse.ArgumentParser: "bench", help="repo-local benchmark tooling (mutation, large-mutation, churn)" ) bench.add_argument("rest", nargs=argparse.REMAINDER) + + cc = sub.add_parser( + "claude-code", + help="scaffold Claude Code integrations (claim-warrants skill + opt-in hook)", + description="Scaffold Claude Code integrations for Dorian. Writes files only —" + " never runs a checker, executes code, or writes outside the target repo.", + ) + cc_sub = cc.add_subparsers(dest="cc_command", required=True) + icw = cc_sub.add_parser( + "install-claim-warrants", + help="scaffold the Dorian claim-warrants skill (and optional reminder Stop hook)" + " into .claude/ so /dorian-claim-warrants drafts claim warrants you then verify", + description="Scaffold a project-local Claude Code skill at" + " .claude/skills/dorian-claim-warrants/ (invoked as /dorian-claim-warrants) that" + " DRAFTS Dorian claim warrants for the checkable facts in a change summary, plus" + " review-first settings examples and an opt-in, reminder-only Stop hook. The skill" + " only drafts; `dorian verify` proves the claims deterministically. Writes files" + " only; never overwrites without --force; idempotent. Not a sandbox — trusted repos.", + ) + icw.add_argument( + "--force", action="store_true", help="overwrite existing scaffolded files (default: skip)" + ) + icw.add_argument("--dry-run", action="store_true", help="print the plan; write nothing") + icw.add_argument( + "--with-hook", + action="store_true", + help="also scaffold the opt-in reminder Stop hook (a file only — not auto-enabled)", + ) + icw.add_argument( + "--no-hook", + action="store_true", + help="skill + settings only, no hook (this is already the default)", + ) + icw.add_argument( + "--settings-only", action="store_true", help="write only the settings examples" + ) + icw.add_argument( + "--print-next-steps", + action="store_true", + help="print the post-install usage guide and trust boundary, then exit (no writes)", + ) + icw.add_argument( + "--target", help="target repo/project root (default: --repo, i.e. current directory)" + ) return p diff --git a/src/dorian/commands.py b/src/dorian/commands.py index 26cbd32..241d945 100644 --- a/src/dorian/commands.py +++ b/src/dorian/commands.py @@ -26,6 +26,7 @@ from dorian import ( bindings, claims_io, + claude_code, datachecks, gitio, init, @@ -1005,6 +1006,109 @@ def _print_init_summary(plan, result, *, dry_run: bool) -> None: print(f" {i}. {step}") +def cmd_claude_code(args: argparse.Namespace) -> int: + """Dispatch `dorian claude-code `.""" + if args.cc_command == "install-claim-warrants": + return _cmd_install_claim_warrants(args) + print(f"dorian claude-code: '{args.cc_command}' not implemented", file=sys.stderr) + return EXIT_USAGE + + +def _install_groups(args: argparse.Namespace) -> frozenset[str] | None: + """Resolve the manifest groups to install from the flags, or None on conflict.""" + if args.with_hook and args.no_hook: + print( + "dorian claude-code: --with-hook and --no-hook are mutually exclusive", + file=sys.stderr, + ) + return None + if args.settings_only: + return frozenset({"settings"}) + groups = set(claude_code.DEFAULT_GROUPS) + if args.with_hook: + groups.add("hook") + return frozenset(groups) + + +def _cmd_install_claim_warrants(args: argparse.Namespace) -> int: + """Scaffold the Dorian claim-warrants Claude Code skill (+ opt-in Stop hook). + Writes files only; never overwrites without --force; idempotent. Path/permission + problems are usage errors (exit 2).""" + if args.print_next_steps: + _print_claim_warrants_next_steps(with_hook=args.with_hook) + return EXIT_OK + target = Path(args.target).resolve() if args.target else _repo(args) + if _missing_repo(target, "claude-code install-claim-warrants"): + return EXIT_USAGE + groups = _install_groups(args) + if groups is None: + return EXIT_USAGE + try: + plan = claude_code.build_plan(target, groups=groups) + result = claude_code.apply(plan, force=args.force, dry_run=args.dry_run) + except (ValueError, OSError) as exc: + print(f"dorian claude-code install-claim-warrants: {exc}", file=sys.stderr) + return EXIT_USAGE + hook_installed = any(f.group == "hook" for f in plan.files) + if args.json: + print( + json.dumps( + { + "repo": str(plan.repo_root), + "is_git": plan.is_git, + "dry_run": args.dry_run, + "created": list(result.created), + "overwritten": list(result.overwritten), + "skipped": list(result.skipped), + "warnings": list(result.warnings), + "hook_installed": hook_installed, + "next_steps": list(claude_code.NEXT_STEPS), + "trust_boundary": claude_code.TRUST_BOUNDARY, + }, + indent=2, + ) + ) + else: + _print_install_summary(plan, result, dry_run=args.dry_run, hook_installed=hook_installed) + return EXIT_OK + + +def _print_install_summary(plan, result, *, dry_run: bool, hook_installed: bool) -> None: + blurbs = {f.path: f.blurb for f in plan.files} + header = ( + "dorian claude-code install-claim-warrants --dry-run (no files written)" + if dry_run + else "Dorian claim-warrants skill installed." + ) + print(header) + written = list(result.created) + list(result.overwritten) + if written: + print("\nWould create:" if dry_run else "\nWrote:") + width = max(len(p) for p in written) + for p in written: + print(f" {p.ljust(width)} {blurbs.get(p, '')}") + if result.skipped: + print("\nSkipped (already present — use --force to overwrite):") + for p in result.skipped: + print(f" {p}") + for w in result.warnings: + print(f"\nwarning: {w}") + _print_claim_warrants_next_steps(with_hook=hook_installed) + + +def _print_claim_warrants_next_steps(*, with_hook: bool) -> None: + print("\nNext:") + for i, step in enumerate(claude_code.NEXT_STEPS, 1): + print(f" {i}. {step}") + if with_hook: + print( + "\nThe reminder Stop hook was scaffolded but is OFF until you register it" + " under hooks.Stop in .claude/settings.json — the exact snippet is in" + f"\n {claude_code.SKILL_DIR}/README.md (it never blocks, never runs verify)." + ) + print(f"\nTrust boundary: {claude_code.TRUST_BOUNDARY}") + + def cmd_sync(args: argparse.Namespace) -> int: repo = _repo(args) if _missing_repo(repo, "sync"): diff --git a/src/dorian/templates/claude_code/dorian-claim-warrants/README.md b/src/dorian/templates/claude_code/dorian-claim-warrants/README.md new file mode 100644 index 0000000..8a7760f --- /dev/null +++ b/src/dorian/templates/claude_code/dorian-claim-warrants/README.md @@ -0,0 +1,65 @@ +# Dorian claim warrants — Claude Code skill bundle + +Installed by `dorian claude-code install-claim-warrants`. Contents: + +``` +dorian-claim-warrants/ + SKILL.md the skill (invoked as /dorian-claim-warrants) + README.md this file + examples/ good-claims.json, bad-claims.md, final-message-example.md + templates/ change-note.md, claims.json skeletons the skill fills in + reference/ checker-selection.md, safety-boundary.md +``` + +## What it does + +After a coding change, invoke **`/dorian-claim-warrants`**. The skill reads your +git diff and the agent's summary, drafts `docs/changes/.md` and +`docs/changes/.claims.json` for the **checkable** facts (config values, +signatures, constants, references), and prints the exact `dorian verify` command. + +**The model only drafts. `dorian verify` proves it** — deterministically and +token-free. A claim is a **DRAFT — not verified until Dorian verify passes**. Dorian +is **not a sandbox**; run it in **trusted repo**s only. + +```bash +dorian verify docs/changes/.md --claims docs/changes/.claims.json \ + --strength-gate=fail --binding-gate=warn +``` + +Commit the resulting `.warrant`. Later, `dorian revalidate --since ` flips a +broken claim to `REVOKED` (exit 4). + +> **Skill not showing as `/dorian-claim-warrants`?** Claude Code only registers a +> *new* top-level skills directory at startup — restart Claude Code (or start a new +> session) after the first install. + +## Optional reminder hook (opt-in) + +`dorian claude-code install-claim-warrants --with-hook` also scaffolds +`.claude/hooks/dorian_claim_warrants_stop.py` — a **reminder-only** Stop hook. It is +**off until you register it**. To enable, add to your project `.claude/settings.json`: + +```json +{ + "hooks": { + "Stop": [ + { "hooks": [ { "type": "command", + "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/dorian_claim_warrants_stop.py\"" } ] } + ] + } +} +``` + +The hook only nudges (`additionalContext`); it never blocks, never runs `dorian +verify` or tests, never writes files, and never executes project code. Suppress it +for a turn with `DORIAN_CLAIM_WARRANTS_SKIP=1`. + +## Settings examples + +`.claude/settings.dorian-claim-warrants.review-first.example.json` pre-allows only +inspection commands (`status`, `bindings`, `bind-suggest`). The `trusted-local` +variant additionally pre-allows `verify`/`revalidate` — use it only when you trust +the local repo and review agent-emitted `claims.json` before running it. Merge the +snippet you want into your own `.claude/settings.json`; the installer never edits it +for you. diff --git a/src/dorian/templates/claude_code/dorian-claim-warrants/SKILL.md b/src/dorian/templates/claude_code/dorian-claim-warrants/SKILL.md new file mode 100644 index 0000000..b64345c --- /dev/null +++ b/src/dorian/templates/claude_code/dorian-claim-warrants/SKILL.md @@ -0,0 +1,114 @@ +--- +name: Dorian claim warrants +description: >- + Draft and verify Dorian claim warrants for the checkable subset of a coding + agent's change summary — config values, signatures/defaults, constants, and + file/symbol references. Use when the user asks to create claim warrants, warrant + what changed, turn a change summary into checkable claim warrants, or run dorian + verify on a finished change. The model only drafts; `dorian verify` proves it + deterministically and token-free. Trusted repos only. +when_to_use: >- + After finishing a coding change and you want the checkable facts in the summary + held honest as the code drifts. Triggers: "claim warrants", "warrant this + change", "dorian verify", "claim warrants for what I changed". +--- + +# Dorian claim warrants + +Invoke with **`/dorian-claim-warrants`** after a coding change. Turn the +**checkable** facts in a coding agent's change summary into deterministic git +`.warrant` receipts that **revoke when later code makes one false** — token-free. + +**You only DRAFT. Dorian PROVES.** A claim is **DRAFT — not verified until Dorian +verify passes** (exits 0 and writes a `.warrant`). No model runs at check time. +Dorian is **not a sandbox** and runs in **trusted repo**s only — `C4 pytest:`/`C5 +shell:` checkers execute code. See [reference/safety-boundary.md](reference/safety-boundary.md). + +## Steps + +### 1. Orient +- Find the repo root (`git rev-parse --show-toplevel`). +- Read `git status --short` and `git diff` (and `git diff --staged`) to see what + *actually* changed. The diff is ground truth. +- Use the agent's final summary if available; **if there is no summary, write a + short change note from the real diff — never from imagination.** +- Choose a short kebab-case `` for the change. + +### 2. Extract only checkable claims +Include only statements a deterministic checker can falsify: +- config / package-metadata values +- Python signatures / defaults +- Python constants +- path / symbol references +- anchored string / regex facts +- a behavior claim **only** when a real, safe, known-passing `pytest` node exists + +Exclude (leave as prose): "cleaner", "better", "faster", "more maintainable", +"refactored", "updated docs", and any security/performance/behavior claim without a +falsifying checker. See [examples/bad-claims.md](examples/bad-claims.md). + +### 3. Choose strong checkers +Match the checker to the claim's `kind` — full map in +[reference/checker-selection.md](reference/checker-selection.md). Quick guide: +- config / package value → `config-value:` · signature → `py-signature:` · + constant → `py-const:` · existence/reference → `symbol:`/`path:` · anchored value + → `regex:` (anchor key **and** value) · real behavior → `C4 pytest:`. +- Never back a **behavior** claim with mere `symbol:` existence; never back a + **quantity** value with a bare existence check. `--strength-gate=fail` refuses both. +See [examples/good-claims.json](examples/good-claims.json) for a worked set. + +### 4. Write draft artifacts +Write two files (default paths): +- `docs/changes/.md` — from [templates/change-note.md](templates/change-note.md): + human summary · checkable claims included · non-checkable claims **intentionally + excluded** · the exact verify command · the trust-boundary note. +- `docs/changes/.claims.json` — valid, minimal; from + [templates/claims.json](templates/claims.json). Stable kebab-case `id`s; + `load_bearing: true` only for facts whose breakage should block a merge. + +### 5. Verify, or prepare verification +**Review-first (default).** Draft the files, then print the exact command and stop: +```bash +dorian verify docs/changes/.md --claims docs/changes/.claims.json \ + --strength-gate=fail --binding-gate=warn +``` +Tell the user: **"DRAFT — not verified until Dorian verify passes."** Do not claim +anything is verified. + +**Trusted-local (only if the user has explicitly approved running Dorian here).** +Run the command above. Then: +- If it exits 4 because a claim is false, **revise or drop that claim** and say why — + never fake a checker. +- If it ERRORs because the environment is missing (no `pytest`, etc.), report it as + fail-closed; do not pretend it passed. +- Preserve `--binding-gate=warn` warnings; do not hide them (weak binding = lower + coverage, **not** a false claim). +- Say "verified" **only** if `dorian verify` actually exited 0 and wrote a `.warrant`. + +### 6. Final response — use this template +``` +Artifact written: docs/changes/.md (+ .claims.json) +Claims drafted: +Claims excluded: +Dorian command run: +Exit code: <0 / 4 / 5 / not run> +Warrant created: .md.warrant, or "no — DRAFT only"> +Warnings: +Files to commit: docs/changes/.md, .claims.json, and the .warrant (once sealed) +Trust boundary: model drafted; Dorian verifies deterministically; not a sandbox; trusted repo only. +``` + +## Later: revalidation +On a future PR, `dorian revalidate --since ` re-checks only the affected +claims and folds a broken warrant to `REVOKED` (exit 4) — token-free. Commit the +`.warrant` alongside the change so the GitHub Action can re-check it. + +## Hard rules +- The model drafts; **Dorian verifies**. Never seal by asserting — only `dorian + verify` seals. +- Never imply Dorian verified the *whole* summary, sandboxed anything, or proved + truth by itself. +- Keep the two axes apart: binding = *when* re-checked; strength = *whether* + falsifiable. A warning is low confidence, **not** a false claim. +- These are **not** "Agent Receipts" (that protocol audits agent *actions*); see + [reference/safety-boundary.md](reference/safety-boundary.md). diff --git a/src/dorian/templates/claude_code/dorian-claim-warrants/examples/bad-claims.md b/src/dorian/templates/claude_code/dorian-claim-warrants/examples/bad-claims.md new file mode 100644 index 0000000..21ccb64 --- /dev/null +++ b/src/dorian/templates/claude_code/dorian-claim-warrants/examples/bad-claims.md @@ -0,0 +1,23 @@ +# Bad claims — do NOT warrant these + +Dorian warrants *specific, checkable, falsifiable* facts. These do not qualify; +leave them as prose in the change note. A claim earns a warrant only when a +deterministic checker can prove it false later. + +| Don't warrant | Why | What to do instead | +|---|---|---| +| "The code is cleaner / more maintainable." | Opinion. No checker can falsify it. | Leave as prose. | +| "This is ~2x faster." | Performance claim with no falsifying check in the claim. | Leave as prose, or back a *specific* asserted behavior with a `C4 pytest:` test you trust. | +| "More secure now." | No deterministic check; security is not a `symbol:` exists. | Leave as prose; rely on SAST/review. | +| "Refactored the auth module." | Vague; nothing to falsify. | Warrant the *specific* surviving facts: a `py-signature:` or `py-const:`. | +| "Updated the docs." | No anchor. | If a doc must contain a specific string, anchor it: `string:`/`regex:` on that file. | +| "All callers were updated." | Quantity over an open set; existence can't prove "all". | Only warrant if a deterministic check covers it (often it can't) — otherwise prose. | +| A **behavior** claim backed only by `symbol:` (exists). | Existence ≠ behavior. `--strength-gate=fail` refuses it. | Use `C4 pytest:` for behavior, or downgrade the claim's `kind` to `reference`. | +| A **quantity** claim ("value is N") backed only by `path:`/`symbol:`. | Existence can't pin a value. | Use `py-const:` / `config-value:` / anchored `regex:`. | +| `string:` on a short (<6 char) or reformat-fragile literal. | Brittle; whitespace/format churn breaks it spuriously. | Anchor both key and value with `regex:`, or use `config-value:`/`py-const:`. | +| `pytest:` pointing at a test that does not exist yet, or one you haven't run. | The checker ERRORs (fail-closed); never fabricate a node. | Only cite a real, safe, known-passing test node. | + +The honest move is to **exclude** the un-checkable statements explicitly in the +change note ("Non-checkable claims intentionally excluded"). Dorian verifies only +what someone wrote down; it cannot catch a lie of omission, and it never claims to +verify the whole summary. diff --git a/src/dorian/templates/claude_code/dorian-claim-warrants/examples/final-message-example.md b/src/dorian/templates/claude_code/dorian-claim-warrants/examples/final-message-example.md new file mode 100644 index 0000000..a56c98a --- /dev/null +++ b/src/dorian/templates/claude_code/dorian-claim-warrants/examples/final-message-example.md @@ -0,0 +1,34 @@ +# Example final assistant message → claim warrants + +This shows how the checkable subset of a normal end-of-turn summary becomes claim +warrants. The skill reads a message like this (plus the real git diff) and drafts +`docs/changes/.md` + `docs/changes/.claims.json`. + +## The agent's summary (what it actually said) + +> I added `verify_token(token, algo="RS256") -> bool` to `src/auth.py`, set +> `LOGIN_TIMEOUT = 30`, bumped `requires-python` to `>=3.11` in `pyproject.toml`, +> and cleaned up the imports. This is a big improvement and should be faster. + +## What the skill keeps vs drops + +| Statement | Verdict | Checker | +|---|---|---| +| `verify_token` signature/default is `(token, algo="RS256") -> bool` | **warrant** | `py-signature:src/auth.py::verify_token::token, algo="RS256" -> bool` | +| `LOGIN_TIMEOUT = 30` | **warrant** | `py-const:src/auth.py::LOGIN_TIMEOUT::30` | +| `requires-python` is `>=3.11` | **warrant** | `config-value:pyproject.toml:project.requires-python:">=3.11"` | +| "cleaned up the imports" | **drop** (prose) | — | +| "big improvement", "should be faster" | **drop** (opinion/perf) | — | + +The three kept facts go into `.claims.json` (see `good-claims.json`); the two +dropped ones are listed under "Non-checkable claims intentionally excluded" in the +change note. Then: + +```bash +dorian verify docs/changes/.md \ + --claims docs/changes/.claims.json \ + --strength-gate=fail --binding-gate=warn +``` + +Only if that exits 0 are the claims sealed into a `.warrant`. Until then they are a +**DRAFT — not verified**. diff --git a/src/dorian/templates/claude_code/dorian-claim-warrants/examples/good-claims.json b/src/dorian/templates/claude_code/dorian-claim-warrants/examples/good-claims.json new file mode 100644 index 0000000..31259be --- /dev/null +++ b/src/dorian/templates/claude_code/dorian-claim-warrants/examples/good-claims.json @@ -0,0 +1,49 @@ +{ + "claims": [ + { + "id": "requires-python-3-11", + "text": "pyproject.toml declares requires-python >=3.11.", + "kind": "quantity", + "load_bearing": true, + "checkers": [ + { "type": "C3", "program": "config-value:pyproject.toml:project.requires-python:\">=3.11\"" } + ] + }, + { + "id": "verify-token-signature", + "text": "verify_token(token, algo=\"RS256\") -> bool keeps its signature and default.", + "kind": "reference", + "load_bearing": true, + "checkers": [ + { "type": "C3", "program": "py-signature:src/auth.py::verify_token::token, algo=\"RS256\" -> bool" } + ] + }, + { + "id": "login-timeout-constant", + "text": "The LOGIN_TIMEOUT constant is 30.", + "kind": "quantity", + "load_bearing": true, + "checkers": [ + { "type": "C3", "program": "py-const:src/auth.py::LOGIN_TIMEOUT::30" } + ] + }, + { + "id": "login-handler-exists", + "text": "app.py defines a login_handler symbol (existence/reference only).", + "kind": "reference", + "load_bearing": false, + "checkers": [ + { "type": "C3", "program": "symbol:src/app.py::login_handler" } + ] + }, + { + "id": "rs256-roundtrip-behaves", + "text": "RS256 tokens verify end to end (backed by a real test node — this RUNS pytest).", + "kind": "behavior", + "load_bearing": true, + "checkers": [ + { "type": "C4", "program": "pytest:tests/test_auth.py::test_rs256_roundtrip" } + ] + } + ] +} diff --git a/src/dorian/templates/claude_code/dorian-claim-warrants/reference/checker-selection.md b/src/dorian/templates/claude_code/dorian-claim-warrants/reference/checker-selection.md new file mode 100644 index 0000000..0a33b98 --- /dev/null +++ b/src/dorian/templates/claude_code/dorian-claim-warrants/reference/checker-selection.md @@ -0,0 +1,56 @@ +# Checker selection — match the checker to the claim + +Two axes, never collapsed (see Dorian's `docs/VALIDATION_HONESTY.md`): + +- **Trigger / binding** — *when* a claim is re-checked (which watched files fire it). +- **Truth / strength** — *whether* the checker can actually **falsify** the claim. + +A green seal says every backed claim held *at seal time*. It does **not** say the +checker is strong enough to catch a future drift. Pick the strongest checker the +claim's kind needs. `--strength-gate=fail` refuses to seal a load-bearing claim +whose checker is too weak to falsify its kind. + +## Claim → checker map + +| The summary says… | `kind` | checker `program` | strength | +|---|---|---|---| +| package/config value (`requires-python`, a default, a version) | `quantity` / `fact` | `config-value:pyproject.toml:project.requires-python:">=3.11"` | structural | +| a Python signature/defaults | `reference` | `py-signature:m.py::f::a, b=1 -> int` | structural | +| a Python constant's value | `quantity` | `py-const:m.py::TIMEOUT::30` | structural | +| "symbol `X` exists / is referenced" | `reference` | `symbol:m.py::X` | existence | +| "path `p` exists" | `reference` | `path:pkg/p.py` | existence | +| a specific string/route is present | `reference` | `string:r.py::/admin` or anchored `regex:` | raw_text | +| "value is N" anchored in code | `quantity` | `regex:m.py::TIMEOUT\s*=\s*30\b` (anchor BOTH key+value) | raw_text | +| a real behavior holds (safe known test) | `behavior` | `pytest:tests/test_x.py::T` — **this RUNS the test** | behavioral | +| data shape / rowcount / freshness | `quantity` / `fact` | typed `C5` (`schema:`/`rowcount:`/`domain:`/`nullrate:`/`freshness:`/`snapshot:`) | data | + +## Truth-strength order (weakest → strongest) + +`unbacked` < `shell_executable` (opaque) < `existence` (`path:`/`symbol:`) < +`raw_text` (`string:`/`regex:`) < `semantic_text` (`code:`) < `snapshot` +(`C1`/`C5 snapshot:`) < `data` (typed `C5`) < `structural` (`py-signature:`, +`py-const:`, `config-value:`) < `behavioral` (`C4 pytest:`). + +## The two adequacy rules `--strength-gate=fail` enforces + +1. A **behavior** claim needs a `C4 pytest:` checker. Backed only by existence/text + (e.g. `symbol:`), it is an `adequacy_mismatch` and the gate refuses it — *exists* + is not *behaves*. +2. A **quantity** claim needs a value-pinning checker (`py-const:` / `config-value:` + / anchored `regex:` / typed `C5`). Backed only by `path:`/`symbol:` (existence) + or an opaque `shell:`, it cannot prove the value, so the gate refuses it. + +For the no-test facts this skill targets — config, signatures, constants, +references — prefer `config-value:`, `py-signature:`, `py-const:` (all structural). +Use `symbol:`/`path:` for genuine existence/reference claims, and reach for +`pytest:` only when a real, safe, known-passing test node exists. + +## Picking `load_bearing` + +- `true` → breaking it should **block** a merge (the warrant folds to `REVOKED`, + exit 4). Use for the facts the change actually depends on. +- `false` → a soft signal that only `DEGRADE`s (exit 3). Non-load-bearing claims + never score high-risk; they are the author's discretionary notes. + +Weak binding or weak strength means **low confidence / coverage, not a false +claim** — strengthen the watch or the checker; never read a warning as falsity. diff --git a/src/dorian/templates/claude_code/dorian-claim-warrants/reference/safety-boundary.md b/src/dorian/templates/claude_code/dorian-claim-warrants/reference/safety-boundary.md new file mode 100644 index 0000000..b1a4a8d --- /dev/null +++ b/src/dorian/templates/claude_code/dorian-claim-warrants/reference/safety-boundary.md @@ -0,0 +1,52 @@ +# Safety & trust boundary + +## The model drafts. Dorian proves. + +This skill (and the optional Stop hook) only **draft** a change note and a +`claims.json`. That is *all* the model decides. The proof step is `dorian verify`, +which runs each claim's **deterministic** checker against the real source — AST +parsing, regex, file/symbol lookup, an actual `pytest` run — with **no LLM, no +model, no judgment call** anywhere in the loop. A warrant is *born verifiable*: if a +load-bearing checker FAILs or ERRORs, the seal is **refused** and nothing is written +(exit 4). **The model cannot make a false claim seal.** + +Never tell the user a claim is "verified" unless `dorian verify` actually exited 0 +and wrote a `.warrant`. Until then it is a **DRAFT — not verified**. + +## Dorian is not a sandbox + +`dorian verify` and `dorian revalidate` execute checker programs. `C3` and typed +`C5` only inspect files, but **`C4 pytest:` and `C5 shell:` execute code** (pytest +collection imports the target module and any `conftest.py`). Treat an +agent-emitted `claims.json` exactly as you treat agent-emitted code: **review it +before you run `verify`, and only ever run it in a trusted repo.** Dorian is built +for trusted, internal repositories — **not** public CI taking forked pull requests. +In a semi-trusted context add `--deny-exec` (env `DORIAN_DENY_EXEC=1`) so `C4`/`C5 +shell:` ERROR instead of running — fail-closed, but **still not a sandbox**. + +## What this is NOT + +- Not an LLM judge — no model runs at check time, ever. +- Not a semantic verifier of the whole summary — it checks only the claims someone + wrote, and cannot catch a lie of omission. +- Not a replacement for tests, SAST, CI, code review, or human judgment. +- Not a generic PR reviewer, dashboard, or SaaS. + +## Not "Agent Receipts" + +Dorian claim warrants are **not** the Agent Receipts / Obsigna protocol. Agent +Receipts records a cryptographically signed, hash-chained audit trail of *what an +agent did* (its tool calls) — "what did it do, on whose authority, with what +inputs". Dorian answers a different question: *is this specific engineering claim +true now, and will it revoke when later code makes it false?* The word "receipt" +here is an explanatory metaphor, not that protocol. The two are complementary, not +substitutes — see Dorian's `docs/CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md`. + +## The optional Stop hook + +`.claude/hooks/dorian_claim_warrants_stop.py` is **reminder-only and opt-in**. It is +not enabled by scaffolding; it runs only after you register it under `hooks.Stop` in +your project `settings.json`. It returns a soft `additionalContext` nudge, never a +block, so it cannot loop. It never runs `dorian verify`, never runs tests, never +writes files, and never executes project code — its only side effect is a read-only +`git status` to see whether anything changed. diff --git a/src/dorian/templates/claude_code/dorian-claim-warrants/templates/change-note.md b/src/dorian/templates/claude_code/dorian-claim-warrants/templates/change-note.md new file mode 100644 index 0000000..3739fa6 --- /dev/null +++ b/src/dorian/templates/claude_code/dorian-claim-warrants/templates/change-note.md @@ -0,0 +1,41 @@ +# Change note — + +> Drafted by the `/dorian-claim-warrants` skill. **DRAFT — not verified until +> `dorian verify` exits 0 and writes a `.warrant`.** The model drafts; Dorian +> proves. No model runs at check time. + +## Summary + + + +## Checkable claims included + +These are the statements a deterministic Dorian checker can falsify. Each maps to +a claim in `.claims.json`: + +- ``: — `` + +## Non-checkable claims intentionally excluded + +Statements from the summary that no deterministic checker can falsify, so they are +**left as prose, not warranted** (this honesty is the point): + +- "" — opinion, not checkable +- "" — excluded + +## Verification command + +```bash +dorian verify docs/changes/.md \ + --claims docs/changes/.claims.json \ + --strength-gate=fail --binding-gate=warn +``` + +## Trust boundary + +The `/dorian-claim-warrants` skill only **drafts** these files. Sealing requires +`dorian verify` to run each claim's deterministic checker and pass — a false claim +is refused, not sealed. Dorian is **not a sandbox**: `C4 pytest:` and `C5 shell:` +checkers execute code, so this runs in **trusted repos** only. Later, +`dorian revalidate --since ` flips a broken claim to `REVOKED` (exit 4). diff --git a/src/dorian/templates/claude_code/dorian-claim-warrants/templates/claims.json b/src/dorian/templates/claude_code/dorian-claim-warrants/templates/claims.json new file mode 100644 index 0000000..0183d5f --- /dev/null +++ b/src/dorian/templates/claude_code/dorian-claim-warrants/templates/claims.json @@ -0,0 +1,13 @@ +{ + "claims": [ + { + "id": "example-config-value", + "text": "Replace with the specific, checkable fact your change asserts.", + "kind": "quantity", + "load_bearing": true, + "checkers": [ + { "type": "C3", "program": "config-value:pyproject.toml:project.version:\"1.2.3\"" } + ] + } + ] +} diff --git a/src/dorian/templates/claude_code/hooks/dorian_claim_warrants_stop.py b/src/dorian/templates/claude_code/hooks/dorian_claim_warrants_stop.py new file mode 100644 index 0000000..156140d --- /dev/null +++ b/src/dorian/templates/claude_code/hooks/dorian_claim_warrants_stop.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +"""Dorian claim-warrants Stop hook — reminder-only, opt-in. + +When a Claude Code turn ends (the ``Stop`` event), this hook may inject a short +reminder asking Claude to draft Dorian *claim warrants* for the checkable facts +in its summary — config values, signatures/defaults, constants, references — via +the ``/dorian-claim-warrants`` skill. + +It is **reminder-only**. It returns ``hookSpecificOutput.additionalContext`` (a +soft nudge Claude reads on its next request); it NEVER returns ``decision: block``, +so it cannot loop or trap the agent. It also: + +- never runs ``dorian verify``, tests, or any project code; +- never writes files; +- only ever shells out to read-only ``git status`` to see whether anything + changed (a VCS query, not project execution); +- fails open: any error → stay silent and exit 0 (it must never break a turn). + +Skip it for a turn by setting any of these env vars (or putting the marker in the +final message): ``DORIAN_CLAIM_WARRANTS_SKIP``, ``NO_DORIAN_CLAIM_WARRANTS``, +``DORIAN_RECEIPTS_SKIP``, ``NO_DORIAN_RECEIPTS``. + +Install: copy to ``.claude/hooks/`` and register under ``hooks.Stop`` in your +project ``.claude/settings.json`` (see the review-first settings example). The +hook is OFF until you add that entry — scaffolding the file does not enable it. + +Stdlib only. Tested by ``tests/test_claim_warrants_hook.py``. +""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys + +# Env vars (or final-message markers) that suppress the reminder for one turn. +SKIP_MARKERS = ( + "DORIAN_CLAIM_WARRANTS_SKIP", + "NO_DORIAN_CLAIM_WARRANTS", + "DORIAN_RECEIPTS_SKIP", + "NO_DORIAN_RECEIPTS", +) + +# A change is "relevant" (worth a claim-warrant reminder) when a code/config file +# moved — the families Dorian's checkers can falsify. Docs-only churn is excluded +# to keep the nudge quiet; the user can still invoke the skill by hand. +RELEVANT_SUFFIXES = ( + ".py", + ".pyi", + ".toml", + ".cfg", + ".ini", + ".json", + ".yaml", + ".yml", + ".txt", + ".lock", +) + +# Final-message phrasings that mean "nothing to warrant" — suppress the nudge even +# when files moved (e.g. a formatting-only or investigation turn). +NO_CHANGE_PHRASES = ( + "no code change", + "no code changes", + "no changes were made", + "did not change any", + "research only", + "investigation only", + "read-only", +) + +REMINDER = ( + "Before ending: consider creating Dorian claim warrants for the checkable " + "facts in your summary. Invoke /dorian-claim-warrants. Draft only checkable " + "claims (config values, signatures/defaults, constants, file/symbol " + "references); leave vague prose ('cleaner', 'faster', 'refactored') out. " + "Dorian verify is the deterministic proof step — the skill only drafts; run " + "it with --strength-gate=fail --binding-gate=warn. A claim is not verified " + "until dorian verify exits 0 and writes a .warrant." +) + + +def load_input(stdin_text: str) -> dict: + """Parse the hook stdin JSON. Invalid/empty input → ``{}`` (caller no-ops).""" + try: + data = json.loads(stdin_text) + except (ValueError, TypeError): + return {} + return data if isinstance(data, dict) else {} + + +def skip_markers_present(env: dict, message: str) -> bool: + """True if a skip marker is set as an env var or appears in the final message.""" + for marker in SKIP_MARKERS: + if env.get(marker): + return True + if marker in message: + return True + return False + + +def changed_paths(cwd: str) -> list[str] | None: + """Repo-relative paths with uncommitted changes, via read-only ``git status``. + + Returns ``None`` when ``cwd`` is not a git work tree or git is unavailable — + the caller treats that as "no signal, stay silent". Never raises. + """ + try: + proc = subprocess.run( + ["git", "-C", cwd or ".", "status", "--porcelain", "--untracked-files=all"], + capture_output=True, + text=True, + timeout=10, + check=False, + ) + except (OSError, subprocess.SubprocessError): + return None + if proc.returncode != 0: + return None # not a git repo (status exits non-zero) → no signal + paths: list[str] = [] + for line in proc.stdout.splitlines(): + # porcelain v1: 'XY ' or 'XY -> '; take the final path. + entry = line[3:] if len(line) > 3 else "" + if " -> " in entry: + entry = entry.split(" -> ", 1)[1] + entry = entry.strip().strip('"') + if entry: + paths.append(entry) + return paths + + +def has_relevant_change(paths: list[str]) -> bool: + return any(p.lower().endswith(RELEVANT_SUFFIXES) for p in paths) + + +def already_has_warrant_artifacts(paths: list[str]) -> bool: + """True if the diff already contains claim-warrant artifacts (the work is done).""" + for p in paths: + low = p.lower() + if low.endswith(".warrant"): + return True + if "docs/changes/" in low and low.endswith(".claims.json"): + return True + return False + + +def last_assistant_message(payload: dict) -> str: + """Best-effort final assistant text. + + Prefers a ``last_assistant_message`` field if the runtime supplies one (e.g. + SubagentStop); otherwise parses the JSONL at ``transcript_path`` from the end + and returns the most recent assistant text block. Any error → ``""``. + """ + direct = payload.get("last_assistant_message") + if isinstance(direct, str) and direct: + return direct + path = payload.get("transcript_path") + if not isinstance(path, str) or not path: + return "" + try: + with open(path, encoding="utf-8", errors="replace") as fh: + lines = fh.readlines() + except OSError: + return "" + for line in reversed(lines): + line = line.strip() + if not line: + continue + try: + obj = json.loads(line) + except ValueError: + continue + text = _assistant_text(obj) + if text: + return text + return "" + + +def _assistant_text(obj: dict) -> str: + """Extract assistant text from one transcript record, or ``""`` if it is not + an assistant text turn. Tolerant of shape drift across transcript versions.""" + if not isinstance(obj, dict): + return "" + msg = obj.get("message", obj) + role = obj.get("role") or (msg.get("role") if isinstance(msg, dict) else None) + kind = obj.get("type") + if role != "assistant" and kind != "assistant": + return "" + content = msg.get("content") if isinstance(msg, dict) else None + if isinstance(content, str): + return content + if isinstance(content, list): + parts = [ + b["text"] + for b in content + if isinstance(b, dict) and b.get("type") == "text" and isinstance(b.get("text"), str) + ] + return "\n".join(parts).strip() + return "" + + +def message_says_no_change(message: str) -> bool: + low = message.lower() + return any(phrase in low for phrase in NO_CHANGE_PHRASES) + + +def should_remind(payload: dict, env: dict) -> bool: + """The full gate. True only when a reminder is warranted; never raises.""" + if payload.get("hook_event_name") != "Stop": + return False + if payload.get("stop_hook_active"): + return False # already continuing from a stop hook → never re-fire + if skip_markers_present(env, ""): + return False + paths = changed_paths(str(payload.get("cwd") or env.get("CLAUDE_PROJECT_DIR") or ".")) + if paths is None: # not a git repo / git unavailable + return False + if not has_relevant_change(paths): + return False + if already_has_warrant_artifacts(paths): + return False + message = last_assistant_message(payload) + if skip_markers_present(env, message): + return False + # remind unless the final message explicitly says nothing was changed + return not (message and message_says_no_change(message)) + + +def reminder_output() -> dict: + return { + "hookSpecificOutput": { + "hookEventName": "Stop", + "additionalContext": REMINDER, + } + } + + +def main() -> int: + try: + payload = load_input(sys.stdin.read()) + if should_remind(payload, dict(os.environ)): + sys.stdout.write(json.dumps(reminder_output())) + except Exception: # a hook must never break the turn; fail open and stay silent. + return 0 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/dorian/templates/claude_code/settings/settings.dorian-claim-warrants.review-first.example.json b/src/dorian/templates/claude_code/settings/settings.dorian-claim-warrants.review-first.example.json new file mode 100644 index 0000000..6621876 --- /dev/null +++ b/src/dorian/templates/claude_code/settings/settings.dorian-claim-warrants.review-first.example.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(dorian bindings:*)", + "Bash(dorian bind-suggest:*)", + "Bash(dorian status:*)" + ] + } +} diff --git a/src/dorian/templates/claude_code/settings/settings.dorian-claim-warrants.trusted-local.example.json b/src/dorian/templates/claude_code/settings/settings.dorian-claim-warrants.trusted-local.example.json new file mode 100644 index 0000000..392813b --- /dev/null +++ b/src/dorian/templates/claude_code/settings/settings.dorian-claim-warrants.trusted-local.example.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(dorian verify:*)", + "Bash(dorian revalidate:*)", + "Bash(dorian bindings:*)", + "Bash(dorian bind-suggest:*)", + "Bash(dorian status:*)" + ] + } +} From 2e6265fda069f58c19302f56e3dd73ac93dbfb5f Mon Sep 17 00:00:00 2001 From: Ajay Surya Date: Sun, 28 Jun 2026 00:53:34 +0530 Subject: [PATCH 2/5] test: cover claim-warrants scaffolder, hook, and wheel packaging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test_claude_code_scaffold.py: manifest/package-data integrity, dry-run writes nothing, default vs --with-hook vs --settings-only, idempotency, --force overwrites only scaffolded files, subdirectory --target, non-git warning, JSON output, skill frontmatter + required safety phrases, valid example/skeleton JSON, no-go branding absent, and an end-to-end scaffold -> verify (--strength-gate=fail) -> drift -> revalidate REVOKED (exit 4). - test_claim_warrants_hook.py: loads the shipped hook template and covers the pure helpers, the should_remind gate (non-Stop, stop_hook_active, skip markers, no-git, no-relevant-change, existing artifacts, no-change message), transcript parsing, real-git changed_paths, and the subprocess entry point (emits additionalContext + writes nothing; silent on non-Stop / invalid JSON / env skip markers). - test_packaging.py: a slow test that builds the wheel, installs it into a clean venv, and scaffolds the skill from package data — proving the templates ship and resolve. Co-Authored-By: Claude Opus 4.8 --- tests/test_claim_warrants_hook.py | 295 ++++++++++++++++++++++++++ tests/test_claude_code_scaffold.py | 325 +++++++++++++++++++++++++++++ tests/test_packaging.py | 27 +++ 3 files changed, 647 insertions(+) create mode 100644 tests/test_claim_warrants_hook.py create mode 100644 tests/test_claude_code_scaffold.py diff --git a/tests/test_claim_warrants_hook.py b/tests/test_claim_warrants_hook.py new file mode 100644 index 0000000..b87d65f --- /dev/null +++ b/tests/test_claim_warrants_hook.py @@ -0,0 +1,295 @@ +"""The opt-in, reminder-only Dorian claim-warrants Stop hook. + +The hook must be safe above all: it never blocks, never writes files, never runs +``dorian``/tests/project code (its only side effect is a read-only ``git status``), +and fails open on any error. It emits a soft ``additionalContext`` reminder only +when a turn left relevant, un-warranted changes. These tests load the ACTUAL +shipped template so a regression in the template fails CI. +""" + +from __future__ import annotations + +import importlib.util +import json +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +HOOK_SRC = ( + Path(__file__).resolve().parent.parent + / "src/dorian/templates/claude_code/hooks/dorian_claim_warrants_stop.py" +) + + +def _load_hook(): + spec = importlib.util.spec_from_file_location("dorian_claim_warrants_stop", HOOK_SRC) + mod = importlib.util.module_from_spec(spec) + assert spec and spec.loader + spec.loader.exec_module(mod) + return mod + + +hook = _load_hook() + +_GIT_ENV = { + "GIT_AUTHOR_NAME": "t", + "GIT_AUTHOR_EMAIL": "t@t", + "GIT_COMMITTER_NAME": "t", + "GIT_COMMITTER_EMAIL": "t@t", +} + + +def _git(repo: Path, *args: str) -> None: + subprocess.run( + ["git", *args], cwd=repo, env={**os.environ, **_GIT_ENV}, check=True, capture_output=True + ) + + +# --- pure helpers ------------------------------------------------------------- + + +def test_load_input_tolerates_garbage() -> None: + assert hook.load_input("") == {} + assert hook.load_input("not json") == {} + assert hook.load_input("[1,2,3]") == {} # not a dict + assert hook.load_input('{"a": 1}') == {"a": 1} + + +def test_skip_markers_env_and_message() -> None: + assert hook.skip_markers_present({"DORIAN_CLAIM_WARRANTS_SKIP": "1"}, "") + assert hook.skip_markers_present({"NO_DORIAN_RECEIPTS": "1"}, "") + assert hook.skip_markers_present({}, "please NO_DORIAN_CLAIM_WARRANTS here") + assert not hook.skip_markers_present({}, "normal message") + + +def test_relevant_change_detection() -> None: + assert hook.has_relevant_change(["src/app.py"]) + assert hook.has_relevant_change(["pyproject.toml"]) + assert not hook.has_relevant_change(["README.md", "docs/guide.md"]) + assert not hook.has_relevant_change([]) + + +def test_already_has_warrant_artifacts() -> None: + assert hook.already_has_warrant_artifacts(["docs/changes/x.claims.json"]) + assert hook.already_has_warrant_artifacts(["docs/changes/x.md.warrant"]) + assert not hook.already_has_warrant_artifacts(["src/app.py"]) + + +def test_message_says_no_change() -> None: + assert hook.message_says_no_change("This was research only.") + assert hook.message_says_no_change("No code changes were needed.") + assert not hook.message_says_no_change("I added a function and a constant.") + + +def test_reminder_output_shape() -> None: + out = hook.reminder_output() + assert out["hookSpecificOutput"]["hookEventName"] == "Stop" + ctx = out["hookSpecificOutput"]["additionalContext"] + assert "/dorian-claim-warrants" in ctx + assert "strength-gate=fail" in ctx + # reminder-only: never a block/decision field + assert "decision" not in out + + +def test_last_assistant_message_prefers_direct_field() -> None: + assert hook.last_assistant_message({"last_assistant_message": "hi"}) == "hi" + + +def test_last_assistant_message_parses_transcript(tmp_path: Path) -> None: + t = tmp_path / "t.jsonl" + t.write_text( + "\n".join( + [ + json.dumps({"type": "user", "message": {"role": "user", "content": "go"}}), + json.dumps( + { + "type": "assistant", + "message": { + "role": "assistant", + "content": [{"type": "text", "text": "first"}], + }, + } + ), + json.dumps( + { + "type": "assistant", + "message": { + "role": "assistant", + "content": [{"type": "text", "text": "LAST one"}], + }, + } + ), + ] + ), + encoding="utf-8", + ) + assert hook.last_assistant_message({"transcript_path": str(t)}) == "LAST one" + + +def test_last_assistant_message_missing_transcript_is_empty() -> None: + assert hook.last_assistant_message({"transcript_path": "/no/such/file.jsonl"}) == "" + assert hook.last_assistant_message({}) == "" + + +# --- should_remind gating (changed_paths/message monkeypatched) --------------- + + +def _payload(**kw): + base = {"hook_event_name": "Stop", "stop_hook_active": False, "cwd": "/x"} + base.update(kw) + return base + + +def test_no_op_on_non_stop_event(monkeypatch) -> None: + monkeypatch.setattr(hook, "changed_paths", lambda cwd: ["src/app.py"]) + monkeypatch.setattr(hook, "last_assistant_message", lambda p: "changed stuff") + assert hook.should_remind(_payload(hook_event_name="PreToolUse"), {}) is False + + +def test_no_op_when_stop_hook_active(monkeypatch) -> None: + monkeypatch.setattr(hook, "changed_paths", lambda cwd: ["src/app.py"]) + assert hook.should_remind(_payload(stop_hook_active=True), {}) is False + + +def test_no_op_on_env_skip_marker(monkeypatch) -> None: + monkeypatch.setattr(hook, "changed_paths", lambda cwd: ["src/app.py"]) + assert hook.should_remind(_payload(), {"NO_DORIAN_CLAIM_WARRANTS": "1"}) is False + + +def test_no_op_outside_git_repo(monkeypatch) -> None: + monkeypatch.setattr(hook, "changed_paths", lambda cwd: None) # not a git repo + assert hook.should_remind(_payload(), {}) is False + + +def test_no_op_when_no_relevant_change(monkeypatch) -> None: + monkeypatch.setattr(hook, "changed_paths", lambda cwd: ["README.md"]) + assert hook.should_remind(_payload(), {}) is False + + +def test_no_op_when_warrant_artifacts_already_present(monkeypatch) -> None: + monkeypatch.setattr( + hook, "changed_paths", lambda cwd: ["src/app.py", "docs/changes/x.claims.json"] + ) + assert hook.should_remind(_payload(), {}) is False + + +def test_no_op_when_message_says_no_change(monkeypatch) -> None: + monkeypatch.setattr(hook, "changed_paths", lambda cwd: ["src/app.py"]) + monkeypatch.setattr(hook, "last_assistant_message", lambda p: "This was research only.") + assert hook.should_remind(_payload(), {}) is False + + +def test_reminds_on_relevant_change(monkeypatch) -> None: + monkeypatch.setattr(hook, "changed_paths", lambda cwd: ["src/app.py"]) + monkeypatch.setattr(hook, "last_assistant_message", lambda p: "added a function and a constant") + assert hook.should_remind(_payload(), {}) is True + + +# --- changed_paths against a real git repo ------------------------------------ + + +def test_changed_paths_reads_real_git(tmp_path: Path) -> None: + repo = tmp_path / "repo" + repo.mkdir() + _git(repo, "init", "-q", "-b", "main") + (repo / "app.py").write_text("x = 1\n", encoding="utf-8") + paths = hook.changed_paths(str(repo)) + assert paths is not None and "app.py" in paths + + +def test_changed_paths_none_outside_git(tmp_path: Path) -> None: + plain = tmp_path / "plain" + plain.mkdir() + assert hook.changed_paths(str(plain)) is None + + +# --- the real entry point via subprocess -------------------------------------- + + +def _run_hook(payload: dict, cwd: Path) -> subprocess.CompletedProcess: + return subprocess.run( + [sys.executable, str(HOOK_SRC)], + input=json.dumps(payload), + capture_output=True, + text=True, + cwd=cwd, + env={k: v for k, v in os.environ.items() if not k.startswith("DORIAN_")}, + timeout=30, + ) + + +def test_entrypoint_emits_reminder_and_writes_nothing(tmp_path: Path) -> None: + repo = tmp_path / "repo" + repo.mkdir() + _git(repo, "init", "-q", "-b", "main") + (repo / "app.py").write_text("def f():\n return 1\n", encoding="utf-8") + transcript = tmp_path / "t.jsonl" + transcript.write_text( + json.dumps( + {"type": "assistant", "message": {"role": "assistant", "content": "I added f()."}} + ) + + "\n", + encoding="utf-8", + ) + before = {p.name for p in repo.rglob("*")} + r = _run_hook( + { + "hook_event_name": "Stop", + "stop_hook_active": False, + "cwd": str(repo), + "transcript_path": str(transcript), + }, + cwd=repo, + ) + assert r.returncode == 0, r.stderr + out = json.loads(r.stdout) + assert out["hookSpecificOutput"]["additionalContext"] + # the hook wrote nothing into the repo + assert {p.name for p in repo.rglob("*")} == before + + +def test_entrypoint_silent_on_non_stop(tmp_path: Path) -> None: + repo = tmp_path / "repo" + repo.mkdir() + _git(repo, "init", "-q", "-b", "main") + (repo / "app.py").write_text("x=1\n", encoding="utf-8") + r = _run_hook({"hook_event_name": "PreToolUse", "cwd": str(repo)}, cwd=repo) + assert r.returncode == 0 + assert r.stdout.strip() == "" + + +def test_entrypoint_silent_on_invalid_json(tmp_path: Path) -> None: + repo = tmp_path / "repo" + repo.mkdir() + r = subprocess.run( + [sys.executable, str(HOOK_SRC)], + input="this is not json", + capture_output=True, + text=True, + cwd=repo, + timeout=30, + ) + assert r.returncode == 0 + assert r.stdout.strip() == "" + + +@pytest.mark.parametrize("marker", hook.SKIP_MARKERS) +def test_entrypoint_silent_on_env_skip(tmp_path: Path, marker: str) -> None: + repo = tmp_path / "repo" + repo.mkdir() + _git(repo, "init", "-q", "-b", "main") + (repo / "app.py").write_text("x=1\n", encoding="utf-8") + r = subprocess.run( + [sys.executable, str(HOOK_SRC)], + input=json.dumps({"hook_event_name": "Stop", "stop_hook_active": False, "cwd": str(repo)}), + capture_output=True, + text=True, + cwd=repo, + env={**os.environ, marker: "1"}, + timeout=30, + ) + assert r.returncode == 0 + assert r.stdout.strip() == "" diff --git a/tests/test_claude_code_scaffold.py b/tests/test_claude_code_scaffold.py new file mode 100644 index 0000000..f510e3c --- /dev/null +++ b/tests/test_claude_code_scaffold.py @@ -0,0 +1,325 @@ +"""`dorian claude-code install-claim-warrants` — scaffolder, skill, and templates. + +The scaffolder writes files only: it never runs a checker, executes code, writes +outside the target repo, or overwrites without ``--force`` (re-running is +idempotent). The load-bearing end-to-end test seals a real claim against a real +change through the scaffolded workflow and then REVOKEs it on drift — the same +born-verifiable contract ``dorian init`` is held to. +""" + +from __future__ import annotations + +import json +import os +import subprocess +from pathlib import Path + +from dorian import claims_io, claude_code +from dorian.cli import main + +SKILL = ".claude/skills/dorian-claim-warrants" +HOOK = ".claude/hooks/dorian_claim_warrants_stop.py" + +# Phrases the feature must NEVER use — it is "claim warrants", not "Agent Receipts" +# (a distinct action-audit protocol). Mentioning Agent Receipts in a *contrast* is +# fine; branding the feature as it is not. +AGENT_RECEIPTS_NO_GO = ( + "Agent Receipts for code", + "cryptographic agent receipts", + "records what the agent did", + "auditable tool-call chain", + "proves all agent actions", +) + +_GIT_ENV = { + "GIT_AUTHOR_NAME": "t", + "GIT_AUTHOR_EMAIL": "t@t", + "GIT_COMMITTER_NAME": "t", + "GIT_COMMITTER_EMAIL": "t@t", +} + + +def _git(repo: Path, *args: str) -> None: + subprocess.run( + ["git", *args], cwd=repo, env={**os.environ, **_GIT_ENV}, check=True, capture_output=True + ) + + +def _seed_repo(repo: Path) -> None: + repo.mkdir(parents=True, exist_ok=True) + _git(repo, "init", "-q", "-b", "main") + + +def _install(repo: Path, *flags: str) -> int: + return main(["--repo", str(repo), "claude-code", "install-claim-warrants", *flags]) + + +# --- packaging / manifest integrity ------------------------------------------ + + +def test_every_manifest_template_is_readable_package_data() -> None: + """Each manifest source resolves through importlib.resources (works from wheel).""" + for t in claude_code.MANIFEST: + content = claude_code._read_template(t.src) + assert content, f"empty/missing template: {t.src}" + + +def test_manifest_dests_are_repo_contained() -> None: + for t in claude_code.MANIFEST: + assert not t.dest.startswith("/") + assert ".." not in t.dest.split("/") + assert t.dest.startswith(".claude/") + + +# --- CLI surface -------------------------------------------------------------- + + +def test_command_is_wired(tmp_path: Path) -> None: + repo = tmp_path / "repo" + _seed_repo(repo) + assert _install(repo, "--dry-run") == 0 + + +def test_dry_run_writes_nothing(tmp_path: Path, capsys) -> None: + repo = tmp_path / "repo" + _seed_repo(repo) + assert _install(repo, "--dry-run") == 0 + assert not (repo / ".claude").exists() + out = capsys.readouterr().out + assert "Would create:" in out + assert f"{SKILL}/SKILL.md" in out + + +def test_default_install_writes_skill_and_settings_no_hook(tmp_path: Path) -> None: + repo = tmp_path / "repo" + _seed_repo(repo) + assert _install(repo) == 0 + assert (repo / SKILL / "SKILL.md").is_file() + assert (repo / SKILL / "README.md").is_file() + assert (repo / ".claude/settings.dorian-claim-warrants.review-first.example.json").is_file() + assert (repo / ".claude/settings.dorian-claim-warrants.trusted-local.example.json").is_file() + # hook is opt-in: NOT written by default + assert not (repo / HOOK).exists() + + +def test_with_hook_installs_hook(tmp_path: Path) -> None: + repo = tmp_path / "repo" + _seed_repo(repo) + assert _install(repo, "--with-hook") == 0 + assert (repo / HOOK).is_file() + assert "stop_hook_active" in (repo / HOOK).read_text() + + +def test_settings_only_writes_only_settings(tmp_path: Path) -> None: + repo = tmp_path / "repo" + _seed_repo(repo) + assert _install(repo, "--settings-only") == 0 + assert not (repo / SKILL / "SKILL.md").exists() + assert not (repo / HOOK).exists() + assert (repo / ".claude/settings.dorian-claim-warrants.review-first.example.json").is_file() + + +def test_with_hook_and_no_hook_conflict_is_usage_error(tmp_path: Path) -> None: + repo = tmp_path / "repo" + _seed_repo(repo) + assert _install(repo, "--with-hook", "--no-hook") == 2 + assert not (repo / ".claude").exists() + + +def test_idempotent_without_force(tmp_path: Path) -> None: + repo = tmp_path / "repo" + _seed_repo(repo) + assert _install(repo) == 0 + skill = repo / SKILL / "SKILL.md" + edited = "MY EDITS\n" + skill.write_text(edited, encoding="utf-8") + assert _install(repo) == 0 # second run does not clobber + assert skill.read_text(encoding="utf-8") == edited + + +def test_force_overwrites_only_scaffolded_files(tmp_path: Path) -> None: + repo = tmp_path / "repo" + _seed_repo(repo) + assert _install(repo) == 0 + skill = repo / SKILL / "SKILL.md" + skill.write_text("MY EDITS\n", encoding="utf-8") + # an unrelated .claude file must be left untouched by --force + other = repo / ".claude" / "settings.json" + other.write_text('{"mine": true}\n', encoding="utf-8") + assert _install(repo, "--force") == 0 + assert "dorian-claim-warrants" in skill.read_text(encoding="utf-8") # regenerated + assert other.read_text(encoding="utf-8") == '{"mine": true}\n' # untouched + + +def test_works_from_subdirectory(tmp_path: Path) -> None: + """--target a subdir: files land in the repo root .claude, not the subdir.""" + repo = tmp_path / "repo" + _seed_repo(repo) + sub = repo / "pkg" / "deep" + sub.mkdir(parents=True) + assert main(["claude-code", "install-claim-warrants", "--target", str(sub)]) == 0 + assert (repo / SKILL / "SKILL.md").is_file() + assert not (sub / ".claude").exists() + + +def test_json_output_is_machine_readable(tmp_path: Path, capsys) -> None: + repo = tmp_path / "repo" + _seed_repo(repo) + assert main(["--repo", str(repo), "--json", "claude-code", "install-claim-warrants"]) == 0 + payload = json.loads(capsys.readouterr().out) + assert f"{SKILL}/SKILL.md" in payload["created"] + assert payload["is_git"] is True + assert payload["hook_installed"] is False + assert payload["trust_boundary"] + + +def test_non_git_target_warns_but_installs(tmp_path: Path, capsys) -> None: + repo = tmp_path / "plain" # no `git init` + repo.mkdir() + assert _install(repo) == 0 + out = capsys.readouterr().out + assert "not a git repository" in out + assert (repo / SKILL / "SKILL.md").is_file() + + +def test_print_next_steps_writes_nothing(tmp_path: Path, capsys) -> None: + repo = tmp_path / "repo" + _seed_repo(repo) + assert _install(repo, "--print-next-steps") == 0 + assert not (repo / ".claude").exists() + assert "Trust boundary:" in capsys.readouterr().out + + +# --- skill / template content ------------------------------------------------- + + +def test_skill_frontmatter_is_parseable(tmp_path: Path) -> None: + repo = tmp_path / "repo" + _seed_repo(repo) + assert _install(repo) == 0 + text = (repo / SKILL / "SKILL.md").read_text(encoding="utf-8") + assert text.startswith("---\n") + fm = text.split("---\n", 2)[1] + assert "description:" in fm # the key that drives auto-load + # directory name (not frontmatter name) is the /command -> /dorian-claim-warrants + assert Path(SKILL).name == "dorian-claim-warrants" + + +def test_required_safety_phrases_present(tmp_path: Path) -> None: + repo = tmp_path / "repo" + _seed_repo(repo) + assert _install(repo) == 0 + text = (repo / SKILL / "SKILL.md").read_text(encoding="utf-8") + for phrase in ( + "DRAFT", + "not verified until Dorian verify passes", + "not a sandbox", + "trusted repo", + "strength-gate=fail", + "binding-gate=warn", + ): + assert phrase in text, f"SKILL.md missing required phrase: {phrase!r}" + + +def test_example_and_template_json_is_valid(tmp_path: Path) -> None: + repo = tmp_path / "repo" + _seed_repo(repo) + assert _install(repo) == 0 + good = repo / SKILL / "examples/good-claims.json" + skeleton = repo / SKILL / "templates/claims.json" + # both are valid JSON and load through Dorian's real claims loader + assert len(claims_io.load_claims(good)) == 5 + assert len(claims_io.load_claims(skeleton)) == 1 + + +def test_no_installed_file_brands_itself_agent_receipts(tmp_path: Path) -> None: + repo = tmp_path / "repo" + _seed_repo(repo) + assert _install(repo, "--with-hook") == 0 + for p in (repo / ".claude").rglob("*"): + if not p.is_file(): + continue + text = p.read_text(encoding="utf-8") + for nogo in AGENT_RECEIPTS_NO_GO: + assert nogo not in text, f"{p} contains no-go branding {nogo!r}" + + +def test_safety_boundary_distinguishes_agent_receipts(tmp_path: Path) -> None: + """The comparison must exist (so users aren't confused) — just not as branding.""" + repo = tmp_path / "repo" + _seed_repo(repo) + assert _install(repo) == 0 + text = (repo / SKILL / "reference/safety-boundary.md").read_text(encoding="utf-8") + assert "Agent Receipts" in text + assert "claim warrants" in text + + +# --- end to end --------------------------------------------------------------- + + +def test_scaffold_seal_and_revoke_end_to_end(tmp_path: Path) -> None: + """Scaffold -> author a real claim from the templates -> verify seals (exit 0) -> + mutate the source -> revalidate folds the warrant to REVOKED (exit 4).""" + repo = tmp_path / "repo" + _seed_repo(repo) + assert _install(repo) == 0 + + # a real, checkable change: a config value the change note asserts + (repo / "pyproject.toml").write_text( + '[project]\nname = "demo"\nversion = "1.4.0"\n', encoding="utf-8" + ) + notes = repo / "docs" / "changes" + notes.mkdir(parents=True) + (notes / "bump.md").write_text("# Bump\n\nThe package version is 1.4.0.\n", encoding="utf-8") + (notes / "bump.claims.json").write_text( + json.dumps( + { + "claims": [ + { + "id": "version-1-4-0", + "text": "pyproject version is 1.4.0.", + "kind": "quantity", + "load_bearing": True, + "checkers": [ + { + "type": "C3", + "program": 'config-value:pyproject.toml:project.version:"1.4.0"', + } + ], + } + ] + } + ), + encoding="utf-8", + ) + _git(repo, "add", "-A") + _git(repo, "commit", "-qm", "change + claim") + + # seal under the strength gate (the command the skill prints) + assert ( + main( + [ + "--repo", + str(repo), + "verify", + "docs/changes/bump.md", + "--claims", + str(notes / "bump.claims.json"), + "--strength-gate=fail", + "--binding-gate=warn", + ] + ) + == 0 + ) + assert (notes / "bump.md.warrant").is_file() + _git(repo, "add", "-A") + _git(repo, "commit", "-qm", "seal") + + # drift: the version changes; the sealed claim is now false + (repo / "pyproject.toml").write_text( + '[project]\nname = "demo"\nversion = "1.5.0"\n', encoding="utf-8" + ) + _git(repo, "add", "-A") + _git(repo, "commit", "-qm", "bump again — breaks the sealed claim") + + assert main(["--repo", str(repo), "revalidate", "--since", "HEAD~1"]) == 4 # REVOKED diff --git a/tests/test_packaging.py b/tests/test_packaging.py index b03f5d0..df7834b 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -121,3 +121,30 @@ def git(*a: str) -> None: assert r.returncode == 0, r.stderr assert "verified 1/1 claim(s)" in r.stdout assert (repo / "note.md.warrant").is_file() + + +def test_installed_scaffolds_claim_warrants_skill(installed_dorian: Path, tmp_path: Path) -> None: + """The packaged binary scaffolds the claim-warrants skill from package data — + proving the templates ship in the wheel and resolve via importlib.resources.""" + repo = tmp_path / "repo" + repo.mkdir() + subprocess.run(["git", "init", "-q"], cwd=repo, check=True, capture_output=True) + r = subprocess.run( + [ + str(installed_dorian), + "--repo", + str(repo), + "claude-code", + "install-claim-warrants", + "--with-hook", + ], + capture_output=True, + text=True, + ) + assert r.returncode == 0, r.stderr + skill = repo / ".claude/skills/dorian-claim-warrants/SKILL.md" + assert skill.is_file() + assert (repo / ".claude/hooks/dorian_claim_warrants_stop.py").is_file() + text = skill.read_text(encoding="utf-8") + for phrase in ("DRAFT", "not a sandbox", "strength-gate=fail"): + assert phrase in text From a3777a7802b8e02703860b000c9f026537608efb Mon Sep 17 00:00:00 2001 From: Ajay Surya Date: Sun, 28 Jun 2026 00:53:42 +0530 Subject: [PATCH 3/5] docs: Dorian claim warrants for Claude Code + Agent Receipts distinction - DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md: the integration guide (what it is/is not, install, invoke, review-first + trusted-local flows, the opt-in Stop hook, checker selection, good vs bad claims, trust boundary, troubleshooting, commit + later revalidation). - CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md: how Dorian claim warrants (deterministic claim-truth that revokes on drift) differ from and complement Agent Receipts / Obsigna (an Ed25519-signed, hash-chained W3C-VC audit trail of agent actions). "receipt" is an explanatory metaphor, never the brand. - README: a one-command "Claude Code claim warrants" section + a command-surface entry. - CLAUDE_CODE_DORIAN_WORKFLOW.md: retitled to "claim warrants"; points at the new skill. - POSITIONING_2026_06_27.md: v1.3.0 naming update + "claim warrants" tagline. Co-Authored-By: Claude Opus 4.8 --- README.md | 20 +- docs/CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md | 56 ++++++ docs/CLAUDE_CODE_DORIAN_WORKFLOW.md | 13 +- ...DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md | 171 ++++++++++++++++++ docs/POSITIONING_2026_06_27.md | 20 +- 5 files changed, 274 insertions(+), 6 deletions(-) create mode 100644 docs/CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md create mode 100644 docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md diff --git a/README.md b/README.md index 23ec59a..36afc03 100644 --- a/README.md +++ b/README.md @@ -308,6 +308,17 @@ claim, so use the lower-level two-step instead: `dorian capture` to build the re ## Using dorian with Claude Code +> **One command: `dorian claude-code install-claim-warrants`.** In a trusted repo this scaffolds a +> project-local Claude Code skill — invoke **`/dorian-claim-warrants`** after a change — that drafts the +> change note + `claims.json` for the checkable facts your agent claimed, then prints the verify command: +> `dorian verify docs/changes/.md --claims docs/changes/.claims.json --strength-gate=fail +> --binding-gate=warn`. The **model only drafts; `dorian verify` proves** it deterministically and +> token-free. Later, `dorian revalidate --since ` REVOKEs a claim the code drifted away from. Add +> `--with-hook` for an opt-in, reminder-only Stop hook. Not a sandbox — trusted repos only. Guide: +> [`docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md`](docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md) +> (and how it differs from Agent Receipts: +> [`docs/CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md`](docs/CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md)). + The intended loop is an agent-in, checker-out handshake: a coding agent writes the change *and* the `claims.json` for what it just did, dorian verifies those claims against the real code, and then keeps re-checking them on every later commit. Nothing about dorian is Claude-specific — any agent (or @@ -395,7 +406,7 @@ jobs: with: fetch-depth: 0 # revalidate diffs against the PR base sha persist-credentials: false # the Action only reads the diff + posts via GITHUB_TOKEN - - uses: ajaysurya1221/dorian/action@v1.2.0 + - uses: ajaysurya1221/dorian/action@v1.3.0 with: fail_on: revoked # install defaults to the published PyPI package (dorian-vwp); pin a @@ -437,6 +448,11 @@ claims. `claims.json`, the change note it backs, and a `.github/workflows/dorian.yml` Action workflow. Writes files only (never runs a checker or executes code), stays inside the repo, and skips existing files unless `--force`. The global `--json` prints a machine-readable plan. +- `dorian claude-code install-claim-warrants [--with-hook] [--dry-run] [--force]` — scaffold a + project-local Claude Code skill (`/dorian-claim-warrants`) that drafts a change note + `claims.json` + for the checkable facts your agent claimed, plus review-first settings examples and an opt-in, + reminder-only Stop hook. The model only drafts; `dorian verify` proves. Writes files only; idempotent. + See [`docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md`](docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md). - `dorian verify --claims claims.json` — the one-shot agent-claims entry point: auto-derive the read-set from each C3/C4/C5 checker, then seal (born-verifiable). C1 span claims use `dorian capture` + `dorian seal` instead. @@ -557,7 +573,7 @@ work perishable, so you find out when it expired. **reproducible on those frozen SHAs only** — not a real-world performance claim; the trigger and truth layers are reported separately. - **PyPI trusted publishing** — `dorian-vwp` is published to PyPI via a Trusted Publisher - (latest: **`v1.2.0`**); `pip install dorian-vwp` installs the released package. + (latest: **`v1.3.0`**); `pip install dorian-vwp` installs the released package. Non-goals stay non-goals: no servers, no dashboards, no hosted control plane, no model at check time. Local-first is the design center. diff --git a/docs/CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md b/docs/CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md new file mode 100644 index 0000000..472b325 --- /dev/null +++ b/docs/CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md @@ -0,0 +1,56 @@ +# Dorian claim warrants vs Agent Receipts + +Two projects with adjacent names answer **different questions**. They are +complementary, not substitutes. This page keeps them distinct so you pick the right +tool — and so Dorian's "receipt" metaphor is never mistaken for the Agent Receipts +protocol. + +## Two different questions + +| | **Agent Receipts** / Obsigna | **Dorian claim warrants** | +|---|---|---| +| Core question | *What did the agent do, on whose authority, with what inputs — and was the log tampered with?* | *Is this specific engineering claim true now, and will it revoke when later code makes it false?* | +| Unit of record | An **action** (a tool call the agent made) | A **claim** (a checkable fact a change asserts) | +| Mechanism | A daemon records a tamper-evident **receipt for every tool call**, structured as a W3C Verifiable Credential (`type AgentReceipt`), **signed with Ed25519** and **hash-chained** to the previous receipt | A deterministic checker (AST/regex/file-symbol/`pytest`/data) seals a passing claim into a git `.warrant`; later drift folds it to `REVOKED` | +| Trust property | Integrity of the **action log** (it can't be silently altered) | Truth of the **stated claim** (it can't silently rot as code drifts) | +| When it runs | Continuously, as the agent acts | At seal time, then on later diffs — **token-free**, no model | +| Cryptography | Ed25519 signatures, hash-chain | Content-addressed warrant id (SHA-256); **unsigned** today | +| Surface | Obsigna ecosystem: hook, MCP proxy, SDKs (Go/TS/Python), dashboard | Local-first CLI + GitHub Action; no SaaS, no dashboard | + +> Facts about Agent Receipts above are from agentreceipts.ai (verified 2026-06-27): +> "a tamper-evident receipt for every tool call your agent makes", "structured as a +> W3C Verifiable Credential with type AgentReceipt", "signed with Ed25519", "hash +> link to the previous receipt, forming a tamper-evident sequence". + +## Why "receipt" here is a metaphor, not a brand + +Dorian claim warrants are **receipts for checkable engineering claims, not receipts +for agent actions.** The word describes the artifact ("proof this stated fact held +when sealed, and an alarm if it later breaks"). Dorian does **not**: + +- record every tool call the agent made, +- prove *what the agent did* or *who authorized it*, +- produce a signed, hash-chained action audit trail, +- replace Agent Receipts, Sigstore, SLSA, in-toto, or audit logs. + +So the product name is **claim warrants**, never "agent receipts". + +## They are complementary + +- **Agent Receipts** can prove the agent actually called `Edit`/`Write`/`Run` — the + provenance of the *actions*. +- **Dorian** can prove the agent's stated engineering *claim* ("the default is still + `False`", "`requires-python` is `>=3.11`") was true when sealed, and detect when a + later commit silently makes it false. + +An action log tells you *that the agent did something*. A claim warrant tells you +*whether what it said it accomplished is still true*. Used together: Agent Receipts +for action provenance, Dorian for claim truth over time. + +## What Dorian deliberately is not + +Dorian is local-first, git-native, deterministic, and token-free at check time. It +is **not** a sandbox (`C4`/`C5` checkers execute code — trusted repos only), not an +LLM judge, not a SaaS, not a dashboard, and not a generic agent-action recorder. See +[`SECURITY_BOUNDARY.md`](SECURITY_BOUNDARY.md) and +[`VALIDATION_HONESTY.md`](VALIDATION_HONESTY.md). diff --git a/docs/CLAUDE_CODE_DORIAN_WORKFLOW.md b/docs/CLAUDE_CODE_DORIAN_WORKFLOW.md index 8b48004..42fb2b5 100644 --- a/docs/CLAUDE_CODE_DORIAN_WORKFLOW.md +++ b/docs/CLAUDE_CODE_DORIAN_WORKFLOW.md @@ -1,8 +1,15 @@ -# Claude Code + Dorian — turn your agent's summary into receipts +# Claude Code + Dorian — turn your agent's summary into claim warrants > A practical workflow for Claude Code / Codex / Cursor users. When the agent finishes a change, it > ends with a summary of specific claims. This is how to make the **checkable** subset into deterministic -> git receipts that fail later if the code drifts — token-free, in a trusted repo. +> git **claim warrants** (receipts for checkable claims) that fail later if the code drifts — token-free, +> in a trusted repo. + +> **One-command Claude Code setup.** `dorian claude-code install-claim-warrants` scaffolds a +> `/dorian-claim-warrants` skill (and an opt-in reminder hook) that drafts the change note + `claims.json` +> for you, then prints the `dorian verify` command. The model drafts; Dorian proves. See +> [`DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md`](DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md). The manual +> loop below is the same thing by hand. ## The loop @@ -62,7 +69,7 @@ dorian status # trust state of every warranted arti ## GitHub Action ```yaml -- uses: ajaysurya1221/dorian/action@v1.2.0 +- uses: ajaysurya1221/dorian/action@v1.3.0 with: fail_on: revoked # block the PR when a sealed claim breaks # for semi-trusted contributors, also: checker_source: base + deny_exec: true diff --git a/docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md b/docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md new file mode 100644 index 0000000..d50fcb8 --- /dev/null +++ b/docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md @@ -0,0 +1,171 @@ +# Dorian Claim Warrants for Claude Code + +A one-command Claude Code integration that turns the **checkable** facts in a +coding agent's change summary into deterministic Dorian `.warrant` receipts — +**claim warrants** — that revoke when later code makes one false. Token-free, in +your own trusted repo. + +> **The model drafts. Dorian proves.** The skill (and the optional hook) only +> *draft* a change note and a `claims.json`. Sealing requires `dorian verify` to +> run each claim's deterministic checker and pass — **no model runs at check +> time, ever.** A claim is a **DRAFT — not verified until Dorian verify passes.** + +## What this is + +- A project-local Claude Code **skill** at + `.claude/skills/dorian-claim-warrants/SKILL.md`, invoked as + **`/dorian-claim-warrants`**. It reads your git diff and the agent's summary, + drafts `docs/changes/.md` + `docs/changes/.claims.json` for the + checkable subset (config values, signatures/defaults, constants, file/symbol + references), and prints the exact `dorian verify` command. +- An **opt-in, reminder-only** Stop hook (`--with-hook`) that nudges Claude to + create claim warrants at the end of a coding turn. It never blocks and never + runs Dorian. +- Review-first settings examples. + +## What this is not + +- **Not** an LLM judge — no model runs at check time. +- **Not** a semantic verifier of the whole summary — it warrants only the claims + written down, and cannot catch a lie of omission. +- **Not** a sandbox — `C4 pytest:` and `C5 shell:` checkers execute code; use it + in **trusted repos** only. +- **Not** a replacement for tests, SAST, CI, code review, or human judgment. +- **Not** "Agent Receipts" — that protocol audits agent *actions*; Dorian warrants + *claim truth*. See [`CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md`](CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md). + +## Install + +```bash +pip install dorian-vwp # or: uv tool install dorian-vwp +cd your-trusted-repo +dorian claude-code install-claim-warrants # add --with-hook for the reminder hook +``` + +This scaffolds (idempotent; never overwrites without `--force`, `--dry-run` to preview): + +``` +.claude/skills/dorian-claim-warrants/ SKILL.md, README.md, examples/, templates/, reference/ +.claude/settings.dorian-claim-warrants.review-first.example.json +.claude/settings.dorian-claim-warrants.trusted-local.example.json +.claude/hooks/dorian_claim_warrants_stop.py (only with --with-hook; not auto-enabled) +``` + +Flags: `--force`, `--dry-run`, `--with-hook`, `--no-hook` (default), `--settings-only`, +`--print-next-steps`, `--target PATH`. + +> **Restart Claude Code** after the first install — a *new* top-level skills +> directory registers only at startup. Then `/dorian-claim-warrants` appears. + +## Invoke + +After a coding change: + +``` +/dorian-claim-warrants +``` + +The skill drafts the change note and claims, then either prints the verify command +(review-first) or runs it (trusted-local). + +## Review-first flow (default) + +The skill drafts `docs/changes/.md` and `docs/changes/.claims.json`, +labels them **DRAFT — not verified**, and prints: + +```bash +dorian verify docs/changes/.md --claims docs/changes/.claims.json \ + --strength-gate=fail --binding-gate=warn +``` + +You review the claims (they are executable input), then run it. Only an exit-0 +`dorian verify` seals the `.warrant`. + +## Trusted-local optional flow + +If you trust the repo and have reviewed how the skill drafts claims, copy +`.claude/settings.dorian-claim-warrants.trusted-local.example.json` into your +`.claude/settings.json` to pre-allow `dorian verify`/`revalidate`. The skill will +then run verify itself — and report the real exit code, never claiming "verified" +unless Dorian actually sealed a warrant. + +## Optional Stop hook + +`dorian claude-code install-claim-warrants --with-hook` scaffolds +`.claude/hooks/dorian_claim_warrants_stop.py`. It is **off until you register it** +under `hooks.Stop` in your project `.claude/settings.json`: + +```json +{ + "hooks": { + "Stop": [ + { "hooks": [ { "type": "command", + "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/dorian_claim_warrants_stop.py\"" } ] } + ] + } +} +``` + +It returns a soft `additionalContext` reminder only — never a block, so it cannot +loop. It no-ops on non-Stop events, on `stop_hook_active`, outside a git repo, when +no relevant code/config changed, when claim-warrant artifacts already exist in the +diff, or when the final message says nothing changed. It never runs `dorian verify` +or tests, never writes files, and never executes project code (its only side effect +is a read-only `git status`). Suppress it for a turn with +`DORIAN_CLAIM_WARRANTS_SKIP=1`. + +## Checker selection guide + +Match the checker to the claim's `kind` (truth axis). Full map in the bundled +`reference/checker-selection.md`. Quick guide: + +| The summary says… | `kind` | checker `program` | +|---|---|---| +| package/config value | `quantity`/`fact` | `config-value:pyproject.toml:project.requires-python:">=3.11"` | +| a Python signature/defaults | `reference` | `py-signature:m.py::f::a, b=1 -> int` | +| a Python constant's value | `quantity` | `py-const:m.py::TIMEOUT::30` | +| symbol/path exists | `reference` | `symbol:m.py::X` / `path:pkg/p.py` | +| an anchored value | `quantity` | `regex:m.py::TIMEOUT\s*=\s*30\b` | +| a real behavior (safe test) | `behavior` | `pytest:tests/test_x.py::T` (RUNS the test) | + +`--strength-gate=fail` refuses to seal a load-bearing **behavior** claim backed only +by existence, or a **quantity** value backed only by a bare existence check. + +## Good vs bad claims + +**Good** (specific, falsifiable): `config-value:` / `py-signature:` / `py-const:` +for no-test facts; `pytest:` for a real behavior. **Bad** (leave as prose): "cleaner", +"faster", "refactored", "updated docs", a behavior backed only by `symbol:`, a +quantity backed only by existence. See the bundled `examples/bad-claims.md`. + +## Security / trust boundary + +`dorian verify`/`revalidate` execute checker programs; `C4 pytest:`/`C5 shell:` +execute code. Treat an agent-emitted `claims.json` like agent-emitted code: review +it before running, and only in a **trusted** repo. In semi-trusted contexts add +`--deny-exec` (fail-closed, **not a sandbox**). Full boundary: +[`SECURITY_BOUNDARY.md`](SECURITY_BOUNDARY.md). + +## Troubleshooting + +- **`/dorian-claim-warrants` doesn't appear** → restart Claude Code (new skills dir + registers at startup). +- **`dorian verify` exits 4** → a load-bearing claim is false or its checker can't + run. Fix the claim or the code; never fake a checker. +- **`dorian verify` exits 5 (ERRORED)** → environment missing (e.g. no `pytest`). + Fail-closed; resolve the environment. +- **The skill won't run tools** → a checked-in project skill needs the workspace + trust dialog accepted first. + +## How to commit artifacts + +Commit `docs/changes/.md`, `docs/changes/.claims.json`, and — once +sealed — `docs/changes/.md.warrant`, alongside your change. + +## How the GitHub Action revalidates later + +On every later PR, the Dorian Action runs `dorian revalidate --since ` and +re-checks only the affected claims. A broken load-bearing claim folds the warrant to +`REVOKED` (exit 4) and blocks the PR, naming the claim — deterministically and +token-free. See [`action/README.md`](../action/README.md); for semi-trusted +contributors add `checker_trust: base` + `deny_exec: true`. diff --git a/docs/POSITIONING_2026_06_27.md b/docs/POSITIONING_2026_06_27.md index 4f20180..20cdbf8 100644 --- a/docs/POSITIONING_2026_06_27.md +++ b/docs/POSITIONING_2026_06_27.md @@ -5,9 +5,27 @@ > [`VALIDATION_HONESTY.md`](VALIDATION_HONESTY.md): no sandbox, no LLM judging, no semantic proof, no > broad compliance, not a replacement for tests/review. +## Naming update (v1.3.0) + +> **Product name: "claim warrants."** After discovering the nearby **Agent Receipts** / Obsigna project +> (a signed, hash-chained audit trail of agent *actions* — W3C Verifiable Credentials, Ed25519), the +> shipped product name is **Dorian claim warrants**, not "agent receipts." "Receipt" stays only as an +> explanatory metaphor: a Dorian claim warrant is a *receipt for a checkable engineering claim, not a +> receipt for an agent action.* The two are complementary, not substitutes — see +> [`CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md`](CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md). Updated tagline: +> +> **Claim warrants for what your coding agent said changed.** +> +> Shipped in v1.3.0 as a one-command Claude Code skill: `dorian claude-code install-claim-warrants` → +> `/dorian-claim-warrants` (the model drafts; Dorian proves). See +> [`DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md`](DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md). + ## Primary tagline -> **Receipts for what your AI coding agent claimed it changed.** +> **Claim warrants for what your coding agent said changed.** + +(Earlier metaphor framing: "receipts for what your AI coding agent claimed it changed" — kept only as +descriptive prose, never as the brand.) Sub-line (when there's room): From 1d326c5016ef8ee28403be1b070f395961764303 Mon Sep 17 00:00:00 2001 From: Ajay Surya Date: Sun, 28 Jun 2026 00:53:52 +0530 Subject: [PATCH 4/5] release: prepare v1.3.0 (claim-warrants integration) + dogfood warrant - Bump 1.2.0 -> 1.3.0 (pyproject, __init__, uv.lock) and the action@v1.3.0 pins (README, action/README, SECURITY_AND_SAFE_RUNNERS, CLAUDE_CODE_DORIAN_WORKFLOW). - BENCHMARK_CURRENT.md re-stamped to 1.3.0: the claim-warrants feature is a CLI/packaging/docs addition touching no checker/binding/fold code, so the figures stand unchanged (suites last executed at v1.2.0). - CHANGELOG [1.3.0] documents the additive, non-breaking feature (core stays zero-dependency; no model in the verification path). - Dogfood: docs/changes/claude-code-claim-warrants.{md,claims.json,md.warrant} seals 11/11 load-bearing facts (CLI surface, packaged templates, hook stop_hook_active + additionalContext, the not-a-sandbox / model-drafts-Dorian-proves / Agent-Receipts docs, zero deps, strength-gate=fail) under --strength-gate=fail --binding-gate=warn. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 37 +++ action/README.md | 6 +- docs/BENCHMARK_CURRENT.md | 4 +- docs/SECURITY_AND_SAFE_RUNNERS.md | 4 +- .../claude-code-claim-warrants.claims.json | 103 ++++++ docs/changes/claude-code-claim-warrants.md | 44 +++ .../claude-code-claim-warrants.md.warrant | 294 ++++++++++++++++++ pyproject.toml | 2 +- src/dorian/__init__.py | 2 +- uv.lock | 2 +- 10 files changed, 488 insertions(+), 10 deletions(-) create mode 100644 docs/changes/claude-code-claim-warrants.claims.json create mode 100644 docs/changes/claude-code-claim-warrants.md create mode 100644 docs/changes/claude-code-claim-warrants.md.warrant diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ab38f2..3656d01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,43 @@ semantics have been stable since 1.0.0. ## [Unreleased] +## [1.3.0] — 2026-06-28 + +The **Dorian Claim Warrants for Claude Code** integration: one command scaffolds a project-local Claude +Code skill that drafts claim warrants for the checkable facts a coding agent says it changed, which +`dorian verify` then proves deterministically. **No breaking changes** — purely additive (a new CLI +subcommand, packaged templates, docs); warrant schema, checker grammar, exit codes, fold policy, and +security posture are unchanged, the core stays zero-dependency, and **no model is ever added to the +verification path**. + +### Added +- **`dorian claude-code install-claim-warrants`** (`src/dorian/claude_code.py`) — scaffolds a + project-local Claude Code skill at `.claude/skills/dorian-claim-warrants/` (invoked as + **`/dorian-claim-warrants`**) that drafts `docs/changes/.md` + `docs/changes/.claims.json` + for the *checkable* subset of a change summary (config values, signatures/defaults, constants, + file/symbol references), then prints `dorian verify … --strength-gate=fail --binding-gate=warn`. The + **model only drafts; Dorian verifies**. Writes files only (never runs a checker or executes code), + stays inside the target repo, never overwrites without `--force`, and is idempotent. Flags: + `--with-hook`, `--no-hook`, `--settings-only`, `--dry-run`, `--force`, `--print-next-steps`, + `--target`. +- **Packaged skill templates** (`src/dorian/templates/claude_code/…`, shipped as wheel package data): + `SKILL.md`, a bundle `README.md`, `examples/` (good/bad claims, a final-message walkthrough), + `templates/` (change-note + claims.json skeletons), and `reference/` (checker-selection map, + safety-boundary). +- **Opt-in, reminder-only Stop hook** (`hooks/dorian_claim_warrants_stop.py`, stdlib only). It returns a + soft `additionalContext` nudge — **never a block**, so it cannot loop — and only when a turn left + relevant, un-warranted code/config changes. It never runs `dorian verify` or tests, never writes + files, and never executes project code (its only side effect is a read-only `git status`); it fails + open. Not enabled by scaffolding — register it under `hooks.Stop` yourself. +- **Docs** — [`docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md`](docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md) + (the integration guide) and [`docs/CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md`](docs/CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md) + (how Dorian claim warrants differ from — and complement — the Agent Receipts action-audit protocol). + +### Changed +- README, `docs/CLAUDE_CODE_DORIAN_WORKFLOW.md`, and `docs/POSITIONING_2026_06_27.md` adopt the + **"claim warrants"** name (keeping "receipt" only as an explanatory metaphor, to avoid collision with + the Agent Receipts project). + ## [1.2.0] — 2026-06-27 C4 import-aware dependency binding, the opt-in truth-axis `--strength-gate`, and the diff --git a/action/README.md b/action/README.md index 7660376..99caf64 100644 --- a/action/README.md +++ b/action/README.md @@ -26,7 +26,7 @@ jobs: fetch-depth: 0 # REQUIRED: revalidate diffs against the PR base # sha, which a shallow clone does not contain persist-credentials: false # the Action reads the diff + posts via GITHUB_TOKEN - - uses: ajaysurya1221/dorian/action@v1.2.0 + - uses: ajaysurya1221/dorian/action@v1.3.0 with: fail_on: revoked # install defaults to the published PyPI package (dorian-vwp); @@ -78,7 +78,7 @@ self-attested-verdict problem for *non-executable* checkers — that is what ```yaml # untrusted / public-fork posture -- uses: ajaysurya1221/dorian/action@v1.2.0 +- uses: ajaysurya1221/dorian/action@v1.3.0 with: deny_exec: "true" # C4/C5 ERROR instead of executing ``` @@ -94,7 +94,7 @@ executed). Implemented and proven by the ```yaml # public / forked-PR posture: trusted checker specs + no code execution -- uses: ajaysurya1221/dorian/action@v1.2.0 +- uses: ajaysurya1221/dorian/action@v1.3.0 with: checker_trust: base # run only base-approved checker specs deny_exec: "true" # and refuse to execute even those (belt and braces) diff --git a/docs/BENCHMARK_CURRENT.md b/docs/BENCHMARK_CURRENT.md index 83e5fd9..7ebd584 100644 --- a/docs/BENCHMARK_CURRENT.md +++ b/docs/BENCHMARK_CURRENT.md @@ -10,9 +10,9 @@ and are kept as-is for provenance. | field | value | | --- | --- | -| dorian version | `1.2.0` | +| dorian version | `1.3.0` | | metric commit | `33e9eaf` (the benchmark figures were measured here, during the release audit) | -| release commit | `81cebbc` (1.0.1) → v1.0.2 announcement hotfix. The 1.0.1 changes (C4 leading-dash nodeid rejection, C5 reconcile per-query timeout, a byte-identical index-once `verify` refactor, the `suggest-claims` / `export --in-toto` commands) plus the v1.0.2 hotfix (export `.warrant` filename disambiguation, `suggest-claims` PEP 263 encoding read, a `symbol_index` non-git `GitError` guard, and CI/SCA/credential/doc hardening) touch no checker numeric behavior; both suites below were **re-run at 1.0.2 and reproduce the metric-commit figures exactly** — binding-lifecycle to the same content-derived `run_id` `168b50d9aa631d52` — so these changes do not move what the suites measure. v1.1.0 added the `dorian init` scaffolder, the PR-comment renderer enhancements (a status line, trust-change counts, sealed-at, and remediation), and build/VCS guards against editor/file-sync `… 2.py` duplicate files (untracked local artifacts — never tracked, never in a CI wheel or on PyPI); v1.1.1 makes the `dorian init` starter claim load-bearing (a scaffold default). All of this is a new command plus output formatting, scaffold defaults, and packaging hygiene only — touching no checker/binding/fold code — so the figures stand unchanged at 1.1.1 (the suites were last executed at 1.0.2, not re-run since). **v1.2.0** adds C4 import-aware binding (a trigger-axis watch widening that affects only C4 `pytest:` claims — not the C1/C3/C5 symbol/regex/string/path/data paths these suites exercise) and the opt-in, default-off `--strength-gate` (advisory; changes no checker verdict or binding). All three suites were **re-run at v1.2.0 and reproduce the metric-commit figures exactly** — binding-lifecycle again landing on the same content-derived `run_id 168b50d9aa631d52` — so 1.2.0 does not move what they measure | +| release commit | `81cebbc` (1.0.1) → v1.0.2 announcement hotfix. The 1.0.1 changes (C4 leading-dash nodeid rejection, C5 reconcile per-query timeout, a byte-identical index-once `verify` refactor, the `suggest-claims` / `export --in-toto` commands) plus the v1.0.2 hotfix (export `.warrant` filename disambiguation, `suggest-claims` PEP 263 encoding read, a `symbol_index` non-git `GitError` guard, and CI/SCA/credential/doc hardening) touch no checker numeric behavior; both suites below were **re-run at 1.0.2 and reproduce the metric-commit figures exactly** — binding-lifecycle to the same content-derived `run_id` `168b50d9aa631d52` — so these changes do not move what the suites measure. v1.1.0 added the `dorian init` scaffolder, the PR-comment renderer enhancements (a status line, trust-change counts, sealed-at, and remediation), and build/VCS guards against editor/file-sync `… 2.py` duplicate files (untracked local artifacts — never tracked, never in a CI wheel or on PyPI); v1.1.1 makes the `dorian init` starter claim load-bearing (a scaffold default). All of this is a new command plus output formatting, scaffold defaults, and packaging hygiene only — touching no checker/binding/fold code — so the figures stand unchanged at 1.1.1 (the suites were last executed at 1.0.2, not re-run since). **v1.2.0** adds C4 import-aware binding (a trigger-axis watch widening that affects only C4 `pytest:` claims — not the C1/C3/C5 symbol/regex/string/path/data paths these suites exercise) and the opt-in, default-off `--strength-gate` (advisory; changes no checker verdict or binding). All three suites were **re-run at v1.2.0 and reproduce the metric-commit figures exactly** — binding-lifecycle again landing on the same content-derived `run_id 168b50d9aa631d52` — so 1.2.0 does not move what they measure. **v1.3.0** adds the Claude Code claim-warrants scaffolder (`dorian claude-code install-claim-warrants`: a new CLI subcommand, packaged skill/hook/settings templates, an opt-in reminder Stop hook, and docs) — a CLI/packaging/docs addition that touches **no checker, binding, or fold code**, so the figures stand unchanged at 1.3.0 (the suites were last executed at v1.2.0, not re-run since) | | Python | 3.12.4 | | platform | darwin (CI matrix: 3.11 / 3.12 / 3.13) | | reproduce | `dorian bench large-mutation` · `dorian bench binding-lifecycle` · `dorian bench realworld-usecases` | diff --git a/docs/SECURITY_AND_SAFE_RUNNERS.md b/docs/SECURITY_AND_SAFE_RUNNERS.md index 5a7553a..e0482be 100644 --- a/docs/SECURITY_AND_SAFE_RUNNERS.md +++ b/docs/SECURITY_AND_SAFE_RUNNERS.md @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 # the action needs full history to revalidate --since base - - uses: ajaysurya1221/dorian/action@v1.2.0 + - uses: ajaysurya1221/dorian/action@v1.3.0 with: checker_trust: base # resolve checker SPECs from the trusted base ref deny_exec: "true" # C4 pytest / C5 shell ERROR instead of executing @@ -113,7 +113,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: ajaysurya1221/dorian/action@v1.2.0 + - uses: ajaysurya1221/dorian/action@v1.3.0 # checker_trust defaults to head, deny_exec defaults to false: # executing checkers run, because contributors are trusted. with: diff --git a/docs/changes/claude-code-claim-warrants.claims.json b/docs/changes/claude-code-claim-warrants.claims.json new file mode 100644 index 0000000..58004f6 --- /dev/null +++ b/docs/changes/claude-code-claim-warrants.claims.json @@ -0,0 +1,103 @@ +{ + "claims": [ + { + "id": "cli-exposes-install-claim-warrants", + "text": "The CLI registers the `install-claim-warrants` subcommand.", + "kind": "reference", + "load_bearing": true, + "checkers": [ + { "type": "C3", "program": "regex:src/dorian/cli.py::\"install-claim-warrants\"" } + ] + }, + { + "id": "cli-handler-exists", + "text": "commands.py defines the claude-code command handler.", + "kind": "reference", + "load_bearing": true, + "checkers": [ + { "type": "C3", "program": "symbol:src/dorian/commands.py::cmd_claude_code" } + ] + }, + { + "id": "skill-template-shipped", + "text": "The skill SKILL.md template ships in source package data.", + "kind": "reference", + "load_bearing": true, + "checkers": [ + { "type": "C3", "program": "path:src/dorian/templates/claude_code/dorian-claim-warrants/SKILL.md" } + ] + }, + { + "id": "hook-handles-stop-hook-active", + "text": "The Stop hook template handles stop_hook_active (loop guard).", + "kind": "reference", + "load_bearing": true, + "checkers": [ + { "type": "C3", "program": "regex:src/dorian/templates/claude_code/hooks/dorian_claim_warrants_stop.py::stop_hook_active" } + ] + }, + { + "id": "hook-uses-additional-context", + "text": "The Stop hook template emits additionalContext (reminder, not block).", + "kind": "reference", + "load_bearing": true, + "checkers": [ + { "type": "C3", "program": "regex:src/dorian/templates/claude_code/hooks/dorian_claim_warrants_stop.py::additionalContext" } + ] + }, + { + "id": "docs-hook-reminder-only", + "text": "The skill doc states the hook is reminder-only.", + "kind": "reference", + "load_bearing": true, + "checkers": [ + { "type": "C3", "program": "regex:docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md::reminder-only" } + ] + }, + { + "id": "docs-model-drafts-dorian-verifies", + "text": "The skill doc states the model drafts and Dorian proves.", + "kind": "reference", + "load_bearing": true, + "checkers": [ + { "type": "C3", "program": "regex:docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md::The model drafts" } + ] + }, + { + "id": "docs-not-a-sandbox", + "text": "The skill doc states Dorian is not a sandbox.", + "kind": "reference", + "load_bearing": true, + "checkers": [ + { "type": "C3", "program": "regex:docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md::not a sandbox" } + ] + }, + { + "id": "docs-agent-receipts-comparison", + "text": "A docs page compares Dorian claim warrants with Agent Receipts.", + "kind": "reference", + "load_bearing": true, + "checkers": [ + { "type": "C3", "program": "regex:docs/CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md::Agent Receipts" } + ] + }, + { + "id": "zero-runtime-dependencies", + "text": "dorian's core ships zero runtime dependencies.", + "kind": "quantity", + "load_bearing": true, + "checkers": [ + { "type": "C3", "program": "config-value:pyproject.toml:project.dependencies:[]" } + ] + }, + { + "id": "demo-uses-strength-gate-fail", + "text": "The integration's documented verify command uses --strength-gate=fail.", + "kind": "reference", + "load_bearing": true, + "checkers": [ + { "type": "C3", "program": "regex:docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md::strength-gate=fail" } + ] + } + ] +} diff --git a/docs/changes/claude-code-claim-warrants.md b/docs/changes/claude-code-claim-warrants.md new file mode 100644 index 0000000..25240bc --- /dev/null +++ b/docs/changes/claude-code-claim-warrants.md @@ -0,0 +1,44 @@ +# Change note — Dorian Claim Warrants for Claude Code (v1.3.0) + +Adds a one-command Claude Code integration: `dorian claude-code +install-claim-warrants` scaffolds a project-local `/dorian-claim-warrants` skill +(and an opt-in, reminder-only Stop hook) that **drafts** claim warrants for the +checkable facts a coding agent says it changed. `dorian verify` then **proves** +them deterministically — no model is ever added to the verification path, and the +core stays zero-dependency. + +This note is dogfooded: the load-bearing facts the feature relies on are sealed in +[`claude-code-claim-warrants.claims.json`](claude-code-claim-warrants.claims.json) +under `--strength-gate=fail --binding-gate=warn`. + +## Checkable claims included + +- The CLI exposes `claude-code install-claim-warrants` (parser + handler). +- The packaged skill template ships in source package data. +- The hook template handles `stop_hook_active` and uses `additionalContext`. +- The docs state the hook is reminder-only, that the model drafts and Dorian + verifies, that Dorian is not a sandbox, and compare claim warrants to Agent + Receipts. +- `pyproject.toml` keeps a zero-dependency runtime core. +- The integration's documented command uses `--strength-gate=fail`. + +## Non-checkable claims intentionally excluded + +- "This makes Dorian easier to adopt" — outcome claim, not deterministically + checkable. +- "The skill drafts good claims" — quality judgment; only `dorian verify` decides + truth, and the skill's drafting is model-assisted at authoring time. + +## Verification command + +```bash +dorian verify docs/changes/claude-code-claim-warrants.md \ + --claims docs/changes/claude-code-claim-warrants.claims.json \ + --strength-gate=fail --binding-gate=warn +``` + +## Trust boundary + +The skill (and hook) only draft. Sealing requires `dorian verify` to run each +deterministic checker and pass. Dorian is **not a sandbox** — `C4`/`C5` checkers +execute code; trusted repos only. diff --git a/docs/changes/claude-code-claim-warrants.md.warrant b/docs/changes/claude-code-claim-warrants.md.warrant new file mode 100644 index 0000000..42abd62 --- /dev/null +++ b/docs/changes/claude-code-claim-warrants.md.warrant @@ -0,0 +1,294 @@ +{ + "spec_version": "0.1", + "warrant": { + "id": "sha256:009ee3c892d9cbe4da843977b777fead934c6994a759fb68374978e610a62d2c", + "artifact": { + "uri": "docs/changes/claude-code-claim-warrants.md", + "content_hash": "sha256:0ebf93ef602303c13f926de33cc984dda14b83bab09dd7aac82613421cacf71b", + "git_ref": "84db0878d04dc4accdd1474e6b62344157ccc9b1" + }, + "produced_by": { + "runner": "manual", + "captured_at": "2026-06-27T19:24:26.515852+00:00", + "run_ref": "", + "model": "", + "prompt_hash": "" + }, + "read_set": [ + { + "id": "rs-0", + "uri": "src/dorian/cli.py", + "selector": null, + "hash": "sha256:3b4fadcbf3119b33bfab487a9e0692ca3368f8f7354da672068e356a5d52e1ab", + "version": "84db0878d04dc4accdd1474e6b62344157ccc9b1", + "scope": "project" + }, + { + "id": "rs-1", + "uri": "src/dorian/commands.py", + "selector": null, + "hash": "sha256:caca1b5423e8f0002c6752754b91750fa178405b356ba3607b682a7ec074c1b6", + "version": "84db0878d04dc4accdd1474e6b62344157ccc9b1", + "scope": "project" + }, + { + "id": "rs-2", + "uri": "src/dorian/templates/claude_code/dorian-claim-warrants/SKILL.md", + "selector": null, + "hash": "sha256:77a452b85bde24e304f544c2b2a74b8b765955509d6d6119335017c9c7d9c5e4", + "version": "84db0878d04dc4accdd1474e6b62344157ccc9b1", + "scope": "project" + }, + { + "id": "rs-3", + "uri": "src/dorian/templates/claude_code/hooks/dorian_claim_warrants_stop.py", + "selector": null, + "hash": "sha256:84f0aae81d1ffb0ea5da821ec4fa1880aab8e24a2c255a33584ee8282804b872", + "version": "84db0878d04dc4accdd1474e6b62344157ccc9b1", + "scope": "project" + }, + { + "id": "rs-4", + "uri": "docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md", + "selector": null, + "hash": "sha256:58c0b3cdf5d39cfebd1e8ca2e21bf18899d40d76f48778b31592565c05817c27", + "version": "84db0878d04dc4accdd1474e6b62344157ccc9b1", + "scope": "project" + }, + { + "id": "rs-5", + "uri": "docs/CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md", + "selector": null, + "hash": "sha256:0850666e836aa7bf96a2117b83f290c9e5d70fb8f3ef351b71ab2675ff79d0dc", + "version": "84db0878d04dc4accdd1474e6b62344157ccc9b1", + "scope": "project" + }, + { + "id": "rs-6", + "uri": "pyproject.toml", + "selector": null, + "hash": "sha256:2c2ebbbf8a81a09caf84472633a4133c3b96d2cb4a77786f76a55830e2ee3a50", + "version": "84db0878d04dc4accdd1474e6b62344157ccc9b1", + "scope": "project" + } + ], + "claims": [ + { + "id": "cli-exposes-install-claim-warrants", + "text": "The CLI registers the `install-claim-warrants` subcommand.", + "kind": "reference", + "anchor": null, + "load_bearing": true, + "supports": [], + "checkers": [ + { + "type": "C3", + "program": "regex:src/dorian/cli.py::\"install-claim-warrants\"", + "expect": "exit:0", + "watch": [ + "src/dorian/cli.py" + ], + "timeout_s": 30 + } + ] + }, + { + "id": "cli-handler-exists", + "text": "commands.py defines the claude-code command handler.", + "kind": "reference", + "anchor": null, + "load_bearing": true, + "supports": [], + "checkers": [ + { + "type": "C3", + "program": "symbol:src/dorian/commands.py::cmd_claude_code", + "expect": "exit:0", + "watch": [ + "src/dorian/commands.py" + ], + "timeout_s": 30 + } + ] + }, + { + "id": "skill-template-shipped", + "text": "The skill SKILL.md template ships in source package data.", + "kind": "reference", + "anchor": null, + "load_bearing": true, + "supports": [], + "checkers": [ + { + "type": "C3", + "program": "path:src/dorian/templates/claude_code/dorian-claim-warrants/SKILL.md", + "expect": "exit:0", + "watch": [ + "src/dorian/templates/claude_code/dorian-claim-warrants/SKILL.md" + ], + "timeout_s": 30 + } + ] + }, + { + "id": "hook-handles-stop-hook-active", + "text": "The Stop hook template handles stop_hook_active (loop guard).", + "kind": "reference", + "anchor": null, + "load_bearing": true, + "supports": [], + "checkers": [ + { + "type": "C3", + "program": "regex:src/dorian/templates/claude_code/hooks/dorian_claim_warrants_stop.py::stop_hook_active", + "expect": "exit:0", + "watch": [ + "src/dorian/templates/claude_code/hooks/dorian_claim_warrants_stop.py" + ], + "timeout_s": 30 + } + ] + }, + { + "id": "hook-uses-additional-context", + "text": "The Stop hook template emits additionalContext (reminder, not block).", + "kind": "reference", + "anchor": null, + "load_bearing": true, + "supports": [], + "checkers": [ + { + "type": "C3", + "program": "regex:src/dorian/templates/claude_code/hooks/dorian_claim_warrants_stop.py::additionalContext", + "expect": "exit:0", + "watch": [ + "src/dorian/templates/claude_code/hooks/dorian_claim_warrants_stop.py" + ], + "timeout_s": 30 + } + ] + }, + { + "id": "docs-hook-reminder-only", + "text": "The skill doc states the hook is reminder-only.", + "kind": "reference", + "anchor": null, + "load_bearing": true, + "supports": [], + "checkers": [ + { + "type": "C3", + "program": "regex:docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md::reminder-only", + "expect": "exit:0", + "watch": [ + "docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md" + ], + "timeout_s": 30 + } + ] + }, + { + "id": "docs-model-drafts-dorian-verifies", + "text": "The skill doc states the model drafts and Dorian proves.", + "kind": "reference", + "anchor": null, + "load_bearing": true, + "supports": [], + "checkers": [ + { + "type": "C3", + "program": "regex:docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md::The model drafts", + "expect": "exit:0", + "watch": [ + "docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md" + ], + "timeout_s": 30 + } + ] + }, + { + "id": "docs-not-a-sandbox", + "text": "The skill doc states Dorian is not a sandbox.", + "kind": "reference", + "anchor": null, + "load_bearing": true, + "supports": [], + "checkers": [ + { + "type": "C3", + "program": "regex:docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md::not a sandbox", + "expect": "exit:0", + "watch": [ + "docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md" + ], + "timeout_s": 30 + } + ] + }, + { + "id": "docs-agent-receipts-comparison", + "text": "A docs page compares Dorian claim warrants with Agent Receipts.", + "kind": "reference", + "anchor": null, + "load_bearing": true, + "supports": [], + "checkers": [ + { + "type": "C3", + "program": "regex:docs/CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md::Agent Receipts", + "expect": "exit:0", + "watch": [ + "docs/CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md" + ], + "timeout_s": 30 + } + ] + }, + { + "id": "zero-runtime-dependencies", + "text": "dorian's core ships zero runtime dependencies.", + "kind": "quantity", + "anchor": null, + "load_bearing": true, + "supports": [], + "checkers": [ + { + "type": "C3", + "program": "config-value:pyproject.toml:project.dependencies:[]", + "expect": "exit:0", + "watch": [ + "pyproject.toml" + ], + "timeout_s": 30 + } + ] + }, + { + "id": "demo-uses-strength-gate-fail", + "text": "The integration's documented verify command uses --strength-gate=fail.", + "kind": "reference", + "anchor": null, + "load_bearing": true, + "supports": [], + "checkers": [ + { + "type": "C3", + "program": "regex:docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md::strength-gate=fail", + "expect": "exit:0", + "watch": [ + "docs/DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md" + ], + "timeout_s": 30 + } + ] + } + ], + "derives_from": [], + "fold_policy": { + "revoke_on": "load_bearing_broken", + "degrade_on": "any_stale_or_broken" + }, + "sealed_at": "2026-06-27T19:24:26Z", + "supersedes": null + } +} diff --git a/pyproject.toml b/pyproject.toml index a2319f6..1edc1f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "dorian-vwp" -version = "1.2.0" +version = "1.3.0" description = "Hold AI agents to what they said they did: deterministic, token-free verification of claims about a change." readme = "README.md" requires-python = ">=3.11" diff --git a/src/dorian/__init__.py b/src/dorian/__init__.py index c613b1d..b57ad48 100644 --- a/src/dorian/__init__.py +++ b/src/dorian/__init__.py @@ -3,4 +3,4 @@ PyPI distribution: `dorian-vwp`; import package: `dorian`; CLI: `dorian`. """ -__version__ = "1.2.0" +__version__ = "1.3.0" diff --git a/uv.lock b/uv.lock index 9d6a331..eb58ce6 100644 --- a/uv.lock +++ b/uv.lock @@ -184,7 +184,7 @@ wheels = [ [[package]] name = "dorian-vwp" -version = "1.2.0" +version = "1.3.0" source = { editable = "." } [package.optional-dependencies] From 1bf87800a45756cbc932cf9939ff8f2fff4cbf42 Mon Sep 17 00:00:00 2001 From: Ajay Surya Date: Sun, 28 Jun 2026 01:01:21 +0530 Subject: [PATCH 5/5] docs: add v1.3.0 release notes Co-Authored-By: Claude Opus 4.8 --- docs/releases/v1.3.0.md | 54 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/releases/v1.3.0.md diff --git a/docs/releases/v1.3.0.md b/docs/releases/v1.3.0.md new file mode 100644 index 0000000..98404a9 --- /dev/null +++ b/docs/releases/v1.3.0.md @@ -0,0 +1,54 @@ +# dorian v1.3.0 — Dorian Claim Warrants for Claude Code + +A feature release: a one-command Claude Code integration that turns the checkable facts in a coding +agent's change summary into Dorian claim warrants. **No breaking changes** — purely additive (a new CLI +subcommand, packaged templates, docs). The warrant format, checker grammar, exit codes, fold policy, and +security posture are unchanged; the core stays zero-dependency; and **no model is added to the +verification path**. + +## What changed + +### Feature: `dorian claude-code install-claim-warrants` +In a trusted repo, one command scaffolds a project-local Claude Code **skill** at +`.claude/skills/dorian-claim-warrants/` — invoked as **`/dorian-claim-warrants`** — that **drafts** +`docs/changes/.md` + `docs/changes/.claims.json` for the *checkable* subset of a change +summary (config values, signatures/defaults, constants, file/symbol references), then prints the proof +command. **The model only drafts; `dorian verify` proves** each claim deterministically and token-free. + +- Writes files only (never runs a checker or executes code), stays inside the target repo, never + overwrites without `--force`, and is idempotent. Flags: `--with-hook`, `--no-hook`, `--settings-only`, + `--dry-run`, `--force`, `--print-next-steps`, `--target`. +- **Packaged templates** ship as wheel package data (resolved via `importlib.resources`): `SKILL.md`, a + bundle README, examples (good/bad claims, a final-message walkthrough), change-note + claims.json + skeletons, and `reference/` (checker-selection map, safety-boundary). +- **Opt-in, reminder-only Stop hook** (stdlib only): returns `additionalContext` — **never a block**, so + it cannot loop — and only when a turn left relevant, un-warranted code/config changes. It never runs + `dorian verify` or tests, never writes files, and never executes project code (its only side effect is + a read-only `git status`); it fails open. Not enabled by scaffolding. + +### Docs +- [`DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md`](../DORIAN_CLAIM_WARRANTS_CLAUDE_CODE_SKILL.md) — the + integration guide. +- [`CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md`](../CLAIM_WARRANTS_VS_AGENT_RECEIPTS.md) — how Dorian claim + warrants (deterministic claim-truth that revokes on drift) differ from and complement the Agent + Receipts / Obsigna action-audit protocol (an Ed25519-signed, hash-chained W3C-Verifiable-Credential + trail of agent *actions*). "Receipt" is an explanatory metaphor, never the brand. +- README + command surface, `CLAUDE_CODE_DORIAN_WORKFLOW.md` (retitled to "claim warrants"), and the + positioning doc adopt the **"claim warrants"** name. + +## Evidence +- Full `uv run pytest` (incl. slow) green on CI across Python 3.11 / 3.12 / 3.13; `ruff check` and + `ruff format --check` clean. +- 46 new tests cover the scaffolder, the hook (loop-safety, fail-open, writes-nothing, env-skip), and + wheel packaging (a real wheel is built, installed into a clean venv, and the skill scaffolded from + package data). End-to-end: scaffold → `verify --strength-gate=fail` → drift → `revalidate` REVOKED + (exit 4). +- Dogfooded: [`docs/changes/claude-code-claim-warrants.md`](../changes/claude-code-claim-warrants.md) + seals 11/11 load-bearing facts under `--strength-gate=fail --binding-gate=warn`. +- An independent fresh-context judge returned **ACCEPT** (no must-fix). + +## Boundary (unchanged) +Not a sandbox (`C4 pytest:` / `C5 shell:` execute code — trusted repos only), not an LLM judge, not a +whole-summary verifier, and not "Agent Receipts". The two axes stay distinct: binding gates *when* a +claim re-checks; strength gates *whether* its checker can falsify it. Weak binding or weak strength means +low confidence, **not** a false claim.