ontoref/adrs/adr-011-mode-guards-and-convergence.ncl
Jesús Pérez 13b03d6edf
Some checks failed
Nickel Type Check / Nickel Type Checking (push) Has been cancelled
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
feat: mode guards, convergence, manifest coverage, doc authoring pattern
## Mode guards and convergence loops (ADR-011)

  - `Guard` and `Converge` types added to `reflection/schema.ncl` and
    `reflection/defaults.ncl`. Guards run pre-flight checks (Block/Warn);
    converge loops iterate until a condition is met (RetryFailed/RetryAll).
  - `sync-ontology.ncl`: 3 guards + converge (zero-drift condition, max 2 iter).
  - `coder-workflow.ncl`: guard (coder-dir-exists) + `novelty-check` step.
  - Rust types in `ontoref-reflection/src/mode.rs`; executor in `executor.rs`
    evaluates guards before steps and convergence loop after.
  - `adrs/adr-011-mode-guards-and-convergence.ncl` added.

  ## Manifest capability completeness

  - `.ontology/manifest.ncl`: 3 → 19 declared capabilities covering the full
    action surface (daemon API, modes, Task Composer, QA, bookmarks, etc.).
  - `sync.nu`: `audit-manifest-coverage` + `sync manifest-check` command.
  - `validate-project.ncl`: 6th category `manifest-cov`.
  - Pre-commit hook `manifest-coverage` added.
  - Migrations `0010-manifest-capability-completeness`,
    `0011-manifest-coverage-hooks`.

  ## Rust doc authoring pattern — canonical `///` convention

  - `#[onto_api]`: `description = "..."` optional when `///` doc comment exists
    above handler — first line used as fallback. `#[derive(OntologyNode)]` same.
  - `ontoref-daemon/src/api.rs`: 42 handlers migrated to `///` doc comments;
    `description = "..."` removed from all `#[onto_api]` blocks.
  - `sync diff --docs --fail-on-drift`: exits 1 on crate `//!` drift; used by
    new `docs-drift` pre-commit hook. `docs-links` hook checks rustdoc broken links.
  - `generator.nu`: mdBook `crates/` chapter — per-crate page from `//!` doc,
    coverage badge, feature flags, implementing practice nodes.
  - `.claude/CLAUDE.md`: `### Documentation Authoring (Rust)` section added.
  - Migration `0012-rust-doc-authoring-pattern`.

  ## OntologyNode derive fixes

  - `#[derive(OntologyNode)]`: `name` and `paths` attributes supported; `///`
    doc fallback for `description`; `artifact_paths` correctly populated.
  - `Core::from_value` calls `merge_contributors()` behind `#[cfg(feature = "derive")]`.

  ## Bug fixes

  - `sync.nu` drift check: exact crate path match (not `str starts-with`);
    first-path-only rule; split on `. ` not `.` to avoid `.ontology/` truncation.
  - `find-unclaimed-artifacts`: fixed absolute vs relative path comparison.
  - Rustdoc broken intra-doc links fixed across all three crates.
  - `ci-docs` recipe now sets `RUSTDOCFLAGS` and actually fails on errors.

  mode guards/converge, manifest coverage validation, 19 capabilities (ADR-011)

  Extend the mode schema with Guard (pre-flight Block/Warn checks) and Converge
  (RetryFailed/RetryAll post-execution loops) — protocol pushes back on invalid
  state and iterates until convergence. ADR-011 records the decision to extend
  modes rather than create a separate action subsystem.

  Manifest expanded from 3 to 19 capabilities covering the full action surface
  (compose, plans, backlog graduation, notifications, coder pipeline, forms,
  templates, drift, quick actions, migrations, config, onboarding). New
  audit-manifest-coverage validator + pre-commit hook + SessionStart hook
  ensure agents always see complete project self-description.

  Bug fix: find-unclaimed-artifacts absolute vs relative path comparison —
  19 phantom MISSING items resolved. Health 43% → 100%.

  Anti-slop: coder novelty-check step (Jaccard overlap against published+QA)
  inserted between triage and publish in coder-workflow.

  Justfile restructured into 5 modules (build/test/dev/ci/assets).
  Migrations 0010-0011 propagate requirements to consumer projects.
2026-03-30 19:08:25 +01:00

114 lines
8.0 KiB
Plaintext

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"],
}