Skip to content

bug: gc sling --formula UX — silent --on failure + undiscoverable required vars #1100

@ryanholtschneider2

Description

@ryanholtschneider2

Before you continue

  • I searched existing issues and did not find a duplicate.
  • I read the relevant docs and contributor guidance.

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:

  1. Initialize a clean city:

    gc init --provider claude --name repro-test /tmp/repro-city-a
    cd /tmp/repro-city-a
    
  2. 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}}"
  3. Note: gc sling --formula takes the filename basename (repro), not the internal formula = field (repro-required-vars). This is itself a minor UX trap.

  4. Create a throwaway bead:

    bd create --title="repro-target" --description="test" --type=task
    # ✓ Created issue: rt-oeu
    BEAD=rt-oeu
    
  5. 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
    
  6. --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
    
  7. 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):

  1. 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).
  2. Move parser.ValidateVars() to run before per-step template rendering, so missing-var errors from both phases are reported in a single invocation.
  3. 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."
  4. Surface required vars in gc formula show <name> output.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions