# How the Swarm-Forge Two-Call Audit Gate Invalidates and Resets on Changes

> Discover how the Swarm-Forge two-call audit gate invalidates pending audits on handoff changes by deleting mismatched files. Learn how it resets for a fresh audit.

- Repository: [Robert C. Martin/swarm-forge](https://github.com/unclebob/swarm-forge)
- Tags: internals
- Published: 2026-08-30

---

**The Swarm-Forge two-call audit gate invalidates pending audits when any handoff details change by deleting mismatched fingerprint files, then requires a fresh audit with incremented counter on the next call.**

The two-call audit gate in unclebob/swarm-forge ensures every Git handoff undergoes deliberate human review before queuing. This security mechanism relies on fingerprint comparison and explicit invalidation logic to prevent accidental or unauthorized task progression.

## How the Two-Call Audit Gate Works

### The First Call: Audit Request

When you invoke [`swarm_handoff.sh`](https://github.com/unclebob/swarm-forge/blob/main/swarm_handoff.sh) with a draft handoff, the script parses and validates the input but **does not queue anything**. Instead, it performs three critical actions (lines 60-73 in `swarmforge/scripts/swarm_handoff.bb`):

- Prints `AUDIT_REQUIRED` to stdout
- Increments the task's `audit_count` via `increment-audit-count!`
- Writes a provisional audit record to `.swarmforge/handoffs/audit_pending/<sender>/<sha256-task-id>.edn`

The handoff stops here. The sender must manually re-read the complete task requirements and invoke the command again.

### The Second Call: Unchanged Handoff Queued

If the second invocation produces an **identical fingerprint** to the pending audit, `submit-after-audit!` (lines 60-73) queues the handoff and deletes the audit files. The `audit_count` does not increment again—the audit gate has been satisfied.

## Invalidation Trigger: Any Change Resets the Gate

The audit gate invalidates when **any** handoff component differs from the pending audit. Modifications that trigger reset include:

- Different commit hash or base-commit
- Changed recipient list
- Altered task name or identifier
- Modified draft content or priority
- Updated non-forwarding flag

### The Invalidation Mechanism

The `invalidate-changed-invocation-audits!` function (lines 311-318) handles cleanup with exclusive locking:

```clojure
;; Pseudocode representation of the invalidation logic
(defn invalidate-changed-invocation-audits! [sender current-fingerprint]
  (with-exclusive-lock
    (doseq [audit-file (pending-audits-for sender)]
      (when (not= (:candidate (read-edn audit-file)) current-fingerprint)
        (delete! audit-file)))
    (remove-empty-directories! sender)))

```

After invalidation, the next [`swarm_handoff.sh`](https://github.com/unclebob/swarm-forge/blob/main/swarm_handoff.sh) call creates a **fresh audit**, increments `audit_count` again, and prints `AUDIT_REQUIRED`.

## Fingerprint Generation and Comparison

The `invocation-fingerprint` function (lines 99-109) captures every handoff detail:

```clojure
(defn invocation-fingerprint
  "Returns a hash-map of all handoff characteristics for audit comparison."
  [sender task-id type recipients priority task commit base-commit non-forwarding? draft-content]
  {:sender sender
   :task-id task-id
   :type type
   :recipients recipients
   :priority priority
   :task task
   :commit commit
   :base-commit base-commit
   :non-forwarding? non-forwarding?
   :draft-hash (sha256 draft-content)})

```

Two fingerprints must match exactly for the second call to proceed.

## Practical Example: Complete Two-Call Flow

```bash

# First call – audit requested, counter increments

$ swarm_handoff.sh feature-handoff.edn
AUDIT_REQUIRED
HANDOFF_NOT_QUEUED
TASK_ID: HTW-2847
COMMIT: a1b2c3d4e5f6

# Inspect pending audit (optional verification)

$ ls .swarmforge/handoffs/audit_pending/alice/
a3f7...9e2d.edn

# Second call – unchanged draft, handoff queues

$ swarm_handoff.sh feature-handoff.edn
HANDOFF_QUEUED
TASK_ID: HTW-2847

```

## Example: Change Invalidates Pending Audit

```bash

# Modify the draft before second call

$ sed -i 's/commit: a1b2c3d4e5f6/commit: deadbeef1234/' feature-handoff.edn

# New call – previous audit invalidated, fresh audit required

$ swarm_handoff.sh feature-handoff.edn
AUDIT_REQUIRED
HANDOFF_NOT_QUEUED
TASK_ID: HTW-2847
COMMIT: deadbeef1234

# audit_count incremented again

```

## Source Code Locations

| Component | Location | Lines |
|-----------|----------|-------|
| Audit gate documentation | [`README.md`](https://github.com/unclebob/swarm-forge/blob/main/README.md) | 277-282 |
| Fingerprint generation | `swarmforge/scripts/swarm_handoff.bb` | 99-109 |
| Invalidation logic | `swarmforge/scripts/swarm_handoff.bb` | 311-318 |
| Submit-after-audit orchestration | `swarmforge/scripts/swarm_handoff.bb` | 60-73 |
| Test coverage | `test/swarmforge/handoff_test.clj` | (throughout) |

## Summary

- **First call** creates pending audit, prints `AUDIT_REQUIRED`, increments `audit_count`
- **Second unchanged call** matches fingerprint, queues handoff, clears audit files
- **Any change** triggers `invalidate-changed-invocation-audits!` to remove stale audits
- **Changed call** starts fresh audit cycle with new counter increment
- **Fingerprint** covers all handoff metadata including draft content hash

## Frequently Asked Questions

### What exactly triggers audit invalidation in Swarm-Forge?

Any modification to the handoff draft invalidates the pending audit. This includes commit hash changes, recipient list updates, task identifier edits, priority adjustments, or even whitespace differences in the draft content that alter the SHA-256 hash computed in `invocation-fingerprint` (lines 99-109).

### Where are pending audit files stored between calls?

Pending audits live at `.swarmforge/handoffs/audit_pending/<sender>/<sha256-task-id>.edn`. The directory structure organizes by sender to enable efficient cleanup in `invalidate-changed-invocation-audits!` (lines 311-318), which iterates only that sender's pending files.

### Does the audit_count increment on every call or only audit requests?

The `audit_count` increments **only when a new audit is created**, not on every call. The first call to an unaudited handoff increments it. A matching second call does not. A changed call that triggers invalidation and fresh audit increments it again.