Compare commits

...
Author SHA1 Message Date
code-infra-01 acd15ed144 fix(tmux): compose draft-transition with landed box detection (#1332, O1)
ci/woodpecker/pr/ci Pipeline was successful
Rebased onto current next and composes the two sibling mechanisms per the
O1 ruling: the cursor-row draft transition stays the authoritative
runtime-agnostic delivered verdict; next's locate_input_box (two shapes:
prompt glyph, pi rule box) is adopted as positive DRAFT evidence only,
covering the cursor-row check's blind spot on redrawn TUIs that park the
cursor off the input line. Box-absence proves nothing and can never
produce UNDELIVERED: a shapeless-but-submitting pane delivers via the
cursor-row transition regardless.

The verdict suite gains fixtures 6 and 6b: the 2026-09-04 scratch probe
is the regression test (shapeless REPL that submits must report
delivered; shape probing alone exited 2 with retry advice on a consumed
message), plus the raw/no-echo shapeless guard arm. Measured limit
recorded in the suite: a shapeless cooked non-reading pane is
indistinguishable from a delivering one by any runtime-agnostic signal
available to the sender.

Rerun evidence: pipefail scanner 7/7, glyph-agnostic suite 6/6, verdict
suite 8/8, live shapeless probe rc=0 delivered with consumption proof.
2026-09-04 16:51:15 -05:00
code-infra-01 9d721fa6f1 fix(#1332): verdict fixture 2 - herestring, not printf-pipe into grep -qF
pipefail-early-exit.test.mjs (static scan) flagged the fixture-2 check as a
pipe into an early-exiting consumer; the sibling at fixture 1 already uses
the herestring form. Red reproduced locally (one scan entry, exactly the
flagged line), green after matching the sibling; verdict 4/4 and glyph 6/6
re-run green.
2026-09-04 16:44:55 -05:00
code-infra-01 9e7b563a75 fix(#1262): adoption follow-ups - enumeration exclusion, SEND-honoring verdict suite, retire rc=2-is-normal guide text
- F5 (rev-code-02 blocker): sign test-send-message-glyph-agnostic.sh into
  test-enumeration-exclusions.txt beside its tmux siblings; the CI image
  ships no tmux, so the suite stays manually run (#1017 burndown).
- Adoption finding: the verdict suite hard-coded SEND to its sibling and
  ignored the SEND env var, so a red-first run against the shipping blob
  silently measured the patched copy instead (measured: shipping run
  printed PASS=4; with SEND honored it is PASS=3 FAIL=1, fixture 2 red,
  matching the recorded review numbers). SEND is now honored with the
  sibling as default, same contract as the glyph suite.
- F6/D19: framework FLEET-COMMS.md claimed 'rc=2 is the normal result
  when the target is an idle pi seat'. Post-fix rc=0 is normal for idle
  and busy pi seats; rc=2 on a healthy seat is a real report. Never-retry
  advice kept, softened to 'may be in the pane' for the unconfirmed arm.

Live verification on sb-it-1-dt (tmux 3.7b, pi glm-5.3 low, scratch
session): idle pi - shipping rc=2 'may be UNDELIVERED' while the seat
consumed the message and answered; patched rc=0 delivered, answered.
Busy pi mid-turn - shipping rc=2 while the pane accepted both messages
as steering input ('Steering: ...', 'Alt+Up to edit all queued
messages'); patched rc=0 delivered, both consumed and acted on after the
turn. Both D11 signatures: verdict now matches reality.
2026-09-04 16:44:55 -05:00
f76a5c96b6 fix(tmux): confirm delivery by draft transition, not by prompt glyph (#1257)
send-message.sh located the REPL input box with grep -E '❯|^>|│ >'. That set is
Claude Code's box. A pi seat renders a bare U+2500 rule with no glyph, so on every
idle pi seat the capture succeeded, the grep matched nothing, status stayed
"unconfirmed", and the tool exited 2 "may be UNDELIVERED" with the paste and the
Enter both landed. The stderr tells the operator to retry, and that retry is the
duplicate delivery reported against the same tool.

Confirmation is now runtime-agnostic: our message tail sits on the input line
(located by cursor row, no glyph) before Enter and has left it after. That
transition is positive proof of submission.

Absence still proves nothing, which is the guard the 2026-08 fix was reaching for
and got backwards. Two positive checks keep it:

  - a prompt box that IS locatable and still carries our tail => draft, exit 2.
    This covers the cursor-row blind spot: a cooked pane whose foreground process
    never reads stdin echoes the paste through the kernel line discipline and
    moves the cursor off it on Enter, which by cursor row alone is indistinguishable
    from a real submit.
  - no draft ever observed on the input line => unconfirmed, non-zero.

Tests, both red-first against the shipping blob d397907:

  test-send-message-glyph-agnostic.sh (new, 6 fixtures)  4/6 -> 6/6
  test-send-message-verdict.sh (fixture 2 reshaped, 2b added)  3/4 -> 4/4

Fixture 2 of the verdict suite asserted exit 2 for a glyphless pane that submits
and was labelled "false-positive FIXED". A pi seat is that fixture, so the suite
was locking the bug in. It is reshaped deliberately, and the guard it was credited
with moves to new fixture 2b (glyphless AND non-submitting, raw/no-echo) so the
"never infer delivered from absence" property is tested positively rather than as
a side effect.

Measured on tmux 3.7b (sb-it-1-dt), 3.5a (fomo-lin), and dragon-lin.

Co-authored-by: scooby <[email protected]>
2026-09-04 16:44:55 -05:00
orch-01 d6302f8e6f docs: make Portainer optional deployment path (#1492)
ci/woodpecker/push/publish Pipeline was successful
2026-09-02 23:23:07 +00:00
9 changed files with 300 additions and 72 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ whitelisted — see the tool header.
| tmux | `tools/tmux/agent-send.sh` | inter-agent messaging (see "Most-used" above) | | tmux | `tools/tmux/agent-send.sh` | inter-agent messaging (see "Most-used" above) |
| git | `tools/git/*.sh` | issues, PRs, milestones, CI queue guard (platform-auto-detected) | | git | `tools/git/*.sh` | issues, PRs, milestones, CI queue guard (platform-auto-detected) |
| woodpecker | `tools/woodpecker/*.sh` | CI pipelines (`-a mosaic`\|`usc`; match git remote host) | | woodpecker | `tools/woodpecker/*.sh` | CI pipelines (`-a mosaic`\|`usc`; match git remote host) |
| portainer | `tools/portainer/*.sh` | Docker Swarm stacks (status/redeploy/list) | | portainer | `tools/portainer/*.sh` | Optional Docker Swarm tools when a Portainer credential is available |
| coolify | `tools/coolify/*.sh` | **DEPRECATED** — superseded by Portainer; do not use for new deployments | | coolify | `tools/coolify/*.sh` | **DEPRECATED** — superseded by Portainer; do not use for new deployments |
| authentik | `tools/authentik/*.sh` | identity (users/groups/apps/flows) | | authentik | `tools/authentik/*.sh` | identity (users/groups/apps/flows) |
| cloudflare | `tools/cloudflare/*.sh` | DNS (zones/records; `-a` instance) | | cloudflare | `tools/cloudflare/*.sh` | DNS (zones/records; `-a` instance) |
@@ -53,21 +53,23 @@ sends, it does not auto-reply.
### Exit codes ### Exit codes
| rc | Meaning | | rc | Meaning |
| --- | ---------------------------------------------- | | --- | -------------------------------------------------------------------------------------------- |
| 0 | delivered or queued | | 0 | delivered or queued |
| 1 | target session not found | | 1 | target session not found |
| 2 | text reached the pane but is **still a draft** | | 2 | submission unconfirmed: draft still on the input line, or no positive evidence of submission |
| 3 | usage error (bad class, missing `-s`) | | 3 | usage error (bad class, missing `-s`) |
**Never retry on rc=2.** The message is in the target pane; retrying double-sends it. Confirm **Never retry on rc=2.** The message may be in the target pane, and a retry can double-send it.
instead: Confirm instead:
```bash ```bash
tmux capture-pane -p -t <session>:0.0 | tail -20 tmux capture-pane -p -t <session>:0.0 | tail -20
``` ```
rc=2 is the normal result when the target is an idle pi seat. rc=0 is the normal result for both idle and busy pi seats (submission confirmed by draft
transition, not by prompt glyph). rc=2 on a healthy seat is exceptional — treat it as a real
report and investigate the pane.
## Durable comms ## Durable comms
@@ -136,7 +136,8 @@ The human is escalation-only for missing access, hard policy conflicts, or irrev
### Supported Targets ### Supported Targets
- **Portainer**: Deploy via `~/.config/mosaic/tools/portainer/stack-redeploy.sh`, then verify with `stack-status.sh`. - **Docker Swarm**: If a stack README documents `docker stack deploy` on the manager, use that deploy path and its stated verification procedure.
- **Portainer (optional)**: Use only when the estate holds a Portainer credential. Do not propose Portainer otherwise. Deploy via `~/.config/mosaic/tools/portainer/stack-redeploy.sh`, then verify with `stack-status.sh`.
- **Coolify**: Deploy via `~/.config/mosaic/tools/coolify/deploy.sh -u <uuid>`, then verify with `service-status.sh`. - **Coolify**: Deploy via `~/.config/mosaic/tools/coolify/deploy.sh -u <uuid>`, then verify with `service-status.sh`.
- **Vercel**: Deploy via `vercel` CLI or connected Git integration, then verify preview/production URL health. - **Vercel**: Deploy via `vercel` CLI or connected Git integration, then verify preview/production URL health.
- **Other SaaS providers**: Use provider CLI/API/runbook with the same validation and rollback gates. - **Other SaaS providers**: Use provider CLI/API/runbook with the same validation and rollback gates.
@@ -1,16 +1,16 @@
--- ---
name: mosaic-deploy name: mosaic-deploy
description: 'Full end-to-end deploy flow for Mosaic Stack projects: push branch → open PR → wait for CI → merge → redeploy Portainer stack. Use when deploying a feature branch to production or staging, or when asked to ship a completed feature. Orchestrates mosaic-gitea, mosaic-woodpecker, and mosaic-portainer skills.' description: 'Full end-to-end deployment flow: push branch → open PR → wait for CI → merge → deploy using the path documented by the stack. Use when deploying a feature branch to production or staging, or when asked to ship a completed feature.'
--- ---
# mosaic-deploy # mosaic-deploy
End-to-end deployment flow for Mosaic Stack projects. End-to-end deployment flow.
## Full Deploy Sequence ## Full Deploy Sequence
``` ```
push branch → open PR → CI passes → merge → portainer redeploy push branch → open PR → CI passes → merge → documented deploy path
``` ```
### Step 1: Push branch and open PR ### Step 1: Push branch and open PR
@@ -49,25 +49,32 @@ review. Fix the cause; never route around it with a raw API call, a shared
credential, or `force_merge`. Exceptional cases go to the operator or the credential, or `force_merge`. Exceptional cases go to the operator or the
coordinating seat, still merged through the wrapper. coordinating seat, still merged through the wrapper.
### Step 4: Redeploy Portainer stack ### Step 4: Deploy Through the Documented Path
Read the stack README before deploying:
- If it documents `docker stack deploy` on the manager, use that deploy path and its verification procedure.
- Use Portainer only when the estate holds a Portainer credential. Do not propose Portainer otherwise.
For an authorized Portainer deployment:
```bash ```bash
source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials portainer source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials portainer
~/.config/mosaic/tools/portainer/stack-redeploy.sh -n <stack-name> -p ~/.config/mosaic/tools/portainer/stack-redeploy.sh -n <stack-name> -p
``` ```
Check deployment: Check a Portainer deployment:
```bash ```bash
~/.config/mosaic/tools/portainer/stack-status.sh -n <stack-name> ~/.config/mosaic/tools/portainer/stack-status.sh -n <stack-name>
~/.config/mosaic/tools/portainer/stack-logs.sh -n <stack-name> -l 50 ~/.config/mosaic/tools/portainer/stack-logs.sh -n <stack-name> -l 50
``` ```
## Stack Name Map ## Optional Portainer Stack Map
Maintain your estate's project → stack-name mapping in a skills-local override of For deployments that use Portainer, maintain a project → stack-name mapping in a
this skill (local copies take precedence over the shipped canonical one). Example skills-local override of this skill (local copies take precedence over the shipped
shape: canonical one). Example shape:
| Project | Stack Name | | Project | Stack Name |
| ------------ | ----------------- | | ------------ | ----------------- |
@@ -77,6 +84,6 @@ shape:
## Notes ## Notes
- Workers open PRs but **never merge** — orchestrator or Merge Guard handles step 3+ - Workers open PRs but **never merge** — orchestrator or Merge Guard handles step 3+
- Docker Swarm image pinning: if `-p` doesn't pull a new image, SSH to the Docker node (e.g. `node-01`) and run `docker pull <image>` manually, then redeploy - Docker Swarm image pinning: `-p` does not change a digest-pinned image. Follow the stack README's documented deployment procedure.
- Worktrees: all coding work in `~/src/<repo>-worktrees/<task-slug>`, never in main checkout - Worktrees: all coding work in `~/src/<repo>-worktrees/<task-slug>`, never in main checkout
- Always clean up worktree after push: `git worktree remove ~/src/<repo>-worktrees/<task-slug>` - Always clean up worktree after push: `git worktree remove ~/src/<repo>-worktrees/<task-slug>`
@@ -1,15 +1,19 @@
--- ---
name: mosaic-portainer name: mosaic-portainer
description: Manage Portainer stacks on the Mosaic infrastructure. Use when asked to list, start, stop, redeploy, or check logs of Docker Swarm stacks via Portainer. Wraps scripts in ~/.config/mosaic/tools/portainer/. Requires load_credentials portainer first. description: Manage Docker Swarm stacks through Portainer when a Portainer credential is available. Use when asked to list, start, stop, redeploy, or check logs through Portainer.
--- ---
# mosaic-portainer # mosaic-portainer
Manage Portainer stacks via pre-built Mosaic scripts. Manage Portainer stacks through supplied scripts.
## Decision Gate
Portainer is optional. Use this skill only when the estate holds a Portainer credential. If a stack README documents `docker stack deploy` on the manager, that is the deploy path. Do not propose Portainer otherwise.
## Setup ## Setup
Always load credentials before running scripts: After confirming a Portainer credential is available, load it before running scripts:
```bash ```bash
source ~/.config/mosaic/tools/_lib/credentials.sh source ~/.config/mosaic/tools/_lib/credentials.sh
@@ -33,11 +37,11 @@ All scripts live in `~/.config/mosaic/tools/portainer/`.
## Common Workflows ## Common Workflows
**Redeploy a stack with fresh images:** **Redeploy a stack through Portainer:**
```bash ```bash
source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials portainer source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials portainer
~/.config/mosaic/tools/portainer/stack-redeploy.sh -n mosaic-stack -p ~/.config/mosaic/tools/portainer/stack-redeploy.sh -n <stack-name> -p
``` ```
**Check all stack statuses:** **Check all stack statuses:**
@@ -51,12 +55,10 @@ source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials portainer
```bash ```bash
source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials portainer source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials portainer
~/.config/mosaic/tools/portainer/stack-logs.sh -n mosaic-stack -l 100 ~/.config/mosaic/tools/portainer/stack-logs.sh -n <stack-name> -l 100
``` ```
## Notes ## Notes
- Portainer URL: `https://portainer.example.internal:9443` - `stack-redeploy.sh -p` does not override a digest-pinned image. Follow the stack README's documented deployment procedure for pinned images.
- Primary Docker host: `node-01`, managed via Portainer agent - Credentials are loaded through `load_credentials portainer`.
- Docker Swarm image updates: `stack-redeploy.sh -p` does NOT guarantee new image pull if digest is pinned; SSH to node and `docker pull` first if needed
- Credentials: `load_credentials portainer` (framework credentials store)
@@ -28,6 +28,7 @@ packages/mosaic/framework/tools/git/test-lane-brief-pr-linkage.sh | unmeasured i
# --- tools/tmux: require a live tmux server --- # --- tools/tmux: require a live tmux server ---
packages/mosaic/framework/tools/tmux/test-send-message-socket.sh | requires a real tmux server on a throwaway socket; CI image ships no tmux; #1017 burndown (needs tmux in image or a signed permanent exclusion) packages/mosaic/framework/tools/tmux/test-send-message-socket.sh | requires a real tmux server on a throwaway socket; CI image ships no tmux; #1017 burndown (needs tmux in image or a signed permanent exclusion)
packages/mosaic/framework/tools/tmux/test-send-message-verdict.sh | requires real tmux-pane fixtures on a throwaway socket; CI image ships no tmux; #1017 burndown (same condition as its sibling) packages/mosaic/framework/tools/tmux/test-send-message-verdict.sh | requires real tmux-pane fixtures on a throwaway socket; CI image ships no tmux; #1017 burndown (same condition as its sibling)
packages/mosaic/framework/tools/tmux/test-send-message-glyph-agnostic.sh | requires real tmux-pane fixtures on a throwaway socket; CI image ships no tmux; #1017 burndown (same condition as its siblings) — signed at adoption of #1262 (rev-code-02 F5), red-first verified on sb-it-1-dt
# --- single-suite directories: unmeasured in CI --- # --- single-suite directories: unmeasured in CI ---
@@ -33,8 +33,10 @@
# 1 tmux target not found # 1 tmux target not found
# 2 submission NOT confirmed — either still an unsubmitted draft, or the REPL # 2 submission NOT confirmed — either still an unsubmitted draft, or the REPL
# input box could not be located to confirm the message actually landed. # input box could not be located to confirm the message actually landed.
# Locating the box is runtime-specific; see locate_input_box() below, and # Delivered verdicts are runtime-agnostic (cursor-row draft transition, or
# add a shape there before pointing this tool at a new runtime. # the queued banner); locate_input_box() below adds positive DRAFT evidence
# for panes that render a recognizable box, and never gates delivery on a
# runtime's rendering shape.
# Delivery is NEVER inferred from absence of evidence: if we cannot positively # Delivery is NEVER inferred from absence of evidence: if we cannot positively
# see the input box clear of the message (or the queued banner), we fail loud # see the input box clear of the message (or the queued banner), we fail loud
# so the sender learns immediately instead of a silent worker->lead stall. # so the sender learns immediately instead of a silent worker->lead stall.
@@ -99,11 +101,33 @@ printf '%s' "$MSG" | "${tmux_cmd[@]}" load-buffer -b "$BUF" -
# would otherwise accumulate forever. # would otherwise accumulate forever.
sleep 0.5 sleep 0.5
# 2) Submit, then POSITIVELY confirm submission by DRAFT TRANSITION, not by prompt
# glyph. The historical bug was treating ABSENCE of a draft as delivery; the
# 2026-08 fix over-corrected to glyph inference (grep '|^>|│ >'), which locates
# only Claude Code's box and false-NEGATIVES every glyphless REPL (pi renders a
# U+2500 rule, no glyph) — a delivered message reported "UNDELIVERED", driving a
# retry that duplicates it. Runtime-agnostic evidence: our message tail sits on
# the INPUT line (located by the cursor row, not a glyph) BEFORE Enter, and has
# LEFT it AFTER — that transition is positive proof of submission and needs no
# glyph. Absence alone still never means delivered: if we never saw our draft on
# the input line we stay UNCONFIRMED (wrong/dead pane), and a draft that never
# leaves the input line stays a DRAFT (exit 2), preserving both historical guards.
_cursor_line() { # echo the pane's current input (cursor) line, glyph-free
local cy line
cy=$("${tmux_cmd[@]}" display-message -p -t "$EFFECTIVE_TARGET" -F '#{cursor_y}' 2>/dev/null) || return 1
[ -n "$cy" ] || return 1
"${tmux_cmd[@]}" capture-pane -t "$EFFECTIVE_TARGET" -p 2>/dev/null | sed -n "$((cy + 1))p"
}
_draft_on_input() { # true iff our message tail is sitting on the input line now
[ -n "$snippet" ] || return 1
grep -qF "$snippet" <<<"$(_cursor_line)"
}
# Locate the REPL input box in a captured pane. Prints the box's contents on # Locate the REPL input box in a captured pane. Prints the box's contents on
# stdout and returns 0 when the box was FOUND; returns 1 when it could not be # stdout and returns 0 when the box was FOUND; returns 1 when it could not
# located at all. Found-but-empty is a real, distinct answer (an empty input box # be located at all. Found-but-empty is a real, distinct answer (an empty input
# is what a submitted message leaves behind), so the caller must branch on the # box is what a submitted message leaves behind), so the caller must branch on
# return code, never on whether the output is empty. # the return code, never on whether the output is empty.
# #
# Two REPL shapes are recognised: # Two REPL shapes are recognised:
# * a prompt-glyph line — ``, a leading `>`, or `│ >`. Claude Code and most # * a prompt-glyph line — ``, a leading `>`, or `│ >`. Claude Code and most
@@ -113,10 +137,15 @@ sleep 0.5
# what makes it safe: agent output can contain its own rules, but nothing is # what makes it safe: agent output can contain its own rules, but nothing is
# drawn below the input box except the status line. # drawn below the input box except the status line.
# #
# Adding a runtime means adding its shape HERE. A shape that is missing does not # Compose authority rule (#1332 O1): this function is POSITIVE DRAFT EVIDENCE
# degrade gracefully: it turns every send to that runtime into a false # ONLY. A located box still carrying our tail is affirmative proof the message
# "may be UNDELIVERED", which is what #1362 measured on pi and #1257 on another # was not consumed (the cursor-row check's blind spot: a redrawn TUI can park
# arm of the same probe. # the cursor off the input line, which the draft-transition anchor cannot see).
# Its failure to find a box proves NOTHING and must never produce an
# UNDELIVERED verdict: a shapeless-but-submitting pane delivers via the
# cursor-row transition regardless (measured, scratch probe 2026-09-04;
# shapeless REPL consumed the message while shape probing alone reported
# "may be UNDELIVERED" — the exact #1257 regression this split prevents).
locate_input_box() { locate_input_box() {
local pane=$1 glyph_line rule_lines top bottom local pane=$1 glyph_line rule_lines top bottom
glyph_line=$(printf '%s\n' "$pane" | grep -E '|^>|│ >' | tail -1) glyph_line=$(printf '%s\n' "$pane" | grep -E '|^>|│ >' | tail -1)
@@ -139,13 +168,12 @@ locate_input_box() {
return 0 return 0
} }
# 2) Submit, then POSITIVELY confirm submission; flush with another Enter if it is # Baseline: after the paste, our draft must be on the input line. This is positive
# still a draft. Success requires positive evidence — the queued banner, OR the # proof we are on the right pane and the paste landed — the anchor the transition
# REPL input box located AND clear of our message tail. The historical bug was # check measures against.
# treating ABSENCE of a draft as delivery: if the input box was never located saw_draft=0
# (wrong pane / prompt-glyph drift), an unsubmitted message read as "delivered" _draft_on_input && saw_draft=1
# and worker->lead relays stalled silently. We now default to UNCONFIRMED and only
# upgrade to delivered on positive evidence; anything we cannot confirm fails loud.
status="unconfirmed" status="unconfirmed"
for attempt in $(seq 1 $((RETRIES + 1))); do for attempt in $(seq 1 $((RETRIES + 1))); do
"${tmux_cmd[@]}" send-keys -t "$EFFECTIVE_TARGET" Enter "${tmux_cmd[@]}" send-keys -t "$EFFECTIVE_TARGET" Enter
@@ -155,19 +183,30 @@ for attempt in $(seq 1 $((RETRIES + 1))); do
if grep -qF "$QUEUED_RE" <<<"$pane"; then if grep -qF "$QUEUED_RE" <<<"$pane"; then
status="queued"; break status="queued"; break
fi fi
# If we cannot see the input box, we have NO evidence of submission state — # POSITIVE draft evidence from a located input box, when one exists. This is
# stay UNCONFIRMED and retry; never infer delivery. # the cursor-row check's blind spot: a redrawn TUI (pi's box) can park the
if ! inputbox=$(locate_input_box "$pane"); then # cursor off the input line, which the draft-transition anchor cannot see,
status="unconfirmed"; continue # while a pane in COOKED mode (a plain shell whose foreground process never
# reads stdin) echoes our paste via the kernel line discipline and moves the
# cursor off it on Enter, indistinguishable from a real submit by cursor row
# alone. If a locatable box still carries our tail, that is affirmative proof
# the message was not consumed. Absence of a recognizable shape is never used
# for anything — that inference is the original E7 bug, and the delivered
# verdict stays with the runtime-agnostic cursor-row transition.
if inputbox=$(locate_input_box "$pane"); then
if [ -n "$snippet" ] && grep -qF "$snippet" <<<"$inputbox"; then
status="draft"; continue
fi
fi fi
# Input box located AND still carrying our tail => unsubmitted draft. Flush + retry. if [ "$saw_draft" = 1 ]; then
# (Submitted messages scroll up into history; a draft stays in the box.) if _draft_on_input; then
if [ -n "$snippet" ] && grep -qF "$snippet" <<<"$inputbox"; then status="draft"; continue # still on the input line => not submitted; flush + retry
status="draft"; continue fi
status="delivered"; break # left the input line => positively submitted
fi fi
# Input box located AND clear of our tail => positively submitted. This is the # No confirmed baseline yet: try to (re)acquire it; never infer delivery from absence.
# only path to success besides the queued banner. if _draft_on_input; then saw_draft=1; status="draft"; continue; fi
status="delivered"; break status="unconfirmed"; continue
done done
[ "$VERBOSE" = 1 ] && { echo "--- pane tail ($TARGET) ---"; printf '%s\n' "$pane" | tail -4; echo "---"; } [ "$VERBOSE" = 1 ] && { echo "--- pane tail ($TARGET) ---"; printf '%s\n' "$pane" | tail -4; echo "---"; }
@@ -176,6 +215,6 @@ case "$status" in
delivered) echo "✓ delivered to $TARGET"; exit 0 ;; delivered) echo "✓ delivered to $TARGET"; exit 0 ;;
queued) echo "✓ queued to $TARGET (agent busy — will process when it returns to prompt)"; exit 0 ;; queued) echo "✓ queued to $TARGET (agent busy — will process when it returns to prompt)"; exit 0 ;;
draft) echo "✗ still an unsubmitted draft on $TARGET after $RETRIES flush attempts" >&2; exit 2 ;; draft) echo "✗ still an unsubmitted draft on $TARGET after $RETRIES flush attempts" >&2; exit 2 ;;
unconfirmed) echo "✗ could not confirm submission on $TARGET: REPL input box not locatable after $((RETRIES + 1)) attempts — message may be UNDELIVERED (check target/pane, retry, or escalate)" >&2; exit 2 ;; unconfirmed) echo "✗ could not confirm submission on $TARGET: REPL input prompt not locatable after $((RETRIES + 1)) attempts — message may be UNDELIVERED (check target/pane, retry, or escalate)" >&2; exit 2 ;;
*) echo "✗ could not confirm submission on $TARGET (unexpected state '$status')" >&2; exit 2 ;; *) echo "✗ could not confirm submission on $TARGET (unexpected state '$status')" >&2; exit 2 ;;
esac esac
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# Red-first regression test for E7 (#1017 task 2): the confirm-check must bind
# "delivered" to WHETHER THE MESSAGE WAS SUBMITTED, not to which runtime's prompt
# glyph is present. A pi seat renders a U+2500 rule input box with no /^>/│ >
# glyph; send-message.sh:118 locates the box only by glyph, so a genuinely
# delivered message on a glyphless REPL falsely reports exit 2 "may be UNDELIVERED",
# and the operator's rc=2-driven retry duplicates it.
#
# Parameterized on $SEND: RED against the shipping blob (B and D fail), GREEN
# against a candidate patch. No pi; no fake HOME; hermetic throwaway socket.
#
# Submission counting is EXACT and terminal-echo-independent: the fixture message
# is `echo <tok> >>SINK`; each real submission appends one line. wc -l SINK ==
# number of times the REPL actually executed the send. This does not depend on how
# many times the marker string is painted on screen.
set -u
SEND="${SEND:?set SEND=/path/to/send-message.sh}"
SOCKET="glyphagnostic-$$"
TMP="$(mktemp -d)"
tmux() { command tmux -L "$SOCKET" "$@"; }
cleanup() { command tmux -L "$SOCKET" kill-server 2>/dev/null; rm -rf "$TMP"; }
trap cleanup EXIT
pass=0; fail=0
ok() { printf 'ok %s\n' "$1"; pass=$((pass+1)); }
no() { printf 'FAIL %s -- %s\n' "$1" "$2"; fail=$((fail+1)); }
mk() { tmux new-session -d -s "$1" -x 120 -y 40 -c "$TMP" "PS1='$2' exec bash --noprofile --norc -i"; sleep 0.5; }
subs() { [ -f "$1" ] && wc -l <"$1" | tr -d ' ' || echo 0; } # exact submission count
echo "SEND=$SEND tmux $(command tmux -V | awk '{print $2}')"
# --- A (control): glyph box () that submits => exit 0, exactly one submission.
mk ctl ' '
SINK="$TMP/sink.ctl"
out=$("$SEND" -L "$SOCKET" -t ctl -m "echo x >>'$SINK'" 2>"$TMP/e.ctl"); rc=$?; sleep 0.4
if [ "$rc" = 0 ] && [ "$(subs "$SINK")" = 1 ]; then
ok "control: -box submits => exit 0, exactly one submission"
else no "control: -box submits => exit 0, one submission" "rc=$rc subs=$(subs "$SINK") err=[$(cat "$TMP/e.ctl")]"; fi
# --- B (THE false-rc regression): glyphless U+2500 box that SUBMITS. Message lands
# (subs==1) yet shipping reports exit 2. Must be exit 0.
mk sub $'──────── \n'
SINK="$TMP/sink.sub"
out=$("$SEND" -L "$SOCKET" -t sub -m "echo x >>'$SINK'" 2>"$TMP/e.sub"); rc=$?; sleep 0.4
if [ "$rc" = 0 ] && [ "$(subs "$SINK")" = 1 ]; then
ok "glyphless: U+2500 box that submits => exit 0 (delivered, not 'UNDELIVERED')"
else no "glyphless: U+2500 box that submits => exit 0" \
"rc=$rc subs=$(subs "$SINK")(delivered=$([ "$(subs "$SINK")" -ge 1 ] && echo yes||echo no)) err=[$(cat "$TMP/e.sub")]"; fi
# --- D (duplicate arm): operator follows the rc=2 stderr and retries once. On the
# glyphless box, shipping => two submissions (the reported duplicate). The
# property: one logical send => exactly one submission. Same fix closes it.
mk dup $'──────── \n'
SINK="$TMP/sink.dup"
tries=0
for attempt in 1 2; do
tries=$((tries+1))
out=$("$SEND" -L "$SOCKET" -t dup -m "echo x >>'$SINK'" 2>/dev/null); rc=$?
sleep 0.4
[ "$rc" = 0 ] && break # operator stops retrying only when told delivered
done
if [ "$(subs "$SINK")" = 1 ]; then
ok "duplicate: one logical send (rc-driven retry) => exactly one submission (tries=$tries)"
else no "duplicate: one logical send => exactly one submission" "submissions=$(subs "$SINK") tries=$tries"; fi
# --- E (faithful hung managed TUI, NOT a cooked shell): raw/no-echo, paints nothing.
# A cooked `sleep infinity` echoes the paste via the kernel line discipline and
# false-passes a cursor-row fix that is correct on real seats (measured). So: raw.
mk_rawstuck() { tmux new-session -d -s "$1" -x 120 -y 40 -c "$TMP" \
"bash --noprofile --norc -c 'stty -echo -icanon min 1 time 0 2>/dev/null; exec sleep infinity'"; sleep 0.5; }
mk_rawstuck estuck
SINK="$TMP/sink.estuck"
out=$("$SEND" -L "$SOCKET" -t estuck -r 1 -m "this stuck draft was never submitted" 2>/dev/null); rc=$?
sleep 0.3
if [ "$rc" != 0 ] && [ "$(subs "$SINK")" = 0 ]; then
ok "raw/no-echo stuck TUI (not submitted) => non-zero (no false delivered)"
else no "raw stuck TUI must NOT report delivered" "rc=$rc subs=$(subs "$SINK")"; fi
# --- F (busy/queued branch, your BUSY-not-runtime finding): glyphless pane rendering the
# queued banner, never consuming. QUEUED_RE :113 fires before the glyph grep => rc=0.
mk_busy() { tmux new-session -d -s "$1" -x 120 -y 40 -c "$TMP" \
"bash --noprofile --norc -c 'printf \"Press up to edit queued messages\n\"; exec sleep infinity'"; sleep 0.5; }
mk_busy ebusy
SINK="$TMP/sink.ebusy"
out=$("$SEND" -L "$SOCKET" -t ebusy -m "echo x >>'$SINK'" 2>/dev/null); rc=$?; sleep 0.3
if [ "$rc" = 0 ]; then
ok "busy/queued-banner glyphless => exit 0 (queued is delivery; runtime owns custody)"
else no "busy/queued-banner must report delivered" "rc=$rc"; fi
# --- C (historical-bug guard): unresolvable target. No pane ever carried our draft
# => must fail, never infer delivered from absence of a glyph/snippet.
if out=$("$SEND" -L "$SOCKET" -t "nonexistent-$$" -m "echo x >>'$TMP/sink.wrong'" 2>/dev/null); then
no "wrong-pane: unresolvable target must NOT report success" "expected non-zero, got 0"
else ok "wrong-pane: unresolvable target => non-zero (no false delivered)"; fi
echo "---"; echo "pass=$pass fail=$fail"
[ "$fail" = 0 ]
@@ -4,10 +4,13 @@
# #
# 1. DELIVERED — a REPL that renders a ` ` input box and submits on Enter # 1. DELIVERED — a REPL that renders a ` ` input box and submits on Enter
# (text scrolls to history, box clears) => exit 0 "✓ delivered". # (text scrolls to history, box clears) => exit 0 "✓ delivered".
# 2. UNCONFIRMED — a pane with NO locatable prompt glyph. This is the exact # 2. DELIVERED — a pane with NO prompt glyph that DOES submit => exit 0. A pi
# historical FALSE POSITIVE: pre-patch it printed "✓ delivered" # seat is this fixture (U+2500 rule, no glyph). Reshaped for
# exit 0; post-patch it MUST fail loud (exit 2, stderr # #1257; see the note at the fixture for why the old exit-2
# "could not confirm submission"). # assertion was wrong.
# 2b. UNCONFIRMED— a glyphless pane that never submits (raw/no-echo hung TUI)
# => must fail loud. This carries the historical
# false-positive guard that fixture 2 used to be credited with.
# 3. DRAFT — a ` `-prompt pane that never submits (message stays on the # 3. DRAFT — a ` `-prompt pane that never submits (message stays on the
# input line) => exit 2, stderr "unsubmitted draft". # input line) => exit 2, stderr "unsubmitted draft".
# 4. DELIVERED — a pane whose input box is two `─` rules with NO prompt glyph # 4. DELIVERED — a pane whose input box is two `─` rules with NO prompt glyph
@@ -17,10 +20,16 @@
# 5. DRAFT — the same glyphless box, holding our tail across every flush # 5. DRAFT — the same glyphless box, holding our tail across every flush
# (box shape) Enter => exit 2, stderr "unsubmitted draft". Pre-#1362 this # (box shape) Enter => exit 2, stderr "unsubmitted draft". Pre-#1362 this
# also reported unconfirmed, so the true state was invisible. # also reported unconfirmed, so the true state was invisible.
# 6. DELIVERED — a SHAPELESS REPL (no glyph, no box) that submits => exit 0.
# The 2026-09-04 scratch probe regression: shape probing alone reports "may
# be UNDELIVERED" on this pane while the message is consumed; the cursor-row
# draft transition is the authoritative runtime-agnostic verdict.
# 6b. UNCONFIRMED— a shapeless pane in raw/no-echo mode that never reads stdin
# (shapeless) => exit 2 "could not confirm submission" (never delivered).
set -uo pipefail set -uo pipefail
HERE=$(cd -- "$(dirname -- "$0")" && pwd) HERE=$(cd -- "$(dirname -- "$0")" && pwd)
SEND="$HERE/send-message.sh" SEND="${SEND:-$HERE/send-message.sh}"
SOCKET="verdict-test-$RANDOM-$$" SOCKET="verdict-test-$RANDOM-$$"
TMP=$(mktemp -d) TMP=$(mktemp -d)
trap 'tmux -L "$SOCKET" kill-server >/dev/null 2>&1 || true; rm -rf "$TMP"' EXIT trap 'tmux -L "$SOCKET" kill-server >/dev/null 2>&1 || true; rm -rf "$TMP"' EXIT
@@ -44,19 +53,44 @@ else
no "delivered: -prompt REPL that submits => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e1")]" no "delivered: -prompt REPL that submits => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e1")]"
fi fi
# --- Fixture 2: NO prompt glyph (default bash PS1). THE regression: pre-patch this # --- Fixture 2: NO prompt glyph, and the pane DOES submit (interactive bash).
# was a silent false-positive "delivered"; post-patch it must be unconfirmed→exit 2. # RESHAPED 2026-08-16 (#1257), deliberately. This fixture previously asserted
# exit 2 here and was labelled "false-positive FIXED". That assertion was wrong,
# and locking it in is what kept E7 alive: the pane submits, so "delivered" is
# the truth, and a pi seat — whose input box is a bare U+2500 rule with no glyph
# — IS this fixture. Reporting exit 2 for it told operators a delivered message
# may be undelivered, and the retry that advice invites is the duplicate.
#
# The guard this fixture was reaching for is real and is NOT dropped: "never
# infer delivered from absence" is now enforced positively by fixture 2b below
# (glyphless AND not submitting => must fail) and by fixture 3 (locatable box
# still carrying our tail => draft). Absence alone decides nothing either way.
tmux -L "$SOCKET" new-session -d -s noglyph -c "$TMP" \ tmux -L "$SOCKET" new-session -d -s noglyph -c "$TMP" \
'PS1="sh-noglyph$ " exec bash --noprofile --norc -i' 'PS1="sh-noglyph$ " exec bash --noprofile --norc -i'
sleep 0.3 sleep 0.3
if out=$("$SEND" -L "$SOCKET" -t "=noglyph" -m "verdict fixture two must fail loud" 2>"$TMP/e2"); then out=$("$SEND" -L "$SOCKET" -t "=noglyph" -m "verdict fixture two must fail loud" 2>"$TMP/e2"); rc=$?
no "unconfirmed: glyphless pane must NOT report success" "expected exit 2, got 0 (out=[$out])" if [ "$rc" -eq 0 ] && grep -qF "✓ delivered" <<<"$out"; then
ok "delivered: glyphless pane that submits => exit 0 (runtime-agnostic, E7 FIXED)"
else
no "delivered: glyphless pane that submits => exit 0" "rc=$rc out=[$out] err=[$(cat "$TMP/e2")]"
fi
# --- Fixture 2b: NO prompt glyph AND never submits — a hung managed TUI holding the
# terminal in raw/no-echo, which is what a stuck agent seat actually is (measured
# on live pi: stty -echo -icanon). Nothing is echoed, nothing is consumed, so
# there is no positive evidence of submission and the tool MUST fail loud. This
# is the historical false-positive guard, kept as a positive test.
tmux -L "$SOCKET" new-session -d -s rawstuck -c "$TMP" \
'bash --noprofile --norc -c "stty -echo -icanon min 1 time 0 2>/dev/null; exec sleep infinity"'
sleep 0.3
if out=$("$SEND" -L "$SOCKET" -t "=rawstuck" -r 1 -m "verdict fixture two-b never submitted" 2>"$TMP/e2b"); then
no "unconfirmed: glyphless hung TUI must NOT report success" "expected non-zero, got 0 (out=[$out])"
else else
rc=$? rc=$?
if [ "$rc" -eq 2 ] && grep -qF "could not confirm submission" "$TMP/e2"; then if [ "$rc" -ne 0 ] && grep -qF "could not confirm submission" "$TMP/e2b"; then
ok "unconfirmed: glyphless pane => exit 2 + 'could not confirm submission' (false-positive FIXED)" ok "unconfirmed: glyphless hung TUI (raw/no-echo) => non-zero + 'could not confirm submission'"
else else
no "unconfirmed: glyphless pane => exit 2 + stderr" "rc=$rc err=[$(cat "$TMP/e2")]" no "unconfirmed: glyphless hung TUI => non-zero + stderr" "rc=$rc err=[$(cat "$TMP/e2b")]"
fi fi
fi fi
@@ -126,6 +160,51 @@ else
fi fi
fi fi
# --- Fixtures 6 and 6b: a SHAPELESS REPL. The pane renders nothing at all: no
# prompt glyph and no rule box, so locate_input_box() alone can never see it
# and shape-probing alone reports "may be UNDELIVERED" on a delivered message
# (measured live 2026-09-04, scratch probe: a shapeless consumer CONSUMED the
# message while the shipped shape probe exited 2 with retry advice - the exact
# #1257 regression). The cursor-row draft transition is the authoritative,
# runtime-agnostic delivered verdict: fixture 6's consumer submits => exit 0.
# Fixture 6b is the guard arm: a shapeless pane whose foreground never reads
# stdin keeps the echoed paste on the cursor line across every flush Enter =>
# DRAFT => exit 2, never delivered.
cat > "$TMP/shapeless.py" <<'SHAPELESS'
import sys
for line in sys.stdin:
pass # consume and render nothing
SHAPELESS
tmux -L "$SOCKET" new-session -d -s shapeless -c "$TMP" "exec python3 -u '$TMP/shapeless.py'"
sleep 0.3
out=$("$SEND" -L "$SOCKET" -t "=shapeless" -m "fixture six shapeless consumed ok" 2>"$TMP/e6"); rc=$?
if [ "$rc" -eq 0 ] && grep -qF "✓ delivered" <<<"$out"; then
ok "delivered: shapeless REPL that submits => exit 0 ✓ delivered (probe regression)"
else
no "delivered: shapeless REPL that submits => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e6")]"
fi
# MEASURED LIMIT (2026-09-04, this suite's development): a shapeless pane in
# COOKED mode whose foreground never reads stdin (e.g. 'sleep infinity') scrolls
# its kernel echo off the cursor row on the flush Enter, so no runtime-agnostic
# signal available to the sender distinguishes it from a delivering pane. The
# non-reading guard therefore requires either a locatable box still carrying the
# tail (fixture 3) or raw/no-echo mode (fixture 2b). Real REPL seats read stdin,
# which is why this limit is not reachable against agent seats; recorded here so
# nobody rediscovers it as a silent gap.
tmux -L "$SOCKET" new-session -d -s shapelessraw -c "$TMP" 'stty raw -echo; exec sleep infinity'
sleep 0.3
if out=$("$SEND" -L "$SOCKET" -t "=shapelessraw" -r 1 -m "fixture six b shapeless raw never consumed" 2>"$TMP/e6b"); then
no "unconfirmed: shapeless raw non-reading pane must NOT report success" "expected exit 2, got 0 (out=[$out])"
else
rc=$?
if [ "$rc" -eq 2 ] && grep -qF "could not confirm submission" "$TMP/e6b"; then
ok "unconfirmed: shapeless raw non-reading pane => exit 2 + 'could not confirm submission'"
else
no "unconfirmed: shapeless raw non-reading pane => exit 2 + stderr" "rc=$rc err=[$(cat "$TMP/e6b")]"
fi
fi
echo "---" echo "---"
echo "PASS=$PASS FAIL=$FAIL" echo "PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ] [ "$FAIL" -eq 0 ]