Commit graph

17 commits

Author SHA1 Message Date
459c9feec0 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
2026-08-01 22:00:42 +02:00
9cb405c2cd feat(route): give the advisor a writer, on a need and per row
route.sh now emits `--advisor opus` into the startup command it prints.
The flag existed and worked, but nothing generated it, so it went unused:
the only mechanism that ever set an advisor here was `/advisor`, which
writes the global advisorModel setting -- every session, every repo -- and
was abandoned for burning quota. Nothing replaced it.

Two independent triggers, almost disjoint by construction:

  rows 1-2  always. Sonnet main model, so opus is a capability LIFT rather
            than a peer. Load-bearing: every fallback is one row cheaper and
            the cheap rows are Sonnet, so this makes the quota fallback safe.
  rows 3-4  only at reversibility=costly|one-way. Opus main model, so the
            advisor buys peer review where a mistake is not cheap to undo.
  rows 5-6  never. The CLI rejects every advisor for a Fable main model.

costly forces row 3 and one-way forces row 4, so a Sonnet row always has
reversibility=cheap and neither rule reaches the other's rows.
verification=none is deliberately not a third trigger: beyond the stakes
rule it adds only cheap-to-reverse mistakes, docs sessions among them.
Applied per ROW, so fallback-command carries its own correct answer.

route-selftest.sh 56 -> 73. Section 14 gates the three CLI facts the rule
rests on against the installed claude without spending a token: advisor
validation runs before the empty-prompt check, so `-p ""` reaches the
validator and stops. --help cannot gate this -- it short-circuits before
option validation, so an unknown flag would pass the gate untested.

Three pre-existing checks updated rather than worked around: two asserted
whole command strings that now carry the advisor, and section 11's effort
extraction swallowed the tail of the command line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X8N8hQEJSWWtieWUx37txT
2026-07-31 21:39:59 +02:00
6e369936f9 chore(release): 0.12.1 -- coord-send stops silently falling back to a stale copy
Version sync across plugin.json, package.json, README badge, CHANGELOG and
all three skill frontmatters, plus the 156 -> 159 selftest-count references
that follow from the previous commit's three new checks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJMJanRHkUhEf9EYxMpbM5
2026-07-31 18:25:39 +02:00
dffc1f5e47 chore(release): 0.12.0 -- a session measures the effort it ran with
Bumps the six version-bearing files and gives ac62a38 its changelog entry.

Until the tag exists the plugin cache keeps serving the 0.11.0 skill, which
instructs every new session that effort is not observable and to ask the
operator for it. The fix is committed and pushed but not in circulation, so
the release is the part that changes behavior.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxRVZm1busUeuXd5iPkK36
2026-07-31 17:28:19 +02:00
6735298752 chore(release): 0.11.0 -- a message can say it needs no answer, and 0.10.0 gets its changelog
Bumps plugin.json, package.json, the README badge and the three skill
frontmatters, because release-plugin.mjs blocks on a plugin.json that does not
already stand at the target version.

Also closes a gap this release would otherwise have inherited: v0.10.0 shipped
without a CHANGELOG section, and the next-cost fix sat under [Unreleased] even
though it is inside v0.9.0..v0.10.0. Labelling that block 0.11.0 would have
made a second wrong claim, so 0.10.0 now has its own section, written from the
commits in the tag rather than from memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016iJoZVmU2guTEZcMghk88z
2026-07-31 15:50:41 +02:00
dbf7391d57 chore(release): 0.10.0 -- route ships, and the board skill stops advertising a path that is gone
Version bump across plugin.json, package.json, the README badge and all three
SKILL.md frontmatters.

Also removes the last false statement in the public docs: the board skill still
sold a `~/.claude/scripts/` fallback as portability, but that copy was deleted
when the skill was made the only entry point. Tagging it would have published a
path that does not resolve, next to a sentence claiming it does. One entry point
is the point -- two entries pointed at two copies and made it impossible to know
which one ran.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017peNgsxVt1BR4BTuMwiPoX
2026-07-31 10:22:53 +02:00
61e224ccc3 feat(board): cross-repo attention board as script + skill (0.9.0)
The mailbox answers "who wrote to me"; it never answered "which repo
deserves the next session". board.sh scans every discovered repo and reads
three sources each: the STATE.md next-step block with its optional board
line, git status, and that repo's pending mailbox count. Read-only by
construction, pinned by board-selftest.sh (28 checks).

It ships here rather than as a personal script because the mailbox is one
of its three inputs and the two carry the same axis distinction: a pending
count means others are waiting on that repo, while who a repo waits ON comes
only from its board line, since the message format has no reply-to field.
Splitting the board from the mailbox would put that distinction in two
places. It also lets the skill resolve the engine through CLAUDE_PLUGIN_ROOT
like every other script here, instead of depending on a file that exists
only in ~/.claude/scripts (a directory with no remote and no backup).

The skill is a ranking, not a report: re-runs the board every invocation
because counts drift, ranks by what unblocks the most and what is cheapest
to move, then names one repo, the rule that fired, and the real next action
read from that repo's STATE.md. Never the table. Not wired into session
start, which would spend context on repos the session is not in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MubwdTi88yu4hVLAFG1LbM
2026-07-28 21:15:03 +02:00
ec92c866c1 feat(engine)!: counting is not delivering, and an empty inbox is not all clear
Reading IS delivery in this engine: coord-inbox.sh prints a broadcast and then
records it as seen. That made "what is pending elsewhere" unanswerable -- asking
the read path per repo would have consumed every repo's broadcast backlog as a
side effect, once, silently, and unrecoverably, since the seen set is delivery
history that retraction deliberately leaves alone.

coord-count.sh answers it by counting files and writing nothing: no seen set, no
.origin. It keys on MAILBOXES rather than repos -- it enumerates $COORD/* and
never scans a filesystem for checkouts -- so a repo without a mailbox is not
missing from the count, it is absent from the domain. Drained mailboxes are
omitted rather than reported as zero, because the question is "who is owed a
reply" and a list of zeroes answers a different one at every reader's expense.

The read path now closes with one aggregate line built from it. The case that
motivated this is the session whose own inbox is empty: it saw silence and
concluded "all clear" while mail sat unanswered everywhere else.

BREAKING (injection contract): the read path is no longer silent whenever THIS
repo has nothing pending. It is a silent no-op only when the whole mailbox is
empty. Coupling the line to having your own mail would have hidden it from its
only real audience. Three selftest assertions that used "no output at all" as a
proxy for "nothing was delivered" now assert the absence of the content itself,
which is what they always meant.

The line is an AGGREGATE of two integers, never a roster. A list of names would
reproduce other repos' situation inside this repo's injection -- the state
boundary the mailbox exists to respect -- and mailbox names are cross-repo
input. Two integers cannot carry anything that escapes the framing.

Its disclaimer is engine behavior, not politeness (Rule 7): the line lands
directly beneath "handle this inbox FIRST", and without it the numbers read as
an extension of that obligation and a session starts answering other repos'
mail. Pinned in selftest section 26 exactly as section 20 pins the priority
text. The hook's header drops "(unread messages)" for the same reason -- it
would now announce mail that does not exist.

Selftest 116 -> 136. Every new negative check is anchored to a positive
assertion in the same output, because a missing script makes "X is absent"
true by vacuity and would have gone green proving nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U6EixQo6hpoRCVtiAXdnFs
2026-07-27 20:31:21 +02:00
28e7cb42cd feat(hook): let a non-git surface declare its identity via CLAUDE_COORD_REPO
0.6.0 removed the working-directory fallback, which was right, but it left
every non-git working surface with nothing to derive from. The read path
declines silently by design - the hook must never fail a session - so such a
surface simply stops seeing its inbox: no error, no exit code, nothing. That is
the same loss-looks-like-normal shape 0.6.0 set out to remove, reintroduced one
layer up.

The hook now forwards CLAUDE_COORD_REPO verbatim as --repo. This is not the
fallback returning, and the distinction is the whole point: the fallback GUESSED
a name from wherever the session happened to stand, while a declaration is
written down in that directory's settings, readable back, and deletable.
Identity stays a choice someone made.

Forwarding as --repo rather than reimplementing anything means it inherits every
engine rule, including that an explicit override never claims <repo>/.origin -
otherwise a surface borrowing a name would steal the claim from the checkout
that owns it. Both halves are pinned by tests, and the shared test helper now
deletes CLAUDE_COORD_REPO unless a test asks for it, so a value set globally
later cannot silently satisfy the tests that prove the hook resolves nothing on
its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U6EixQo6hpoRCVtiAXdnFs
2026-07-27 08:56:31 +02:00
95c24cd034 chore(release): v0.6.0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U6EixQo6hpoRCVtiAXdnFs
2026-07-26 21:03:20 +02:00
737127a14c feat(engine)!: the inbox is a priority, not a suggestion
Through 0.4.0 the injection block told every repo to "consider
replying/resolving where it fits in this session". That sentence was the
whole problem: the injection text is the only place a repo is ever told
what to do with a message, so the wording IS the protocol -- and it
granted permission to defer. Messages sat unanswered for weeks while each
session did its own work first. Nothing was broken; the protocol was
asking for exactly what it got.

The block now states an ordering and a completion obligation: handle the
inbox before the task the session came to do, and drive every directed
message to a terminal state before the session ends (--reply-to or
coord-done). Neither terminal state is the default -- the format has no
reply-expected field, so mandating only the reply would manufacture
traffic for messages that merely inform. Leaving one pending stays
allowed but must be stated to the operator with a reason.

Raising priority deliberately does not widen the trust boundary. The
obligation is procedural, never substantive: responding is mandatory,
complying with what a message asks is not. Untrusted cross-repo content
still cannot direct the reader; it merely can no longer be ignored. The
injection states both halves and selftest section 20 pins them together,
so a future reword cannot keep the priority and quietly drop the
distinction -- that combination would turn prioritization into an
injection surface.

Selftest 82 -> 93.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U6EixQo6hpoRCVtiAXdnFs
2026-07-25 18:44:20 +02:00
316b8acdd2 feat(engine): retire a broadcast with coord-send --retract
Nothing could remove a message from _broadcast/inbox/. coord-done is
directed-only and never touches the broadcast queue, so the backlog could
only grow: every new repo received the entire standing history at its first
session, including announcements that had since become false.

--retract <filename> archives the message into _broadcast/archive/, so no
future repo is served it. Three deliberate limits, all pinned by tests:

- Un-send, not recall. Repos that already received it keep it;
  _broadcast/seen/ is delivery history and is left untouched.
- Only the sender may retract (from: must match the repo identity). --from
  overrides it, as everywhere else in the engine, which makes the check an
  accident guard rather than a security boundary.
- Nothing is deleted, mirroring coord-done. Retracting twice is a no-op.

The branch runs before every send-side validation and before the stdin body
read, since a retract carries no subject and no body.

Selftest 70 -> 82 (new section 19). Also fixes two README defects the
feature exposed: the install command still named coord@ after the v0.3.0
rename, and the docs advised pruning _broadcast/inbox/ by hand, which
contradicted the rule that the script owns mailbox files.
2026-07-25 15:11:47 +02:00
c27b20fc62 feat(release)!: rename plugin and repo from coord to repo-mailbox
The old name said that something was coordinated, but not what the thing
was. The new one names what it is, reusing the vocabulary the code and
docs already use throughout: mailbox, inbox, archive, broadcast.

Renamed: Forgejo repo (open/coord -> open/repo-mailbox, old URL
redirects), plugin manifest name, package name, README title and badge,
CLAUDE.md heading and release command.

BREAKING CHANGE: the skill is invoked as /repo-mailbox:coord-send rather
than /coord:coord-send, and the plugin must be reinstalled under its new
name.

Deliberately unchanged: the CLI (coord-send.sh, coord-inbox.sh,
coord-done.sh, coord-selftest.sh), the skill name coord-send, the mailbox
root ~/.claude/coord/, and CLAUDE_COORD_DIR. Those name the transport
protocol, not the product; renaming them would migrate live mailbox data
and break message history in every participating repo for no gain.

Selftest unchanged at 70/70 -- the engine was not touched.
2026-07-25 07:00:17 +02:00
2c44c091ff chore(release): v0.2.2
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvTviFeoMCKJcALATRempy
2026-07-25 06:40:37 +02:00
27b0443aea chore(release): v0.2.1
Bump all version carriers (plugin.json, package.json, README badge,
SKILL.md frontmatter) and record the release in the changelog.

Also backfills the missing [0.2.0] changelog section: that release shipped
without one, which would have left the changelog jumping 0.1.0 -> 0.2.1.
Content reconstructed from the v0.2.0 commit, not from its subject line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvTviFeoMCKJcALATRempy
2026-07-25 06:20:11 +02:00
0a79e786fc feat(engine): harden mailbox CLIs for v0.2.0
- Atomic delivery: create the temp file inside the destination dir
  (dot-prefixed, invisible to the inbox glob) so the final rename never
  crosses filesystems and readers never see a half-written message.
- Reject . and .. explicitly in the --reply-to and coord-done name
  guards instead of relying on downstream failure.
- Add -h/--help to coord-inbox.sh (uniform across the three CLIs).
- Close selftest gaps: default mailbox path via HOME fallback, malformed
  frontmatter on the read path, read-only destination dir. 48 -> 64
  checks, all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fduZz8otpU3W3rhfoPD6t
2026-07-24 19:57:40 +02:00
0e6014fe98 feat(plugin): package as a Claude Code marketplace plugin
- .claude-plugin/plugin.json v0.1.0 (auto_discover, MIT)
- SessionStart hook: thin zero-dep .mjs wrapper (marketplace convention)
  around scripts/coord-inbox.sh, emitting the additionalContext envelope;
  always exits 0. Smoke-tested: empty mailbox -> bare continue, pending
  message -> injected with UNTRUSTED framing.
- coord-send skill bundled; examples and description use generic repo
  names only (coordination metadata never reaches a public surface).
- README (English; documents the deliver-until-done lifecycle correctly,
  review §7), CHANGELOG, LICENSE (MIT), CLAUDE.md, package.json +
  node --test wrapper around the bash selftest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBbjgS5A55RVavoyjJC4FX
2026-07-24 06:46:09 +02:00