Skip to content

fix(agent/cursor): route Windows launcher through PowerShell -File to… - #1709

Merged
Bohan-J merged 1 commit into
multica-ai:mainfrom
HuChundong:fix/cursor-windows-long-prompt
Apr 29, 2026
Merged

fix(agent/cursor): route Windows launcher through PowerShell -File to…#1709
Bohan-J merged 1 commit into
multica-ai:mainfrom
HuChundong:fix/cursor-windows-long-prompt

Conversation

@HuChundong

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes Multica-spawned cursor-agent always exiting 1 on Windows when the
prompt contains newlines (i.e. nearly every real task).

Root cause. The official cursor-agent installer ships
cursor-agent.cmd, whose body is

powershell ... -File cursor-agent.ps1 %*

CreateProcess for a .cmd file routes through cmd.exe, and %* in a
batch file is expanded by re-tokenising the original command line.
That re-tokenisation mangles any argument containing newlines or other
whitespace — most notably a long, multi-line -p <prompt>. The agent
ends up seeing a truncated prompt and either reports Workspace Trust Required or exits with status 1 before producing a session id.

Manually pasting an equivalent command into PowerShell works because
PowerShell tokenises the user input itself instead of recovering tokens
from the command-line string — exactly what we want Go's os/exec to do
for us.

Fix. When LookPath resolves cursor-agent to a .cmd / .bat
launcher and a sibling cursor-agent.ps1 exists, the daemon now invokes
PowerShell directly:

powershell -NoProfile -ExecutionPolicy Bypass -File <ps1> <args...>

This is exactly what the .cmd does internally; we simply skip the
cmd.exe re-tokenisation step. Each argv is passed as a discrete
token, so multi-line prompts and other whitespace-heavy values survive
intact. macOS / Linux behaviour is unchanged (the path is gated by
//go:build windows); the official cursor-agent launch chain is
preserved (no node.exe shortcut, no prompt mutation, no extra flags).

PowerShell host resolution prefers pwsh.exe (PS 7) on PATH, then
powershell.exe on PATH, then falls back to
%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe. The
lookup is a package-level variable so it can be stubbed in tests.

Related Issue

Closes #1297

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Refactor / code improvement (no behavior change)
  • Documentation update
  • Tests (adding or improving test coverage)
  • CI / infrastructure

Changes Made

  • server/pkg/agent/cursor.go — call chooseCursorInvocation(...) to
    pick argv[0] and the full argv just before exec.CommandContext.
  • server/pkg/agent/cursor_invocation.go — shared
    chooseCursorInvocation entry point with a doc-comment that explains
    the cmd %* trap, so future maintainers don't "optimise" the rewrite
    away.
  • server/pkg/agent/cursor_invocation_other.go (//go:build !windows)
    — no-op passthrough, keeps non-Windows builds free of any
    Windows-only dependency.
  • server/pkg/agent/cursor_invocation_windows.go (//go:build windows)
    — detect .cmd/.bat + sibling cursor-agent.ps1, build the
    PowerShell argv, log a structured Info line on activation. Exports
    powerShellLookup as a package variable for test injection.
  • server/pkg/agent/cursor_invocation_test.go — passthrough behaviour
    for non-launcher targets (runs on every platform).
  • server/pkg/agent/cursor_invocation_windows_test.go
    (//go:build windows) — four tests covering: successful rewrite with
    a multi-line prompt, .exe direct launch (skip), missing .ps1
    (skip), missing PowerShell host (skip). All paths use a stubbed
    powerShellLookup so the suite never spawns real PowerShell.
  • server/pkg/agent/exec_fixture_windows_test.go — restored Windows
    test helper writeTestExecutable. Required for the pkg/agent test
    binary to compile on Windows (consumed by existing
    claude_test.go / codex_test.go / kimi_test.go); without it the
    whole test package can't build on Windows and the new cursor tests
    can't run either.

How to Test

  1. Unit tests (any platform):

    cd server
    go test ./pkg/agent/ -run "Cursor" -count=1 -v
    

    Expected: all TestChooseCursorInvocation_* and
    TestPlatformCursorInvocation_* cases pass on Windows; the
    passthrough case also passes on macOS / Linux.

  2. End-to-end on Windows (reproduces the issue from [Bug]: [Windows] Multica-spawned cursor-agent always exits 1; manual run succeeds #1297):

    • Stop the existing daemon.

    • Build & start the daemon from this branch (make daemon / your
      usual launch).

    • Assign a Cursor agent task with a real, multi-line prompt
      (anything ≥ a few hundred characters with newlines is enough; the
      reported failure trigger is a typical Multica system prompt).

    • Before the fix: task fails almost immediately with
      cursor-agent exited with error: exit status 1 and the stderr
      tail mentions Workspace Trust Required.

    • After the fix: task runs to completion. The daemon log shows a
      new line:

      cursor-agent: routing through powershell -File to preserve argv tokens
      powershell=...\powershell.exe ps1=...\cursor-agent.ps1
      original=...\cursor-agent.cmd
      

      and the subsequent agent command line lists argv[0] as
      PowerShell with -NoProfile -ExecutionPolicy Bypass -File ... cursor-agent.ps1 followed by the original cursor-agent argv.

  3. Sanity on non-Windows: behaviour is unchanged (the new code path
    is gated by //go:build windows); go test ./pkg/agent/... should
    pass exactly as on main.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots (N/A — daemon-only)
  • I have updated relevant documentation to reflect my changes (doc-comments in new files explain the cmd %* trap)
  • I have considered and documented any risks above
  • I will address all reviewer comments before requesting merge

Risks considered

  • PowerShell ExecutionPolicy. We pass -ExecutionPolicy Bypass, the
    same flag the official cursor-agent.cmd already uses, so we don't
    weaken policy beyond what the upstream installer does.
  • PS profile side effects. -NoProfile matches the .cmd,
    guaranteeing identical environment.
  • Locked-down hosts without PowerShell. Both pwsh.exe and
    powershell.exe are missing → chooseCursorInvocation falls back to
    the original .cmd invocation (the test
    TestPlatformCursorInvocation_SkipsWhenPowerShellMissing pins this
    behaviour). Worst case is the pre-existing behaviour, never worse.
  • cursor-agent shipped without .ps1. Same fallback —
    TestPlatformCursorInvocation_SkipsWhenPS1Missing pins it.
  • Future installer changes the launcher layout. We only rewrite
    when we actually see a sibling .ps1; other layouts hit the
    passthrough and behave exactly as today.

AI Disclosure

AI tool used: Cursor

Prompt / approach:
Reproduced #1297 locally with a small go run-able harness that drives
backend.Execute with both a short prompt and an embedded production
long prompt, confirming the failure mode and bisecting the cause to the
.cmd %* re-tokenisation. Iterated on the fix in chat: started from a
prompt-normalisation hypothesis (rejected — masks the root cause and
silently rewrites user content), then moved to invoking PowerShell
directly with -File so we stay on the official launch chain while
sidestepping cmd.exe. Refactored the platform-specific bits behind a
build-tag boundary, exposed the PowerShell lookup as an injectable
variable, and wrote a Windows-only unit suite that exercises every
fallback branch without spawning real PowerShell. Final state validated
with go vet ./..., go build ./..., and go test ./pkg/agent/... on
Windows.

Screenshots (optional)

N/A — daemon-only change.

@vercel

vercel Bot commented Apr 26, 2026

Copy link
Copy Markdown

@HuChundong is attempting to deploy a commit to the IndexLabs Team on Vercel.

A member of the Team first needs to authorize it.

@Bohan-J

Bohan-J commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator

Hi @HuChundong, thanks for tracking down the root cause here — the analysis of the cmd %* re-tokenisation on multi-line prompts is exactly right, and the powershell -File rewrite is the right shape for the cursor-agent installer layout (no native .exe, only .ps1).

One thing before this can merge: the PR is currently CONFLICTING against main. The conflict is just server/pkg/agent/exec_fixture_windows_test.go#1718 (the opencode fix) merged after you opened this and added a functionally identical version of that file. Could you rebase on top of latest main and drop your copy of exec_fixture_windows_test.go? The version already on main covers what your tests need.

The rest of the diff (cursor.go + the four new cursor_invocation*.go files) should rebase cleanly. Once it's green I'll take another pass and approve.

Also worth noting: the issue this PR closes (#1297) was closed prematurely by the reporter — the actual bug is still real, and another user (@brholtkamp) confirmed it on 2026-04-22. Suggest reopening #1297 so the GitHub auto-close on merge lands meaningfully and so users still hitting it have a single thread to subscribe to.

@HuChundong
HuChundong force-pushed the fix/cursor-windows-long-prompt branch from 9ae686f to 8ba9bf0 Compare April 27, 2026 09:41
@HuChundong

Copy link
Copy Markdown
Contributor Author

fixed, hope we can merge this. ^_^

@Bohan-J

Bohan-J commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator

Hi @HuChundong, thanks for the quick turnaround on the rebase + dropping exec_fixture_windows_test.go — the cursor fix itself looks great in 8ba9bf0c.

Unfortunately the rebase appears to have squashed against an outdated base, and the PR now silently reverts three unrelated commits already on main. git diff origin/main..HEAD shows 157 lines of pure deletions outside server/pkg/agent/cursor*:

File Reverts
.github/workflows/release.yml #1687 (ci(release): skip homebrew-tap publish on forks) — re-introduces the 401 on fork tag pushes
apps/web/features/landing/i18n/{en,zh}.ts #1745 (docs(changelog): publish v0.2.18 release notes) — deletes the entire v0.2.18 changelog entry
server/cmd/multica/cmd_daemon.go, cmd_daemon_unix.go, cmd_daemon_windows.go 4c81fbed fix(daemon/windows): break out of parent shell Job Object so daemon survives — re-breaks a recently-fixed Windows daemon bug

Could you re-rebase against the latest main? The cleanest path is probably:

git fetch origin
git reset --hard origin/main
git cherry-pick 8ba9bf0c
# resolve any trivial conflicts (there shouldn't be any in cursor*.go)
git push --force-with-lease

After that, git diff origin/main..HEAD --stat should show only the six server/pkg/agent/cursor* files and nothing else. Once that's clean I'll re-review and approve.

@HuChundong
HuChundong force-pushed the fix/cursor-windows-long-prompt branch from 8ba9bf0 to 539823b Compare April 27, 2026 10:19
@HuChundong

Copy link
Copy Markdown
Contributor Author

should be ok now

@Bohan-J

Bohan-J commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator

@HuChundong sorry to keep going back and forth on this — the rebase actually went the other way this time. 539823b6 now reverts 8+ commits already on main (vs. 3 in the previous push). Diffstat is 21 files / +419 / -712, but only 6 of those files are the cursor fix; the rest are unintended deletions:

File What it reverts
packages/core/labels/mutations.ts #1746fix(labels): apply label attach optimistically
server/cmd/server/comment_trigger_integration_test.go (deleted), server/internal/handler/comment.go, server/internal/service/task.go #1747fix(comments): cancel triggered tasks when comment is deleted
server/pkg/agent/codex.go, server/pkg/agent/codex_test.go (deleted) #1730 + earlier Codex hardening
server/internal/daemon/{config,daemon,daemon_test,types}.go, server/cmd/multica/cmd_daemon.go Daemon GC / logging / window-suppression series
server/pkg/db/generated/agent.sql.go, server/pkg/db/queries/agent.sql #1476feat(server): orphan-task recovery

The cursor change itself is identical to last push and still looks good — the issue is purely the base your branch is on.

I think the cleanest reset is to drop the local branch entirely and re-create it from origin/main, then cherry-pick just your cursor commit:

git fetch origin
git checkout fix/cursor-windows-long-prompt
git reset --hard origin/main             # important: origin/main, not local main
git cherry-pick 539823b6                  # your current cursor commit
git diff origin/main..HEAD --stat         # should list ONLY the 6 server/pkg/agent/cursor*.go files
git push --force-with-lease

If git diff origin/main..HEAD --stat shows anything outside server/pkg/agent/cursor*.go, the rebase is still wrong and we shouldn't push. After that I'll re-review.

@HuChundong
HuChundong force-pushed the fix/cursor-windows-long-prompt branch from 539823b to 17d372a Compare April 27, 2026 10:47
@HuChundong

Copy link
Copy Markdown
Contributor Author

maybe i should use multica to create this PR,😂

@Bohan-J

Bohan-J commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator

@HuChundong much closer this time — 17d372ad is down to just 2 unintended files (vs. 21 last push). Last bit to clean up:

git diff origin/main..HEAD --stat still shows two files that aren't part of the cursor fix:

  • packages/ui/markdown/linkify.ts (-86) — the diff removes isEscaped / findMatchingBracket / findInlineLinkEnd
  • packages/views/editor/utils/preprocess-links.test.ts (-15)

These were added by #1761 (fix: preserve authored markdown links during linkify) on main. Letting this PR merge as-is would silently revert that fix.

I think the issue is that your local main is stale, so git rebase main rebases against the wrong tip every time. Could you try this exact sequence to bypass local main entirely?

git fetch origin
git checkout fix/cursor-windows-long-prompt
git reset --hard origin/main          # <- origin/main, NOT main
git cherry-pick 17d372ad               # your current cursor commit
git diff origin/main..HEAD --stat      # MUST show only 6 files, all under server/pkg/agent/cursor*
git push --force-with-lease

The check after the cherry-pick is the important one — if git diff origin/main..HEAD --stat lists anything outside server/pkg/agent/cursor*.go, we know the branch is still on a stale base and we shouldn't push yet. Once that diffstat is clean (just the 6 files) I'll re-review and approve immediately.

… preserve multi-line prompts

On Windows the official cursor-agent installer ships cursor-agent.cmd whose
body is `powershell ... -File cursor-agent.ps1 %*`. CreateProcess for a .cmd
file goes through cmd.exe, and `%*` in a batch file is expanded by
re-tokenising the original command line, which mangles arguments containing
newlines or other whitespace - most notably a long, multi-line `-p <prompt>`.
The agent then only sees a truncated prompt and fails with "Workspace Trust
Required" or exits 1 immediately.

When LookPath resolves cursor-agent to a .cmd/.bat launcher and a sibling
cursor-agent.ps1 exists, invoke PowerShell directly with `-File <ps1>` so
Go's os/exec passes each argv as a discrete token. This is exactly what the
.cmd does internally; we just skip the cmd.exe re-tokenisation step.
PowerShell host resolution prefers pwsh.exe (PS 7) on PATH, then
powershell.exe on PATH, and finally falls back to
%SystemRoot%\System32\WindowsPowerShell\v1.0.

Platform-specific code is split via build tags
(cursor_invocation_windows.go / cursor_invocation_other.go) so non-Windows
builds carry no Windows-only dependencies. The lookup is exposed as a
package variable to make the Windows path fully unit-testable without
spawning real PowerShell. Five unit tests cover: passthrough on non-launcher
targets, successful rewrite with a multi-line prompt, .exe direct launch
(skip), missing .ps1 (skip), and missing PowerShell host (skip).

The change leaves macOS / Linux behaviour entirely untouched and stays on
the official cursor-agent launch chain - no node.exe direct invocation, no
prompt mutation, no extra flags.

Closes multica-ai#1297

Made-with: Cursor
@HuChundong
HuChundong force-pushed the fix/cursor-windows-long-prompt branch from 17d372a to 3a01bc9 Compare April 29, 2026 03:18
@Bohan-J

Bohan-J commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator

Diffstat is clean now (just the 6 cursor files), CI is green, local go test ./pkg/agent/ -run Cursor passes. Thanks for sticking with the rebase, @HuChundong — solid root-cause analysis on the cmd %* re-tokenisation, and the powershell -File rewrite is exactly the right shape for the cursor-agent installer layout. Merging now.

@Bohan-J
Bohan-J merged commit 805071b into multica-ai:main Apr 29, 2026
2 checks passed
wingtonrbrito pushed a commit to wingtonrbrito/multica that referenced this pull request May 27, 2026
… preserve multi-line prompts (multica-ai#1709)

On Windows the official cursor-agent installer ships cursor-agent.cmd whose
body is `powershell ... -File cursor-agent.ps1 %*`. CreateProcess for a .cmd
file goes through cmd.exe, and `%*` in a batch file is expanded by
re-tokenising the original command line, which mangles arguments containing
newlines or other whitespace - most notably a long, multi-line `-p <prompt>`.
The agent then only sees a truncated prompt and fails with "Workspace Trust
Required" or exits 1 immediately.

When LookPath resolves cursor-agent to a .cmd/.bat launcher and a sibling
cursor-agent.ps1 exists, invoke PowerShell directly with `-File <ps1>` so
Go's os/exec passes each argv as a discrete token. This is exactly what the
.cmd does internally; we just skip the cmd.exe re-tokenisation step.
PowerShell host resolution prefers pwsh.exe (PS 7) on PATH, then
powershell.exe on PATH, and finally falls back to
%SystemRoot%\System32\WindowsPowerShell\v1.0.

Platform-specific code is split via build tags
(cursor_invocation_windows.go / cursor_invocation_other.go) so non-Windows
builds carry no Windows-only dependencies. The lookup is exposed as a
package variable to make the Windows path fully unit-testable without
spawning real PowerShell. Five unit tests cover: passthrough on non-launcher
targets, successful rewrite with a multi-line prompt, .exe direct launch
(skip), missing .ps1 (skip), and missing PowerShell host (skip).

The change leaves macOS / Linux behaviour entirely untouched and stays on
the official cursor-agent launch chain - no node.exe direct invocation, no
prompt mutation, no extra flags.

Closes multica-ai#1297

Made-with: Cursor
xiaoyue26 pushed a commit to xiaoyue26/multica that referenced this pull request May 30, 2026
… preserve multi-line prompts (multica-ai#1709)

On Windows the official cursor-agent installer ships cursor-agent.cmd whose
body is `powershell ... -File cursor-agent.ps1 %*`. CreateProcess for a .cmd
file goes through cmd.exe, and `%*` in a batch file is expanded by
re-tokenising the original command line, which mangles arguments containing
newlines or other whitespace - most notably a long, multi-line `-p <prompt>`.
The agent then only sees a truncated prompt and fails with "Workspace Trust
Required" or exits 1 immediately.

When LookPath resolves cursor-agent to a .cmd/.bat launcher and a sibling
cursor-agent.ps1 exists, invoke PowerShell directly with `-File <ps1>` so
Go's os/exec passes each argv as a discrete token. This is exactly what the
.cmd does internally; we just skip the cmd.exe re-tokenisation step.
PowerShell host resolution prefers pwsh.exe (PS 7) on PATH, then
powershell.exe on PATH, and finally falls back to
%SystemRoot%\System32\WindowsPowerShell\v1.0.

Platform-specific code is split via build tags
(cursor_invocation_windows.go / cursor_invocation_other.go) so non-Windows
builds carry no Windows-only dependencies. The lookup is exposed as a
package variable to make the Windows path fully unit-testable without
spawning real PowerShell. Five unit tests cover: passthrough on non-launcher
targets, successful rewrite with a multi-line prompt, .exe direct launch
(skip), missing .ps1 (skip), and missing PowerShell host (skip).

The change leaves macOS / Linux behaviour entirely untouched and stays on
the official cursor-agent launch chain - no node.exe direct invocation, no
prompt mutation, no extra flags.

Closes multica-ai#1297

Made-with: Cursor
Bohan-J pushed a commit that referenced this pull request Jul 21, 2026
…e-tokenised (MUL-4992)

On Windows a Cursor task whose prompt contains CLI-like flags failed in ~2s
with `error: unknown option '-X'` and no agent output (#5649). A pasted build
log such as

    go build -ldflags "-X main.version=foo" -o bin/server ./cmd/server

was enough to kill the run.

buildCursorArgs put the whole prompt in argv as the positional after -p.
The official Windows launcher chain ends in `& node.exe index.js $args`
inside cursor-agent.ps1, where PowerShell re-serialises $args onto node's
command line. Under Windows PowerShell 5.1 and pwsh <= 7.2 (Legacy native
argument passing) an argument holding embedded double quotes is not
re-escaped, so the quoted region closes early, node's argv parser re-splits
at the interior spaces, and `-X` reaches commander.js as a standalone flag.

#1709 removed the cmd.exe `%*` re-tokenisation but stopped at the
Go -> PowerShell boundary, one hop before this. Its Windows tests only
compare the argv slice Go builds and never execute a shim, which is why the
gap stayed invisible.

Fix: keep the prompt off every command line. cursor-agent's -p is a boolean
print-mode switch and the prompt is positional; with no positional prompt and
a non-TTY stdin the CLI reads stdin to EOF and uses that as the prompt. So
drop the prompt from argv on all platforms and write it to stdin, leaving
only fixed, content-free flags in argv. No shell or launcher on any platform
can re-tokenise what is not on a command line.

The write runs in its own goroutine: a prompt larger than the pipe buffer
(~64 KiB) blocks mid-write until the child drains it, and the child cannot
drain while nothing reads its stdout. Closing stdin signals end-of-prompt, so
it is closed on both success and error paths, and on cancellation to release
a blocked write. Write failures surface in the result diagnostic, ranked
below explicit agent errors so an early child exit (bad auth, bad flag) is
not masked by the resulting EPIPE.

Tests: a prompt carrying the exact `-ldflags "-X ..."` shape must arrive
byte-for-byte on stdin and appear nowhere in argv; a 512 KiB prompt must not
deadlock; the prompt is written verbatim (the CLI trims it, we do not). Both
new unix tests fail against the pre-fix code. A Windows-tagged test drives a
real PowerShell host through the same .cmd -> -File rewrite.

Closes #5649

Co-authored-by: multica-agent <github@multica.ai>
Bohan-J added a commit that referenced this pull request Jul 21, 2026
…e-tokenised (MUL-4992) (#5711)

* fix(agent/cursor): send prompt on stdin so CLI-like flags cannot be re-tokenised (MUL-4992)

On Windows a Cursor task whose prompt contains CLI-like flags failed in ~2s
with `error: unknown option '-X'` and no agent output (#5649). A pasted build
log such as

    go build -ldflags "-X main.version=foo" -o bin/server ./cmd/server

was enough to kill the run.

buildCursorArgs put the whole prompt in argv as the positional after -p.
The official Windows launcher chain ends in `& node.exe index.js $args`
inside cursor-agent.ps1, where PowerShell re-serialises $args onto node's
command line. Under Windows PowerShell 5.1 and pwsh <= 7.2 (Legacy native
argument passing) an argument holding embedded double quotes is not
re-escaped, so the quoted region closes early, node's argv parser re-splits
at the interior spaces, and `-X` reaches commander.js as a standalone flag.

#1709 removed the cmd.exe `%*` re-tokenisation but stopped at the
Go -> PowerShell boundary, one hop before this. Its Windows tests only
compare the argv slice Go builds and never execute a shim, which is why the
gap stayed invisible.

Fix: keep the prompt off every command line. cursor-agent's -p is a boolean
print-mode switch and the prompt is positional; with no positional prompt and
a non-TTY stdin the CLI reads stdin to EOF and uses that as the prompt. So
drop the prompt from argv on all platforms and write it to stdin, leaving
only fixed, content-free flags in argv. No shell or launcher on any platform
can re-tokenise what is not on a command line.

The write runs in its own goroutine: a prompt larger than the pipe buffer
(~64 KiB) blocks mid-write until the child drains it, and the child cannot
drain while nothing reads its stdout. Closing stdin signals end-of-prompt, so
it is closed on both success and error paths, and on cancellation to release
a blocked write. Write failures surface in the result diagnostic, ranked
below explicit agent errors so an early child exit (bad auth, bad flag) is
not masked by the resulting EPIPE.

Tests: a prompt carrying the exact `-ldflags "-X ..."` shape must arrive
byte-for-byte on stdin and appear nowhere in argv; a 512 KiB prompt must not
deadlock; the prompt is written verbatim (the CLI trims it, we do not). Both
new unix tests fail against the pre-fix code. A Windows-tagged test drives a
real PowerShell host through the same .cmd -> -File rewrite.

Closes #5649

Co-authored-by: multica-agent <github@multica.ai>

* test(agent/cursor): prove the Windows launcher fix on both PowerShell hosts in CI

The stdin fix has to hold on the host that actually exhibits the bug.
powershell.exe (5.1) and pwsh <= 7.2 default to Legacy native argument
passing; pwsh >= 7.3 defaults to Standard. A fix verified only on the newer
host would not be a fix for the reporter.

Run the shim probe against every PowerShell host on PATH rather than only the
one defaultPowerShellLookup would select, and hook the windows-tagged launcher
tests into the existing windows-execenv CI job. These tests are windows-tagged
and the backend job runs on ubuntu, so until now they ran nowhere.

Co-authored-by: multica-agent <github@multica.ai>

* test(ci): run Windows launcher tests verbosely so skips are visible

A skipped or unmatched test still reports "ok", which would make the
Windows job look like coverage it is not providing.

Co-authored-by: multica-agent <github@multica.ai>

* test(agent/cursor): close both coverage gaps in the #5649 regression tests

Two tests claimed guarantees they did not actually establish.

Windows: the fake cursor-agent.ps1 called [Console]::In.ReadToEnd() and wrote
the result itself, so it never launched a native child. The official shim ends
in `& node.exe index.js $args`, and that last hop is precisely where the bug
lives -- PowerShell re-serialises $args onto the child command line, and
whether the child inherits stdin was left unproven. The fake ps1 now
re-executes the test binary as a real native child (helper-process idiom),
which records the argv it actually received and drains stdin. Both PowerShell
hosts still run.

Interlock: the large-prompt fake drained stdin before writing any stdout, so
the child always unblocked the parent immediately and the test passed even
against a synchronous write -- it could not fail for the reason it existed.
The fake now floods stdout past pipe capacity *before* reading stdin, creating
the real mutual block. Verified: with the writer made synchronous the test
deadlocks to its 30s timeout, and passes only with the concurrent writer.

Also adds the missing cancellation case: a child that never reads stdin leaves
the writer blocked forever, so cancelling the context must close stdin,
release the writer and settle the run as aborted.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
ba0f3 pushed a commit to r2d-ai/multica that referenced this pull request Jul 22, 2026
…e-tokenised (MUL-4992) (multica-ai#5711)

* fix(agent/cursor): send prompt on stdin so CLI-like flags cannot be re-tokenised (MUL-4992)

On Windows a Cursor task whose prompt contains CLI-like flags failed in ~2s
with `error: unknown option '-X'` and no agent output (multica-ai#5649). A pasted build
log such as

    go build -ldflags "-X main.version=foo" -o bin/server ./cmd/server

was enough to kill the run.

buildCursorArgs put the whole prompt in argv as the positional after -p.
The official Windows launcher chain ends in `& node.exe index.js $args`
inside cursor-agent.ps1, where PowerShell re-serialises $args onto node's
command line. Under Windows PowerShell 5.1 and pwsh <= 7.2 (Legacy native
argument passing) an argument holding embedded double quotes is not
re-escaped, so the quoted region closes early, node's argv parser re-splits
at the interior spaces, and `-X` reaches commander.js as a standalone flag.

multica-ai#1709 removed the cmd.exe `%*` re-tokenisation but stopped at the
Go -> PowerShell boundary, one hop before this. Its Windows tests only
compare the argv slice Go builds and never execute a shim, which is why the
gap stayed invisible.

Fix: keep the prompt off every command line. cursor-agent's -p is a boolean
print-mode switch and the prompt is positional; with no positional prompt and
a non-TTY stdin the CLI reads stdin to EOF and uses that as the prompt. So
drop the prompt from argv on all platforms and write it to stdin, leaving
only fixed, content-free flags in argv. No shell or launcher on any platform
can re-tokenise what is not on a command line.

The write runs in its own goroutine: a prompt larger than the pipe buffer
(~64 KiB) blocks mid-write until the child drains it, and the child cannot
drain while nothing reads its stdout. Closing stdin signals end-of-prompt, so
it is closed on both success and error paths, and on cancellation to release
a blocked write. Write failures surface in the result diagnostic, ranked
below explicit agent errors so an early child exit (bad auth, bad flag) is
not masked by the resulting EPIPE.

Tests: a prompt carrying the exact `-ldflags "-X ..."` shape must arrive
byte-for-byte on stdin and appear nowhere in argv; a 512 KiB prompt must not
deadlock; the prompt is written verbatim (the CLI trims it, we do not). Both
new unix tests fail against the pre-fix code. A Windows-tagged test drives a
real PowerShell host through the same .cmd -> -File rewrite.

Closes multica-ai#5649

Co-authored-by: multica-agent <github@multica.ai>

* test(agent/cursor): prove the Windows launcher fix on both PowerShell hosts in CI

The stdin fix has to hold on the host that actually exhibits the bug.
powershell.exe (5.1) and pwsh <= 7.2 default to Legacy native argument
passing; pwsh >= 7.3 defaults to Standard. A fix verified only on the newer
host would not be a fix for the reporter.

Run the shim probe against every PowerShell host on PATH rather than only the
one defaultPowerShellLookup would select, and hook the windows-tagged launcher
tests into the existing windows-execenv CI job. These tests are windows-tagged
and the backend job runs on ubuntu, so until now they ran nowhere.

Co-authored-by: multica-agent <github@multica.ai>

* test(ci): run Windows launcher tests verbosely so skips are visible

A skipped or unmatched test still reports "ok", which would make the
Windows job look like coverage it is not providing.

Co-authored-by: multica-agent <github@multica.ai>

* test(agent/cursor): close both coverage gaps in the multica-ai#5649 regression tests

Two tests claimed guarantees they did not actually establish.

Windows: the fake cursor-agent.ps1 called [Console]::In.ReadToEnd() and wrote
the result itself, so it never launched a native child. The official shim ends
in `& node.exe index.js $args`, and that last hop is precisely where the bug
lives -- PowerShell re-serialises $args onto the child command line, and
whether the child inherits stdin was left unproven. The fake ps1 now
re-executes the test binary as a real native child (helper-process idiom),
which records the argv it actually received and drains stdin. Both PowerShell
hosts still run.

Interlock: the large-prompt fake drained stdin before writing any stdout, so
the child always unblocked the parent immediately and the test passed even
against a synchronous write -- it could not fail for the reason it existed.
The fake now floods stdout past pipe capacity *before* reading stdin, creating
the real mutual block. Verified: with the writer made synchronous the test
deadlocks to its 30s timeout, and passes only with the concurrent writer.

Also adds the missing cancellation case: a child that never reads stdin leaves
the writer blocked forever, so cancelling the context must close stdin,
release the writer and settle the run as aborted.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
ba0f3 pushed a commit to r2d-ai/multica that referenced this pull request Aug 7, 2026
…e-tokenised (MUL-4992) (multica-ai#5711)

* fix(agent/cursor): send prompt on stdin so CLI-like flags cannot be re-tokenised (MUL-4992)

On Windows a Cursor task whose prompt contains CLI-like flags failed in ~2s
with `error: unknown option '-X'` and no agent output (multica-ai#5649). A pasted build
log such as

    go build -ldflags "-X main.version=foo" -o bin/server ./cmd/server

was enough to kill the run.

buildCursorArgs put the whole prompt in argv as the positional after -p.
The official Windows launcher chain ends in `& node.exe index.js $args`
inside cursor-agent.ps1, where PowerShell re-serialises $args onto node's
command line. Under Windows PowerShell 5.1 and pwsh <= 7.2 (Legacy native
argument passing) an argument holding embedded double quotes is not
re-escaped, so the quoted region closes early, node's argv parser re-splits
at the interior spaces, and `-X` reaches commander.js as a standalone flag.

multica-ai#1709 removed the cmd.exe `%*` re-tokenisation but stopped at the
Go -> PowerShell boundary, one hop before this. Its Windows tests only
compare the argv slice Go builds and never execute a shim, which is why the
gap stayed invisible.

Fix: keep the prompt off every command line. cursor-agent's -p is a boolean
print-mode switch and the prompt is positional; with no positional prompt and
a non-TTY stdin the CLI reads stdin to EOF and uses that as the prompt. So
drop the prompt from argv on all platforms and write it to stdin, leaving
only fixed, content-free flags in argv. No shell or launcher on any platform
can re-tokenise what is not on a command line.

The write runs in its own goroutine: a prompt larger than the pipe buffer
(~64 KiB) blocks mid-write until the child drains it, and the child cannot
drain while nothing reads its stdout. Closing stdin signals end-of-prompt, so
it is closed on both success and error paths, and on cancellation to release
a blocked write. Write failures surface in the result diagnostic, ranked
below explicit agent errors so an early child exit (bad auth, bad flag) is
not masked by the resulting EPIPE.

Tests: a prompt carrying the exact `-ldflags "-X ..."` shape must arrive
byte-for-byte on stdin and appear nowhere in argv; a 512 KiB prompt must not
deadlock; the prompt is written verbatim (the CLI trims it, we do not). Both
new unix tests fail against the pre-fix code. A Windows-tagged test drives a
real PowerShell host through the same .cmd -> -File rewrite.

Closes multica-ai#5649

Co-authored-by: multica-agent <github@multica.ai>

* test(agent/cursor): prove the Windows launcher fix on both PowerShell hosts in CI

The stdin fix has to hold on the host that actually exhibits the bug.
powershell.exe (5.1) and pwsh <= 7.2 default to Legacy native argument
passing; pwsh >= 7.3 defaults to Standard. A fix verified only on the newer
host would not be a fix for the reporter.

Run the shim probe against every PowerShell host on PATH rather than only the
one defaultPowerShellLookup would select, and hook the windows-tagged launcher
tests into the existing windows-execenv CI job. These tests are windows-tagged
and the backend job runs on ubuntu, so until now they ran nowhere.

Co-authored-by: multica-agent <github@multica.ai>

* test(ci): run Windows launcher tests verbosely so skips are visible

A skipped or unmatched test still reports "ok", which would make the
Windows job look like coverage it is not providing.

Co-authored-by: multica-agent <github@multica.ai>

* test(agent/cursor): close both coverage gaps in the multica-ai#5649 regression tests

Two tests claimed guarantees they did not actually establish.

Windows: the fake cursor-agent.ps1 called [Console]::In.ReadToEnd() and wrote
the result itself, so it never launched a native child. The official shim ends
in `& node.exe index.js $args`, and that last hop is precisely where the bug
lives -- PowerShell re-serialises $args onto the child command line, and
whether the child inherits stdin was left unproven. The fake ps1 now
re-executes the test binary as a real native child (helper-process idiom),
which records the argv it actually received and drains stdin. Both PowerShell
hosts still run.

Interlock: the large-prompt fake drained stdin before writing any stdout, so
the child always unblocked the parent immediately and the test passed even
against a synchronous write -- it could not fail for the reason it existed.
The fake now floods stdout past pipe capacity *before* reading stdin, creating
the real mutual block. Verified: with the writer made synchronous the test
deadlocks to its 30s timeout, and passes only with the concurrent writer.

Also adds the missing cancellation case: a child that never reads stdin leaves
the writer blocked forever, so cancelling the context must close stdin,
release the writer and settle the run as aborted.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: [Windows] Multica-spawned cursor-agent always exits 1; manual run succeeds

2 participants