let d = import "adr-defaults.ncl" in d.make_adr { id = "adr-011", title = "Mode Guards and Convergence — Active Partner and Refinement Loop in the Mode Schema", status = 'Accepted, date = "2026-03-30", context = "Reflection modes executed procedures as typed DAGs but lacked two capabilities that caused real failures: (1) modes could run against projects in invalid states — missing ontology files, unavailable tools, incomplete manifests — because preconditions were informational text, not executable checks. An agent following the protocol would execute sync-ontology on a project without core.ncl and get an opaque nickel error instead of a clear block. (2) Modes like sync-ontology require iteration — scan, diff, propose, apply, then verify that drift is zero. If drift remained after one pass, the mode reported success and the agent moved on. There was no mechanism for the protocol to say 'keep going until this condition is met'. Both gaps were identified during a systematic comparison against 45 augmented coding patterns (lexler.github.io/augmented-coding-patterns): Active Partner (#1) requires the system to push back on invalid actions, and Refinement Loop (#36) requires iteration until convergence. A separate action subsystem (ext/action) was considered and rejected in favor of extending the existing mode schema.", decision = "Extend reflection/schema.ncl with two new optional fields on ModeBase: guards (Array Guard) for pre-flight executable checks, and converge (Converge) for post-execution convergence loops. Guards run before any step and can Block (abort) or Warn (continue with message). Converge evaluates a condition command after all steps complete and re-executes failed or all steps up to max_iterations times. Both are backward-compatible — all existing modes export unchanged with guards defaulting to [] and converge being optional.", rationale = [ { claim = "Guards formalize the Active Partner pattern: the protocol pushes back before acting", detail = "Guards are executable shell commands with a severity (Block/Warn) and a human-readable reason. Unlike preconditions (informational text), guards run and block. This prevents agents from executing modes against projects in invalid states. The guard reason tells the agent exactly what to fix.", }, { claim = "Converge formalizes the Refinement Loop pattern: modes iterate until a condition is met", detail = "Modes like sync-ontology need to iterate: apply changes, re-diff, apply again if drift remains. The converge field lets the mode contract declare this expectation. The executor handles the loop logic — mode authors just declare the condition, max iterations, and retry strategy (RetryFailed or RetryAll).", }, { claim = "Extending the mode schema is simpler than creating a separate action subsystem", detail = "The alternative was ext/action — a new NCL schema for action contracts with gates, events, and convergence conditions. This would have created a parallel concept to modes with overlapping responsibilities. Since modes are already the execution abstraction, adding guards and converge to the same schema keeps a single concept for all executable procedures. Consumer projects learn one schema, not two.", }, ], consequences = { positive = [ "Agents get clear, early feedback when a mode cannot run (guard reason instead of opaque errors)", "Iterative workflows like sync-ontology converge automatically instead of requiring manual re-execution", "The mode schema remains the single execution abstraction — no competing action/workflow concept", "Backward compatible: all 19 existing modes export unchanged", "describe mode shows guards and converge sections — agents see the full execution contract", ], negative = [ "Guard commands add shell execution before steps — latency increases for guarded modes", "Convergence loops can mask underlying issues if max_iterations is set too high — a mode that never converges wastes resources silently", "Mode authors must write correct shell commands for guard checks and convergence conditions — no NCL-level validation of command correctness", ], }, alternatives_considered = [ { option = "ext/action — separate action contract schema with gates, events, and convergence", why_rejected = "Creates a parallel execution abstraction competing with modes. Consumer projects would need to learn both modes and actions, and the boundary between them would be ambiguous. The mode schema already has steps, dependencies, and error strategies — guards and converge are natural extensions of the same concept.", }, { option = "Executable preconditions — make existing preconditions[] run commands instead of being text", why_rejected = "Preconditions serve a different purpose: they document what the human should verify. Making them executable would lose the documentation function. Guards are a separate concept: machine-checked pre-flight blocks. Both can coexist — preconditions for humans, guards for machines.", }, { option = "External convergence via Vapora workflows — let the orchestrator handle iteration", why_rejected = "Convergence is a property of the mode itself, not of the orchestrator. sync-ontology should declare that it iterates until zero drift regardless of whether it runs via CLI, Vapora, or CI. Pushing this to the orchestrator means every orchestrator must know which modes need iteration and under what conditions — that knowledge belongs in the mode contract.", }, ], constraints = [ { id = "guard-schema-present", claim = "reflection/schema.ncl exports a Guard type with id, cmd, reason, and severity fields", scope = "reflection/schema.ncl", severity = 'Hard, rationale = "The Guard type is the contract that all mode consumers rely on. Removing or renaming its fields breaks all guarded modes and the executor.", check = { tag = 'Grep, pattern = "Guard.*=.*_Guard", paths = ["reflection/schema.ncl"], must_be_empty = false, }, }, { id = "converge-schema-present", claim = "reflection/schema.ncl exports a Converge type with condition, max_iterations, and strategy fields", scope = "reflection/schema.ncl", severity = 'Hard, rationale = "The Converge type is the contract for iterative modes. Removing it breaks the convergence loop in the executor.", check = { tag = 'Grep, pattern = "Converge.*=.*_Converge", paths = ["reflection/schema.ncl"], must_be_empty = false, }, }, { id = "executor-guards-implemented", claim = "The mode executor in reflection/nulib/modes.nu evaluates guards before executing steps", scope = "reflection/nulib/modes.nu", severity = 'Hard, rationale = "Guards without executor support are declarations without effect. The executor must run each guard cmd and abort on Block failures.", check = { tag = 'Grep, pattern = "Guards.*Active Partner", paths = ["reflection/nulib/modes.nu"], must_be_empty = false, }, }, ], ontology_check = { decision_string = "Extend mode schema with guards and converge", invariants_at_risk = ["protocol-not-runtime"], verdict = 'RequiresJustification, }, invariant_justification = { invariant = "protocol-not-runtime", claim = "Guards and converge are declarative NCL fields — the protocol defines what to check and when to iterate, not how to execute. The executor in modes.nu is tooling, not protocol. A consumer project could implement its own executor that reads the same guards and converge declarations.", mitigation = "The schema (reflection/schema.ncl) is protocol. The executor (reflection/nulib/modes.nu) is tooling. Both are clearly separated. A project can use the schema without the executor.", }, related_adrs = ["adr-002"], }