Before you continue
Gas City version
0.15.0 (commit: 73f09dd-dirty, built: 2026-04-16T19:09:14Z)
Environment
Ubuntu 24.04.3 LTS, Linux 6.17.0-20-generic x86_64, bash, tmux provider, Claude agent backend.
Reproduction
Self-contained, walked end-to-end in a fresh gc init city under /tmp:
-
Initialize a clean city:
gc init --provider claude --name repro-test /tmp/repro-city-a
cd /tmp/repro-city-a
-
Add a minimal formula with two required = true vars, one referenced in a step title and one not:
# /tmp/repro-city-a/formulas/repro.toml
formula = "repro-required-vars"
version = 1
[vars]
[vars.target_id]
description = "Bead being worked on"
required = true
[vars.workspace]
description = "Workspace path"
required = true
[[steps]]
id = "do-work"
title = "Do work for {{target_id}}"
description = "Target: {{target_id}}, workspace: {{workspace}}"
-
Note: gc sling --formula takes the filename basename (repro), not the internal formula = field (repro-required-vars). This is itself a minor UX trap.
-
Create a throwaway bead:
bd create --title="repro-target" --description="test" --type=task
# ✓ Created issue: rt-oeu
BEAD=rt-oeu
-
Bare sling to the built-in mayor agent creates a convoy but does not instantiate the formula DAG:
gc sling mayor $BEAD
# Auto-convoy rt-jca
# Slung rt-oeu → mayor
bd list --all --json | jq '[.[] | select(.metadata."gc.root_bead_id" != null)] | length'
# → 0
-
--on silently fails regardless of whether the bead is already in a convoy:
# Fresh bead (no convoy)
bd create --title="fresh" --description="no convoy" --type=task # → rt-pvz
gc sling mayor repro --formula --on rt-pvz --var target_id=rt-pvz --var workspace=/tmp/x \
> /tmp/o 2> /tmp/e; echo "exit=$? stdout=$(wc -c < /tmp/o) stderr=$(wc -c < /tmp/e)"
# exit=1 stdout=0 stderr=0
# Bead already in convoy (from step 5)
gc sling mayor repro --formula --on rt-oeu > /tmp/o 2> /tmp/e; echo "exit=$?"
# exit=1 stdout=0 stderr=0
-
Discover required vars across two failure paths. The title-render path reports vars referenced in step titles, all at once per invocation. ValidateVars reports all remaining required = true vars. The two paths run sequentially and aren't merged, so the minimum is 2 round-trips whenever any required var is absent from all step titles:
gc sling mayor repro --formula
# gc sling: instantiating formula "repro": step "repro-required-vars.do-work": \
# bead title contains unresolved variable(s) target_id — missing or misspelled --var(s)?
gc sling mayor repro --formula --var target_id=rt-oeu
# gc sling: instantiating formula "repro": formula "repro": variable validation failed:
# - variable "workspace" is required
gc sling mayor repro --formula --var target_id=rt-oeu --var workspace=/tmp/repro
# Slung formula "repro" (wisp root rt-32w) → mayor
Verified the title-render path itself does batch — a title with {{a}} {{b}} {{c}} all-unset reports all three in one error. The separation is strictly between the title-render and ValidateVars phases.
Expected behavior
--on <bead> either succeeds (attaches formula to the existing bead, auto-binding variables derivable from the bead/convoy context) or emits a non-empty, actionable error on stderr. Silent exit=1 is undebuggable.
- Missing-variable errors from the title-render path and
ValidateVars are consolidated into a single invocation's output, not reported in two serial phases.
gc sling --formula <name> accepts either the filename basename OR the internal formula = field, or the CLI documents that only one works.
gc formula show <name> surfaces required variables prominently (Required vars: section, or --required-only filter) so operators can construct a full --var list without iterative failure.
Actual behavior
--on path exits 1 with no output on either stream in every configuration I tested (bead in convoy, bead not in convoy, vars provided, vars missing).
- Required-variable discovery takes a minimum of 2 round-trips whenever any required var is missing from all step titles.
gc sling mayor <bead> (no --formula) silently accepts the command, creates a convoy wrapper around the bead, but does not instantiate any formula DAG — zero step beads materialize. From the outside, identical to a successful formula sling.
Logs, screenshots, or traces
# Captured from the repro session above
$ gc sling mayor repro --formula --on rt-oeu > /tmp/o 2> /tmp/e; echo "exit=$? stdout=$(wc -c < /tmp/o) stderr=$(wc -c < /tmp/e)"
exit=1 stdout=0 stderr=0
$ gc sling mayor repro --formula
gc sling: instantiating formula "repro": step "repro-required-vars.do-work": bead title contains unresolved variable(s) target_id — missing or misspelled --var(s)?
$ gc sling mayor repro --formula --var target_id=rt-oeu
gc sling: instantiating formula "repro": formula "repro": variable validation failed:
- variable "workspace" is required
$ gc sling mayor $BEAD
Auto-convoy rt-jca
Slung rt-oeu → mayor
$ bd list --all --json | jq '[.[] | select(.metadata."gc.root_bead_id" != null and .status != "closed")] | length'
0
Additional context
Code citations (verified on main at 73f09dd):
- Required-variable validation:
internal/formula/parser.go:515 builds the variable validation failed message from a collected errs slice — so multi-variable reporting is already supported in this path. The separation lives between this validator and the template-rendering step, which raises bead title contains unresolved variable(s) before ValidateVars runs.
--on flag handling: cmd/gc/cmd_sling.go references opts.OnFormula in the dispatch/preview paths (:1394, :1442, :1540, :1552) but the failure path returns err silently — likely a return err where the error is non-nil but never rendered to stderr.
- Formula name resolution:
gc formula list output appears keyed by filename basename, which diverges from the in-file formula = field. Worth confirming in internal/formula/ loader tests.
Related issues:
Proposed fixes (lowest-risk first):
- Ensure the
--on failure path always emits a non-empty stderr message on exit ≥ 1. Smallest diff: wrap the failure return in cmd/gc/cmd_sling.go with fmt.Fprintf(cmd.ErrOrStderr(), "gc sling: --on failed: %v\n", err).
- Move
parser.ValidateVars() to run before per-step template rendering, so missing-var errors from both phases are reported in a single invocation.
- Add a warning in
gc sling <agent> <bead> when the target agent has a non-empty EffectiveDefaultSlingFormula() but --formula was not passed: "Warning: has default formula ; bead will be routed raw. Use --formula to instantiate, or --no-formula to suppress this warning."
- Surface required vars in
gc formula show <name> output.
- Resolve
gc sling --formula <name> against both the filename basename and the internal formula = field; document which is authoritative.
(1) and (3) are the highest-impact low-risk wins — they close the "silent success/failure" observability gap.
Before you continue
Gas City version
0.15.0 (commit: 73f09dd-dirty, built: 2026-04-16T19:09:14Z)
Environment
Ubuntu 24.04.3 LTS, Linux 6.17.0-20-generic x86_64, bash, tmux provider, Claude agent backend.
Reproduction
Self-contained, walked end-to-end in a fresh
gc initcity under/tmp:Initialize a clean city:
Add a minimal formula with two
required = truevars, one referenced in a step title and one not:Note:
gc sling --formulatakes the filename basename (repro), not the internalformula =field (repro-required-vars). This is itself a minor UX trap.Create a throwaway bead:
Bare sling to the built-in
mayoragent creates a convoy but does not instantiate the formula DAG:--onsilently fails regardless of whether the bead is already in a convoy:Discover required vars across two failure paths. The title-render path reports vars referenced in step titles, all at once per invocation.
ValidateVarsreports all remainingrequired = truevars. The two paths run sequentially and aren't merged, so the minimum is 2 round-trips whenever any required var is absent from all step titles:Verified the title-render path itself does batch — a title with
{{a}} {{b}} {{c}}all-unset reports all three in one error. The separation is strictly between the title-render and ValidateVars phases.Expected behavior
--on <bead>either succeeds (attaches formula to the existing bead, auto-binding variables derivable from the bead/convoy context) or emits a non-empty, actionable error on stderr. Silentexit=1is undebuggable.ValidateVarsare consolidated into a single invocation's output, not reported in two serial phases.gc sling --formula <name>accepts either the filename basename OR the internalformula =field, or the CLI documents that only one works.gc formula show <name>surfaces required variables prominently (Required vars:section, or--required-onlyfilter) so operators can construct a full--varlist without iterative failure.Actual behavior
--onpath exits 1 with no output on either stream in every configuration I tested (bead in convoy, bead not in convoy, vars provided, vars missing).gc sling mayor <bead>(no--formula) silently accepts the command, creates a convoy wrapper around the bead, but does not instantiate any formula DAG — zero step beads materialize. From the outside, identical to a successful formula sling.Logs, screenshots, or traces
Additional context
Code citations (verified on
mainat73f09dd):internal/formula/parser.go:515builds thevariable validation failedmessage from a collectederrsslice — so multi-variable reporting is already supported in this path. The separation lives between this validator and the template-rendering step, which raisesbead title contains unresolved variable(s)beforeValidateVarsruns.--onflag handling:cmd/gc/cmd_sling.goreferencesopts.OnFormulain the dispatch/preview paths (:1394,:1442,:1540,:1552) but the failure path returnserrsilently — likely areturn errwhere the error is non-nil but never rendered to stderr.gc formula listoutput appears keyed by filename basename, which diverges from the in-fileformula =field. Worth confirming ininternal/formula/loader tests.Related issues:
gc sling --formulacreates duplicate molecules for same source issue. Same surface, combined effect: bare sling has no dedup,--formula --onsilently fails on conflict, so users re-invoke and end up with duplicates.gc sling --formulasilently creates beads with unresolved template variables. Closed; the "step title renders before full var validation" path appears to still exist.gc agent claim/gc molvsgc hook/gc sling/gc formula#40 (CLOSED) — clarifies canonical work-routing workflow. Suggests this UX trap has been a recurring source of confusion.Proposed fixes (lowest-risk first):
--onfailure path always emits a non-empty stderr message on exit ≥ 1. Smallest diff: wrap the failure return incmd/gc/cmd_sling.gowithfmt.Fprintf(cmd.ErrOrStderr(), "gc sling: --on failed: %v\n", err).parser.ValidateVars()to run before per-step template rendering, so missing-var errors from both phases are reported in a single invocation.gc sling <agent> <bead>when the target agent has a non-emptyEffectiveDefaultSlingFormula()but--formulawas not passed: "Warning: has default formula ; bead will be routed raw. Use --formula to instantiate, or --no-formula to suppress this warning."gc formula show <name>output.gc sling --formula <name>against both the filename basename and the internalformula =field; document which is authoritative.(1) and (3) are the highest-impact low-risk wins — they close the "silent success/failure" observability gap.