diff --git a/README.md b/README.md index d3ad103a..bad01443 100644 --- a/README.md +++ b/README.md @@ -851,7 +851,7 @@ When a CLI runs in a Codeman-managed session, these environment variables are se ### Rules of the road (read before you POST) -1. **Single-line input, ending in `\r`.** Programmatic input is sent as literal text, and Enter fires **only when the input contains a carriage return**: `{"input":"run tests\r"}`. Without the `\r` the text sits on the session's prompt unsubmitted (and a combined `wait` runs its full timeout on a turn that never started). Embedded newlines are stripped rather than rejected, so `"echo A\necho B\r"` runs the joined command `echo Aecho B`: send one line per call. +1. **Input must end in `\r`.** Programmatic input is sent as literal text, and Enter fires **only when the input contains a carriage return**: `{"input":"run tests\r"}`. Without the `\r` the text sits on the session's prompt unsubmitted (and a combined `wait` runs its full timeout on a turn that never started). With `useMux`, embedded newlines are kept and the lines go in as one paste, so `"echo A\necho B\r"` runs both commands in a shell; the check that re-presses a stranded Enter cannot reliably recognise a multi-line paste, so put long multi-line content in a file the agent is told to read. 2. **Make input idempotent.** Include a stable `clientId` and a monotonic per-session `seq` on `POST …/input`. The server de-duplicates, so a retry after a dropped connection can't double-deliver a prompt. 3. **Auth.** If `CODEMAN_PASSWORD` is set, send HTTP Basic auth (user `admin` or `CODEMAN_USERNAME`) or a `codeman_session` cookie. The default loopback install is passwordless. A missing `Origin` header is allowed, so plain `curl` works; cross-site browser origins are rejected (CSRF guard). ⚠️ A `401` replies with the bare string `Unauthorized`, **not** the JSON envelope, so piping it into `jq` throws a parse error instead of showing the failure: check the status before parsing. 4. **Response envelope.** Most endpoints return `{ "success": true, "data": … }` (errors: `{ "success": false, "error", "errorCode" }`). A few legacy GETs return bare bodies — **handle both** (`body.data ?? body`). diff --git a/README.zh-CN.md b/README.zh-CN.md index eb5dd9ba..e70bd7a2 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -853,7 +853,7 @@ deleted 9f2d8e5f, aff9c691, be9f1f15 标签消失 ### 行路规则(POST 之前先读) -1. **只发单行输入,而且必须以 `\r` 结尾。** 编程输入按字面文本发送,**只有当输入里含回车符时才会触发 Enter**:`{"input":"run tests\r"}`。少了 `\r`,文本就停在会话的输入框里不被提交(同一次调用里的 `wait` 还会在一个压根没开始的回合上耗满整个超时)。内嵌的换行会被剥掉而不是报错,因此 `"echo A\necho B\r"` 执行的是拼起来的 `echo Aecho B`:一次调用只发一行。 +1. **输入必须以 `\r` 结尾。** 编程输入按字面文本发送,**只有当输入里含回车符时才会触发 Enter**:`{"input":"run tests\r"}`。少了 `\r`,文本就停在会话的输入框里不被提交(同一次调用里的 `wait` 还会在一个压根没开始的回合上耗满整个超时)。使用 `useMux` 时内嵌的换行会保留,各行作为一次粘贴送入,因此 `"echo A\necho B\r"` 在 shell 里会执行两条命令;但重新按下滞留 Enter 的检查无法可靠识别多行粘贴,所以较长的多行内容最好写进文件,让 agent 自己去读。 2. **让输入幂等。** 在 `POST …/input` 上带上稳定的 `clientId` 和按会话单调递增的 `seq`。服务端会去重,因此连接中断后的重试不会重复投递提示。 3. **认证。** 若设置了 `CODEMAN_PASSWORD`,发送 HTTP Basic 认证(用户 `admin` 或 `CODEMAN_USERNAME`)或 `codeman_session` cookie。默认的环回安装无密码。缺失的 `Origin` 头被允许,因此普通 `curl` 可用;跨站的浏览器 origin 会被拒绝(CSRF 防护)。⚠️ `401` 回的是裸字符串 `Unauthorized`,**不是** JSON 信封,直接喂给 `jq` 只会抛解析错误而看不到真正的失败原因:先看状态码,再解析。 4. **响应信封。** 多数端点返回 `{ "success": true, "data": … }`(错误:`{ "success": false, "error", "errorCode" }`)。少数遗留 GET 返回裸响应体 —— **两种都要处理**(`body.data ?? body`)。 diff --git a/docs/cron-guide.md b/docs/cron-guide.md index 0be6ec77..3ea21f62 100644 --- a/docs/cron-guide.md +++ b/docs/cron-guide.md @@ -207,9 +207,10 @@ trailing carriage return: | `typed` | `session.writeViaMux()` — the text as a tmux bracketed paste + Enter (a single character is typed, so a dialog answer selects) | Default; like a human pasting the prompt and pressing Enter. | | `paste` | `session.write()` — writes directly to the PTY/mux | Bulk paste-style delivery. | -> ⚠️ **Single-line only — enforced.** Like all programmatic input in Codeman, -> multi-line delivery would be silently corrupted (Ink-based TUIs treat a -> newline as submit; typed mode fuses lines). So newlines are **rejected**: the +> ⚠️ **Single-line only — enforced.** A job fires unattended, and the submit +> check that re-presses an ignored Enter cannot recognise a multi-line paste in +> the composer, so a stranded prompt would go unnoticed (in paste mode the +> newlines reach the CLI as keys). So newlines are **rejected**: the > schema and the form refuse a multi-line `promptText`, and at fire time a > prompt file whose content is multi-line (after stripping trailing newlines) > fails the run with a clear `errorMessage`. Put multi-line instructions in a diff --git a/docs/extending-codeman.md b/docs/extending-codeman.md index cad0d04e..3b76697a 100644 --- a/docs/extending-codeman.md +++ b/docs/extending-codeman.md @@ -294,8 +294,8 @@ if ! jq -e '.data.wait.matched' <<<"$R" >/dev/null; then fi # 3. Send the prompt AND register the wait in one call, so the answer cannot be -# the previous turn's idle state. Single line only, ending in \r (otherwise -# Enter is never sent and this wait times out on a turn that never started). +# the previous turn's idle state. It must end in \r (otherwise Enter is never +# sent and this wait times out on a turn that never started). W=$("${CURL[@]}" -X POST "$API/api/v1/sessions/$SID/input" \ -H 'Content-Type: application/json' \ -d '{"input":"Run the test suite and summarize the failures\r","useMux":true, @@ -414,14 +414,16 @@ Every one of these has cost somebody real time. shipped bugs more than once. - **`text/plain` bodies stay raw.** Auto-parsing them as JSON enabled simple-request CSRF, so it is deliberate. Send `application/json`. -- **Prompts are single-line and must end with `\r`.** The server splits your text - and Enter into two separate tmux writes (Ink needs them apart), but it sends the - Enter **only when the input contains a carriage return**. Without it your text - sits on the prompt unsubmitted, which is the single most common "the wait - endpoints don't work" report: the wait runs its full timeout on a turn that never - started. Newlines inside the string are stripped rather than rejected, so - `"echo A\necho B\r"` runs the single joined command `echo Aecho B`: send one line - per call. +- **Prompts must end with `\r`.** The server splits your text and Enter into two + separate tmux writes (Ink needs them apart), but it sends the Enter **only when + the input contains a carriage return**. Without it your text sits on the prompt + unsubmitted, which is the single most common "the wait endpoints don't work" + report: the wait runs its full timeout on a turn that never started. With + `useMux`, newlines inside the string are kept: the text goes in as one paste and + the Enter after it submits all of it, so `"echo A\necho B\r"` runs both commands + in a shell. 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. - **`wait-output`'s `from=now` is not "printed after you asked".** tmux repaints the visible screen on attach, on resize, and on any TUI redraw, and a repaint arrives as ordinary output, so text already on screen can satisfy a fresh wait. diff --git a/docs/wiki/Cron-Jobs.md b/docs/wiki/Cron-Jobs.md index e45f694f..36d04993 100644 --- a/docs/wiki/Cron-Jobs.md +++ b/docs/wiki/Cron-Jobs.md @@ -51,10 +51,11 @@ or a server restart shifts later runs slightly. That drift is accepted rather th ## Prompts are single line -This is the rule people trip over. Programmatic input into an agent session is single line -everywhere in Codeman, because the terminal UIs these CLIs use treat a newline as submit. A -multi-line prompt would be silently mangled, so it is **rejected** instead: the form refuses -it, and a prompt file whose contents are multi-line fails the run with a clear message. +This is the rule people trip over. A cron job fires with nobody watching, and the check that +re-presses an Enter the agent ignored cannot recognise a multi-line paste in the composer, so +a multi-line prompt could sit there unsent. In `paste` mode the newlines would reach the agent +as keys. So a multi-line prompt is **rejected**: the form refuses it, and a prompt file whose +contents are multi-line fails the run with a clear message. For anything longer than a sentence, put the instructions in a file and make the prompt tell the agent to read it: diff --git a/docs/wiki/Driving-Codeman-From-An-Agent.md b/docs/wiki/Driving-Codeman-From-An-Agent.md index bcf1bc06..3d976294 100644 --- a/docs/wiki/Driving-Codeman-From-An-Agent.md +++ b/docs/wiki/Driving-Codeman-From-An-Agent.md @@ -127,11 +127,12 @@ Read these before writing any code. Each one has cost somebody an afternoon. 1. **Input must end with `\r`.** Enter fires only when the payload contains a carriage return. Without it the text sits unsubmitted on the prompt, the request still succeeds, - and a combined wait burns its full timeout on a turn that never started. Embedded - newlines are kept: the text goes in as one paste and the Enter after it submits all of - it, so `"echo A\necho B\r"` runs both commands in a shell. 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. + and a combined wait burns its full timeout on a turn that never started. With + `useMux`, embedded newlines are kept: the text goes in as one paste and the Enter after + it submits all of it, so `"echo A\necho B\r"` runs both commands in a shell. 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. 2. **Make input idempotent.** Send a stable `clientId` and a monotonic per-session `seq`. The server deduplicates, so a retry after a dropped connection cannot double-deliver. 3. **Auth.** With `CODEMAN_PASSWORD` set, use HTTP Basic or the session cookie. A missing diff --git a/docs/wiki/Input-And-Voice.md b/docs/wiki/Input-And-Voice.md index 223835c6..928e511f 100644 --- a/docs/wiki/Input-And-Voice.md +++ b/docs/wiki/Input-And-Voice.md @@ -125,9 +125,9 @@ Sending prompts over the API has one rule that catches everyone: **the payload m `\r`** or Enter is never sent. The request still succeeds, the text sits unsubmitted in the composer, and any wait burns its whole timeout on a turn that never started. -Embedded newlines are kept: multi-line input goes in as one paste and the Enter after it -submits all of it, so `"echo A\necho B\r"` runs both commands in a shell. The check that -re-presses a stranded Enter cannot reliably recognise a multi-line paste, so long +With `useMux`, embedded newlines are kept: multi-line input goes in as one paste and the +Enter after it submits all of it, so `"echo A\necho B\r"` runs both commands in a shell. The +check that re-presses a stranded Enter cannot reliably recognise a multi-line paste, so long multi-line content is still safer in a file the agent is told to read. See [Driving Codeman From An Agent](Driving-Codeman-From-An-Agent). diff --git a/plugins/codeman/skills/codeman/reference/endpoints.md b/plugins/codeman/skills/codeman/reference/endpoints.md index 5e8b5d38..89ea8ba5 100644 --- a/plugins/codeman/skills/codeman/reference/endpoints.md +++ b/plugins/codeman/skills/codeman/reference/endpoints.md @@ -420,8 +420,9 @@ callers send no body at all. **or Enter is never sent**: the text is typed onto the worker's prompt and sits there unsubmitted. This is [symptom 1](#1-deliveredtrue-then-every-wait-times-out), the number-one silent failure. -- Newlines in `input` are kept: the text goes in as one bracketed paste and the `\r` - submits all lines. To send a bare Enter (confirm a dialog), send `{"input":"\r"}`. +- With `useMux`, newlines in `input` are kept: the text goes in as one bracketed paste + and the `\r` submits all lines (without `useMux`, multi-line input is written straight + into the pane). To send a bare Enter (confirm a dialog), send `{"input":"\r"}`. - `input` is capped at **65536** characters. ⚠️ **Two caps disagree and the smaller one is the real one**: the Zod schema allows 100000 (`schemas.ts:1035`), so a 65537-to-100000 character body passes validation and *then* 400s at the route against diff --git a/plugins/codeman/skills/codeman/reference/verbs.md b/plugins/codeman/skills/codeman/reference/verbs.md index f85fb631..47ca5cb7 100644 --- a/plugins/codeman/skills/codeman/reference/verbs.md +++ b/plugins/codeman/skills/codeman/reference/verbs.md @@ -303,10 +303,11 @@ duplicate resend costs nothing. Codeman types the text and sends Enter **only when the input contains a carriage return**; without it your command sits unsubmitted on the worker's prompt and everything downstream times out. No response field catches this: `delivered:true` -means "written to the pane", **not** "submitted". Newlines are kept: the text goes in -as one paste, and the `\r` submits all of it. Build the body with `jq -n` for any prompt you did not -author as a literal, because the inline `-d '{"input":"'"$P"'\r"}'` pattern breaks on -the first double quote, backslash or `$` in a real prompt: +means "written to the pane", **not** "submitted". With `useMux`, newlines are kept: the +text goes in as one paste, and the `\r` submits all of it. Build the body with `jq -n` +for any prompt you did not author as a literal, because the inline +`-d '{"input":"'"$P"'\r"}'` pattern breaks on the first double quote, backslash or `$` +in a real prompt: ```bash BODY=$(jq -n --arg p "$PROMPT" '{input:($p+"\r"),useMux:true,clientId:"agent-1",seq:1,wait:true,waitTimeout:60000}') @@ -587,9 +588,9 @@ allowlist in §4. ### 5.9 Big input via the workspace -The composer is a single line capped at 65536 characters with newlines stripped, which -makes it a bad channel for a spec, a diff or a file list. The workspace is the good -one, and for a local or docker case you are on the same filesystem as the worker. +The composer is capped at 65536 characters, and a multi-line paste cannot be confirmed +as submitted, which makes it a bad channel for a spec, a diff or a file list. The +workspace is the good one, and for a local or docker case you are on the same filesystem as the worker. 1. Write `TASK.md` into the worker's workspace with your own file tools. The path is `.data.casePath` from `quick-start`, or the `workingDir` you passed to diff --git a/skills/codeman/reference/endpoints.md b/skills/codeman/reference/endpoints.md index 5e8b5d38..89ea8ba5 100644 --- a/skills/codeman/reference/endpoints.md +++ b/skills/codeman/reference/endpoints.md @@ -420,8 +420,9 @@ callers send no body at all. **or Enter is never sent**: the text is typed onto the worker's prompt and sits there unsubmitted. This is [symptom 1](#1-deliveredtrue-then-every-wait-times-out), the number-one silent failure. -- Newlines in `input` are kept: the text goes in as one bracketed paste and the `\r` - submits all lines. To send a bare Enter (confirm a dialog), send `{"input":"\r"}`. +- With `useMux`, newlines in `input` are kept: the text goes in as one bracketed paste + and the `\r` submits all lines (without `useMux`, multi-line input is written straight + into the pane). To send a bare Enter (confirm a dialog), send `{"input":"\r"}`. - `input` is capped at **65536** characters. ⚠️ **Two caps disagree and the smaller one is the real one**: the Zod schema allows 100000 (`schemas.ts:1035`), so a 65537-to-100000 character body passes validation and *then* 400s at the route against diff --git a/skills/codeman/reference/verbs.md b/skills/codeman/reference/verbs.md index f85fb631..47ca5cb7 100644 --- a/skills/codeman/reference/verbs.md +++ b/skills/codeman/reference/verbs.md @@ -303,10 +303,11 @@ duplicate resend costs nothing. Codeman types the text and sends Enter **only when the input contains a carriage return**; without it your command sits unsubmitted on the worker's prompt and everything downstream times out. No response field catches this: `delivered:true` -means "written to the pane", **not** "submitted". Newlines are kept: the text goes in -as one paste, and the `\r` submits all of it. Build the body with `jq -n` for any prompt you did not -author as a literal, because the inline `-d '{"input":"'"$P"'\r"}'` pattern breaks on -the first double quote, backslash or `$` in a real prompt: +means "written to the pane", **not** "submitted". With `useMux`, newlines are kept: the +text goes in as one paste, and the `\r` submits all of it. Build the body with `jq -n` +for any prompt you did not author as a literal, because the inline +`-d '{"input":"'"$P"'\r"}'` pattern breaks on the first double quote, backslash or `$` +in a real prompt: ```bash BODY=$(jq -n --arg p "$PROMPT" '{input:($p+"\r"),useMux:true,clientId:"agent-1",seq:1,wait:true,waitTimeout:60000}') @@ -587,9 +588,9 @@ allowlist in §4. ### 5.9 Big input via the workspace -The composer is a single line capped at 65536 characters with newlines stripped, which -makes it a bad channel for a spec, a diff or a file list. The workspace is the good -one, and for a local or docker case you are on the same filesystem as the worker. +The composer is capped at 65536 characters, and a multi-line paste cannot be confirmed +as submitted, which makes it a bad channel for a spec, a diff or a file list. The +workspace is the good one, and for a local or docker case you are on the same filesystem as the worker. 1. Write `TASK.md` into the worker's workspace with your own file tools. The path is `.data.casePath` from `quick-start`, or the `workingDir` you passed to diff --git a/src/cron/cron-service.ts b/src/cron/cron-service.ts index 7a57424c..7b0a6599 100644 --- a/src/cron/cron-service.ts +++ b/src/cron/cron-service.ts @@ -97,7 +97,7 @@ const CRON_PSEUDO_FS_TREES: readonly string[] = ['/proc', '/sys', '/dev']; /** Sync blocklist for the create/update workingDir gate (no settings extras). */ const CRON_WORKING_DIR_BLOCKED_TREES: readonly string[] = [...DEFAULT_BLOCKED_TREES, ...CRON_PSEUDO_FS_TREES]; -/** Prompt delivery is single-line only (writeViaMux/Ink constraint). */ +/** Prompt delivery is single-line only: the submit check cannot recognise a multi-line paste. */ const HAS_NEWLINE = /[\r\n]/; /** The three session calls prompt delivery needs, so it can be tested without a PTY. */ diff --git a/src/respawn-controller.ts b/src/respawn-controller.ts index ef5d84f8..41f7f490 100644 --- a/src/respawn-controller.ts +++ b/src/respawn-controller.ts @@ -2829,7 +2829,8 @@ export class RespawnController extends EventEmitter { let updatePrompt = this.config.updatePrompt; if (statusBlock?.recommendation) { - // Append RECOMMENDATION to the update prompt (single-line — writeViaMux breaks on newlines) + // Append RECOMMENDATION to the update prompt (single-line: the submit check cannot + // recognise a multi-line paste in the composer) const rec = statusBlock.recommendation.replace(/\n/g, ' ').substring(0, 200); updatePrompt = `${this.config.updatePrompt} (Claude's last recommendation: ${rec})`; this.logAction('ralph', `Using RECOMMENDATION: ${rec.substring(0, 50)}...`); diff --git a/src/web/schemas.ts b/src/web/schemas.ts index ce577394..0a276d3c 100644 --- a/src/web/schemas.ts +++ b/src/web/schemas.ts @@ -1905,7 +1905,7 @@ export const ScheduledRunSchema = z.object({ /** 'HH:MM' 24-hour time. */ const hhmmSchema = z.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, 'Time must be HH:MM (24-hour)'); -/** Prompt delivery is single-line only (writeViaMux/Ink constraint) — reject newlines outright. */ +/** Prompt delivery is single-line only (the submit check cannot see a multi-line paste) — reject newlines outright. */ const noNewlines = (v: string) => !/[\r\n]/.test(v); /** Shared field shape for creating/updating a scheduled job. */