Nothing your agent calls done counts until the Auditor runs it again.

aepkit is the command line for the Auditor/Executor Protocol. An Executor agent does one task at a time and pastes the unedited output. An Auditor runs every check again, then gives the verdict.

pipx install aepkit
Open on GitHub

v0.7.7Python 3.11+No dependenciesMIT Or hand the repository to your agent

Auditor re-runs P1-T1 with the role check removed exit 1
$ aepkit negcontrol --file crm/export.py --break-cmd "sed -i '' 's/raise PermissionError/pass  # raise PermissionError/' crm/export.py" --test-cmd "python -m pytest -q tests/test_export.py" --expect PermissionError
$ backed up crm/export.py -> /tmp/aepkit-negcontrol-1gp6951y/export.py
$ sed -i '' 's/raise PermissionError/pass  # raise PermissionError/' crm/export.py

$ python -m pytest -q tests/test_export.py   # expect FAILURE matching /PermissionError/
..                                                                       [100%]
2 passed in 0.01s
(exit 0)
$ restored crm/export.py from backup (byte-identical)
$ python -m pytest -q tests/test_export.py   # expect SUCCESS
..                                                                       [100%]
2 passed in 0.01s
(exit 0)

FAIL: the test did not fail when the protection was removed. Either the break command didn't do what you meant, or the protection isn't real.
From the demo run, a CRM for an online store. The Executor reported the task done and its tests passed. They still passed with the protection commented out, so the Auditor rejected it.

Every check runs twice

The Executor pastes the command output. The Auditor runs the same command again before any verdict, so a pasted “tested and working” closes nothing on its own.

The two roles

Protections get broken on purpose

aepkit negcontrol removes a protection, confirms the test fails for the reason you named, restores the file byte for byte and confirms the test passes again.

See one fail

Drift shows up in lint

Plan, instructions, evidence and corrections live in four files. aepkit lint cross-checks them: missing reports, done without output, gates with no negative control.

The four documents

A run from start to finish: a CRM for an online store

The goal: one customer record per buyer from the store’s order export, lifetime value per customer, and a customer export only the owner can run. Two phases, five task and gate IDs. Two mistakes were planted to show what catches them. Every block below is copied unedited from that run, and the whole run is published here.

Scaffold the documents

Three documents and an empty annexes/ folder. The Auditor fills in the plan and writes each task with its files, its steps and the literal command that verifies it.

Auditor starts the run with aepkit initexit 0
$ aepkit init docs/store-crm --name "Store CRM"
wrote  docs/store-crm/plan-of-record.md
wrote  docs/store-crm/execution-guide.md
wrote  docs/store-crm/compliance-log.md

docs/store-crm/ scaffolded. Read execution-guide.md's Rules of engagement first.

A report with no output is a failed report

The Executor marked the task done and wrote “ran the tests, all green” where the output belongs. aepkit lint treats a done report without pasted output as failed. The Executor pasted the real run, and the Auditor ran it again before approving.

