mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-28 18:09:14 +02:00
Compare commits
2 Commits
codex/agen
...
agent/j/d0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce0a33e06b | ||
|
|
43220252f8 |
@@ -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) {
|
||||
|
||||
@@ -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 + 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.
|
||||
// - 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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user