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
|
|
@ -1587,6 +1587,151 @@ check "missing sibling: --inbox-plan carries a machine-readable warning key, not
|
|||
|
||||
/bin/rm -rf "$SIB_ROOT" "$SIB_COORD" "$SIB_SCRATCH" 2>/dev/null
|
||||
|
||||
# --- 18. Dispatch rendering (--dispatch) ------------------------------------
|
||||
# The FOURTH rendering of the same scan, and the one that carries a task INTO
|
||||
# another repo. It exists here rather than in a new script for one reason: the
|
||||
# block format has exactly one generator, and board.sh already owns it
|
||||
# (tab=/repo=/dir=/command=/paste=, consumed by morning's plan_parse). A second
|
||||
# emitter of the same file format is the drift defect this repo's CLAUDE.md
|
||||
# warns about, and it would be a second place to get `paste=` wrong.
|
||||
#
|
||||
# Read-only holds: every check here is a read (test -s on the prompt file, the
|
||||
# path grammar, the scan), and the FILE WRITES - the prompt file and the plan
|
||||
# file - stay with the caller. That is the brief-nightly.sh split, unchanged.
|
||||
DSP="$ROOT/dispatch.prompt"
|
||||
printf 'ORDRE X - do the thing.\nWith $(whoami) and `id` and "quotes" in the body.\n' > "$DSP"
|
||||
DSPEMPTY="$ROOT/empty.prompt"
|
||||
: > "$DSPEMPTY"
|
||||
DSPTRAITS='--path known --verification strong --reversibility cheap --scope local --rationale t'
|
||||
|
||||
# --target-pane has NO DEFAULT, and that is the same rule route.sh's
|
||||
# --last-effort carries: the answer is a MEASUREMENT of the world (does the
|
||||
# target repo already have a Ghostty pane), and a calculator that guesses it
|
||||
# produces a dispatch that verifies green and opens nothing. Defaulting to
|
||||
# "no" would be worst: that is the plan-file form, which morning's
|
||||
# plan_drop_open (morning:1788) silently drops for a repo that already has a
|
||||
# pane - measured four times in one day, 2026-08-16, and read every time as a
|
||||
# broken plan file rather than as the filter doing its job.
|
||||
d1="$("$BOARD" --roots "$ROOT" --dispatch --repo repo-a --prompt-file "$DSP" $DSPTRAITS 2>&1)"; rc=$?
|
||||
[ "$rc" -eq 2 ]; check "dispatch: refuses without --target-pane (no default, it is a measurement)" $?
|
||||
printf '%s' "$d1" | grep -q 'command='; [ $? -ne 0 ]; check "dispatch: emits no command when it refuses for a missing --target-pane" $?
|
||||
d2="$("$BOARD" --roots "$ROOT" --dispatch --repo repo-a --prompt-file "$DSP" --target-pane maybe $DSPTRAITS 2>&1)"; rc=$?
|
||||
[ "$rc" -eq 2 ]; check "dispatch: refuses a --target-pane value outside yes|no" $?
|
||||
|
||||
# The prompt file is the whole payload: an empty one produces a session that
|
||||
# is started and then told nothing, which is indistinguishable from a session
|
||||
# waiting for Go - and the operator has to diagnose it from the far end.
|
||||
d3="$("$BOARD" --roots "$ROOT" --dispatch --repo repo-a --prompt-file "$ROOT/nope.prompt" --target-pane no $DSPTRAITS 2>&1)"; rc=$?
|
||||
[ "$rc" -eq 2 ]; check "dispatch: refuses a prompt file that does not exist" $?
|
||||
d4="$("$BOARD" --roots "$ROOT" --dispatch --repo repo-a --prompt-file "$DSPEMPTY" --target-pane no $DSPTRAITS 2>&1)"; rc=$?
|
||||
[ "$rc" -eq 2 ]; check "dispatch: refuses an EMPTY prompt file (test -s, not test -e)" $?
|
||||
|
||||
# The PATH is what has to be shell-clean, not the prompt CONTENT. Verified
|
||||
# directly (2026-08-16): `"$(cat f)"` passes the file's bytes as ONE argv
|
||||
# element with no re-evaluation, so $(...), backticks, quotes and UTF-8 in the
|
||||
# BODY are inert - that is precisely why the dispatch form puts the prompt in
|
||||
# argv instead of inlining it. The path, though, sits inside those quotes and
|
||||
# IS evaluated, so a space or a quote in it splits the command.
|
||||
mkdir -p "$ROOT/dir with space"
|
||||
cp "$DSP" "$ROOT/dir with space/p.prompt"
|
||||
d5="$("$BOARD" --roots "$ROOT" --dispatch --repo repo-a --prompt-file "$ROOT/dir with space/p.prompt" --target-pane no $DSPTRAITS 2>&1)"; rc=$?
|
||||
[ "$rc" -eq 2 ]; check "dispatch: refuses a prompt PATH that is not shell-clean" $?
|
||||
|
||||
# A relative path resolves against whatever directory the pane is in, which
|
||||
# for the paste-only form is a repo this script never sees - so the command
|
||||
# would read a different file, or none, with no error anywhere near the
|
||||
# dispatch.
|
||||
d5b="$(cd "$ROOT" && "$BOARD" --roots "$ROOT" --dispatch --repo repo-a --prompt-file dispatch.prompt --target-pane no $DSPTRAITS 2>&1)"; rc=$?
|
||||
[ "$rc" -eq 2 ]; check "dispatch: refuses a RELATIVE prompt path (it resolves in the pane, not here)" $?
|
||||
|
||||
d6="$("$BOARD" --roots "$ROOT" --dispatch --repo no-such-repo --prompt-file "$DSP" --target-pane no $DSPTRAITS 2>&1)"; rc=$?
|
||||
[ "$rc" -eq 2 ]; check "dispatch: refuses a repo the scan does not know" $?
|
||||
|
||||
# Invalid traits must refuse, never degrade to a command without them: a
|
||||
# command missing --advisor reads exactly like a row that legitimately has no
|
||||
# advisor.
|
||||
d7="$("$BOARD" --roots "$ROOT" --dispatch --repo repo-a --prompt-file "$DSP" --target-pane no \
|
||||
--path bogus --verification strong --reversibility cheap --scope local --rationale t 2>&1)"; rc=$?
|
||||
[ "$rc" -eq 2 ]; check "dispatch: refuses trait values route.sh rejects, rather than emitting a partial command" $?
|
||||
|
||||
# --- The plan-file form (target has NO pane) ---
|
||||
d8="$("$BOARD" --roots "$ROOT" --dispatch --repo repo-a --prompt-file "$DSP" --target-pane no $DSPTRAITS 2>/dev/null)"; rc=$?
|
||||
[ "$rc" -eq 0 ]; check "dispatch: the plan-file form exits 0" $?
|
||||
printf '%s\n' "$d8" | grep -q '^form=plan-file'; check "dispatch: names its output form machine-readably" $?
|
||||
# The four keys morning's plan_parse requires (lib/plan.sh:30-46). Named here
|
||||
# rather than by sourcing that library: it lives in another repo, and this
|
||||
# selftest pins OUR side of the contract.
|
||||
printf '%s\n' "$d8" | grep -q '^tab=1$'; check "dispatch: plan block carries tab= (plan_parse discards a block without it)" $?
|
||||
printf '%s\n' "$d8" | grep -q '^repo=repo-a$'; check "dispatch: plan block carries repo=" $?
|
||||
printf '%s\n' "$d8" | grep -q "^dir=$ROOT/repo-a\$"; check "dispatch: plan block carries dir=" $?
|
||||
printf '%s\n' "$d8" | grep -q '^paste=cd '; check "dispatch: plan block carries a paste= that cds first" $?
|
||||
# The prompt reaches the session through argv. A bare model+effort command
|
||||
# forces the operator to type Go, and the session must then GUESS the task out
|
||||
# of STATE instead of being handed it - measured twice on 2026-08-16.
|
||||
printf '%s\n' "$d8" | grep -q "command=.*\"\$(cat $DSP)\""; check "dispatch: the command carries the prompt in argv, never bare model+effort" $?
|
||||
printf '%s\n' "$d8" | grep -q "^paste=cd $ROOT/repo-a && claude .*\"\$(cat $DSP)\""; check "dispatch: paste= is the complete one-string form, prompt included" $?
|
||||
|
||||
# The cost comes from route.sh's row table and nowhere else, so --advisor is a
|
||||
# property of the ROW. These two differ ONLY in reversibility: both land on
|
||||
# row 3 (Opus 5/high), and only the costly one carries the advisor. A dispatch
|
||||
# that took --model/--effort directly would have no source for that flag at
|
||||
# all, and forwarding it blindly is how a session ends up looking peer-reviewed
|
||||
# without being.
|
||||
d9="$("$BOARD" --roots "$ROOT" --dispatch --repo repo-a --prompt-file "$DSP" --target-pane no \
|
||||
--path known --verification strong --reversibility cheap --scope multi-file --rationale t 2>/dev/null)"
|
||||
printf '%s\n' "$d9" | grep -q '^command=claude --model opus --effort high "'; check "dispatch: model+effort come from the route row (row 3, no advisor at cheap)" $?
|
||||
d10="$("$BOARD" --roots "$ROOT" --dispatch --repo repo-a --prompt-file "$DSP" --target-pane no \
|
||||
--path known --verification strong --reversibility costly --scope local --rationale t 2>/dev/null)"
|
||||
printf '%s\n' "$d10" | grep -q '^command=claude --model opus --effort high --advisor opus "'; check "dispatch: the advisor flag follows the ROW (costly -> row 3 with advisor)" $?
|
||||
printf '%s\n' "$d8" | grep -q '^next-cost=Sonnet 5/high$'; check "dispatch: the row's next-cost is reported alongside the command" $?
|
||||
|
||||
# --no-go stops the Go MESSAGE, not the work: the startup command - prompt in
|
||||
# argv and all - is typed regardless (morning:806). An operator decided on the
|
||||
# opposite premise once already, and it had to be corrected before the run.
|
||||
printf '%s\n' "$d8" | grep -qi 'no-go'; check "dispatch: the plan-file form states what --no-go does and does not stop" $?
|
||||
# The dry-run is NOT the pane gate, and the output must not imply it is.
|
||||
# Measured 2026-08-16 against the real morning: run without a tty (which is
|
||||
# every Claude session) it prints "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 - the one case it would exist to catch.
|
||||
printf '%s\n' "$d8" | grep -q 'probe-panes'; check "dispatch: points at the measurement that CAN answer the pane question" $?
|
||||
|
||||
# --- The paste-only form (target ALREADY has a pane) ---
|
||||
d11="$("$BOARD" --roots "$ROOT" --dispatch --repo repo-a --prompt-file "$DSP" --target-pane yes $DSPTRAITS 2>/dev/null)"; rc=$?
|
||||
[ "$rc" -eq 0 ]; check "dispatch: the paste-only form exits 0" $?
|
||||
printf '%s\n' "$d11" | grep -q '^form=paste-only'; check "dispatch: names the paste-only form machine-readably" $?
|
||||
# No tab= is the load-bearing half: a block with tab= is a plan block, and a
|
||||
# plan block for a repo that already has a pane is the one morning drops in
|
||||
# silence. Withholding the key makes the wrong use impossible rather than
|
||||
# merely discouraged.
|
||||
printf '%s\n' "$d11" | grep -q '^tab='; [ $? -ne 0 ]; check "dispatch: the paste-only form emits NO tab= (it must never be fed to morning as a plan)" $?
|
||||
printf '%s\n' "$d11" | grep -q '^paste=claude '; check "dispatch: paste-only starts the session directly" $?
|
||||
# One repo per terminal tab: the operator is already standing in that repo, so
|
||||
# a cd is at best noise and at worst a session started in the wrong directory.
|
||||
printf '%s\n' "$d11" | grep -q '^paste=cd '; [ $? -ne 0 ]; check "dispatch: paste-only carries NO cd (the operator is already in that tab)" $?
|
||||
printf '%s\n' "$d11" | grep -q "^paste=claude .*\"\$(cat $DSP)\""; check "dispatch: paste-only also carries the prompt in argv" $?
|
||||
printf '%s\n' "$d11" | grep -qi 'exit'; check "dispatch: paste-only says the existing session must be exited first" $?
|
||||
printf '%s\n' "$d11" | grep -q 'plan_drop_open'; check "dispatch: paste-only names the filter that made this form necessary" $?
|
||||
|
||||
/bin/rm -rf "$ROOT/dir with space" 2>/dev/null
|
||||
|
||||
# --- 19. The dispatch skill's own engine line ------------------------------
|
||||
# Same defect class coord-selftest section 28 pins for coord-send/SKILL.md: a
|
||||
# Bash tool call never has CLAUDE_PLUGIN_ROOT set, so a `:-` fallback wins
|
||||
# every time the line is actually executed and routes through some other copy.
|
||||
# Fail loud (empty path) rather than silently running the wrong script.
|
||||
DSKILL="$DIR/../skills/dispatch/SKILL.md"
|
||||
[ -f "$DSKILL" ]; check "dispatch SKILL.md is where the selftest expects it" $?
|
||||
grep -q 'BOARD="\${CLAUDE_PLUGIN_ROOT}/scripts/board.sh"' "$DSKILL"
|
||||
check "dispatch SKILL.md's engine line has no fallback path" $?
|
||||
if grep -q 'BOARD=.*CLAUDE_PLUGIN_ROOT:-' "$DSKILL"; then rc=1; else rc=0; fi
|
||||
check "dispatch SKILL.md's BOARD assignment carries no fallback" "$rc"
|
||||
# The pane question has exactly one measurement, and the skill must send the
|
||||
# caller to it rather than to the dry-run, which cannot answer it from a
|
||||
# session (no tty -> morning assumes an empty window).
|
||||
grep -q 'probe-panes' "$DSKILL"
|
||||
check "dispatch SKILL.md names --probe-panes as the pane measurement" $?
|
||||
|
||||
echo ""
|
||||
echo "board-selftest: $PASS passed, $FAIL failed"
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
|
|
|
|||
191
scripts/board.sh
191
scripts/board.sh
|
|
@ -151,7 +151,28 @@
|
|||
# debt field, not re-derived) so a caller can tell a request from a notice
|
||||
# without recomputing it - the second explicit ask.
|
||||
#
|
||||
# Usage: board.sh [--roots <dir>[,<dir>...]] [--plain] [--brief|--plan|--inbox-plan]
|
||||
# --dispatch is a FIFTH rendering, and the only one that carries a task INTO a
|
||||
# repo rather than reporting on it: "start a session in repo X, on order Y, at
|
||||
# cost Z". It is here, and not in a script of its own, because 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 format drifting
|
||||
# apart. Read-only is untouched: every check is a read, and the two writes a
|
||||
# dispatch needs (the prompt file, the plan file) stay with the caller, exactly
|
||||
# as brief-nightly.sh owns the briefing's write.
|
||||
#
|
||||
# board.sh --dispatch --repo <name> --prompt-file <abs path>
|
||||
# --target-pane <yes|no>
|
||||
# --path P --verification V --reversibility R --scope S
|
||||
# --rationale "why"
|
||||
#
|
||||
# Model, effort and --advisor come from route.sh's row table, never from a
|
||||
# --model/--effort pair: the advisor flag is a property of the ROW, and a
|
||||
# dispatch that took the model directly would have no honest source for it.
|
||||
# --target-pane is a MEASUREMENT (morning --probe-panes) and has no default -
|
||||
# see the dispatch() comment for what happens when it is guessed.
|
||||
#
|
||||
# Usage: board.sh [--roots <dir>[,<dir>...]] [--plain]
|
||||
# [--brief|--plan|--inbox-plan|--dispatch ...]
|
||||
# [--focus "<prose>"]
|
||||
# Env: CLAUDE_COORD_DIR overrides the mailbox root.
|
||||
# BOARD_ROOTS overrides the default scan roots.
|
||||
|
|
@ -166,6 +187,9 @@ BRIEF=0
|
|||
PLAN=0
|
||||
INBOX_PLAN=0
|
||||
FOCUS=""
|
||||
DISPATCH=0
|
||||
D_REPO=""; D_PROMPT=""; D_PANE=""
|
||||
D_PATH=""; D_VER=""; D_REV=""; D_SCOPE=""; D_RAT=""
|
||||
|
||||
# Sibling calculator, invoked rather than reimplemented: the rubric that turns
|
||||
# four traits into a model has exactly one copy, and it is route.sh's row
|
||||
|
|
@ -187,6 +211,25 @@ while [ $# -gt 0 ]; do
|
|||
# `shift 2` guard as --roots, for the same bash 3.2 reason.
|
||||
--focus) [ $# -ge 2 ] || { echo "board: --focus requires a value" >&2; exit 2; }
|
||||
FOCUS="$2"; shift 2 ;;
|
||||
# Dispatch rendering: one repo, one prompt file, one startup command. Same
|
||||
# `shift 2` guard as every other value flag, for the same bash 3.2 reason.
|
||||
--dispatch) DISPATCH=1; BRIEF=0; PLAN=0; INBOX_PLAN=0; shift ;;
|
||||
--repo) [ $# -ge 2 ] || { echo "board: --repo requires a value" >&2; exit 2; }
|
||||
D_REPO="$2"; shift 2 ;;
|
||||
--prompt-file) [ $# -ge 2 ] || { echo "board: --prompt-file requires a value" >&2; exit 2; }
|
||||
D_PROMPT="$2"; shift 2 ;;
|
||||
--target-pane) [ $# -ge 2 ] || { echo "board: --target-pane requires a value" >&2; exit 2; }
|
||||
D_PANE="$2"; shift 2 ;;
|
||||
--path) [ $# -ge 2 ] || { echo "board: --path requires a value" >&2; exit 2; }
|
||||
D_PATH="$2"; shift 2 ;;
|
||||
--verification) [ $# -ge 2 ] || { echo "board: --verification requires a value" >&2; exit 2; }
|
||||
D_VER="$2"; shift 2 ;;
|
||||
--reversibility) [ $# -ge 2 ] || { echo "board: --reversibility requires a value" >&2; exit 2; }
|
||||
D_REV="$2"; shift 2 ;;
|
||||
--scope) [ $# -ge 2 ] || { echo "board: --scope requires a value" >&2; exit 2; }
|
||||
D_SCOPE="$2"; shift 2 ;;
|
||||
--rationale) [ $# -ge 2 ] || { echo "board: --rationale requires a value" >&2; exit 2; }
|
||||
D_RAT="$2"; shift 2 ;;
|
||||
--plain) shift ;;
|
||||
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "board: unknown argument: $1 (ignored)" >&2; shift ;;
|
||||
|
|
@ -654,6 +697,152 @@ if [ "$BRIEF" -eq 1 ]; then
|
|||
exit 0
|
||||
fi
|
||||
|
||||
# --- Dispatch rendering (--dispatch) ---------------------------------------
|
||||
# The FOURTH rendering, and the only one that carries a task INTO another repo:
|
||||
# "start a session in repo X, on order Y, at cost Z". It lives here because 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 format,
|
||||
# which is the drift defect this repo's CLAUDE.md warns about, and a second
|
||||
# place to get `paste=` wrong.
|
||||
#
|
||||
# Read-only holds. Everything below is a read; the two FILE WRITES a dispatch
|
||||
# needs - the prompt file and the plan file - stay with the caller, exactly as
|
||||
# brief-nightly.sh already owns the briefing's write.
|
||||
#
|
||||
# THE COST COMES FROM route.sh's ROW TABLE, never from a --model/--effort pair
|
||||
# handed in. That is not tidiness: `--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 (it drops it for a Fable main model without failing).
|
||||
# A dispatch that took the model directly would have no source for that flag,
|
||||
# and both available guesses are wrong in the same direction - a session that
|
||||
# looks peer-reviewed without being. Scoring the four traits is the caller's
|
||||
# judgement; turning them into a command is this lookup.
|
||||
dispatch() {
|
||||
# --target-pane is REQUIRED and has no default, the same rule route.sh's
|
||||
# --last-effort carries: it is a MEASUREMENT of the world (does the target
|
||||
# repo already have a Ghostty pane), and this script must never learn to look
|
||||
# for a terminal. 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 - "0 of 1", which reads as a
|
||||
# broken plan file. Measured four times on one day, 2026-08-16, by two
|
||||
# different repos.
|
||||
case "$D_PANE" in
|
||||
yes|no) ;;
|
||||
"") echo "board: --dispatch requires --target-pane yes|no - measure it (morning --probe-panes), never guess it" >&2; exit 2 ;;
|
||||
*) echo "board: --target-pane must be yes or no (got: $D_PANE)" >&2; exit 2 ;;
|
||||
esac
|
||||
[ -n "$D_REPO" ] || { echo "board: --dispatch requires --repo <name>" >&2; exit 2; }
|
||||
[ -n "$D_PROMPT" ] || { echo "board: --dispatch requires --prompt-file <path> - the order the session is dispatched ON" >&2; exit 2; }
|
||||
[ -n "$D_RAT" ] || { echo "board: --dispatch requires --rationale (route.sh requires it, and for the same reason: a score with no stated why cannot be audited)" >&2; exit 2; }
|
||||
|
||||
# The prompt PATH is what has to be shell-clean, not the prompt CONTENT.
|
||||
# Verified directly 2026-08-16: "$(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 exactly why the prompt
|
||||
# goes in argv instead of being inlined. The path sits inside those quotes
|
||||
# and IS evaluated, so a space or a quote there splits the command.
|
||||
# Absolute, because a relative path resolves against whatever directory the
|
||||
# pane happens to be in - which for the paste-only form is a repo this
|
||||
# script never sees.
|
||||
case "$D_PROMPT" in
|
||||
/*) ;;
|
||||
*) echo "board: --prompt-file must be an absolute path (a relative one resolves against the pane's directory, not this one): $D_PROMPT" >&2; exit 2 ;;
|
||||
esac
|
||||
case "$D_PROMPT" in
|
||||
*[!A-Za-z0-9/._-]*) echo "board: --prompt-file path is not shell-clean; it is expanded inside the command: $D_PROMPT" >&2; exit 2 ;;
|
||||
esac
|
||||
[ -f "$D_PROMPT" ] || { echo "board: no prompt file at $D_PROMPT" >&2; exit 2; }
|
||||
# test -s, not test -e: an empty prompt file starts a session and then tells
|
||||
# it nothing, which from the far end is indistinguishable from a session
|
||||
# waiting for a Go that will never come.
|
||||
[ -s "$D_PROMPT" ] || { echo "board: the prompt file is empty: $D_PROMPT (a dispatched session would be started and told nothing)" >&2; exit 2; }
|
||||
|
||||
d_rec="$(awk -F'|' -v n="$D_REPO" '$3==n {print; exit}' "$RECORDS")"
|
||||
[ -n "$d_rec" ] || { echo "board: no repo named '$D_REPO' in the scanned roots ($ROOTS)" >&2; exit 2; }
|
||||
d_status="$(printf '%s' "$d_rec" | cut -d'|' -f4)"
|
||||
d_dir="$(printf '%s' "$d_rec" | cut -d'|' -f10)"
|
||||
d_neste="$(printf '%s' "$d_rec" | cut -d'|' -f12-)"
|
||||
[ -d "$d_dir" ] || { echo "board: the directory for $D_REPO does not exist: $d_dir" >&2; exit 2; }
|
||||
|
||||
# Rejected traits REFUSE. Degrading to a command without them is the worst
|
||||
# available outcome: a command missing --advisor is indistinguishable from a
|
||||
# row that legitimately has none.
|
||||
d_route="$(bash "$ROUTE" --path "$D_PATH" --verification "$D_VER" \
|
||||
--reversibility "$D_REV" --scope "$D_SCOPE" --rationale "$D_RAT" 2>&1)" || {
|
||||
echo "board: route.sh rejected the traits, so there is no command to give:" >&2
|
||||
printf '%s\n' "$d_route" >&2
|
||||
exit 2
|
||||
}
|
||||
d_cmd="$(printf '%s\n' "$d_route" | sed -n 's/^command=//p')"
|
||||
d_cost="$(printf '%s\n' "$d_route" | sed -n 's/^next-cost=//p')"
|
||||
[ -n "$d_cmd" ] || { echo "board: route.sh produced no command for those traits" >&2; exit 2; }
|
||||
|
||||
# The prompt in argv is the whole point. A bare model+effort command forces
|
||||
# the operator to type Go, and the session then has to GUESS its task out of
|
||||
# STATE.md instead of being handed it - delivered bare twice on 2026-08-16
|
||||
# before the operator corrected it ("gi meg alltid komplette
|
||||
# oppstartskommandoer for nye sesjoner").
|
||||
d_full="$d_cmd \"\$(cat $D_PROMPT)\""
|
||||
|
||||
if [ "$D_PANE" = "no" ]; then
|
||||
printf '# DISPATCH %s - plan-file form (target has no pane)\n' "$D_REPO"
|
||||
printf '# Write this whole output to a file and run: morning --plan-file <fil> --no-go\n'
|
||||
printf 'form=plan-file\n'
|
||||
printf '\n'
|
||||
printf 'tab=1\n'
|
||||
printf 'repo=%s\n' "$D_REPO"
|
||||
printf 'dir=%s\n' "$d_dir"
|
||||
printf 'why=dispatch\n'
|
||||
printf 'status=%s\n' "$d_status"
|
||||
printf 'neste=%s\n' "$d_neste"
|
||||
printf 'next-cost=%s\n' "$d_cost"
|
||||
printf 'command=%s\n' "$d_full"
|
||||
printf 'paste=cd %s && %s\n' "$d_dir" "$d_full"
|
||||
printf '\n'
|
||||
# --no-go semantics, in the OUTPUT and not only in a comment: it stops the
|
||||
# Go MESSAGE, never the work. morning:806 is exact - "--no-go says nothing
|
||||
# is typed once the startup command is in" - so the startup command, prompt
|
||||
# in argv and all, is typed regardless and the session starts working by
|
||||
# itself. An operator decided on the opposite premise once already
|
||||
# (2026-08-16) and it had to be corrected before the run.
|
||||
printf 'note=--no-go stops only the follow-up Go message. The startup command - prompt in argv included - is typed regardless (morning:806), so the dispatched session starts working on its own.\n'
|
||||
# What the dry-run does and does not prove, measured rather than assumed
|
||||
# (2026-08-16, this dispatch run through the real morning): it validates
|
||||
# that the block parses and that a command comes out - "opening: 1 of 1".
|
||||
# It does NOT validate the pane question. Run from a Claude session there
|
||||
# is no tty, so morning prints "window: unknown (no terminal to measure),
|
||||
# assuming an empty window" and plan_drop_open never fires at all. A gate
|
||||
# built on the dry-run would therefore pass the self-dispatch case every
|
||||
# single time, which is the case it was supposed to catch.
|
||||
printf 'note=Verify before running: morning --plan-file <fil> --dry-run must report 1 of 1. That proves the block parses and yields a command - it does NOT prove the target has no pane: run from a Claude session there is no tty, morning assumes an empty window and plan_drop_open never fires (measured 2026-08-16).\n'
|
||||
printf 'note=The pane question is answered by morning --probe-panes, whose DIR column works without a tty. That measurement is what --target-pane carries.\n'
|
||||
else
|
||||
printf '# DISPATCH %s - paste-only form (target ALREADY has a pane)\n' "$D_REPO"
|
||||
printf '# No plan file, deliberately: run from a real terminal, morning plan_drop_open\n'
|
||||
printf '# (morning:1788) drops a block whose repo already has a pane and reports\n'
|
||||
printf '# "0 of 1", which reads as a broken plan file. Hand the operator the line\n'
|
||||
printf '# instead. (Without a tty morning cannot see the panes at all, so a dry-run\n'
|
||||
printf '# from a Claude session would report 1 of 1 and prove nothing.)\n'
|
||||
printf 'form=paste-only\n'
|
||||
printf 'repo=%s\n' "$D_REPO"
|
||||
printf 'dir=%s\n' "$d_dir"
|
||||
printf 'next-cost=%s\n' "$d_cost"
|
||||
# No cd: one repo per terminal tab, and the operator is already standing in
|
||||
# that one. A cd here is noise at best and a session started in the wrong
|
||||
# directory at worst.
|
||||
printf 'paste=%s\n' "$d_full"
|
||||
printf '\n'
|
||||
printf 'note=Run this in the existing tab AFTER /exit - it starts a new session in the pane that is already there.\n'
|
||||
printf 'note=Deliberately no tab= key: a block carrying one is a plan block, and a plan block for this repo is exactly what plan_drop_open discards.\n'
|
||||
printf 'note=morning --relaunch bypasses the filter but opens a SECOND tab beside the existing one (morning:1784), which is rarely what is wanted when the operator is already sitting there.\n'
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$DISPATCH" -eq 1 ]; then
|
||||
dispatch
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Focus resolution (--focus) --------------------------------------------
|
||||
# --focus is the first CUTOFF this format has ever had, and the plan documents
|
||||
# at length that it has none: it takes one position (the order), it hides
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue