Compare commits

...

2 Commits

Author SHA1 Message Date
J
ce0a33e06b fix(daemon): keep Windows assignment brief file-only (address review)
Review catch on #3654: the previous commit added platform-agnostic prose
recommending "--content-file or --content-stdin" in the Available Commands
entry and the assignment-triggered step-6 example. The assignment path has
no BuildCommentReplyInstructions OS override, so on Windows an agent following
step 6 literally would pipe its final comment through PowerShell and drop
non-ASCII bytes (#2198 / #2236 / #2376) — contradicting this PR's own
Windows file-only rule in the ## Comment Formatting section.

Make the platform-agnostic surfaces defer to the OS-aware ## Comment
Formatting section (the single source of truth) instead of naming stdin.
The flag synopsis still lists all three modes.

Add TestInjectRuntimeConfigWindowsAssignmentBriefStaysFileOnly: a Windows
assignment-triggered brief must not contain any prescriptive "... or
--content-stdin" recommendation.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 17:30:26 +08:00
J
43220252f8 fix(daemon): make comment-posting guardrail provider-agnostic (MUL-2904)
Agents inlining a backtick-wrapped token into `multica issue comment add
--content "..."` had the shell run it as a command substitution, silently
deleting the token; the stored comment never matched the model's intent, so
it retried forever — spamming OKK-497 with duplicate comments.

The corruption is shell-driven, not provider-driven, so extend the
"never inline --content; use --content-file / quoted-HEREDOC --content-stdin"
rule from Codex-only to ALL providers:

- BuildCommentReplyInstructions: collapse the Linux/macOS non-Codex inline
  branch into the unified quoted-HEREDOC stdin template.
- buildMetaSkillContent: rename "Codex-Specific Comment Formatting" ->
  "Comment Formatting" and emit it for every provider; strengthen the
  Available Commands entry and the assignment step-6 examples to steer away
  from inline --content.
- Windows behavior unchanged (file-only; avoids PowerShell ASCII drop).

Tests: flip the non-Codex Linux reply test into a MUL-2904 regression,
broaden the stdin-emphasis test across providers, and pin the
provider-agnostic guardrail.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 17:05:31 +08:00
5 changed files with 218 additions and 142 deletions

View File

@@ -1259,22 +1259,16 @@ func TestInjectRuntimeConfigRequiresExplicitCommentPost(t *testing.T) {
}
}
// TestInjectRuntimeConfigAvailableCommandsIsNeutral pins that the core
// Available Commands section lists comment input modes neutrally for every
// non-Codex provider on every host OS, with no "MUST pipe via stdin" mandate.
//
// Background: #1795 / #1851 introduced "MUST pipe via stdin" /
// `--description-stdin` directives in the global section to fix Codex's
// habit of emitting literal `\n` inside `--content "..."` (MUL-1467).
// That mandate landed in the all-provider section and ended up steering every
// provider at stdin — which then broke non-ASCII bytes on Windows shells
// (#2198 / #2236 / #2376). This rollback keeps the strong Codex-specific
// mandate in the Codex-Specific section (pinned by
// TestInjectRuntimeConfigCodexLinuxEmphasizesStdin) and leaves the core global
// command entry neutral.
// TestInjectRuntimeConfigCommentGuardrailIsProviderAgnostic pins that the
// "never inline --content for agent-authored comments" guardrail reaches EVERY
// provider on every host OS — post-MUL-2904 the corruption is shell-driven, so
// the directive is no longer Codex-scoped. The Available Commands entry still
// lists all three input modes as available, and the legacy over-broad
// `--description-stdin` / "MUST pipe via stdin" phrasings (#1795 / #1851, which
// broke Windows non-ASCII) must NOT reappear.
//
// Not parallel: mutates the package-level runtimeGOOS.
func TestInjectRuntimeConfigAvailableCommandsIsNeutral(t *testing.T) {
func TestInjectRuntimeConfigCommentGuardrailIsProviderAgnostic(t *testing.T) {
saved := runtimeGOOS
t.Cleanup(func() { runtimeGOOS = saved })
@@ -1300,7 +1294,7 @@ func TestInjectRuntimeConfigAvailableCommandsIsNeutral(t *testing.T) {
}
s := string(data)
// Available Commands lists all three input modes as fact.
// Available Commands lists all three input modes as available.
for _, want := range []string{
"--content \"...\"",
"--content-stdin",
@@ -1311,16 +1305,28 @@ func TestInjectRuntimeConfigAvailableCommandsIsNeutral(t *testing.T) {
}
}
// "MUST pipe via stdin" must NOT appear in any non-Codex
// provider's runtime config: it was the over-spread of
// the Codex-specific fix.
// The provider-agnostic guardrail must now reach non-Codex
// providers too: a dedicated Comment Formatting section that
// bans inline `--content` for agent-authored comments.
for _, want := range []string{
"## Comment Formatting",
"Never use inline `--content` for agent-authored comments",
} {
if !strings.Contains(s, want) {
t.Errorf("%s missing provider-agnostic comment guardrail %q\n---\n%s", configFile, want, s)
}
}
// The legacy over-broad mandate (#1795 / #1851) must NOT
// reappear — it is what broke Windows non-ASCII for every
// provider.
for _, banned := range []string{
"MUST pipe via stdin",
"Agent-authored comments should always pipe content via stdin",
"use `--description-stdin` and pipe a HEREDOC",
} {
if strings.Contains(s, banned) {
t.Errorf("%s carries over-spread Codex mandate %q for non-Codex provider %s\n---\n%s", configFile, banned, provider, s)
t.Errorf("%s reintroduces over-broad legacy mandate %q for provider %s\n---\n%s", configFile, banned, provider, s)
}
}
})
@@ -1328,49 +1334,63 @@ func TestInjectRuntimeConfigAvailableCommandsIsNeutral(t *testing.T) {
}
}
// TestInjectRuntimeConfigCodexLinuxEmphasizesStdin pins the
// Codex-Specific Comment Formatting section's "MUST stdin" mandate on
// non-Windows hosts. This is the MUL-1467 / #1795 / #1851 fix scoped
// back to where it belongs.
// TestInjectRuntimeConfigLinuxCommentFormattingEmphasizesStdin pins that the
// "## Comment Formatting" section emits the quoted-HEREDOC stdin mandate on
// non-Windows hosts for EVERY provider, not just Codex. Post-MUL-2904 the
// guardrail is provider-agnostic because the corruption is shell-driven; the
// quoted delimiter is what blocks backtick / `$()` substitution in the body.
//
// Not parallel: mutates the package-level runtimeGOOS.
func TestInjectRuntimeConfigCodexLinuxEmphasizesStdin(t *testing.T) {
func TestInjectRuntimeConfigLinuxCommentFormattingEmphasizesStdin(t *testing.T) {
saved := runtimeGOOS
t.Cleanup(func() { runtimeGOOS = saved })
runtimeGOOS = "linux"
dir := t.TempDir()
if _, err := InjectRuntimeConfig(dir, "codex", TaskContextForEnv{
IssueID: "issue-1",
TriggerCommentID: "comment-1",
}); err != nil {
t.Fatalf("InjectRuntimeConfig failed: %v", err)
}
data, err := os.ReadFile(filepath.Join(dir, "AGENTS.md"))
if err != nil {
t.Fatalf("read AGENTS.md: %v", err)
}
s := string(data)
for _, provider := range []string{"codex", "claude", "opencode"} {
t.Run(provider, func(t *testing.T) {
dir := t.TempDir()
if _, err := InjectRuntimeConfig(dir, provider, TaskContextForEnv{
IssueID: "issue-1",
TriggerCommentID: "comment-1",
}); err != nil {
t.Fatalf("InjectRuntimeConfig failed: %v", err)
}
fileName := "CLAUDE.md"
if provider != "claude" {
fileName = "AGENTS.md"
}
data, err := os.ReadFile(filepath.Join(dir, fileName))
if err != nil {
t.Fatalf("read %s: %v", fileName, err)
}
s := string(data)
for _, want := range []string{
"Codex-Specific Comment Formatting",
"always use `--content-stdin` with a HEREDOC",
"even for short single-line replies",
"Never use inline `--content` for agent-authored comments",
"Keep the same `--parent` value",
"do not rely on `\\n` escapes",
} {
if !strings.Contains(s, want) {
t.Errorf("AGENTS.md missing Codex multiline guidance %q\n---\n%s", want, s)
}
for _, want := range []string{
"## Comment Formatting",
"always use `--content-stdin` with a HEREDOC",
"even for short single-line replies",
"<<'COMMENT'",
"Never use inline `--content` for agent-authored comments",
"Keep the same `--parent` value",
"do not rely on `\\n` escapes",
} {
if !strings.Contains(s, want) {
t.Errorf("%s missing comment-formatting guidance %q\n---\n%s", fileName, want, s)
}
}
// The heading is no longer Codex-scoped.
if strings.Contains(s, "Codex-Specific Comment Formatting") {
t.Errorf("%s still carries the old Codex-scoped heading\n---\n%s", fileName, s)
}
})
}
}
// TestInjectRuntimeConfigCodexWindowsUsesContentFile pins that on Windows
// the Codex-Specific section directs the agent at `--content-file` instead
// of `--content-stdin`. PowerShell 5.1 / cmd.exe re-encode piped HEREDOC
// bytes through the active console codepage and silently drop non-ASCII
// as `?` before reaching `multica.exe` (#2198 / #2236 / #2376).
// the Comment Formatting section directs the agent at `--content-file`
// instead of `--content-stdin`. PowerShell 5.1 / cmd.exe re-encode piped
// HEREDOC bytes through the active console codepage and silently drop
// non-ASCII as `?` before reaching `multica.exe` (#2198 / #2236 / #2376).
//
// Not parallel: mutates the package-level runtimeGOOS.
func TestInjectRuntimeConfigCodexWindowsUsesContentFile(t *testing.T) {

View File

@@ -109,7 +109,9 @@ func BuildColdCommentsHint(issueID, triggerCommentID string) string {
// 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:
// The template is platform-aware but provider-agnostic — the failure it
// guards against lives at the shell layer, so it cannot be scoped to one
// provider (MUL-2904):
//
// - Windows + any provider → write a UTF-8 file, post with `--content-file`.
// This is the only path that survives Windows shells (PowerShell 5.1
@@ -117,15 +119,18 @@ func BuildColdCommentsHint(issueID, triggerCommentID string) string {
// 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 + Codexstdin/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.
// - Linux/macOS + any provider`--content-stdin` with a QUOTED HEREDOC
// (`<<'COMMENT'`). The quoted delimiter stops the shell from expanding
// backticks, `$()`, or `$VAR` inside the body. Inlining `--content "..."`
// instead lets the shell rewrite the body BEFORE the CLI receives it: a
// backtick-wrapped token becomes a failed command substitution that is
// silently deleted, the stored comment no longer matches what the model
// intended, and a model that notices the mismatch can retry forever
// (MUL-2904 / OKK-497). It also sidesteps Codex's habit of emitting
// literal `\n` escapes inside `--content` (MUL-1467).
//
// provider is retained for caller symmetry and future per-provider tweaks; the
// guardrail itself is intentionally identical across providers.
func BuildCommentReplyInstructions(provider, issueID, triggerCommentID string) string {
if triggerCommentID == "" {
return ""
@@ -145,37 +150,24 @@ func BuildCommentReplyInstructions(provider, issueID, triggerCommentID string) s
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.
// Linux/macOS, any provider: `--content-stdin` with a quoted HEREDOC. The
// quoted delimiter (`<<'COMMENT'`) is what makes this safe — it stops the
// shell from running backtick / `$()` substitution or `$VAR` expansion on
// the body. Inlining `--content "..."` is what triggered the MUL-2904
// duplicate-comment loop, so it is banned for every provider here, not just
// 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`; the shell rewrites unescaped backticks, `$()`, `$VAR`, or quotes in the body before the CLI receives them, and it is easy to lose formatting or compress a structured reply into one line.\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",
" 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,
)
}

View File

@@ -42,13 +42,14 @@ func TestBuildCommentReplyInstructionsCodexLinux(t *testing.T) {
}
}
// TestBuildCommentReplyInstructionsNonCodexLinux pins that every non-Codex
// provider on Linux/macOS gets the lightweight pre-#1795 inline template.
// The "MUST stdin" mandate was originally a Codex-specific fix that
// #1795 / #1851 accidentally spread to every provider, breaking Windows
// non-ASCII for all of them (#2198 / #2236 / #2376). Non-Codex providers
// handle inline escaping correctly and the CLI server-decodes `\n` etc.,
// so the inline template works on every non-Windows platform.
// TestBuildCommentReplyInstructionsNonCodexLinux pins the MUL-2904 regression:
// EVERY provider on Linux/macOS — not just Codex — gets the quoted-HEREDOC
// `--content-stdin` template and is steered away from inline `--content "..."`.
// The duplicate-comment loop on OKK-497 happened because an agent inlined a
// backtick-wrapped table name into `--content`; the shell ran it as a command
// substitution, silently deleted it, the stored comment no longer matched the
// model's intent, and the model retried forever. The corruption is shell-driven,
// so the guardrail cannot be scoped to one provider.
//
// Not parallel: mutates the package-level runtimeGOOS.
func TestBuildCommentReplyInstructionsNonCodexLinux(t *testing.T) {
@@ -66,7 +67,8 @@ func TestBuildCommentReplyInstructionsNonCodexLinux(t *testing.T) {
got := BuildCommentReplyInstructions(provider, issueID, triggerID)
for _, want := range []string{
"multica issue comment add " + issueID + " --parent " + triggerID + " --content \"...\"",
"cat <<'COMMENT' | multica issue comment add " + issueID + " --parent " + triggerID + " --content-stdin",
"Always use `--content-stdin`",
"do NOT reuse --parent values from previous turns",
"If you decide to reply",
} {
@@ -75,18 +77,11 @@ func TestBuildCommentReplyInstructionsNonCodexLinux(t *testing.T) {
}
}
// Non-Codex / non-Windows providers must NOT receive the
// Codex-specific "MUST stdin" mandate or its HEREDOC
// template — that was the over-spread of #1795 / #1851.
for _, banned := range []string{
"Always use `--content-stdin`",
"<<'COMMENT'",
"--parent " + triggerID + " --content-stdin",
"--parent " + triggerID + " --content-file",
} {
if strings.Contains(got, banned) {
t.Errorf("%s reply instructions still steers at codex template: %q\n---\n%s", name, banned, got)
}
// The regression itself: agent-authored comments must never be
// steered at inline `--content "..."`, which the shell can
// rewrite (backticks / `$()` / quotes) before the CLI sees it.
if strings.Contains(got, "--content \"...\"") {
t.Errorf("%s reply instructions still offers the inline --content form\n---\n%s", name, got)
}
})
}
@@ -249,3 +244,65 @@ func TestInjectRuntimeConfigWindowsCommentTriggerHasNoStdin(t *testing.T) {
})
}
}
// TestInjectRuntimeConfigWindowsAssignmentBriefStaysFileOnly pins the PR #3654
// review fix: on Windows, the ASSIGNMENT-triggered brief must never *recommend*
// `--content-stdin`. Unlike the comment-trigger path, the assignment workflow
// has no BuildCommentReplyInstructions override, so an agent that follows the
// "post your final results" step literally would pipe its final comment through
// PowerShell and drop non-ASCII bytes (#2198 / #2236 / #2376). The OS-aware
// ## Comment Formatting section (file-only on Windows) is the single source of
// truth; the Available Commands entry and step 6 must defer to it, not re-offer
// stdin. The flag synopsis may still *list* `--content-stdin` as available.
//
// Not parallel: mutates the package-level runtimeGOOS.
func TestInjectRuntimeConfigWindowsAssignmentBriefStaysFileOnly(t *testing.T) {
saved := runtimeGOOS
t.Cleanup(func() { runtimeGOOS = saved })
runtimeGOOS = "windows"
// Assignment-triggered: IssueID set, no TriggerCommentID.
ctx := TaskContextForEnv{IssueID: "issue-1"}
for _, provider := range []string{"claude", "codex", "opencode"} {
t.Run(provider, func(t *testing.T) {
dir := t.TempDir()
if _, err := InjectRuntimeConfig(dir, provider, ctx); err != nil {
t.Fatalf("InjectRuntimeConfig failed: %v", err)
}
fileName := "CLAUDE.md"
if provider != "claude" {
fileName = "AGENTS.md"
}
data, err := os.ReadFile(filepath.Join(dir, fileName))
if err != nil {
t.Fatalf("read %s: %v", fileName, err)
}
s := string(data)
// The Windows Comment Formatting section is file-only.
for _, want := range []string{
"## Comment Formatting",
"On Windows, **always write the comment body to a UTF-8 file",
"do NOT pipe via `--content-stdin`",
} {
if !strings.Contains(s, want) {
t.Errorf("%s missing Windows file-only guidance %q\n---\n%s", fileName, want, s)
}
}
// No prose may RECOMMEND stdin on Windows. The flag synopsis may
// still list `--content-stdin`; only the prescriptive "file or
// stdin" phrasings are banned.
for _, banned := range []string{
"or `--content-stdin`",
"using `--content-file` or `--content-stdin`",
"use `--content-file <path>` or `--content-stdin`",
} {
if strings.Contains(s, banned) {
t.Errorf("%s recommends stdin on Windows: %q\n---\n%s", fileName, banned, s)
}
}
})
}
}

View File

@@ -432,40 +432,47 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
b.WriteString("- `multica issue update <id> [--title X] [--description X | --description-stdin | --description-file <path>] [--priority X] [--status X] [--assignee X | --assignee-id <uuid>] [--parent <issue-id>] [--project <project-id>] [--due-date <RFC3339>]` — Update issue fields; use `--parent \"\"` to clear parent.\n")
b.WriteString("- `multica repo checkout <url> [--ref <branch-or-sha>]` — Check out a repository into the working directory (creates a git worktree with a dedicated branch; use `--ref` for review/QA on a specific branch, tag, or commit)\n")
b.WriteString("- `multica issue status <id> <status>` — Shortcut for `issue update --status` when you only need to flip status (todo, in_progress, in_review, done, blocked, backlog, cancelled)\n")
// Available Commands lists `multica issue comment add` neutrally —
// three input modes, pick what fits.
// The previous "MUST pipe via stdin" mandate (#1795 / #1851) was
// originally a Codex-specific fix for codex emitting literal `\n`
// escapes inside `--content "..."`, but it landed in this global
// section and ended up steering every provider at stdin, which then
// burned non-ASCII bytes on Windows where the agent's shell layer
// (typically PowerShell) re-encodes the pipe through an ASCII /
// non-UTF-8 codepage and drops non-representable bytes as `?`
// (issues #2198 / #2236 / #2376).
// Available Commands lists `multica issue comment add` with all three input
// modes, but the menu entry now actively steers agents away from inlining
// `--content` for agent-authored bodies. The prescriptive form-by-platform
// guidance lives in the "## Comment Formatting" section below.
//
// Strong "MUST" wording lives in the Codex-Specific section below
// where it actually belongs; non-Codex providers handle inline
// escaping correctly and can pick whichever flag suits their
// content. The `--content-file` line in the menu doubles as a
// pointer at the Windows-safe path.
b.WriteString("- `multica issue comment add <issue-id> [--content \"...\" | --content-stdin | --content-file <path>] [--parent <comment-id>] [--attachment <path>]` — Post a comment. Pick the input mode that preserves your content; run `multica issue comment add --help` for details.\n")
// Two distinct shell-layer hazards motivate this, and both bite an inlined
// body before the CLI ever runs:
// - Backtick / `$()` command substitution, `$VAR` expansion, and quote /
// newline mangling on Linux/macOS shells. A backtick-wrapped token in
// the body is executed and silently deleted, corrupting the stored
// comment and triggering a retry loop (MUL-2904 / OKK-497).
// - Non-ASCII bytes dropped as `?` on Windows, where the shell layer
// (typically PowerShell) re-encodes a stdin pipe through an ASCII /
// non-UTF-8 codepage (issues #2198 / #2236 / #2376) — which is why
// Windows uses `--content-file`, not stdin.
// Because the corruption is shell-driven, the guardrail is provider-agnostic.
b.WriteString("- `multica issue comment add <issue-id> [--content \"...\" | --content-stdin | --content-file <path>] [--parent <comment-id>] [--attachment <path>]` — Post a comment. For agent-authored bodies, do NOT inline `--content` — the shell can rewrite backticks, `$()`, quotes, or newlines before the CLI sees them; use the platform-correct non-inline mode shown in ## Comment Formatting below. Run `multica issue comment add --help` for details.\n")
b.WriteString("- `multica issue metadata list <issue-id> [--output json]` — List every metadata key pinned to an issue. Empty `{}` is normal.\n")
b.WriteString("- `multica issue metadata set <issue-id> --key <k> --value <v> [--type string|number|bool]` — Pin (or overwrite) a single metadata key. The CLI auto-infers JSON primitives, so URLs and plain text are stored as strings — pass `--type number` or `--type bool` only when the semantic type matters.\n")
b.WriteString("- `multica issue metadata delete <issue-id> --key <k>` — Remove a metadata key.\n\n")
b.WriteString("### Squad maintenance\n")
b.WriteString("- `multica squad member set-role <squad-id> --member-id <id> --member-type <agent|member> --role <role> [--output json]` — Change a squad member role in place; use this instead of remove+add when only the role changes.\n\n")
if provider == "codex" {
b.WriteString("## Codex-Specific Comment Formatting\n\n")
if runtimeGOOS == "windows" {
b.WriteString("Codex often follows the per-turn reply command literally. On Windows, **always write the comment body to a UTF-8 file with your file-write tool first, then post it with `--content-file <path>`** — do NOT pipe via `--content-stdin`. PowerShell 5.1's `$OutputEncoding` defaults to ASCIIEncoding when piping to a native command, silently dropping non-ASCII characters as `?` before they reach `multica.exe`. Never use inline `--content` for agent-authored comments. ")
b.WriteString("Keep the same `--parent` value from the trigger comment when replying. ")
b.WriteString("Do not compress a multi-paragraph answer into one line and do not rely on `\\n` escapes.\n\n")
} else {
b.WriteString("Codex often follows the per-turn reply command literally. For issue comments, always use `--content-stdin` with a HEREDOC, even for short single-line replies. ")
b.WriteString("Never use inline `--content` for agent-authored comments. Keep the same `--parent` value from the trigger comment when replying. ")
b.WriteString("Do not compress a multi-paragraph answer into one line and do not rely on `\\n` escapes.\n\n")
}
// Comment Formatting guardrail for ALL providers. The MUL-2904
// duplicate-comment loop happened because an agent inlined a backtick-wrapped
// table name into `--content "..."`; the shell ran it as a command
// substitution, silently deleted it, and the model retried forever. Because
// the corruption is shell-driven, not provider-driven, this directive is not
// scoped to Codex — every agent-authored comment must avoid inline
// `--content`. The platform split mirrors BuildCommentReplyInstructions:
// Windows → file (stdin pipes drop non-ASCII), Linux/macOS → quoted HEREDOC
// over stdin (the quoted delimiter blocks backtick / `$()` / `$VAR`).
b.WriteString("## Comment Formatting\n\n")
if runtimeGOOS == "windows" {
b.WriteString("On Windows, **always write the comment body to a UTF-8 file with your file-write tool first, then post it with `--content-file <path>`** — do NOT pipe via `--content-stdin`. PowerShell 5.1's `$OutputEncoding` defaults to ASCIIEncoding when piping to a native command, silently dropping non-ASCII characters as `?` before they reach `multica.exe`. Never use inline `--content` for agent-authored comments. ")
b.WriteString("Keep the same `--parent` value from the trigger comment when replying. ")
b.WriteString("Do not compress a multi-paragraph answer into one line and do not rely on `\\n` escapes.\n\n")
} else {
b.WriteString("For issue comments, always use `--content-stdin` with a HEREDOC, even for short single-line replies — use a quoted delimiter (`<<'COMMENT'`) so the shell does not expand backticks, `$()`, or `$VAR` inside the body. `--content-file <path>` works too. ")
b.WriteString("Never use inline `--content` for agent-authored comments: unescaped backticks, `$()`, `$VAR`, or quotes in the body are rewritten by the shell before the CLI receives them. Keep the same `--parent` value from the trigger comment when replying. ")
b.WriteString("Do not compress a multi-paragraph answer into one line and do not rely on `\\n` escapes.\n\n")
}
// Inject available repositories section.
@@ -606,9 +613,9 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
fmt.Fprintf(&b, "4. Run `multica issue status %s in_progress`\n", ctx.IssueID)
b.WriteString("5. Follow your Skills and Agent Identity to complete the task (write code, investigate, etc.)\n")
if ctx.IsSquadLeader {
fmt.Fprintf(&b, "6. **Post your final results as a comment** (unless your outcome is `no_action` — in that case, calling `multica squad activity %s no_action --reason \"...\"` alone is sufficient; you MUST exit without posting any comment. DO NOT post a comment announcing no_action or saying you are exiting silently): `multica issue comment add %s --content \"...\"`. Your results are only visible to the user if posted via this CLI call; text in your terminal or run logs is NOT delivered.\n", ctx.IssueID, ctx.IssueID)
fmt.Fprintf(&b, "6. **Post your final results as a comment** (unless your outcome is `no_action` — in that case, calling `multica squad activity %s no_action --reason \"...\"` alone is sufficient; you MUST exit without posting any comment. DO NOT post a comment announcing no_action or saying you are exiting silently): post it with `multica issue comment add %s` using the platform-correct non-inline mode from ## Comment Formatting (never inline `--content`). Your results are only visible to the user if posted via this CLI call; text in your terminal or run logs is NOT delivered.\n", ctx.IssueID, ctx.IssueID)
} else {
fmt.Fprintf(&b, "6. **Post your final results as a comment — this step is mandatory**: `multica issue comment add %s --content \"...\"`. Your results are only visible to the user if posted via this CLI call; text in your terminal or run logs is NOT delivered.\n", ctx.IssueID)
fmt.Fprintf(&b, "6. **Post your final results as a comment — this step is mandatory**: post it with `multica issue comment add %s` using the platform-correct non-inline mode from ## Comment Formatting (never inline `--content`). Your results are only visible to the user if posted via this CLI call; text in your terminal or run logs is NOT delivered.\n", ctx.IssueID)
}
b.WriteString("7. Before exiting: only if this run produced a fact that clears the high bar (important AND likely to be re-read by future runs on this same issue, e.g. a new PR URL or deploy URL), or you noticed a metadata key from entry that is now stale, pin or clear it via `multica issue metadata set`/`delete`. Most runs write nothing here — that is the expected outcome, not a gap. When in doubt, do not write. See the `## Issue Metadata` section above for the full bar.\n")
fmt.Fprintf(&b, "8. When done, run `multica issue status %s in_review`\n", ctx.IssueID)

View File

@@ -9,11 +9,11 @@ import (
// BuildPrompt constructs the task prompt for an agent CLI.
// Keep this minimal — detailed instructions live in CLAUDE.md / AGENTS.md
// injected by execenv.InjectRuntimeConfig. The provider string is used by
// comment-triggered tasks: Codex's per-turn reply template needs the
// platform-aware "stdin or file" variant, every other provider gets a
// lightweight inline template (or Windows file for any provider on
// Windows).
// injected by execenv.InjectRuntimeConfig. The provider string is threaded
// through to comment-triggered tasks' per-turn reply template; that template
// is provider-agnostic now (Linux/macOS → quoted-HEREDOC stdin, Windows
// file) because the shell-layer corruption it guards against is not specific
// to any one provider (MUL-2904).
func BuildPrompt(task Task, provider string) string {
if task.ChatSessionID != "" {
return buildChatPrompt(task)