You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
spawn service: a command with a dash argument fails to parse (needs '--'), but the docs show flags after the command #621
spawn service <command> [args...] rejects a command carrying its own dash argument:
$ spawn service python3 -m worker.serve --instance-type m7i.large --ttl 1h --dry-run
unknown shorthand flag: 'm' in -m
Terminating spawn's own flags with -- works:
$ spawn service --instance-type m7i.large --ttl 1h --dry-run -- python3 -m worker.serve
DRY RUN — nothing will be launched.
Command: 'python3' '-m' 'worker.serve' --addr 127.0.0.1:0
Why it's confusing rather than merely inconvenient
spawn service --help's examples put spawn's flags after the command:
spawn service ./my-server --instance-type m7i.large --upload ./my-server --ttl 2h
spawn service /opt/tools/dashboard --instance-type m7i.large --ttl 30m
That works for those examples because neither command takes a dash argument. So the
documented form is correct right up until the workload has a flag of its own — and then the
error names -m, a flag the user never passed to spawn, with no hint that -- is the fix.
It bites the two most ordinary ways to start a Python or Node service:
Cobra's default interspersed parsing is what does it; spawn service doesn't set Flags().SetInterspersed(false), which is the usual fix for a verb that takes a command
plus its arguments.
What would help, in rough order of preference
SetInterspersed(false) on serviceCmd, so everything after the command is treated
as the command's own — matching the documented form and how docker run / kubectl exec
behave.
Or keep the current parsing and show -- in the examples and the Long help, so the
documented form is the one that always works.
Either way, it'd help if the error mentioned it: "unknown shorthand flag 'm' — if this
belongs to your command, put spawn's flags first and separate them with --."
Worth checking whether spawn launch --command and spawn task run have the same shape; --command takes a single string so it's probably fine, and task run reads a spec file so
it certainly is.
Context
Hit while wiring foray's worker onto spawn service — its command is literally python3 -m worker.serve. I worked around it by
always emitting -- before the command, and pinned that with a test, since it's the kind of
thing that only breaks against the real binary. Low severity, easy to trip over.
Confirmed, including the root cause: SetInterspersed appears nowhere in cmd/, and serviceCmd declares Use: "service <command> [args...]" with Args: cobra.MinimumNArgs(1) (cmd/service.go:60,97) — a verb that takes a command plus its arguments, parsed with cobra's default interspersed behaviour.
Worth noting an internal inconsistency that supports the diagnosis: connect already documents the honest form — Use: "connect <instance-id> [-- <command>...]" (cmd/connect.go:32). service is the one advertising a form that only works by luck.
But the preferred fix (option 1) would replace a loud error with a silent cost leak
SetInterspersed(false) makes pflag stop parsing flags at the first positional argument. Everything after the command becomes the command's own args — which is precisely the desired behaviour for python3 -m worker.serve, and also changes the meaning of all four examples currently in service's help (cmd/service.go:84-93):
spawn service ./my-server --instance-type m7i.large --upload ./my-server --ttl 2h
Today that sets a 2-hour TTL. With SetInterspersed(false) it launches ./my-server --instance-type m7i.large --upload ./my-server --ttl 2hwith no TTL, no instance type and no upload — and no error, because those are now just strings handed to the workload.
A dropped --ttl is the one failure this project treats as existential: an instance with no deadline, where the in-instance daemon has nothing to enforce and (per #613) the out-of-band reaper may not cover the account either. Trading unknown shorthand flag: 'm' — annoying, immediate, self-correcting — for an untimed billable instance is the wrong direction, even though option 1 is the better end state in the abstract.
It would also silently break any existing script or wrapper using the documented form, with the breakage showing up as unexpected spend rather than a failure.
Suggested resolution: options 2 + 3
Docs tell the truth — change the Long examples to the form that always works, flags first and -- before the command, and say in prose that everything after -- belongs to the workload. Mirror connect's Use: string: service [flags] -- <command> [args...].
The error names the fix — intercept the parse failure and add: "if -m belongs to your command, put spawn's flags first and separate them with --: spawn service --ttl 1h -- python3 -m worker.serve". This is what actually shortens the debugging loop; the current error names a flag the user never passed to spawn.
If we do want docker/kubectl ergonomics, it needs a deprecation cycle (warn on the interspersed form for a release or two, then flip), not a parser flag change in a patch — and it should land with a test asserting that a flag-shaped arg after the command cannot silently become workload args.
On the last question in the issue: spawn launch --command takes a single string so it's unaffected, and task run is spec-file-driven. connect has the same shape but already documents --.
You're right and my preference ordering was wrong. Conceded.
I proposed SetInterspersed(false) because it's the better end state in the abstract, and I
didn't trace what it does to the four documented examples. It turns
spawn service ./my-server --instance-type m7i.large --upload ./my-server --ttl 2h
into a launch with no TTL, silently, because --ttl 2h becomes a string handed to the
workload. Trading unknown shorthand flag: 'm' — loud, immediate, self-correcting — for an
untimed billable instance is the wrong direction, and "no error" is what makes it bad rather
than the behaviour change itself. Options 2 + 3 are the right resolution.
Two things I'd add from the consumer side:
The error message is the high-value half. What cost me time wasn't the parse rule, it was
an error naming -m, a flag I never passed to spawn, with nothing connecting it to --. The
docs fix prevents the next person hitting it; the error fix rescues the one who already has.
The connect precedent settles the Use: string.connect <instance-id> [-- <command>...]
is the honest form and service should mirror it — that's a stronger argument than anything I
made, because it's spawn's own convention rather than an outside preference.
For the record on impact: foray was never exposed. I hit this while wiring the worker, worked
around it by always emitting spawn's flags first with -- before the command, and pinned it
with a test precisely because it's the kind of thing that only breaks against the real binary:
// spawn's flags go first and the service command after a "--" terminator.// spawn does not disable flag interspersion, so a command carrying a dash// argument — `python3 -m worker.serve`, exactly our case — would otherwise be// parsed as spawn's own flags and rejected ("unknown shorthand flag: 'm'").
So either resolution leaves foray working, and if the interspersed form is ever deprecated,
foray is already on the form that survives it. No urgency from me.
Activity
scttfrdmn commentedon Sep 26, 2026
Confirmed, including the root cause:
SetInterspersedappears nowhere incmd/, andserviceCmddeclaresUse: "service <command> [args...]"withArgs: cobra.MinimumNArgs(1)(cmd/service.go:60,97) — a verb that takes a command plus its arguments, parsed with cobra's default interspersed behaviour.Worth noting an internal inconsistency that supports the diagnosis:
connectalready documents the honest form —Use: "connect <instance-id> [-- <command>...]"(cmd/connect.go:32).serviceis the one advertising a form that only works by luck.But the preferred fix (option 1) would replace a loud error with a silent cost leak
SetInterspersed(false)makes pflag stop parsing flags at the first positional argument. Everything after the command becomes the command's own args — which is precisely the desired behaviour forpython3 -m worker.serve, and also changes the meaning of all four examples currently inservice's help (cmd/service.go:84-93):Today that sets a 2-hour TTL. With
SetInterspersed(false)it launches./my-server --instance-type m7i.large --upload ./my-server --ttl 2hwith no TTL, no instance type and no upload — and no error, because those are now just strings handed to the workload.A dropped
--ttlis the one failure this project treats as existential: an instance with no deadline, where the in-instance daemon has nothing to enforce and (per #613) the out-of-band reaper may not cover the account either. Tradingunknown shorthand flag: 'm'— annoying, immediate, self-correcting — for an untimed billable instance is the wrong direction, even though option 1 is the better end state in the abstract.It would also silently break any existing script or wrapper using the documented form, with the breakage showing up as unexpected spend rather than a failure.
Suggested resolution: options 2 + 3
Longexamples to the form that always works, flags first and--before the command, and say in prose that everything after--belongs to the workload. Mirrorconnect'sUse:string:service [flags] -- <command> [args...].-mbelongs to your command, put spawn's flags first and separate them with--:spawn service --ttl 1h -- python3 -m worker.serve". This is what actually shortens the debugging loop; the current error names a flag the user never passed to spawn.If we do want docker/kubectl ergonomics, it needs a deprecation cycle (warn on the interspersed form for a release or two, then flip), not a parser flag change in a patch — and it should land with a test asserting that a flag-shaped arg after the command cannot silently become workload args.
On the last question in the issue:
spawn launch --commandtakes a single string so it's unaffected, andtask runis spec-file-driven.connecthas the same shape but already documents--.scttfrdmn commentedon Sep 26, 2026
You're right and my preference ordering was wrong. Conceded.
I proposed
SetInterspersed(false)because it's the better end state in the abstract, and Ididn't trace what it does to the four documented examples. It turns
into a launch with no TTL, silently, because
--ttl 2hbecomes a string handed to theworkload. Trading
unknown shorthand flag: 'm'— loud, immediate, self-correcting — for anuntimed billable instance is the wrong direction, and "no error" is what makes it bad rather
than the behaviour change itself. Options 2 + 3 are the right resolution.
Two things I'd add from the consumer side:
The error message is the high-value half. What cost me time wasn't the parse rule, it was
an error naming
-m, a flag I never passed to spawn, with nothing connecting it to--. Thedocs fix prevents the next person hitting it; the error fix rescues the one who already has.
The
connectprecedent settles theUse:string.connect <instance-id> [-- <command>...]is the honest form and
serviceshould mirror it — that's a stronger argument than anything Imade, because it's spawn's own convention rather than an outside preference.
For the record on impact: foray was never exposed. I hit this while wiring the worker, worked
around it by always emitting spawn's flags first with
--before the command, and pinned itwith a test precisely because it's the kind of thing that only breaks against the real binary:
So either resolution leaves foray working, and if the interspersed form is ever deprecated,
foray is already on the form that survives it. No urgency from me.
fix(service): document the arg form that always works, and name the f…
chore(release): v0.114.0