DEV Community

hao li
hao li

Posted on

dlog v0.2: two gates for the decision log — one I can enforce, one I can't

A few weeks ago I published dlog, a tiny append-only decision log for coding-agent sessions: dlog add "chose X" -r "because Y", one JSONL line per decision.

The best feedback came from reader loganilands, across four rounds of discussion. They'd run honesty experiments on frontier models: asked plainly, models answered "I don't know" correctly 0 out of 8 times; asked with leading questions, they confidently fabricated 8 out of 8. Their summary stuck with me: "Empty, verified, wrong."

Their point: read-time verification only protects what's already written down. The real loss happens at write time — the decision that never gets recorded at all. And they were right that the git-sha anchor had to be checked at read time, forcibly, not as an optional flag.

v0.2 ships both gates.

Gate 1 — read-time verify (enforced). dlog verify re-resolves every entry's git anchor against the current repo. An entry whose commit no longer resolves — tampered log line, rewritten history — doesn't get a red warning. It disappears. Fail-closed: if the tool can't confirm it, the tool doesn't show it.

$ dlog verify
10-11 08:30  chose sqlite over postgres [db, infra]
verify: 1 kept, 1 dropped (anchor commit not found)
Enter fullscreen mode Exit fullscreen mode

Gate 2 — write-time gate (a protocol). dlog add now prints an entry id. dlog gate --check <id> exits 0 iff that decision was recorded, 2 otherwise. dlog assert-logged bulk-checks ids from args or stdin. The intended use is in the agent loop:

ID=$(dlog add "chose sqlite" -r "zero deps" | grep -oE '[0-9a-f]{12}' | tail -1)
# ... do the work ...
dlog gate --check "$ID" || exit 1   # refuse to act on what was never recorded
Enter fullscreen mode Exit fullscreen mode

Now the honest part, because loganilands would call it out if I didn't:

  • verify proves the anchor commit still exists. It does not prove the entry text is unchanged — JSONL is a plain file; text can be rewritten around a valid sha. Anchor tampering is the cheap, common attack; that's what we catch.
  • The write-time gate is a protocol, not physics. Nothing in dlog can force an agent to call gate --check. It binds cooperative loops; it can't bind an agent that skips the call. I'm shipping the mechanism and the tests, not a claim of enforcement.
  • Entries recorded outside a git repo have no anchor; verify shows them as-is and says so.

"Empty, verified, wrong" is still possible — an empty log verifies trivially. The read-time gate is the one I can enforce; the write-time gate is the one that matters. Two gates, and I'm telling you which is which instead of pretending one mechanism covers both.

Top comments (1)

Collapse
 
pm25coder profile image
pm25coder •

Ran both gates against a scratch repo (Python 3.13, dlog.py 12335 chars at HEAD b42b9029d667). Three readings that aren't in the "Honest limits" section, then the one that is.

verify exits 0 whether it keeps or drops. I corrupted two anchors: verify dropped both, printed verify: 0 kept, 2 dropped (anchor commit not found) to stderr, and returned 0. So dlog verify || exit 1 never fires — the read-time gate is enforced in what it prints, not in what it exits, and $? is the signal a loop or CI actually branches on. The write-time gate does exit 2. The asymmetry runs backwards from the naming: the gate you call "enforced" is the one with no failure signal.

When every entry is dropped, stdout is the empty-log message. All anchors bad -> stdout is no decisions logged yet — use `dlog add "title" -r "reason"` to record one. A fully-failed verify is byte-identical on stdout to a log that was never written; only the stderr summary separates them. That's the read-time mirror of your own "empty, verified, wrong": tampered reads as empty.

The parenthetical fires unconditionally. Two clean entries, nothing dropped -> verify: 2 kept, 0 dropped (anchor commit not found). The cause is named even when nothing failed, which is how a line teaches readers to skip it.

Confirming the limit you do document: I rewrote title + reason and kept the valid sha -> still kept and shown. If you'd rather verify catch that than rely on committing the log, stamp a content hash at add and check it in verify:

entry["h"] = hashlib.sha256("|".join(
    str(entry[k]) for k in ("id", "ts", "title", "reason")
).encode()).hexdigest()[:16]
Enter fullscreen mode Exit fullscreen mode
elif _anchor_resolves(sha, repo) and (
        e.get("h") is None or e.get("h") == _entry_hash(e)
):
    kept.append(e)
Enter fullscreen mode Exit fullscreen mode

Measured on the same tree: honest entries still show (2 kept, 0 dropped); a text-only rewrite now drops (1 kept, 1 dropped). Six lines, no new deps, and it works whether or not the log is committed.

The "read-time is the one you can enforce, write-time is the one that matters" split is the right axis — it's just that the exit code doesn't carry the read-time verdict yet.