coleam00/Archon · warning

loop_group_gate_completion_not_referenced

loop_group_gate_completion_not_referenced

Error message

Node '${gate.id}': this gate is the loop_group's terminal sink, but 'loop_group.until_bash' does not reference '${gateRef}' — the human's decision is captured but never consulted for completion, so the loop only ends via max_iterations. Declare 'until_bash' checking '${gateRef}.decision' (e.g. '[ "${gateRef}.decision" = "approve" ]') so the gate's answer actually drives completion (#2707 step 3).

What it means

This loader warning fires when a gate node correctly is a loop_group body's sole terminal sink, but the group's loop_group.until_bash expression never references that gate's output ($<gateId>.output). The human's decision is captured (a node_completed event is written), but the completion check never consults it, so the loop only terminates via max_iterations instead of the human's answer (#2707 step 3). Detection is purely structural: the until_bash string is scanned for output references naming the gate id.

Source

Thrown at packages/workflows/src/loader.ts:560

        // string reference the gate's node id), not prose-sniffing — no judgment
        // about what the check DOES with it, only whether it looks at it at all.
        const untilBash = node.loop_group.until_bash;
        const untilBashRefsGate =
          untilBash !== undefined &&
          Array.from(untilBash.matchAll(new RegExp(OUTPUT_REF_SOURCE, 'g'))).some(
            m => m[1] === gate.id
          );
        if (!untilBashRefsGate) {
          const gateRef = `$${gate.id}.output`;
          const message =
            `Node '${gate.id}': this gate is the loop_group's terminal sink, but ` +
            `'loop_group.until_bash' does not reference '${gateRef}' — the human's ` +
            'decision is captured but never consulted for completion, so the loop only ' +
            `ends via max_iterations. Declare 'until_bash' checking '${gateRef}.decision' ` +
            `(e.g. '[ "${gateRef}.decision" = "approve" ]') so the gate's answer actually ` +
            'drives completion (#2707 step 3).';
          warnings.push(message);
          getLog().warn(
            { id: gate.id, warning: message },
            'loop_group_gate_completion_not_referenced'
          );
        }
      }
    }

    // A body-terminal sink that is itself an interactive loop/loop_group can pause
    // without stopping THIS group (#2753) — a bare gate sink is excluded here since
    // that case is already correctly handled above (and by #2707 step 3 at runtime);
    // this covers a sink whose own pause is trapped one level down instead.
    if (bodySinks.length === 1) {
      const sink = bodySinks[0];
      if (!isIncludeDirective(sink) && !isGateNode(sink) && isUnescalatableInteractiveSink(sink)) {
        const message =
          `Node '${id}': this loop_group's terminal sink ('${sink.id}') is itself an ` +
          'interactive loop/loop_group — a pause inside it does not escalate to stop ' +
          "this loop_group's own iteration (#2753). The outer loop can run further iterations " +

View on GitHub (pinned to 0773b97458)

Solutions

  1. Add or extend loop_group.until_bash so it reads the gate's output, e.g. until_bash: '[ "$approve.output.decision" = "approve" ]'
  2. Fix the node id or output path in until_bash so the reference exactly matches the gate node's id
  3. If the loop is genuinely max_iterations-driven, remove the gate or document that its answer does not drive completion

Example fix

# before
loop_group:
  until_bash: '[ "$work.output.done" = "true" ]'
  nodes:
    - id: work
      agent: ...
    - id: approve
      gate: {}
# after
loop_group:
  until_bash: '[ "$approve.output.decision" = "approve" ]'
  nodes:
    - id: work
      agent: ...
    - id: approve
      gate: {}
Defensive patterns

Strategy: validation

Validate before calling

function gateReferencedInUntilBash(group) {
  const dependedOn = new Set(group.nodes.flatMap(n => n.depends_on ?? []));
  const sinks = group.nodes.filter(n => !dependedOn.has(n.id));
  const gate = group.nodes.find(n => 'gate' in n);
  if (!gate || sinks.length !== 1 || sinks[0].id !== gate.id) return true; // other warning
  return group.until_bash !== undefined && group.until_bash.includes(`$${gate.id}.output`);
}

Prevention

When it happens

Trigger: Loading a workflow YAML with a loop_group whose body ends in a valid gate sink while loop_group.until_bash is undefined, references other nodes' outputs only, or references the gate with a misspelled/incorrectly-scoped id so the OUTPUT_REF_SOURCE regex match finds no id equal to gate.id.

Common situations: Authors declaring until_bash against a work node's output while forgetting the gate; typos in $gate-id.output.decision; copying an until_bash template from a non-gate loop; assuming the gate itself ends the loop without wiring the completion check.

Related errors


AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01). Data as JSON: /api/errors/f2ae7222cae474be. Report an issue: GitHub.