feat(board): --dispatch, the startup command a dispatched session can act on
"Start a session in repo X, on order Y, at cost Z" was produced by hand, and it misfired four times on 2026-08-16 across two repos. Three distinct holes, all measured, all closed here: 1. A bare `claude --model X --effort Y` forces the operator to type Go, and the session then guesses its task out of STATE.md. The emitted command carries the prompt in argv: `... "$(cat <file>)"`. Verified directly that this passes the file's bytes as ONE argv element with no re-evaluation, so $(...), backticks, quotes and UTF-8 in the prompt BODY are inert - only the PATH is expanded, so it must be absolute and shell-clean. 2. --no-go stops only the follow-up Go message, never the work (morning:806). The plan-file form says so in its own output, not just in a comment. 3. A session dispatching its own next session gets an empty plan: morning's plan_drop_open (morning:1788) drops a block whose repo already has a pane, and --dry-run says "0 of 1", which reads as a broken plan file. --dispatch therefore emits two forms, chosen by --target-pane: a plan block, or a bare paste line for the tab that already exists (and no tab= key at all, so it can never be fed to morning as a plan). Generator ownership, the question left open for two sessions: it goes in board.sh, which already owns the block format including paste=. A second emitter of tab=/repo=/dir=/command=/paste= would be two copies of one file format. Read-only survives - the prompt file and the plan file are written by the caller, the brief-nightly.sh split unchanged. --target-pane yes|no is REQUIRED with no default, the same rule --last-effort carries: it is a measurement (morning --probe-panes, which works without a tty), and the dry-run cannot substitute for it - run from a Claude session morning reports "window: unknown ... assuming an empty window" and plan_drop_open never fires, so a dry-run gate would pass the self-dispatch case every time. Cost comes from route.sh's row table; --dispatch deliberately takes no --model/--effort, because --advisor opus is a property of the ROW and a dispatch taking the model directly has no honest source for that flag. New skills/dispatch/SKILL.md is the front door. board-selftest 183 -> 217. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ett8uHMDLir6trFaMzrYRu
This commit is contained in:
parent
21e2873e21
commit
1ee003328c
5 changed files with 580 additions and 10 deletions
68
CLAUDE.md
68
CLAUDE.md
|
|
@ -173,7 +173,7 @@ marketplace plugin. Three components, one boundary:
|
|||
- **Board (`scripts/board.sh`):** cross-repo attention board. Reads STATE.md
|
||||
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
|
||||
STATE.md and no mailbox. Pinned by `board-selftest.sh` (183 checks).
|
||||
STATE.md and no mailbox. Pinned by `board-selftest.sh` (217 checks).
|
||||
|
||||
**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
|
||||
|
|
@ -331,6 +331,59 @@ marketplace plugin. Three components, one boundary:
|
|||
would run the cd and then a bare newline, which fails SILENTLY by leaving the
|
||||
operator in the right directory with no session started.
|
||||
|
||||
**`board.sh --dispatch` is the FOURTH rendering, and the generator-ownership
|
||||
question it settles was open for two sessions.** It carries a task INTO
|
||||
another repo — "start a session in repo X, on order Y, at cost Z" — and emits
|
||||
either a plan block or a single paste line. It lives in `board.sh` rather
|
||||
than in a script of its own for one reason, and it is the reason the operator
|
||||
and `.claude` both named first: the block format has exactly ONE generator,
|
||||
and this file already is it. A second emitter of
|
||||
`tab=`/`repo=`/`dir=`/`command=`/`paste=` would be two copies of one file
|
||||
format, drifting apart, with a second place to get `paste=` wrong. Read-only
|
||||
survives untouched: every check is a read, and the two writes a dispatch needs
|
||||
(prompt file, plan file) stay with the caller — the same split
|
||||
`brief-nightly.sh` already carries for the briefing.
|
||||
|
||||
**The cost comes from `route.sh`'s row table, and `--dispatch` deliberately
|
||||
refuses a `--model`/`--effort` pair.** `--advisor opus` is a property of the
|
||||
ROW; two rows share a model/effort pair while differing on it, and the CLI
|
||||
accepts a wrong advisor silently. A dispatch taking the model directly would
|
||||
have no honest source for that flag, and both available guesses produce the
|
||||
same failure — a session that looks peer-reviewed without being. A Fable
|
||||
dispatch is therefore not a `--dispatch` outcome at all, exactly as it is not
|
||||
a `route.sh` outcome; it is a hand-written override.
|
||||
|
||||
**`--target-pane yes|no` is REQUIRED, with no default, and that is the same
|
||||
rule `--last-effort` carries.** It is a measurement of the world — does the
|
||||
target repo already have a Ghostty pane — and this repo must never learn to
|
||||
look for a terminal itself; the caller measures with `morning --probe-panes`
|
||||
and passes the fact in. Defaulting would be worst at `no`: that is the
|
||||
plan-file form, and `morning`'s `plan_drop_open` (morning:1788) silently drops
|
||||
a plan block for a repo that already has a pane, reporting "0 of 1" — which
|
||||
reads as a broken plan file. Measured four times on 2026-08-16 by two
|
||||
different repos. The `yes` form therefore emits **no `tab=` key at all**:
|
||||
`plan_parse` discards a block without one, so the wrong use is impossible
|
||||
rather than merely discouraged.
|
||||
|
||||
**The dry-run is a parse check, not the pane gate, and the difference was
|
||||
measured (2026-08-16).** `morning --plan-file <f> --dry-run` proves the block
|
||||
parses and yields a command. Run from a Claude session there is no tty, so
|
||||
`morning` prints "window: unknown ... assuming an empty window" and
|
||||
`plan_drop_open` never fires — a gate built on it would pass the self-dispatch
|
||||
case every single time, which is the one case it would exist to catch.
|
||||
`--probe-panes`, by contrast, DOES work without a tty: it cannot identify the
|
||||
anchor pane, but the `DIR` column is there.
|
||||
|
||||
**The prompt goes in argv, and only the PATH has to be shell-clean.** Verified
|
||||
directly: `"$(cat f)"` hands the file's bytes to the session as one argv
|
||||
element with no re-evaluation, so `$(...)`, backticks, quotes and UTF-8 in the
|
||||
BODY are inert — which is precisely why the prompt is passed this way instead
|
||||
of inlined. The path sits inside those quotes and IS evaluated, so it must be
|
||||
absolute (a relative one resolves against the pane's directory, not the
|
||||
emitter's) and drawn from a safe character class. An empty prompt file is
|
||||
refused with `test -s`: it would start a session and tell it nothing, which
|
||||
from the far end is indistinguishable from one waiting for a Go.
|
||||
|
||||
**Driving a terminal from the plan does NOT belong here, and the measurement
|
||||
in `docs/ghostty-orchestration-measurement.md` is the argument, not taste.**
|
||||
It is a version-pinned undocumented composition over a preview API whose
|
||||
|
|
@ -432,7 +485,7 @@ marketplace plugin. Three components, one boundary:
|
|||
startup command. Corollary pinned by section 13: `skills/route/SKILL.md` must
|
||||
never declare an `effort:` frontmatter field, because frontmatter overrides the
|
||||
session effort and the reading would then measure the skill, not the session.
|
||||
- **Skills (`skills/coord-send/`, `skills/board/`, `skills/route/`):** natural-language front
|
||||
- **Skills (`skills/coord-send/`, `skills/board/`, `skills/route/`, `skills/dispatch/`):** natural-language front
|
||||
doors mapping user intent to engine invocations. No mailbox logic lives here
|
||||
either. `board` additionally owns the *ranking* — which repo wins and why —
|
||||
since `board.sh` deliberately prints evidence and takes no position. `route`
|
||||
|
|
@ -480,7 +533,7 @@ obligations in another repo.
|
|||
builtins only in hook and tests.
|
||||
- TDD: no behavior change without a failing selftest check first.
|
||||
`bash scripts/coord-selftest.sh` must exit 0 (220/220),
|
||||
`bash scripts/board-selftest.sh` must exit 0 (183/183),
|
||||
`bash scripts/board-selftest.sh` must exit 0 (217/217),
|
||||
`bash scripts/route-selftest.sh` must exit 0 (69/69) and
|
||||
`bash scripts/state-line-guard-selftest.sh` must exit 0 (23/23).
|
||||
- English for all code, docs, and commit messages (public repo). Norwegian
|
||||
|
|
@ -501,6 +554,11 @@ obligations in another repo.
|
|||
(default `~/.claude/briefing.md`), so point that at a scratch path when
|
||||
testing. Installed as a launchd agent from `launchd/`, which points at the
|
||||
SOURCE repo, never the version-pinned plugin cache.
|
||||
- Dispatch smoke test: `bash scripts/board.sh --dispatch --repo repo-mailbox
|
||||
--prompt-file /tmp/x.prompt --target-pane yes --path known --verification
|
||||
strong --reversibility cheap --scope local --rationale smoke` (read-only;
|
||||
needs a non-empty `/tmp/x.prompt`. Use `--target-pane yes` in a smoke test:
|
||||
it produces no plan file, so nothing can be handed to `morning` by accident)
|
||||
- Route smoke test: `bash scripts/route.sh --path known --verification strong
|
||||
--reversibility cheap --scope local --rationale x` (writes nothing, instant)
|
||||
- Sweep smoke test: `bash scripts/coord-sweep.sh` (dry-run is the default, so
|
||||
|
|
@ -510,8 +568,8 @@ obligations in another repo.
|
|||
## Release
|
||||
|
||||
Version must agree across: `.claude-plugin/plugin.json`, `package.json`,
|
||||
README version badge, `skills/coord-send/SKILL.md`, `skills/board/SKILL.md` and
|
||||
`skills/route/SKILL.md` frontmatter, git tag `vX.Y.Z`, and the catalog `ref` in
|
||||
README version badge, `skills/coord-send/SKILL.md`, `skills/board/SKILL.md`,
|
||||
`skills/route/SKILL.md` and `skills/dispatch/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 repo-mailbox` (tag + ref bump
|
||||
together);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue