Files
multica/server/internal/daemon/execenv/reply_instructions.go
Bohan Jiang 046e4b1efa fix(execenv): switch every provider's Windows reply template to --content-file (#2411)
Three user reports converge on the same Windows-shell encoding bug:

- #2198 / #2236 — Chinese, Codex on Win11. Comments / descriptions
  generated by the agent arrive as `?`.
- #2376 — Cyrillic, non-Codex agent ("Ops Lead") on Win11 Desktop.
  Title preserved (argv → CreateProcessW UTF-16), description / agent
  reply garbled (stdin → shell-codepage re-encoding).

woodcoal's independent diagnosis on #2198 confirms the root cause:
Windows PowerShell 5.1's `$OutputEncoding` defaults to ASCIIEncoding
when piping to a native command, so non-ASCII bytes are silently
replaced with `?` before they reach `multica.exe`. The CLI's stdin
parsing is fine; the bytes are corrupted upstream, in the agent's
shell layer.

This PR ships the fix that supersedes the codex-only attempt in
PR #2265 (which is closed in favour of this one):

## CLI

Add `--content-file <path>` to `multica issue comment add` and
`--description-file <path>` to `multica issue {create,update}`. The
CLI reads bytes off disk via `os.ReadFile` and skips the shell
entirely; UTF-8 survives end-to-end regardless of `$OutputEncoding`
or `chcp`. The three input modes (`--content`, `--content-stdin`,
`--content-file`) are mutually exclusive.

## Runtime config

`buildMetaSkillContent`'s Available Commands section is rewritten as a
neutral three-mode menu. The previous unconditional "MUST pipe via
stdin" / `--description-stdin` mandate (over-spread from #1795 /
#1851's Codex-multi-line fix) is gone for non-Codex providers; the
strong directive now lives only in the Codex-Specific section, which
branches on host:

- Codex / Linux+macOS: `--content-stdin` + HEREDOC (preserves MUL-1467
  fix against codex's literal `\n` habit).
- Codex / Windows: `--content-file` (PowerShell ASCII pipe is the
  exact bug we're patching).

## Per-turn reply template

`BuildCommentReplyInstructions` now takes a provider arg and branches
provider × OS:

- Windows + any provider → `--content-file` (the bug is shell-layer,
  not provider-layer; #2376 shows non-Codex agents on Windows also
  hit it). All providers write a UTF-8 file with their file-write tool
  and post via `--content-file ./reply.md`.
- Linux/macOS + Codex → stdin/HEREDOC (MUL-1467 protection).
- Linux/macOS + non-Codex → lightweight pre-#1795 inline
  `--content "..."`. The CLI server-side decodes `\n`, so escaped
  multi-line works; the agent retains stdin / file as escape hatches
  for richer formatting.

`BuildPrompt` and `buildCommentPrompt` gain a `provider` arg;
`daemon.runTask` already has it in scope.

## Tests

- `TestResolveTextFlag` — file-source verbatim with non-ASCII
  (`标题 / Заголовок / 中文段落`), missing-file error, empty-file
  rejection, three-way mutual exclusion.
- `TestInjectRuntimeConfigAvailableCommandsIsNeutral` — every
  non-Codex provider × {linux, darwin, windows} pins the three-mode
  menu present + over-spread "MUST stdin" substrings absent.
- `TestInjectRuntimeConfigCodexLinuxEmphasizesStdin` +
  `TestInjectRuntimeConfigCodexWindowsUsesContentFile` — Codex
  section's per-OS branch.
- `TestBuildCommentReplyInstructionsCodexLinux` +
  `TestBuildCommentReplyInstructionsNonCodexLinux` +
  `TestBuildCommentReplyInstructionsWindowsUsesContentFile` — the
  reply-template provider × OS matrix.
- `TestInjectRuntimeConfigWindowsCommentTriggerHasNoStdin` — end-to-end
  AGENTS.md / CLAUDE.md on Windows has no prescriptive stdin
  directive, for claude / codex / opencode.

`go test ./...` and `go vet ./...` clean.

Closes #2198, #2236, #2376.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-11 17:05:45 +08:00

85 lines
4.9 KiB
Go

package execenv
import "fmt"
// BuildCommentReplyInstructions returns the canonical block telling an agent
// how to post its reply for a comment-triggered task. Both the per-turn
// prompt (daemon.buildCommentPrompt) and the CLAUDE.md workflow
// (InjectRuntimeConfig) call this so the trigger comment ID and the
// --parent value cannot drift between surfaces.
//
// The explicit "do not reuse --parent from previous turns" wording exists
// because resumed Claude sessions keep prior turns' tool calls in context
// and will otherwise copy the old --parent UUID forward.
//
// The template is provider- and platform-aware:
//
// - Windows + any provider → write a UTF-8 file, post with `--content-file`.
// This is the only path that survives Windows shells (PowerShell 5.1
// defaults to ASCIIEncoding when piping to native commands and drops
// non-ASCII as `?`; cmd.exe is at the mercy of `chcp`). The original
// reports — #2198 (Chinese), #2236 (Chinese), #2376 (Cyrillic, observed
// on a non-Codex agent) — all match this signature.
// - Linux/macOS + Codex → stdin/HEREDOC. Codex tends to emit literal `\n`
// escapes inside `--content "..."` and produce broken multi-line stored
// comments (MUL-1467); stdin sidesteps that.
// - Linux/macOS + non-Codex → lightweight inline `--content "..."`.
// The CLI's `util.UnescapeBackslashEscapes` decodes `\n` server-side,
// so escaped multi-line works correctly. This is the pre-#1795 default,
// restored after we found #1795 / #1851 had expanded a Codex-specific
// fix into a global mandate that broke Windows non-ASCII for every
// provider.
func BuildCommentReplyInstructions(provider, issueID, triggerCommentID string) string {
if triggerCommentID == "" {
return ""
}
if runtimeGOOS == "windows" {
return fmt.Sprintf(
"If you decide to reply, post it as a comment — always use the trigger comment ID below, "+
"do NOT reuse --parent values from previous turns in this session.\n\n"+
"On Windows, write the reply body to a UTF-8 file with your file-write tool, then post it with `--content-file`. "+
"Do NOT pipe via `--content-stdin` — Windows PowerShell 5.1's `$OutputEncoding` defaults to ASCIIEncoding when piping to native commands and silently drops non-ASCII (Chinese, Japanese, Cyrillic, accents, emoji) as `?` before the bytes reach `multica.exe`. "+
"Do NOT use inline `--content`; it is easy to lose formatting or accidentally compress a structured reply into one line.\n\n"+
"Use this form, preserving the same issue ID and --parent value:\n\n"+
" # 1. Write the reply body to a UTF-8 file (e.g. reply.md) with your file-write tool.\n"+
" # 2. Then run:\n"+
" multica issue comment add %s --parent %s --content-file ./reply.md\n\n"+
"Do NOT write literal `\\n` escapes to simulate line breaks; the file preserves real newlines.\n",
issueID, triggerCommentID,
)
}
if provider == "codex" {
return fmt.Sprintf(
"If you decide to reply, post it as a comment — always use the trigger comment ID below, "+
"do NOT reuse --parent values from previous turns in this session.\n\n"+
"Always use `--content-stdin` with a HEREDOC for agent-authored issue comments, even when the reply is a single line. "+
"Do NOT use inline `--content`; it is easy to lose formatting or accidentally compress a structured reply into one line.\n\n"+
"Use this form, preserving the same issue ID and --parent value:\n\n"+
" cat <<'COMMENT' | multica issue comment add %s --parent %s --content-stdin\n"+
" First paragraph.\n"+
"\n"+
" Second paragraph.\n"+
" COMMENT\n\n"+
"Do NOT write literal `\\n` escapes to simulate line breaks; the HEREDOC preserves real newlines.\n",
issueID, triggerCommentID,
)
}
// Non-Codex providers on Linux/macOS: lightweight inline template, no
// platform branch. Pre-#1795 default, restored after we found that
// #1795 / #1851 had expanded a Codex-specific fix into a global mandate
// that broke Windows non-ASCII for every provider. The CLI decodes
// `\n` etc. server-side, so escaped multi-line is fine; for richer
// formatting the agent can still reach for `--content-stdin` (works
// on Linux/macOS) or `--content-file <path>` (works on every platform),
// both listed in Available Commands above.
return fmt.Sprintf(
"If you decide to reply, post it as a comment — always use the trigger comment ID below, "+
"do NOT reuse --parent values from previous turns in this session.\n\n"+
"Use this form, preserving the same issue ID and --parent value:\n\n"+
" multica issue comment add %s --parent %s --content \"...\"\n\n"+
"For multi-line bodies, code blocks, or content with quotes/backticks, prefer `--content-stdin` "+
"(pipe a HEREDOC) or `--content-file <path>` (read a UTF-8 file). See Available Commands above for the full menu.\n",
issueID, triggerCommentID,
)
}