Decision left no record
The failure
Section titled “The failure”A situation that should have produced a record — a decision made, a memory worth keeping — produces nothing. Nobody notices the absence, because there is nothing there to notice. The record that should exist and the record that was never made look identical from the outside: empty. The cost lands later, when the reasoning behind a choice is needed and was never written down.
How it works
Section titled “How it works”The trigger and the record are tied together: when a situation calls for a record, the turn must declare it and produce the artifact. Two gates enforce the two halves — that a record was declared at all, and that the declared record actually landed.
-
A context wakes on the declaring tag or a touch under the record’s paths, and accumulates what showed up, so the gates can read it back at
Stop. -
A structure file-guard holds the record files to their allowed layout, so a record can only land where it belongs.
-
Two gates at
Stop: one refuses when the situation called for a record and none was declared; the other refuses when a record was declared but the artifact never landed.
A context can only track; the gates are what refuse the turn.
The actual configuration
Section titled “The actual configuration”This is the real example shipped at
examples/completeness-artifact-on-trigger/. Three natures under .sloprail/ —
each a *.yaml declaration plus the scripts it names:
Directory.sloprail/
Directorycontext/
Directorytag-declared/
- context.yaml
- enter.sh
- exit.sh
Directoryfile-guard/
- structure.yaml
Directorygate/
Directorytag-required/
- gate.yaml
- refuse.sh
Directoryverify-artifact-produced/
- gate.yaml
- verify-tag-and-artifact.sh
context
Section titled “context”Wakes on the declaring tag or a touch under the record paths, and logs what
appeared into its registry so both gates can read it at Stop.
# Wakes on either a #update/#decision/#skip tag or a file touch under the# artifact paths. Each occurrence logs what showed up into the registry# (sr-session state), so the paired Stop gate can read back which tag was# declared and which artifacts landed — even across separate tool calls.on: - event: PostTagWrite match: any(event.tags, .label == "update" or .label == "decision" or .label == "skip") - event: PostFileCreate match: event.path startsWith "memories/updates/" or event.path startsWith "memories/decisions/" - event: PostFileUpdate match: event.path startsWith "memories/updates/" or event.path startsWith "memories/decisions/"enter: ./enter.shexit: ./exit.sh#!/usr/bin/env bash# enter: logs whichever fired (tag or artifact touch) into the registry# (sr-session state) rather than into `payload`, so a tag and an artifact landing# in different tool calls both get recorded.set -uo pipefail
input="$(cat)"kind="$(printf '%s' "$input" | jq -r '.event.kind // ""')"
case "$kind" in PostTagWrite) tags="$(printf '%s' "$input" | jq -r '.event.tags[]?.label // empty')" while IFS= read -r tag; do [ -z "$tag" ] && continue sr-session state set "tag:$tag" "declared" done <<< "$tags" ;; PostFileCreate|PostFileUpdate) path="$(printf '%s' "$input" | jq -r '.event.path // ""')" [ -n "$path" ] && sr-session state set "artifact:$path" "$kind" ;;esac
jq -n '{active_since: "trajectory"}'#!/usr/bin/env bash# exit: thin, reads the paired gate's verdict (verify-artifact-produced).set -uo pipefail
input="$(cat)"status="$(printf '%s' "$input" | jq -r '.gates["verify-artifact-produced"].status // "fail"' 2>/dev/null)"
if [ "$status" = "pass" ]; then exit 0fi
exit 1file-guard
Section titled “file-guard”A structure file-guard: an allow-list of the paths a record may occupy. Anything outside it is disallowed.
allow: - glob: "memories/updates/*.md" - regex: "^memories/decisions/[0-9]{8}_[a-z0-9-]+/.*\\.md$"Two gates. One refuses the stop when no record was declared for a situation that needed one; the other refuses when a record was declared but its artifact never landed.
# tag-declared activates only once a tag was written, so `not ...active` is# exactly "no tag declared". No `require`: match reads the context's current# state fresh each Stop, so nothing has to run first.on: - event: Stop match: not context["tag-declared"].activechecks: - script: ./refuse.sh#!/usr/bin/env bash# match already confirmed no tag was declared — just refuse with the remedy.echo "This turn declared no tag (#update, #decision, or #skip) — declare one before the turn can end." >&2exit 1# Runs only once tag-declared is active, reading the registry it accumulated.on: - event: Stop match: context["tag-declared"].activerequire: - context: tag-declaredchecks: - script: ./verify-tag-and-artifact.sh#!/usr/bin/env bash# Reads tag-declared's registry: which tags were declared, and how many artifacts# landed. #skip needs no artifact; any other tag must have a matching artifact.# The no-tag-at-all case is the sibling tag-required gate's job, not this one's.## `require: [{context: tag-declared}]` guarantees the context ran first, so the# entries are current. `state list` emits JSON-LINES, so reads slurp with `jq -s`.set -uo pipefail
entries="$(sr-session state list --owner tag-declared 2>/dev/null)"
tags="$(printf '%s' "$entries" | jq -s -r '[.[] | select(.key | startswith("tag:"))] | .[].key | ltrimstr("tag:")')"artifacts="$(printf '%s' "$entries" | jq -s -r '[.[] | select(.key | startswith("artifact:"))] | length')"
if echo "$tags" | grep -qx "skip"; then exit 0fi
if [ "${artifacts:-0}" -eq 0 ]; then echo "Turn declared a tag ($tags) but no matching artifact was produced this turn — the tag was stated, the artifact was not." >&2 exit 1fi
exit 0