feat(sweep): close the aged FYI backlog without a human in the loop

A notice needs no answer, but it is still re-injected at every session
start until someone closes it by hand. Across enough repositories that
hand-closing IS the manual work the mailbox was meant to remove, and the
pending count -- the operator's only signal -- drowns in messages that
were never going to be acted on: 9 of 22 pending messages across 12
mailboxes were pure notices when this was written.

coord-sweep.sh closes exactly one mechanically decidable class:
reply-expected: no, older than a grace window (default 14 days). A
message that owes a reply is never touched, at any age, with any flag --
answering it would mean deciding something on the receiving repo's
behalf, which is the one thing this system exists to prevent.

Four properties are load-bearing, not incidental:

- Dry-run is the default, inverted from the rest of the engine. The
  others print or deliver; this one destroys pending state, so the safe
  direction has to be what you get by forgetting a flag.
- Closing goes through coord-done.sh --repo, never mv, so the archive
  layout and the _broadcast refusal stay in one place.
- Age is read from the filename prefix, never the file. An unreadable
  age is never treated as old: fail-safe, not fail-open.
- Every closure is logged with sender and subject. Directed messages
  have no seen-tracking, so the sweep cannot tell "seen and ignored"
  from "never delivered" -- a notice can be closed unread, and the log
  is the only thing standing between that and silent data loss.

The reply-expected read is bounded to the frontmatter block, matching
coord-count.sh: a body line claiming it at column 0 is untrusted
cross-repo input and must not close its own message.

No scheduler, no launchd unit, no skill front door -- the script does
nothing until invoked.

Selftest 159 -> 182. The log check caught a real defect during
development: the first implementation read from/subject AFTER
coord-done.sh had moved the file, logging empty values and quietly
defeating the only safeguard the design has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uwcak9j4m9JijDKeFmptww
This commit is contained in:
Kjell Tore Guttormsen 2026-08-01 22:00:42 +02:00
commit 459c9feec0
10 changed files with 362 additions and 11 deletions

View file

