max visits rejects variable reference - #291
Merged
Merged
Conversation
StepSpec.MaxVisits was a primitive int decoded directly by gohcl, which rejected variable traversals such as max_visits = var.max_refine even when the variable had a known default. The sibling parallel_max attribute already avoided this by reading from sp.Remain and evaluating through decodeIntAttr + FoldExpr. Remove the primitive MaxVisits field from StepSpec so max_visits stays in Remain, add a decodeMaxVisits helper that uses the same FoldExpr path, and wire it into the three step compile paths (adapter, iterating, subworkflow). This preserves the existing >= 0 validation and produces the same clear single diagnostics for runtime-only references, unknown variables, and fractional values that parallel_max already emits. Add regression tests covering var.* and local.* references, missing defaults, fractional values, negative values, runtime-only references, literal fallback, and JSON round-trip of StepNode.MaxVisits. Update the generated language spec to reflect the schema change and keep max_visits documented as an additional step attribute. Closes: max_visits_rejects_variable_reference Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
brokenbot
approved these changes
Jul 31, 2026
brokenbot
left a comment
Collaborator
There was a problem hiding this comment.
Approved. max_visits now decodes via the same compile-time fold path as parallel_max (decodeMaxVisits → decodeIntAttr → FoldExpr), accepting literal/var./local. and rejecting runtime-only, fractional, and negative values with clear single diagnostics. All three step compile paths wired through; StepNode.MaxVisits and engine enforcement unchanged. New regression test covers var ref, local ref, missing default, fractional, negative, runtime-only, literal, and JSON round-trip. Full workflow tests pass; all CI green. Meets all workstream exit criteria.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
max_visits_rejects_variable_reference
Repository:
criteriaConfirmed behavior
On both refs under test (
origin/mainat0829f887968d42d8b5e4d53685c6f48086390cafand stablev0.5.5at6ac528757d2615d64b4b5f78949a12bdae1870fa), the compiler rejects a step attribute of the formmax_visits = var.<name>even when the variable has a known, immutable default.Measured behavior:
criteria compileexit code:1.run-main.mdandrun-stable.md, Shape A):max_visits = local.<name>(Shape C), string-typed variables (Shape E), fractional variables (Shape F), variables without defaults (Shape G), negative variable values (Shape H), and runtime-only references such assteps.first.done,each.value,data.store.limit, andwhile.index(runtime shapes).max_total_steps,max_step_retries,max_visits_warn_threshold) are rejected forvar.*in the same way (Shape J). This is adjacent context for a possible separate workstream; it is not part of the required fix for this report.Already correct — do not regress these:
max_visits = 3compiles cleanly and the resulting FSM JSON contains the resolved integer (run-main.md,run-stable.md, Shape I, exit0).max_visits = -1reaches the semantic validator and emitsstep "review_plan": max_visits must be >= 0(Shape I-negative, exit1).max_visitscompiles cleanly with the default unlimited behavior.parallel_max = var.<name>compiles cleanly (Shape K, exit0), confirming that a sibling numeric step attribute already uses a variable-accepting decode path.0).Root cause identified from evidence:
StepSpec.MaxVisitsis declared as a primitiveintstruct field with anhcl:"max_visits,optional"tag, sogohcldecodes it directly and forbids variable traversals. The sibling attributeparallel_maxis read fromsp.Remainand evaluated throughdecodeIntAttr→FoldExpr, which is why it acceptsvar.*andlocal.*. The same primitive-field problem applies toPolicySpecnumeric fields.Required behavior
max_visitson astepblock must accept a compile-time-resolvable expression, specifically:variablewith a known default or a value supplied via--var,localwhose value is a known integer.max_visitsmust continue to reject runtime-only references (steps.first.done,each.value,data.store.limit,while.index) with a clear diagnostic that says the attribute requires a compile-time value.max_visitsmust continue to validate that the resolved value is a whole number and>= 0. Negative values must produce the existingmax_visits must be >= 0diagnostic; fractional values (e.g., a variable whose default is3.5) must produce a type/whole-number diagnostic.Variables not allowedandUnsuitable value typeformax_visitsmust no longer appear when the expression is a valid compile-time value.parallel_max = var.<name>must continue to compile cleanly and support--varoverrides (regression guard).workflow/package that compiles a workflow withmax_visits = var.max_refine(and optionallylocal.max_refine) and asserts:StepNode.MaxVisitsequals the expected integer,Exit criteria
criteria compileexits0on a workflow containingmax_visits = var.max_refinewheremax_refinehas a known default.criteria plan --var max_refine=7reflects the override value in the plan output.criteria compileexits1with a clear, single diagnostic formax_visits = var.max_refinewhen the variable has no default and no--varoverride is supplied.criteria compileexits1with a type/whole-number diagnostic for a variable whose default is3.5used asmax_visits.criteria compileexits1withmax_visits must be >= 0formax_visits = -1(or a variable whose default is-1).criteria compileexits1with a clear diagnostic for runtime-only references such asmax_visits = each.value.make testpasses after the change.