diff --git a/docs/api-reference.md b/docs/api-reference.md index 1bd86888..b1323a58 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -437,7 +437,9 @@ the raw write, and an explicit `"useMux": false` forces it. With `"useMux": true`, a line feed is kept: the text goes in as one bracketed paste (`\r\n` read as `\n`, leading and trailing line breaks dropped), then Enter if the input carried a `\r`; a pane that has not enabled bracketed paste receives each line -break as a carriage return and runs the lines as they arrive. +break as a carriage return and runs the lines as they arrive. The check that +re-presses a stranded Enter cannot reliably recognise a multi-line paste, so a long +multi-line prompt to a just-started agent is still safer in a file it is told to read. ```bash curl -s -X POST "$API/api/v1/sessions/$SID/input" \ diff --git a/plugins/codeman/skills/codeman/SKILL.md b/plugins/codeman/skills/codeman/SKILL.md index e67e96c9..621748ae 100644 --- a/plugins/codeman/skills/codeman/SKILL.md +++ b/plugins/codeman/skills/codeman/SKILL.md @@ -70,7 +70,7 @@ later call opens with, and your first REAL call performs them anyway: ```bash . "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null -[ "${CODEMAN_PREAMBLE:-}" = 1.42.0 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; } +[ "${CODEMAN_PREAMBLE:-}" = 1.43.1 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; } ``` ⚠️ **Never spend a Bash call on this check alone.** §1's block opens with this same @@ -98,8 +98,8 @@ PRE="${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" mkdir -p "$(dirname "$PRE")" # Rewrite unless the file already ends with THIS version's stamp, so a stale or a # half-written file self-heals here instead of costing you a round trip to rm it. -grep -qs '^CODEMAN_PREAMBLE=1.42.0$' "$PRE" || (umask 077; cat > "$PRE" <<'PREAMBLE' -# ---- Codeman agent preamble 1.42.0 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ---- +grep -qs '^CODEMAN_PREAMBLE=1.43.1$' "$PRE" || (umask 077; cat > "$PRE" <<'PREAMBLE' +# ---- Codeman agent preamble 1.43.1 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ---- # Positional parameters are always written with braces, ${1} and never a bare dollar-digit: Claude Code # replaces a bare one in a skill body with the matching word of the skill's arguments, and §0 is pasted # from that body (test/agent-skill.test.ts pins it). @@ -339,6 +339,15 @@ spawn_workers() { # pane clearly finished means that profile, so switch that worker to markers. sendwait() { local sid="${1:?}" p="${2:?}" seq="${3:-$(date +%s)}" body r c head n=0 tmp bg i + # One line, like every other waiting sender (`toSingleLine`, `codeman agent send`, + # the TUI): the server pastes a multi-line prompt as one block, and the loop below, + # like the server's own submit check, cannot recognise a pasted block that is still + # on the composer, so a prompt like `Review this:\n- item 1` would sit unsent for + # the whole re-wait. Each line is trimmed, blank lines are dropped, the rest are + # joined with a space. For a long multi-line task, write a file and send its path. + case "$p" in *$'\n'*|*$'\r'*) + p=$(printf '%s\n' "$p" | tr '\r' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e '/^$/d' | paste -sd ' ' -) ;; + esac # `wait:"stop,exit"`, never the `wait:true` default set: that set also carries # `idle`, which is INFERRED from output stabilization and flaps mid-turn. On a # dsh worker whose TUI repaints rarely the session reads `idle` while the model @@ -410,10 +419,10 @@ last_text() { # The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept # bare on purpose: the write condition above anchors on it with $, so an inline comment # here would fail that match and rewrite this file on every single bootstrap. -CODEMAN_PREAMBLE=1.42.0 +CODEMAN_PREAMBLE=1.43.1 PREAMBLE ) -. "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.42.0 ] || { echo "preamble at $PRE is stale or truncated: rm it and re-run this block"; exit 1; } +. "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.43.1 ] || { echo "preamble at $PRE is stale or truncated: rm it and re-run this block"; exit 1; } ``` Every later Bash call that touches the API starts with the same two loader lines from @@ -464,7 +473,7 @@ and no per-call body to hand-build. ```bash . "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null # §0 loader -[ "${CODEMAN_PREAMBLE:-}" = 1.42.0 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; } +[ "${CODEMAN_PREAMBLE:-}" = 1.43.1 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; } N=(alpha beta) # INVENT one fresh case name per worker; never list cases first # (a name may carry a mode: `beta:deepseek`, see below) T=('reply with one line: the absolute path of your working directory' diff --git a/plugins/codeman/skills/codeman/preamble.sh b/plugins/codeman/skills/codeman/preamble.sh index 43c30afa..b6dd5f42 100644 --- a/plugins/codeman/skills/codeman/preamble.sh +++ b/plugins/codeman/skills/codeman/preamble.sh @@ -1,4 +1,4 @@ -# ---- Codeman agent preamble 1.42.0 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ---- +# ---- Codeman agent preamble 1.43.1 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ---- # Positional parameters are always written with braces, ${1} and never a bare dollar-digit: Claude Code # replaces a bare one in a skill body with the matching word of the skill's arguments, and §0 is pasted # from that body (test/agent-skill.test.ts pins it). @@ -238,6 +238,15 @@ spawn_workers() { # pane clearly finished means that profile, so switch that worker to markers. sendwait() { local sid="${1:?}" p="${2:?}" seq="${3:-$(date +%s)}" body r c head n=0 tmp bg i + # One line, like every other waiting sender (`toSingleLine`, `codeman agent send`, + # the TUI): the server pastes a multi-line prompt as one block, and the loop below, + # like the server's own submit check, cannot recognise a pasted block that is still + # on the composer, so a prompt like `Review this:\n- item 1` would sit unsent for + # the whole re-wait. Each line is trimmed, blank lines are dropped, the rest are + # joined with a space. For a long multi-line task, write a file and send its path. + case "$p" in *$'\n'*|*$'\r'*) + p=$(printf '%s\n' "$p" | tr '\r' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e '/^$/d' | paste -sd ' ' -) ;; + esac # `wait:"stop,exit"`, never the `wait:true` default set: that set also carries # `idle`, which is INFERRED from output stabilization and flaps mid-turn. On a # dsh worker whose TUI repaints rarely the session reads `idle` while the model @@ -309,4 +318,4 @@ last_text() { # The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept # bare on purpose: the write condition above anchors on it with $, so an inline comment # here would fail that match and rewrite this file on every single bootstrap. -CODEMAN_PREAMBLE=1.42.0 +CODEMAN_PREAMBLE=1.43.1 diff --git a/plugins/codeman/skills/codeman/reference/recipes.md b/plugins/codeman/skills/codeman/reference/recipes.md index be1db504..99ef3108 100644 --- a/plugins/codeman/skills/codeman/reference/recipes.md +++ b/plugins/codeman/skills/codeman/reference/recipes.md @@ -21,7 +21,7 @@ by sourcing the preamble file the §0 bootstrap wrote, and checking its version ```bash . "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null -[ "${CODEMAN_PREAMBLE:-}" = 1.42.0 ] || { echo "preamble missing or stale; re-run the §0 bootstrap"; exit 1; } +[ "${CODEMAN_PREAMBLE:-}" = 1.43.1 ] || { echo "preamble missing or stale; re-run the §0 bootstrap"; exit 1; } ``` Do **not** re-paste the preamble body into each call. Sourcing it is what retires the diff --git a/skills/codeman/SKILL.md b/skills/codeman/SKILL.md index e67e96c9..621748ae 100644 --- a/skills/codeman/SKILL.md +++ b/skills/codeman/SKILL.md @@ -70,7 +70,7 @@ later call opens with, and your first REAL call performs them anyway: ```bash . "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null -[ "${CODEMAN_PREAMBLE:-}" = 1.42.0 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; } +[ "${CODEMAN_PREAMBLE:-}" = 1.43.1 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; } ``` ⚠️ **Never spend a Bash call on this check alone.** §1's block opens with this same @@ -98,8 +98,8 @@ PRE="${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" mkdir -p "$(dirname "$PRE")" # Rewrite unless the file already ends with THIS version's stamp, so a stale or a # half-written file self-heals here instead of costing you a round trip to rm it. -grep -qs '^CODEMAN_PREAMBLE=1.42.0$' "$PRE" || (umask 077; cat > "$PRE" <<'PREAMBLE' -# ---- Codeman agent preamble 1.42.0 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ---- +grep -qs '^CODEMAN_PREAMBLE=1.43.1$' "$PRE" || (umask 077; cat > "$PRE" <<'PREAMBLE' +# ---- Codeman agent preamble 1.43.1 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ---- # Positional parameters are always written with braces, ${1} and never a bare dollar-digit: Claude Code # replaces a bare one in a skill body with the matching word of the skill's arguments, and §0 is pasted # from that body (test/agent-skill.test.ts pins it). @@ -339,6 +339,15 @@ spawn_workers() { # pane clearly finished means that profile, so switch that worker to markers. sendwait() { local sid="${1:?}" p="${2:?}" seq="${3:-$(date +%s)}" body r c head n=0 tmp bg i + # One line, like every other waiting sender (`toSingleLine`, `codeman agent send`, + # the TUI): the server pastes a multi-line prompt as one block, and the loop below, + # like the server's own submit check, cannot recognise a pasted block that is still + # on the composer, so a prompt like `Review this:\n- item 1` would sit unsent for + # the whole re-wait. Each line is trimmed, blank lines are dropped, the rest are + # joined with a space. For a long multi-line task, write a file and send its path. + case "$p" in *$'\n'*|*$'\r'*) + p=$(printf '%s\n' "$p" | tr '\r' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e '/^$/d' | paste -sd ' ' -) ;; + esac # `wait:"stop,exit"`, never the `wait:true` default set: that set also carries # `idle`, which is INFERRED from output stabilization and flaps mid-turn. On a # dsh worker whose TUI repaints rarely the session reads `idle` while the model @@ -410,10 +419,10 @@ last_text() { # The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept # bare on purpose: the write condition above anchors on it with $, so an inline comment # here would fail that match and rewrite this file on every single bootstrap. -CODEMAN_PREAMBLE=1.42.0 +CODEMAN_PREAMBLE=1.43.1 PREAMBLE ) -. "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.42.0 ] || { echo "preamble at $PRE is stale or truncated: rm it and re-run this block"; exit 1; } +. "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.43.1 ] || { echo "preamble at $PRE is stale or truncated: rm it and re-run this block"; exit 1; } ``` Every later Bash call that touches the API starts with the same two loader lines from @@ -464,7 +473,7 @@ and no per-call body to hand-build. ```bash . "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null # §0 loader -[ "${CODEMAN_PREAMBLE:-}" = 1.42.0 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; } +[ "${CODEMAN_PREAMBLE:-}" = 1.43.1 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; } N=(alpha beta) # INVENT one fresh case name per worker; never list cases first # (a name may carry a mode: `beta:deepseek`, see below) T=('reply with one line: the absolute path of your working directory' diff --git a/skills/codeman/preamble.sh b/skills/codeman/preamble.sh index 43c30afa..b6dd5f42 100644 --- a/skills/codeman/preamble.sh +++ b/skills/codeman/preamble.sh @@ -1,4 +1,4 @@ -# ---- Codeman agent preamble 1.42.0 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ---- +# ---- Codeman agent preamble 1.43.1 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ---- # Positional parameters are always written with braces, ${1} and never a bare dollar-digit: Claude Code # replaces a bare one in a skill body with the matching word of the skill's arguments, and §0 is pasted # from that body (test/agent-skill.test.ts pins it). @@ -238,6 +238,15 @@ spawn_workers() { # pane clearly finished means that profile, so switch that worker to markers. sendwait() { local sid="${1:?}" p="${2:?}" seq="${3:-$(date +%s)}" body r c head n=0 tmp bg i + # One line, like every other waiting sender (`toSingleLine`, `codeman agent send`, + # the TUI): the server pastes a multi-line prompt as one block, and the loop below, + # like the server's own submit check, cannot recognise a pasted block that is still + # on the composer, so a prompt like `Review this:\n- item 1` would sit unsent for + # the whole re-wait. Each line is trimmed, blank lines are dropped, the rest are + # joined with a space. For a long multi-line task, write a file and send its path. + case "$p" in *$'\n'*|*$'\r'*) + p=$(printf '%s\n' "$p" | tr '\r' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e '/^$/d' | paste -sd ' ' -) ;; + esac # `wait:"stop,exit"`, never the `wait:true` default set: that set also carries # `idle`, which is INFERRED from output stabilization and flaps mid-turn. On a # dsh worker whose TUI repaints rarely the session reads `idle` while the model @@ -309,4 +318,4 @@ last_text() { # The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept # bare on purpose: the write condition above anchors on it with $, so an inline comment # here would fail that match and rewrite this file on every single bootstrap. -CODEMAN_PREAMBLE=1.42.0 +CODEMAN_PREAMBLE=1.43.1 diff --git a/skills/codeman/reference/recipes.md b/skills/codeman/reference/recipes.md index be1db504..99ef3108 100644 --- a/skills/codeman/reference/recipes.md +++ b/skills/codeman/reference/recipes.md @@ -21,7 +21,7 @@ by sourcing the preamble file the §0 bootstrap wrote, and checking its version ```bash . "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null -[ "${CODEMAN_PREAMBLE:-}" = 1.42.0 ] || { echo "preamble missing or stale; re-run the §0 bootstrap"; exit 1; } +[ "${CODEMAN_PREAMBLE:-}" = 1.43.1 ] || { echo "preamble missing or stale; re-run the §0 bootstrap"; exit 1; } ``` Do **not** re-paste the preamble body into each call. Sourcing it is what retires the diff --git a/test/agent-skill-sendwait.test.ts b/test/agent-skill-sendwait.test.ts new file mode 100644 index 00000000..d0d45e53 --- /dev/null +++ b/test/agent-skill-sendwait.test.ts @@ -0,0 +1,80 @@ +/** + * @fileoverview `sendwait` in the packaged agent preamble sends one line. + * + * Runs the REAL `skills/codeman/preamble.sh` in bash with `CURL` replaced by a stub + * that records the request body, so it checks what the function puts on the wire, + * not a copy of its logic. The server pastes a multi-line prompt as one block, and + * neither sendwait's Enter loop nor the server's submit check can recognise a pasted + * block still sitting on the composer, so the prompt must arrive as one line, the + * way `toSingleLine` joins it for the other unattended senders. + * + * Port: N/A (no server; the stub answers every request). + */ + +import { describe, it, expect } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { toSingleLine } from '../src/utils/single-line.js'; + +const PREAMBLE = join(__dirname, '..', 'skills', 'codeman', 'preamble.sh'); + +// The preamble builds every request body with jq. +let hasJq = true; +try { + execFileSync('jq', ['--version'], { stdio: 'ignore' }); +} catch { + hasJq = false; +} + +/** The `input` field of the first body sendwait posts for `prompt`. */ +function sentInput(prompt: string): string { + // sendwait calls CURL inside $(...), a subshell, so the stub records to a file. + const dir = mkdtempSync(join(tmpdir(), 'codeman-sendwait-')); + const bodies = join(dir, 'bodies'); + const script = ` + . "$PREAMBLE" >/dev/null + stub() { + while [ "$#" -gt 0 ]; do + if [ "$1" = --data-binary ]; then printf '%s\\n' "$2" >> "$BODIES"; shift; fi + shift + done + printf '%s' '{"data":{"delivered":true,"wait":{"timedOut":false}}}' + } + CURL=(stub) + sendwait worker1234 "$PROMPT" 7 >/dev/null + `; + try { + execFileSync('bash', ['-c', script], { + env: { + PATH: process.env.PATH, + HOME: process.env.HOME, + PREAMBLE, + PROMPT: prompt, + BODIES: bodies, + CODEMAN_API_URL: 'http://127.0.0.1:9', + CODEMAN_SESSION_ID: 'caller-session-0000', + }, + }); + const first = readFileSync(bodies, 'utf-8').split('\n')[0]; + return (JSON.parse(first) as { input: string }).input; + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +describe.skipIf(!hasJq)('preamble sendwait', () => { + it('joins a multi-line prompt onto one line before sending it', () => { + expect(sentInput('Review this:\n- item 1\n- item 2')).toBe('Review this: - item 1 - item 2\r'); + }); + + it('joins the same way as toSingleLine (trimmed lines, blank lines dropped, \\r\\n as a break)', () => { + const prompt = ' first line \r\n\n\t second\n\n \nthird\n'; + expect(sentInput(prompt)).toBe(toSingleLine(prompt) + '\r'); + }); + + it('leaves a single-line prompt, glob characters included, as it was', () => { + expect(sentInput('fix * in ?.ts ')).toBe('fix * in ?.ts \r'); + }); +});