Compare commits
No commits in common. "main" and "v0.35.0" have entirely different histories.
14 changed files with 26 additions and 448 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "repo-mailbox",
|
"name": "repo-mailbox",
|
||||||
"version": "0.36.1",
|
"version": "0.35.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.",
|
"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": {
|
"author": {
|
||||||
"name": "Kjell Tore Guttormsen"
|
"name": "Kjell Tore Guttormsen"
|
||||||
|
|
|
||||||
54
CHANGELOG.md
54
CHANGELOG.md
|
|
@ -7,60 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [0.36.1] - 2026-09-23
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- **Removed dead references to retired repositories.** A CLAUDE.md
|
|
||||||
paragraph, two board-selftest comments and the 2026-09-03 coordination-debt
|
|
||||||
measurement named repositories that no longer exist, or a real mailbox as an
|
|
||||||
example; all are reworded without the names, and the measured numbers are
|
|
||||||
unchanged. No behaviour change.
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- **`tests/tracked-terms.test.mjs`.** Fails when any tracked path or line
|
|
||||||
matches a term in the untracked `tests/excluded-terms.local.md`. Each term is
|
|
||||||
proven against a known-positive sample before the scan runs. When the list
|
|
||||||
is absent the check skips and says so, rather than passing.
|
|
||||||
|
|
||||||
## [0.36.0] - 2026-09-23
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- **The `dispatch` skill describes a third form: the background session.**
|
|
||||||
When the dispatching session starts the child itself (operator decision
|
|
||||||
2026-09-22), the command is `cd <dir> && claude --model <m> --effort <e>
|
|
||||||
--disallowedTools AskUserQuestion,SendMessage,ListAgents --bg --name <name>
|
|
||||||
"<starter>"`. `--model` sits directly after `claude`, because the budget hook
|
|
||||||
counts a start only in that position. The three tools are disallowed per
|
|
||||||
hardening T4. The ASCII starter claims by full path with `--repo`, compares against NESTE,
|
|
||||||
pins the branch with `git checkout -B`, forbids push/tag/release, and closes
|
|
||||||
by full path, also with `--repo`. Inside a worktree the verbs would otherwise
|
|
||||||
derive the worktree's name as the identity. That was measured: a close
|
|
||||||
without `--repo` exited 1 and left the order in flight. The skill also says what the PM does afterwards: watch the
|
|
||||||
order file, re-measure on the commit, and leave the merge to the operator's
|
|
||||||
push round. The skill description now triggers on the form. `board.sh` is
|
|
||||||
unchanged: the form has no pane, no plan block and no `morning` consumer, so
|
|
||||||
`--target-pane` stays `yes|no`. board-selftest §19b pins all of it (17
|
|
||||||
checks, 14 red before the skill changed).
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- **The background starter signs its report with `--from <repo>`, and
|
|
||||||
`coord-send` refuses a sender derived inside a linked git worktree.** In a
|
|
||||||
worktree, `basename(git toplevel)` is the worktree's name, so the child's
|
|
||||||
report was signed `from: dispatch-bg-form` (measured on 741ada8), a mailbox
|
|
||||||
no session holds. The starter now reports by full path with `--from`
|
|
||||||
(board-selftest §19b, red before the skill changed, with a known-negative
|
|
||||||
control). `coord-send` exits 2 on a derived sender inside a linked worktree,
|
|
||||||
naming `--from` and the repo the worktree belongs to. Linked-ness is git's
|
|
||||||
own test (git-dir differs from git-common-dir), so a submodule, the main
|
|
||||||
checkout, an explicit `--from` and a worktree named like its repo all still
|
|
||||||
send (coord-selftest section 39, 9 checks, 3 red before the fix). The six
|
|
||||||
other scripts that derive identity the same way are unchanged and named as a
|
|
||||||
gap in CLAUDE.md.
|
|
||||||
|
|
||||||
## [0.35.0] - 2026-09-18
|
## [0.35.0] - 2026-09-18
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
61
CLAUDE.md
61
CLAUDE.md
|
|
@ -16,7 +16,7 @@ marketplace plugin. Three components, one boundary:
|
||||||
`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.
|
`coord-sweep.sh` closes the aged FYI backlog machine-wide.
|
||||||
Everything is pinned by `coord-selftest.sh`
|
Everything is pinned by `coord-selftest.sh`
|
||||||
(266 checks, throwaway mailbox via `CLAUDE_COORD_DIR`).
|
(257 checks, throwaway mailbox via `CLAUDE_COORD_DIR`).
|
||||||
|
|
||||||
**`ktg-plugin-marketplace` is a RETIRED `--to` address (operator decision
|
**`ktg-plugin-marketplace` is a RETIRED `--to` address (operator decision
|
||||||
2026-08-15), rejected rather than redirected.** It is a polyrepo directory,
|
2026-08-15), rejected rather than redirected.** It is a polyrepo directory,
|
||||||
|
|
@ -80,33 +80,6 @@ marketplace plugin. Three components, one boundary:
|
||||||
`coord-send.sh`'s own guard comment protects dot names as real repos
|
`coord-send.sh`'s own guard comment protects dot names as real repos
|
||||||
(`basename` of a git toplevel under a hidden directory).
|
(`basename` of a git toplevel under a hidden directory).
|
||||||
|
|
||||||
**A sender derived inside a LINKED WORKTREE is refused, not signed (order
|
|
||||||
20260923T082933Z-466405184, 2026-09-23).** A background session runs in
|
|
||||||
`<repo>/.claude/worktrees/<name>`, and `basename(git toplevel)` there is
|
|
||||||
`<name>`: measured by the PM on 741ada8, the child's report arrived signed
|
|
||||||
`from: dispatch-bg-form`, a mailbox no session holds, so a reply to it goes
|
|
||||||
nowhere. That is section 22's "invented identity" in a new place, and it gets
|
|
||||||
section 22's answer: exit 2, naming `--from` and the repo the worktree belongs
|
|
||||||
to. Refusal was chosen over a warning because stderr is where a session least
|
|
||||||
looks and the message would already be delivered, and over a silent redirect
|
|
||||||
to the main repo's name for the reason the retired `ktg-plugin-marketplace`
|
|
||||||
address is rejected rather than redirected. Linked-ness is git's own test
|
|
||||||
(`--git-dir` differs from `--git-common-dir`), never a path pattern and never
|
|
||||||
".git is a file", because a SUBMODULE also has `.git` as a file and its
|
|
||||||
basename IS its name. Selftest section 39 pins that control, the main
|
|
||||||
checkout, an explicit `--from` inside the worktree, and a worktree directory
|
|
||||||
that carries the repo's own name. It fails OPEN when git cannot answer. The
|
|
||||||
dispatch skill's background starter now signs its report with `--from`
|
|
||||||
(board-selftest §19b), so the refusal is the net, not the path.
|
|
||||||
**Bounded gap, stated rather than closed:** six more scripts derive identity
|
|
||||||
the same way (`coord-done`, `coord-inbox`, `coord-order-send`,
|
|
||||||
`coord-order-claim`, `coord-order-done`, `coord-order-inbox` - measured with
|
|
||||||
`grep -n 'rev-parse --show-toplevel' scripts/*.sh`). The order named
|
|
||||||
`coord-send` only. `coord-order-done` already fails loudly (exit 1, "no
|
|
||||||
claimed order"); `coord-done` does not - measured inside a worktree, it prints
|
|
||||||
`no inbox for <worktree-name>` on stderr and exits 0, having archived nothing.
|
|
||||||
Reported to `.claude` for its own order.
|
|
||||||
|
|
||||||
**`coord-sweep.sh` is the only path that closes a message with no human in
|
**`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
|
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
|
one mechanically decidable class - `reply-expected: no`, older than the grace
|
||||||
|
|
@ -494,7 +467,7 @@ marketplace plugin. Three components, one boundary:
|
||||||
- **Board (`scripts/board.sh`):** cross-repo attention board. Reads STATE.md
|
- **Board (`scripts/board.sh`):** cross-repo attention board. Reads STATE.md
|
||||||
next-step blocks + board lines, `git status`, and mailbox pending counts, and
|
next-step blocks + board lines, `git status`, and mailbox pending counts, and
|
||||||
prints one line per repo. Read-only by construction: it writes to no repo, no
|
prints one line per repo. Read-only by construction: it writes to no repo, no
|
||||||
STATE.md and no mailbox. Pinned by `board-selftest.sh` (497 checks).
|
STATE.md and no mailbox. Pinned by `board-selftest.sh` (478 checks).
|
||||||
|
|
||||||
**It lives here because the mailbox is one of its three inputs, and it carries
|
**It lives here because the mailbox is one of its three inputs, and it carries
|
||||||
the same axis distinction the mailbox does.** A pending count means *others
|
the same axis distinction the mailbox does.** A pending count means *others
|
||||||
|
|
@ -942,7 +915,7 @@ marketplace plugin. Three components, one boundary:
|
||||||
before anything was written: **12 nested repos across the tree, exactly 1
|
before anything was written: **12 nested repos across the tree, exactly 1
|
||||||
with a STATE.md** (`from-ai-to-chitta/content-sadhguru`, which had been
|
with a STATE.md** (`from-ai-to-chitta/content-sadhguru`, which had been
|
||||||
running work and reporting to nobody). The other 11 are vendored or
|
running work and reporting to nobody). The other 11 are vendored or
|
||||||
experimental checkouts; the operator's decision is
|
experimental checkouts under `claude-code-100x/`; the operator's decision is
|
||||||
that they stay invisible ON PURPOSE - they do not even reach the `UTEN
|
that they stay invisible ON PURPOSE - they do not even reach the `UTEN
|
||||||
STATE.md` bucket, because that bucket is for repos someone opens. Verified
|
STATE.md` bucket, because that bucket is for repos someone opens. Verified
|
||||||
after the fix with `board.sh`, not from memory:
|
after the fix with `board.sh`, not from memory:
|
||||||
|
|
@ -1541,25 +1514,6 @@ marketplace plugin. Three components, one boundary:
|
||||||
sits in choosing the four trait values, and the skill must never reason its
|
sits in choosing the four trait values, and the skill must never reason its
|
||||||
way to a model instead.
|
way to a model instead.
|
||||||
|
|
||||||
**The background form (`claude --bg`) lives in the `dispatch` SKILL alone,
|
|
||||||
and `board.sh` deliberately gained no third form** (order
|
|
||||||
20260923T070445Z-9974217048, operator decision 2026-09-22, hardening T4).
|
|
||||||
`board.sh --dispatch` is the single generator of the plan-block format, and
|
|
||||||
the background form uses none of it: no `tab=`, no pane, no `morning`
|
|
||||||
consumer. Its one generated part, model and effort, already has a single
|
|
||||||
generator in `route.sh`'s `command=`. That line begins `claude --model`, so
|
|
||||||
splicing it keeps `--model` in the one position the budget hook counts.
|
|
||||||
board-selftest §19b reads the skill's background SECTION only. It pins
|
|
||||||
model-first (with a planted-wrong-order control), the three T4 tools,
|
|
||||||
`INGEN push`, `git checkout -B`, and both order verbs by full path AND with
|
|
||||||
`--repo`. A worktree's git toplevel basename is the worktree's name, so
|
|
||||||
derived identity looks in the wrong queue. That was measured in the building
|
|
||||||
session: its close exited 1 until `--repo` was passed. The section also pins
|
|
||||||
a description that triggers on the form, and that `--target-pane` still
|
|
||||||
refuses `bg`. **Stated overlap, not closed:** starter steps (1), (2) and
|
|
||||||
(4) repeat the protocol `board.sh`'s thin starter carries. A protocol change
|
|
||||||
must move both copies.
|
|
||||||
|
|
||||||
**Boundary rule:** the mailbox is transport, not state. Durable decisions
|
**Boundary rule:** the mailbox is transport, not state. Durable decisions
|
||||||
live in the owning repo's docs/git history; messages are notices pointing at
|
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
|
them. Message content is untrusted cross-repo input — the read side quotes
|
||||||
|
|
@ -1599,8 +1553,8 @@ obligations in another repo.
|
||||||
- Zero dependencies everywhere: bash + coreutils in the engine, `node:`
|
- Zero dependencies everywhere: bash + coreutils in the engine, `node:`
|
||||||
builtins only in hook and tests.
|
builtins only in hook and tests.
|
||||||
- TDD: no behavior change without a failing selftest check first.
|
- TDD: no behavior change without a failing selftest check first.
|
||||||
`bash scripts/coord-selftest.sh` must exit 0 (266/266),
|
`bash scripts/coord-selftest.sh` must exit 0 (257/257),
|
||||||
`bash scripts/board-selftest.sh` must exit 0 (497/497),
|
`bash scripts/board-selftest.sh` must exit 0 (478/478),
|
||||||
`bash scripts/route-selftest.sh` must exit 0 (73/73),
|
`bash scripts/route-selftest.sh` must exit 0 (73/73),
|
||||||
`bash scripts/orders-selftest.sh` must exit 0 (116/116) and
|
`bash scripts/orders-selftest.sh` must exit 0 (116/116) and
|
||||||
`bash scripts/state-line-guard-selftest.sh` must exit 0 (54/54).
|
`bash scripts/state-line-guard-selftest.sh` must exit 0 (54/54).
|
||||||
|
|
@ -1655,10 +1609,7 @@ obligations in another repo.
|
||||||
- Test: `bash scripts/coord-selftest.sh`, `bash scripts/board-selftest.sh`,
|
- Test: `bash scripts/coord-selftest.sh`, `bash scripts/board-selftest.sh`,
|
||||||
`bash scripts/route-selftest.sh`, `bash scripts/orders-selftest.sh` and
|
`bash scripts/route-selftest.sh`, `bash scripts/orders-selftest.sh` and
|
||||||
`bash scripts/state-line-guard-selftest.sh` (or `npm test`, the Node wrapper
|
`bash scripts/state-line-guard-selftest.sh` (or `npm test`, the Node wrapper
|
||||||
around all five plus the hook tests and the README-number check).
|
around all five plus the hook tests and the README-number check)
|
||||||
`tests/tracked-terms.test.mjs` fails when any tracked path or line matches
|
|
||||||
a term in the untracked `tests/excluded-terms.local.md`, and SKIPS, loudly,
|
|
||||||
when that file is absent - the term list itself is never committed.
|
|
||||||
- Order queue smoke test: `CLAUDE_COORD_DIR=$(mktemp -d) bash
|
- Order queue smoke test: `CLAUDE_COORD_DIR=$(mktemp -d) bash
|
||||||
scripts/coord-order-send.sh --to smoke --from tester --subject s --message m`
|
scripts/coord-order-send.sh --to smoke --from tester --subject s --message m`
|
||||||
then `CLAUDE_COORD_DIR=<same> bash scripts/coord-order-inbox.sh --repo smoke`
|
then `CLAUDE_COORD_DIR=<same> bash scripts/coord-order-inbox.sh --repo smoke`
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -10,11 +10,11 @@ Session A in repo X leaves a message for repo Y; the next session in repo Y gets
|
||||||
|
|
||||||
*AI-generated: all code produced by Claude Code through dialog-driven development.*
|
*AI-generated: all code produced by Claude Code through dialog-driven development.*
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -98,8 +98,6 @@ Scoring is judgement and belongs to the skill; turning scores into a row is a lo
|
||||||
|
|
||||||
**Handing a task to another session (the `dispatch` skill).** "Start a session in repo X on this order." `board.sh --dispatch` turns that into one line the operator can paste: the order written to a prompt file, the model and effort looked up from the same row table `route.sh` uses, and the prompt passed **in argv** — `claude --model … --effort … "$(cat <file>)"` — so the session is handed its task instead of having to guess it out of STATE.md. It emits one of two forms, and which one is a measurement rather than a preference: a repo with no terminal pane gets a plan block (`morning --plan-file <f> --no-go`), while a repo that already has one gets a bare paste line for that tab, because a plan block for an already-open repo is silently dropped by the driver and reads as a broken plan file. `--target-pane yes|no` is therefore required with no default, exactly as `route.sh` refuses to default `--last-effort`: it is a fact about the world, and this plugin never looks for a terminal itself. Read-only holds — the prompt file and the plan file are written by the caller, never by `board.sh`.
|
**Handing a task to another session (the `dispatch` skill).** "Start a session in repo X on this order." `board.sh --dispatch` turns that into one line the operator can paste: the order written to a prompt file, the model and effort looked up from the same row table `route.sh` uses, and the prompt passed **in argv** — `claude --model … --effort … "$(cat <file>)"` — so the session is handed its task instead of having to guess it out of STATE.md. It emits one of two forms, and which one is a measurement rather than a preference: a repo with no terminal pane gets a plan block (`morning --plan-file <f> --no-go`), while a repo that already has one gets a bare paste line for that tab, because a plan block for an already-open repo is silently dropped by the driver and reads as a broken plan file. `--target-pane yes|no` is therefore required with no default, exactly as `route.sh` refuses to default `--last-effort`: it is a fact about the world, and this plugin never looks for a terminal itself. Read-only holds — the prompt file and the plan file are written by the caller, never by `board.sh`.
|
||||||
|
|
||||||
**A third form: the background session.** When the dispatching session starts the child itself, the skill describes `claude --model <m> --effort <e> --disallowedTools AskUserQuestion,SendMessage,ListAgents --bg --name <name> "<starter>"`. `--model` comes directly after `claude`, and the model and effort are taken from `route.sh`. The three disallowed tools are there because a background child cannot be answered: a question stops the child, and a message reaching back to its parent is a direction nobody asked for. The starter tells the child to pin its branch with `git checkout -B`, to commit there, and to push, tag or release nothing. Merging and publishing belong to the operator. This form lives in the skill alone. It has no pane, no plan block and no `morning` consumer, so `board.sh --dispatch` still emits exactly the two forms above.
|
|
||||||
|
|
||||||
**Making the order outlive the tab (the order queue).** A prompt file passed through argv dies with the pane it was typed into, and nothing in the receiving repo records that an order ever arrived. Measured 2026-08-17: one order was dispatched three times over 90 minutes before it was worked, because the first two tabs ran something else. So dispatch now delivers the order into the recipient's own queue — `~/.claude/coord/<repo>/orders/` — and the pasted line becomes a thin **starter** carrying only the order id. The order text has one home. If the tab is never run, nothing is lost: the order stays pending, is re-injected at every session start in that repo, and shows up in `board`'s ORDRE column, which sits beside INN and is never summed with it — INN is "others are waiting on you", ORDRE is "work is waiting on this repo".
|
**Making the order outlive the tab (the order queue).** A prompt file passed through argv dies with the pane it was typed into, and nothing in the receiving repo records that an order ever arrived. Measured 2026-08-17: one order was dispatched three times over 90 minutes before it was worked, because the first two tabs ran something else. So dispatch now delivers the order into the recipient's own queue — `~/.claude/coord/<repo>/orders/` — and the pasted line becomes a thin **starter** carrying only the order id. The order text has one home. If the tab is never run, nothing is lost: the order stays pending, is re-injected at every session start in that repo, and shows up in `board`'s ORDRE column, which sits beside INN and is never summed with it — INN is "others are waiting on you", ORDRE is "work is waiting on this repo".
|
||||||
|
|
||||||
Ownership is explicit rather than implied. An order is pending until a session **claims** it, and the claim is a rename with no check-then-act step, so of any number of racing sessions exactly one wins and the rest get a clean refusal. The claiming session owns it until it either closes it with a commit pointer or **returns** it with a reason recorded in the order itself. At claim time the session is told to compare the order against its own `STATE.md` next step and to state any divergence in its first reply — a dispatch that displaces a live next step is a decision, and this makes it an uttered one. A session that claims an order and dies is the one remaining way an order could vanish, so claimed orders stay visible in the injection with their in-flight age; that is a visible-again rule, not a lease timer, because nothing here can know that a session is dead.
|
Ownership is explicit rather than implied. An order is pending until a session **claims** it, and the claim is a rename with no check-then-act step, so of any number of racing sessions exactly one wins and the rest get a clean refusal. The claiming session owns it until it either closes it with a commit pointer or **returns** it with a reason recorded in the order itself. At claim time the session is told to compare the order against its own `STATE.md` next step and to state any divergence in its first reply — a dispatch that displaces a live next step is a decision, and this makes it an uttered one. A session that claims an order and dies is the one remaining way an order could vanish, so claimed orders stay visible in the injection with their in-flight age; that is a visible-again rule, not a lease timer, because nothing here can know that a session is dead.
|
||||||
|
|
@ -202,8 +200,8 @@ Note that raising the inbox's priority (Rule 7) deliberately does **not** widen
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
bash scripts/coord-selftest.sh # 266 checks against a throwaway mailbox
|
bash scripts/coord-selftest.sh # 257 checks against a throwaway mailbox
|
||||||
bash scripts/board-selftest.sh # 497 checks against a throwaway repo tree
|
bash scripts/board-selftest.sh # 478 checks against a throwaway repo tree
|
||||||
bash scripts/route-selftest.sh # 73 checks, incl. the route->board round trip
|
bash scripts/route-selftest.sh # 73 checks, incl. the route->board round trip
|
||||||
bash scripts/orders-selftest.sh # 116 checks, incl. the 20-way barriered claim race
|
bash scripts/orders-selftest.sh # 116 checks, incl. the 20-way barriered claim race
|
||||||
bash scripts/state-line-guard-selftest.sh # 54 checks, incl. the Edit replace_all projection and the ratchet
|
bash scripts/state-line-guard-selftest.sh # 54 checks, incl. the Edit replace_all projection and the ratchet
|
||||||
|
|
|
||||||
|
|
@ -117,10 +117,10 @@ denominator.
|
||||||
claude-playlist-corpus 1 0 -
|
claude-playlist-corpus 1 0 -
|
||||||
graceful-handoff 3 0 -
|
graceful-handoff 3 0 -
|
||||||
human-friendly-style 1 0 -
|
human-friendly-style 1 0 -
|
||||||
(retired repository A) 1 0 -
|
ki-produktivitetsmodell 1 0 -
|
||||||
llm-ingestion-guard 1 0 25
|
llm-ingestion-guard 1 0 25
|
||||||
llm-security 2 0 -
|
llm-security 2 0 -
|
||||||
(retired repository B) 2 0 -
|
mcp-servere 2 0 -
|
||||||
okr 1 0 -
|
okr 1 0 -
|
||||||
org-ops 5 2 -
|
org-ops 5 2 -
|
||||||
portfolio-optimiser-commons 3 0 -
|
portfolio-optimiser-commons 3 0 -
|
||||||
|
|
@ -142,10 +142,10 @@ something it must never touch, and is a defect, not a result.
|
||||||
gap is entirely the moving cutoff.** Verified two ways rather than assumed.
|
gap is entirely the moving cutoff.** Verified two ways rather than assumed.
|
||||||
Yesterday's 14-day cutoff was ~20260820T184902; today's is 20260821T055442.
|
Yesterday's 14-day cutoff was ~20260820T184902; today's is 20260821T055442.
|
||||||
Four notices timestamped 2026-08-20 between those two instants
|
Four notices timestamped 2026-08-20 between those two instants
|
||||||
(`human-friendly-style` T210113Z, retired repository A T205611Z, `org-ops`
|
(`human-friendly-style` T210113Z, `ki-produktivitetsmodell` T205611Z, `org-ops`
|
||||||
T210955Z and T211358Z) crossed the boundary in one calendar day: 7 + 4 = 11.
|
T210955Z and T211358Z) crossed the boundary in one calendar day: 7 + 4 = 11.
|
||||||
Independently, `--days 15` today yields 6, and those 6 plus
|
Independently, `--days 15` today yields 6, and those 6 plus
|
||||||
retired repository B's `20260820T104644Z` - which sits between the 15-day cutoff and
|
`mcp-servere/20260820T104644Z` - which sits between the 15-day cutoff and
|
||||||
yesterday's 14-day one - reconstruct yesterday's 7 exactly. The engine is
|
yesterday's 14-day one - reconstruct yesterday's 7 exactly. The engine is
|
||||||
consistent; the 7 was a measurement of a moment, never a constant, and reading
|
consistent; the 7 was a measurement of a moment, never a constant, and reading
|
||||||
it as one would have been face 3 of the verification law pointed at our own
|
it as one would have been face 3 of the verification law pointed at our own
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "repo-mailbox",
|
"name": "repo-mailbox",
|
||||||
"version": "0.36.1",
|
"version": "0.35.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
||||||
|
|
@ -1486,7 +1486,7 @@ mkrepo "$IP_ROOT/ip-repo-clean"
|
||||||
} > "$IP_ROOT/ip-repo-clean/STATE.md"
|
} > "$IP_ROOT/ip-repo-clean/STATE.md"
|
||||||
|
|
||||||
# ip-orphan: a mailbox with pending mail but NO matching repo directory
|
# ip-orphan: a mailbox with pending mail but NO matching repo directory
|
||||||
# anywhere under the scanned roots (the ktg-plugin-marketplace / old-sandbox /
|
# anywhere under the scanned roots (the ktg-plugin-marketplace / svv /
|
||||||
# llm-ingestion-guard case: "ingen katalog", never opened as a tab).
|
# llm-ingestion-guard case: "ingen katalog", never opened as a tab).
|
||||||
mkdir -p "$IP_COORD/ip-orphan/inbox"
|
mkdir -p "$IP_COORD/ip-orphan/inbox"
|
||||||
echo "msg" > "$IP_COORD/ip-orphan/inbox/2026-msg1-from-w.md"
|
echo "msg" > "$IP_COORD/ip-orphan/inbox/2026-msg1-from-w.md"
|
||||||
|
|
@ -1919,81 +1919,6 @@ check "dispatch SKILL.md's ORDER assignment carries no fallback" "$rc"
|
||||||
grep -q -- '--order-id' "$DSKILL"
|
grep -q -- '--order-id' "$DSKILL"
|
||||||
check "dispatch SKILL.md dispatches on --order-id" $?
|
check "dispatch SKILL.md dispatches on --order-id" $?
|
||||||
|
|
||||||
# --- 19b. The dispatch skill's BACKGROUND form -----------------------------
|
|
||||||
# Order 20260923T070445Z-9974217048 (.claude; operator decision 2026-09-22):
|
|
||||||
# the PM starts sessions itself again with `claude --bg`, and hardening T4
|
|
||||||
# says every such child is started with AskUserQuestion, SendMessage and
|
|
||||||
# ListAgents disallowed and told that the operator pushes. The form had been
|
|
||||||
# used for 15+ starts and was written down nowhere in the skill. Every check
|
|
||||||
# reads the background SECTION only, never the whole file, so prose elsewhere
|
|
||||||
# (the far-end section's bare verb names, for one) cannot satisfy or trip it.
|
|
||||||
dsk_bg="$(awk '/^## The background form/{f=1; print; next} f && /^## /{exit} f' "$DSKILL")"
|
|
||||||
[ -n "$dsk_bg" ]; check "dispatch SKILL.md has a '## The background form' section" $?
|
|
||||||
dsk_bgl="$(printf '%s\n' "$dsk_bg" | grep -E 'claude .*--bg( |$)')"
|
|
||||||
[ -n "$dsk_bgl" ]; check "background section carries a 'claude ... --bg' command line" $?
|
|
||||||
# --model DIRECTLY after claude: pre-dispatch-budget.sh counts a start only in
|
|
||||||
# that position (measured 2026-09-22 - a valued flag before --model breaks it).
|
|
||||||
# The predicate is a function so its known-positive control runs the SAME code.
|
|
||||||
dsk_model_first() { printf '%s\n' "$1" | grep -E 'claude .*--bg( |$)' | grep -vqE 'claude --model [^ ]+ '; }
|
|
||||||
if [ -n "$dsk_bgl" ] && ! dsk_model_first "$dsk_bgl"; then rc=0; else rc=1; fi
|
|
||||||
check "every 'claude --bg' line has --model directly after 'claude'" "$rc"
|
|
||||||
dsk_model_first 'cd /x && claude --bg --name x --model sonnet --effort high "go"'
|
|
||||||
check "control: the --model-first predicate catches a planted '--bg --name x --model' line" $?
|
|
||||||
dsk_dis="$(printf '%s\n' "$dsk_bgl" | sed -n 's/.*--disallowedTools \([^ ]*\).*/\1/p' | head -1)"
|
|
||||||
for t in AskUserQuestion SendMessage ListAgents; do
|
|
||||||
case ",$dsk_dis," in *",$t,"*) rc=0 ;; *) rc=1 ;; esac
|
|
||||||
check "background command disallows $t (T4)" "$rc"
|
|
||||||
done
|
|
||||||
printf '%s\n' "$dsk_bg" | grep -q 'INGEN push'
|
|
||||||
check "background starter says INGEN push (the operator pushes)" $?
|
|
||||||
printf '%s\n' "$dsk_bg" | grep -q 'git checkout -B '
|
|
||||||
check "background starter pins the branch with 'git checkout -B'" $?
|
|
||||||
# The verbs are not on PATH (ORDRE 65): a bg child executes the starter
|
|
||||||
# literally, so both must be called by full path, and no line may start with
|
|
||||||
# the bare verb as a command.
|
|
||||||
printf '%s\n' "$dsk_bg" | grep -q 'bash [^ ]*/scripts/coord-order-claim\.sh '
|
|
||||||
check "background starter claims by FULL path to coord-order-claim.sh" $?
|
|
||||||
printf '%s\n' "$dsk_bg" | grep -q 'bash [^ ]*/scripts/coord-order-done\.sh '
|
|
||||||
check "background starter closes by FULL path to coord-order-done.sh" $?
|
|
||||||
# ...and both carry --repo. A background child runs in a WORKTREE, whose git
|
|
||||||
# toplevel basename is the worktree's name, not the repo's, so a verb that
|
|
||||||
# derives its identity looks in the wrong queue. Measured 2026-09-23 in the
|
|
||||||
# session that built this section: claim ran before the worktree and won;
|
|
||||||
# done ran inside it and exited 1 "no claimed order ... for dispatch-bg-form".
|
|
||||||
printf '%s\n' "$dsk_bg" | grep -q 'coord-order-claim\.sh --repo [^ ]* '
|
|
||||||
check "background starter claims with --repo (a worktree's name is not the repo's)" $?
|
|
||||||
printf '%s\n' "$dsk_bg" | grep -q 'coord-order-done\.sh --repo [^ ]* '
|
|
||||||
check "background starter closes with --repo (a worktree's name is not the repo's)" $?
|
|
||||||
if printf '%s\n' "$dsk_bg" | grep -qE '^[[:space:]]*coord-order-(claim|done) '; then rc=1; else rc=0; fi
|
|
||||||
check "background section has no bare coord-order-claim/-done command line" "$rc"
|
|
||||||
# ...and the REPORT is signed with --from, by full path. coord-send derives its
|
|
||||||
# sender from the same toplevel basename, so in the worktree the report was
|
|
||||||
# signed `from: dispatch-bg-form` (measured by the PM on 741ada8, order
|
|
||||||
# 20260923T082933Z-466405184). The predicate reads the starter BLOCK only and
|
|
||||||
# is a function, so the known-negative control runs the same code: every
|
|
||||||
# mention of coord-send in the starter must be the full-path call with --from.
|
|
||||||
dsk_starter="$(printf '%s\n' "$dsk_bg" | awk '/^```text/{f=1; next} f && /^```/{exit} f')"
|
|
||||||
dsk_unsigned_report() {
|
|
||||||
n_all="$(printf '%s\n' "$1" | grep -o 'coord-send' | wc -l | tr -d ' ')"
|
|
||||||
n_ok="$(printf '%s\n' "$1" | grep -oE 'bash [^ ]*/scripts/coord-send\.sh --from [^ ]+ ' | wc -l | tr -d ' ')"
|
|
||||||
[ "$n_all" -eq 0 ] || [ "$n_ok" -ne "$n_all" ]
|
|
||||||
}
|
|
||||||
[ -n "$dsk_starter" ] && ! dsk_unsigned_report "$dsk_starter"
|
|
||||||
check "background starter reports by FULL path to coord-send.sh with --from (a worktree's name is not the repo's)" $?
|
|
||||||
dsk_unsigned_report 'ORDRE x ... (4) Lukk ... Rapport som coord til .claude via coord-send.'
|
|
||||||
check "control: the report predicate catches a starter that says 'via coord-send' without --from" $?
|
|
||||||
# The description is what makes a skill trigger; a form it does not mention is
|
|
||||||
# a form nobody reaches through the front door.
|
|
||||||
dsk_fm="$(awk 'NR==1 && /^---$/{f=1; next} f && /^---$/{exit} f' "$DSKILL")"
|
|
||||||
printf '%s\n' "$dsk_fm" | grep -q -- 'claude --bg'
|
|
||||||
check "dispatch SKILL.md description names 'claude --bg'" $?
|
|
||||||
printf '%s\n' "$dsk_fm" | grep -qi 'bakgrunn'
|
|
||||||
check "dispatch SKILL.md description carries a Norwegian background trigger" $?
|
|
||||||
# Known-negative: the background form is the SKILL's, not a third board.sh
|
|
||||||
# rendering - --target-pane stays exactly yes|no.
|
|
||||||
"$BOARD" --roots "$ROOT" --dispatch --repo repo-a --prompt-file "$DSP" --target-pane bg $DSPTRAITS >/dev/null 2>&1; rc=$?
|
|
||||||
[ "$rc" -eq 2 ]; check "control: --target-pane still refuses 'bg' (board.sh gained no third form)" $?
|
|
||||||
|
|
||||||
# --- 20. The ORDRE column ---------------------------------------------------
|
# --- 20. The ORDRE column ---------------------------------------------------
|
||||||
# Two mailbox-derived columns, never summed and never merged: INN is "others
|
# Two mailbox-derived columns, never summed and never merged: INN is "others
|
||||||
# are waiting on YOU", ORDRE is "authorized work is waiting on this REPO".
|
# are waiting on YOU", ORDRE is "authorized work is waiting on this REPO".
|
||||||
|
|
@ -3151,7 +3076,7 @@ check "pendingage: an unparseable filename reads ?, never a fabricated 0" $?
|
||||||
# --- 31. Nested git repos with a STATE.md (admission a) ---------------------
|
# --- 31. Nested git repos with a STATE.md (admission a) ---------------------
|
||||||
# Operator decision 2026-09-03, order 20260903T190201Z-238406410-from-.claude:
|
# Operator decision 2026-09-03, order 20260903T190201Z-238406410-from-.claude:
|
||||||
# a git repo nested at depth 2 UNDER a directory that is itself a git repo
|
# a git repo nested at depth 2 UNDER a directory that is itself a git repo
|
||||||
# (e.g. from-ai-to-chitta/*) was invisible to the board - the
|
# (claude-code-100x/*, from-ai-to-chitta/*) was invisible to the board - the
|
||||||
# discovery loop adds a depth-1 repo and never looks inside it, and the
|
# discovery loop adds a depth-1 repo and never looks inside it, and the
|
||||||
# else-branch container scan is reached only when the depth-1 entry is NOT a
|
# else-branch container scan is reached only when the depth-1 entry is NOT a
|
||||||
# repo. Admission is criterion (a) and nothing wider: such a repo enters the
|
# repo. Admission is criterion (a) and nothing wider: such a repo enters the
|
||||||
|
|
|
||||||
|
|
@ -1388,46 +1388,6 @@ check "launchd control: a program path INSIDE the plugin cache is caught" $?
|
||||||
|
|
||||||
/bin/rm -f "$BADPL" "$CACHEPL" 2>/dev/null
|
/bin/rm -f "$BADPL" "$CACHEPL" 2>/dev/null
|
||||||
|
|
||||||
# 39. A LINKED WORKTREE's directory name is not a repo name, so a sender derived
|
|
||||||
# from it is invented, and section 22's rule applies: refuse, never guess.
|
|
||||||
# Order 20260923T082933Z-466405184 (.claude): a background child runs in
|
|
||||||
# <repo>/.claude/worktrees/<name>, and basename(git toplevel) there is <name>.
|
|
||||||
# Measured by the PM on 741ada8: the child's report arrived signed
|
|
||||||
# `from: dispatch-bg-form`, a mailbox no session holds, so any reply to it
|
|
||||||
# would have gone nowhere. Linked-ness is git's own test (git-dir differs from
|
|
||||||
# git-common-dir), not a path pattern and not ".git is a file": a SUBMODULE also
|
|
||||||
# has .git as a file, and its basename IS its real name - the control below.
|
|
||||||
WT="$(cd "$(mktemp -d)" && pwd -P)"
|
|
||||||
git -C "$WT" init -q wtmain >/dev/null 2>&1
|
|
||||||
git -C "$WT/wtmain" -c user.name=t -c user.email=t@t -c commit.gpgSign=false commit -q --allow-empty -m init >/dev/null 2>&1
|
|
||||||
git -C "$WT/wtmain" worktree add -q "$WT/wt-child" >/dev/null 2>&1
|
|
||||||
[ -f "$WT/wt-child/.git" ]; check "worktree fixture: wt-child really is a linked worktree (ground truth)" $?
|
|
||||||
wto="$( (cd "$WT/wt-child" && "$SEND" --to wttarget --subject s --message "WT-BODY" </dev/null) 2>&1 )"; rc=$?
|
|
||||||
[ "$rc" -eq 2 ]; check "worktree: a sender derived in a linked worktree is refused (exit 2)" $?
|
|
||||||
[ -z "$(ls "$CLAUDE_COORD_DIR/wttarget/inbox" 2>/dev/null)" ]; check "worktree: nothing is delivered under the worktree's name" $?
|
|
||||||
printf '%s' "$wto" | grep -q -- '--from wtmain'; check "worktree: the refusal names --from and the repo the worktree belongs to" $?
|
|
||||||
wtok="$( (cd "$WT/wt-child" && "$SEND" --to wttarget --from wtmain --subject s --message "WT-OK" </dev/null) 2>&1 )"; rc=$?
|
|
||||||
[ "$rc" -eq 0 ] && grep -q '^from: wtmain$' "$CLAUDE_COORD_DIR"/wttarget/inbox/*.md 2>/dev/null
|
|
||||||
check "worktree control: an explicit --from still sends from inside the worktree" $?
|
|
||||||
wtm="$( (cd "$WT/wtmain" && "$SEND" --to wtmaintarget --subject s --message "WT-MAIN" </dev/null) 2>&1 )"; rc=$?
|
|
||||||
[ "$rc" -eq 0 ] && grep -q '^from: wtmain$' "$CLAUDE_COORD_DIR"/wtmaintarget/inbox/*.md 2>/dev/null
|
|
||||||
check "worktree control: the MAIN checkout of the same repo still derives its sender" $?
|
|
||||||
# A worktree whose directory happens to carry the repo's own name derives the
|
|
||||||
# right identity, so refusing it would be a false positive.
|
|
||||||
mkdir -p "$WT/elsewhere"
|
|
||||||
git -C "$WT/wtmain" worktree add -q "$WT/elsewhere/wtmain" >/dev/null 2>&1
|
|
||||||
( cd "$WT/elsewhere/wtmain" && "$SEND" --to wtsame --subject s --message "WT-SAME" </dev/null >/dev/null 2>&1 ); rc=$?
|
|
||||||
[ "$rc" -eq 0 ]; check "worktree control: a worktree named exactly like its repo is not refused" $?
|
|
||||||
# Submodule control: .git is a file there too, and git-dir == git-common-dir.
|
|
||||||
git -C "$WT" init -q wtsub >/dev/null 2>&1
|
|
||||||
git -C "$WT/wtsub" -c user.name=t -c user.email=t@t -c commit.gpgSign=false commit -q --allow-empty -m init >/dev/null 2>&1
|
|
||||||
git -C "$WT/wtmain" -c protocol.file.allow=always submodule add -q "$WT/wtsub" wtsub >/dev/null 2>&1
|
|
||||||
[ -f "$WT/wtmain/wtsub/.git" ]; check "submodule fixture: .git really is a file in the submodule (ground truth)" $?
|
|
||||||
( cd "$WT/wtmain/wtsub" && "$SEND" --to wtsubtarget --subject s --message "WT-SUB" </dev/null >/dev/null 2>&1 ); rc=$?
|
|
||||||
[ "$rc" -eq 0 ] && grep -q '^from: wtsub$' "$CLAUDE_COORD_DIR"/wtsubtarget/inbox/*.md 2>/dev/null
|
|
||||||
check "worktree control: a SUBMODULE (.git is a file) still derives its own name" $?
|
|
||||||
/bin/rm -rf "$WT" 2>/dev/null
|
|
||||||
|
|
||||||
echo "----"
|
echo "----"
|
||||||
echo "PASS=$PASS FAIL=$FAIL"
|
echo "PASS=$PASS FAIL=$FAIL"
|
||||||
[ "$FAIL" -eq 0 ]
|
[ "$FAIL" -eq 0 ]
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,6 @@
|
||||||
# the broadcast queue so no future repo receives it. This is un-send, not
|
# the broadcast queue so no future repo receives it. This is un-send, not
|
||||||
# recall - repos that already received it are unaffected.
|
# recall - repos that already received it are unaffected.
|
||||||
# --from overrides the sender/self identity (default: basename of git toplevel/cwd).
|
# --from overrides the sender/self identity (default: basename of git toplevel/cwd).
|
||||||
# Inside a linked git worktree the default is refused (exit 2): the toplevel's
|
|
||||||
# basename there is the worktree's name, not the repo's - pass --from.
|
|
||||||
#
|
#
|
||||||
# Exit: 0 delivered, 1 delivered but --reply-to's original could NOT be closed
|
# Exit: 0 delivered, 1 delivered but --reply-to's original could NOT be closed
|
||||||
# (the reply is sent; do not re-send it, close the original by hand),
|
# (the reply is sent; do not re-send it, close the original by hand),
|
||||||
|
|
@ -64,33 +62,13 @@ done
|
||||||
# a real message was delivered under exactly that name. An invented identity is
|
# a real message was delivered under exactly that name. An invented identity is
|
||||||
# worse than none: it signs mail as a repo that does not exist and, on the read
|
# worse than none: it signs mail as a repo that does not exist and, on the read
|
||||||
# side, opens a mailbox that may belong to someone else. Refuse and say how.
|
# side, opens a mailbox that may belong to someone else. Refuse and say how.
|
||||||
DERIVED_FROM=0
|
|
||||||
if [ -z "$FROM" ]; then
|
if [ -z "$FROM" ]; then
|
||||||
FROM="$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null)"
|
FROM="$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null)"
|
||||||
DERIVED_FROM=1
|
|
||||||
fi
|
fi
|
||||||
if [ -z "$FROM" ]; then
|
if [ -z "$FROM" ]; then
|
||||||
echo "coord-send: cannot resolve sender identity (not inside a git repo); pass --from <repo> to choose one explicitly" >&2
|
echo "coord-send: cannot resolve sender identity (not inside a git repo); pass --from <repo> to choose one explicitly" >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
# The same rule for a LINKED WORKTREE: its toplevel basename is the worktree's
|
|
||||||
# directory name (<repo>/.claude/worktrees/<name> for a background session), so
|
|
||||||
# the derived sender is invented. git's own test decides linked-ness - git-dir
|
|
||||||
# differs from git-common-dir - because ".git is a file" is also true of a
|
|
||||||
# submodule, whose basename IS its name. A worktree directory that happens to
|
|
||||||
# carry the repo's own name derives correctly and passes. Fails OPEN when git
|
|
||||||
# cannot answer: the refusal needs a positive measurement.
|
|
||||||
if [ "$DERIVED_FROM" -eq 1 ]; then
|
|
||||||
WT_GD="$(git rev-parse --path-format=absolute --git-dir 2>/dev/null)"
|
|
||||||
WT_CD="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)"
|
|
||||||
if [ -n "$WT_GD" ] && [ -n "$WT_CD" ] && [ "$WT_GD" != "$WT_CD" ]; then
|
|
||||||
WT_REPO="$(basename "$(dirname "$WT_CD")")"
|
|
||||||
if [ "$FROM" != "$WT_REPO" ]; then
|
|
||||||
echo "coord-send: refusing to sign as '$FROM': that is a linked git worktree's directory name, not a repo name; pass --from explicitly (this worktree belongs to: --from $WT_REPO)" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
# A leading _ is reserved for engine internals (_broadcast today; the rule
|
# A leading _ is reserved for engine internals (_broadcast today; the rule
|
||||||
# reserves the namespace so a later _seen or _config cannot reopen the hole).
|
# reserves the namespace so a later _seen or _config cannot reopen the hole).
|
||||||
case "$FROM" in
|
case "$FROM" in
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ description: >-
|
||||||
user names no repo and no tool — choosing *between* repos is this skill. Not for
|
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,
|
"where were we" inside the current repo: that is this repo's own STATE.md,
|
||||||
already injected at session start.
|
already injected at session start.
|
||||||
version: "0.36.1"
|
version: "0.35.0"
|
||||||
---
|
---
|
||||||
|
|
||||||
# board — which repo deserves the next session
|
# board — which repo deserves the next session
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ description: >-
|
||||||
covers retiring a broadcast that has become wrong or obsolete: "retract that
|
covers retiring a broadcast that has become wrong or obsolete: "retract that
|
||||||
broadcast", "that announcement is outdated, pull it", "trekk tilbake kringkastingen",
|
broadcast", "that announcement is outdated, pull it", "trekk tilbake kringkastingen",
|
||||||
"den broadcasten er utdatert".
|
"den broadcasten er utdatert".
|
||||||
version: "0.36.1"
|
version: "0.35.0"
|
||||||
---
|
---
|
||||||
|
|
||||||
# coord-send — natural-language front door for inter-repo messages
|
# coord-send — natural-language front door for inter-repo messages
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,12 @@ description: >-
|
||||||
Also triggers on Norwegian phrasings: "dispatch en økt i repo X", "start en økt
|
Also triggers on Norwegian phrasings: "dispatch en økt i repo X", "start en økt
|
||||||
der med denne ordren", "gi meg oppstartskommandoen for Y", "send arbeidet til
|
der med denne ordren", "gi meg oppstartskommandoen for Y", "send arbeidet til
|
||||||
<repo>", "skriv planfila for den økten", "åpne en tab for X med denne
|
<repo>", "skriv planfila for den økten", "åpne en tab for X med denne
|
||||||
oppgaven", "hvordan starter jeg neste økt på dette". Also covers the
|
oppgaven", "hvordan starter jeg neste økt på dette". Trigger even when no tool
|
||||||
background form, where the dispatching session starts the child itself with
|
|
||||||
`claude --bg`: "start it in the background", "run that as a background
|
|
||||||
session", "launch a bg session on this order", "start en bakgrunnsøkt i repo
|
|
||||||
X", "kjør det i bakgrunnen", "start barnet selv". Trigger even when no tool
|
|
||||||
is named — producing a runnable startup command for another session IS this
|
is named — producing a runnable startup command for another session IS this
|
||||||
skill. Not for choosing WHICH repo deserves the next session (that is `board`),
|
skill. Not for choosing WHICH repo deserves the next session (that is `board`),
|
||||||
not for scoring model and effort alone (that is `route`), and not for sending a
|
not for scoring model and effort alone (that is `route`), and not for sending a
|
||||||
message to another repo (that is `coord-send`).
|
message to another repo (that is `coord-send`).
|
||||||
version: "0.36.1"
|
version: "0.35.0"
|
||||||
---
|
---
|
||||||
|
|
||||||
# dispatch — hand a session a task it can actually start on
|
# dispatch — hand a session a task it can actually start on
|
||||||
|
|
@ -33,12 +29,9 @@ what everyone assumed it stopped. All three were measured on 2026-08-16 — four
|
||||||
separate misfires in one day, by two different repos — and this skill exists so
|
separate misfires in one day, by two different repos — and this skill exists so
|
||||||
they are not re-derived a fifth time.
|
they are not re-derived a fifth time.
|
||||||
|
|
||||||
**In the paste and plan-file forms you produce the command and never run it.**
|
**You produce the command. You never run it.** Starting a session in another
|
||||||
Starting a session in another repo spends the operator's quota and takes an
|
repo spends the operator's quota and takes an action inside a repo this session
|
||||||
action inside a repo this session does not own. Hand back the finished command
|
does not own. Hand back the finished command and stop.
|
||||||
and stop. The one exception is **the background form** (its own section below):
|
|
||||||
since the operator's decision of 2026-09-22 the PM starts sessions itself with
|
|
||||||
`claude --bg`, and that form is run, not handed over.
|
|
||||||
|
|
||||||
## The engine
|
## The engine
|
||||||
|
|
||||||
|
|
@ -188,104 +181,6 @@ a pane. Hand back the `paste=` line, and say it goes in the existing tab **after
|
||||||
operator is already standing in that one. `morning --relaunch` bypasses the
|
operator is already standing in that one. `morning --relaunch` bypasses the
|
||||||
filter but opens a *second* tab beside the existing one, which is rarely wanted.
|
filter but opens a *second* tab beside the existing one, which is rarely wanted.
|
||||||
|
|
||||||
## The background form
|
|
||||||
|
|
||||||
**When:** the PM starts the child session itself (operator decision
|
|
||||||
2026-09-22, `~/.claude/docs/2026-09-22-tos-bakgrunnsoekter-og-natt.md`). Not
|
|
||||||
when the operator is to paste a line into a tab. That is still steps 3–5
|
|
||||||
above. The background form replaces steps 3–5 only. Steps 1–2 are unchanged:
|
|
||||||
the order goes into the recipient's queue first, and the id is the handle.
|
|
||||||
There is no pane question, because a background session has no pane.
|
|
||||||
|
|
||||||
The command, run by the dispatching session through its Bash tool:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd <absolute repo dir> && claude --model <m> --effort <e> --disallowedTools AskUserQuestion,SendMessage,ListAgents --bg --name <short-name> "<starter>"
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`--model` directly after `claude`, always.**
|
|
||||||
`~/.claude/hooks/pre-dispatch-budget.sh` counts a start only in that position.
|
|
||||||
Measured 2026-09-22: `claude --model … --bg --name x` is counted, but a flag
|
|
||||||
with a value in front of `--model` breaks the pattern and the start goes
|
|
||||||
uncounted. The form must not depend on the hook learning more forms. Take the
|
|
||||||
model and effort from `route.sh`'s `command=` line (score the traits as in
|
|
||||||
step 4). That line already begins `claude --model <m> --effort <e>`, so
|
|
||||||
appending the rest keeps the order by construction. A Fable override is
|
|
||||||
written by hand, in the same order.
|
|
||||||
- **`--disallowedTools AskUserQuestion,SendMessage,ListAgents`** (hardening T4,
|
|
||||||
`~/.claude/docs/2026-09-22-herding-s1-steg4.md` § 3). A background child
|
|
||||||
cannot be answered. `AskUserQuestion` was open and stopped two sessions
|
|
||||||
before this guard existed. `SendMessage`/`ListAgents` close the child→PM
|
|
||||||
direction. The PM's own messages to the child are still delivered.
|
|
||||||
- **The `cd` is right here, and only here.** The no-`cd` rule is for a line
|
|
||||||
pasted into a terminal tab, where the operator already stands in the repo.
|
|
||||||
This line is run by a session standing in another repo, and `claude --bg`
|
|
||||||
starts in the working directory, so the `cd` is what picks the repo.
|
|
||||||
- **`--name`**: a short name (`<repo>-<topic>`), so `claude agents` and
|
|
||||||
`claude logs <id>` can be read at a glance.
|
|
||||||
|
|
||||||
The starter is **one line of ASCII, with no `$`, backtick or double quote**.
|
|
||||||
It sits inside the double quotes above and is evaluated by the shell, exactly
|
|
||||||
like `board.sh`'s thin starter. `<PLUGIN_ROOT>` is the absolute install
|
|
||||||
directory the engine lines at the top of this skill resolve to (the parent of
|
|
||||||
`dirname "$ORDER"`, e.g. the version-pinned plugin cache). Write it out in
|
|
||||||
full, because a child executes the starter literally and the order verbs are
|
|
||||||
not on PATH (ORDRE 65).
|
|
||||||
|
|
||||||
```text
|
|
||||||
ORDRE <id> ligger i dette repoets ordrekoe. Gjor dette i rekkefolge: (1) claim den - bash <PLUGIN_ROOT>/scripts/coord-order-claim.sh --repo <repo> <id> - claim skriver ut hele ordreteksten. FULL STI ER MED VILJE. Claim KUN denne ordren. (2) Sammenlign mot STATE.md NESTE og uttal avviket i ditt FORSTE svar. (3) Du er en bakgrunnsoekt isolert i en NY worktree: sett grenen din til <hash> (git checkout -B <gren> <hash>) og verifiser. Commit paa den grenen, ingen fletting, INGEN push/tag/release - operatoren pusher. Du kan ikke stille spoersmaal; ved et teknisk valg, ta det anbefalte og skriv valgt X fordi Y. Lange kjoringer i forgrunnen, aldri arm venter. (4) Lukk med bash <PLUGIN_ROOT>/scripts/coord-order-done.sh --repo <repo> <id> --commit HASH - eller --return --reason. (5) Rapporter til <sender> med bash <PLUGIN_ROOT>/scripts/coord-send.sh --from <repo> --to <sender> - gren, hasher, roed til groenn, selftest-tall.
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`--repo <repo>` on both verbs, always.** A background child runs in a
|
|
||||||
worktree, and without `--repo` the verbs derive their identity from the git
|
|
||||||
toplevel's basename. That basename is the worktree's name, not the repo's.
|
|
||||||
Measured 2026-09-23: a claim made before the worktree won. The close made
|
|
||||||
inside the worktree exited 1 with `no claimed order … for <worktree-name>`,
|
|
||||||
and the order stayed in flight.
|
|
||||||
- **`--from <repo>` on the report, by full path.** `coord-send` derives its
|
|
||||||
sender from the same toplevel basename, so the report was signed with the
|
|
||||||
worktree's name (measured on 741ada8: `from: dispatch-bg-form`, a mailbox no
|
|
||||||
session holds). Since 0.36.0 `coord-send` refuses a sender derived inside a
|
|
||||||
linked worktree, so a starter without `--from` now fails loudly instead of
|
|
||||||
delivering, but the starter must still say it: the child executes it
|
|
||||||
literally.
|
|
||||||
- **`Claim KUN denne ordren`** stays in even when the queue holds only this
|
|
||||||
one. Another order can land between the dispatch and the child's first turn.
|
|
||||||
- **`<hash>` is the commit the child must build on, not automatically `main`.**
|
|
||||||
If the repo tracks STATE.md/PLAN.md and the newest of those commits sits on a
|
|
||||||
branch that has not been merged, the hash is that branch's tip. Measured
|
|
||||||
wrong 2026-09-23: `d5a6bac` was given where `8bd27d8` was meant.
|
|
||||||
- **`INGEN push/tag/release`**: the child commits on its branch and stops. The
|
|
||||||
operator's push round merges, pushes, and runs `release-plugin.mjs`.
|
|
||||||
|
|
||||||
**Measured platform facts** (agent-view docs + 2026-09-22): a background
|
|
||||||
session is forced into its own worktree. It cannot write in the main checkout,
|
|
||||||
not even after `ExitWorktree`. It never merges. That is why the starter pins
|
|
||||||
the branch and forbids the merge rather than asking for either.
|
|
||||||
|
|
||||||
**What happens afterwards, and it is the PM's:**
|
|
||||||
|
|
||||||
1. **Watch the order, not the process.** The child's claim moves the order to
|
|
||||||
`~/.claude/coord/<repo>/orders/claimed/<id>.md`. Closing moves it to
|
|
||||||
`orders/archive/<id>.md` (executed), or back to `orders/<id>.md` (returned,
|
|
||||||
with the reason written into it). `claude agents` / `claude logs <id>` show
|
|
||||||
the session. The file shows whether the work reached a terminal state.
|
|
||||||
2. **Re-measure on the commit.** Read the `commit=` the child wrote into the
|
|
||||||
archived order. Then run that repo's suites at that hash yourself. The
|
|
||||||
child's report is a claim, not a measurement.
|
|
||||||
3. **The merge is the operator's.** The branch waits for the operator's push
|
|
||||||
round, which merges, pushes and releases. A background child never does
|
|
||||||
any of the three.
|
|
||||||
|
|
||||||
**Why this is not a `board.sh` rendering.** `board.sh --dispatch` is the one
|
|
||||||
generator of the plan-block format (`tab=`/`dir=`/`command=`/`paste=`), and
|
|
||||||
this form uses none of it. There is no block, no pane and no `morning`
|
|
||||||
consumer. The one generated part, model and effort, already has its single
|
|
||||||
generator in `route.sh`. So `--target-pane` stays exactly `yes|no`, and the
|
|
||||||
selftest pins that. Stated overlap: steps (1), (2) and (4) of the starter
|
|
||||||
repeat the protocol in `board.sh`'s thin starter. If the protocol changes,
|
|
||||||
both copies must move together.
|
|
||||||
|
|
||||||
## Three things that must reach the operator
|
## Three things that must reach the operator
|
||||||
|
|
||||||
Say these in the handover, not only in the plan file. Each was a real
|
Say these in the handover, not only in the plan file. Each was a real
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ description: >-
|
||||||
the operator names no model and no tool — choosing the model for the next
|
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:
|
session IS this skill. Not for choosing which REPO gets the next session:
|
||||||
that is the `board` skill.
|
that is the `board` skill.
|
||||||
version: "0.36.1"
|
version: "0.35.0"
|
||||||
---
|
---
|
||||||
|
|
||||||
# route — what the next session should run with
|
# route — what the next session should run with
|
||||||
|
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
// No tracked file may carry a term from a LOCAL, untracked term list - not in
|
|
||||||
// its content and not in its path. The list itself is deliberately kept out of
|
|
||||||
// the repository (tests/excluded-terms.local.md, covered by `*.local.md` in
|
|
||||||
// .gitignore): a check that spelled its own pattern out would be a tracked file
|
|
||||||
// carrying exactly what it forbids.
|
|
||||||
//
|
|
||||||
// Absent list = SKIPPED, loudly, never passed: a check that could not run must
|
|
||||||
// not read as a check that found nothing.
|
|
||||||
//
|
|
||||||
// Format: one case-insensitive regex per line; `#` lines and blank lines are
|
|
||||||
// ignored; `sample: <text>` lines are known-positive controls. Every sample
|
|
||||||
// must match some term and every term must be matched by some sample, so a
|
|
||||||
// term that can no longer find anything fails here instead of passing vacuously.
|
|
||||||
import { test } from 'node:test';
|
|
||||||
import assert from 'node:assert';
|
|
||||||
import { execFileSync } from 'node:child_process';
|
|
||||||
import { existsSync, readFileSync } from 'node:fs';
|
|
||||||
import { dirname, join } from 'node:path';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
|
|
||||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
||||||
const listFile = join(root, 'tests', 'excluded-terms.local.md');
|
|
||||||
|
|
||||||
// Named exceptions: tracked path -> reason. Empty on purpose; an entry needs a
|
|
||||||
// stated reason, never a silent widening.
|
|
||||||
const EXEMPT = new Map();
|
|
||||||
|
|
||||||
function loadList() {
|
|
||||||
const terms = [];
|
|
||||||
const samples = [];
|
|
||||||
for (const raw of readFileSync(listFile, 'utf8').split('\n')) {
|
|
||||||
const line = raw.trim();
|
|
||||||
if (line === '' || line.startsWith('#')) continue;
|
|
||||||
if (line.startsWith('sample:')) samples.push(line.slice('sample:'.length).trim());
|
|
||||||
else terms.push(new RegExp(line, 'i'));
|
|
||||||
}
|
|
||||||
return { terms, samples };
|
|
||||||
}
|
|
||||||
|
|
||||||
function hitsIn(text, terms) {
|
|
||||||
return terms.filter((re) => re.test(text)).map((re) => re.source);
|
|
||||||
}
|
|
||||||
|
|
||||||
test('no tracked file carries a term from the local term list', (t) => {
|
|
||||||
if (!existsSync(listFile)) {
|
|
||||||
t.skip(`SKIPPED, not passed: ${listFile} is absent, so nothing was checked`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { terms, samples } = loadList();
|
|
||||||
assert.ok(terms.length > 0, 'term list holds no terms');
|
|
||||||
|
|
||||||
// Known-positive controls, before anything depends on the matcher.
|
|
||||||
for (const s of samples) {
|
|
||||||
assert.ok(hitsIn(s, terms).length > 0, `control sample matches no term: ${s}`);
|
|
||||||
}
|
|
||||||
for (const re of terms) {
|
|
||||||
assert.ok(samples.some((s) => re.test(s)), `term /${re.source}/ has no matching sample`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = execFileSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf8' })
|
|
||||||
.split('\0')
|
|
||||||
.filter((p) => p !== '' && !EXEMPT.has(p));
|
|
||||||
assert.ok(files.length > 0, 'git ls-files listed no tracked files');
|
|
||||||
|
|
||||||
const found = [];
|
|
||||||
for (const path of files) {
|
|
||||||
for (const term of hitsIn(path, terms)) found.push(`${path} (path): /${term}/`);
|
|
||||||
const lines = readFileSync(join(root, path), 'latin1').split('\n');
|
|
||||||
lines.forEach((line, i) => {
|
|
||||||
for (const term of hitsIn(line, terms)) found.push(`${path}:${i + 1}: /${term}/`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
t.diagnostic(`scanned ${files.length} tracked files against ${terms.length} terms`);
|
|
||||||
assert.deepStrictEqual(found, [], `${found.length} hit(s):\n${found.join('\n')}`);
|
|
||||||
});
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue