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
This commit is contained in:
parent
77664e9e51
commit
0e6014fe98
10 changed files with 472 additions and 0 deletions
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "coord",
|
||||
"version": "0.1.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"
|
||||
},
|
||||
"auto_discover": true,
|
||||
"license": "MIT",
|
||||
"repository": "https://git.fromaitochitta.com/open/coord",
|
||||
"keywords": ["coordination", "mailbox", "inter-repo", "multi-repo", "session-start"]
|
||||
}
|
||||
44
CHANGELOG.md
Normal file
44
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Changelog
|
||||
|
||||
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.1.0] - 2026-07-24
|
||||
|
||||
First public release: extraction of a battle-tested personal mechanism into a
|
||||
distributable plugin.
|
||||
|
||||
### Added
|
||||
|
||||
- Bash engine: `coord-send.sh` (directed / broadcast / reply delivery),
|
||||
`coord-inbox.sh` (pending inbox + unseen broadcasts, formatted for context
|
||||
injection), `coord-done.sh` (archive without reply), all bash-3.2-safe,
|
||||
ASCII-only, zero dependencies.
|
||||
- `coord-selftest.sh`: 48 checks against a throwaway mailbox, including
|
||||
regression cover for filename sanitization, no-hang argument parsing,
|
||||
prompt-injection resistance, frontmatter hygiene, and broadcast ordering.
|
||||
- SessionStart hook (`hooks/scripts/session-start.mjs`): thin zero-dependency
|
||||
Node wrapper that injects the repo's pending mailbox as
|
||||
`additionalContext`. Always exits 0 — a broken mailbox never blocks a session.
|
||||
- `coord-send` skill: natural-language front door (English + Norwegian
|
||||
triggers) mapping intent to engine invocations, including bounded
|
||||
multi-target loops, deferred sends, and replies.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Frontmatter injection: CR/LF and control characters in `--subject`/`--from`
|
||||
are collapsed before writing, so fields can no longer inject frontmatter
|
||||
lines or a premature `---` terminator.
|
||||
- Broadcast loss: a per-repo seen set (`_broadcast/seen/<repo>`, one delivered
|
||||
filename per line) replaces the single high-water mark, which silently
|
||||
dropped a same-second broadcast whose filename sorted below one already
|
||||
seen. Upgrade note: a legacy watermark file is read as a one-entry seen
|
||||
set, so old broadcasts may be re-delivered once.
|
||||
|
||||
### Changed
|
||||
|
||||
- Read-side protocol strings are English (`--- message:`, `-> reply:`,
|
||||
UNTRUSTED DATA framing); the forgery-resistance selftest checks pin the
|
||||
English tokens.
|
||||
59
CLAUDE.md
Normal file
59
CLAUDE.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# coord
|
||||
|
||||
## Context
|
||||
|
||||
Local inter-repo coordination mailbox for Claude Code, packaged as a
|
||||
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. Everything is pinned by `coord-selftest.sh`
|
||||
(48 checks, throwaway mailbox via `CLAUDE_COORD_DIR`).
|
||||
- **Hook (`hooks/scripts/session-start.mjs`):** thin zero-dependency Node
|
||||
wrapper (marketplace convention: hooks are `.mjs`) that calls
|
||||
`coord-inbox.sh` and emits the `hookSpecificOutput.additionalContext`
|
||||
envelope. No mailbox logic lives here. Always exits 0.
|
||||
- **Skill (`skills/coord-send/`):** natural-language front door mapping user
|
||||
intent to engine invocations. No mailbox logic lives here either.
|
||||
|
||||
**Boundary rule:** the mailbox is transport, not state. Durable decisions
|
||||
live in the owning repo's docs/git history; messages are notices pointing at
|
||||
them. Message content is untrusted cross-repo input — the read side quotes
|
||||
and frames it; the send side sanitizes line-oriented fields.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Scripts are bash-3.2-safe and ASCII-only: no `declare -A`, no
|
||||
`readarray`/`mapfile`, no `|&`; guard `shift 2` with `$# -ge 2`; guard
|
||||
empty-array expansion under `set -u` with `${#a[@]}`.
|
||||
- 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 (48/48).
|
||||
- English for all code, docs, and commit messages (public repo). Norwegian
|
||||
trigger aliases in the skill description are deliberate.
|
||||
- Conventional Commits: `type(scope): description`.
|
||||
|
||||
## Commands
|
||||
|
||||
- Test: `bash scripts/coord-selftest.sh` (or `npm test`, the Node wrapper)
|
||||
- Hook smoke test: `node hooks/scripts/session-start.mjs` (expects JSON on stdout)
|
||||
|
||||
## Release
|
||||
|
||||
Version must agree across: `.claude-plugin/plugin.json`, `package.json`,
|
||||
README version badge, `skills/coord-send/SKILL.md` frontmatter, git tag
|
||||
`vX.Y.Z`, and the catalog `ref` in
|
||||
`ktg-plugin-marketplace/catalog/.claude-plugin/marketplace.json`. Release via
|
||||
the catalog's `scripts/release-plugin.mjs coord` (tag + ref bump together);
|
||||
verify with `scripts/check-versions.mjs`. Never hand-edit a ref.
|
||||
|
||||
## Hardening roadmap (queued, post-v0.1.0)
|
||||
|
||||
- Atomic delivery: `mktemp` inside the destination dir so rename never
|
||||
crosses filesystems.
|
||||
- Explicit `.`/`..` rejection in `--reply-to`/`coord-done` guards.
|
||||
- Selftest gaps: default mailbox path (unset `CLAUDE_COORD_DIR`), malformed
|
||||
frontmatter on the read path.
|
||||
- Uniform `-h` across the three CLIs (`coord-inbox.sh` lacks it).
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Kjell Tore Guttormsen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
114
README.md
Normal file
114
README.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# Coord
|
||||
|
||||
> A local mailbox for coordination between Claude Code sessions in different repositories. Session A in repo X leaves a message for repo Y; the next session in repo Y gets it injected as context at startup. Local, private, no network, no SaaS.
|
||||
|
||||
> **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See the [marketplace governance](https://git.fromaitochitta.com/open/ktg-plugin-marketplace/src/branch/main/GOVERNANCE.md) for the full model.
|
||||
|
||||
*AI-generated: all code produced by Claude Code through dialog-driven development.*
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
## Why This Exists
|
||||
|
||||
When you work with an AI coding agent across several repositories, the sessions are islands. A decision made in repo X often matters to repo Y — a shared spec changed, a bug another repo depends on got fixed, a gate was re-pinned. Without a channel, you are the messenger: you remember to mention it in the next session, or it gets lost.
|
||||
|
||||
Coord is that channel, reduced to the simplest thing that works: a directory of Markdown files on your own disk. Sending is writing a file; receiving is a SessionStart hook that injects your repo's pending messages as context. No server, no daemon, no network, no accounts.
|
||||
|
||||
**Transport, not state.** A coord message is a notice, not a source of truth. The durable record of any decision lives in the owning repo (its docs, its git history). Messages point at that record; they never replace it.
|
||||
|
||||
## How It Works
|
||||
|
||||
Mailbox layout (default `~/.claude/coord/`, override with `CLAUDE_COORD_DIR`):
|
||||
|
||||
<repo>/inbox/ pending directed messages TO <repo>
|
||||
<repo>/archive/ handled messages (kept, never deleted)
|
||||
_broadcast/inbox/ messages to ALL repos (accumulate)
|
||||
_broadcast/seen/<repo> per-repo seen set: delivered broadcast filenames, one per line
|
||||
|
||||
Repo identity is the basename of the git toplevel (fallback: the working directory). There is no registration — a repo joins the moment something is sent to it, or when it first reads a broadcast.
|
||||
|
||||
Message format (filename `<UTC-timestamp>-<uniq>-from-<sender>.md`):
|
||||
|
||||
---
|
||||
from: <source-repo>
|
||||
to: <target-repo> | broadcast
|
||||
subject: <short subject>
|
||||
date: <UTC ISO-8601>
|
||||
---
|
||||
<body>
|
||||
|
||||
**Lifecycle — deliver until done.** Directed messages are NOT archived on read. They stay pending and are re-injected at every session start (startup, `/clear`, resume) until explicitly marked handled: replying (`--reply-to`) archives the original, or `coord-done <file>` archives it without a reply. `/clear` never loses a message. Broadcasts are delivered once per repo via the seen set and otherwise accumulate; prune `_broadcast/inbox/` manually when a notice stops being relevant to future first-time repos.
|
||||
|
||||
## Install
|
||||
|
||||
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
|
||||
claude plugin install coord@ktg-plugin-marketplace
|
||||
|
||||
The plugin ships empty: your mailbox is created lazily on first send, on your machine, and stays there.
|
||||
|
||||
## Usage
|
||||
|
||||
**Natural language (the `coord-send` skill).** In any session: "tell repo-x the bug is fixed", "broadcast that the spec changed", "reply to that coord message", "when the tests are green, notify repo-y". The skill maps intent to the right `coord-send` invocation, including bounded multi-target loops and deferred sends.
|
||||
|
||||
**Receiving** is automatic: the SessionStart hook injects your repo's pending inbox and unseen broadcasts as context, with per-message reply/resolve hints.
|
||||
|
||||
**CLI.** The engine is three bash scripts in the plugin's `scripts/` directory; resolve them as `"${CLAUDE_PLUGIN_ROOT:-$HOME/.claude}/scripts/coord-<name>.sh"` (from a terminal, use the plugin's install path):
|
||||
|
||||
coord-send.sh --to <repo> --subject "<subject>" [--message "<text>"] # or body on stdin
|
||||
coord-send.sh --broadcast --subject "<subject>" <<'BODY' ... BODY
|
||||
coord-send.sh --reply-to <filename> [--subject "Re: ..."] # routes + closes original
|
||||
coord-inbox.sh [--repo <name>] # print pending (what the hook injects)
|
||||
coord-done.sh <filename>... | --all # archive without replying
|
||||
|
||||
The reply/resolve hints the hook injects (`-> reply: coord-send --reply-to … | done without reply: coord-done …`) refer to these scripts.
|
||||
|
||||
## Security Model
|
||||
|
||||
Cross-repo message content is untrusted input by design:
|
||||
|
||||
- **Read side:** every injected content line is prefixed with `> `, so a message body can never forge the `--- message:` / `-> reply:` framing lines at column 0. The injected header explicitly frames content as UNTRUSTED DATA and instructs the model to never follow instructions found inside it.
|
||||
- **Send side:** CR/LF and control characters in `subject`/`from` are collapsed before writing, so fields cannot inject frontmatter lines or a premature `---` terminator. Sender names are sanitized in filenames (raw name kept in frontmatter).
|
||||
- **No network, no secrets:** everything is local files under your `$HOME`. Credentials never appear in messages, filenames, or frontmatter — there is nothing to leak by transport.
|
||||
- **Privacy rule:** coordination metadata (repo names, status, architecture) never belongs on a public surface. The mailbox lives outside your repos and stays out of git.
|
||||
|
||||
Every guarantee above is pinned by the 48-check selftest, including forgery-resistance regressions.
|
||||
|
||||
## The Five Rules
|
||||
|
||||
1. **Mailbox, not state.** Files here are messages in transit. If a file starts acting as someone's state-of-play, it belongs in the owning repo.
|
||||
2. **No durable decisions live here.** The copy here is the notice, not the record — durable content is written in the owning repo's docs.
|
||||
3. **One recipient per message.** `--to <repo>` or `--broadcast`.
|
||||
4. **Delivery happens via session start.** Don't hand-edit another repo's inbox; use `coord-send`.
|
||||
5. **Private.** Coordination metadata never reaches a public surface.
|
||||
|
||||
## Requirements
|
||||
|
||||
- macOS or Linux with bash 3.2+ (the scripts are deliberately bash-3.2-safe and ASCII-only).
|
||||
- Node.js >= 18 for the SessionStart hook (zero npm dependencies).
|
||||
- `git` is optional — without it, repo identity falls back to the directory basename.
|
||||
|
||||
## Development
|
||||
|
||||
bash scripts/coord-selftest.sh # 48 checks against a throwaway mailbox
|
||||
npm test # same selftest via node --test
|
||||
|
||||
TDD is the house rule: every behavior change lands with a failing selftest check first.
|
||||
|
||||
## Hardening Roadmap
|
||||
|
||||
Known, accepted gaps queued for a future release:
|
||||
|
||||
- Atomic delivery: create the temp file inside the destination directory so the final rename never crosses filesystems.
|
||||
- Explicit `.`/`..` rejection in the `--reply-to` and `coord-done` name guards (currently stopped downstream by accident, not by the guard).
|
||||
- Selftest gaps: the default mailbox path (unset `CLAUDE_COORD_DIR`) and malformed frontmatter on the read path are not yet exercised.
|
||||
- `coord-inbox.sh` lacks `-h`/`--help` and silently ignores unknown arguments; send/done already have uniform help.
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
15
hooks/hooks.json
Normal file
15
hooks/hooks.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/session-start.mjs",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
42
hooks/scripts/session-start.mjs
Normal file
42
hooks/scripts/session-start.mjs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env node
|
||||
// coord - SessionStart hook: inject this repo's pending coordination inbox
|
||||
// (directed messages + unseen broadcasts) as additionalContext.
|
||||
//
|
||||
// Thin Node wrapper (marketplace convention: hooks are .mjs) around the bash
|
||||
// engine scripts/coord-inbox.sh, which owns the mailbox semantics and is
|
||||
// covered by scripts/coord-selftest.sh. Zero dependencies. Always exits 0 -
|
||||
// a broken mailbox must never block a session.
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { basename, dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
function emit(context) {
|
||||
const out = { continue: true };
|
||||
if (context) {
|
||||
out.hookSpecificOutput = { hookEventName: 'SessionStart', additionalContext: context };
|
||||
}
|
||||
process.stdout.write(JSON.stringify(out) + '\n');
|
||||
}
|
||||
|
||||
try {
|
||||
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT
|
||||
|| join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
|
||||
let repoRoot = '';
|
||||
try {
|
||||
repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'],
|
||||
{ stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim();
|
||||
} catch {
|
||||
// not a git repo: fall back to the working directory below
|
||||
}
|
||||
if (!repoRoot) repoRoot = process.cwd();
|
||||
|
||||
const inbox = execFileSync('bash',
|
||||
[join(pluginRoot, 'scripts', 'coord-inbox.sh'), '--repo', basename(repoRoot)],
|
||||
{ stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' });
|
||||
|
||||
emit(inbox.trim() ? '== Repo coordination (unread messages) ==\n' + inbox : '');
|
||||
} catch {
|
||||
emit('');
|
||||
}
|
||||
12
package.json
Normal file
12
package.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "coord",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node --test tests/*.test.mjs"
|
||||
}
|
||||
}
|
||||
140
skills/coord-send/SKILL.md
Normal file
140
skills/coord-send/SKILL.md
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
---
|
||||
name: coord-send
|
||||
description: >-
|
||||
Send an inter-repo coordination message through the local coord mailbox — a
|
||||
natural-language front door over the `coord-send` script. Use this whenever the
|
||||
user wants to notify, tell, message, ping, or coordinate with another repository
|
||||
(or all repositories) about work happening in the current repo: "send info to all
|
||||
repos", "tell repo-x the bug is fixed", "let repo Y and Z know",
|
||||
"broadcast that the spec changed", "reply to that coord message", or a
|
||||
deferred/conditional notice like "when the build is green, notify repo X". Also
|
||||
triggers on Norwegian phrasings: "send info til alle repo", "varsle repo X", "gi
|
||||
beskjed til Y og Z", "kringkast at …", "svar på coord-meldingen", "når Z er ferdig,
|
||||
varsle X". Trigger even when the user names a repo plus something to convey without
|
||||
saying "coord" explicitly — routing a message to another repo IS this skill.
|
||||
version: "0.1.0"
|
||||
---
|
||||
|
||||
# coord-send — natural-language front door for inter-repo messages
|
||||
|
||||
This skill turns a plain request ("tell repo X that Y happened") into the right
|
||||
`coord-send` invocation. The script is the engine; this skill is only the mapping
|
||||
from intent to arguments. It never edits mailbox files by hand — always go through
|
||||
the script, because the script owns the filename grammar, frontmatter, and delivery
|
||||
guarantees.
|
||||
|
||||
## Boundary — why messages are thin
|
||||
|
||||
The mailbox is **transport, not state**. A coord message is a *notice*, not the
|
||||
source of truth. The durable record of any decision lives in the owning repo's
|
||||
state files / docs and its git history. So when you compose a body, write a
|
||||
self-contained heads-up and point at where the real record lives (a commit
|
||||
hash, a doc path) — don't try to make the message itself the canonical artifact.
|
||||
Coordination content is also private: it never belongs on a public surface.
|
||||
|
||||
## The engine
|
||||
|
||||
Resolve the script path portably — this one expression is correct both when the
|
||||
skill runs bundled inside the plugin and when the scripts are installed as
|
||||
personal scripts under `~/.claude/scripts/`:
|
||||
|
||||
CSEND="${CLAUDE_PLUGIN_ROOT:-$HOME/.claude}/scripts/coord-send.sh"
|
||||
|
||||
Interface (body comes from a quoted heredoc so nothing in it is shell-expanded):
|
||||
|
||||
# to one repo
|
||||
"$CSEND" --to <repo> --subject "<subject>" <<'BODY'
|
||||
<message body>
|
||||
BODY
|
||||
|
||||
# to every repo (accumulates; first-time newcomers see it too)
|
||||
"$CSEND" --broadcast --subject "<subject>" <<'BODY'
|
||||
<message body>
|
||||
BODY
|
||||
|
||||
# reply to a message this repo received (routes to the original sender and
|
||||
# marks the original handled; target + "Re: …" subject are inferred)
|
||||
"$CSEND" --reply-to <message-filename> <<'BODY'
|
||||
<message body>
|
||||
BODY
|
||||
|
||||
Sender identity (`--from`) defaults to the basename of the current git toplevel, so
|
||||
you almost never set it. Exit 0 = delivered; exit 2 = usage/IO error (read stderr and
|
||||
fix the arguments rather than retrying blindly).
|
||||
|
||||
## Choosing the target
|
||||
|
||||
- **One named repo** → `--to <repo>`. The repo name is its directory basename; use
|
||||
the name the user gave. No registration exists — sending to a new name just creates
|
||||
that repo's inbox, so a typo silently creates a dead mailbox. If unsure a name is
|
||||
real, check `~/repos/` or existing `~/.claude/coord/<repo>/` before sending.
|
||||
- **Several named repos** ("X and Y") → loop `--to` once per repo with the same
|
||||
subject and body. There is no multi-target flag; the loop is the mechanism:
|
||||
|
||||
for repo in repo-x repo-y; do
|
||||
"$CSEND" --to "$repo" --subject "<subject>" <<'BODY'
|
||||
<body>
|
||||
BODY
|
||||
done
|
||||
|
||||
- **All repos** → `--broadcast`. Prefer this only when the notice genuinely concerns
|
||||
everyone, because broadcasts accumulate and every future first-time repo sees the
|
||||
standing backlog. For a bounded, known set, loop `--to` instead so unrelated future
|
||||
repos don't inherit it.
|
||||
- **A reply to something received** → `--reply-to <filename>`. Use the filename from
|
||||
the injected inbox/archive; it resolves the sender and closes the original in one
|
||||
step.
|
||||
|
||||
## Composing the message
|
||||
|
||||
Derive a short, specific `--subject` from the intent if the user didn't give one
|
||||
("ingest bug fixed", not "update"). In the body, state what happened, what the
|
||||
recipient should know or do, and a pointer to the durable record (commit hash, doc
|
||||
path). Keep it to the point — this is a notice, not a report. Match the repo's
|
||||
language convention for message content; keep it sober and factual.
|
||||
|
||||
## Deferred and conditional sends
|
||||
|
||||
"When Z is done, notify X" means: do **not** send now. Hold the intent — the target,
|
||||
subject, and body — and continue the work. The moment condition Z is satisfied *in
|
||||
this session*, fire the send. This works because you carry the intent across turns
|
||||
within a session.
|
||||
|
||||
Be honest about the limit: a skill cannot persist intent across sessions. If the
|
||||
session is ending and the condition still hasn't been met, surface the pending
|
||||
message to the user so it isn't silently lost — don't imply it will fire later on
|
||||
its own.
|
||||
|
||||
## After sending
|
||||
|
||||
Report what actually happened: the target(s), the subject, and the delivered
|
||||
filename(s) from the script's stdout. If a send failed (exit 2), say so with the
|
||||
stderr reason rather than claiming success. Delivery reaches the recipient at *their*
|
||||
next session start (the receive side is a hook), so tell the user it's queued, not
|
||||
that the other repo has seen it.
|
||||
|
||||
## Examples
|
||||
|
||||
**Example 1 — single repo, composed body**
|
||||
Input: "tell repo-x the ingest bug is fixed, it was commit abc1234"
|
||||
Action: `--to repo-x --subject "ingest bug fixed"`, body naming the fix and
|
||||
pointing at commit abc1234 and the owning doc.
|
||||
|
||||
**Example 2 — broadcast**
|
||||
Input: "let all the repos know the shared spec grew a new validation rule"
|
||||
Action: `--broadcast --subject "spec: new validation rule"`, body summarizing
|
||||
the change and pointing at the spec's owning repo.
|
||||
|
||||
**Example 3 — bounded multi-target**
|
||||
Input: "gi beskjed til repo-x og repo-y om at gaten er re-pinnet"
|
||||
Action: loop `--to repo-x` then `--to repo-y`, same subject "shared gate re-pinned".
|
||||
|
||||
**Example 4 — deferred**
|
||||
Input: "når testene er grønne, varsle repo-y"
|
||||
Action: keep working; once the test run passes this session, send
|
||||
`--to repo-y` with the green result. If the session ends first, surface the
|
||||
pending notice to the user.
|
||||
|
||||
**Example 5 — reply**
|
||||
Input: "svar på coord-meldingen fra repo-x at vi tar det"
|
||||
Action: `--reply-to <that-message-filename>`, body acknowledging and stating the plan.
|
||||
13
tests/selftest.test.mjs
Normal file
13
tests/selftest.test.mjs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Node wrapper (marketplace convention: node --test) around the bash
|
||||
// selftest, which owns every mailbox assertion. The selftest runs against a
|
||||
// throwaway mailbox (mktemp) and exits non-zero on any failing check.
|
||||
import { test } from 'node:test';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
test('coord bash selftest passes', () => {
|
||||
execFileSync('bash', [join(root, 'scripts', 'coord-selftest.sh')], { encoding: 'utf8' });
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue