Reconcile docs with reality; fix module graph, token compare, health #1

Open
kami wants to merge 216 commits from webui-and-audit-reconciliation into master
2 changed files with 288 additions and 37 deletions
Showing only changes of commit f54fb0036d - Show all commits
+119 -24
View File
@@ -9,6 +9,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
)
@@ -360,27 +361,87 @@ func (b *TmuxBackend) LaunchTransport(harness string) LaunchTransport {
return LaunchInline
}
// promptLine matches the harness input editor's own line. Its remainder is
// what is still sitting unsubmitted, whether that is literal text or the
// TUI's own "[Pasted text #1 +66 lines]" placeholder.
// promptLine matches a harness input line. A queued or already-accepted
// message renders with the same prefix, so a match alone proves nothing about
// what is still unsubmitted; see inputState.
var promptLine = regexp.MustCompile(`(?m)^[ \t]*[>][ \t]*(.*)$`)
// pendingInput returns whatever the input editor still holds.
func pendingInput(pane string) string {
m := promptLine.FindAllStringSubmatch(pane, -1)
if len(m) == 0 {
return ""
}
return strings.TrimSpace(m[len(m)-1][1])
// separatorRow matches the rule Claude Code draws above and below its editor.
var separatorRow = regexp.MustCompile(`^[\s─━┄┅┈┉-]+$`)
// InputState is what the interactive editor holds. Active distinguishes the
// live editor from queued or already-submitted input: both render with the
// same "" prefix, but only the live editor owns the pane cursor (F17,
// found live during burn-in run 3).
type InputState struct {
Text string
Active bool
}
// ConfirmLaunch waits for observable proof that the harness accepted the
// prompt. An input editor that still holds the submitted text is definite
// failure, which is the whole point: TaskLaunchAcknowledged previously meant
// "Prompt returned nil" and so reported a launch that never happened.
// sameInput compares editor content to what was submitted. Soft wrapping may
// break the text at any column and re-indent the continuation, so the
// comparison ignores whitespace entirely.
func sameInput(a, b string) bool {
strip := func(s string) string { return strings.Join(strings.Fields(s), "") }
return strip(a) == strip(b)
}
// inputState reads the editor that owns the cursor. Scanning every line
// beginning with "" cannot tell an unsubmitted prompt from one the
// harness already queued, which is why this asks tmux where the cursor is.
func (b *TmuxBackend) inputState(ctx context.Context, s Session) (InputState, error) {
out, err := b.command(ctx, "display-message", "-p", "-t", tmuxTarget(s.PaneID), "#{cursor_y}")
if err != nil {
return InputState{}, err
}
row, err := strconv.Atoi(strings.TrimSpace(string(out)))
if err != nil {
return InputState{}, fmt.Errorf("tmux backend: cursor row %q: %w", strings.TrimSpace(string(out)), err)
}
// Screen rows, unjoined: -J merges wrapped rows and would invalidate the
// cursor row index.
raw, err := b.command(ctx, "capture-pane", "-p", "-t", tmuxTarget(s.PaneID))
if err != nil {
return InputState{}, err
}
rows := strings.Split(strings.TrimRight(string(raw), "\n"), "\n")
if row < 0 || row >= len(rows) {
return InputState{}, nil
}
start := -1
for i := row; i >= 0; i-- {
if promptLine.MatchString(rows[i]) {
start = i
break
}
if separatorRow.MatchString(rows[i]) {
break
}
}
if start < 0 {
return InputState{}, nil
}
parts := []string{strings.TrimSpace(promptLine.FindStringSubmatch(rows[start])[1])}
for i := start + 1; i <= row; i++ {
parts = append(parts, strings.TrimSpace(rows[i]))
}
return InputState{Text: strings.TrimSpace(strings.Join(parts, " ")), Active: true}, nil
}
// launchResubmitLimit bounds the intervention independently of the observation
// deadline. A slow TUI must not receive a fortieth Enter after it accepted the
// first: three exact-editor resubmits, then observation only.
const launchResubmitLimit = 3
// ConfirmLaunch drives the submit to a decision instead of assuming one Enter
// landed. Burn-in run 3 proved the submit is not deterministic: the text
// reached the editor on all three attempts and the following Enter never took
// effect. So this resends Enter while the live editor still holds exactly what
// was submitted, up to launchResubmitLimit times, then observes until the
// deadline.
//
// This is not a timing workaround for the submit itself. The submit is
// deterministic; this waits for the harness to visibly react to it.
// The returned evidence records how many submits it took, which is the only
// way to tell a harness that needs a second Enter from one that needed none.
func (b *TmuxBackend) ConfirmLaunch(ctx context.Context, s Session, submitted string) (string, error) {
timeout, poll := b.LaunchConfirmTimeout, b.LaunchConfirmPoll
if timeout <= 0 {
@@ -389,14 +450,42 @@ func (b *TmuxBackend) ConfirmLaunch(ctx context.Context, s Session, submitted st
if poll <= 0 {
poll = 250 * time.Millisecond
}
deadline := b.now().Add(timeout)
var last string
first := b.now()
deadline := first.Add(timeout)
attempts, resubmits := 1, 0
var lastResubmit time.Time
evidence := func(kind string) string {
return fmt.Sprintf("confirmation=%s submit_attempts=%d first_submit_at=%s confirmed_at=%s",
kind, attempts, first.UTC().Format(time.RFC3339Nano), b.now().UTC().Format(time.RFC3339Nano))
}
var last InputState
for {
pane, err := b.PaneCapture(ctx, s, "recent")
state, err := b.inputState(ctx, s)
if err != nil {
return "", err
}
if last = pendingInput(pane); last == "" {
last = state
switch {
case state.Active && sameInput(state.Text, submitted):
// Exactly what was submitted still owns the cursor, so the submit
// did not take. Resend Enter, spaced so a TUI that accepted the
// previous one cannot receive another inside its own redraw.
if resubmits < launchResubmitLimit && (lastResubmit.IsZero() || b.now().Sub(lastResubmit) >= 2*poll) {
if err := b.SendKeys(ctx, s, []string{"Enter"}); err != nil {
return "", err
}
resubmits++
attempts++
lastResubmit = b.now()
}
case state.Active && queuedInput(state.Text):
// The harness accepted the instruction and parked it behind the
// current turn. Queued is submitted.
return evidence("queued"), nil
case state.Active && state.Text != "":
// Something else is in the editor, a paste placeholder for
// instance. Not proof of acceptance, so keep observing.
default:
status, statusErr := b.AgentStatus(ctx, s)
if statusErr != nil {
return "", statusErr
@@ -405,18 +494,18 @@ func (b *TmuxBackend) ConfirmLaunch(ctx context.Context, s Session, submitted st
case "exited":
return "", fmt.Errorf("%w: pane %s exited before the harness reacted", ErrPromptNotSubmitted, s.PaneID)
case "busy":
return "input editor cleared, agent busy", nil
return evidence("busy"), nil
case "blocked":
// A permission dialog is the harness acting on the
// instruction, so submission is proven even though the
// session now needs an operator.
return "input editor cleared, agent awaiting approval", nil
return evidence("blocked"), nil
default:
return "input editor cleared", nil
return evidence("editor_cleared"), nil
}
}
if !b.now().Before(deadline) {
return "", fmt.Errorf("%w: pane %s still holds %q after %s", ErrPromptNotSubmitted, s.PaneID, last, timeout)
return "", fmt.Errorf("%w: pane %s still holds %q after %s and %d submits", ErrPromptNotSubmitted, s.PaneID, last.Text, timeout, attempts)
}
select {
case <-ctx.Done():
@@ -425,3 +514,9 @@ func (b *TmuxBackend) ConfirmLaunch(ctx context.Context, s Session, submitted st
}
}
}
// queuedInput recognizes the editor Claude Code shows once it has taken a
// prompt and parked it behind the running turn.
func queuedInput(text string) bool {
return strings.Contains(strings.ToLower(text), "queued message")
}
+169 -13
View File
@@ -140,22 +140,173 @@ func TestLaunchTransportIsHarnessSpecific(t *testing.T) {
}
}
// The exact F15 signature: the TUI coalesced the launch text into a paste and
// absorbed the Enter, leaving the instruction in the input editor.
func TestPendingInputSeesAnUnsubmittedPaste(t *testing.T) {
stalled := " b141e4f6 - none | 0/1.0m ctx\n [Pasted text #1 +66 lines]\n────────────\n"
if got := pendingInput(stalled); got != "[Pasted text #1 +66 lines]" {
t.Fatalf("pendingInput=%q, want the unsubmitted paste", got)
// F17, found live during burn-in run 3: a queued or already-accepted prompt
// renders with the same "" prefix as an unsubmitted one, so only cursor
// ownership can tell them apart.
func TestInputStateReadsTheEditorOwningTheCursor(t *testing.T) {
wrapped := "\u2500\u2500\u2500\u2500\n\u276f @.orchestra/launch.md is your complete Orchestra launch instruction. Read\n .orchestra/launch.md now and follow it.\n\u2500\u2500\u2500\u2500\n"
b := paneTmux(t, wrapped, 2)
state, err := b.inputState(context.Background(), Session{PaneID: "s:1.0"})
if err != nil {
t.Fatal(err)
}
if got := pendingInput("esc to interrupt\n \n"); got != "" {
t.Fatalf("pendingInput=%q, want empty for a cleared editor", got)
if !state.Active || !sameInput(state.Text, LaunchReference) {
t.Fatalf("state=%+v, want the wrapped launch reference in the active editor", state)
}
if got := pendingInput(" @.orchestra/launch.md is your complete Orchestra launch instruction."); got == "" {
t.Fatal("a one-line reference still sitting in the editor must count as pending")
// The same text, but the cursor sits in the empty editor below it: the
// harness took the prompt and queued it.
queued := " \u276f @.orchestra/launch.md is your complete Orchestra launch instruction. Read\n .orchestra/launch.md now and follow it.\n\u2500\u2500\u2500\u2500\n\u276f Press up to edit queued messages\n"
b = paneTmux(t, queued, 3)
state, err = b.inputState(context.Background(), Session{PaneID: "s:1.0"})
if err != nil {
t.Fatal(err)
}
if got := pendingInput("no editor line here"); got != "" {
t.Fatalf("pendingInput=%q, want empty when no editor line is visible", got)
if !state.Active || sameInput(state.Text, LaunchReference) {
t.Fatalf("state=%+v, want the queued marker rather than the launch text", state)
}
if !queuedInput(state.Text) {
t.Fatalf("state=%+v, want queued input recognized", state)
}
}
// Run 3 proved one Enter is not enough. A launch still owning the cursor must
// be resubmitted, and the evidence must say how many submits it took.
func TestConfirmLaunchResubmitsUntilTheEditorClears(t *testing.T) {
b, keys := submitTmux(t, 2)
evidence, err := b.ConfirmLaunch(context.Background(), Session{PaneID: "s:1.0"}, LaunchReference)
if err != nil {
t.Fatalf("confirm: %v", err)
}
if !strings.Contains(evidence, "submit_attempts=2") {
t.Fatalf("evidence=%q, want two submits recorded", evidence)
}
if got := enterCount(t, keys); got != 1 {
t.Fatalf("resent Enter %d times, want 1", got)
}
}
// The intervention is bounded independently of the observation deadline: a TUI
// that already accepted the prompt must not receive a fortieth Enter.
func TestConfirmLaunchBoundsResubmission(t *testing.T) {
b, keys := submitTmux(t, 99)
// Generous deadline on purpose: the cap must be what stops the resends.
b.LaunchConfirmTimeout = 3 * time.Second
if _, err := b.ConfirmLaunch(context.Background(), Session{PaneID: "s:1.0"}, LaunchReference); !errors.Is(err, ErrPromptNotSubmitted) {
t.Fatalf("err=%v, want ErrPromptNotSubmitted", err)
}
if got := enterCount(t, keys); got != launchResubmitLimit {
t.Fatalf("resent Enter %d times, want the %d-resubmit cap", got, launchResubmitLimit)
}
}
// Queued is submitted. Treating it as a stalled editor would kill a pane whose
// harness had already accepted the instruction.
func TestConfirmLaunchAcceptsQueuedInput(t *testing.T) {
pane := "\u2500\u2500\u2500\u2500\n\u276f Press up to edit queued messages\n"
b := paneTmux(t, pane, 1)
evidence, err := b.ConfirmLaunch(context.Background(), Session{PaneID: "s:1.0"}, LaunchReference)
if err != nil {
t.Fatalf("confirm: %v", err)
}
if !strings.Contains(evidence, "confirmation=queued") {
t.Fatalf("evidence=%q, want queued input confirmed", evidence)
}
}
// paneTmux serves one fixed pane with the cursor on the given row.
func paneTmux(t *testing.T, pane string, cursor int) *TmuxBackend {
t.Helper()
dir := t.TempDir()
paneFile := filepath.Join(dir, "pane")
if err := os.WriteFile(paneFile, []byte(pane), 0o644); err != nil {
t.Fatal(err)
}
bin := filepath.Join(dir, "tmux")
script := `#!/bin/sh
cmd=""
for a in "$@"; do
case "$a" in
capture-pane|has-session|display-message|send-keys) cmd=$a; break;;
esac
done
case "$cmd" in
has-session) exit 0 ;;
capture-pane) cat ` + paneFile + ` ; exit 0 ;;
display-message)
case "$*" in
*cursor_y*) printf '` + fmt.Sprint(cursor) + `\n' ;;
*) printf '0\tclaude\n' ;;
esac
exit 0 ;;
esac
exit 0
`
if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
t.Fatal(err)
}
return &TmuxBackend{Binary: bin, LaunchConfirmTimeout: time.Second, LaunchConfirmPoll: 5 * time.Millisecond}
}
// submitTmux holds the launch reference in the active editor until the given
// number of submits has been received, then clears it. The first submit is the
// one Prompt already sent, so submitOn=2 means one resend is required.
func submitTmux(t *testing.T, submitOn int) (*TmuxBackend, string) {
t.Helper()
dir := t.TempDir()
keys := filepath.Join(dir, "keys")
bin := filepath.Join(dir, "tmux")
script := `#!/bin/sh
dir=` + dir + `
cmd=""
for a in "$@"; do
case "$a" in
capture-pane|has-session|display-message|send-keys) cmd=$a; break;;
esac
done
submits=$(cat $dir/submits 2>/dev/null || echo 1)
case "$cmd" in
has-session) exit 0 ;;
send-keys)
case "$*" in
*Enter*)
echo Enter >> ` + keys + `
echo $((submits+1)) > $dir/submits ;;
esac
exit 0 ;;
capture-pane)
if [ "$submits" -ge "` + fmt.Sprint(submitOn) + `" ]; then
printf 'esc to interrupt\n\xe2\x9d\xaf \n'
else
printf '\xe2\x94\x80\x0a\xe2\x9d\xaf ` + LaunchReference + `\n'
fi
exit 0 ;;
display-message)
case "$*" in
*cursor_y*)
if [ "$submits" -ge "` + fmt.Sprint(submitOn) + `" ]; then printf '1\n'; else printf '1\n'; fi ;;
*) printf '0\tclaude\n' ;;
esac
exit 0 ;;
esac
exit 0
`
if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
t.Fatal(err)
}
return &TmuxBackend{Binary: bin, LaunchConfirmTimeout: time.Second, LaunchConfirmPoll: 5 * time.Millisecond}, keys
}
func enterCount(t *testing.T, path string) int {
t.Helper()
b, err := os.ReadFile(path)
if os.IsNotExist(err) {
return 0
}
if err != nil {
t.Fatal(err)
}
return len(strings.Fields(string(b)))
}
// fakeTmux scripts capture-pane so confirmation can be driven without waiting
@@ -179,7 +330,12 @@ for a in "$@"; do
done
case "$cmd" in
has-session) exit 0 ;;
display-message) printf '0\tclaude\n'; exit 0 ;;
display-message)
case "$*" in
*cursor_y*) printf '0\n' ;;
*) printf '0\tclaude\n' ;;
esac
exit 0 ;;
capture-pane)
if [ "` + fail + `" = "1" ]; then echo "no server running" >&2; exit 1; fi
n=$(cat $state 2>/dev/null || echo 0); n=$((n+1)); echo $n > $state