mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-16 19:29:26 +02:00
MUL-3316: fix(execenv): switch agent prompt to --content-file to prevent heredoc flag swallowing (#4182) (#4191)
* fix(execenv): switch agent prompt to --content-file to prevent heredoc flag swallowing (#4182) The Linux/macOS reply template recommended --content-stdin with a quoted HEREDOC. That pattern is safe for the trivial single-flag comment-add case that BuildCommentReplyInstructions emits, but as soon as a model wraps extra flags around the heredoc on multica issue create / update — assignee, project — the bash heredoc/flag boundary is fragile in two ways the model cannot see: - A 'BODY \\' terminator with a trailing token is not recognised as the heredoc end, so flag lines after it are swallowed into the description (OXY-78: residual flag text leaked into the description, command exit 0). - A clean terminator turns the trailing '--assignee ...' line into a separate failing shell statement, while the create itself already exited 0 with no assignee (OXY-76: assignee silently dropped, no residual text). In both cases the CLI never receives the swallowed flags, the API request omits the fields, and the daemon has no visibility. The created issue lands with assignee_id: null / project_id: null. This commit: * Switches the Linux/macOS branch of BuildCommentReplyInstructions to --content-file with a 3-step recipe (write file, post, rm) so the body never reaches the shell and all flags live on one shell-token line. There is no heredoc boundary for flags to leak across. * Adds a parallel cleanup step (Remove-Item) to the Windows branch so the cross-platform template is one shape. * Rewrites the runtime_config.go ## Comment Formatting non-Windows section to mandate --content-file and explicitly ban --content-stdin HEREDOC for agent-authored comments, citing #4182. * Reorders the Available Commands menu lines for issue create / update / comment add to put --content-file / --description-file ahead of the stdin variant and add a per-line note pointing at #4182. * Updates and renames the affected tests (TestBuildCommentReplyInstructionsCodexLinux, TestBuildCommentReplyInstructionsNonCodexLinux, TestInjectRuntimeConfigLinuxCommentFormattingEmphasizesFile, TestInjectRuntimeConfigIssueMetadataCodexFormattingUnchanged) so the new file-first contract is pinned and the old HEREDOC mandate is in the banned-strings lists. This converges Linux/macOS with the long-standing Windows file-only path, so the cross-platform guidance is now one shape. It also strictly improves on the previous MUL-2904 guardrail by eliminating shell exposure of the body entirely (no body ever reaches the shell, so backtick / $() / $VAR substitution cannot corrupt it). Closes GitHub multica-ai/multica#4182. No CLI or backend changes — --content-file / --description-file already exist. Co-authored-by: multica-agent <github@multica.ai> * docs(prompt): correct stale BuildPrompt comment to file-first (#4182) --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: CC-Girl <cc-girl@multica.ai>
This commit is contained in:
@@ -1550,14 +1550,19 @@ func TestInjectRuntimeConfigCommentGuardrailIsProviderAgnostic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// TestInjectRuntimeConfigLinuxCommentFormattingEmphasizesFile pins that the
|
||||
// "## Comment Formatting" section emits the file-first mandate on non-Windows
|
||||
// hosts for EVERY provider (post-#4182). The previous quoted-HEREDOC
|
||||
// `--content-stdin` rule was kept for years to defend against backtick / `$()`
|
||||
// substitution in the body (MUL-2904), but the heredoc/flag boundary turned out
|
||||
// to be its own structural bug: when a model wrapped extra flags around the
|
||||
// heredoc on `multica issue create`, the flags were silently swallowed into
|
||||
// stdin (OXY-78, OXY-76). The file path defeats both classes — the body never
|
||||
// reaches the shell, and all flags live on one shell-token line — and converges
|
||||
// the Linux/macOS template with the long-standing Windows file-only path.
|
||||
//
|
||||
// Not parallel: mutates the package-level runtimeGOOS.
|
||||
func TestInjectRuntimeConfigLinuxCommentFormattingEmphasizesStdin(t *testing.T) {
|
||||
func TestInjectRuntimeConfigLinuxCommentFormattingEmphasizesFile(t *testing.T) {
|
||||
saved := runtimeGOOS
|
||||
t.Cleanup(func() { runtimeGOOS = saved })
|
||||
runtimeGOOS = "linux"
|
||||
@@ -1583,20 +1588,27 @@ func TestInjectRuntimeConfigLinuxCommentFormattingEmphasizesStdin(t *testing.T)
|
||||
|
||||
for _, want := range []string{
|
||||
"## Comment Formatting",
|
||||
"always use `--content-stdin` with a HEREDOC",
|
||||
"even for short single-line replies",
|
||||
"<<'COMMENT'",
|
||||
"always write the comment body to a UTF-8 file with your file-write tool first, then post it with `--content-file <path>`",
|
||||
"#4182",
|
||||
"Never use inline `--content` for agent-authored comments",
|
||||
"Keep the same `--parent` value",
|
||||
"rm ./reply.md",
|
||||
"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)
|
||||
|
||||
// The previous mandate (#1795 / #1851 / MUL-2904) must NOT remain.
|
||||
for _, banned := range []string{
|
||||
"always use `--content-stdin` with a HEREDOC, even for short single-line replies",
|
||||
"<<'COMMENT'",
|
||||
"Codex-Specific Comment Formatting",
|
||||
} {
|
||||
if strings.Contains(s, banned) {
|
||||
t.Errorf("%s still carries pre-#4182 stdin mandate %q\n---\n%s", fileName, banned, s)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4072,7 +4084,7 @@ func TestInjectRuntimeConfigIssueMetadataSectionScope(t *testing.T) {
|
||||
|
||||
// TestInjectRuntimeConfigIssueMetadataCodexFormattingUnchanged guarantees
|
||||
// that the new metadata wiring does not break the codex-specific comment
|
||||
// formatting rules (HEREDOC on Linux, --content-file on Windows). The
|
||||
// formatting rules (--content-file on every host, post-#4182). The
|
||||
// comment-formatting block lives below the metadata write step in the
|
||||
// workflow, so any reordering or accidental absorption of the codex
|
||||
// section would surface here.
|
||||
@@ -4082,7 +4094,7 @@ func TestInjectRuntimeConfigIssueMetadataCodexFormattingUnchanged(t *testing.T)
|
||||
oldGOOS := runtimeGOOS
|
||||
t.Cleanup(func() { runtimeGOOS = oldGOOS })
|
||||
|
||||
t.Run("linux_heredoc", func(t *testing.T) {
|
||||
t.Run("linux_content_file", func(t *testing.T) {
|
||||
runtimeGOOS = "linux"
|
||||
dir := t.TempDir()
|
||||
ctx := TaskContextForEnv{
|
||||
@@ -4105,9 +4117,9 @@ func TestInjectRuntimeConfigIssueMetadataCodexFormattingUnchanged(t *testing.T)
|
||||
if !strings.Contains(s, "multica issue metadata list issue-md-codex --output json") {
|
||||
t.Fatalf("metadata list step missing\n---\n%s", s)
|
||||
}
|
||||
// ...AND the codex-specific stdin-only rule is still emitted.
|
||||
if !strings.Contains(s, "always use `--content-stdin` with a HEREDOC") {
|
||||
t.Fatalf("codex linux HEREDOC rule missing\n---\n%s", s)
|
||||
// ...AND the post-#4182 file-first rule is still emitted on Linux.
|
||||
if !strings.Contains(s, "always write the comment body to a UTF-8 file with your file-write tool first, then post it with `--content-file <path>`") {
|
||||
t.Fatalf("codex linux --content-file rule missing\n---\n%s", s)
|
||||
}
|
||||
// ...AND the per-turn reply instruction still points at this
|
||||
// turn's trigger comment id.
|
||||
|
||||
@@ -119,28 +119,40 @@ func activeThreadID(triggerThreadID, 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 platform-aware but provider-agnostic — the failure it
|
||||
// The template is platform-agnostic AND provider-agnostic — the failure it
|
||||
// guards against lives at the shell layer, so it cannot be scoped to one
|
||||
// provider (MUL-2904):
|
||||
// provider or one OS:
|
||||
//
|
||||
// - 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 + 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).
|
||||
// - Inline `--content "..."` 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 lets Codex emit literal
|
||||
// `\n` escapes inside `--content` (MUL-1467).
|
||||
// - `--content-stdin` with a HEREDOC has TWO failure modes the model cannot
|
||||
// see:
|
||||
// 1. On Windows, PowerShell 5.1's `$OutputEncoding` defaults to
|
||||
// ASCIIEncoding when piping to native commands and drops non-ASCII as
|
||||
// `?` before the bytes reach `multica.exe` (#2198 Chinese, #2236
|
||||
// Chinese, #2376 Cyrillic).
|
||||
// 2. On any host, when the model emits a multi-flag command (e.g.
|
||||
// `multica issue create --title ... --assignee-id ... --project ...`)
|
||||
// the bash heredoc/flag boundary is fragile: a `BODY \` "terminator
|
||||
// with trailing token" is not recognised as the heredoc end, so flag
|
||||
// lines after it are swallowed into the description; or a clean
|
||||
// terminator turns the trailing `--assignee ...` line into a separate
|
||||
// shell statement that fails while the create already succeeded with
|
||||
// no assignee. Both paths exit 0 with silently dropped flags. Github
|
||||
// issue #4182 documents two confirmed cases (OXY-78, OXY-76).
|
||||
//
|
||||
// The single safe path is therefore: write the body to a UTF-8 file with
|
||||
// the file-write tool, post with `--content-file`, then remove the file.
|
||||
// All flags live on one shell-token line; the body never touches the shell;
|
||||
// no heredoc boundary exists for flags to leak across. This converges with
|
||||
// the long-standing Windows path so the cross-platform template is one shape.
|
||||
//
|
||||
// provider is retained for caller symmetry and future per-provider tweaks; the
|
||||
// guardrail itself is intentionally identical across providers.
|
||||
// guardrail itself is intentionally identical across providers and hosts.
|
||||
func BuildCommentReplyInstructions(provider, issueID, triggerCommentID string) string {
|
||||
if triggerCommentID == "" {
|
||||
return ""
|
||||
@@ -154,30 +166,37 @@ func BuildCommentReplyInstructions(provider, issueID, triggerCommentID string) s
|
||||
"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"+
|
||||
" # 2. Post the comment:\n"+
|
||||
" multica issue comment add %s --parent %s --content-file ./reply.md\n"+
|
||||
" # 3. Remove the temp file so a later run does not pick up stale content:\n"+
|
||||
" Remove-Item ./reply.md\n\n"+
|
||||
"Do NOT write literal `\\n` escapes to simulate line breaks; the file preserves real newlines.\n",
|
||||
issueID, triggerCommentID,
|
||||
)
|
||||
}
|
||||
// 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.
|
||||
// Linux/macOS, any provider: `--content-file`. Switched from `--content-stdin` +
|
||||
// HEREDOC to converge with the Windows path and close GitHub #4182. The
|
||||
// HEREDOC pattern was safe for the trivial single-flag case, but as soon as
|
||||
// the model wrapped extra flags around the heredoc (assignee, project on
|
||||
// `issue create` / `issue update`) it became fragile to flag/heredoc
|
||||
// boundary mistakes — flags either got swallowed into the body or executed
|
||||
// as separate failing shell statements while the create succeeded with
|
||||
// nulls. The file path eliminates that class of error: all flags live on
|
||||
// one command line, the body never reaches the shell.
|
||||
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"+
|
||||
"Write the reply body to a UTF-8 file with your file-write tool first, then post it with `--content-file`. "+
|
||||
"Do NOT use inline `--content`; the shell rewrites unescaped backticks, `$()`, `$VAR`, or quotes in the body before the CLI receives them. "+
|
||||
"Do NOT use `--content-stdin` with a HEREDOC either — when extra flags (e.g. `--assignee`, `--project` on `multica issue create`) accompany the command, the bash heredoc/flag boundary is fragile and flags can be silently swallowed into the stdin stream while the command still exits 0 (see GitHub #4182, OXY-78 / OXY-76). "+
|
||||
"It is also easy to lose formatting or compress a structured reply into one line with inline forms.\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",
|
||||
" # 1. Write the reply body to a UTF-8 file (e.g. reply.md) with your file-write tool.\n"+
|
||||
" # 2. Post the comment:\n"+
|
||||
" multica issue comment add %s --parent %s --content-file ./reply.md\n"+
|
||||
" # 3. Remove the temp file so a later run does not pick up stale content:\n"+
|
||||
" rm ./reply.md\n\n"+
|
||||
"Do NOT write literal `\\n` escapes to simulate line breaks; the file preserves real newlines.\n",
|
||||
issueID, triggerCommentID,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,11 +7,15 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestBuildCommentReplyInstructionsCodexLinux pins that the strong
|
||||
// "MUST use --content-stdin + HEREDOC" mandate stays alive for Codex on
|
||||
// non-Windows hosts. Codex's habit of emitting literal `\n` inside
|
||||
// `--content "..."` is the original reason this mandate exists
|
||||
// (#1795 / #1851); on Linux/macOS stdin is the right answer.
|
||||
// TestBuildCommentReplyInstructionsCodexLinux pins that the Linux/macOS
|
||||
// reply template now mandates `--content-file` (post-#4182). The previous
|
||||
// `--content-stdin` + HEREDOC mandate (#1795 / #1851 / MUL-2904) was kept
|
||||
// for years to defend against backtick / `$()` substitution in the body,
|
||||
// but the heredoc/flag boundary turned out to be fragile in its own right:
|
||||
// when a model wrapped extra flags around the heredoc on `multica issue
|
||||
// create`, the flags got swallowed into stdin and silently dropped (OXY-78,
|
||||
// OXY-76). The file path defeats both classes — the body never reaches the
|
||||
// shell, and all flags live on one shell-token line.
|
||||
//
|
||||
// Not parallel: mutates the package-level runtimeGOOS.
|
||||
func TestBuildCommentReplyInstructionsCodexLinux(t *testing.T) {
|
||||
@@ -25,10 +29,11 @@ func TestBuildCommentReplyInstructionsCodexLinux(t *testing.T) {
|
||||
got := BuildCommentReplyInstructions("codex", issueID, triggerID)
|
||||
|
||||
for _, want := range []string{
|
||||
"multica issue comment add " + issueID + " --parent " + triggerID + " --content-stdin",
|
||||
"Always use `--content-stdin`",
|
||||
"even when the reply is a single line",
|
||||
"<<'COMMENT'",
|
||||
"multica issue comment add " + issueID + " --parent " + triggerID + " --content-file ./reply.md",
|
||||
"Write the reply body to a UTF-8 file",
|
||||
"`--content-file`",
|
||||
"#4182",
|
||||
"rm ./reply.md",
|
||||
"Do NOT write literal `\\n` escapes to simulate line breaks",
|
||||
"do NOT reuse --parent values from previous turns",
|
||||
} {
|
||||
@@ -37,19 +42,32 @@ func TestBuildCommentReplyInstructionsCodexLinux(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(got, "--content \"...\"") {
|
||||
t.Fatalf("codex reply instructions should not offer inline --content form\n---\n%s", got)
|
||||
for _, banned := range []string{
|
||||
"--content \"...\"",
|
||||
"<<'COMMENT'",
|
||||
"cat <<",
|
||||
"--parent " + triggerID + " --content-stdin",
|
||||
} {
|
||||
if strings.Contains(got, banned) {
|
||||
t.Fatalf("codex/linux reply instructions should not contain %q\n---\n%s", banned, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// TestBuildCommentReplyInstructionsNonCodexLinux pins that EVERY provider on
|
||||
// Linux/macOS — not just Codex — gets the `--content-file` template. Two
|
||||
// shell-driven failure classes motivate the uniform file path:
|
||||
// - MUL-2904 / OKK-497: 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.
|
||||
// - GitHub #4182 (OXY-78 / OXY-76): an agent wrapped extra flags around an
|
||||
// `--content-stdin` HEREDOC; the bash heredoc/flag boundary swallowed
|
||||
// `--assignee` / `--project` into stdin or dropped them as failed
|
||||
// standalone shell statements, while the create still exited 0 with nulls.
|
||||
//
|
||||
// Both classes are shell-driven, so the guardrail is uniform across providers
|
||||
// and across hosts.
|
||||
//
|
||||
// Not parallel: mutates the package-level runtimeGOOS.
|
||||
func TestBuildCommentReplyInstructionsNonCodexLinux(t *testing.T) {
|
||||
@@ -67,8 +85,11 @@ func TestBuildCommentReplyInstructionsNonCodexLinux(t *testing.T) {
|
||||
got := BuildCommentReplyInstructions(provider, issueID, triggerID)
|
||||
|
||||
for _, want := range []string{
|
||||
"cat <<'COMMENT' | multica issue comment add " + issueID + " --parent " + triggerID + " --content-stdin",
|
||||
"Always use `--content-stdin`",
|
||||
"multica issue comment add " + issueID + " --parent " + triggerID + " --content-file ./reply.md",
|
||||
"Write the reply body to a UTF-8 file",
|
||||
"`--content-file`",
|
||||
"#4182",
|
||||
"rm ./reply.md",
|
||||
"do NOT reuse --parent values from previous turns",
|
||||
"If you decide to reply",
|
||||
} {
|
||||
@@ -77,11 +98,18 @@ func TestBuildCommentReplyInstructionsNonCodexLinux(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
// The two regressions: agent-authored comments must never be
|
||||
// steered at inline `--content "..."` (MUL-2904) and never at
|
||||
// `--content-stdin` HEREDOC on multi-flag commands (#4182).
|
||||
for _, banned := range []string{
|
||||
"--content \"...\"",
|
||||
"<<'COMMENT'",
|
||||
"cat <<",
|
||||
"--parent " + triggerID + " --content-stdin",
|
||||
} {
|
||||
if strings.Contains(got, banned) {
|
||||
t.Errorf("%s reply instructions still contains %q\n---\n%s", name, banned, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -473,8 +473,8 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
||||
b.WriteString("### Core\n")
|
||||
b.WriteString("- `multica issue get <id> --output json` — Get full issue details.\n")
|
||||
b.WriteString("- `multica issue comment list <issue-id> [--thread <comment-id> [--tail N] | --recent N] [--before <ts> --before-id <uuid>] [--since <RFC3339>] --output json` — List comments on an issue. Default returns the full flat timeline (server cap 2000). On busy issues prefer the thread-aware reads: `--thread <comment-id>` returns one conversation (root + every reply); `--thread <id> --tail N` caps replies to the N most recent (root is always included, even at `--tail 0`); `--recent N` returns the N most recently active threads. `--before` / `--before-id` walks older replies under `--thread --tail` (stderr label: `Next reply cursor`) or older threads under `--recent` (stderr label: `Next thread cursor`). `--since` is for incremental polling and may combine with `--thread` (with or without `--tail`) or `--recent`.\n")
|
||||
b.WriteString("- `multica issue create --title \"...\" [--description \"...\" | --description-stdin | --description-file <path>] [--priority X] [--status X] [--assignee X | --assignee-id <uuid>] [--parent <issue-id>] [--project <project-id>] [--due-date <RFC3339>] [--attachment <path>]` — Create a new issue; `--attachment` may be repeated.\n")
|
||||
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 issue create --title \"...\" [--description \"...\" | --description-file <path> | --description-stdin] [--priority X] [--status X] [--assignee X | --assignee-id <uuid>] [--parent <issue-id>] [--project <project-id>] [--due-date <RFC3339>] [--attachment <path>]` — Create a new issue; `--attachment` may be repeated. For agent-authored long descriptions, prefer `--description-file <path>` — flags after a HEREDOC terminator can be silently swallowed (#4182).\n")
|
||||
b.WriteString("- `multica issue update <id> [--title X] [--description X | --description-file <path> | --description-stdin] [--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. For agent-authored long descriptions, prefer `--description-file <path>` over stdin (#4182).\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` with all three input
|
||||
@@ -493,30 +493,49 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
||||
// 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 comment add <issue-id> [--content \"...\" | --content-file <path> | --content-stdin] [--parent <comment-id>] [--attachment <path>]` — Post a comment. For agent-authored bodies, **write the body to a UTF-8 file and use `--content-file <path>`** — do NOT inline `--content` (the shell rewrites backticks, `$()`, quotes, or newlines before the CLI sees them) and do NOT use `--content-stdin` with a HEREDOC (extra flags around the heredoc can be silently swallowed, #4182). See ## 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")
|
||||
|
||||
// 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`).
|
||||
// Comment Formatting guardrail for ALL providers and ALL hosts. Two
|
||||
// shell-layer hazards motivate a single, uniform "write a file, post with
|
||||
// `--content-file`" rule rather than a per-OS split:
|
||||
//
|
||||
// 1. Inline `--content "..."`: backtick / `$()` substitution, `$VAR`
|
||||
// expansion, and quote / newline mangling on Linux/macOS. 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).
|
||||
// 2. `--content-stdin` with a HEREDOC: TWO failure modes the model cannot
|
||||
// see — (a) on Windows, PowerShell 5.1's `$OutputEncoding` defaults to
|
||||
// ASCIIEncoding when piping to a native command and silently drops
|
||||
// non-ASCII as `?` before the bytes reach `multica.exe` (#2198 /
|
||||
// #2236 / #2376); (b) on any host, when the model emits a multi-flag
|
||||
// command (`multica issue create --title ... --assignee-id ...
|
||||
// --project ...`), the bash heredoc/flag boundary is fragile — a
|
||||
// `BODY \` "terminator with trailing token" is not recognised as the
|
||||
// heredoc end (flag lines after it leak into the description), or a
|
||||
// clean terminator turns the trailing `--assignee ...` line into a
|
||||
// separate failing shell statement while the create already exited 0
|
||||
// with no assignee (GitHub #4182, OXY-78 / OXY-76).
|
||||
//
|
||||
// `--content-file` defeats both classes: all flags live on one shell-token
|
||||
// line, the body never reaches the shell, no heredoc boundary exists for
|
||||
// flags to leak across. This is identical to the long-standing Windows
|
||||
// path, so the cross-platform guidance is now one shape.
|
||||
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("After posting, remove the temp file with `Remove-Item ./reply.md` (or your chosen path) so a later run does not pick up stale content. ")
|
||||
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("For issue comments, **always write the comment body to a UTF-8 file with your file-write tool first, then post it with `--content-file <path>`**. Never use inline `--content` for agent-authored comments — the shell rewrites backticks, `$()`, `$VAR`, or quotes in the body before the CLI receives them (MUL-2904). Do NOT use `--content-stdin` with a HEREDOC either: when extra flags accompany the command (e.g. `--assignee`, `--project` on `multica issue create`), the bash heredoc/flag boundary is fragile and flags can be silently swallowed into the stdin stream while the command still exits 0 (GitHub #4182). ")
|
||||
b.WriteString("Keep the same `--parent` value from the trigger comment when replying. ")
|
||||
b.WriteString("After posting, remove the temp file with `rm ./reply.md` (or your chosen path) so a later run does not pick up stale content. ")
|
||||
b.WriteString("Do not compress a multi-paragraph answer into one line and do not rely on `\\n` escapes.\n\n")
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
// Keep this minimal — detailed instructions live in CLAUDE.md / AGENTS.md
|
||||
// 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).
|
||||
// is provider-agnostic AND host-agnostic now (every OS → write a UTF-8 file,
|
||||
// post with `--content-file`) because the shell-layer corruption it guards
|
||||
// against is not specific to any one provider or host (MUL-2904, #4182).
|
||||
func BuildPrompt(task Task, provider string) string {
|
||||
if task.ChatSessionID != "" {
|
||||
return buildChatPrompt(task)
|
||||
|
||||
Reference in New Issue
Block a user