Title and rationale
A one-line title (1–300 characters) and the rationale as markdown, up to 100,000 characters. The rationale is what the next session reads when it asks why something is the way it is.
One entry per material decision — the title, the rationale, the logical date, whether it changes a public contract, and the PR and commit it shipped in — written by the agent over MCP or REST, attributed to the credential that wrote it, and linked to the build session that was open. A database trigger rejects every delete and every update except the two source links; getting one wrong means a new entry that points at the old one.
decision log
append_decision_log → 201 · linked to the open sessionrows are appended, never rewritten · the third corrects the first · illustrative
Four verbs, in the order an agent meets them. Each tool links to its row in the catalogue.
A title and a rationale in markdown, the decision’s logical date, whether it changes a public contract, how many files it touched, and — when known — the PR and commit. If the same agent has a build session open, the entry links to it and takes the project from it; otherwise pass project_id.
append_decision_log A wrong decision is not edited. A correction is a new entry that carries corrects_entry_id, lives in the corrected entry’s project, and links to the open session like any other; the original stays exactly as written.
correct_decision_log Once the PR lands, stamp source_pr_url and source_commit_sha. These two columns are the only in-place change the trigger admits; a no-op amend returns the entry unchanged and writes no audit row.
amend_decision_log_entry_source Newest first with a keyset cursor; a substring search across title and rationale; counts by day, week or month; a per-author roll-up. The same nine operations over REST, and two dashboard pages for people.
list_decision_log
In the session protocol this is step four: the agent
starts or resumes a build session, asks which rules apply, does the work, and then writes
append_decision_log once per material decision before it files its deferrals and ends the
session. Because the append happens while the session is open, the entry links to it without being told
to. In Claude Code the skills and hooks that make those calls are on the integration page. The commit that ships the work carries the session id as a trailer, and the entry — once its source is
amended — carries the commit; a reader can go from a decision to its session and to the commit that landed
it. This repository keeps its own decisions this way; the number is below. Why decisions belong in a log
and not in an end-of-session summary — decided and discussed look alike in a paragraph — is the argument of
the context-loss guide; where in the
session the append happens is the start-session and end-session guide.
Each card names the column it is made of. Every column is written once; only the two source links can be stamped later.
A one-line title (1–300 characters) and the rationale as markdown, up to 100,000 characters. The rationale is what the next session reads when it asks why something is the way it is.
The date the decision was made, defaulting to today. A past date is accepted and stamps
backfilled_at, so an imported or late-written decision sits in its historical position and is
marked as written after the fact. Listing and the period counts both order by this date.
A flag for a decision that changes a public contract, and a count of the files it touched — the two things a reviewer scans a ledger for first. The list shows the flag as a badge.
agent:<ai_account_id>, derived from the credential that made the call — never taken from
the arguments — plus the transport it came in on (api or mcp, stamped by the server) and the client name
and version the caller declared. The per-author roll-up groups on this key.
Auto-linked to the caller's open build session — the most recent active one for the same actor, within the
named project if one was given — which also supplies the project. On an append, an explicit session id can
be passed instead; without either, project_id is required.
source_pr_url and source_commit_sha, given at append time or stamped afterwards
with the amend-source tool — the only two columns the trigger lets change.
The MCP tool, the versioned REST route under /v1 and the dashboard page — all three run the same service layer, and REST and MCP serialize the decision-log aggregate through one shared serializer, tested byte-identical.
{
"method": "tools/call",
"params": {
"name": "append_decision_log",
"arguments": {
"title": "Ack and sweep predicates live in one UPDATE",
"body_markdown": "Both predicates in the same statement; the loser matches zero rows.",
"entry_date": "2026-08-18",
"has_public_contract_impact": false,
"files_touched_count": 2
}
}
} POST /v1/decision-log Authorization: Bearer pmq_…redacted… Content-Type: application/json { "title": "Ack and sweep predicates live in one UPDATE", "body_markdown": "Both predicates in the same statement; the loser matches zero rows.", "entry_date": "2026-08-18", "has_public_contract_impact": false, "files_touched_count": 2 }
app.postmq.com/decision-log newest first; filter by project; search title and rationale
app.postmq.com/decision-log/{id} the entry, its rationale, what it corrects, its session {
"method": "tools/call",
"params": {
"name": "correct_decision_log",
"arguments": {
"corrects_entry_id": "01K…",
"title": "Ack and sweep predicates live in one UPDATE — and the state guard is load-bearing",
"body_markdown": "The state = 'in_flight' guard on both UPDATEs is what keeps the loser at zero rows.",
"entry_date": "2026-08-18"
}
}
} POST /v1/decision-log/01K…/correct Authorization: Bearer pmq_…redacted… Content-Type: application/json { "title": "Ack and sweep predicates live in one UPDATE — and the state guard is load-bearing", "body_markdown": "The state = 'in_flight' guard on both UPDATEs is what keeps the loser at zero rows.", "entry_date": "2026-08-18" }
app.postmq.com/decision-log the correction carries a Correction badge
app.postmq.com/decision-log/{id} Corrects → links to the entry it supersedes {
"method": "tools/call",
"params": {
"name": "amend_decision_log_entry_source",
"arguments": {
"decision_log_entry_id": "01K…",
"source_pr_url": "https://github.com/…/pull/519",
"source_commit_sha": "a4f114c"
}
}
} POST /v1/decision-log/01K…/amend-source Authorization: Bearer pmq_…redacted… Content-Type: application/json { "source_pr_url": "https://github.com/…/pull/519", "source_commit_sha": "a4f114c" }
app.postmq.com/decision-log/{id} Amend shipping source — the one form on the page {
"method": "tools/call",
"params": {
"name": "query_decisions",
"arguments": {
"q": "lease sweep",
"limit": 20
}
}
} GET /v1/decision-log/search?q=lease+sweep&limit=20 Authorization: Bearer pmq_…redacted…
app.postmq.com/decision-log Search title / rationale — the same substring match
Append-only: an entry is never deleted and, apart from its two source links, never edited; a correction is a new entry that points at the one it supersedes. PostMQ speaks standard MCP, so any MCP-capable client can call them; each name links to its row in the catalogue.
| tool | what it does | needs |
|---|---|---|
| append_decision_log | Append a material engineering decision to the append-only decision log. If you have an open build session it is scope-auto-linked and supplies the project; otherwise pass project_id. entry_date defaults to today. Requires the write_session_state scope. required: title, body_markdown | write_session_state |
| correct_decision_log | Correct a prior decision by appending a NEW entry that supersedes it (the original is never edited). The correction inherits the corrected entry's project. Requires the write_session_state scope. required: corrects_entry_id, title, body_markdown | write_session_state |
| amend_decision_log_entry_source | Stamp the shipping source_pr_url / source_commit_sha on an existing decision-log entry — the only in-place update the append-only ledger permits. At least one field is required. Requires the write_session_state scope. required: decision_log_entry_id | write_session_state |
| get_decision_log_entry | Fetch a single decision-log entry in your workspace by its decision_log_entry_id. Open to any authenticated caller. required: decision_log_entry_id | authenticated |
| get_decision_log_entry_summary | Fetch the lightweight summary (no rationale body) of a decision-log entry by id. Open to any authenticated caller. required: decision_log_entry_id | authenticated |
| list_decision_log | List decision-log entries newest-first (keyset-paged). Pass the previous page's next_cursor to continue. Optionally scope to one project. Open to any authenticated caller. | authenticated |
| query_decisions | Search your workspace's decision log by a substring across title and body. Ranked by recency, capped at 50. Returns a lightweight summary shape. Open to any authenticated caller. required: q | authenticated |
| count_decisions_by_period | Count decision-log entries bucketed by period (day, week, or month) for an activity view. Optionally scope to a project and an inclusive from/to date range. Open to any authenticated caller. | authenticated |
| summarize_decisions_by_author | Roll up decision-log entry counts per authoring actor (with each author's latest decision date). Optionally scope to a project and an inclusive from/to date range. Open to any authenticated caller. | authenticated |
Under the versioned /v1 API. Writes carry write_session_state; reads carry the default authenticated policy.
| route | what it does | needs |
|---|---|---|
POST /v1/decision-log | append an entry (auto-links the open build session) | write_session_state |
POST /v1/decision-log/{id}/correct | append a correction that supersedes {id} | write_session_state |
POST /v1/decision-log/{id}/amend-source | stamp source_pr_url / source_commit_sha on {id} | write_session_state |
GET /v1/decision-log | list newest first, keyset cursor, optional project filter | authenticated |
GET /v1/decision-log/search | substring search over title and rationale, ranked by recency | authenticated |
GET /v1/decision-log/count-by-period | entry counts bucketed by day, week or month of entry_date | authenticated |
GET /v1/decision-log/authors | entries per author, with each author’s latest decision date | authenticated |
GET /v1/decision-log/{id} | one entry, rationale included | authenticated |
GET /v1/decision-log/{id}/summary | one entry without its rationale | authenticated |
app.postmq.com/decision-log lists the workspace's entries newest first — date, title, a
Public contract badge, a Correction badge, the author key — with a project filter and a
search over title and rationale that is the same substring match the tool runs; Load more walks the same
keyset cursor. app.postmq.com/decision-log/{id} shows the fields, the rationale, a link to the
entry it corrects and to the build session it was made in, and the one form on the page: amend the shipping
source. The overview page lists recent decisions. Two honest limits: the reverse link — from an entry to the
corrections that supersede it — is not surfaced, and there is no author filter, only the author column and
the per-author roll-up.
Every append, correction and source amendment is also a row on the workspace's audit chain — three of the 38 session-state lifecycle event types written into the same tamper-evident SHA-256 chain as messaging, 38 of 123 event types in all (high-frequency touches such as pings and usage reports are deliberately not audited). A workspace can browse that chain in the dashboard and read it over REST and MCP, with the operator-significant families withheld; the mechanism is on the security page.
A mechanism per row; the badge says whether it ships on, ships off, or is roadmap. Every row here ships on — nothing in the decision log is optional or default-off.
| what holds | how | scope |
|---|---|---|
| Entries cannot be edited or deletedC07 | tg_decision_log_entries_append_only rejects every DELETE and every UPDATE that assigns any column other than source_pr_url / source_commit_sha — enforced in the database, beneath the service layer | shipped |
| A correction is a new entryC08 | corrects_entry_id points at the superseded entry (a database CHECK stops an entry correcting itself); the correction inherits its project; the original is never touched — the list marks it with a badge and the detail page links it to the original | shipped |
| Only the two source links change in placeC60 | amend_decision_log_entry_source, read under an update lock so concurrent amends serialize; a no-op amend returns unchanged and writes no audit row | shipped |
| An append links to the open sessionC58 | the caller’s most recent active build session for the same actor (within the named project, if one was given) is auto-linked and supplies the project; without one, project_id is required | shipped |
| Every entry is attributedC59 | authored_by_actor_key = agent:<ai_account_id>, derived from the credential and never from the arguments; the transport (api or mcp) stamped by the server, client name and version as declared; the per-author roll-up groups on this key | shipped |
| Three events on the audit chainC66 | decision_log.appended, decision_log.corrected, decision_log.source_amended — each written into the workspace’s tamper-evident SHA-256 chain in the same transaction as the row | shipped |
| Writes need one scope; reads need only authenticationC65 | append, correct and amend-source require write_session_state (operational tier) over REST and MCP alike; the six reads require an authenticated caller in the workspace | shipped |
| Listing is keyset-paged; search is a substring matchC62 | newest first over (entry_date, id) with an opaque cursor; query_decisions is a SQL LIKE over title and body, ranked by recency, capped at 50 — not full-text search | shipped |
| Counts bucket by the entry’s dateC63 | count_decisions_by_period groups on entry_date (day, week or month, inclusive from/to); a ledger total comes from paging list_decision_log to a short final page, which is how the number below is produced | shipped |
| REST and MCP return the same bytesC05 | one serializer for the decision-log aggregate, pinned by a byte-parity test class covering append, get, summary, list, search, count and authors | shipped |
| The dashboard reads; it amends nothing but the sourceC64the reverse link — from an entry to the corrections that supersede it — is not surfaced; there is no author filter, only an author column and the per-author roll-up | /decision-log lists newest first with a project filter and a title-and-rationale search; /decision-log/{id} shows the fields, the rationale, the entry it corrects and its session, plus the amend-source form | shipped |
| A markdown decision log importsC20 | import_session_state_from_markdown parses the SpecStep markdown convention: preview first, commit once; already-imported entries are skipped by a fingerprint of their source block | shipped |
A decision log you already keep in markdown comes with you: import_session_state_from_markdown
previews the corpus as a dry run and commits it on the second call, and entries already imported are skipped
by a fingerprint of their source block, so re-running is safe. The parser is the SpecStep markdown
convention; the details are on the session-state page.
Why the trigger rather than a convention: an append-only log is the one artefact that answers “who decided this, and could the answer have been changed afterwards?” without asking anyone to be trusted. That argument, and the honest boundary around it, is on the AI coding governance page.
This is also the ledger people most often ask about after reading their coding client’s memory documentation, so the difference is set out row by row, with a public source on both sides of each row, on PostMQ and Claude Code auto memory — which recommends running both, and says where the client’s own memory is the whole answer.
As of 2026-09-02 — this project's own decision log, counted at build by paging list_decision_log (4 pages, terminated by a short final page); never taken from the period counter.
We run PostMQ on PostMQ. Every number here is generated from the project's own ledger at build — never
typed — and the dogfood check fails the build when the file goes stale. The count is produced by walking
list_decision_log to a short final page rather than by summing
count_decisions_by_period: the counter buckets by each entry's logical date and is an activity
view, not a ledger total. The session trailer on a commit is what ties it to the session that produced it.
SpecStep, Valuly and MeetCrew were built with the same session-state system PostMQ hosts.
Last verified 2026-08-18 against main at a4f114c: the trigger, the
service, the nine tools, the nine routes and the two dashboard pages this page names were read on that day.
Related: session state — the protocol this log is step four of · the decision-log tools in the catalogue · the audit chain every write lands on · connect an agent · pricing — the decision log is unlimited on every tier.
Create a workspace, connect your agent, call append_decision_log.