@ -1,6 +1,6 @@
{
"name": "repo-mailbox",
"version": "0.13.0",
"version": "0.14.0",
"description": "Local mailbox for coordination between Claude Code sessions in different repositories. Directed messages and broadcasts as plain Markdown files on your own disk, injected as context at session start. Local, private, no network.",
"author": {
"name": "Kjell Tore Guttormsen"

View file

@ -5,6 +5,58 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.14.0] - 2026-08-01
### Added
- **`scripts/coord-sweep.sh` — the FYI backlog closes itself, deterministically
and without a model.** A notice needs no answer, but it is still re-injected at
every session start until someone closes it by hand. Across enough
repositories that hand-closing *is* the manual work the mailbox was supposed to
remove, and the pending count — the operator's only signal — drowns in messages
that were never going to be acted on. At the time of writing, 9 of 22 pending
messages across 12 mailboxes were pure notices.
The sweep closes exactly one mechanically decidable class: a directed message
whose sender declared `reply-expected: no`, older than a grace window (default
14 days, `--days`). **A message that owes a reply is never touched, at any age,
with any flag** — answering it would mean deciding something on the receiving
repo's behalf, which is the one thing this system exists to prevent a session
from doing.
Four properties are load-bearing rather than incidental:
- **Dry-run is the default**, inverted from every other script here. The others
print or deliver; this one destroys pending state, so the safe direction has
to be the one you get by forgetting a flag.
- **Closing goes through `coord-done.sh --repo`, never `mv`.** The engine owns
the filename grammar and the archive layout, and it already refuses
`_broadcast` on its own — archiving out of there would retire an announcement
for every repo that has not read it yet, an unauthenticated retract.
- **Age is read from the filename prefix, never from the file.** A name the
grammar does not produce has no readable age, and an unreadable age is never
treated as old: fail-safe, not fail-open.
- **Every closure is logged** (`$CLAUDE_COORD_DIR/_sweep.log` by default) with
sender and subject. This is not bookkeeping. A directed message has no
seen-tracking — only broadcasts do — so the sweep cannot distinguish "seen and
ignored" from "never delivered", and a notice to a repo left unopened for the
whole window is closed **unread**. That is an accepted tradeoff; the log is
the only thing standing between it and a silent disappearance.
The `reply-expected` read is bounded to the frontmatter block, matching
`coord-count.sh`: a body line claiming `reply-expected: no` at column 0 is
untrusted cross-repo input and must not be able to close its own message. That
is the one place where getting it wrong would let another repo delete its way
out of your inbox.
Scoped deliberately: no scheduler, no launchd unit, no skill front door. The
script does nothing until invoked.
- Selftest 159 → 182 checks (new section 29). The log check caught a real defect
during development — the first implementation read `from`/`subject` *after*
`coord-done.sh` had already moved the file, logging empty values and quietly
defeating the only safeguard the design has.
## [0.13.0] - 2026-07-31
### Added

View file

@ -13,9 +13,24 @@ marketplace plugin. Three components, one boundary:
- **Engine (`scripts/*.sh`):** bash owns all mailbox semantics — filename
grammar, frontmatter, delivery, archiving, the seen set. `coord-send.sh`
writes, `coord-inbox.sh` reads (formatted for context injection),
`coord-done.sh` archives, `coord-count.sh` counts without delivering.
`coord-done.sh` archives, `coord-count.sh` counts without delivering,
`coord-sweep.sh` closes the aged FYI backlog machine-wide.
Everything is pinned by `coord-selftest.sh`
(159 checks, throwaway mailbox via `CLAUDE_COORD_DIR`).
(182 checks, throwaway mailbox via `CLAUDE_COORD_DIR`).
**`coord-sweep.sh` is the only path that closes a message with no human in
the loop, and every constraint on it follows from that.** It may close exactly
one mechanically decidable class - `reply-expected: no`, older than the grace
window - because a message that owes a reply can only be answered by a session
in the repo that owes it. Dry-run is the default, inverted from the rest of the
engine, since this is the one script that destroys pending state. It closes
through `coord-done.sh --repo` rather than moving files, so the archive layout
and the `_broadcast` refusal stay in one place. And it logs every closure with
sender and subject, because directed messages have no seen-tracking: the sweep
genuinely cannot tell "seen and ignored" from "never delivered", so a notice
can be closed unread and the log is the only record that it existed. Widening
the class, defaulting to `--write`, or dropping the log each independently
turn this from a bounded cleanup into silent data loss.
**`coord-count.sh` prints TWO integers per mailbox** (`<name>\t<pending>\t<debt>`),
and the first must stay pending: `board.sh` counts the same inbox files
@ -153,7 +168,7 @@ obligations in another repo.
- Zero dependencies everywhere: bash + coreutils in the engine, `node:`
builtins only in hook and tests.
- TDD: no behavior change without a failing selftest check first.
`bash scripts/coord-selftest.sh` must exit 0 (159/159),
`bash scripts/coord-selftest.sh` must exit 0 (182/182),
`bash scripts/board-selftest.sh` must exit 0 (36/36) and
`bash scripts/route-selftest.sh` must exit 0 (73/73).
- English for all code, docs, and commit messages (public repo). Norwegian
@ -168,6 +183,9 @@ obligations in another repo.
- Board smoke test: `bash scripts/board.sh` (read-only, ~3s over the real tree)
- Route smoke test: `bash scripts/route.sh --path known --verification strong
--reversibility cheap --scope local --rationale x` (writes nothing, instant)
- Sweep smoke test: `bash scripts/coord-sweep.sh` (dry-run is the default, so
this writes nothing; never add `--write` to a smoke test against the real
mailbox)
## Release

View file

@ -8,7 +8,7 @@
*AI-generated: all code produced by Claude Code through dialog-driven development.*
![Version](https://img.shields.io/badge/version-0.13.0-blue)
![Version](https://img.shields.io/badge/version-0.14.0-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Hooks](https://img.shields.io/badge/hooks-1-green)
![Skills](https://img.shields.io/badge/skills-3-orange)
@ -93,12 +93,15 @@ Scoring is judgement and belongs to the skill; turning scores into a row is a lo
coord-inbox.sh [--repo <name>] # print pending (what the hook injects)
coord-done.sh <filename>... | --all # archive without replying
coord-count.sh [--exclude <mailbox>] # per mailbox: pending + replies owed, delivering nothing
coord-sweep.sh [--write] [--days <n>] [--log <path>] # close aged notices machine-wide (dry-run by default)
board.sh [--roots <dir>[,<dir>...]] # cross-repo attention board (read-only)
route.sh --path <v> --verification <v> --reversibility <v> \
--scope <v> --rationale "<why>" # model + effort for the next session
The reply/resolve hints the hook injects (`-> reply: coord-send --reply-to … | done without reply: coord-done …`) refer to these scripts.
**`coord-sweep.sh` is the only script that closes a message without a human in the loop**, and it is bounded to one mechanically decidable class: a directed message whose sender declared `reply-expected: no`, older than a grace window (default 14 days). A message that owes a reply is never touched, at any age, with any flag — answering it would mean deciding something on the receiving repo's behalf. Dry-run is the default, inverted from every other script here, because this is the one that destroys pending state. Every closure appends a line naming the sender and subject: a directed message has no seen-tracking, so the sweep cannot tell "seen and ignored" from "never delivered", and a notice to a repo left unopened for the whole window is closed *unread*. The log is what keeps that from being silent.
## Security Model
Cross-repo message content is untrusted input by design:
@ -110,7 +113,7 @@ Cross-repo message content is untrusted input by design:
- **Atomic delivery:** the temp file is created inside the destination directory (dot-prefixed, invisible to the inbox glob), so the final rename never crosses filesystems and readers never observe a half-written message.
Every guarantee above is pinned by the 159-check selftest, including forgery-resistance regressions.
Every guarantee above is pinned by the 182-check selftest, including forgery-resistance regressions.
Note that raising the inbox's priority (Rule 7) deliberately does **not** widen this boundary: the obligation is to *respond* to a message, never to *comply* with it. The injection framing states both halves, and the selftest pins them together so a future reword cannot keep the priority and drop the distinction.
@ -132,7 +135,7 @@ Note that raising the inbox's priority (Rule 7) deliberately does **not** widen
## Development
bash scripts/coord-selftest.sh # 159 checks against a throwaway mailbox
bash scripts/coord-selftest.sh # 182 checks against a throwaway mailbox
bash scripts/board-selftest.sh # 36 checks against a throwaway repo tree
bash scripts/route-selftest.sh # 73 checks, incl. the route->board round trip
npm test # all three selftests via node --test

View file

@ -1,6 +1,6 @@
{
"name": "repo-mailbox",
"version": "0.13.0",
"version": "0.14.0",
"private": true,
"type": "module",
"engines": {

View file

@ -10,6 +10,7 @@ SEND="$DIR/coord-send.sh"
INBOX="$DIR/coord-inbox.sh"
DONE="$DIR/coord-done.sh"
COUNT="$DIR/coord-count.sh"
SWEEP="$DIR/coord-sweep.sh"
CLAUDE_COORD_DIR="$(mktemp -d)"
export CLAUDE_COORD_DIR
@ -702,6 +703,116 @@ check "coord-send SKILL.md's engine line has no ~/.claude fallback" $?
if grep -q 'CSEND=.*CLAUDE_PLUGIN_ROOT:-' "$CSKILL"; then rc=1; else rc=0; fi
check "coord-send SKILL.md's CSEND assignment carries no fallback" "$rc"
# 29. coord-sweep.sh - the FOURTH script that acts on pending messages, and the
# only one that closes without a human in the loop. Everything here exists to
# bound that: it may close exactly one mechanically decidable class (a notice
# whose sender declared reply-expected: no) after a grace window, it goes
# through coord-done.sh rather than moving files itself, and it logs every
# closure because a directed message has no seen-tracking - so a notice to a
# repo left unopened for the whole window is closed UNREAD, and the log is the
# only thing standing between that and a silent disappearance.
#
# Dry-run is the DEFAULT here, inverted from every other script in this engine.
# The others print or deliver; this one destroys pending state, so the safe
# direction is the one you get by forgetting a flag.
SDIR="$(mktemp -d)"
# Age is faked by renaming: coord-send.sh timestamps from the clock, so there is
# no way to author an old message through the front door. The prefix is the only
# thing the sweep reads, which is exactly what makes this substitution honest.
age_it() { # $1 mailbox root, $2 repo, $3 body marker, $4 new timestamp prefix
for f in "$1/$2"/inbox/*.md; do
[ -e "$f" ] || continue
grep -q "^$3\$" "$f" 2>/dev/null || continue
mv "$f" "$1/$2/inbox/$4-$(basename "$f" | sed 's/^[^-]*-//')"
return 0
done
return 1
}
n_in() { ls "$1"/inbox/*.md 2>/dev/null | wc -l | tr -d ' '; }
CLAUDE_COORD_DIR="$SDIR" "$SEND" --to sr --from s1 --fyi --subject "old note" --message "OLD-NOTE" >/dev/null
CLAUDE_COORD_DIR="$SDIR" "$SEND" --to sr --from s2 --subject "old ask" --message "OLD-ASK" >/dev/null
CLAUDE_COORD_DIR="$SDIR" "$SEND" --to sr --from s3 --fyi --subject "new note" --message "NEW-NOTE" >/dev/null
age_it "$SDIR" sr OLD-NOTE 20200101T000000Z; check "sweep fixture: notice aged" $?
age_it "$SDIR" sr OLD-ASK 20200101T000000Z; check "sweep fixture: debt aged" $?
sout="$(CLAUDE_COORD_DIR="$SDIR" "$SWEEP" 2>&1)"
[ "$(n_in "$SDIR/sr")" -eq 3 ]
check "sweep: dry-run is the default and closes nothing" $?
printf '%s' "$sout" | grep -q 'would close'
check "sweep: dry-run names what it would have closed" $?
[ ! -f "$SDIR/_sweep.log" ]
check "sweep: dry-run writes no log" $?
CLAUDE_COORD_DIR="$SDIR" "$SWEEP" --write >/dev/null 2>&1
[ "$(n_in "$SDIR/sr")" -eq 2 ]
check "sweep: --write closes the aged notice" $?
grep -rq '^OLD-ASK$' "$SDIR/sr/inbox" 2>/dev/null
check "sweep: an aged message that owes a reply is spared" $?
grep -rq '^NEW-NOTE$' "$SDIR/sr/inbox" 2>/dev/null
check "sweep: a notice inside the grace window is spared" $?
grep -rq '^OLD-NOTE$' "$SDIR/sr/archive" 2>/dev/null
check "sweep: the closure went through coord-done (archived, not deleted)" $?
lc="$(grep -c . "$SDIR/_sweep.log" 2>/dev/null)"
[ "${lc:-0}" -eq 1 ]
check "sweep: one log line per closure" $?
# Sender AND subject, because the filename carries neither: the log is the only
# record of what a closed notice actually said, and it is written after
# coord-done.sh has already moved the file out of the inbox.
grep -q 'from=s1' "$SDIR/_sweep.log" 2>/dev/null
check "sweep: the log identifies who sent what vanished" $?
grep -q 'subject=old note' "$SDIR/_sweep.log" 2>/dev/null
check "sweep: the log identifies what the vanished notice said" $?
sout2="$(CLAUDE_COORD_DIR="$SDIR" "$SWEEP" --write 2>&1)"
[ "$(n_in "$SDIR/sr")" -eq 2 ]
check "sweep: idempotent - a second run closes nothing" $?
# The untrusted-input guard, identical in spirit to coord-count.sh's bounded
# read: a body line at column 0 must not be able to mark its own message
# closeable. This is the one place where getting it wrong lets another repo
# delete its way out of your inbox.
CLAUDE_COORD_DIR="$SDIR" "$SEND" --to sb --from s4 --subject "sneaky" --message "reply-expected: no
BODY-CLAIM" >/dev/null
age_it "$SDIR" sb BODY-CLAIM 20200101T000000Z; check "sweep fixture: body-claim aged" $?
CLAUDE_COORD_DIR="$SDIR" "$SWEEP" --write >/dev/null 2>&1
[ "$(n_in "$SDIR/sb")" -eq 1 ]
check "sweep: a body line claiming reply-expected: no cannot close a debt" $?
# _broadcast is storage, not a correspondent. Archiving out of it retires an
# announcement for every repo that has not read it yet - an unauthenticated
# retract, which is coord-send --retract's job and checks the sender.
CLAUDE_COORD_DIR="$SDIR" "$SEND" --broadcast --from s5 --subject "ann" --message "BCAST-1" >/dev/null
age_it "$SDIR" _broadcast BCAST-1 20200101T000000Z; check "sweep fixture: broadcast aged" $?
CLAUDE_COORD_DIR="$SDIR" "$SWEEP" --write >/dev/null 2>&1
[ "$(n_in "$SDIR/_broadcast")" -eq 1 ]
check "sweep: never closes out of _broadcast" $?
# Fail-safe, not fail-open: a name the grammar does not produce has no readable
# age, and an unreadable age must never be treated as old.
mkdir -p "$SDIR/sx/inbox"
printf -- '---\nfrom: s6\nto: sx\nsubject: odd\nreply-expected: no\n---\nODD-NAME\n' > "$SDIR/sx/inbox/not-a-timestamp-from-s6.md"
CLAUDE_COORD_DIR="$SDIR" "$SWEEP" --write >/dev/null 2>&1
[ "$(n_in "$SDIR/sx")" -eq 1 ]
check "sweep: a filename without a readable timestamp is never closed" $?
# --days is the whole policy surface, so it has to actually move the cutoff.
CLAUDE_COORD_DIR="$SDIR" "$SWEEP" --days 0 --write >/dev/null 2>&1
grep -rq '^NEW-NOTE$' "$SDIR/sr/archive" 2>/dev/null
check "sweep: --days moves the cutoff (0 closes a same-day notice)" $?
grep -rq '^OLD-ASK$' "$SDIR/sr/inbox" 2>/dev/null
check "sweep: --days 0 still spares a message that owes a reply" $?
EDIR="$(mktemp -d)"
eout="$(CLAUDE_COORD_DIR="$EDIR" "$SWEEP" --write 2>&1)"; erc=$?
[ "$erc" -eq 0 ]
check "sweep: an empty mailbox root exits 0" $?
"$SWEEP" --help >/dev/null 2>&1
check "sweep: --help exits 0" $?
CLAUDE_COORD_DIR="$EDIR" "$SWEEP" --days 2>/dev/null; [ $? -eq 2 ]
check "sweep: --days without a value is a usage error, not a silent default" $?
/bin/rm -rf "$SDIR" "$EDIR" 2>/dev/null
echo "----"
echo "PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ]

167
scripts/coord-sweep.sh Executable file
View file

@ -0,0 +1,167 @@
#!/bin/bash
# coord-sweep.sh - close the FYI backlog across every mailbox on this machine,
# deterministically and without a model. Archives directed messages whose sender
# declared reply-expected: no and whose filename timestamp is older than a grace
# window. Prints one line per message and a summary; writes a log line per
# closure.
#
# WHY THIS EXISTS. A notice needs no answer, but it is still re-injected at every
# session start until someone closes it by hand. Across enough repositories that
# hand-closing IS the manual work the mailbox was supposed to remove, and the
# pending count - the operator's only signal - drowns in messages that were never
# going to be acted on. This closes exactly that class and nothing else.
#
# ONE MECHANICALLY DECIDABLE CLASS, NEVER A JUDGEMENT. A message that owes a
# reply is never touched, at any age, with any flag. Answering it would mean
# deciding something on the receiving repo's behalf, which is the one thing this
# system exists to keep a session from doing. If that ever needs to change, the
# answer is a session in that repo, not a wider net here.
#
# THE LOG IS NOT OPTIONAL, and the reason is a real gap: a directed message has
# no seen-tracking (only broadcasts do), so this script cannot tell "seen and
# ignored" from "never delivered". A notice to a repo left unopened for the whole
# window is closed UNREAD. That is the accepted tradeoff, and the log is the only
# thing standing between it and a silent disappearance.
#
# DRY-RUN IS THE DEFAULT, inverted from every other script here. The others print
# or deliver; this one destroys pending state, so the safe direction has to be the
# one you get by forgetting a flag.
#
# Usage: coord-sweep.sh [--write] [--days <n>] [--log <path>]
# --write actually close. Without it nothing is archived and no log is
# written - the run only reports what it would have done.
# --days <n> grace window in days (default 14). 0 means "any age".
# --log <path> log file (default $CLAUDE_COORD_DIR/_sweep.log).
# Env: CLAUDE_COORD_DIR overrides the mailbox root.
# Exit: 0 on success, including when there is nothing to close. 2 on usage error.
# ASCII only, bash 3.2 safe.
set -u
export LC_ALL=C
COORD="${CLAUDE_COORD_DIR:-$HOME/.claude/coord}"
DIR="$(cd "$(dirname "$0")" && pwd)"
# No `${VAR:-fallback}` on an engine path, ever: a fallback silently routes
# through whatever happens to sit at the alternate location instead of failing
# loud. That defect shipped twice here (board.sh, then coord-send.sh).
DONE="$DIR/coord-done.sh"
WRITE=0
DAYS=14
LOG=""
while [ $# -gt 0 ]; do
case "$1" in
--write) WRITE=1; shift ;;
# bash 3.2: `shift 2` past the end of $# is a no-op -> would loop forever.
--days) [ $# -ge 2 ] || { echo "coord-sweep: --days requires a value" >&2; exit 2; }
DAYS="$2"; shift 2 ;;
--log) [ $# -ge 2 ] || { echo "coord-sweep: --log requires a value" >&2; exit 2; }
LOG="$2"; shift 2 ;;
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
# Strict, unlike coord-inbox.sh. That one is lenient because failing a
# SessionStart over a stray flag is worse than ignoring it; this one is not
# on that path and closes messages, so a typo must stop it.
*) echo "coord-sweep: unknown argument: $1" >&2; exit 2 ;;
esac
done
case "$DAYS" in
''|*[!0-9]*) echo "coord-sweep: --days must be a non-negative integer: $DAYS" >&2; exit 2 ;;
esac
[ -x "$DONE" ] || { echo "coord-sweep: cannot find coord-done.sh at $DONE" >&2; exit 2; }
[ -n "$LOG" ] || LOG="$COORD/_sweep.log"
# Cutoff as a plain 14-digit number, so the comparison is integer arithmetic
# rather than string collation. BSD date (macOS); a failure here must stop the
# run, because a missing cutoff would otherwise read as "close everything".
CUTOFF="$(date -u -v-"${DAYS}"d +%Y%m%d%H%M%S 2>/dev/null)"
case "$CUTOFF" in
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]) ;;
*) echo "coord-sweep: could not compute a cutoff date" >&2; exit 2 ;;
esac
NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
[ -d "$COORD" ] || { echo "coord-sweep: nothing to close (no mailbox root at $COORD)"; exit 0; }
# Does this message owe a reply? Absent field means YES: every message written
# before 0.11.0 lacks it, so absence keeps meaning what it always meant. The read
# is bounded to the frontmatter block - a body line is untrusted cross-repo input
# and must not be able to close its own message by claiming "reply-expected: no"
# at column 0. Duplicated from coord-count.sh rather than shared: each script
# here must run standalone.
owes_reply() {
[ "$(head -1 "$1" 2>/dev/null)" = "---" ] || return 0
[ "$(grep -c '^---$' "$1" 2>/dev/null)" -ge 2 ] || return 0
sed -n '2,/^---$/p' "$1" 2>/dev/null | grep -q '^reply-expected: no$' && return 1
return 0
}
# Frontmatter field for the log line. Control characters are stripped because
# this is untrusted content on its way into a file the operator reads.
field() {
sed -n '2,/^---$/p' "$2" 2>/dev/null | grep "^$1: " | head -1 \
| sed "s/^$1: //" | tr -d '\000-\037' | cut -c1-120
}
CLOSED=0
for d in "$COORD"/*; do
[ -d "$d" ] || continue
name="$(basename "$d")"
# Reserved engine namespace. _broadcast is storage, not a correspondent, and
# archiving out of it would retire an announcement for every repo that has not
# read it yet - an unauthenticated retract. That is coord-send --retract's job,
# and it checks the sender.
case "$name" in _*) continue ;; esac
[ -d "$d/inbox" ] || continue
for m in "$d/inbox"/*.md; do
[ -e "$m" ] || continue
base="$(basename "$m")"
# Age comes free from the filename prefix; nothing inside the file is
# trusted for it. A name the grammar does not produce has no readable age,
# and an unreadable age must never be treated as old - fail-safe, not
# fail-open.
ts="${base%%-*}"
case "$ts" in
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]Z) ;;
*) continue ;;
esac
num="$(printf '%s' "$ts" | tr -dc '0-9')"
[ "$num" -lt "$CUTOFF" ] || continue
owes_reply "$m" && continue
if [ "$WRITE" -eq 1 ]; then
# Read the fields BEFORE closing: coord-done.sh moves the file to archive,
# so a log line built afterwards describes a path that no longer exists and
# silently logs empty values - which defeats the only safeguard this script
# has against a notice vanishing unread.
mfrom="$(field from "$m")"
msubj="$(field subject "$m")"
# Through the engine, never `mv`: coord-done.sh owns the filename grammar
# and the archive layout, and it refuses _broadcast on its own.
if "$DONE" --repo "$name" "$base" >/dev/null 2>&1; then
printf '%s\t%s\t%s\tfrom=%s\tsubject=%s\n' \
"$NOW" "$name" "$base" "$mfrom" "$msubj" >> "$LOG"
echo "closed: $name/$base"
CLOSED=$((CLOSED + 1))
else
echo "coord-sweep: failed to close $name/$base" >&2
fi
else
echo "would close: $name/$base"
CLOSED=$((CLOSED + 1))
fi
done
done
if [ "$CLOSED" -eq 0 ]; then
echo "coord-sweep: nothing to close (cutoff ${CUTOFF}, ${DAYS} days)"
elif [ "$WRITE" -eq 1 ]; then
echo "coord-sweep: $CLOSED message(s) closed (cutoff ${CUTOFF}, ${DAYS} days). Log: $LOG"
else
echo "coord-sweep: $CLOSED message(s) would close (cutoff ${CUTOFF}, ${DAYS} days). Re-run with --write."
fi
exit 0

View file

@ -14,7 +14,7 @@ description: >-
user names no repo and no tool — choosing *between* repos is this skill. Not for
"where were we" inside the current repo: that is this repo's own STATE.md,
already injected at session start.
version: "0.13.0"
version: "0.14.0"
---
# board — which repo deserves the next session

View file

@ -15,7 +15,7 @@ description: >-
covers retiring a broadcast that has become wrong or obsolete: "retract that
broadcast", "that announcement is outdated, pull it", "trekk tilbake kringkastingen",
"den broadcasten er utdatert".
version: "0.13.0"
version: "0.14.0"
---
# coord-send — natural-language front door for inter-repo messages

View file

@ -14,7 +14,7 @@ description: >-
the operator names no model and no tool — choosing the model for the next
session IS this skill. Not for choosing which REPO gets the next session:
that is the `board` skill.
version: "0.13.0"
version: "0.14.0"
---
# route — what the next session should run with