P0-T2 as first reported: compliance-log.md at commit 78c3482, lines 42 to 46
### P0-T2 — DONE
**Changed:** `crm/orders.py`, `tests/test_orders.py`
**Verify output:** ran the tests, all green.
**Observations:** none
**Decisions made without stopping:** totals parsed with `Decimal` before converting to cents.
Auditor lints the documents before auditing P0-T2exit 1
$ aepkit lint docs/store-crm
[done without evidence] 1 report(s) marked DONE in compliance-log.md have no pasted Verify output (the protocol records these as FAILED):
  - P0-T2
  Paste the literal command output under **Verify output:** inside a ``` code block; a sentence about the result is not output.

1 issue(s) found.

The gate breaks its own protection

Before phase 0 closes, negcontrol removes the email normalization and watches the import count 6 customers where there are 4. Then it restores the file, checks it is byte-identical, and watches the tests pass again.

Auditor re-runs the negative control of gate P0-G1exit 0
$ aepkit negcontrol --file crm/customers.py --break-cmd "sed -i '' 's/raw.strip().lower()/raw/' crm/customers.py" --test-cmd "python -m pytest -q tests/test_orders.py" --expect "assert 6 == 4"
$ backed up crm/customers.py -> /tmp/aepkit-negcontrol-7_vhvi1l/customers.py
$ sed -i '' 's/raw.strip().lower()/raw/' crm/customers.py

$ python -m pytest -q tests/test_orders.py   # expect FAILURE matching /assert 6 == 4/
.FF                                                                      [100%]
=================================== FAILURES ===================================
____________________ test_sample_export_has_four_customers _____________________

    def test_sample_export_has_four_customers():
        book = CustomerBook()
        assert import_orders(SAMPLE, book) == 6
>       assert len(book) == 4
E       assert 6 == 4
E        +  where 6 = len(<crm.customers.CustomerBook object at 0x10a952660>)

tests/test_orders.py:16: AssertionError
__________________ test_lifetime_value_merges_email_variants ___________________

    def test_lifetime_value_merges_email_variants():
        book = CustomerBook()
        import_orders(SAMPLE, book)
>       assert book.get("ana@mail.com").lifetime_value_cents == 8190
E       AssertionError: assert 4200 == 8190
E        +  where 4200 = Customer(email='ana@mail.com', name='Ana Ruiz', order_totals_cents=[4200]).lifetime_value_cents
E        +    where Customer(email='ana@mail.com', name='Ana Ruiz', order_totals_cents=[4200]) = get('ana@mail.com')
E        +      where get = <crm.customers.CustomerBook object at 0x10a9a9450>.get

tests/test_orders.py:22: AssertionError
=========================== short test summary info ============================
FAILED tests/test_orders.py::test_sample_export_has_four_customers - assert 6...
FAILED tests/test_orders.py::test_lifetime_value_merges_email_variants - Asse...
2 failed, 1 passed in 0.02s
(exit 1)
$ restored crm/customers.py from backup (byte-identical)
$ python -m pytest -q tests/test_orders.py   # expect SUCCESS
...                                                                      [100%]
3 passed in 0.01s
(exit 0)

OK: negative control verified — the check fails without the protection and passes with it.

Green tests, rejected task

The Executor delivered the role check and two passing tests, and the Auditor’s re-run passed too. The negative control did not: with the check commented out, both tests still passed, because both log in as the owner. The Auditor rejected the task and wrote annex P1-A1.

Auditor re-runs the verify command of P1-T1exit 0
$ python -m pytest -q tests/test_export.py
..                                                                       [100%]
2 passed in 0.01s
Auditor checks the run after rejecting P1-T1exit 0
$ aepkit status docs/store-crm
Status board:
  APPROVED        3
  PENDING         1
  REJECTED        1

2 open (not APPROVED):
  - P1-T1: REJECTED (report: DONE)
  - P1-G1: PENDING

Most recent entry: P1-G1 (PENDING)

The fix proves itself

The annex names what failed, how the Auditor established it, and the exact commands that must pass. The Executor added a test that calls the export as support staff. With the check removed, that test now fails with the expected error.

Executor runs the negative control from annex P1-A1exit 0
$ aepkit negcontrol --file crm/export.py --break-cmd "sed -i '' 's/raise PermissionError/pass  # raise PermissionError/' crm/export.py" --test-cmd "python -m pytest -q tests/test_export.py" --expect PermissionError
$ backed up crm/export.py -> /tmp/aepkit-negcontrol-oxtzzhqo/export.py
$ sed -i '' 's/raise PermissionError/pass  # raise PermissionError/' crm/export.py

$ python -m pytest -q tests/test_export.py   # expect FAILURE matching /PermissionError/
..F                                                                      [100%]
=================================== FAILURES ===================================
__________________________ test_support_cannot_export __________________________

    def test_support_cannot_export():
>       with pytest.raises(PermissionError):
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       Failed: DID NOT RAISE PermissionError

tests/test_export.py:30: Failed
=========================== short test summary info ============================
FAILED tests/test_export.py::test_support_cannot_export - Failed: DID NOT RAI...
1 failed, 2 passed in 0.02s
(exit 1)
$ restored crm/export.py from backup (byte-identical)
$ python -m pytest -q tests/test_export.py   # expect SUCCESS
...                                                                      [100%]
3 passed in 0.01s
(exit 0)

OK: negative control verified — the check fails without the protection and passes with it.

Only APPROVED closes an item

A done report without a verdict shows as awaiting audit. --json prints the same reading for scripts and tools, including P2, a phase the plan declares and the guide has not broken down yet.

Auditor after closing P1-G1exit 0
$ aepkit status docs/store-crm
Status board:
  APPROVED        5

0 open (not APPROVED):

Most recent entry: P1-G1 (APPROVED)
status --json at the same point, 106 lines, scrollsexit 0
$ aepkit status --json docs/store-crm
{
  "schema": 1,
  "items": [
    {
      "id": "P0-T1",
      "phase": 0,
      "kind": "task",
      "state": "APPROVED",
      "verdict": "APPROVED",
      "report": "DONE",
      "executor": null,
      "conflict": null,
      "inLog": true
    },
    {
      "id": "P0-T2",
      "phase": 0,
      "kind": "task",
      "state": "APPROVED",
      "verdict": "APPROVED",
      "report": "DONE",
      "executor": null,
      "conflict": null,
      "inLog": true
    },
    {
      "id": "P0-G1",
      "phase": 0,
      "kind": "gate",
      "state": "APPROVED",
      "verdict": "APPROVED",
      "report": "DONE",
      "executor": null,
      "conflict": null,
      "inLog": true
    },
    {
      "id": "P1-T1",
      "phase": 1,
      "kind": "task",
      "state": "APPROVED",
      "verdict": "APPROVED",
      "report": "DONE",
      "executor": null,
      "conflict": null,
      "inLog": true
    },
    {
      "id": "P1-G1",
      "phase": 1,
      "kind": "gate",
      "state": "APPROVED",
      "verdict": "APPROVED",
      "report": "DONE",
      "executor": null,
      "conflict": null,
      "inLog": true
    }
  ],
  "phases": [
    {
      "number": 0,
      "id": "P0",
      "title": "Customer records",
      "weight": 1.0,
      "brokenDown": true,
      "items": [
        "P0-T1",
        "P0-T2",
        "P0-G1"
      ]
    },
    {
      "number": 1,
      "id": "P1",
      "title": "Export access",
      "weight": 1.0,
      "brokenDown": true,
      "items": [
        "P1-T1",
        "P1-G1"
      ]
    },
    {
      "number": 2,
      "id": "P2",
      "title": "Win-back list",
      "weight": 1.0,
      "brokenDown": false,
      "items": []
    }
  ],
  "counts": {
    "APPROVED": 5
  },
  "open": [],
  "mostRecent": {
    "id": "P1-G1",
    "state": "APPROVED"
  },
  "documents": {
    "executionGuide": true,
    "planOfRecord": true
  }
}

The run used aepkit 0.7.7 and pytest on macOS, in Guided mode. The author of this page played both roles in turn, and each Auditor re-run is its own capture. Commit history, every capture with its exit code, notes on the run.

Two roles, in two separate sessions

One person can hold both roles, in two agent sessions kept apart, so the Auditor never checks its own work.

Auditor

Writes the plan as numbered tasks with gates, decides what done means for each, and checks every delivery by running it again.

Owns
Instructions, gates, verdicts
Never
Writes feature code
Output
Task expansions, verdicts (APPROVED, CONDITIONAL, REJECTED), remediation annexes

Executor

Does one task at a time, in order, and reports with the output that shows it works. A question it cannot settle comes back as BLOCKED.

Owns
Implementation, terminal evidence
Never
Redesigns the architecture or expands scope
Output
Working code, unedited terminal output

Guided

You paste each handoff into a fresh Executor session and see every verdict. The demo run above is Guided.

Autonomous

The Auditor launches Executors as subagents with an explicit model per task, answers their BLOCKED questions in the log, and reports once per closed phase. It interrupts you only for a numbered list of critical risks.

Where it pays off: money calculations, authorization and tenant boundaries, destructive migrations, background workers nobody watches. For a script or a prototype, reading the diff is enough.

Four documents, each with one job

The instructions an Executor follows never mix with the discussion behind them, and the evidence never mixes with the instructions. The excerpts are from the demo run as it ended.

The plan of record

The outcome, the problem, three options compared, the riskiest assumption for the first phase to test, the decisions, the non-goals and the phases. Nobody implements from it.

docs/store-crm/plan-of-record.md, lines 7 to 16
**Target version / milestone:** 0.1, customer list the shop can trust
**Operating mode:** Guided

---

## 1. Why this exists (opportunity framing)

- **Target outcome:** one customer record per buyer, built from the shop's order export, with lifetime value per customer, and a customer export only the owner can run.
- **Problem / friction:** the shop's order export repeats the buyer on every order and the same person appears as `Ana@Mail.com` and `ana@mail.com `, so a spreadsheet count overstates customers and splits their spend.
- **Cost of doing nothing:** win-back emails go twice to the same person, and support staff keep copying the full customer list (with emails) into their own files.

Optional for Claude Code: the aeprogress pane

A mod that follows a run beside the chat and watches the machine while the Auditor launches Executors. It installs from the GitHub repository; there is no store listing. The protocol works without it, and the pane needs aepkit and a run.

Progress, workers and load in one pane

Overall and per-phase progress, who works on which task ID, what awaits audit and what needs attention. Workers can be Claude Code subagents, Orca workers, or any harness that keeps a workers file. It refreshes after each turn and every 20 seconds while open.

  • The machine’s load, from sysctl on macOS, /proc/loadavg on Linux, typeperf on Windows
  • On macOS, when Spotlight indexing keeps the machine busy, it recommends excluding the project folder and never changes the setting
  • No network requests, no data sent anywhere
The aeprogress pane: 43.3% overall, a bar per phase, phase P2 open with its task chips, P2-T3 in progress on a subagent, P2-T4 awaiting audit and P2-T2 conditional.

A launch limit that adapts to the machine

While a run is open, the mod refuses another Executor past 2 live workers, 1 building worker, or a machine more than 75% busy for a build, and the refusal says which limit was hit. A handoff that builds nothing carries the line Load: docs-only and only counts against the worker limit. Each limit is an option in /config.

Claude Code refuses to launch the P2-G1 Executor: 1 building worker is live (limit 1), and the machine is 91% busy (load 7.3 of 8 cores; limit 75% for a build).
Scroll sideways to read the whole screenshot.

Compaction when a phase closes

In Autonomous mode, once a gate is approved and the Auditor’s brief is on disk, the Auditor calls compact_and_resume. A 40-second countdown lets you cancel. Then the session is compacted and the Auditor resumes from its own message. Typing a prompt, pressing Esc or running /compact yourself takes over.

The countdown band above the prompt: Compacting in 33s, with Cancel and Compact now.
Scroll sideways to read the whole screenshot.

At the prompt of a Claude Code terminal session, version 2.1.287 or later. The command adds the repository as the plugin’s source and installs the protocol skill with the pane.

/plugin install auditor-executor-protocol --marketplace tBeltty/auditor-executor-protocol

Then open the pane on a run with /aeprogress docs/<task-name>. Setup, options and how each number is computed are in the plugin’s README.

These three screenshots come from the plugin’s README and show its example run, csv-export. They were not taken from the store CRM demo above.

Hand the repository to your agent

Paste this into Claude Code, Cursor, Antigravity or another coding agent, in the project where you want the protocol. It reads the README and installs aepkit and the skill.

Prompt for your agent

Install the Auditor/Executor Protocol from https://github.com/tBeltty/auditor-executor-protocol in this project. Read its README, install aepkit with pipx, then run aepkit install-skill so you can use the protocol here.

Or install it yourself

  1. Install the CLI. It needs Python 3.11 or later and nothing else.

    pipx install aepkit
  2. Copy the protocol into your agent’s skills. It detects Antigravity, Claude Code or Cursor.

    aepkit install-skill
  3. Start a run, then ask your agent to act as the Auditor for it.

    aepkit init docs/my-task