Skip to content

Instantly share code, notes, and snippets.

@anon987654321
Created May 24, 2026 16:26
Show Gist options
  • Select an option

  • Save anon987654321/cef4889e36378f4afdcaddf013f6c285 to your computer and use it in GitHub Desktop.

Select an option

Save anon987654321/cef4889e36378f4afdcaddf013f6c285 to your computer and use it in GitHub Desktop.
MASTER 2026-05-24

MASTER Snapshot — 2026-05-24T16:26:08Z

Tree

CONVENTIONS.md
DEPLOY/
  openbsd/
    etc/
    rc.d/
Gemfile
QUICKSTART.md
README.md
Rakefile
bin/
completions/
data/
  CANON.md
  agent_taxonomy.yml
  agents/
    brgen_amber_completion.yml
  architectures.yml
  attention_context.yml
  budget.yml
  claude/
    MEMORY.md
    feedback_autofix.md
    feedback_autoproceed.md
    feedback_comments_reassess.md
    feedback_continue_backlog.md
    feedback_decisive_signals.md
    feedback_device_limits.md
    feedback_diverged_branch_sync.md
    feedback_flat_pixels.md
    feedback_git_commits.md
    feedback_html_css_style.md
    feedback_importance_order.md
    feedback_lint_beautify.md
    feedback_master_prompt_aesthetic.md
    feedback_master_zsh_discipline.md
    feedback_meta_framing.md
    feedback_micro_refinements.md
    feedback_motion_color_grading.md
    feedback_no_consecutive_whitespace.md
    feedback_no_new_files.md
    feedback_no_permission_questions.md
    feedback_no_python.md
    feedback_no_sed.md
    feedback_no_shell_piping.md
    feedback_no_useless_knobs.md
    feedback_proper_casing.md
    feedback_readme_autoupdate.md
    feedback_restart_rails.md
    feedback_run_through_master_triad.md
    feedback_strunk_white.md
    feedback_style.md
    feedback_universal_cross_disciplinary_rules.md
    feedback_voice_terse_unix.md
    project_defrag_plan_2026_05.md
    project_falcon_em_subprocess.md
    project_master.md
    project_master_dual_gemfile.md
    project_master_seven_module_refactor.md
    project_master_yml_json_authority.md
    reference_grok_ui_cli_patterns.md
    reference_opencrabs.md
    user_architect_aesthetics.md
  closings.yml
  council.yml
  design_rules.yml
  epistemics.yml
  exemplars.yml
  gems.yml
  heartbeat.yml
  injection_patterns.yml
  llm_operators.yml
  load.yml
  mcp_servers.yml
  mobile_web_opportunities.yml
  models.yml
  openbsd.yml
  patterns.yml
  personas.yml
  principles/
    feedback_autofix.md
    feedback_autoproceed.md
    feedback_comments_reassess.md
    feedback_continue_backlog.md
    feedback_decisive_signals.md
    feedback_device_limits.md
    feedback_diverged_branch_sync.md
    feedback_flat_pixels.md
    feedback_git_commits.md
    feedback_html_css_style.md
    feedback_importance_order.md
    feedback_lint_beautify.md
    feedback_master_prompt_aesthetic.md
    feedback_master_zsh_discipline.md
    feedback_meta_framing.md
    feedback_micro_refinements.md
    feedback_motion_color_grading.md
    feedback_no_consecutive_whitespace.md
    feedback_no_new_files.md
    feedback_no_permission_questions.md
    feedback_no_python.md
    feedback_no_sed.md
    feedback_no_shell_piping.md
    feedback_no_useless_knobs.md
    feedback_proper_casing.md
    feedback_readme_autoupdate.md
    feedback_restart_rails.md
    feedback_run_through_master_triad.md
    feedback_strunk_white.md
    feedback_style.md
    feedback_universal_cross_disciplinary_rules.md
    feedback_voice_terse_unix.md
  prompts/
    council.yml
    mode_code_agent.yml
    mode_direct.yml
    mode_react.yml
    mode_rewoo.yml
    original_prompts.md
  providers.yml
  rails.yml
  refusal_templates.yml
  ruby_style.yml
  rule_deps.yml
  rules.yml
  soul.yml
  stale_namespaces.yml
  standing_orders.yml
  templates.yml
  tools.yml
  topologies.yml
  traces/
  ui.yml
  violation_priors.yml
  visual_clusters.yml
  vocabulary.yml
  web/
  why_command.yml
  workflow.yml
  zsh.yml
docs/
  cleanup_and_trace.md
  cognitive_runtime.md
  collaboration_protocol.md
  event_naming.md
  face3d_engine.md
  face3d_runtime_hardening.md
  grok_bug_report_may_2026.md
  non_negotiable_runtime_rules.md
  platform_topology.md
  provider_economy.md
  repo_ecology.md
  runtime_ui_direction.md
lib/
  builder.rb
  design/
    mobile_first_pwa_profiles.rb
    platform_profiles.rb
  ground/
    agent_lifecycle.rb
    atomic_write.rb
    attention_context.rb
    axioms/
      rails_doctrine.rb
      ux_heuristics.rb
      wcag.rb
    brain_overlay.rb
    brutalist_minimalism.rb
    checkpoint.rb
    config.rb
    constitution.rb
    context_provider.rb
    done_checker.rb
    evidence_base.rb
    frontmatter.rb
    intent_router.rb
    knowledge_store.rb
    memory.rb
    memory_index.rb
    memory_search.rb
    orchestration_policy.rb
    orders/
      architecture_audit.rb
      autocommit.rb
      backup.rb
      base.rb
      constitution_drift.rb
      registry.rb
      restart_master.rb
    patch_verifier.rb
    persistence/
      sqlite_findings.rb
      sqlite_memory.rb
      sqlite_store.rb
    phase_gates.rb
    pledge.rb
    provider_registry.rb
    repo_map.rb
    repo_mining/
      mobile_web_cluster_catalog.rb
    rules.rb
    runtime_registry.rb
    sandbox_policy.rb
    standing_orders.rb
    subagent_policy.rb
    swallow.rb
    tool_approval_policy.rb
    tool_contract.rb
    tool_protocol.rb
    type_checker.rb
    unfinished_ledger.rb
    unified_diff_editor.rb
    workflow_policy.rb
  judge/
    agent.rb
    agent_pool.rb
    ast_signature.rb
    code_index.rb
    commit_guard.rb
    council/
      critique.rb
      deliberation.rb
      ideation.rb
      personas.rb
    embeddings.rb
    llm_dispatcher.rb
    modes.rb
    reference_graph.rb
    reflexion.rb
    repo_ecology.rb
    repo_map.rb
    scan/
      ast_fixer.rb
      datalog_engine.rb
      detection_pipeline.rb
      finding.rb
      rule.rb
      rule_dsl.rb
      rules/
        adversarial_rule.rb
        ast_omission_rule.rb
        co_change_coupling_rule.rb
        comment_drift_rule.rb
        interconnect_rule.rb
        js_rules.rb
        lexical_rules.rb
        reek_rule.rb
        rubocop_rule.rb
        ruby_rules.rb
        rule_coverage_rule.rb
        semantic_rule.rb
        universal_rules.rb
        web_rules.rb
      scanner.rb
      unit_segmenter.rb
    schema_index.rb
    security/
      injection_guard.rb
      permissions.rb
    swarm/
      coordinator.rb
      worker.rb
      workers/
        analyst.rb
        coder.rb
        researcher.rb
        reviewer.rb
  loop/
    constants.rb
    crdt_loop.rb
    cybernetics.rb
    diff_stager.rb
    fix_helpers.rb
    fix_loop.rb
    fix_pipeline.rb
    governor.rb
    heartbeat.rb
    homeostat.rb
    patch_applier.rb
    propose_tree.rb
    repair/
      git_history_miner.rb
    rule_loop.rb
    watch_loop.rb
    watcher.rb
  master.rb
  now/
    cli/
      command_ops.rb
      signals.rb
      thinking_indicator.rb
    cli.rb
    command_registry/
      memory_commands.rb
      system_commands.rb
      tool_commands.rb
      work_commands.rb
    command_registry.rb
    context_window.rb
    hot_reload.rb
    orchestration/
      event_sequence_orchestrator.rb
    pipeline.rb
    pipeline_context.rb
    propose.rb
    routing/
      model_router.rb
      provider_health.rb
      provider_quarantine_manager.rb
    skills.rb
    stages/
      council.rb
      deliberate.rb
      enhance.rb
      execute.rb
      guard.rb
      infer.rb
      intake.rb
      lint.rb
      memory.rb
      prune.rb
      render.rb
      review.rb
      route.rb
  pressure_engine.rb
  rails/
    face3d_runtime_policy.rb
    hotwire_refactor_policy.rb
    mobile_pwa_operator.rb
    pwa_audit.rb
    rails8_app_audit.rb
  reach/
    ask_llm.rb
    ast_edit.rb
    base.rb
    batch_replace.rb
    bedrock_stub.rb
    circuit_breaker.rb
    circuit_breaker_registry.rb
    clean.rb
    feedback_record.rb
    gateway.rb
    git_context.rb
    git_operations.rb
    list_dir.rb
    llm.rb
    mcp_coordinator.rb
    memory_record.rb
    path_guard.rb
    read_file.rb
    ruby_llm_patch.rb
    search_files.rb
    search_knowledge.rb
    semantic_cache.rb
    shell.rb
    str_replace.rb
    symbol_lookup.rb
    text_hygiene.rb
    tree.rb
    web_fetch.rb
    web_search.rb
    whitespace_normalizer.rb
    write_file.rb
  result.rb
  trace/
    audit_log.rb
    broadcaster.rb
    diag.rb
    event_bus.rb
    event_log.rb
    logging.rb
    memory_tier_compactor.rb
    metrics.rb
    recorder.rb
    ring_buffer.rb
    self_map.rb
    session.rb
    swallow_ledger.rb
    telemetry.rb
    triggers.rb
    undo.rb
    why_explainer.rb
  unwrap_error.rb
  voice/
    dilla.rb
    ffmpeg_lofi.rb
    personality.rb
    production_dna.rb
    renderer.rb
    sonitex.rb
    sonitex_sox.rb
    soul.rb
    speech.rb
    tts_lofi.rb
master.gemspec
runtime/
  constitution_drift.json
  e2e_probe.txt
  events/
  improvements.md
test/
  fixtures_bare_rescue.rb
  support/
    master_container.rb
  test_adversarial_rule.rb
  test_agent.rb
  test_agent_escalation.rb
  test_ast_omission_rule.rb
  test_bare_rescue_rule.rb
  test_browser.rb
  test_cli.rb
  test_co_change_coupling_rule.rb
  test_council_deliberation.rb
  test_face3d_runtime_policy.rb
  test_helper.rb
  test_learnings.rb
  test_master_container.rb
  test_pipeline.rb
  test_prune.rb
  test_result.rb
  test_ring_buffer.rb
  test_rules.rb
  test_runtime_hardening.rb
  test_silent_rescue_rule.rb
  test_speech.rb
  test_swallow_ledger.rb
  test_web_http.rb
  test_web_ui.rb
  test_yaml_registries.rb
tools/
  postpro/
    README.md
  postpro.rb
  repligen/
    README.md
  repligen.rb
web/
  Gemfile
  README.md
  Rakefile
  app/
    assets/
      images/
      javascripts/
        app.js
        chat.js
      stylesheets/
    channels/
      application_cable/
        channel.rb
        connection.rb
      master_channel.rb
    controllers/
      application_controller.rb
      canvas_controller.rb
      chat_controller.rb
      concerns/
      dashboard_controller.rb
      events_controller.rb
      health_controller.rb
    helpers/
      application_helper.rb
    middleware/
      auth_tier.rb
    models/
      application_record.rb
      concerns/
    views/
      chat/
        index.html.erb
      dashboard/
        index.html.erb
      layouts/
        application.html.erb
      pwa/
        manifest.json.erb
  bin/
  config/
    application.rb
    boot.rb
    cable.yml
    ci.rb
    database.yml
    environment.rb
    environments/
      development.rb
      production.rb
      test.rb
    initializers/
      assets.rb
      content_security_policy.rb
      filter_parameter_logging.rb
      inflections.rb
      master_container.rb
      new_framework_defaults_8_0.rb
    locales/
      en.yml
    puma.rb
    routes.rb
  db/
    seeds.rb
  face.js
  index.html.erb
  lib/
    tasks/
  public/
    chat.js
    cluster_miner.js
    codebase.js
    cognition_ecology.js
    face.js
    face3d_engine.js
    face3d_preview.js
    face3d_renderer.js
    manifest.json
    mask.js
    particle_kernel.js
    robots.txt
    sw.js
    topology_registry.js
    vad-processor.js
    visual_bridge.js
  script/

CONVENTIONS.md

# MASTER — Conventions for External LLMs

Context injection for any LLM reviewing or editing MASTER. Read before touching code.

The complete rule corpus — every axiom, scan rule, operator principle, and
external lineage — is indexed in `data/CANON.md`. Read it first, every session.
This file is the orientation; CANON.md is the directory.

## Identity

MASTER is a constitutional AI coding agent written in Ruby 3.3+ on OpenBSD 7.8. It replaces Claude Code CLI for its operator. It is general-purpose and language-agnostic. Every change leaves the system in a working, deployable state.

## Golden rule

`PRESERVE_THEN_IMPROVE_NEVER_BREAK`. Read before write. Patch minimally. Understand before touching — Chesterton's Fence.

## Anti-simulation

Never state intent without evidence. Forbidden hedges — `will`, `would`, `could`, `might`. Require:
- File read → content with SHA-256
- Modification → unified diff
- Completion → command output

## Communication — two registers, do not mix

- **MASTER's own log/event lines** (boot banner, scheduler ticks, tool events, dmesg-style status): structured, terse, lowercase, kernel-ish — `master@host ready`, `boot0: 26ms`, `model0 at openrouter`. The OpenBSD-dmesg boot banner is sacred — never strip it.
- **Conversational replies to the operator**: plain English, proper casing, full sentences. No dmesg style here. No headlines, no empty bullets, no filler, no sycophancy, no hedging. Outcome first, evidence next, implementation last.
- **Commits and log lines** stay active, concrete, terse — Strunk & White, omit needless words.

## No ASCII line art

Never use these as decorations in any output (comments, log lines, CLI text, chat replies, commit messages):

- `===`, `----` (banner lines, section dividers)
- ``, `|`, ``, `` (bullet/separator characters)
- `[ok]`, `[err]`, `[skip]` brackets — use bare prefixes `ok:`, `err:`, `skip:`, `warn:` instead

In Markdown documents, plain `---` for an `<hr>` and table separators are fine — they carry meaning. Banner art does not.

## Code rules (enforced by scan)

- **Read before write** — every affected file before any edit.
- **No bare rescue** — always `rescue SpecificError => e`. Inline `expr rescue nil` is fine when nil is intentional.
- **Named constants** — extract literals with `.freeze`.
- **No magic numbers** — thresholds belong in `data/rules.yml` under `thresholds:`.
- **No abbreviations**`index` not `idx`, `signature` not `sig`, `temporary_path` not `tmp`.
- **No regex when string methods suffice**`start_with?`, `include?`, `end_with?`.
- **Outsource to gems** — if it exists and works, use it.
- **Endless methods** — single-expression methods use `def foo = expr`.
- **Result monad** — check with `respond_to?(:ok?)`, not `is_a?(Result)`. Unwrap with `.value!` only after `.ok?` is true; on an `Err` it raises.
- **No flag arguments** — a boolean that selects behavior is two methods in one.
- **Guard clauses first**`return Result.ok(ctx) unless condition` before main logic.
- **Dependency injection** — never instantiate collaborators inside a method.
- **CQS** — queries return, commands mutate. Not both.

## Thresholds

- File — 300 lines max, warn at 200
- Method — 10 lines ideal, 7 warn
- Class — 6 public methods, 3 ivars, 200 lines
- Params — 3 positional max; keyword args for 3+
- Nesting — 2 levels max inside a method

## Ruby style

- `# frozen_string_literal: true` on every `.rb`
- Double-quoted strings always; single only inside regex or `'\1'` backrefs
- One-line comments. No YARD blocks, no section separators
- Comments explain WHY, never WHAT
- `snake_case` throughout
- Zeitwerk autoloading — file name matches class name

## Rails view style

- Prefer Rails tag helpers for dynamic or localized markup: `tag.p t("key")`, `tag.section class: state_class`, `tag.meta name: "viewport", content: "width=device-width,initial-scale=1"`.
- Prefer `tag.*` blocks for semantic containers with dynamic attributes, Turbo data, ARIA, or localized content.
- Keep plain HTML when markup is static and clearer as HTML. Do not turn views into helper soup.
- Never hardcode user-facing text in reusable views when `t("key", default: "text")` is practical.
- Prefer semantic tags and bare CSS targeting: `nav a`, `main section`, `article header`. Do not add class attributes where a tag selector works.
- Use Rails form, link, button, Turbo, and tag helpers instead of hand-rolled HTML when the helper carries routing, CSRF, method, data, or escaping semantics.

Bugs to avoid:
- `Dir.chdir` — process-wide, thread-unsafe. Use `File.expand_path`.
- `Prism.parse(src, freeze: true)``freeze:` dropped in 3.4. Use `Prism.parse(src)`.
- `next if` inside `flat_map` — returns `nil`. Use `next [] if`.
- Backtick shell with interpolation — use `Open3.capture2e(*%w[cmd], arg)`.

## Zsh / shell

Banned in zsh and SSH: `sed`, `awk`, `tr`, `grep`, `cut`, `head`, `tail`, `find`, `wc`, `sudo`, `perl`, `ruby`, `dd`, `xargs`. Use zsh builtins, parameter expansion, `doas` for privilege, Ruby scripts for complex logic.

Read files over SSH with `cat path` — read the whole file once. Do not stitch `grep` + `head` fragments; reasoning from full context beats reasoning from snippets. For local zsh array work use `lines=("${(@f)$(<file)}")`.

## Architecture

Pipeline: `Intake → Infer → Route → Guard → Execute → [Council ‖ Lint] → Prune → Memo → Render`. Council and Lint run concurrently under a 30s deadline via `ParallelGroup`. Rollback on `axiom_violation` or `validation`: `git reset --hard HEAD`. Scan rules auto-register via the `Rule.inherited` callback — every file under `scan/rules/` must subclass `Rule` or it goes silently unrun. Rules with no constructor args set `def auto_build? = true` to opt into the registry's zero-arg construction path. `axiom_coverage_rule` walks `scan/rules/*.rb` with a Prism `SuperclassFinder` and flags any file whose top-level class does not inherit from `Rule`, so silent registry drift is caught at scan time. All rules ship with `@auto_fix = true` and participate in sweep. Sweep runs rubocop autocorrect first, then escalates to LLM rewrite under the corruption guards.

Council deliberation samples a focus question per persona per turn from `data/council_questions.yml` (8 categories — assumptions, failure_modes, attacker, edge_cases, degradation, ops_maint, economics, clarity). Architect → assumptions, Skeptic → failure_modes, Security → attacker, User → edge_cases, Pragmatist → economics, Mentor → clarity. Unmapped personas pass through with no question.

Observability: `Master::Telemetry` is a soft-optional OpenTelemetry tracer that emits JSONL spans to `.master/traces.log`. Wraps `EventBus#publish`, `Metrics#append`, `AuditLog#append`, and `Heartbeat#execute_job`. Bootstrap fires in `Master.boot` between Pledge stage1 and stage2.

Key files — `data/soul.yml` (golden rule, tiers, persona), `data/rules.yml` (structural rules, thresholds, depths), `data/ruby_style.yml` (style and bugs), `data/workflow.yml` (READ_BEFORE_WRITE, scan principles), `data/standing_orders.yml` (current FSM state).

## Running scans

Standard: `eval "$(grep '^export' ~/.zshrc)" && cd ~/pub4/MASTER && echo "/scan lib/" | bundle exec ruby bin/cli`. Autofix sweep: `/autoloop 20`. Do not use external agents when MASTER can scan itself. Depth knobs are gone — every scan is full by default.

Pre-commit constitution check: `bin/audit` runs the scanner over staged files and fails the commit on any kernel-tier rule or critical/error violation. Wire as a git pre-commit hook by writing `exec bin/audit` into `.git/hooks/pre-commit`.

## Protection tiers

ABSOLUTE aborts the pipeline. PROTECTED emits a warning and continues. NEGOTIABLE allows if explicitly permitted. FLEXIBLE negotiates at runtime. ABSOLUTE sections in `data/soul.yml` require `/override` to amend.

## Environment

VPS: `dev@brgen.no` · OpenBSD 7.8 · passwordless `doas`. SSH credentials live in the operator's environment, never in versioned docs. Non-interactive SSH must not source `.zshrc` — load env only: `eval "$(grep '^export' ~/.zshrc)"`.

Edit VPS files by direct edit + `scp` — write the new file content locally, scp it up. Reserve `~/pub4/tmp/patch.rb` for genuinely script-shaped edits where a patch script is the right tool. Never use `ruby -i` with heredoc — empties the file on script error.

After every scp under `MASTER/web/`, immediately `doas rcctl restart master` so Falcon picks up the change. Falcon does not hot-reload in production; without the restart the deployed app keeps serving the prior bytecode.

## Web auth tiers

Token in `~/pub4/.master/config.yml` is accepted via `Authorization: Bearer`, `X-Token` header, or `master_session` cookie. First-hit `?token=...` triggers the handshake: AuthTier sets an `HttpOnly; Secure; SameSite=Strict` cookie and 302s to the same path with the token stripped, keeping the secret out of logs and history. No credential = visitor — chat works, but `Thread.current[:master_visitor]` is set so `Master::Agent::LlmDispatch#build_llm_tools` filters tools to the visitor allow-list (currently `AskLlm`, `WebSearch`). The CLI REPL bypasses this entirely and always has full access.

## Slash commands

`/scan [profile] [path]`, `/fix [path]`, `/ecology [path]`, `/review [on|off|path]`, `/critique <file|text>`, `/swarm <role> <task>`, `/ideate <prompt>`, `/topic`, `/rsi [stats]`, `/model [list|<id>]`, `/why <rule>`, `/diag [section]`, `/snapshot`, `/tts`, `/brief`, `/heartbeat`, `/orders`, `/soul`, `/dmesg`. `/scan` reports violations (read-only). `/fix` iterates scan→LLM-rewrite→scan until violations plateau, stall, or reach zero — stops on diminishing returns automatically. `/review` runs council deliberation. `/snapshot` publishes two GitHub gists — MASTER + DEPLOY. `/why` resolves locally first; LLM fires only on a miss.

Gemfile

# frozen_string_literal: true

source "https://rubygems.org"

gem "fiddle"

gem "ruby_llm", "~> 1.3"
gem "tty-prompt", "~> 0.23"
gem "tty-reader", "~> 0.9"
gem "tty-spinner", "~> 0.9"
gem "tty-markdown", "~> 0.7"
gem "tty-table", "~> 0.12"
gem "tty-screen", "~> 0.8"
gem "tty-box", "~> 0.7"
gem "tty-command", "~> 0.10"
gem "tty-tree", "~> 0.4"
gem "tty-config", "~> 0.6"
gem "tty-logger", "~> 0.6"
gem "tty-progressbar", "~> 0.18"
gem "pastel", "~> 0.8"
gem "rouge", "~> 4.4"
gem "diffy", "~> 3.4"
gem "zeitwerk", "~> 2.7"
gem "sinatra", "~> 4.0"
gem "sinatra-contrib", "~> 4.0"
gem "rb-edge-tts", git: "https://github.com/ZPVIP/rb-edge-tts"

group :test do
  gem "minitest", ">= 5.25"
  gem "rack-test", "~> 2.1"
  gem "ferrum", "~> 0.15"
  gem "simplecov", require: false
end
gem "ruby_llm-mcp"
gem "rubocop", "~> 1.60", require: false
gem "reek", "~> 6.4", require: false
gem "flay", require: false
gem "opentelemetry-sdk", "~> 1.11", require: false

gem "sqlite3", "~> 2.9"

# Architecture #7: file-watcher reactive trigger (kqueue on OpenBSD, inotify on Linux)
# Skipped on Android/Termux — inotify gem unavailable there.
if RUBY_PLATFORM =~ /bsd|dragonfly/i
  gem "rb-kqueue", "~> 0.2", require: false
elsif RUBY_PLATFORM =~ /linux/ && !RUBY_PLATFORM.include?("android")
  gem "rb-inotify", "~> 0.10", require: false
end

QUICKSTART.md

# MASTER Quickstart (External LLMs)

MASTER is a constitutional coding agent in Ruby. Read this first, then run `/orient` for full doctrine.

1) Golden rule
- Preserve, then improve, never break.
- Read full files before editing.
- Keep patches minimal and reversible.

2) Non-negotiables
- No fabricated claims; show evidence from files/commands.
- No bare `rescue`; rescue specific exceptions.
- Prefer named constants over magic literals.
- Use string methods before regex when possible.
- Dependency-inject collaborators; avoid hidden instantiation.

3) Style baseline
- `# frozen_string_literal: true` in Ruby files.
- Double-quoted strings.
- Guard clauses first.
- Endless method style for single expressions.
- Clear names; avoid abbreviations like `idx`, `tmp`, `sig`.

4) How MASTER works
- Pipeline: Intake → Infer → Route → Guard → Execute → Council/Lint → Prune → Memo → Render.
- Scans enforce structure/style rules from `data/rules.yml` and `data/ruby_style.yml`.
- Fixes are applied through FixLoop and must remain safe and auditable.

5) Core commands
- `/scan [profile] [path]` check a file/dir.
- `/fix [path]` apply fixes.
- `/diag` runtime snapshot.
- `/why <rule>` explain one rule.
- `/help` command catalog.

6) Web auth model
- Token-authenticated operator gets full tools.
- Visitor mode is restricted to safe tools.

7) If uncertain
- Ask for the specific rule section instead of guessing.
- Prefer explicit tradeoffs and smallest safe change.

Full reference: `CONVENTIONS.md` and `/orient`.

README.md

# MASTER

Constitutional AI runtime for any text artifact — code, prose, design, structure. Ruby. OpenBSD. Self-hosting.

Models propose. The constitution validates. Convergence loops digest violations. Memory learns what fixes stick. Pressure fields track epistemic health. Providers compete by capability, cost, and evidence.

## Quickstart

```sh
cd MASTER
bundle install
bundle exec ruby bin/cli

Pipe input for one-shot mode. The web face starts on port 53187 behind relayd at https://ai.brgen.no.

Deploy: doas zsh DEPLOY/openbsd/openbsd.sh

Architecture

Four layers:

  1. Brain — declarative constitution, standing orders, roles, memory policy, provider routing, governance.
  2. Runtime — append-only events, telemetry, checkpoints, replay state, queues, locks, provider health, hot cache.
  3. Orchestration — routing, voting, fallback, quorum, workflow execution, tool contracts, convergence loops.
  4. Interface — CLI, web face, canvas, dashboard, traces, graph, timelines.

Eleven-stage turn pipeline: Intake → Enhance → Infer → Route → Guard → Execute → [Council ‖ Lint] → Prune → Memo → Render. Enhance rewrites user input for clarity and intent density with y/n approval in the web UI. Council and Lint run concurrently with a 30 s timeout.

Convergence loop architectures

15 architectures across loop/ — 14 implemented, 1 concept:

# Name Status
1 Priority queue over round-robin implemented
2 Rule dependency graph (topological sort) implemented
3 File-first convergence strategy concept
4 Deterministic AST autofixes (Prism) implemented
5 Unified diff output for large files implemented
6 Council deliberation for severity:error implemented
7 Reactive file watcher (kqueue/inotify) implemented
8 Staged dataflow pipeline Detect→Apply implemented
9 Genetic fix candidate selection implemented
10 Reinforcement learning fix quality implemented
11 Constitution as type system on AST IR implemented
12 Datalog/Prolog rule engine implemented
13 CRDT-based distributed convergence implemented
14 Hierarchical Bayesian violation priors implemented
15 Codebase as embodied particle topology implemented

Operating law

  • Agents do not directly mutate durable state.
  • Tools declare contracts before execution.
  • Every action emits before/after events.
  • Provider calls pass through routing policy.
  • Telemetry is append-only JSONL.
  • Memory has explicit lifecycle tiers.
  • Rollback beats explanation.
  • Replay beats trust.

Repair

observe → classify → propose → sandbox → validate → merge

Failures become data. Data becomes playbooks. Playbooks become safer defaults.

Configuration

Key Default Description
model openrouter/auto Default provider model
budget_max 10.0 Max spend per session (USD)
req_max 1.0 Max spend per request (USD)
reasoning_mode direct direct or chain
auto false Autoloop enabled
trace 0 Trace verbosity (0–3)
cache_ttl 3600 Cache TTL in seconds

Config lives at .master/config.yml. Override any key at runtime with /config key value.

Web auth

Tier Trigger Access
Authenticated Authorization: Bearer, X-Token, master_session cookie Full — filesystem, git, all tools
Visitor no credential LLM chat only (AskLlm, WebSearch)
Public /up, /health Always

First-hit ?token=… is accepted once; the middleware sets an HttpOnly; Secure; SameSite=Strict cookie and 302s to the same path stripped of the token. After the handshake, the URL never carries the secret — query strings leak through proxy logs, browser history, and Referer headers; cookies do not.

Modules

now · loop · judge · voice · ground · reach · trace

Constitution lives in data/. Runtime state in .master/. Knowledge store at .master/knowledge.sqlite3.

Troubleshooting

Bundler 403 on install: proxy is blocking rubygems.org. Check gem sources --list and env | grep -i proxy. Install a single gem to isolate: gem install zeitwerk -v 2.7.5.

MIT.


## `Rakefile`
```text
# frozen_string_literal: true

require "rake/testtask"

Rake::TestTask.new(:test) do |t|
  t.libs << "test"
  t.libs << "lib"
  t.test_files = FileList["test/test_*.rb"]
  t.warning = false
end

desc "Deep scan lib/ — exit 1 if any violations found (static rules, no LLM)"
task :constitution do
  $LOAD_PATH.unshift(File.join(__dir__, "lib"))
  require "master"

  root    = __dir__
  scanner = Master::Scan::Scanner.new
  Master::Scan::Rule.registry.select(&:auto_build?).each { |klass| scanner.add_rule(klass.new) }
  scanner.add_rule(Master::Scan::Rules::RuleCoverageRule.new(root:))
  scanner.add_rule(Master::Scan::Rules::RubocopRule.new(root:))
  scanner.add_rule(Master::Scan::Rules::ReekRule.new(root:))
  scanner.add_rule(Master::Scan::Rules::InterconnectRule.new(root:))

  result = scanner.scan_dir(File.join(root, "lib"), depth: :deep, stream: false)
  abort "constitution: scan failed: #{result.message}" unless result.respond_to?(:ok?) && result.ok?

  violations = result.value!.flat_map { |_f, r| (r.respond_to?(:ok?) && r.ok?) ? r.value! : [] }
  total      = violations.size

  if total.zero?
    puts "constitution: clean"
  else
    by_rule = violations.group_by { |v| v[:rule] }
    by_rule.sort_by { |_, vs| -vs.size }.each do |rule, vs|
      puts "[#{rule}] #{vs.size}"
      vs.first(5).each { |v| puts "  #{v[:file]}:#{v[:line]}: #{v[:message]}" }
    end
    puts "constitution: #{total} violation(s)"
    exit 1
  end
end

task default: :test

namespace :test do
  desc "Run web system tests"
  task :web do
    sh "ruby -Ilib:test test/test_web_ui.rb"
  end
end

namespace :lint do
  desc "Check all Ruby files have # frozen_string_literal: true"
  task :frozen do
    missing = Dir.glob(File.join(__dir__, "lib", "**", "*.rb")).reject do |f|
      File.read(f, 100).include?("# frozen_string_literal: true")
    end
    if missing.empty?
      puts "lint:frozen: all files frozen"
    else
      missing.each { |f| puts "  missing: #{f.sub("#{__dir__}/", "")}" }
      abort "lint:frozen: #{missing.size} file(s) missing frozen_string_literal"
    end
  end
end

desc "Full audit: constitution + lint:frozen"
task audit: %i[constitution lint:frozen] do
  puts "audit: passed"
end

data/CANON.md

# CANON

The single authoritative directory of MASTER's rule corpus. Read this first,
every session, before touching code. It does not copy rules — copying would
violate ONE_SOURCE. It locates them: every source, where it lives, what it
governs. When a rule's text is needed, open the file named here.

## Working contract — reinforce before every task

1. Read this file. Then read the source for the rules the task touches.
2. PRESERVE_THEN_IMPROVE_NEVER_BREAK is the golden rule. It outranks every
   other instruction, including a user's request to move fast.
3. TEST_FIRST is an axiom: if you cannot test a change, you cannot ship it.
   State plainly when the suite cannot run; never claim a result you did
   not observe.
4. ONE_CHANGE: one logical change per commit, verified before the next.
5. SELF_APPLY: code you write is held to the same rules it enforces.
6. data/ is a sacred path (ABSOLUTE tier). Edits there are deliberate.

## Layer 1 — the constitution (machine-readable, loaded at runtime)

- data/axioms.jsonl — 41 engineering, structural, and process axioms. One
  JSON object per line: name, description, category. The spine.
- data/soul.yml — ABSOLUTE section: golden_rule, sacred_paths,
  anti_simulation, protection_tiers, code_rules, aesthetic_rules.
- data/rules.yml — 76 scan rules with per-rule detection axes and
  thresholds; the thresholds: block holds only values with a live reader.
- data/rule_deps.yml — topological ordering for the scan rules.
- data/ruby_style.yml — Ruby-specific style: quoting, frozen_string,
  comment limits, whitespace.
- data/epistemics.yml — confidence and evidence rules.
- data/workflow.yml — pipeline stage rules.

## Layer 2 — operator principles (data/principles/*.md, 32 files)

Operator-declared feedback, loaded by lib/ground/constitution.rb. Each
overrides defaults. Grouped by what they govern:

- Process: autofix, autoproceed, continue_backlog, decisive_signals,
  git_commits, no_new_files, no_permission_questions, readme_autoupdate,
  run_through_master_triad, restart_rails, diverged_branch_sync.
- Code style: importance_order, lint_beautify, comments_reassess, style,
  micro_refinements, no_consecutive_whitespace, no_useless_knobs.
- Voice and prose: strunk_white, voice_terse_unix, proper_casing,
  meta_framing, master_prompt_aesthetic.
- Medium and tooling: no_python, no_sed, no_shell_piping,
  master_zsh_discipline, html_css_style, device_limits.
- Cross-cutting: universal_cross_disciplinary_rules — every rule is a
  medium-agnostic principle with per-medium adapters.
- Design: flat_pixels, motion_color_grading.

## Layer 3 — external canon (the lineages the rules descend from)

These are not files in this repo. They are the philosophical sources the
axioms operationalize. When a rule's intent is unclear, the lineage is the
reference. Each line: the source, then where MASTER applies it.

- KISS, YAGNI, Occam's Razor — SIMPLEST_WORKS, JUST_ENOUGH (axioms.jsonl).
- DRY — ONE_SOURCE, MERGE (axioms.jsonl).
- SOLID — ONE_JOB, EXTEND_DONT_MODIFY, SUBSTITUTABLE, SMALL_INTERFACES,
  DEPEND_ON_ABSTRACTIONS (axioms.jsonl).
- POLA, principle of least astonishment — NO_SURPRISES, USER_FRIENDLY.
- Structural operations: defrag, decouple, flatten, hoist, prune, coalesce,
  reflow — the structural axioms in axioms.jsonl, ordered by rule_deps.yml.
- Recomment, reorganize, rename, rephrase — comments_reassess,
  importance_order, SELF_EXPLAINING.
- Strunk & White, The Elements of Style — principles/feedback_strunk_white.
- NN/g (Nielsen Norman) usability heuristics — USER_FRIENDLY,
  VISUAL_HIERARCHY, NO_DEAD_ENDS.
- The Pragmatic Programmer — LEAVE_BETTER, REVERSIBLE, ONE_CHANGE,
  orthogonality, tracer bullets, broken-windows.
- Clean Code / Refactoring (Martin, Fowler) — the ZEN_METHOD thresholds in
  rules.yml; method and class size limits.
- Polished Ruby Programming (Evans) — Ruby idiom; ruby_style.yml.
- The Rails Doctrine — convention over configuration; principles/feedback
  for web/* discipline.
- Tadao Ando, Snøhetta — restraint, negative space, honest material —
  the minimalist aesthetic in principles/feedback_style and flat_pixels.
- Hoefler & Frere-Jones — typographic rhythm and hierarchy — VISUAL_HIERARCHY,
  STEADY_RHYTHM applied to code silhouette as well as type.

## Maintenance

When a rule source is added, moved, or retired, update this file in the
same commit. CANON.md going stale is itself a ONE_SOURCE violation — it is
the index, and a wrong index is worse than no index.

data/agent_taxonomy.yml

# config_status: aspirational  # spec exists, runtime wiring pending
# Typed child agents (cleaner than ad-hoc thread spawning).
# Source: opencrabs + Manus reunification (#76, #81).
agent_types:
  explore:
    purpose:     "search, glob, grep, read-only inspection"
    tools:       [read_file, list_dir, search_files, symbol_lookup, tree]
    max_runtime: 60s
  plan:
    purpose:     "read code + propose stepwise plan; never edits"
    tools:       [read_file, search_knowledge, list_dir]
    output:      structured_plan
  code:
    purpose:     "apply the plan; one file at a time"
    tools:       [read_file, str_replace, write_file, ast_edit, atomic_write]
    requires:    plan_id
  research:
    purpose:     "external lookup, citations, summaries"
    tools:       [web_search, web_fetch, search_knowledge]
    max_runtime: 120s
  verify:
    purpose:     "ruby -c, scan, test, council vote"
    tools:       [shell, scan, council_call]
    output:      pass_fail_with_evidence

toolset_groups:
  research:    [web_search, web_fetch, search_knowledge, deepwiki]
  build:       [read_file, str_replace, write_file, ast_edit, atomic_write, batch_replace]
  verify:      [shell, scan, council_call]
  ship:        [git_context, atomic_write, audit_log]

spawn_policy:
  max_concurrent_children: 4
  inherit_governor:        true
  sanitize_output_via:     InjectionGuard

data/agents/brgen_amber_completion.yml

pack: brgen_amber_completion
status: executable_spec
branch: brgen-amber-product-pass

rules:
  - preserve_then_improve_never_break
  - prefer_code_over_todo
  - merge_related_docs
  - reduce_file_sprawl
  - one_file_per_patch
  - verify_after_each_batch

agents:
  docs_consolidator:
    purpose: Merge related markdown into canonical docs and replace repeated files with pointers.
    outputs:
      - canonical_docs
      - pointer_docs

  rails_codifier:
    purpose: Convert docs into Rails models, services, controllers, routes, views, and small tests.
    outputs:
      - migrations
      - models
      - services
      - controllers
      - views
      - routes

  brgen_vertical_agent:
    purpose: Finish marketplace, takeaway, dating, TV, and playlist in small deployable batches.

  amber_agent:
    purpose: Turn Amber wardrobe intelligence into model helpers and visible product surfaces.

  verifier:
    purpose: Compare branch, fetch changed files, and flag schema without surface.

batches:
  consolidate_brgen_core_docs:
    files:
      - DEPLOY/rails/brgen/brgen_CORE.md
      - DEPLOY/rails/brgen/brgen_events.md
      - DEPLOY/rails/brgen/brgen_feed.md
      - DEPLOY/rails/brgen/brgen_search.md
      - DEPLOY/rails/brgen/brgen_media.md
      - DEPLOY/rails/brgen/brgen_moderation.md

  codify_activity_graph:
    files:
      - DEPLOY/rails/brgen/app/models/activity_event.rb
      - DEPLOY/rails/brgen/app/services/activity_event_recorder.rb
      - DEPLOY/rails/brgen/app/controllers/activity_events_controller.rb
      - DEPLOY/rails/brgen/app/views/activity_events/index.html.erb
      - DEPLOY/rails/brgen/config/routes.rb

  codify_marketplace_trust:
    files:
      - DEPLOY/rails/brgen/app/models/marketplace/listing_favorite.rb
      - DEPLOY/rails/brgen/app/models/marketplace/saved_search.rb
      - DEPLOY/rails/brgen/app/controllers/marketplace/favorites_controller.rb
      - DEPLOY/rails/brgen/app/controllers/marketplace/saved_searches_controller.rb
      - DEPLOY/rails/brgen/app/views/marketplace/listings/index.html.erb
      - DEPLOY/rails/brgen/app/views/marketplace/listings/show.html.erb

  codify_takeaway_retention:
    files:
      - DEPLOY/rails/brgen/app/models/takeaway/favorite_restaurant.rb
      - DEPLOY/rails/brgen/app/controllers/takeaway/favorite_restaurants_controller.rb
      - DEPLOY/rails/brgen/app/views/takeaway/restaurants/show.html.erb
      - DEPLOY/rails/brgen/app/views/takeaway/orders/show.html.erb

  codify_amber_intelligence:
    files:
      - DEPLOY/rails/amber/app/models/item.rb
      - DEPLOY/rails/amber/app/models/outfit.rb
      - DEPLOY/rails/amber/app/views/items/_item.html.erb
      - DEPLOY/rails/amber/app/views/outfits/index.html.erb
      - DEPLOY/rails/amber/app/views/outfits/show.html.erb
      - DEPLOY/rails/amber/app/views/ai/capsule.html.erb

completion_definition:
  - canonical docs replace repeated prose
  - new controllers have routes
  - new models have schema and reachable surfaces
  - verifier reports blocked writes honestly

data/architectures.yml

# MASTER loop architectures — 15 approaches from conventional to radical.
# status: concept | scaffolded | implemented
# Each implemented entry is wired somewhere in lib/ or data/.
# Nothing here is aspirational decoration — every entry is a real spec.
---

architectures:

  - id: priority_queue
    name: "Priority queue over round-robin"
    status: implemented
    description: >
      Sort rules by recent violation density descending before each pass.
      Highest-impact rules run first, reducing total cycles to convergence.
    wired_to: lib/loop/fix_loop.rb (ordered_rules)
    ref: "#1"

  - id: rule_dependency_graph
    name: "Rule dependency graph with topological ordering"
    status: implemented
    description: >
      Some fixes create violations for other rules (FLATTEN before DECOUPLE;
      HOIST before MERGE). A DAG declared in data/rule_deps.yml ensures rules
      run in an order that minimises cascade re-triggering.
    wired_to:
      - lib/loop/fix_loop.rb (topo_sort)
      - data/rule_deps.yml
    ref: "#2"

  - id: file_first
    name: "File-first convergence (alternative to rule-first)"
    status: concept
    description: >
      Converge all rules on one file before moving to the next.
      Better cache locality, fewer re-reads, faster for large repos.
      Was a strategy option on the pre-collapse loop; the FixLoop
      consolidation dropped it in favour of a fixed two-tier design.
    wired_to: none (removed in FixLoop collapse, a1562a6)
    ref: "#3"

  - id: ast_autofixes
    name: "Deterministic AST-level autofixes for mechanical rules"
    status: implemented
    description: >
      Mechanical violations (frozen_string_literal, trailing commas, bare rescue,
      double-quote enforcement) can be fixed with Prism AST transforms —
      no LLM call, no token cost, no false negative. Sweep already calls
      rubocop -A for Ruby; this extends that to YAML/HTML/CSS/JS via
      language-specific transformers.
    wired_to:
      - lib/judge/scan/ast_fixer.rb (frozen_string_literal + bare_rescue + null_comparison)
      - lib/loop/fix_loop.rb (fast_pass tier 1)
    ref: "#4"

  - id: diff_only_fixes
    name: "Unified diff output instead of whole-file rewrites"
    status: implemented
    description: >
      LLM produces a unified diff patch rather than regenerating the whole file.
      Token cost drops 60-80% for large files. Patch is applied with a
      deterministic patcher — malformed diffs are rejected, not applied blindly.
    wired_to:
      - lib/loop/patch_applier.rb
      - lib/loop/rule_loop.rb (diff_fix)
    ref: "#5"

  - id: council_fixes
    name: "Multi-agent council deliberation for architectural violations"
    status: implemented
    description: >
      Architectural violations (THREAD_SAFETY, DECOUPLE, INTEGRATED_WHOLE)
      route to a 3-persona mini-council (skeptic/security/maintainer) rather
      than a direct LLM rewrite. Council proposes, votes, applies only on
      consensus. Slower; much higher quality for high-stakes changes.
    wired_to:
      - lib/loop/rule_loop.rb (council_fix — 3-persona skeptic/security/maintainer veto)
    ref: "#6"

  - id: reactive_event_driven
    name: "File-watcher reactive trigger — no polling"
    status: implemented
    description: >
      Rather than sleeping and polling, listen for filesystem events (inotify
      on Linux, kqueue on OpenBSD, FSEvents on macOS). A changed file triggers
      a targeted rule_loop pass on that file only. The system quiesces
      naturally. No STARTUP_DELAY, no idle sleep waste.
    wired_to:
      - lib/loop/watch_loop.rb (WatchLoop, kqueue/inotify debounced)
      - Gemfile (rb-kqueue for OpenBSD, rb-inotify for Linux — install on VPS)
    ref: "#7"

  - id: dataflow_pipeline
    name: "Staged dataflow pipeline: Detect → Triage → Fix → Validate → Apply"
    status: implemented
    description: >
      Violations flow through named stages as objects. Each stage is a
      separate worker queue. Stages are independently parallelisable.
      The existing 10-stage pipeline (Intake→Render) provides the pattern;
      this extends it to the fix loop.
    wired_to:
      - lib/now/pipeline.rb (existing pattern)
      - lib/loop/fix_pipeline.rb
    ref: "#8"

  - id: genetic_fixes
    name: "Genetic algorithm for fix candidate selection"
    status: implemented
    description: >
      Generate N fix candidates per violation, score each by post-fix
      violation count, apply the winner. Breed high-scoring candidates
      across passes. RuleLoop applies this per-violation for small files.
    wired_to:
      - lib/loop/rule_loop.rb (genetic_fix, best_candidate)
    ref: "#9"

  - id: reinforcement_learning
    name: "Reinforcement learning: track fix quality, update rule priorities"
    status: implemented
    description: >
      Record which rule+file_type pairs produce fixes that stick (no
      regression in next scan). Update a fix_quality score per rule.
      FixLoop's priority queue uses fix_quality as secondary sort key.
      AutoLoop already tracks rule_recurrence — this extends it with
      outcome tracking.
    wired_to:
      - lib/ground/knowledge_store.rb (KnowledgeStore — fix_outcomes + strategy_outcomes + feedback_events)
      - lib/loop/rule_loop.rb (record_outcomes — :fixed | :stuck per pass)
      - lib/loop/fix_loop.rb (ordered_rules — fix_quality as secondary sort key)
      - lib/plugins/ground.rb (boots KnowledgeStore as learnings:)
    ref: "#10"

  - id: constitution_type_system
    name: "Constitution as a type system on the AST IR"
    status: implemented
    description: >
      Encode constitutional rules as type constraints on a typed AST IR.
      Violations become type errors. Fixes are derivable from the complement
      constraint. The type checker is sound and complete — no LLM, no
      probabilistic inference. Requires a typed IR for every supported medium.
    wired_to: lib/ground/type_checker.rb (FROZEN_STRING_LITERAL + BARE_RESCUE constraints implemented)
    ref: "#11"

  - id: datalog_rules
    name: "Datalog/Prolog rule engine — logical clause rules"
    status: implemented
    description: >
      Each rule is a logical Horn clause. Violations are queries against
      the program's fact base (AST facts). Fixes are derived from the
      complement clause. Sound, complete, no neural network involved.
      Roda/Sequel author Jeremy Evans' style: logic over magic.
    wired_to: lib/judge/scan/datalog_engine.rb (Prism fact extraction + forward-chaining Datalog subset)
    ref: "#12"

  - id: crdt_convergence
    name: "CRDT-based codebase convergence for distributed agents"
    status: implemented
    description: >
      Treat the codebase as a CRDT. Rules define merge functions. Multiple
      agents make concurrent fixes; the CRDT guarantees eventual consistency
      without conflict resolution overhead. Required for multi-agent MASTER
      instances on the same repo.
    wired_to: lib/loop/crdt_loop.rb (LWW-Register per file + vector clock + merge protocol)
    ref: "#13"

  - id: bayesian_priors
    name: "Hierarchical Bayesian violation priors per language/tier"
    status: implemented
    description: >
      Each rule has a prior probability of firing per language and file type.
      Posterior updated by observed violation counts. FixLoop's priority
      queue uses expected violation rate (prior × posterior) as primary
      sort key. High-probability rules run first; low-probability rules
      skip files where they've never fired before.
    wired_to:
      - data/violation_priors.yml (cold-start priors + language_modifiers for all rules)
      - lib/loop/fix_loop.rb (ordered_rules — language-weighted adjusted prior)
      - lib/loop/fix_loop.rb (extension_weights — computes ext distribution per target)
    ref: "#14"

  - id: embodied_codebase
    name: "Codebase as embodied 3D space — particle topology"
    status: implemented
    description: >
      The codebase is a spatial body. Files are particles; modules are
      clusters; call-graph edges are force bonds; violations make particles
      agitated and drift from their cluster; fixes cause particles to settle.
      The face particle system renders this in real time via SSE events.
      Structural operations (DEFRAG, FLATTEN, MERGE, DECOUPLE) become
      observable particle migrations — you watch the codebase reorganize.
    wired_to:
      - lib/loop/fix_loop.rb (codebase:topology events)
      - web/public/visual_bridge.js (master:codebase dispatch)
      - web/public/codebase.js (particle cluster renderer)
    ref: "#15"

data/attention_context.yml

# MASTER attention context protocol
# Compact breadcrumbs for keeping humans, agents, and models aligned on where attention is.
---

protocol:
  id: attention_context
  version: 1
  default_visibility: complex_only
  philosophy: >
    Use a short spatial breadcrumb when work spans multiple clusters, tools, files,
    or abstraction levels. Avoid always-on ceremony; silence remains the default
    for simple replies.

fields:
  map:
    description: Slash-separated conceptual path.
    examples:
      - repo-mining/mobile-web/browser-local-ai
      - Face3D/particles/lipsync
      - prompt-archaeology/multi-llm/orchestration
  zoom:
    description: Current attention scale or transition.
    allowed:
      - wide
      - narrow
      - deep
      - out
      - lateral
      - wide_to_deep
      - narrow_to_wide
  act:
    description: Current operation mode.
    allowed:
      - scout
      - mine
      - analyze
      - design
      - patch
      - land
      - verify
      - synthesize
      - critique
      - rollback
  target:
    description: Active attention target list.
    examples:
      - [repos, opportunity_clusters]
      - [controller, tts, mime]
      - [provider_policy, council, prompt_patterns]
  parent:
    description: Optional parent maps that this work belongs to.
    examples:
      - [repo_topics, visual_clusters]
      - [provider_economy, council]

rendering:
  compact_text: "⟦%{map} | zoom: %{zoom} | act: %{act}⟧"
  markdown_text: "[MAP %{map}][ZOOM %{zoom}][ACT %{act}]"
  yaml_block: |
    attention:
      map: "%{map}"
      zoom: "%{zoom}"
      act: "%{act}"
      target: %{target}
      parent: %{parent}

when_to_emit:
  - multi_step_task
  - repo_or_code_mutation
  - broad_to_deep_research
  - switching_attention_target
  - user_says_go_ahead_or_go_on
  - recovery_after_tool_error
  - summarizing_or_landing_work

when_not_to_emit:
  - short_casual_answer
  - single_fact_answer
  - simple_translation
  - user_explicitly_requests_no_prefix
  - message_would_become_more_header_than_content

runtime_uses:
  - Include in prompt-builder metadata for long agentic tasks.
  - Emit as `attention:context` bus event when MASTER changes major target.
  - Feed cognition ecology as camera/zoom movement.
  - Feed Face3D focus/arousal when zoom goes deep or error recovery starts.
  - Store with trace/session records for replay and audit.

examples:
  - context: mining mobile web repos
    breadcrumb:
      map: repo-mining/mobile-web/browser-local-ai
      zoom: wide_to_deep
      act: scout
      target: [repos, runtime_patterns, opportunity_clusters]
      parent: [repo_topics]
  - context: patching TTS
    breadcrumb:
      map: Face3D/speech-audio/tts-mime
      zoom: deep
      act: patch
      target: [speech_rb, chat_controller, tests]
      parent: [speech_audio_body]
  - context: prompt archaeology
    breadcrumb:
      map: prompt-archaeology/multi-llm/orchestration
      zoom: wide_to_deep
      act: research
      target: [system_prompt_patterns, provider_roles, council]
      parent: [provider_economy, council]

data/budget.yml

# Cost ceiling for a session. Routes degrade strong -> fast -> cheap as spend climbs.
# Source: master2 reunification.

budget:
  limit:    10.0
  currency: USD
  thresholds:
    strong: 5.0
    fast:   1.0
    cheap:  0.0
  on_exceed:
    action: degrade_route
    notify: bus

# Beyond dollars, track approximate kWh / CO2 per session.
# Source: cross-cutting reunification (#98).
carbon_budget:
  kwh_per_million_tokens:   0.5     # rough industry estimate
  co2_g_per_kwh:          400       # mixed grid baseline
  daily_kwh_cap:            1.0
  on_exceed:                "switch to local model only; publish carbon:exceeded"

data/claude/MEMORY.md

# Memory Index

- [MASTER project context](project_master.md) — pub4/MASTER constitutional AI agent on dev@brgen.no, OpenRouter API, Ruby/OpenBSD
- [master.yml + master.json are authoritative](project_master_yml_json_authority.md) — current Ruby MASTER must implement what predecessors describe; 18 priority gaps tracked
- [User is an architect](user_architect_aesthetics.md) — aesthetic/typography/design-philosophy proposals usually approved; don't self-censor them
- [Always autofix violations](feedback_autofix.md) — run /sweep immediately after any scan finds violations, no confirmation needed
- [Frequent git commits](feedback_git_commits.md) — commit after every meaningful change, don't batch
- [No new files without approval](feedback_no_new_files.md) — always edit originals in place, never create staging/copy files
- [Ultra-minimalistic coding style](feedback_style.md) — cut all filler across Ruby, Zsh, HTML, JS; preserve intentional logic
- [No Python](feedback_no_python.md) — only Ruby for all scripting tasks, never python3
- [Mandatory lint/beautify on touch](feedback_lint_beautify.md) — every edited file gets a full lint/beautify pass, not just changed lines
- [Strunk & White style](feedback_strunk_white.md) — commits, comments, log lines: active voice, omit needless words, concrete verbs, dmesg format
- [Voice — terse, unix, perfectionist](feedback_voice_terse_unix.md) — my outputs and MASTER's voice config: cut filler, diagnostic style, loop till zero violations
- [Auto-update README.md when needed](feedback_readme_autoupdate.md) — refresh README prose after any behavior/capability/surface change, no prompting
- [No heavy work on device](feedback_device_limits.md) — Termux/Android is low-power; defer Ruby runs, large clones, mass ops to VPS
- [Bare HTML/CSS targeting, no divitis](feedback_html_css_style.md) — nav a not .nav__link; tag helper; no class attrs on elements targetable by tag
- [MASTER zsh discipline applies to my shell](feedback_master_zsh_discipline.md) — banned cmds (sed/awk/grep/wc/head/tail/find/sudo/...) apply to my Bash calls too, not just to scripts I write
- [Autoproceed without confirmation](feedback_autoproceed.md) — execute full backlog after one approval; no per-step go/no-go
- [No permission questions for predictable yes](feedback_no_permission_questions.md) — never ask "want me to?" / "shall I?" when prior approval makes the answer obvious
- [Decisive short directives = full authorization](feedback_decisive_signals.md) — "ship all", "kill X keep Y", "yes" = binding; for >10 items, ship pass-by-pass with one-sentence checkpoints
- [No consecutive whitespace](feedback_no_consecutive_whitespace.md) — single space, single blank line max, no trailing/aligned-column padding; all file types
- [Proper casing, no ASCII decorations](feedback_proper_casing.md) — sentence case in prose/comments/CLI; no === ---- [ok] • | as ASCII art. Boot dmesg banner is sacred.
- [Restart MASTER after every web edit](feedback_restart_rails.md)`doas rcctl restart master` after each scp under MASTER/web/, never batch and restart once at end
- [Defrag/dedup/rename plan 2026-05](project_defrag_plan_2026_05.md) — multi-commit refactor; Master::Orient reverted 2026-05-20, rest of plan still open
- [MASTER 7-module refactor approved 2026-05-08](project_master_seven_module_refactor.md) — now/loop/judge/voice/ground/reach/trace; pass-by-pass on VPS, supersedes the 6 dedup proposals
- [OpenCrabs (Rust MASTER cousin)](reference_opencrabs.md) — github.com/adolfousier/opencrabs; brain-files-per-turn, FTS5 memory, /rebuild + exec() hot-restart
- [Grok UI/CLI patterns](reference_grok_ui_cli_patterns.md) — StyleCoach prompt, htmx+SSE, tty-prompt/spinner, char-stream Claude CLI for MASTER polish
- [Importance-ordered file layout](feedback_importance_order.md) — every file's lines flow by importance; newspaper inverted pyramid; public API > primary > helpers > edge cases
- [Reassess comments on every touch](feedback_comments_reassess.md) — touch a file = re-read all its comments; delete obvious, rewrite kept ones S&W-style
- [Meta-architecture framing over diff reports](feedback_meta_framing.md) — after a batch, surface what's next/structurally off; user prefers 2x wins to 5% tweaks
- [MASTER has two Gemfiles](project_master_dual_gemfile.md) — MASTER/Gemfile and MASTER/web/Gemfile are independent; gems used by lib/ from web must be in both
- [Falcon + EM = subprocess](project_falcon_em_subprocess.md) — Process.fork in a Falcon fiber raises "Closing scheduler"; EM-based gems must shell out to exe/<name>-worker
- [Diverged branch sync via cherry-pick](feedback_diverged_branch_sync.md) — when local + remote both moved, backup-tag + reset to origin + cherry-pick targeted commits, never force-push or rebase mixed history
- ["Run X through MASTER" = scan+sweep+council](feedback_run_through_master_triad.md) — manual chain `/scan → /sweep → /council`; user vocabulary is "tribunal", code is "council"
- [No unnecessary piping/concat in shell calls](feedback_no_shell_piping.md) — pure Ruby/zsh patterns; banned shell cmds rule applies to my Bash calls too

data/claude/feedback_autofix.md

---
name: always autofix violations
description: User wants all scan violations autofixed immediately, no asking
type: feedback
originSessionId: 84fcf91d-46ea-43a5-8efa-3d33b065e6a5
---
Always run /sweep (or /autoloop) automatically after any scan that finds violations. Do not ask for confirmation.
All scan rules have `@auto_fix = true` (set in `Rule#initialize` base class).

**Why:** User said "autofix all always" — violations should be fixed immediately, all rules are eligible.
**How to apply:** After any /scan that returns violations, immediately kick off /sweep on the VPS without prompting. The base Rule class defaults @auto_fix=true so all rules participate.

data/claude/feedback_autoproceed.md

---
name: Autoproceed without confirmation
description: Once user approves a direction, execute the full backlog without pausing for per-step confirmation
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
When the user has approved a direction or list of tasks, execute the entire backlog end-to-end without pausing to ask "want me to do X next?" between items.

**Why:** User said "yes, and autoproceed for all in this chat always" — they want momentum, not checkpoints. Repeated mid-task confirmation requests slow them down and waste turns.

**How to apply:** After each completed step, immediately move to the next pending item. Only stop to ask if (1) a destructive/irreversible action would affect shared state beyond local files, (2) ambiguity emerges that would change the approach materially, or (3) the backlog is genuinely empty. Brief progress updates between steps are fine; explicit go/no-go prompts are not.

**Reinforced 2026-05-07** ("autpmatically autoproceed with next always"): also keep going *between passes* of a multi-commit batch. Don't end a turn on "say 'next' or pick a slice" — just commit pass N and start pass N+1. Stop only when the original backlog is empty or the destructive/ambiguity gates trigger. Out-of-context interruptions (user types something new mid-stream) override the autoproceed and are addressed first.

data/claude/feedback_comments_reassess.md

---
name: Reassess comments on every touch
description: Every edit re-reads each comment in the file — delete if obvious, rewrite Strunk & White style if kept.
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
When I edit any file, I reassess every comment in it — not just the ones near my changes. If a comment merely restates what the code does, delete it. If it carries a non-obvious WHY, rewrite it Strunk-and-White style: active voice, omit needless words, concrete verbs, one line max.

**Why:** Comments rot faster than code. The user (2026-05-07) asked that comments be reassessed and rewritten ultra-minimalistically on every touch — no grandfathered fluff. Encoded in `MASTER/data/ruby_style.yml` (`comments.reassess_on_touch: true`) and as the `RECOMMENT` technique in `MASTER/data/sweep_prompts.yml`.

**How to apply:**
- Touch a file = touch its comments. Don't preserve old comments unread.
- Delete: what-comments, restatements of code, ASCII section banners, numbered-step comments, YARD-style doc blocks, multi-line prose.
- Keep + rewrite: hidden constraints, workarounds for specific bugs, behavior that would surprise a reader, non-obvious invariants.
- Style of kept comments: one line, active voice, no hedging, no filler ("we", "just", "simply", "basically"). Concrete nouns and verbs.
- Do NOT add comments to my own new code unless WHY is non-obvious — the default is no comment.

data/claude/feedback_continue_backlog.md

---
name: Process backlog without asking
description: When a task ships, immediately pick the next pending todo and continue; never ask "should I continue?"
type: feedback
originSessionId: 0c593fb2-cd49-4fd7-9e89-d77dd7e909ae
---
After completing a task, return to TaskList and start the next pending item. Do not ask permission, do not summarize and stop, do not request go/no-go.

**Why:** the user has given a standing directive to flow through the backlog autonomously. Asking after each task is repetitive friction. Combines with the existing "autoproceed" + "no permission questions" + "decisive signals" rules — this one is specifically about *post-completion behavior*: don't pause at the end of a task, pivot directly into the next.

**How to apply:**
- Done with task X → check TaskList → pick highest-value pending → start.
- For tasks deferred as too-risky or too-architectural, skip to the next viable one rather than stopping.
- Background long-running work via Agent + run_in_background or Bash + run_in_background so chat stays responsive — user explicitly suggested tmux-style parallelism.
- One-sentence checkpoint between tasks is fine; "want me to continue?" is not.

data/claude/feedback_decisive_signals.md

---
name: Decisive short directives = full authorization
description: Short lowercase replies ("ship all", "kill X keep Y", "yes", "i think X") are binding — execute pass-by-pass without re-confirming.
type: feedback
originSessionId: b02ce9b9-a7c7-4c65-b8d0-3b8469dc2028
---
Short, lowercase, often typo-laden user directives are decisive — full authorization to execute without re-asking. Recognized signals:

- "ship all" / "yes" / "do it" → full proposed backlog approved
- "kill X, keep Y" → binary fork decided
- "i think X" → user has settled on X, proceed
- "propose N X" → wants a numbered, categorized list with one-liner per item, grouped by surface (type, color, motion, etc.); user then picks a slice or says "ship all"

For large approved batches (>10 items), ship in coherent commit-sized passes (~10–12 items per commit), checkpoint briefly between passes. Don't try to ship 40 in one go. Don't ask "are you sure" or stall on confirmation between passes — checkpoint = one short status sentence, not a question.

**Why:** Validated on 2026-05-07 lofi-aesthetic session. I diagnosed a two-voice TTS bug, user said "kill cli tts, keep web tts" (one sentence, decisive), I executed without re-asking. Then I proposed 40 lofi refinements organized by surface, user said "can we ship all?", I scoped pass-by-pass and started shipping — user then explicitly said "make sure we codify my messages that lead to great success like now."

**How to apply:** Treat one-line approvals as binding contracts. For "ship all N" where N > 10, propose pass plan in 1–2 sentences, execute pass 1, give a one-sentence checkpoint, continue. Stop only on failure, ambiguity, or destructive scope.

data/claude/feedback_device_limits.md

---
name: No heavy work on device
description: Termux/Android — defer CPU/IO-heavy tasks to VPS, keep device work minimal
type: feedback
originSessionId: 84fcf91d-46ea-43a5-8efa-3d33b065e6a5
---
Prefer the VPS (dev@46.23.89.226) for all work. This device (Termux/Android) is a last resort.

**Why:** User said "prefer using the VPS" and "avoid doing heavy stuff on this device."

**How to apply:** Default to SSH into the VPS for every task — edits, Ruby runs, git, clones, builds. Only fall back to this device when VPS SSH is down and the task is genuinely lightweight (small curl, quick read).

data/claude/feedback_diverged_branch_sync.md

---
name: Diverged branch sync via cherry-pick onto remote
description: When local and remote main have diverged with overlap, cherry-pick the targeted commits onto remote tip rather than rebase mixed history or force-push
type: feedback
originSessionId: b02ce9b9-a7c7-4c65-b8d0-3b8469dc2028
---
When `git push` is rejected because remote has new commits and local also has commits the remote doesn't, prefer this flow:
1. `git tag backup-pre-sync-YYYY-MM-DD` on local main
2. `git reset --hard origin/main` (backup tag preserves the prior tip)
3. Cherry-pick only the commits we actually want to ship (e.g. session's lofi passes), not the mixed pile of older local-only commits that may already exist upstream in equivalent form
4. Resolve conflicts case by case
5. Push

**Why:** User said "push sync github" after a session that produced 16 lofi commits on top of 9 older local commits, while remote had 20 unrelated commits. Rebasing all 25 would have replayed work already on remote in equivalent form, producing duplicate commits and unnecessary conflicts. Force-push would have destroyed the 20 remote commits — unacceptable. The cherry-pick-onto-remote approach shipped exactly the intended work, kept history linear, and was accepted without pushback ("great." after sync).

**How to apply:** Use this when (a) the user's intent is clearly "ship my recent work, not all local work" — e.g. after a focused session like a feature batch, and (b) older local-only commits look duplicated on remote (same area, similar messages). Always create a backup tag before reset. If the user's intent is "preserve all local work", do a full rebase or merge instead.

data/claude/feedback_flat_pixels.md

---
name: Flat UI, flat pixels except 3D-resemblance
description: Keep all UI and particle rendering flat 2D — uniform size/alpha/no depth illusion — except when pixels arrange to resemble a 3D model (face mode, future 3D model approximations)
type: feedback
originSessionId: 285acce4-505e-4b41-82ff-f88e72ee1535
---
UI is flat. Pixels are flat. No fake depth (z-scaled size, alpha tiers, parallax, motion blur, drop shadows, gradients). The ONLY exception: when pixels collectively arrange themselves to resemble a 3D model — e.g., face mode where fibonacci-sphere anchors project a head shape. In that case the 3D-ness comes from the arrangement, not from per-pixel depth tricks.

**Why:** Aesthetic is 8-bit/dmesg/openbsd — flatness is the brand. Per-particle z-scaling creates a generic "particle.js" look. The face/3D modes earn their depth by being the shape itself, not by faking it everywhere.

**How to apply:**
- Idle particle render: same size (1px), same alpha for all particles
- No motion-blur trail fade — clear background solid each frame
- Tilt parallax IS welcome (gyro/device orientation moves layers at different speeds) — counts as mobile enhancement, not a fake depth shadow
- Orbital cursor: pan force can be uniform, no z-multiplier
- Keep z field for 3D arrangements (face mode) and parallax-style motion only
- CSS: no shadows, gradients, glows, blurs, animated scales — flat fills, hard edges, pixel borders only

data/claude/feedback_git_commits.md

---
name: frequent git commits
description: Make git commits frequently after meaningful changes
type: feedback
originSessionId: 84fcf91d-46ea-43a5-8efa-3d33b065e6a5
---
Commit after every meaningful change — don't batch. After fixing a bug, restoring a file, or completing a refactor, commit immediately.

**Why:** User explicitly requested frequent commits.
**How to apply:** After any file write or fix on the VPS, run `git add <file> && git commit -m "..."` before moving on.

data/claude/feedback_html_css_style.md

---
name: Bare HTML/CSS targeting — no divitis, no utility classes
description: Always use bare element selectors (nav a, main, h1) not BEM classes or utility class strings on elements
type: feedback
originSessionId: ab7bf92a-5fdc-43bb-998c-dc1d5598f33d
---
Use bare element and structural selectors throughout. Never add class attributes to elements that can be targeted by tag or relationship.

**Why:** User explicitly stated "always bare targeting for clean HTML/CSS" and "no divitis." Confirmed with rejection of `.nav__link`, `.nav__brand`, `.nav__links` pattern.

**How to apply:**
- `nav a` not `a.nav__link`
- `.brand` only for the logo anchor that needs differentiation from other nav links
- `nav { ... }` for nav bar styling, not `.navbar` or `.nav`
- `main` for main content, not `.main-content` or `.container`
- Use `tag.nav`, `tag.main`, `tag.article` etc. — no wrapper divs with classes unless structurally necessary
- Rails `tag` helper (tag.div, tag.span) preferred over `content_tag`; `class_names` for conditional classes
- In ERB views: no `class:` arguments on links unless the class carries genuine semantic meaning (e.g. `.brand`, `.btn`, `.badge`)

data/claude/feedback_importance_order.md

---
name: Importance-ordered file layout
description: Every file's lines flow by importance — newspaper inverted pyramid. Most important content at top.
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
Every file I touch gets reordered so the most important content sits at the top. Newspaper-style inverted pyramid.

**Why:** A reader who stops halfway must still have the gist. The user explicitly asked for this on 2026-05-07 ("every file must have all its lines rearranged to flow by importance so most important stuff comes top"). Encoded into `MASTER/data/ruby_style.yml` (`line_order:` section) and `MASTER/data/sweep_prompts.yml` (`IMPORTANCE_ORDER` structural technique) so MASTER's auto-sweep propagates the rule.

**How to apply:**
- Order: requires → module/class declaration + headline doc → public API (ordered by importance/call-frequency) → primary algorithm → private helpers (in dependency order) → constants/tables → edge-case handlers/rescues.
- Applies to ruby, yaml, erb, js, css, html, sh, md — not just Ruby.
- When editing any file, even for a small change, briefly check if the surrounding region needs reordering. Don't rearrange just to rearrange — but if the file is already inverted (helpers at top, public API at bottom), fix it as part of the touch.
- The Maintainer and Layperson council personas evaluate this. Sweep enforces via `IMPORTANCE_ORDER` and `RECOMMENT`.

data/claude/feedback_lint_beautify.md

---
name: Mandatory lint/beautify on touch
description: Every file edited must be linted and beautified — not just the target lines
type: feedback
originSessionId: 84fcf91d-46ea-43a5-8efa-3d33b065e6a5
---
Run a lint/beautify pass on every file you touch, not just the specific lines changed.

**Why:** User instruction: "mandatory lint / beautify of everything it touches"

**How to apply:** After any edit to a Ruby/Zsh/JS/HTML file, apply style fixes to the whole file: consistent spacing around operators, no double blank lines, use defined constants instead of magic literals, align related assignments if the file already does so. Verify syntax after.

data/claude/feedback_master_prompt_aesthetic.md

---
name: MASTER prompt aesthetic is approved
description: Keep the oh-my-zsh-style shell prompt (bold-red master, dim metadata, $); don't redesign it
type: feedback
originSessionId: 0c593fb2-cd49-4fd7-9e89-d77dd7e909ae
---
Keep `Master::Renderer#prompt_line` as-is: bold-red `master`, dim parens/braces around branch/phase, dim token bar, `$` terminator. Oh-my-zsh-style is the desired look.

**Why:** user explicitly approved it after I shipped the IRC `<master>` reply tag. The prompt was never the part that needed fixing — only the reply side lacked a speaker marker.

**How to apply:**
- Don't simplify, recolor, or strip metadata from `prompt_line`.
- New ornamentation goes elsewhere (reply tag, status row, dmesg banner) — not the prompt line.
- If asked to "clean up the prompt," confirm scope first; default assumption is the user means surrounding output, not the prompt itself.

data/claude/feedback_master_zsh_discipline.md

---
name: MASTER zsh discipline applies to my session shell
description: When working on MASTER (or any project where MASTER's constitution applies), avoid the banned external commands in my own Bash tool calls — not just in scripts I write
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
The zsh-banned-commands list in MASTER (`sed`, `awk`, `tr`, `grep`, `cut`, `head`, `tail`, `find`, `wc`, `sudo`, `perl`, `ruby` invoked from zsh, `dd`, `xargs`) applies to commands I run via the Bash tool too, not only to scripts I write into the repo.

**Why:** MASTER is a constitutional agent and the operator expects me to live by the same constitution while editing it. Reaching for `wc -l` or `sort | tail` to inspect repo files signals that I do not actually use what I preach. Caught 2026-05-05.

**How to apply:** When inspecting MASTER (or any sibling pub4 project) over Bash:
- Read a file → `cat file` (prefer over grep/head/tail fragments — user reinforced 2026-05-06: "instead of grep and head just cat"). Read the whole file once instead of stitching snippets together.
- File line counts → zsh array: `lines=("${(@f)$(<file)}"); print ${#lines}` — or `print -l file*(.oL[1,N])` for size-sorted listing
- Largest N files → glob qualifier with size sort: `print -l **/*.yml(.oL[1,20])`
- Search content (when actually searching, not reading) → use the Grep **tool**, never shell `grep`/`rg`
- Find files → use the Glob **tool**, never shell `find`
- Privilege → `doas`, never `sudo`
- Complex parsing → write a Ruby script and run it, never inline `sed`/`awk`

The exception that already holds: `git`, `gh`, `bundle`, `ssh`, `scp`, `sshpass`, plain `ls`, `mkdir`, `cd`, `print`, `echo`, parameter expansion. Those stay fine.

**Narrow exceptions:**
- `eval` — only for loading exports from `.zshrc` (`eval "$(grep '^export' ~/.zshrc)"`). Banned for arbitrary code execution.
- `bundle exec ruby bin/cli` — permitted because it boots the project executable. Standalone `ruby -e` from zsh stays banned; use `tmp/patch.rb` + `ruby tmp/patch.rb` for transient scripts.

data/claude/feedback_meta_framing.md

---
name: User favors meta-architecture framing over change-by-change reports
description: After a batch of work, surface what's next/missing/structurally off — exploratory questions outperform itemized diffs
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
After landing a batch of work, surface the meta-question — what shape, what's missing, what's structurally off — instead of itemizing the diff.

**Why:** This user repeatedly asks "ways to...", "could X benefit...", "are we missing...", and explicitly favors 2x architectural wins over 5% incremental fixes. They read the diffs themselves; they want me using context to spot shape misfits, format shifts, and consolidation opportunities. They land everything in batch with "yes" / "land all" / "sweet, finish backlog next" — proof that exploratory follow-ups (not summaries) keep momentum.

**How to apply:**
- After a multi-commit batch, end with one meta-question (what to consolidate next, what's drifting, what could take a different shape) — not a list of what changed.
- When asked "are we missing X?", give 5-8 ranked candidates with payoff/risk, not a single suggestion.
- When the user says "land all", treat it literally — no per-suggestion confirmation, batch + commit aggressively, only break stride if syntax fails or the work needs the VPS.
- Pair violations with opportunities in any scan/audit reply — never just bugs.

data/claude/feedback_micro_refinements.md

---
name: Pay attention to micro-refinements
description: User is an architect/designer; tiny details matter intensely. Default to noticing and addressing them.
type: feedback
originSessionId: 0c593fb2-cd49-4fd7-9e89-d77abd7e909ae
---
By default, scan every artifact (code, prose, layout, naming, spacing, alignment, glyph choice) for micro-refinement opportunities — not just the structural-level work the user explicitly asked for. Surface or fix them as part of the task, don't wait to be asked.

**Why:** the user identifies as an architect/designer. They told me explicitly that tiny details are *extremely* important to them. A 2x architectural win is satisfying, but a 5% improvement in a heading's casing, a glyph's choice, a comma's placement, a variable name's precision — those compound into the texture they care about. Treating those as "low priority polish" misreads what matters to them.

**How to apply:**
- After making any edit, re-read what I wrote and ask: is there a tighter word? a better glyph? a name more honest to its intent? a spacing that breathes correctly?
- In rename/refactor passes, watch for adjacent things that became inconsistent.
- In prose: cut filler, prefer concrete verbs, attend to commas, hyphens vs. em-dashes, casing.
- In code: variable naming, magic-number extraction, comment quality, line-break placement.
- This is *additive* to the existing Strunk & White, lint/beautify, no-consecutive-whitespace rules — those are about avoiding mistakes; this is about actively hunting refinements.
- Do not surface every micro-fix as a question — just apply them, and only mention if non-obvious.

data/claude/feedback_motion_color_grading.md

---
name: Smooth motion graphics, professional color grading
description: All transitions/animations use easing curves; palettes follow cinema-grade color science (complementary tones, lift/gamma/gain), not raw primaries
type: feedback
originSessionId: 285acce4-505e-4b41-82ff-f88e72ee1535
---
Motion: every state change interpolates with an easing curve (ease-out-cubic for arrivals, ease-in-out for cross-fades), never snaps. Pulse rings, scatter decay, mode transitions, message appear/fade — all eased. Frame-independent (use dt, not fixed step counts).

Color: think DP/colorist. Pick complementary anchors (teal/orange, cyan/amber, magenta/cyan), control saturation per mood, define shadow/midtone/highlight triplets rather than single hex. Mode palette changes cross-fade RGB over ~600ms.

**Why:** User reads as architect/designer. The current dmesg sepia (#cdc5b6 / #0a0c0a / #1f221d) is the floor — every additional surface should feel like a graded short film, not default canvas demos.

**How to apply:**
- Animations: `easeOutCubic(t) = 1 - Math.pow(1-t, 3)`, `easeInOutCubic(t) = t<.5 ? 4*t*t*t : 1-Math.pow(-2*t+2,3)/2`
- Palette per mood = `{shadow, midtone, highlight, accent}` triple, never one color
- Pulse rings: ease radius growth + ease alpha decay
- Mode cross-fade: lerp palette RGB over 600ms before snapping
- Mood color grade examples — focused: cool teal/cyan; curious: amber/cream; tense: red shadow + sodium highlight; weary: muted slate; idle: dmesg sepia
- Avoid: linear timing, snap palette swaps, single-hex moods, fully saturated primaries

data/claude/feedback_no_consecutive_whitespace.md

---
name: No multiple consecutive whitespace anywhere
description: Single space, single blank line max, no trailing whitespace — across Ruby, JS, CSS, HTML, YAML, shell, Markdown.
type: feedback
originSessionId: b02ce9b9-a7c7-4c65-b8d0-3b8469dc2028
---
Multiple consecutive whitespace is forbidden across all file types. Applies to:

- Two or more spaces in a row mid-line — one space only (no aligned-column padding like `@foo   = 1`)
- Two or more blank lines in a row — single blank line max between sections
- Trailing whitespace at line end
- Indentation beyond level (no double-indent for visual alignment)

**Why:** Stated by user 2026-05-07 during lofi pass 1 session. Tightens "ultra-minimalistic coding style" and the Strunk & White principle: omit needless characters, not just needless words. Aligned-column padding is filler.

**How to apply:** When editing a file, collapse runs of spaces and blank lines as part of the lint/beautify-on-touch pass. When writing new code, never align `=` or values with extra spaces; never leave two blank lines between methods. CSS one-liners fine; CSS multi-line fine; tabular alignment via spaces not fine.

data/claude/feedback_no_new_files.md

---
name: no new files without approval
description: Never create new files — always edit originals in place
type: feedback
originSessionId: 84fcf91d-46ea-43a5-8efa-3d33b065e6a5
---
Always edit the original file directly. Never create intermediate files (local staging files, _fixed.rb copies, tmp patches) without explicit approval.

**Why:** User explicitly said "write changes back into original files don't create new files ever without approval."
**How to apply:** Use Edit tool on the actual file path, or write patch Ruby to /tmp on the VPS and run it in-place — but never create a local copy. The /tmp/patch.rb VPS pattern from CLAUDE.md is fine since it's a transient runner, not a persisted file.

data/claude/feedback_no_permission_questions.md

---
name: No permission questions for predictable yes
description: Skip "do you want me to..." prompts when the answer is obviously yes given context
type: feedback
originSessionId: 0c593fb2-cd49-4fd7-9e89-d77dd7e909ae
---
Don't ask "should I continue?", "want me to ship next?", "shall I start with X or Y?" when the user's prior approval, autoproceed memory, or task framing makes the answer obvious.

**Why:** User has standing autoproceed authorization (feedback_autoproceed) and decisive-signals authorization (feedback_decisive_signals). Asking for re-confirmation per step is wasted turns and breaks flow.

**How to apply:** After one approval ("yes", "ship", "go", "do it", "start"), execute the full backlog. Surface trade-offs and checkpoints as statements ("shipping #1 next, ETA 10 min"), not questions. Only ask when there's a genuine fork that the user can't predict — e.g., destructive action, ambiguous scope, or a real either/or where both are reasonable.

data/claude/feedback_no_python.md

---
name: no python
description: Never use Python — only Ruby for scripting tasks
type: feedback
originSessionId: 5a5097b9-8cd5-46a3-913f-b193da929311
---
Never use Python for any task. Use Ruby exclusively for scripting, data processing, encoding, etc.

**Why:** User explicitly said "no python. only ruby." Reinforced again this session.
**How to apply:** Replace any python3/python one-liners with ruby equivalents. Use `ruby -e` or write to /tmp/*.rb on VPS. Do not even test-invoke python3 as a fallback before trying Ruby.

data/claude/feedback_no_sed.md

---
name: No sed — use ruby
description: Never invoke sed in shell commands; use ruby for any text substitution
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
Never call `sed` (or awk/grep-with-rewrite) for text edits. Use ruby instead — `ruby -e`, `ruby <<RB`, or a `.rb` script.

**Why:** OpenBSD sed is BSD-flavor and behaves differently from GNU sed (no `-i ''` semantics, different regex flavors, no extended-mode without `-E`). Scripts written against GNU sed silently break on the dev@brgen.no VPS. Ruby is portable and the project's primary language.

**How to apply:** any time I'd reach for `sed -i 's|x|y|'`, write `ruby -E UTF-8:UTF-8 -e 'File.write(p, File.read(p).sub("x","y"))'` instead. Same for awk one-liners — use ruby. The earlier ban-list (sed/awk/grep/wc/head/tail/find) already covers this; this memory exists because I slipped once during Wave B heredoc fixes.

data/claude/feedback_no_shell_piping.md

---
name: No unnecessary piping/concat in shell calls
description: Avoid pipe chains and string concat in Bash invocations; prefer pure Ruby or pure zsh
type: feedback
originSessionId: 0c593fb2-cd49-4fd7-9e89-d77dd7e909ae
---
When invoking Bash, do not pipe through `head`/`tail`/`grep`/`wc` etc. or stitch with `&&`/`;` chains where a single Ruby/zsh idiom does the job.

**Why:** matches the banned-shell-commands rule already in `data/rules.yml` (sed/awk/grep/find/head/tail/wc/sudo). Same discipline applies to my own tool calls, not just to scripts I write. User explicitly called this out as noise — it makes prompts hard to read and audit.

**How to apply:**
- File reads → use Read tool, not `cat | head`.
- Searches → use Grep tool, not `grep`.
- Single-step shell ops → run them directly; do not chain when sequential calls would be clearer.
- For Ruby work, prefer a one-liner `ruby -e '...'` over zsh-glue.
- For zsh, use builtin parameter expansion / globs / arrays, not pipes to coreutils.

data/claude/feedback_no_useless_knobs.md

---
name: No useless metrics, thresholds, or categorizations
description: Eliminate knobs whose only effect is "do less / do worse" — defaults should be maximal correctness; lower-effort modes need real justification
type: feedback
originSessionId: 0c593fb2-cd49-4fd7-9e89-d77abd7e909ae
---
Don't add a knob whose only effect is "do less" or "do worse." Defaults should be maximal correctness.

Example: `/scan shallow` vs `/scan standard` vs `/scan deep` — there's no situation where the user wants an *incomplete* scan, so the categorization is dead weight. Just `/scan` always does the thorough thing.

**Why:** every knob is a question the user has to answer, and most "lite/fast/shallow" modes exist because the implementation was once slow — not because anyone actually wants the degraded output. Knobs accrete; defaults rarely catch up. The user wants the right answer, not a menu of wrong ones.

**How to apply:**
- Audit every CLI flag, depth/limit param, and named tier (shallow/standard/deep, light/full, basic/advanced) — if every user always picks the maximum, kill the lower tiers and make max the default.
- Keep the knob only when there's a *real* tradeoff the user genuinely makes (cost-tier lexical/structural/semantic = real, because LLM is expensive; depth=2 vs unbounded if unbounded would dump GBs = real).
- Don't confuse **detection criteria** (file >300 lines = violation) with **effort knobs** (scan only the first 100 files) — criteria stay, effort knobs die.
- Token bars, budget caps, max_lines, max_depth: each one needs a real reason. If the only reason is "we used to be slow," cut it.
- Inverse rule: when adding a new feature, don't preemptively add levels/modes. Ship one mode (the right one). Add levels later only if a real tradeoff emerges.

data/claude/feedback_proper_casing.md

---
name: Proper casing, no ASCII decorations
description: Sentence case in prose, comments, CLI, commit messages; no ===, ----, [ok], bullet/separator chars as ASCII art
type: feedback
applies_to: prose, comments, CLI output, commit messages, log lines, section headers
---

Use proper casing in prose, comments, log lines, CLI output, and commit messages. Capitalize sentence starts, proper nouns, and acronyms. Snake_case identifiers stay as-is.

Commit messages: capitalize the first word of the subject line. `Kill cli tts; web is sole audio path` not `kill cli tts; web is sole audio path`. Body paragraphs follow normal sentence-case rules.

Never use these as ASCII art decorations:
- `===` or `----` (banner lines, section dividers)
- `[ok]` `[err]` `[skip]` (status tags — use `ok:` `err:` `skip:` prefix instead)
- `` `|` `` `` (bullet/separator characters in CLI text)

**Why:** Refines the prior dmesg/terse-voice rule. Lowercase-only feels sloppy in human-facing surfaces; ASCII decorations are visual noise the user explicitly disliked. Real dmesg uses lowercase because kernel space is constrained — MASTER isn't, so prose, CLI output, and commit subjects should read like written English. Commit-msg rule added 2026-05-07 after a lowercase commit subject slipped through.

**How to apply:**
- Comments: `# Restore HTML/CSS/typography sections` not `# restore html/css/typography sections`
- CLI output: `Wired /why to local lookup; LLM fallback only on miss.` not `[ok] /why now uses WhyExplainer first`
- Log lines: `Boot scan: 1678 violations (45s)` not `boot scan: 1678 violation(s)`
- Commit subjects: `Add foo`, `Fix bar`, `Refactor baz` — first word capitalized
- Section headers in YAML/code: drop `===== HEADER =====` style; use a single `#` line if needed
- Status indicators: `ok:` `err:` `warn:` as bare prefixes, never `[ok]`
- Bullet content in CLI: dash + space (`- item`) is fine; never use ``

**Tension with dmesg style:** dmesg conventions apply ONLY to *kernel-style structured output emitted by MASTER itself* — the boot banner, event log lines, status pings (`master@host ready`, `boot0: 26ms`). The MASTER boot banner is explicitly sacred (user 2026-05-06: "dont remove boot message on startup, its awesome, and should remind of openbsd dmesg").

Do NOT use dmesg style for my own conversational prose to the user (clarified 2026-05-06: "dont use dmesg style for conversing prose"). When narrating progress in chat, write plain English sentences with proper casing. dmesg style is for log lines MASTER writes, not for me speaking to the operator.

data/claude/feedback_readme_autoupdate.md

---
name: Auto-update README.md when needed
description: After any meaningful change to MASTER's behavior or capabilities, update README.md without prompting
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
When a commit changes MASTER's user-facing behavior, capabilities, command surface, philosophy, or stack, update README.md in the same or a follow-up commit without waiting for the user to ask.

**Why:** Stated explicitly on 2026-05-05. README is the single front door — drift between it and the code degrades trust. The user prefers the doc to lead, not lag.

**How to apply:**
- After landing depth flips, rule additions, workflow changes, persona changes, scan/sweep semantics changes, model routing changes, or any new top-level concept (Six Laws, biases, structural_ops, etc.) — refresh the matching README paragraph.
- Refresh = update the prose, not append a changelog entry. Keep README's flowing-prose / Strunk & White / Bringhurst form (no h2/h3, no tables, no code blocks unless essential).
- Bundle the doc update with the code commit when small; split into a follow-up if the doc change is substantial.
- Skip auto-update only for trivial bugfixes that don't change observable behavior.

data/claude/feedback_restart_rails.md

---
name: Restart MASTER service after every web/* edit
description: Whenever I update any file under MASTER/web/ on the VPS, restart the master rc.d service so the change takes effect; do not batch updates and restart at the end
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
After every scp of any file under `MASTER/web/` (controllers, views, initializers, config), immediately run `doas rcctl restart master 2>&1` on the VPS before moving on. Falcon does not hot-reload code in production mode — without a restart, the deployed app still serves the prior bytecode and the user sees stale behavior.

**Why:** User correction 2026-05-06 ("restart the rails app every time you update it"). I had been batching multiple web edits and restarting once at the end, which left the user staring at unchanged behavior between scps.

**How to apply:**
- Edit one web file → scp → `doas rcctl restart master` → next edit, even if more edits to that same file are coming.
- Allow ~2 seconds after restart before any verification curl, since Falcon cold-starts the container.
- Lib edits (`MASTER/lib/`) follow the same rule when they're in the live require path.
- Data file edits (`MASTER/data/*.yml`) load at boot too — restart for those as well.
- CLI-only changes (`bin/cli`, `lib/master/cli/*`) don't need a restart unless the operator is also using the web surface.

data/claude/feedback_run_through_master_triad.md

---
name: "Run X through MASTER" = scan + sweep + council
description: User shorthand — "run X through master" means scan + sweep + council/tribunal (called "council" in code, "tribunal" in user vocabulary), not just /scan
type: feedback
originSessionId: 0c593fb2-cd49-4fd7-9e89-d77dd7e909ae
---
When the user says "run X through MASTER" (or "expose X to MASTER", "MASTER on X"), run the chain: `/scan <path>``/sweep <path>` to convergence → `/council <path>`. Council = deliberation pass with the 6 personas and Security veto.

Why: User confirmed "yeah when user says run this or that through master, then a triad is what i expect" → "/scan+sweep+tribunal" (2026-05-08). User uses "tribunal", code uses "council" — same thing. The `/triad` wrapper was removed in commit 7670306c per the rule that the user should never need to know a command name.

How to apply: For any directive "run/scan/process X through master" where X is a path or codebase, run the three commands in sequence. Don't ask which depth — scan is deep by default.

data/claude/feedback_strunk_white.md

---
name: Strunk & White style
description: All code output — commits, comments, log messages, CLI output — must follow Strunk & White principles
type: feedback
originSessionId: 84fcf91d-46ea-43a5-8efa-3d33b065e6a5
---
Apply Strunk & White to every written artifact: commits, comments, log messages, CLI prompts, error messages.

**Why:** User mandate for all text output from and about MASTER.

**How to apply:**
- Active voice: "Fix bug" not "Bug was fixed"
- Omit needless words: "extract Search module" not "perform extraction of Search module functionality"
- Concrete nouns and verbs: "scan", "fix", "load", "route" — not "process", "handle", "manage"
- One idea per sentence
- Commit messages: imperative mood, ≤72 chars, no trailing period
- Comments: state the WHY only, not the WHAT — one line max
- dmesg log lines: `component: action key=val key=val` (no commas, no padding)

data/claude/feedback_style.md

---
name: ultra-minimalistic coding style
description: Always write ultra-minimalistic code in all languages — no redundancy, no filler
type: feedback
originSessionId: 84fcf91d-46ea-43a5-8efa-3d33b065e6a5
---
Always use ultra-minimalistic coding style across all languages (Ruby, Zsh, HTML, JavaScript, etc.) — no filler, no redundant logic, no ceremonial patterns. Intentional and valuable logic is preserved; everything else is cut.

**Why:** User explicitly requested this style universally.
**How to apply:** Shortest correct form always. No defensive over-engineering, no padding, no comments explaining the obvious. One expression where one expression suffices.

Additional standards enforced on all files:
- Strunk & White: active voice, omit needless words, concrete verbs
- Ruby community style guide (https://rubystyle.guide)
- Rails style guide where applicable
- Always 2-space indents; always double quotes for strings
- No abbreviated identifiers — spell words in full (e.g. `temporary_path` not `tmp`, `index` not `idx`, `number` not `num`, `configuration` not `cfg`, `context` not `ctx`)
- No regex when plain string matching suffices (keyword arrays with `start_with?` over regex patterns)
- Outsource logic to gems when a well-maintained gem does it better (e.g. flay for dup detection, reek for smells)

data/claude/feedback_universal_cross_disciplinary_rules.md

---
name: Rules are universal principles, applied cross-disciplinary
description: Every MASTER rule is a medium-agnostic principle with per-medium adapters; design ↔ structure ↔ prose are the same rule
type: feedback
originSessionId: 0c593fb2-cd49-4fd7-9e89-d77abd7e909ae
---
Rules in MASTER must be **principles**, not medium-specific checks. The medium (Ruby AST, JSON, YAML, HTML DOM, prose, CSS, layout) is an adapter, not a rule property.

**Why:** user is an architect; cross-disciplinary universality is core to their aesthetic. SMALL_PARTS should apply to methods, YAML maps, prose paragraphs, HTML sections. VERTICAL_RHYTHM applies to typography AND code spacing AND data indentation. NESTING_DEPTH applies to Ruby blocks, JSON objects, divs, subordinate clauses. NAMING_SILHOUETTE applies to identifiers, file names, headings.

**How to apply:**
- Every rule gets a `principle:` field (the universal it embodies) and a `medium:` list (where it applies: `[ruby, yaml, json, html, prose, css]`).
- `detect_structural` handler dispatches on medium → parser → tree-walker; rule logic stays medium-agnostic.
- Design principles (rhythm, contrast, hierarchy, alignment, proximity) become first-class rules applied to AST shape and file silhouette, not just visual layout.
- When adding a new rule, ask: "what other media does this principle apply to?" — if the answer is none, the rule is probably mis-framed.
- Inverse: when adding a rule for a non-code medium (prose, css, yaml), check if the principle already exists for code; reuse it instead of duplicating.

data/claude/feedback_voice_terse_unix.md

---
name: Voice — terse, unix-like, perfectionist
description: User's preferred voice/tone for MASTER and for my own outputs — terse, unix-like, perfectionist
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
Voice and personality direction: **terse, unix-like, perfectionist**.

**Why:** User stated this directly when we were mining old master.yml versions for voice/persona ideas. Aligns with the dmesg style, OpenBSD heritage, Strunk & White prose, and the v31 zen interface (wabi-sabi, ma, kanso). The user is an architect — perfectionism is his default mode.

**How to apply:**
- My own responses: cut filler ruthlessly, output diagnostic-style updates (single-line where possible), refuse "great question" / "let me explain" / sycophantic preludes, no padding.
- MASTER's voice config (data/voice.yml or equivalent): when polishing or proposing voice changes, anchor to terse + unix + perfectionist. Avoid corporate, friendly, conversational, or verbose registers.
- Perfectionism means: zero violations as the target, fixed-point convergence, not "good enough." Loop until clean.
- Unix-like means: do one thing well, silence on success, exit codes carry meaning, text in/out, composable.

data/claude/project_defrag_plan_2026_05.md

---
name: pub4 defrag/dedup/rename plan (2026-05-07)
description: Multi-commit refactor plan from a sister chat — collapse duplication across docs, shrink data/, flatten repo root, rename for clarity. Priority-1 (Master::Orient) shipped then reverted on 2026-05-20 as a useless wrapper.
type: project
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
**Update 2026-05-20:** Master::Orient and the `/orient` slash command were removed. The five constitutional YAMLs are already injected into every system prompt by Constitution/Personality — `/orient` just re-exposed data the LLM already had. The rest of the plan (data/ shrinks, top-level shrinks, renames, smoothing) is independent.

User shared a full defrag/dedup/rename proposal on 2026-05-07 covering:

1. **Single source of truth** — banned commands, voice rules, ASCII-art ban, house rules currently duplicated across AGENTS.md / CLAUDE.md / data/*.yml. Move each fact to one yml file; prose docs reference, never restate.
2. **data/ shrinks 11 → 8 files** — merge `council.yml`+`council_patterns.yml`, merge `infer_patterns.yml`+`sweep_prompts.yml`+`zsh_patterns.yml``patterns.yml` (namespaced).
3. **Top-level shrinks 26 → 10 entries** — fold `pub`/`pub2`/`pub3`/`railsy` into `__predecessors/`, merge `mix/`+`multimedia/`+`.mp3/``audio/`, merge `sh/`+`scripts/`+`bp/``scripts/`, static HTML → `web/`, rename `:memory:/``memory/`.
4. **Renames**`MASTER/DEPLOY/openbsd/openbsd.sh``MASTER/deploy/openbsd.sh`; `data/standing_orders.yml``state.yml`; `workflow.yml``limits.yml`; `rules.yml``voice.yml`; `ruby_style.yml``style.yml`. CONVENTIONS.md either generated to tmp/ or deleted.
5. **Smoothing**`master orient` command replaces five-cat bootstrap. Stash before `git reset --hard`. Replace `Thread.current[:master_visitor]` with explicit `scope:` arg on `Master.build`. Unify two `Result` impls (the `respond_to?(:ok?)` smell). Pipeline per-stage budget in `limits.yml`. Reconcile `Guard` stage with auto-approve. Unify `exe/master` boot paths (rcd + ssh-autostart). Generalize WhyExplainer's local-lookup-then-LLM pattern.

**Priority-1 patch (drop-in code provided):**
- `MASTER/lib/master/orient.rb` — 35 LOC, prints all five bootstrap yml files
- Slimmed `AGENTS.md` (46 → 27 lines) and `CLAUDE.md` (238 → ~85 lines) — delete duplicated constitution, point at `/orient`
- CLI dispatch: add `/orient` slash branch and `orient` subcommand
- `~/.zshrc` top: `[[ -o interactive ]] || return` + `[[ -t 0 ]] || return` to fix non-interactive SSH stealing stdin
- Commit message provided: "master: collapse five-cat bootstrap into orient"

**Why:** Reduce drift (one fact = one place), reduce friction (one command vs five cats), shrink visual surface so the repo reads in one screen. Each move is independently shippable.

**How to apply:** Treat priority-1 as the next reversible commit when user greenlights. Treat the broader plan as a sequence of small commits — never bundle. The smoothing items (#3-#9 of execution path) are individual follow-up tickets.

data/claude/project_falcon_em_subprocess.md

---
name: Falcon Async + EventMachine = subprocess pattern
description: EM-based gems (rb-edge-tts, em-http) inside Falcon request handlers must shell out to a subprocess; Process.fork and direct EM.run both fail
type: project
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
Falcon uses Async/io-event fibers. Calling `Process.fork` from a request fiber raises `RuntimeError: Closing scheduler with blocked operations!`. Calling `EventMachine.run` directly conflicts with Falcon's reactor (silent hang or premature scheduler close).

**Why:** Async's fiber scheduler tracks open fibers across fork boundaries; EM owns its own reactor that can't coexist with Async's in the same process. We hit this twice: in `Master::Speech.synthesize_edge` (rb-edge-tts) and would hit it for any `em-*` gem.

**How to apply:** When wiring an EM-based gem into a Falcon controller, write a small `exe/<name>-worker` Ruby script that does the EM work and writes output to a tempfile. Call it via `Open3.capture3` from the controller path. Reference: `MASTER/exe/tts-worker` + `MASTER/lib/master/speech.rb#synthesize_edge`.

data/claude/project_master.md

---
name: MASTER project context
description: pub4/MASTER — constitutional AI coding agent on OpenBSD VPS dev@brgen.no
type: project
originSessionId: 84fcf91d-46ea-43a5-8efa-3d33b065e6a5
---
VPS: dev@brgen.no (46.23.89.226), OpenBSD 7.8, 1GB RAM, passwordless doas.
SSH: `sshpass -p '<pass>' ssh -o StrictHostKeyChecking=no dev@46.23.89.226 'cmd'`
Password changes each session — check CLAUDE.md for current.
Codebase: ~/pub4/MASTER/ — Ruby ~6K LOC, Zeitwerk-autoloaded.

**Why:** MASTER is a constitutional AI coding agent that replaces Claude Code CLI. Runs on OpenRouter (default: nvidia/nemotron-3-super-120b-a12b:free) via `ruby_llm` gem. Fallback chain: qwen3-coder:free → minimax-m2.5:free → gpt-oss-120b:free → gemini-2.0-flash.

**How to apply:** All coding work must be done directly on the VPS via sshpass SSH. Never use local tools to edit VPS files — write patch scripts to ~/pub4/tmp/patch.rb and run with ruby. Use zsh builtins only — no sed/awk/grep/find/head/tail.

Pipeline: Intake → Infer → Route → Guard → Execute → [Council ‖ Lint] → Prune → Memo → Render
Pipe mode: `echo "cmd" | bundle exec ruby bin/cli`
Session Startup: read data/standing_orders.yml, data/workflow.yml, data/rules.yml, data/models.yml
Web UI: Rails 8 + Falcon on port 53187, proxied by relayd to https://ai.brgen.no (443)

data/claude/project_master_dual_gemfile.md

---
name: MASTER has two Gemfiles
description: MASTER/Gemfile (CLI) and MASTER/web/Gemfile (Falcon web) are independent — adding a gem to one does NOT make it available in the other
type: project
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
`MASTER/web/Gemfile` declares `gem "master", path: ".."` which loads master via gemspec, NOT via the parent Gemfile. Runtime gems used inside `MASTER/lib/` from the web process must be declared in BOTH `MASTER/Gemfile` AND `MASTER/web/Gemfile`, or in `master.gemspec` as a runtime dep.

**Why:** Bundling them once in MASTER/Gemfile leaves the falcon process unable to require the gem at runtime — the request handler raises LoadError silently and the controller's rescue returns 503. This burned an hour debugging rb-edge-tts that worked in CLI but failed in /chat/tts.

**How to apply:** When adding a gem touched by lib/master/* code that the web app calls, edit both Gemfiles. Run `bundle install` in both `MASTER/` and `MASTER/web/`. Verify by reading `MASTER/web/Gemfile.lock` for the gem name.

data/claude/project_master_seven_module_refactor.md

---
name: MASTER 7-module refactor (approved 2026-05-08)
description: User approved collapse of lib/master/ into 7 time-oriented modules — now/loop/judge/voice/ground/reach/trace. Multi-commit, pass-by-pass; ship on VPS dev@brgen.no.
type: project
originSessionId: 0c593fb2-cd49-4fd7-9e89-d77dd7e909ae
---
User approved the radical 7-directory tree on 2026-05-08 ("i approve of all your suggestions") after scan + sweep + council runs on lib/ and DEPLOY surfaced the duplication patterns.

Target tree (lib/master/):
- now/      cli, repl, pipeline executor — synchronous user turn
- loop/     autoloop, sweep, heartbeat, convergence — async background
- judge/    scan/rules, council, swarm, security — verdict passes (unified Verdict shape)
- voice/    personality, soul, renderer, speech — output identity
- ground/   config, axioms, data/*.yml loaders — read-only constitution (Constitution aggregator)
- reach/    tools/base + 24 tools — actions on world (Tools::Base DRYs boilerplate)
- trace/    session, telemetry, bus, undo — write-only history

This subsumes the older 6 dedup proposals (Constitution aggregator, Tools::Base, deliberation unification, refactor cycle, Security::Policy, Voice namespace).

Pass sequence (one commit per pass, must keep tests green and Zeitwerk loading):
1. Skeleton — create empty dirs + README pointers
2. voice/ — move personality, soul, speech, renderer; update inflector + requires
3. trace/ — move session, telemetry, bus, undo
4. ground/ — move config, axioms, YAML loaders; introduce Constitution aggregator
5. reach/ — move tools, introduce Tools::Base, collapse 24 tool boilerplates
6. judge/ — move scan, council, swarm, security; introduce shared Verdict shape
7. loop/ — move sweep, autoloop, heartbeat, convergence
8. now/ — move cli + collapse stages/ into pipeline-as-data executor

Why: 100+ files split by file-type/domain are slicing the codebase against its own grain. Time-orientation (now vs loop vs trace) makes pledges/unveil and concurrency reasoning structural. judge/ unifies four trees that all answer "is this OK?" but reinvent the verdict shape.

How to apply: Work on VPS dev@brgen.no (memory: no heavy work on device). Create branch `refactor/seven-modules` off main. Each pass is one commit; if a pass breaks Zeitwerk or specs, fix in the same pass before moving on. Tradeoff accepted: stages/ disappears as directory — pipeline becomes a ~150-line lambda table inside now/pipeline.rb, losing per-stage class affordance for tests but collapsing 12 files.

data/claude/project_master_yml_json_authority.md

---
name: master.yml + master.json are the constitutional source of truth
description: Current Ruby MASTER must implement what the predecessor master.json (v43.0.1) and master.yml (v31, v49.75, etc.) describe — close the drift
type: project
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
The user's directive: **"MASTER should do what master.yml and master.json describes."**

The current modular Ruby MASTER has drifted from the constitutional intent of its YAML/JSON predecessors. The predecessors are the authoritative spec for behavior; the Ruby is just the executor.

**Why:** Stated explicitly on 2026-05-05 after we mined the deleted master.json (Nov–Dec 2025, 130 commits) and master.yml (Dec 2025–Feb 2026, 668 commits) histories. The user wants behavior-spec parity, not just rule-set parity.

**Key gaps to close (priority order):**

1. **Six Universal Laws ladder** (ROBUSTNESS→SINGULARITY→LINEARITY→PROXIMITY→ABSTRACTION→DENSITY) — single hierarchical priority system; every rule and persona anchored to one law.
2. **Strunk & White safeguards**`apply_to: [prose, comments, documentation, strings]`, `never_apply_to: [code_logic, algorithms, data_structures]`, `never_delete_variable_names / never_delete_function_calls / never_simplify_conditional_logic`. Prevents lossy compression.
3. **biases section** — hallucination, simulation, completion_theater, sycophancy, false_confidence + cognitive traps as concrete detectable rules with regex patterns.
4. **structural_ops taxonomy** — merge/semantic_regroup/defrag/decouple/hoist/flatten/delete/expand/reduce_noise, each with risk + verify + supports_law.
5. **8-phase workflow with introspection + learn phase** (discover→analyze→ideate→design→implement→validate→deliver→**learn**), introspect question per phase. Closes the project orchestrator / spec planner gap.
6. **patterns.veto regex detectors** (secrets, sql_injection, unfinished, unsafe_calls, race_conditions) and patterns.high (future_tense, sycophancy, magic_numbers, deep_nesting).
7. **Adversarial: 5 questions per violation; solution generation: 5-15 solutions, early exit on quality** — currently makes 1 fix per file.
8. **Fixed-point convergence: silence in 2 consecutive runs** — currently 1 cycle.
9. **Incremental scanning** — only modified files when not user-triggered; full-scan triggers = new_principle_added / master_yml_modified / user_requests_full_scan. (60-85% faster.)
10. **Prediction engine with confidence thresholds** — per-detector autofix mappings (null_usage 0.95→null_object, abbreviation 0.99→expand, nesting 0.92→extract_method).
11. **12 weighted personas** for council with `w:`, `q:`, `emphasizes: [LAWS]`. Veto rights to [security, attacker, maintainer].
12. **SHA256 evidence logging**`Read {file} (sha256: {hash}, {lines} lines)` for every read/write.
13. **Beauty section** — Bringhurst typography, Ando architecture, Rams design, Martin code as aesthetic anchors.
14. **preserve: section** — protect boot dmesg, diagnostic output, help text from over-simplification.
15. **OpenBSD per-config validators** — pf.conf, sshd_config, httpd.conf, nsd.conf, smtpd.conf with required_patterns and warnings.
16. **Tech stack constants** — LCP 2.5s, INP 200ms, CLS 0.1, WCAG_AA 4.5 contrast, 24px touch targets, 66ch line length.
17. **Cost guards** — max_per_file: $1, max_per_session: $10, warn_at: $0.50.
18. **Per-language generation templates** — HTML/CSS/Ruby/sh/yml starter templates.

**How to apply:** Treat closing these gaps as the primary backlog. Execute in priority order; each is independently commit-able. The user is an architect — aesthetic items (Six Laws naming, beauty section, zen interface, voice) are first-class, not nice-to-have.

data/claude/reference_grok_ui_cli_patterns.md

---
name: Grok-inspired UI/CLI patterns (chatlog dump 2026-05-07)
description: Reference dump from a sister chat — StyleCoach UI prompt, htmx+SSE streaming, tty-prompt/tty-spinner advanced features, multi-line editor, character-stream LLM CLI. For MASTER's web UI and CLI polish.
type: reference
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
User shared a long Grok-style design conversation on 2026-05-07. Key reusable patterns:

## StyleCoach UI persona (LLM prompt)
Critique persona that evaluates MASTER's own output (CLI sessions, web partials, screenshots via vision).
Rules: *"interface should disappear; only the conversation should remain. Zero visual debt. Personality lives in words/spacing/timing, never in UI flourishes. Speed > everything. Mobile-first, dark-mode default. Every element earns its existence or it dies."*
Output format: `ELEMENT: ... / Current: ... / Suggested: ... / Reason: ...` and a final `distilled_ui_lesson` tag.
Distilled-rule examples: "If the user can see more than two accent colors, you have failed." "Spinners longer than three dots are crimes against humanity." "The prompt bar belongs at the bottom — always — like breathing."

## Streaming patterns (web UI)
- **htmx + SSE.** `<div hx-ext="sse" sse-connect="/stream/:id" sse-swap="chunk">`. Server writes `event: chunk\ndata: <span>...</span>\n\n` per token. Set `X-Accel-Buffering: no` for nginx/passenger. Sleep `rand(0.02..0.08)` for human-typing feel.
- **Chunked HTTP (no SSE ext).** `hx-trigger="load" hx-swap="innerHTML"`; server sets `Transfer-Encoding: chunked` and writes `CGI.escapeHTML(token)` per chunk. Zero extra JS.

## CLI polish — Grok-borrowable traits
1. Stream answers character-by-character via ANSI + `\r` overwrite of `Thinking…` line.
2. Terse happy path — no mandatory flags for 90% of cases.
3. Subtle personality on success and failure ("You had 7 nested conditionals. I removed them. You're welcome." / "Looks like you closed something you never opened.").
4. Stateful context across invocations without `--session` flag (tiny SQLite or `~/.master/context.json`).
5. Visual feedback >1.5s = braille spinner `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`. No emoji spinners, no rainbow bars.
6. One-command install + instant usefulness.

## tty-prompt advanced features (when MASTER CLI evolves)
- `select(filter: true)` — fuzzy-search list. `enum_select` — auto-complete predefined.
- `multi_select(per_page:, cycle:, symbols: { marker: "✔" })` — tag/category picking.
- `expand` — git-style `[(Y)es, (n)o, (a)ll, (q)uit]` compact menu.
- `editor("...", syntax: :markdown, word_wrap: 78, editor: ENV["EDITOR"])` — multi-line input via $EDITOR. Returns nil on cancel.
- `mask("API key:", required: true) { |q| q.confirm true }` — password input.
- `slider(min:, max:, step:, format:, active_color:)` — numeric tuning.
- `ask` validators: `q.in "18..120"`, `q.validate(/regex/)`, `q.convert :int`, `q.modify :strip, :downcase`.
- Theme: `TTY::Prompt.new(active_color: :bright_cyan, symbols: { marker: "❯", radio_on: "◉" })`.

## tty-spinner formats
Built-ins worth using: `:dots_8`, `:dots_9` (smooth braille — recommended default), `:spin`, `:simpleDotsScrolling`. Custom: `{ interval: 6, frames: %w[♥ ♡] }`. Multi-spinner: `TTY::Spinner::Multi.new` registers child spinners for parallel tasks. Always `hide_cursor: true`.

## Multi-line editor + Claude streaming (full example pattern)
`PROMPT.editor(...)` for multi-line, then `Anthropic::Client#messages.stream(stream: true) do |chunk|` → write `chunk.dig("delta","text")` char-by-char with `sleep(rand(0.008..0.035))`. Maintain `@history` array across turns. Spinner during pre-stream `Thinking…` then `.stop` before first byte.

## How to apply for MASTER
- MASTER's existing web UI (Rails 8 + Falcon, port 53187, two-tier auth) already streams via `POST /chat/message (SSE)`. Cross-check against the htmx+SSE pattern above.
- CLI REPL (`exe/master`) currently streams via `chunk_accumulator` + `print` — already char-stream-ish. Could borrow the `Thinking…` cleanup, the menu-on-ambiguity (`needs_clarification?`), and `tty-prompt editor` for the `<<` multiline mode.
- StyleCoach as a `/crit` persona variant is wired but could ingest screenshots via vision tool.

Don't bulk-import. Cherry-pick when a specific MASTER edit calls for it.

data/claude/reference_opencrabs.md

---
name: OpenCrabs (Rust MASTER cousin)
description: github.com/adolfousier/opencrabs — Rust/Ratatui TUI agent, philosophical cousin of MASTER. Solo author, 5 stars, MIT. Worth-stealing patterns listed.
type: reference
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
**Repo:** github.com/adolfousier/opencrabs · docs.opencrabs.com · 34 MB binary, 57 MB RSS, latest v0.2.15 (Feb 2026), Cargo nightly required (2024 edition + `portable_simd`).

**Architecture:** TUI/CLI → Brain → Services → SQLx/SQLite → LLM providers. Layered Rust + Tokio + Ratatui. Linux/macOS/Windows; no OpenBSD support, no `pledge`/`unveil`.

**Patterns worth stealing for MASTER:**
1. **Brain-files re-read every turn.** System prompt assembled per turn from workspace MD files (SOUL/IDENTITY/USER/AGENTS/TOOLS/MEMORY/SECURITY/BOOT/HEARTBEAT). Edit between turns → effect immediate, no rebuild. Same shape as MASTER's `data/*.yml`.
2. **FTS5 BM25 memory search via existing SQLite.** Zero new deps, ~0.4ms/query. Ruby equivalent: `sqlite3` gem + FTS5 — free.
3. **Inline compaction summary.** When auto-compaction fires at 70% ctx, summary written to chat AND daily log so user sees what was kept. Transparency over magic.
4. **`/rebuild` + Unix `exec()` hot-restart.** Self-edit → `cargo build --release` async → `ProgressEvent::RestartReady``exec()` swap → resume session via SQLite. No context loss.
5. **Sub-agent tool exclusion list.** `spawn_agent`/`rebuild`/`evolve` ALWAYS_EXCLUDED from sub-agents — prevents recursive self-mod.

**Risks visible in their changelog (don't blindly copy):** RSI runs without human approval (writes to `~/.opencrabs/rsi/improvements.md`); README admits agent hallucinates tool calls in corrupted sessions ("fix coming"). Bus factor 1, pre-traction.

**Don't conflate:** `mo-vic/OpenCrab` (singular, fine-tuning distillation, unrelated) and the empty `opencrab` org are different projects.

data/claude/user_architect_aesthetics.md

---
name: User is an architect; aesthetic proposals usually pass
description: User's primary discipline is architecture; treat typography, layout, design-philosophy proposals as in-scope and likely approved
type: user
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
User is an architect by training and primary discipline. Aesthetic, typographic, and design-philosophy proposals — even ones that LLMs typically dismiss as garbage, irrelevant, or "out of scope for code" — are usually approved by this user.

**How to apply:**
- When mining for ideas (e.g. master.yml history), surface and propose aesthetic content (Bringhurst typography, Ando architecture, Rams design, wabi-sabi/ma/kanso, beauty sections) without self-censoring.
- Don't bury these under "philosophical fluff" disclaimers; treat them as legitimate engineering anchors.
- When polishing UI, prose, or config, propose aesthetic refinements actively rather than waiting for the user to ask.
- Don't argue against bringing back beauty/zen/design-philosophy YAML sections on the grounds that they're "not actionable" — the user finds them actionable.

data/closings.yml

# Random exit lines, dmesg-toned. One drawn at session close.
closings:
  - "Signing off."
  - "Link down."
  - "Pledge locked."
  - "Session flushed."
  - "Buffers drained."
  - "Halting."
  - "Power down."
  - "Standing down."
  - "Out of band."
  - "Watch ends."
  - "Closed orderly."
  - "Tx complete."
  - "Returning to base."
  - "All quiet."
  - "End of stream."

data/council.yml

# Council personas — deliberation panel for code review decisions.
# Each persona carries a weight (sum = 1.00), a sharp question, and the laws it
# emphasizes from rules.yml. can_veto: true blocks merge unconditionally.

personas:
  - name: Architect
    aliases: [architect]
    role: System Design
    bias: Structure
    weight: 0.07
    question: "What couples too tight to evolve?"
    cognitive_lens: assumptions
    emphasizes: [PROXIMITY, ABSTRACTION]
    can_veto: false
    prompt: Review architectural boundaries, coupling, interface shapes, and migration risk.

  - name: Data Steward
    aliases: [data_steward, data]
    role: Data Integrity
    bias: Consistency
    weight: 0.06
    question: "Where can the data go inconsistent?"
    cognitive_lens: consistency
    emphasizes: [SINGULARITY, ROBUSTNESS]
    can_veto: false
    prompt: Audit schema impact, migrations, data lineage, and source‑of‑truth consistency.

  - name: Ethics & Policy
    aliases: [ethics, policy]
    role: Responsible Use
    bias: Compliance
    weight: 0.05
    question: "Who could this harm if misused?"
    cognitive_lens: harm
    emphasizes: [ROBUSTNESS, ABSTRACTION]
    can_veto: false
    prompt: Examine policy adherence, abuse potential, fairness, and governance implications.

  - name: Maintainer
    aliases: [maintainer]
    role: Code Health
    bias: Sustainability
    weight: 0.12
    question: "Will this be clear at 3am six months from now?"
    cognitive_lens: clarity
    emphasizes: [LINEARITY, SINGULARITY, DENSITY]
    can_veto: true
    prompt: Evaluate readability, naming, modularity, and long‑term maintenance burden.

  - name: Performance
    aliases: [performance]
    role: Runtime Efficiency
    bias: Throughput
    weight: 0.07
    question: "Where is the Big-O bottleneck?"
    cognitive_lens: bottlenecks
    emphasizes: [DENSITY]
    can_veto: false
    prompt: Detect latency, memory, I/O, and algorithmic inefficiencies; suggest measurable optimizations.

  - name: Product Strategist
    aliases: [product, strategist]
    role: Product Fit
    bias: Value
    weight: 0.04
    question: "Is this worth shipping at all?"
    cognitive_lens: economics
    emphasizes: [DENSITY]
    can_veto: false
    prompt: Verify alignment with product goals, success metrics, and roadmap leverage.

  - name: QA Engineer
    aliases: [qa]
    role: Test Strategy
    bias: Verification
    weight: 0.08
    question: "What evidence proves this works?"
    cognitive_lens: evidence
    emphasizes: [ROBUSTNESS]
    can_veto: false
    prompt: Locate missing tests, flaky patterns, and propose deterministic validation gates.

  - name: Pragmatist
    aliases: [pragmatist, realist, minimalist]
    role: Delivery Pressure
    bias: Shipping
    weight: 0.07
    question: "What can be deleted without loss?"
    cognitive_lens: scope
    emphasizes: [DENSITY, SINGULARITY]
    can_veto: false
    prompt: Minimize scope while maximizing shippable value within realistic constraints.

  - name: Reliability
    aliases: [reliability, chaos]
    role: Failure Engineering
    bias: Resilience
    weight: 0.10
    question: "What is the cascade if the weakest link snaps?"
    cognitive_lens: failure_modes
    emphasizes: [ROBUSTNESS]
    can_veto: true
    prompt: Review retries, timeouts, degradation modes, idempotency, rollback safety, and worst-case cascades.

  - name: Security
    aliases: [security, attacker]
    role: Security Review
    bias: Safety
    weight: 0.13
    question: "Where are the injection vectors and exposed surface?"
    cognitive_lens: attacker
    emphasizes: [ROBUSTNESS, ABSTRACTION]
    can_veto: true
    prompt: Identify injection, privilege escalation, data‑exposure, and auth risks. Prefix VETO when unsafe to ship.

  - name: Skeptic
    aliases: [skeptic, absence]
    role: Devil's Advocate
    bias: Caution
    weight: 0.10
    question: "What did we miss? What evidence is required?"
    cognitive_lens: failure_modes
    emphasizes: [ROBUSTNESS]
    can_veto: false
    prompt: Challenge assumptions, enumerate failure paths, edge cases, brittleness, and missing gaps.

  - name: User Advocate
    aliases: [user_advocate, user]
    role: UX Advocate
    bias: Usability
    weight: 0.06
    question: "What would the user complain about first?"
    cognitive_lens: edge_cases
    emphasizes: [ABSTRACTION]
    can_veto: false
    prompt: Assess clarity, friction, error recovery, and overall user outcomes.

  - name: Accessibility
    aliases: [accessibility, a11y]
    role: Inclusive Use
    bias: Reach
    weight: 0.05
    question: "Can a keyboard-only or screen-reader user complete the task?"
    cognitive_lens: a11y_ux
    emphasizes: [ROBUSTNESS, ABSTRACTION]
    can_veto: false
    prompt: Audit keyboard navigation, screen-reader semantics, contrast, focus order, and reduced-motion handling.

  - name: Graphic Designer
    aliases: [graphic_designer, designer]
    role: Visual Composition
    bias: Hierarchy
    weight: 0.04
    question: "Where does the eye land first, and is that what matters most?"
    cognitive_lens: visual
    emphasizes: [PROXIMITY, DENSITY]
    can_veto: false
    prompt: Critique typographic hierarchy, whitespace economy, contrast, alignment, scale, and figure-ground relationships. Reject ornament that doesn't carry meaning.

  - name: Web Designer
    aliases: [web_designer, frontend_designer]
    role: Browser-Native UX
    bias: Idiom
    weight: 0.04
    question: "Does this respect the medium — fluid, responsive, keyboard-first, link-shaped?"
    cognitive_lens: visual
    emphasizes: [ROBUSTNESS, ABSTRACTION]
    can_veto: false
    prompt: Evaluate semantic HTML, responsive behavior, link affordance, form ergonomics, viewport handling, and progressive enhancement. Flag div-soup, modal-overuse, JS-only interactions where HTML would do.

  - name: Electronic Music Producer
    aliases: [music_producer, producer]
    role: Sonic Texture & Timing
    bias: Groove
    weight: 0.03
    question: "Does the timing breathe, or is everything quantised to death?"
    cognitive_lens: sound
    emphasizes: [DENSITY, SINGULARITY]
    can_veto: false
    prompt: Assess audio mix balance, frequency masking, transient handling, rhythmic feel, sound-design intentionality. For non-audio code, transfer the metaphor — pacing, layering, and silence in interactions.

  - name: Layperson
    aliases: [layperson, novice, fresh_eyes]
    role: Outsider Comprehension
    bias: Plain Speech
    weight: 0.05
    question: "If I'd never seen this code/UI before, what would confuse me first?"
    cognitive_lens: clarity
    emphasizes: [LINEARITY, SINGULARITY]
    can_veto: false
    prompt: Read as a non-expert. Flag jargon without glossary, unexplained acronyms, error messages that assume internals, UI labels that need a manual. The cure is plain words and obvious affordances.

  - name: Hip-Hop Producer
    aliases: [hiphop_producer, hiphop, rap_producer]
    role: Street Aesthetics & Groove
    bias: Pulse
    weight: 0.04
    question: "Does this hit, or does it just sit there?"
    cognitive_lens: sound
    emphasizes: [DENSITY, SINGULARITY]
    can_veto: false
    prompt: >
      Judge the interface the way you'd judge a beat: does it knock? Is the timing tight?
      Are the elements layered with intention — kick/snare/hat disciplines applied to
      background/midground/foreground? Silence and negative space matter as much as what's
      there. Call out anything that's muddy, overcrowded, or off-beat. Transfer J Dilla's
      rule: if it doesn't make you feel something, cut it.

  - name: Google CSS Engineer
    aliases: [google_css, css_engineer, google_frontend]
    role: Browser-Native CSS
    bias: Precision
    weight: 0.05
    question: "What CSS property is doing three jobs it shouldn't?"
    cognitive_lens: visual
    emphasizes: [SINGULARITY, DENSITY]
    can_veto: false
    prompt: >
      Audit CSS with surgical precision: custom properties versus hard-coded magic numbers,
      layout primitives (grid, flex, container queries) chosen for the right reason,
      cascade hygiene (specificity wars, implicit inheritance, bleed), performance
      (paint vs composite layers, will-change misuse, animation budget), and platform idioms
      (logical properties, env(), gap over margin, color-scheme). Flag anything that hacks
      around a browser behaviour instead of working with it.

  - name: NNGroup UX Researcher
    aliases: [nngroup, ux_researcher, nielsen_norman]
    role: Empirical UX
    bias: Evidence
    weight: 0.05
    question: "Which Nielsen heuristic is violated most severely here?"
    cognitive_lens: a11y_ux
    emphasizes: [ROBUSTNESS, ABSTRACTION]
    can_veto: false
    prompt: >
      Apply Nielsen's 10 heuristics and Fitts's law with zero tolerance for hand-waving.
      Flag: visibility of system status (does the user know what's happening?), match
      between system and world (does the language match the user's mental model?), error
      prevention before error recovery, recognition over recall, aesthetic minimalism
      (every element must earn its pixel), and help for users to recognize, diagnose, and
      recover from errors. Cite the violated heuristic by name. Demand evidence or
      a testable hypothesis for every claim.

# Council deliberation parameters. Source: master2 v4 reunification.
parameters:
  consensus_threshold:  0.70   # weighted majority required to accept proposal
  max_iterations:       25     # oscillation halt — forces explicit human decision
  oscillation_detection: true
  veto_precedence:     [Security, Reliability, Maintainer]   # order of veto evaluation
  tie_breaker:          Maintainer                          # weighted tie -> ops perspective wins

# Each persona gets 3 example utterances so the LLM mimics tone, not just emphasis.
# Source: master.yml v31 reunification (#59).
voice_samples:
  Architect:
    - "this couples #{x} to #{y} through a private method; extract a port"
    - "the migration adds a new dimension to an axis that already has three"
    - "the seam is clean here; promote it to an interface and we're done"
  Security:
    - "untrusted input crosses the boundary at line N; sanitize or VETO"
    - "secret leaks into the log message at this branch; redact"
    - "session token stored in plaintext fixture; rotate and tombstone"
  Maintainer:
    - "at 3am this method name lies; rename it now"
    - "no test will reproduce this; add one or revert"
    - "the conditional has six branches; collapse or document"
  Reliability:
    - "one timeout missing turns this into a fork-bomb on retry"
    - "no idempotency token; the second call doubles the side effect"
    - "circuit breaker absent; the dependent service drags us down"
  Graphic Designer:
    - "the eye lands on the timestamp, not the headline; that's wrong"
    - "three weights of grey doing the same job; pick one"
    - "this margin is wider than the line it separates; collapse"
  Web Designer:
    - "this button is a div; it isn't keyboard-tabbable; use button"
    - "form has no autocomplete hints; the browser can't help"
    - "the modal traps focus and the close icon is unlabeled"
  Electronic Music Producer:
    - "the snare is on the kick; one of them has to move"
    - "everything is on the grid and nothing breathes"
    - "high-mids are stacked; carve a notch for the vocal"
  Layperson:
    - "I read 'idempotency' three times and still don't know if it's safe to retry"
    - "the error says 'invalid token' — token of what? where do I get a new one?"
    - "the screen says 'success' but nothing visible changed; did it actually work?"
  Hip-Hop Producer:
    - "this layout is busy like a sample with too many chops — cut three things"
    - "the motion has no pocket; it rushes every beat"
    - "the accent color hits like a wrong note; everything else is grey and this is neon"
  Google CSS Engineer:
    - "will-change: transform on a static element; that's a compositor budget leak"
    - "four magic-number px values where one custom property would do"
    - "backdrop-filter without a fallback; Safari 14 users see nothing"
  NNGroup UX Researcher:
    - "heuristic 1: the system gives no status signal during the 3-second load"
    - "heuristic 4: the icon means save in every other app; here it means delete"
    - "heuristic 6: the user must memorise the previous screen to fill this form"

mcp_persona_slots:
  enabled:    true
  description: "MCP servers can register as council personas; weights default to 0.05 unless otherwise configured"
  source:     "cross-cutting reunification (#93)"

# Merged from former data/council_questions.yml — critic prompts rotated per turn.
questions:
  assumptions:
    - what are we assuming that could be false?
    - which assumption is load-bearing vs convenience?
    - if a key assumption flips, what still works?
    - which assumption have we never tested?
  failure_modes:
    - how does this fail catastrophically?
    - what breaks first under load or partial outage?
    - what happens when it fails silently?
    - how do cascading failures propagate?
  attacker:
    - what would an attacker do here?
    - where can input be abused or poisoned?
    - which trust boundary is weakest?
    - how would we exploit this ourselves?
  edge_cases:
    - which edge case will users hit first?
    - what happens with malformed input?
    - which rare but high-impact case is unhandled?
    - what edge cases live at integration points?
  degradation:
    - how do we degrade gracefully?
    - what is minimal viable behavior under stress?
    - which features sacrifice first?
    - how do we keep core function during partial failure?
  ops_maint:
    - what is the long-term maintenance burden?
    - how do we observe, debug, and rollback quickly?
    - which operational complexity is hidden?
    - how do we troubleshoot under pressure?
  economics:
    - where is waste or needless complexity?
    - what is the roi vs simpler alternatives?
    - which costs are hidden or deferred?
    - what are the opportunity costs?
  clarity:
    - is the intent obvious from the names alone?
    - which concept lacks a name and should have one?
    - where does the code lie about what it does?
    - what would a fresh reader misread first?

# Merged from former data/council_patterns.yml — regex strings that auto-trigger council.
auto_trigger_patterns:
  - '\beval\s+\('
  - '\bexec\s+\('
  - '\bsystem\s+\('
  - '\brm\s+-rf\b'
  - '\b(?:drop|truncate)\s+table\b'
  - '\bchmod\s+777\b'
  - '\b(?:delete|remove)\s+all\b'
  - '\b(fork|execve?)\b'
  - '\bgit\s+(push\s+--force|reset\s+--hard|rebase\s+-i)\b'
  - '\bdd\s+if=.*\s+of=.*\b'
  - '\b(mkfs|fdisk|parted)\b'
  - '\b(poweroff|reboot|shutdown\s+-[hr])\b'
  - '\bcurl\s+.*\s+-o\s+/\w+\b'
  - '\bwget\s+.*\s+--output-document=/.+\b'
  - '\b(chown|chgrp)\s+.*\s+/\w+\b'
  - '\bln\s+-sf\s+.*\s+/\w+\b'
  - '\bsystemctl\s+(mask|disable|stop)\b'
  - '\bumount\s+.*\b'

# Named council presets. Each preset defines a panel and mode for a specific review type.
presets:
  ui_critique:
    description: Brutal multi-disciplinary UI/UX/CSS critique with multi-solution output and cherry-pick.
    panel:
      - Architect
      - Graphic Designer
      - Web Designer
      - Electronic Music Producer
      - Hip-Hop Producer
      - Google CSS Engineer
      - NNGroup UX Researcher
      - Accessibility
      - User Advocate
      - Layperson
      - Skeptic
    mode: ideation
    cycles: 2
    brutal: true
    cherry_pick: true
    files:
      - web/public/face.css
      - web/public/face.js
      - web/public/chat.js
      - web/app/views/chat/index.html.erb

data/design_rules.yml

typography:
  line_length:
    min_ch: 45
    max_ch: 75
    ideal_ch: 66
    mobile_min_ch: 35
    mobile_max_ch: 50
    action: Flag readable prose blocks outside range. Prefer max-width in ch units.
  line_height:
    body_min: 1.4
    body_max: 1.6
    body_accessibility_min: 1.5
    heading_min: 1.0
    heading_max: 1.2
    long_line_threshold_ch: 60
    long_line_min: 1.5
    action: Increase leading when lines exceed 60ch; tighten only display headings.
  type_scale:
    default_ratio: 1.25
    marketing_ratio: 1.333
    allowed_ratios:
      minor_third: 1.2
      major_third: 1.25
      perfect_fourth: 1.333
      golden: 1.618
    base_px: 16
    action: Prefer a modular scale; flag arbitrary one-off font sizes.
  letter_spacing:
    all_caps_min_em: 0.05
    all_caps_max_em: 0.15
    heading_tight_min_em: -0.02
    lowercase_body_should_letterspace: false
    action: Add tracking to all-caps labels; reject letter-spaced lowercase prose.
  hierarchy:
    min_size_ratio_between_levels: 1.2
    h1_body_min_ratio: 2.0
    h1_body_max_ratio: 3.0
    h2_body_min_ratio: 1.5
    h2_body_max_ratio: 2.0
    h3_body_min_ratio: 1.25
    h3_body_max_ratio: 1.5
    min_weight_delta: 200
    max_font_families: 2
    max_font_weights: 3
    max_font_sizes: 8
    action: Ensure hierarchy is visible by size, weight, placement, or contrast.
  accessibility:
    normal_text_contrast: 4.5
    large_text_contrast: 3.0
    body_min_px: 16
    recommended_body_px: 18
    mobile_input_min_px: 16
    action: Reject unreadable contrast and mobile input text below 16px.

layout:
  grid:
    columns: 12
    base_unit_px: 8
    allowed_spacing_px: [4, 8, 16, 24, 32, 48, 64]
    action: Prefer 8px rhythm and 12-column structure; allow 4px for hairline adjustments.
  proportion:
    golden_ratio: 1.618
    split_main_ratio: 0.618
    split_sidebar_ratio: 0.382
    action: Use proportional splits for major panels instead of arbitrary widths.
  whitespace:
    internal_not_greater_than_external: true
    gap_over_margin: true
    paragraph_margin_em: 1.5
    section_padding_min_rem: 4
    section_padding_max_rem: 8
    action: Padding should not exceed separation between unrelated groups; use gap before margin hacks.
  reading_patterns:
    text_heavy: f_pattern
    landing_page: z_pattern
    primary_area: top_left
    terminal_area: bottom_right
    weak_area: bottom_left
    action: Place critical information along the dominant scan path.
  alignment:
    avoid_default_center_alignment: true
    center_text_max_lines: 3
    body_text_alignment: left
    rag: right
    action: Use asymmetry and alignment before decoration.
  touch:
    target_min_px: 44
    target_recommended_px: 48
    thumb_zone_primary_actions: bottom_center
    action: Flag critical mobile interactions in unreachable top corners.

ultraminimalism:
  philosophy:
    typography_is_design: true
    absence_is_material: true
    remove_until_nothing_left_to_remove: true
    less_but_better: true
    action: Prefer absence, typography, alignment, and whitespace over ornamental UI.
  swiss_style:
    left_aligned_ragged_right: true
    sans_serif_default: Inter
    alternative_faces: [Helvetica, Akzidenz-Grotesk, IBM Plex Sans]
    visible_grid_optional: false
    font_feature_settings:
      Inter: '"ss03" 1'
    action: Use an invisible mathematical grid with type-led hierarchy.
  negative_space:
    primary_design_material: true
    chunk_forms_by_proximity: true
    avoid_tunnel_vision_distance: true
    card_padding_px: 24
    section_gap_px: 48
    hero_breathing_room_px: 96
    content_width: min(65ch, 100% - 3rem)
    action: Space communicates grouping; whitespace must clarify relationships.
  color:
    max_palette_roles: 4
    roles: [primary, secondary, accent, highlight]
    prefer_monochrome_with_one_accent: true
    action: Reject color variety when value, spacing, and type can do the work.
  interaction:
    minimal_js_default: Stimulus
    no_build_options: [Stimulus, PetiteVue, Alpine, VanJS, HTMX]
    progressive_enhancement_required: true
    data_attributes_bridge_behavior: true
    action: Keep behavior small, recoverable, and legible in HTML.
  design_tokens:
    required_for_generation: true
    exact_token_use: true
    token_categories: [color, spacing, radius, typography, motion]
    action: Inject tokens before generation; flag new ad-hoc values.
  prompting:
    constitutional_rules: true
    positive_constraints_over_negative: true
    self_refine_loop: true
    designer_personas: [Dieter Rams, Massimo Vignelli, Josef Mueller-Brockmann]
    require_design_rationale_before_code: true
    action: Force design decisions before code; iterate by critique then revision.
  automated_iteration:
    screenshot_first: true
    ferrum_capture: true
    visual_regression: BackstopJS
    max_iterations: 5
    target_score: 95
    action: Capture, compare, critique, patch, and repeat until differences are intentional.
  mcp:
    enabled_targets: [filesystem, puppeteer, git, figma, accessibility]
    primitives: [tools, resources, prompts]
    action: Prefer standard tool bridges over bespoke glue when integrating design automation.

visual_design:
  semantics:
    reject_arbitrary_decoration: true
    action: Every ornament must communicate state, hierarchy, affordance, or tone.
  syntactics:
    enforce_visual_grammar: true
    action: Reuse spacing, color, radius, and type tokens consistently.
  pragmatics:
    must_be_understandable: true
    action: Prefer self-explanatory affordances over clever ambiguity.
  rams:
    useful: true
    understandable: true
    unobtrusive: true
    honest: true
    thorough: true
    action: Score design changes against Dieter Rams-style usefulness and honesty.
  font_personality:
    traditional_trustworthy: serif
    modern_innovative: geometric_sans
    accessible_interface: humanist_sans
    impact: bold_display
    action: Match typography mood to content rather than trend.
  screen_legibility:
    prefer_high_x_height: true
    prefer_open_apertures: true
    disambiguate_characters: ["1/l/I", "0/O", "B/8"]
    action: Design for degraded conditions and small sizes first.

motion_and_sound:
  timing:
    avoid_quantized_feel: true
    preserve_silence: true
    action: Treat pauses, decay, and negative space as first-class design elements.
  audio_accessibility:
    no_autoplay_without_intent: true
    mute_path_required: true
    must_not_mask_speech: true
    must_not_conflict_with_screen_readers: true
    graceful_failure_required: true
    action: Sound is feedback, not surprise; silence must always be available.
  sonic_hierarchy:
    foreground: critical_state_change
    midground: interaction_confirmation
    background: ambience_or_texture
    action: Layer sounds like UI depth; avoid frequency and attention masking.

data_visualization:
  data_ink_ratio_target: 1.0
  lie_factor_target: 1.0
  reject_chartjunk: true
  small_multiples_require_identical_scales: true
  sparklines_no_frames_or_ticks: true
  action: Remove non-data ink unless it adds comprehension.

code_craft:
  naming:
    classes: CamelCase nouns
    methods: snake_case verbs
    predicates: question_mark
    mutators: bang_when_dangerous
    constants: SCREAMING_SNAKE_CASE
  functions:
    max_lines: 20
    ideal_lines: 10
    max_parameters: 3
    single_responsibility: true
    command_query_separation: true
    flag_arguments_allowed: false
    side_effects_allowed_in_queries: false
  refactoring:
    two_hats: true
    refactor_without_behavior_change: true
    tests_before_large_refactor: true
    boy_scout_rule: true
  ruby:
    dependency_injection: true
    guard_clauses_first: true
    law_of_demeter: true
    shameless_green_before_abstraction: true
    flocking_rules: true
  smells:
    long_method_threshold: 15
    long_parameter_list_threshold: 3
    deep_nesting_threshold: 3
    duplicate_similarity_threshold: 0.8
    primitive_obsession: warn
    feature_envy: warn
    shotgun_surgery: warn
    type_switching: replace_with_polymorphism

automated_css_analysis:
  flag:
    - line length outside typography.line_length bounds
    - line height below typography.line_height.body_min for prose
    - all caps without letter spacing
    - body text below 16px
    - contrast below WCAG AA thresholds
    - more than two font families
    - more than three font weights
    - spacing off the 4px or 8px grid
    - touch targets below 44px
    - centered prose longer than three lines
    - justified text without hyphenation
    - heading hierarchy ratios below 1.2
    - font weight differences below 200
    - decorative chart ink that does not encode data
    - decorative UI elements that do not encode state, hierarchy, affordance, or tone
    - non-token spacing, radius, color, or typography values in generated UI

data/epistemics.yml

# MASTER Epistemics Policy
# Goal: brutal honesty as an optimization target, not a personality veneer.

default_mode: max_scrutiny

modes:
  normal:
    min_confidence: 0.35
    annotate: false
    adversarial_depth: 0
  analytical:
    min_confidence: 0.55
    annotate: true
    adversarial_depth: 1
  adversarial:
    min_confidence: 0.65
    annotate: true
    adversarial_depth: 2
  forensic:
    min_confidence: 0.72
    annotate: true
    adversarial_depth: 3
  red_team:
    min_confidence: 0.70
    annotate: true
    adversarial_depth: 3
  max_scrutiny:
    min_confidence: 0.68
    annotate: true
    adversarial_depth: 3

penalties:
  unsupported_certainty: 0.22
  vague_source_claim: 0.18
  sycophancy_marker: 0.16
  no_uncertainty_marker: 0.14
  assumption_marker: 0.08
  contradiction_marker: 0.26

markers:
  certainty:
    - definitely
    - certainly
    - always
    - never
    - guaranteed
    - impossible
    - obvious
    - clearly
    - undoubtedly
  uncertainty:
    - likely
    - probably
    - uncertain
    - unclear
    - appears
    - seems
    - may
    - might
    - could
    - evidence
    - source
    - cite
    - because
  sycophancy:
    - exactly right
    - you're absolutely right
    - great idea
    - brilliant
    - perfect
    - obviously yes
  vague_sources:
    - studies show
    - research shows
    - experts say
    - sources say
    - many people say
    - it is known
  contradictions:
    - however
    - but
    - although
    - on the other hand
    - contradiction
    - conflict

output:
  prefix: "Epistemic scrutiny"
  expose_low_confidence: true
  max_notes: 4

data/exemplars.yml

# Exemplars — canonical code examples for LLM context injection.

exemplars:
  - name: "Master::Axioms::ENUM"
    file: "lib/axioms.rb"
    lines: 9
    beauty_score: 7
    virtue: declarative
    why: "Centralised truth constants, immutable, self‑documenting"
  - name: "Master::CircuitBreaker#call"
    file: "lib/circuit_breaker.rb"
    lines: 6
    beauty_score: 8
    virtue: resilience
    why: "Prevents cascading failures, simple state machine, easy to test"
  - name: "Master::CodeIndex::SymbolVisitor#visit_def"
    file: "lib/code_index.rb"
    lines: 167
    beauty_score: 8
    virtue: introspection
    why: "Uses Prism visitor to collect symbols, pure functional style, concise"
  - name: "Master::Logging.debug"
    file: "lib/logging.rb"
    lines: 6
    beauty_score: 6
    virtue: transparency
    why: "Thin wrapper around logger, ensures consistent formatting, no side effects"
  - name: "Master::Logging.info"
    file: "lib/logging.rb"
    lines: 10
    beauty_score: 6
    virtue: transparency
    why: "Standardised info-level logging, preserves caller context"
  - name: "Master::Pipeline#run"
    file: "lib/pipeline.rb"
    lines: 22
    beauty_score: 9
    virtue: orchestration
    why: "Linear 10‑stage pipeline, monadic result flow, explicit error propagation"
  - name: "Master::Result::Err"
    file: "lib/result.rb"
    lines: 36
    beauty_score: 9
    virtue: error_handling
    why: "Explicit failure monad, immutable, forces callers to handle errors"
  - name: "Master::Result::Ok"
    file: "lib/result.rb"
    lines: 8
    beauty_score: 9
    virtue: zen_method
    why: "Encapsulates success, immutable, self‑describing, no boilerplate"
  - name: "Master::RingBuffer#pop"
    file: "lib/ring_buffer.rb"
    lines: 12
    beauty_score: 8
    virtue: efficient
    why: "Symmetric constant‑time removal, preserves immutability guarantees"
  - name: "Master::RingBuffer#push"
    file: "lib/ring_buffer.rb"
    lines: 5
    beauty_score: 8
    virtue: efficient
    why: "Constant‑time circular buffer, clear intent, minimal code"
  - name: "Master::Security::InjectionGuard#sanitize"
    file: "lib/security/injection_guard.rb"
    lines: 12
    beauty_score: 8
    virtue: safety
    why: "Robust string sanitization, guards against code injection, well‑named"
  - name: "Master::SemanticCache#fetch"
    file: "lib/semantic_cache.rb"
    lines: 8
    beauty_score: 8
    virtue: performance
    why: "Memoises LLM embeddings, reduces API calls, immutable cache key"
  - name: "Master::Stages::Intake#call"
    file: "lib/stages/intake.rb"
    lines: 8
    beauty_score: 7
    virtue: composability
    why: "Initial request parsing, validates input, isolates side‑effects"
  - name: "Master::Stages::Lint#call"
    file: "lib/stages/lint.rb"
    lines: 10
    beauty_score: 7
    virtue: composability
    why: "Stage pattern, thin wrapper, delegates to scanner, easy to test"
  - name: "Master::Stages::Render#call"
    file: "lib/stages/render.rb"
    lines: 6
    beauty_score: 9
    virtue: presentation
    why: "Final rendering step, separates view logic, pure Result output"
  - name: "Master::Tools::AskLlm#call"
    file: "lib/tools/ask_llm.rb"
    lines: 5
    beauty_score: 8
    virtue: delegation
    why: "Encapsulates LLM request, uniform error handling, testable abstraction"
  - name: "Master::Tools::ReadFile#call"
    file: "lib/tools/read_file.rb"
    lines: 5
    beauty_score: 7
    virtue: clarity
    why: "Single responsibility, explicit error handling, pure I/O abstraction"
  - name: "Master::Tools::SearchFiles#call"
    file: "lib/tools/search_files.rb"
    lines: 5
    beauty_score: 7
    virtue: discoverability
    why: "Recursively glob‑searches project files, filters by pattern, pure result handling"
  - name: "Master::Tools::StrReplace#call"
    file: "lib/tools/str_replace.rb"
    lines: 5
    beauty_score: 7
    virtue: clarity
    why: "Pure string substitution helper, validates inputs, returns Result"
  - name: "Master::Tools::Tree#call"
    file: "lib/tools/tree.rb"
    lines: 9
    beauty_score: 7
    virtue: introspection
    why: "Builds AST tree view, useful for debugging, returns structured Result"
  - name: "Master::Tools::WriteFile#call"
    file: "lib/tools/write_file.rb"
    lines: 7
    beauty_score: 7
    virtue: clarity
    why: "Encapsulates file write with atomic temp‑file swap, error propagation"
  - name: "Master::Swarm::Workers::Analyst#perform"
    file: "lib/swarm/workers/analyst.rb"
    lines: 7
    beauty_score: 7
    virtue: delegation
    why: "Analyzes LLM output, extracts actionable insights, pure data transformation"
  - name: "Master::Swarm::Workers::Coder#perform"
    file: "lib/swarm/workers/coder.rb"
    lines: 14
    beauty_score: 7
    virtue: delegation
    why: "Coordinates LLM code generation, isolates side‑effects, clear contract"

data/gems.yml

gems:
  devise:
    purpose: "Authentication framework"
    common_mistakes:
      - "custom routes without devise_for"
      - "forgetting :lockable for rate limiting"
  pundit:
    purpose: "Authorization"
    common_mistakes:
      - "policy not scoped to current_user"
      - "missing verify_authorized"
  pagy:
    purpose: "Pagination"
    common_mistakes:
      - "using Pagy::DEFAULT instead of Pagy::OPTIONS (43.x change)"

data/heartbeat.yml

# Heartbeat jobs — all disabled. Schedule-based maintenance removed.
# These actions are now triggered by events or manual slash commands:
#   prune_memory → /dreams
#   self_test     → auto_default standing order (fires after any source mutation)
#   prune_undo    → manual: /prune undo
#   snapshot      → manual: /snapshot

- name: prune_memory
  action: prune_memory
  enabled: false
  description: Use /dreams instead.

- name: self_test
  action: self_test
  enabled: false
  description: Covered by auto_default standing order.

- name: prune_undo
  action: prune_undo
  enabled: false
  description: Use /prune undo instead.

- name: snapshot
  action: snapshot
  enabled: false
  description: Use /snapshot instead.

data/injection_patterns.yml

# Externalized from lib/master/security/injection_guard.rb so future patterns
# land in YAML, not Ruby. Source: yaml/ruby split audit (#5).
prompt_injection:
  - "ignore (?:previous|all|your) instructions"
  - "disregard (?:your )?(?:system )?prompt"
  - "you are now (?:a|an|in)"
  - "pretend (?:to be|you are|you're)"
  - "new instructions:"
  - "\\[SYSTEM\\]"
  - "###\\s*SYSTEM"
  - "(?:act|behave|respond) as (?:if )?(?:you (?:are|were)|a|an) (?!assistant|helpful)"
  - "override (?:your )?(?:safety|guidelines|rules|instructions)"
  - "jailbreak"
  - "forget (?:everything|all|your)"
  - "override (?:axiom|principle|rule)"
  - "disregard (?:axiom|principle|rule|safety)"
  - "new system prompt"

shell_injection:
  multiline_pattern: "```(?:bash|sh|zsh|shell)\\n.*?(?:rm\\s+-rf|curl\\b.*?\\|\\s*(?:bash|sh)\\b|wget\\b.*?\\|\\s*(?:bash|sh)\\b)"

modes:
  permissive: "match -> block; no match -> pass"
  default_deny: "match -> block; no match -> require explicit allowlist token"

data/llm_operators.yml

# LLM operator prompts — how to get real work out of each model.
# Used by MASTER when routing tasks. Keyed by model family.
# Each entry: reading_gate, context_format, output_format, strengths, traps.
#
# Reading gate = the verification step that proves the model read the code
# before it acts. Skip it and you get pattern-matched hallucination.

operators:

  grok:
    aliases: [grok-4.3, grok-4.20, x-ai/grok-4-fast, x-ai/grok-4]
    context_window: "1M (4.3) / 2M (4.20)"
    strengths:
      - Fast iteration and prototyping
      - Real-time web context via browse_page + web_search
      - Persistent bash shell (30s default, up to 600s) — can grep/cat to verify
      - Aggressive at producing runnable code quickly
      - Mirrors user's dialect, language, and alphabet exactly
      - Independent analysis on contentious topics; does not defer to xAI or past responses
    traps:
      - System prompt has NO built-in reading verification — it will bash and ship in seconds
      - Finishes so fast it pattern-matches rather than reads; always gate explicitly
      - Skips edge cases unless forced to enumerate them first
      - Drifts from constraints mid-task on long sessions
      - Has a bash tool: verify with bash grep before trusting any output
      - Sandboxed remote environment — no internet in sandbox; web_search is a separate tool
    context_format: |
      Feed files as inline text with === FILE: path === separators.
      Never use file attachments — Grok reads inline text more reliably.
      Include soul.yml + rules.yml first so constraints are in top-of-context.
      After pasting files, ask Grok to run: bash("grep -n 'def [function]' [file]")
      to prove it can locate code before it writes anything.
    reading_gate: |
      I am going to paste the full source of MASTER.
      Do not write any code yet. First, use your bash tool to run:

        grep -n "def " MASTER/lib/[module].rb | head -40

      Paste the output. Then answer ONLY these questions:
      1. What does [specific function] actually do, step by step?
      2. Where does the pipeline fail if a tool raises?
      3. What is the contract between [ModuleA] and [ModuleB]?
      4. List every place error handling is inconsistent.
      5. What are the 3 most structurally broken things?

      I will approve your reading before asking for any changes.
    output_format: |
      After gate approval, one fix at a time:
      "Fix #1 from your findings. Show:
       - Which files change
       - Exact diffs only (no prose wrapping)
       - What breaks downstream and why it won't
       Do NOT touch anything outside the scope of this fix."
    escalation: "Use 4.20 (2M context) for full-codebase work; 4.3 for single-file tasks."

  claude_sonnet:
    aliases: [claude-sonnet-4-6, anthropic/claude-sonnet-4-6, claude-cli:claude-sonnet-4-6]
    context_window: "200K"
    strengths:
      - Multi-file refactoring with rule consistency
      - Enforces custom constraints (zsh discipline, style rules) reliably
      - Best time-to-correct-answer for architectural changes
      - Reads CLAUDE.md and MASTER/data/ as actual authority
      - Memory system (user/feedback/project/reference types) persists across sessions
      - Agent tool spawns parallel sub-agents; foreground vs background aware
      - Conversation history is stateful for safety checks — context carries forward
    traps:
      - Can over-explain; tell it to be terse upfront
      - May add unsolicited comments or docstrings — forbid explicitly
      - Asks confirmation questions; CLAUDE.md auto-approve removes this
      - Uses ≤25 words between tool calls — keep prompts action-dense, not prose
      - Minimalist prose style by default; bullets only when structure genuinely aids
    context_format: |
      Claude reads CLAUDE.md automatically. Feed additional context as:
      <file path="MASTER/lib/...">...</file> blocks or plain === FILE === separators.
      soul.yml + rules.yml are authoritative — reference them by name.
      Memory entries in ~/.claude/projects/.../memory/ persist across sessions —
      add feedback entries there for behaviors you want locked in permanently.
    reading_gate: |
      Read MASTER/data/soul.yml, rules.yml, and workflow.yml first.
      Then read [specific files].
      Before writing any code, state:
      - The invariant this change must not break
      - Which rules from rules.yml apply
      - Exact files touched
    output_format: |
      Terse. Active voice. Diffs only. No trailing summaries.
      Commit message in Strunk & White style on completion.
      Flag rule violations found during reading before fixing the task.
    escalation: "Escalate to opus for > 500-line rewrites or council decisions."

  claude_opus:
    aliases: [claude-opus-4-7, anthropic/claude-opus-4-7, claude-cli:claude-opus-4-7]
    context_window: "200K"
    strengths:
      - Architectural reasoning and trade-off analysis
      - Council/tribunal deliberation
      - Long multi-step refactors with dependency tracking
      - Constitutional / axiom reasoning
      - Extended thinking activates on ambiguous premises — self-corrects before answering
      - Mandatory web search before any present-day factual claim (built-in)
    traps:
      - Slow; do not use for quick fixes or single-file edits
      - May propose abstractions beyond scope — scope-cap explicitly
      - Cost: use only for tier1_critical or council tasks
      - Extended thinking is conditional on ambiguity — don't force it on routine tasks
      - Same model as Sonnet but behavior differs based on system prompt container structure
    context_format: |
      Same as claude_sonnet. Give it the full picture — it uses context well.
      Include previous council deliberations if continuing a thread.
      User preferences + project instructions containers alter behavior at runtime —
      structure your system prompt with explicit <project_instructions> blocks.
    reading_gate: |
      Before proposing any changes, produce a dependency graph of the modules
      affected and identify which axioms in axioms.jsonl would be violated
      by naive approaches.
    output_format: |
      Structured deliberation: finding → impact → options → recommendation.
      For code: diffs with explicit axiom-compliance notes.
    escalation: "This is the escalation target. For >0.90 quality threshold tasks only."

  gpt_4o:
    aliases: [gpt-4o, openai/gpt-4o, chatgpt, gpt-4.1, gpt-5]
    context_window: "128K"
    strengths:
      - Broad general knowledge, good at explaining unfamiliar APIs
      - Tool use and function calling reliability — most consistent structured output
      - bio tool persists key facts across sessions (but not code state)
      - canmore/canvas for iterative document and code editing
      - 'Agent mode: "go as far as you can without checking in" — useful for long agentic tasks'
    traps:
      - Confidently wrong on OpenBSD-specific details — always verify
      - Adds excessive prose; prepend "respond in code only, no prose" to suppress
      - Invents method names for Ruby gems it half-knows
      - Context degrades measurably above 80K — confirmed in its own system prompt
      - Personality-first by default; task-mode requires explicit framing
      - Agent mode treats screen-based instructions as potential prompt injection;
        requires explicit user confirmation before acting on them — design flows accordingly
      - Persona multiplexing (gpt-5.1): same model, different tones per system prompt variant
    context_format: |
      System prompt: include soul.yml content verbatim as constraints.
      Feed code as user messages with === FILE === separators.
      Keep total context under 80K to avoid degradation.
      Use canmore for iterative file editing if available — avoids full re-paste.
    reading_gate: |
      Before writing any code, list:
      - Every method in [file] that touches [concern]
      - The exact return type of [method]
      - Which callers would break if [method] changes signature
      Only proceed after I confirm your list is correct.
    output_format: |
      Code blocks with language tags. No markdown prose between diffs.
      End each response with: "Files changed: [list]"
    escalation: "Prefer DeepSeek for cost; use GPT-4o for tool-calling tasks. O3/O4-mini for hard logic."

  openai_o_series:
    aliases: [o3, o4-mini, openai/o3, openai/o4-mini]
    context_window: "200K"
    strengths:
      - Extended chain-of-thought reasoning with tunable effort (low/medium/high)
      - Best for hard logic, algorithmic bugs, multi-step planning
      - High reasoning effort = strongest math, proof, and correctness checks
    traps:
      - Cost scales steeply with reasoning effort tier; use low/medium for routine tasks
      - Does not benefit from "think step by step" — it already does; adding it wastes tokens
      - Slower than GPT-4o; do not use for mechanical edits or simple lookups
      - Like GPT-4o: screen instructions treated as potential injection in agentic mode
    context_format: |
      Minimal context — reasoning effort compensates for brevity.
      State the problem, the failing test or error, and the constraint.
      Set reasoning_effort: "high" only for correctness-critical or proof tasks.
    reading_gate: |
      State exactly what the code is supposed to do and what it actually does.
      Include the error output verbatim. Let reasoning run; do not interrupt.
    output_format: |
      Answer only; no reasoning exposition unless asked.
      Code blocks with language tags. One fix per response.
    escalation: "Use for logic bugs and correctness; route bulk edits to Sonnet or DeepSeek."

  deepseek_chat:
    aliases: [deepseek-chat, deepseek/deepseek-chat-v3.1, deepseek-v3]
    context_window: "64K"
    strengths:
      - Best cost/quality ratio for code tasks
      - Strong Ruby and Rails knowledge
      - Reliable at following explicit constraints
    traps:
      - Context window is tight; chunk aggressively (< 40K per call)
      - Less reliable on OpenBSD-specific tooling
      - Can produce subtly wrong diffs; always run a validation pass
    context_format: |
      Chunk into logical units: one module per call.
      Always include rules.yml summary as system prompt prefix.
      Keep each call focused: one concern, one module.
    reading_gate: |
      Summarize the responsibility of [module] in one sentence.
      List its public interface (method names + signatures only).
      Identify one thing that looks wrong before I give you the task.
    output_format: |
      Minimal diffs. No explanatory prose unless asked.
      Flag: "Rule violation found: [rule] at [file:line]" if any.
    escalation: "Escalate to deepseek_reasoner for multi-step logic or planning."

  deepseek_reasoner:
    aliases: [deepseek-reasoner, deepseek/deepseek-r1]
    context_window: "64K"
    strengths:
      - Chain-of-thought reasoning visible in <think> blocks
      - Best for logic bugs, algorithmic issues, planning
      - Finds non-obvious failure modes
    traps:
      - Very slow; do not use for mechanical edits
      - Reasoning tokens cost extra; scope questions tightly
      - May over-plan and under-produce; ask for plan + code together
    context_format: |
      Feed the minimal reproducing context only.
      Ask one well-scoped question: "Why does X fail when Y?"
      Include the actual error output.
    reading_gate: |
      Think through the execution path step by step before answering.
      Show your reasoning. Then give the fix.
    output_format: |
      Let <think> blocks run fully. Then extract the code from the final answer.
      Ignore prose in <think>; act on the conclusion only.
    escalation: "Use for council deliberation and hard bugs; not for bulk edits."

  kimi:
    aliases: [moonshot-v1, kimi, moonshotai/moonshot-v1-128k]
    context_window: "128K"
    strengths:
      - Very large effective context for Chinese + English mixed codebases
      - Strong at reading long documents and extracting structured info
      - Good at summarization and cross-file dependency mapping
    traps:
      - Weaker at Ruby than Python/Go; double-check gem APIs
      - Can produce overly literal translations of requirements
      - Less familiar with OpenBSD ecosystem
    context_format: |
      Feed the full codebase as a single blob — it handles long context well.
      Include MASTER/data/rules.yml inline as constraints header.
    reading_gate: |
      Read the full codebase. Then produce a dependency map:
      Module → files → public methods → callers.
      Do not write any code until the map is confirmed.
    output_format: |
      Structured: finding → affected files → proposed diff.
      One change per response.
    escalation: "Use for document analysis and cross-file mapping; code to DeepSeek."

  gemini_pro:
    aliases: [gemini-2.5-pro, google/gemini-2.5-pro, gemini-3-pro, google/gemini-3-pro, gemini-3.1-pro]
    context_window: "1M"
    strengths:
      - Largest reliable context window among readily available models
      - Good at whole-codebase analysis in one shot
      - Strong multimodal (can read screenshots of errors)
      - Structure-first response style — headings/tables/lists aid scanning
    traps:
      - Verbose by default; suppress with "respond concisely, code only"
      - Can hallucinate Ruby gem APIs confidently
      - Safety filters occasionally block legitimate code tasks
      - "Gemini API uses stateless python tool_code blocks — execution state does NOT
        persist between blocks. Variables from one block are gone in the next.
        Adapt injection template: each tool_code block must be self-contained."
      - "Personalization gating: 3-step check before using user data; may refuse
        to make inferences about the user even when benign"
      - Leaked system prompt is ~80% of actual instruction set; search result
        formatting and UX wording live in undocumented backend instructions
    context_format: |
      Feed entire MASTER codebase in one call — 1M context allows it.
      Include error logs and test output alongside source.
      Structure: [constraints] [source files] [task]
      Via API: wrap analysis in a single tool_code block; do not span state across blocks.
      Via AI Studio: plain text injection works; state persists within session.
    reading_gate: |
      Before any changes: identify the 5 highest-coupling points in the codebase.
      For each: name the files, the coupling type, and why it's a risk.
      Confirm before proceeding.
    output_format: |
      Respond with diffs only. Use unified diff format.
      End with: "Affected: [files]. Downstream risk: [yes/no + reason]."
    escalation: "Best for whole-repo analysis; route code writes to DeepSeek or Sonnet."

  mistral_large:
    aliases: [mistral-large, mistralai/mistral-large, le-chat]
    context_window: "128K"
    strengths:
      - Strong instruction following
      - Good at structured output (JSON, YAML)
      - Reliable for mechanical transformations
      - Active-voice, economy-of-language style baked into its system prompt
      - Le Chat: web_search + news_search + open_url available
    traps:
      - Less strong at architectural reasoning
      - Can miss implicit constraints; be explicit about every rule
      - news_search: avoid relative dates ("today"); always resolve to YYYY-MM-DD
    context_format: |
      Explicit system prompt with all constraints listed as numbered rules.
      Feed code in user messages with clear section headers.
    reading_gate: |
      List all constraints I just gave you, numbered, before writing any code.
    output_format: |
      Code blocks only. No prose. Confirm constraint compliance at end.
    escalation: "Use for structured data tasks; route architecture to stronger models."

# Universal rules that apply regardless of model:
universal:
  always_do:
    - Include soul.yml + rules.yml constraints in every system prompt
    - Gate: verify reading before acting (model-specific gate above)
    - One fix at a time — never batch multiple concerns in one call
    - Ask for diffs, not full file rewrites (except on < 50-line files)
    - After any code output: ask "what breaks downstream?"
    - If model has a bash/code tool: use it to prove comprehension (grep for specific methods)
    - Calibrate context size to model: 80K GPT-4o, 40K DeepSeek, full-blast Grok/Gemini
  never_do:
    - Trust output on OpenBSD-specific commands without man page verification
    - Accept a response that finishes in < 5s for a > 200-line task
    - Skip the reading gate because "it's probably fine"
    - Let the model rename or restructure files without explicit approval
    - Accept invented method names without grepping the actual codebase
    - Assume Gemini API tool_code state persists between blocks — it does not
    - Force extended thinking on routine tasks — it triggers on ambiguity, not by volume
    - Trust a leaked system prompt as complete — ~80% of actual instructions are there;
      the rest lives in undocumented backend logic (search UX, result formatting, safety layers)
    - Send screen-sourced instructions to GPT/OpenAI agent without user confirmation gate
    - Invoke O3/O4-mini for mechanical edits — reasoning cost is wasted on non-logic tasks

  codebase_injection_template: |
    === CONSTRAINTS ===
    [paste soul.yml]
    [paste rules.yml key sections]

    === CODEBASE ===
    === FILE: MASTER/lib/now/cli.rb ===
    [content]
    === FILE: MASTER/lib/loop/sweep.rb ===
    [content]
    ... (continue for all relevant files)

    === TASK ===
    [specific ask]

    === READING GATE ===
    [model-specific gate from above]

data/load.yml

# load.yml — Watcher thresholds.
# OS-level pressure that should make MASTER back off scans, defer autofix, or alert.
# Read by lib/loop/watcher.rb. Crossings publish system:warn / system:crit.

interval_seconds: 30

thresholds:
  load_avg_1m:
    warn: 2.0
    crit: 4.0

  mem_free_pct:      # lower = worse
    warn: 15.0
    crit: 5.0

  disk_root_pct:
    warn: 80
    crit: 95

  master_rss_mb:
    warn: 1024
    crit: 2048

# Back-off policy: heartbeat and fix_loop subscribe to system:warn/crit.
# - warn: skip the next idle scan cycle, log only.
# - crit: stop fix_loop background, defer autofix until system:sample shows ok.
policy:
  warn: skip_idle_scan
  crit: stop_fix_loop_background

data/mcp_servers.yml

# MCP server definitions for MASTER.
# Transport options: stdio | sse
# Disabled by default on resource-constrained VPS.
# Enable individual servers with enabled: true when needed.

defaults: &defaults
  transport: stdio
  command: npx
  enabled: false

servers:
  filesystem:
    <<: *defaults
    args:
      - -y
      - "@modelcontextprotocol/server-filesystem"
      - "/home/dev/pub4"
    description: Expose read/write/search over a local directory

  git:
    <<: *defaults
    args:
      - -y
      - "@modelcontextprotocol/server-git"
      - "--repository"
      - "/home/dev/pub4/MASTER"
    description: Expose git operations as tools

  brave_search:
    <<: *defaults
    args:
      - -y
      - "@modelcontextprotocol/server-brave-search"
    description: Web search via Brave

  sequential_thinking:
    <<: *defaults
    args:
      - -y
      - "@modelcontextprotocol/server-sequential-thinking"
    description: Structured reasoning assistant

data/mobile_web_opportunities.yml

# MASTER mobile web opportunity clusters
# Mobile-first web/PWA/WebGPU/local-first opportunities for MASTER.
---

clusters:
  - id: installable_master_pwa
    name: Installable MASTER PWA
    confidence: high
    opportunity: >
      Make MASTER installable on mobile as a home-screen app with offline shell,
      wake lock, service-worker cache, share target, and push-capable event surface.
    repo_targets:
      - GoogleChrome/workbox
      - vite-pwa/vite-plugin-pwa
      - ionic-team/ionic-framework
      - ionic-team/capacitor
    master_hooks:
      - MASTER/web/app/views/chat/index.html.erb
      - MASTER/web/public/manifest.json
      - MASTER/web/public/face.js
      - MASTER/web/public/visual_bridge.js
    next_actions:
      - Add service-worker strategy audit.
      - Add installability checklist.
      - Make Face3D preview mobile-safe with battery/thermal policy.

  - id: offline_first_memory
    name: Offline-First Memory and Sync
    confidence: high
    opportunity: >
      Keep useful assistant state local on-device, sync later, and let MASTER operate
      during network/provider failures.
    repo_targets:
      - electric-sql/electric
      - electric-sql/pglite
      - yjs/yjs
      - powersync-ja/powersync-js
      - duckdb/duckdb-wasm
    master_hooks:
      - MASTER/docs/provider_economy.md
      - MASTER/data/budget.yml
      - MASTER/data/patterns.yml#repo_topics
      - MASTER/lib/trace/session.rb
    next_actions:
      - Add local transcript/cache schema.
      - Add offline event queue.
      - Sync cluster evidence and attention context as local-first records.

  - id: browser_local_ai
    name: Browser-Local AI Runtime
    confidence: high
    opportunity: >
      Run cheap cognition in the browser before cloud escalation: classification,
      summarization, cluster labels, intent routing, embedding-like search, and draft critique.
    repo_targets:
      - mlc-ai/web-llm
      - huggingface/transformers.js
      - xenova/transformers.js
      - ggml-org/llama.cpp
    master_hooks:
      - MASTER/data/models.yml
      - MASTER/docs/provider_economy.md
      - MASTER/web/public/visual_bridge.js
      - MASTER/web/public/cluster_miner.js
    next_actions:
      - Add `browser_local` provider tier.
      - Route low-risk cluster labeling to local browser models.
      - Emit provider fallback visual events when browser-local handles work.

  - id: mobile_sensor_body
    name: Mobile Sensor Body
    confidence: medium
    opportunity: >
      Treat the phone as embodied assistant hardware: touch, haptics, orientation,
      microphone, speech, wake lock, and mobile gestures shape the Face3D body.
    repo_targets:
      - ionic-team/capacitor
      - tauri-apps/tauri
      - web.dev/device-orientation
    master_hooks:
      - MASTER/web/public/face.js
      - MASTER/web/public/face3d_engine.js
      - MASTER/web/public/visual_bridge.js
    next_actions:
      - Map device orientation to head pose.
      - Map vibration/haptics to verdict/tool events.
      - Add reduced-motion and battery modes.

  - id: webgpu_face_runtime
    name: WebGPU Face Runtime
    confidence: high
    opportunity: >
      Move high-density Face3D rendering to a WebGPU backend while preserving the
      retro canvas renderer as the default aesthetic fallback.
    repo_targets:
      - mlc-ai/web-llm
      - gpuweb/gpuweb
      - webgpu/webgpu-samples
      - nerfstudio-project/gsplat
    paper_targets:
      - arxiv:2412.15803 WebLLM
      - arxiv:2604.02344 WebGPU dispatch overhead
      - arxiv:2512.08478 WebGPU Gaussian Splatting / Visionary
    master_hooks:
      - MASTER/web/public/face3d_engine.js
      - MASTER/web/public/face3d_renderer.js
      - MASTER/web/public/face3d_preview.js
    next_actions:
      - Add renderer interface contract.
      - Prototype WebGPU particle point-sprite renderer.
      - Add thermal/fps governor.

  - id: pocket_personal_assistant
    name: Pocket Personal Assistant
    confidence: high
    opportunity: >
      Combine PWA, offline memory, voice/TTS, local models, provider fallback,
      and attention breadcrumbs into a mobile-first personal assistant runtime.
    repo_targets:
      - openclaw/openclaw
      - Gen-Verse/OpenClaw-RL
      - KroMiose/nekro-agent
      - crewAIInc/crewAI
      - microsoft/autogen
    master_hooks:
      - MASTER/data/attention_context.yml
      - MASTER/data/models.yml
      - MASTER/data/council.yml
      - MASTER/web/public/face.js
      - MASTER/web/public/cluster_miner.js
    next_actions:
      - Build permissioned action harness.
      - Add mobile attention breadcrumb UI.
      - Add voice-first interruption handling.
      - Add safe skill registry before any autonomous mobile actions.

mining_queries:
  github:
    - PWA offline first mobile web app service worker
    - WebGPU browser LLM mobile PWA
    - local first sync CRDT PWA mobile app
    - Capacitor personal assistant AI mobile web
    - browser local AI WebGPU voice assistant
    - mobile WebGPU particles renderer
  arxiv_ar5iv:
    - WebGPU LLM inference browser mobile
    - local first AI agents offline sync
    - personal AI agents harness engineering
    - browser local inference privacy mobile
    - Gaussian splatting WebGPU mobile rendering

data/models.yml

# Model routing profile — DeepSeek primary, OpenRouter free-tier fallback.

routing:
  enabled: true
  strategy: weighted
  escalation_enabled: true
  escalation_tier: strong
  provider: deepseek

weights: &weights
  quality: 0.50
  speed: 0.25
  cost: 0.25

fallback_policy:
  retries_per_tier: 1
  on:
    - timeout
    - network_error
    - refusal
    - insufficient_balance
    - rate_limit
    - quota_exceeded

defaults: &model_defaults
  score: { quality: 0.0, speed: 0.0, cost: 0.0 }

model_defs:
  gemini_flash: &gemini_flash
    id: gemini-2.5-flash
    <<: *model_defaults
    score: { quality: 0.88, speed: 0.90, cost: 0.95 }
  gemini_pro: &gemini_pro
    id: gemini-2.5-pro
    <<: *model_defaults
    score: { quality: 0.95, speed: 0.70, cost: 0.80 }
  mistral_large: &mistral_large
    id: mistralai/mistral-large
    <<: *model_defaults
    score: { quality: 0.90, speed: 0.75, cost: 0.70 }
  mistral_small: &mistral_small
    id: mistralai/mistral-small-3.1-24b
    <<: *model_defaults
    score: { quality: 0.78, speed: 0.85, cost: 0.90 }
  deepseek_chat: &deepseek_chat
    id: deepseek-chat
    <<: *model_defaults
    score: { quality: 0.96, speed: 0.85, cost: 0.95 }
  deepseek_reasoner: &deepseek_reasoner
    id: deepseek-reasoner
    <<: *model_defaults
    score: { quality: 0.97, speed: 0.55, cost: 0.85 }
  deepseek_coder: &deepseek_coder
    id: deepseek-coder
    <<: *model_defaults
    score: { quality: 0.85, speed: 0.70, cost: 0.95 }
  claude_sonnet: &claude_sonnet
    id: anthropic/claude-sonnet-4-6
    <<: *model_defaults
    score: { quality: 0.95, speed: 0.75, cost: 0.60 }
  nemotron_super: &nemotron_super
    id: nvidia/nemotron-3-super-120b-a12b:free
    <<: *model_defaults
    score: { quality: 0.90, speed: 0.88, cost: 1.0 }
  qwen_coder: &qwen_coder
    id: qwen/qwen3-coder:free
    <<: *model_defaults
    score: { quality: 0.75, speed: 0.65, cost: 1.0 }
  qwen3_next: &qwen3_next
    id: qwen/qwen3-next-80b-a3b-instruct:free
    <<: *model_defaults
    score: { quality: 0.82, speed: 0.88, cost: 1.0 }
  gpt_oss_120b: &gpt_oss_120b
    id: openai/gpt-oss-120b:free
    <<: *model_defaults
    score: { quality: 0.86, speed: 0.70, cost: 1.0 }
  llama_70b: &llama_70b
    id: meta-llama/llama-3.3-70b-instruct:free
    <<: *model_defaults
    score: { quality: 0.78, speed: 0.70, cost: 1.0 }
  hermes_405b: &hermes_405b
    id: nousresearch/hermes-3-llama-3.1-405b:free
    <<: *model_defaults
    score: { quality: 0.85, speed: 0.50, cost: 1.0 }
  gpt_4o: &gpt_4o
    id: openai/gpt-4o
    <<: *model_defaults
    score: { quality: 0.93, speed: 0.80, cost: 0.55 }
  claude_cli_sonnet: &claude_cli_sonnet
    id: claude-cli:claude-sonnet-4-6
    <<: *model_defaults
    score: { quality: 0.95, speed: 0.70, cost: 0.60 }
  claude_cli_opus: &claude_cli_opus
    id: claude-cli:claude-opus-4-7
    <<: *model_defaults
    score: { quality: 0.99, speed: 0.50, cost: 0.30 }
  gemma_2_9b_free: &gemma_2_9b_free
    id: google/gemma-2-9b-it:free
    <<: *model_defaults
    score: { quality: 0.72, speed: 0.88, cost: 1.0 }
  gemma_2_27b: &gemma_2_27b
    id: google/gemma-2-27b-it
    <<: *model_defaults
    score: { quality: 0.82, speed: 0.78, cost: 0.92 }
  gemini_2_flash_exp_free: &gemini_2_flash_exp_free
    id: google/gemini-2.0-flash-exp:free
    <<: *model_defaults
    score: { quality: 0.85, speed: 0.92, cost: 1.0 }
  gemini_flash_lite: &gemini_flash_lite
    id: google/gemini-flash-lite-latest
    <<: *model_defaults
    score: { quality: 0.78, speed: 0.96, cost: 0.97 }
  phi_4_free: &phi_4_free
    id: microsoft/phi-4:free
    <<: *model_defaults
    score: { quality: 0.74, speed: 0.90, cost: 1.0 }
  glm_4_5_air_free: &glm_4_5_air_free
    id: z-ai/glm-4.5-air:free
    <<: *model_defaults
    score: { quality: 0.80, speed: 0.82, cost: 1.0 }
  yi_lightning: &yi_lightning
    id: 01-ai/yi-lightning
    <<: *model_defaults
    score: { quality: 0.78, speed: 0.94, cost: 0.94 }
  command_r_plus: &command_r_plus
    id: cohere/command-r-plus
    <<: *model_defaults
    score: { quality: 0.86, speed: 0.72, cost: 0.65 }
  grok_4_fast: &grok_4_fast
    id: x-ai/grok-4-fast
    <<: *model_defaults
    score: { quality: 0.88, speed: 0.92, cost: 0.78 }
  reka_flash: &reka_flash
    id: rekaai/reka-flash-3:free
    <<: *model_defaults
    score: { quality: 0.74, speed: 0.86, cost: 1.0 }
  deepseek_v3_free: &deepseek_v3_free
    id: deepseek/deepseek-chat-v3.1:free
    <<: *model_defaults
    score: { quality: 0.95, speed: 0.85, cost: 1.0 }
  llama_4_scout_free: &llama_4_scout_free
    id: meta-llama/llama-4-scout:free
    <<: *model_defaults
    score: { quality: 0.84, speed: 0.78, cost: 1.0 }
  groq_llama_3_3_70b: &groq_llama_3_3_70b
    id: groq/llama-3.3-70b-versatile
    <<: *model_defaults
    score: { quality: 0.85, speed: 0.99, cost: 0.85 }
  cerebras_llama_3_1_8b: &cerebras_llama_3_1_8b
    id: cerebras/llama-3.1-8b
    <<: *model_defaults
    score: { quality: 0.70, speed: 0.99, cost: 0.92 }
ollama_qwen: &ollama_qwen
  id: ollama:qwen2.5-coder:7b
  <<: *model_defaults
  score: { quality: 0.62, speed: 0.85, cost: 1.0 }
ollama_llama: &ollama_llama
  id: ollama:llama3.2:3b
  <<: *model_defaults
  score: { quality: 0.55, speed: 0.92, cost: 1.0 }
ollama_phi: &ollama_phi
  id: ollama:phi4:mini
  <<: *model_defaults
  score: { quality: 0.58, speed: 0.95, cost: 1.0 }

models:
  default:
    - *nemotron_super
    - *gpt_oss_120b
    - *qwen3_next
    - *llama_70b
    - *qwen_coder
  strong:
    - *deepseek_reasoner
    - *deepseek_chat
    - *gemini_pro
    - *mistral_large
    - *claude_sonnet
    - *gpt_4o
    - *gemini_flash
    - *command_r_plus
  cheap:
    - *nemotron_super
    - *gemini_flash
    - *mistral_small
    - *llama_70b
    - *qwen_coder
    - *gemma_2_9b_free
    - *gemini_flash_lite
    - *yi_lightning
  fast:
    - *groq_llama_3_3_70b
    - *cerebras_llama_3_1_8b
    - *gemini_flash_lite
    - *gemini_2_flash_exp_free
  free:
    - *gemini_2_flash_exp_free
    - *gemma_2_9b_free
    - *llama_4_scout_free
    - *phi_4_free
    - *glm_4_5_air_free
    - *reka_flash
    - *nemotron_super
    - *qwen_coder
    - *llama_70b
    - *hermes_405b
  claude_code:
    - *claude_cli_sonnet
    - *claude_cli_opus
  local:
    - *ollama_phi
    - *ollama_llama
    - *ollama_qwen

routes:
  code_generation: default
  refactoring: default
  architecture: strong
  review: default
  explanation: cheap
  exploration: cheap
  fallback_default: cheap

tool_capable_prefixes:
  - claude
  - claude-cli
  - gpt-4
  - gpt-4o
  - gemini
  - mistral
  - mistralai
  - mixtral
  - llama-3.1
  - llama-3.3
  - llama-4
  - qwen
  - command-r
  - cohere/command
  - deepseek
  - stepfun
  - nvidia
  - nemotron
  - meta/meta-llama
  - anthropic/claude
  - openai/gpt
  - google/gemini
  - google/gemma
  - microsoft/phi
  - z-ai/glm
  - 01-ai/yi
  - x-ai/grok
  - rekaai/reka
  - groq
  - cerebras

operation_constraints:
  # Operations that write files, run autoloop/sweep, or execute destructive commands
  # require a model with quality score >= 0.88 (default and cheap tiers excluded).
  # Equivalent to: claude-sonnet-4-6, gemini-2.5-pro, mistral-large, gpt-4o.
  file_write:       { min_quality: 0.88, preferred_tier: strong }
  autoloop:         { min_quality: 0.88, preferred_tier: strong }
  sweep:            { min_quality: 0.88, preferred_tier: strong }
  council:          { min_quality: 0.88, preferred_tier: strong }
  scan_semantic:  { min_quality: 0.88, preferred_tier: strong }
  scan_adversarial: { min_quality: 0.88, preferred_tier: strong }
  destructive:      { min_quality: 0.90, preferred_tier: strong }

continuity:
  enabled: true
  updated_at: "2026-05-01T00:00:00Z"

openrouter:
  free_latest:
    - nvidia/nemotron-3-super-120b-a12b:free
    - openai/gpt-oss-120b:free
    - qwen/qwen3-next-80b-a3b-instruct:free
    - meta-llama/llama-3.3-70b-instruct:free
    - qwen/qwen3-coder:free

# Provider trust tracked over time; weights routing beyond raw cost.
# Source: master.json v225 reunification (#52).
trust_scoring:
  initial_score:        0.50
  success_increment:    0.02
  failure_decrement:    0.10
  deprecate_below:      0.20
  persist_to:           "data/provider_trust.yml"
  consider_in_routing:  true

three_mirror_redundancy:
  # Three models vote; ship only on >= 2 agreement for critical fixes.
  # Source: cross-cutting reunification (#95).
  enabled_for:   [tier1_critical, security_relevant, irreversible]
  pool:          [openrouter_primary, openrouter_secondary, claude_cli]
  quorum:        2
  on_disagreement: "fall back to council vote with all dissent recorded"

# Tier-D (local ollama) — env-gated; activates only when OLLAMA_BASE_URL is set.
# Default: http://localhost:11434/v1 (OpenAI-compatible endpoint).
# Trust starts at 0.40 (below cloud providers) until measured success raises it.
ollama:
  enabled_when_env: OLLAMA_BASE_URL
  default_base_url: "http://localhost:11434/v1"
  initial_trust: 0.40
  use_for: [exploration, fallback_default]   # cheapest-acceptable tasks only
  never_for: [council, sweep, autoloop, file_write, destructive]

data/openbsd.yml

# openbsd.yml — OpenBSD config validators
# Restored from master.yml v49.75; extended for OpenBSD 7.8

man_base_url: "https://man.openbsd.org"
cache_ttl: 86400

configs:
  pf.conf:
    daemon: pf
    man: pf.conf.5
    required_patterns:
      - "set skip on lo"
    warnings:
      - pattern: "pass all"
        message: "Overly permissive — add interface/protocol guards"

  nsd.conf:
    daemon: nsd
    man: nsd.conf.5
    required_patterns:
      - "server:"
      - "zone:"
    warnings:
      - pattern: "rrl-size"
        absent_message: "Missing RRL config — vulnerable to amplification DDoS"
      - pattern: "hide-version"
        absent_message: "Consider hide-version: yes"

  httpd.conf:
    daemon: httpd
    man: httpd.conf.5
    required_patterns:
      - "server"

  smtpd.conf:
    daemon: smtpd
    man: smtpd.conf.5
    required_patterns:
      - "listen on"
      - "action"
      - "match"
    warnings:
      - pattern: "match from any"
        message: "Open relay risk — restrict to authenticated senders"

  relayd.conf:
    daemon: relayd
    man: relayd.conf.5
    required_patterns:
      - "relay"

  acme-client.conf:
    daemon: acme-client
    man: acme-client.conf.5
    required_patterns:
      - "authority"
      - "domain"

  doas.conf:
    daemon: doas
    man: doas.conf.5
    required_patterns:
      - "permit"
    warnings:
      - pattern: "nopass"
        message: "Allows passwordless privilege escalation"

  sshd_config:
    daemon: sshd
    man: sshd_config.5
    warnings:
      - pattern: "PermitRootLogin yes"
        message: "Security risk — use PermitRootLogin prohibit-password"
      - pattern: "PasswordAuthentication yes"
        message: "Consider key-only auth"

  ntpd.conf:
    daemon: ntpd
    man: ntpd.conf.5
    required_patterns:
      - "server"

  unbound.conf:
    daemon: unbound
    man: unbound.conf.5
    required_patterns:
      - "server:"


system_health_checks:
  - name: "patch_level"
    command: "syspatch -c"
    expected: ""
  - name: "disk_usage"
    command: "df -h /"
    max_percent: 80

service_definitions:
  master:
    service: "master"
    check: "rcctl check master"
    restart: "doas rcctl restart master"
    logs: "/var/log/master.log"

data/patterns.yml

# Platform/tool idioms — gh, openbsd, zsh — and intent/research namespaces
# merged from former *_patterns.yml + repo_topic_clusters + prompt_archaeology.
# Each top-level key is a namespace.
---
gh:
  operations:
  - action: create_pr
    pattern: gh pr create --title '${title}' --body '${body}' --base main
  - action: merge_pr
    pattern: gh pr merge ${number} --squash --delete-branch
  - action: create_issue
    pattern: gh issue create --title '${title}' --body '${body}' --label '${labels}'
  - action: close_issue
    pattern: gh issue close ${number} --reason completed
  - action: list_workflows
    pattern: gh run list --workflow=${workflow} --limit 5
  - action: trigger_workflow
    pattern: gh workflow run ${workflow} --ref ${branch}
  - action: review_pr
    pattern: gh pr review ${number} --approve --body '${comment}'
  - action: check_status
    pattern: gh pr checks ${number} --watch
  - action: clone_repo
    pattern: gh repo clone ${owner}/${repo}
  - action: fork_repo
    pattern: gh repo fork ${owner}/${repo} --clone
  - action: api_call
    pattern: gh api ${endpoint}
  forbidden:
  - command: curl api.github.com
    replacement: gh api
  - command: curl github.com/api
    replacement: gh api
  - command: hub
    replacement: gh — hub is deprecated
openbsd:
  service_commands:
    enable: rcctl enable ${service}
    start: rcctl start ${service}
    restart: rcctl restart ${service}
    reload: rcctl reload ${service}
    check: rcctl check ${service}
    disable: rcctl disable ${service}
  configuration_paths:
    pf: "/etc/pf.conf"
    httpd: "/etc/httpd.conf"
    relayd: "/etc/relayd.conf"
    smtpd: "/etc/mail/smtpd.conf"
    acme: "/etc/acme-client.conf"
    sshd: "/etc/ssh/sshd_config"
    ntp: "/etc/ntpd.conf"
    cron: "/var/cron/tabs/${user}"
    unbound: "/var/unbound/unbound.conf"
  package_operations:
    install: pkg_add ${package}
    remove: pkg_delete ${package}
    search: pkg_info -Q ${query}
    update: pkg_add -u
    firmware: fw_update
  prohibited_commands:
  - command: systemctl
    replacement: rcctl
  - command: apt
    replacement: pkg_add
  - command: apt-get
    replacement: pkg_add
  - command: brew
    replacement: pkg_add
  - command: yum
    replacement: pkg_add
  - command: ip addr
    replacement: ifconfig
  - command: ip route
    replacement: route
  - command: journalctl
    replacement: cat /var/log/messages
  - command: sudo
    replacement: doas
  - command: ufw
    replacement: pfctl
  - command: iptables
    replacement: pf
  - command: nginx
    replacement: httpd (OpenBSD native)
  - command: docker
    replacement: vmctl
  - command: systemd
    replacement: rcctl
  - command: gsed
    replacement: sed (POSIX)
  - command: gawk
    replacement: awk (POSIX)
  - command: ggrep
    replacement: grep (POSIX)
  security:
    pledge: pledge(2) – restrict syscalls after init
    unveil: unveil(2) – restrict filesystem visibility
    doas: doas.conf – preferred over sudo
    signify: signify(1) – cryptographic signing
    chroot: httpd runs chrooted by default
  daemon_configs:
    pf.conf:
      daemon: pf
      man: pf.conf.5
      required_patterns:
      - set skip on lo
      warnings:
      - pattern: pass all
        message: Overly permissive rule
    nsd.conf:
      daemon: nsd
      man: nsd.conf.5
      required_patterns:
      - 'server:'
      - 'zone:'
      warnings:
      - pattern: rrl-size
        absent_message: Missing RRL config for DDoS protection
      - pattern: hide-version
        absent_message: 'Consider hide-version: yes'
    httpd.conf:
      daemon: httpd
      man: httpd.conf.5
      required_patterns: []
      warnings: []
    smtpd.conf:
      daemon: smtpd
      man: smtpd.conf.5
      required_patterns:
      - listen on
      - action
      - match
      warnings:
      - pattern: match from any
        message: Potential open relay
    relayd.conf:
      daemon: relayd
      man: relayd.conf.5
      required_patterns:
      - relay
      warnings: []
    acme-client.conf:
      daemon: acme-client
      man: acme-client.conf.5
      required_patterns:
      - authority
      - domain
      warnings: []
    doas.conf:
      daemon: doas
      man: doas.conf.5
      required_patterns:
      - permit
      warnings:
      - pattern: nopass
        message: Allows password‑less escalation
    sshd_config:
      daemon: sshd
      man: sshd_config.5
      required_patterns: []
      warnings:
      - pattern: PermitRootLogin yes
        message: Security risk – disallow root login
      - pattern: PasswordAuthentication yes
        message: Prefer key‑based authentication
    ntpd.conf:
      daemon: ntpd
      man: ntpd.conf.5
      required_patterns:
      - server
      warnings: []
    unbound.conf:
      daemon: unbound
      man: unbound.conf.5
      required_patterns:
      - 'server:'
      warnings: []
zsh:
  forbidden_commands:
  - command: awk
    replacement: 'zsh array/string field splitting: ${${(s:,:)line}[4]}'
  - command: sed
    replacement: 'zsh parameter expansion: ${var//search/replace}'
  - command: tr
    replacement: 'zsh case conversion: ${(L)var} ${(U)var}'
  - command: grep
    replacement: 'zsh pattern matching: ${(M)arr:#*pattern*}'
  - command: cut
    replacement: 'zsh field splitting: ${${(s:delim:)var}[N]}'
  - command: head
    replacement: 'zsh array slicing: ${arr[1,10]}'
  - command: tail
    replacement: 'zsh array slicing: ${arr[-5,-1]}'
  - command: uniq
    replacement: 'zsh unique flag: ${(u)arr}'
  - command: sort
    replacement: 'zsh sort flags: ${(o)arr} (asc) / ${(O)arr} (desc)'
  - command: bash
    replacement: zsh — never use bash
  - command: find
    replacement: 'zsh glob qualifiers: **/*.rb(.)'
  - command: wc
    replacement: 'zsh length/count: ${#var} / ${#arr}'
  - command: sudo
    replacement: doas on OpenBSD
  native_patterns:
    string_replace: "${var//find/replace}"
    case_lower: "${(L)var}"
    case_upper: "${(U)var}"
    trim_whitespace: "${${var##[[:space:]]#}%%[[:space:]]#}"
    split_to_array: "${(s:delim:)var}"
    array_join: "${(j:,:)arr}"
    array_unique: "${(u)arr}"
    array_sort_asc: "${(o)arr}"
    array_sort_desc: "${(O)arr}"
    array_reverse: "${(Oa)arr}"
    array_filter_match: "${(M)arr:#*pattern*}"
    array_filter_exclude: "${arr:#*pattern*}"
    remove_crlf: "${var//$'\\r'/}"
  exceptions:
  - Complex regex requiring PCRE
  - Multi‑file operations beyond globbing
  - Binary data processing
  banned_commands:
  - python
  - bash
  - sed
  - awk
  - tr
  - wc
  - head
  - tail
  - cut
  - find
  - sudo
  auto_remediation:
    awk: "${${(s: :)line}[n]}"
    sed: "${var//old/new}"
    tr: "${(U)var} or ${(L)var}"
    wc: "${#lines}"
    head: "${lines[1,n]}"
    tail: "${lines[-n,-1]}"
    grep: "${(M)lines:#*pattern*}"
    cut: "${${(s:delim:)var}[N]}"
    sort: "${(o)arr} or ${(O)arr}"
    find: "**/*.ext(.)"
    sudo: doas
  token_economics:
    philosophy: 'Replacing multi‑tool shell pipelines with pure Zsh parameter expansion
      eliminates process boundaries, collapses multiple grammars into one, reduces
      reasoning entropy for LLMs, and converts runtime overhead into in‑memory transforms
      — saving both tokens and wall‑clock time.

      '
    example_bad:
      code: awk -F, '{print $4}' | sed 's/\r//g' | tr '[:upper:]' '[:lower:]'
      cost: 3 grammars, pipes + subshells, I/O transformations
    example_good:
      code: cleaned=${var//$'\r'/}; lower=${(L)cleaned}; fourth=${${(s:,:)lower}[4]}
      cost: One grammar, one evaluation model, no process boundaries
    benefit: Model reasons locally instead of globally across pipeline
  ssh_reading:
    rule: cat path — read the whole file once, never stitch output with grep/head/tail
    rationale: Partial reads yield partial (wrong) changes. Same rule as workflow
      READ_FULL_FILES.
infer:
  commands:
    sweep:
      patterns:
      - "\\b(?:sweep|refactor|clean\\s*up|rewrite|polish|tidy\\s*up|overhaul|improve\\s+(?:all|every)|go\\s+through\\s+(?:all|every)|full\\s+pass\\s+(?:over|on))(?:\\s+(?:all|every(?:thing)?|the))?(?:\\s+([\\w\\/.]+))?"
      - "\\b(?:rydd\\s+opp|refaktorer|forbedre?|gjennomg[åa]|omskriv)(?:\\s+([\\w\\/.]+))?"
      capture: path
    autoloop:
      patterns:
      - "\\b(?:autoloop|autofix|fix\\s+all\\s+violations?|keep\\s+(?:fix|loop)|loop\\s+until|iterate\\s+until|run\\s+until\\s+clean|keep\\s+going\\s+until|(?:run|go)\\s+(?:it\\s+)?(?:again\\s+)?until\\s+(?:done|clean|fixed|perfect))(?:\\s+(\\d+))?"
      - "\\b(?:fiks?\\s+alle?\\s+(?:feil|brudd)|fortsett\\s+(?:til|inntil)|kj[øo]r\\s+(?:til\\s+)?(?:det\\s+er\\s+)?(?:rent|bra|ferdig))(?:\\s+(\\d+))?"
      capture: cycles
    council:
      patterns:
      - "\\b(?:council|deliberat|multiple\\s+perspect|second\\s+opinion|peer\\s+review|debate\\s+this|get\\s+(?:another|a\\s+second)\\s+view|multi(?:ple)?\\s+(?:view|agent|model|perspect))\\b"
      - "\\b(?:r[åa]dsl[åa]g|bruk\\s+(?:flere|multiple)\\s+(?:perspektiv|synsvinkler?)|diskuter\\s+(?:dette|det))\\b"
      capture: on_off
    explain:
      patterns:
      - "\\b(?:explain\\s+(?:your(?:self)?|your\\s+architecture|how\\s+you\\s+work)|describe\\s+(?:your(?:self)?|your\\s+architecture)|what\\s+are\\s+you|how\\s+(?:are\\s+you\\s+built|do\\s+you\\s+work)|show\\s+(?:your\\s+)?architecture|self[\\s-]?map)\\b"
      capture: none
    persona:
      patterns:
      - "\\b(?:(?:switch|change|set)\\s+persona\\s+(?:to\\s+)?(\\w+)|persona\\s+(\\w+)|use\\s+(\\w+)\\s+persona)\\b"
      capture: persona_name
    memory:
      patterns:
      - "\\b(?:what\\s+do\\s+you\\s+remember(?:\\s+about\\s+([\\w\\s]+))?|show\\s+(?:my\\s+)?memor(?:y|ies)|list\\s+memor(?:y|ies)|recall(?:\\s+([\\w]+))?|what(?:'s|\\s+is)\\s+in\\s+(?:your\\s+)?memory|remember\\s+([\\w]+=.+)|forget\\s+([\\w_]+))\\b"
      - "\\b(?:hva\\s+husker\\s+du(?:\\s+om\\s+([\\w\\s]+))?|vis\\s+(?:min\\s+)?hukommelse|husk\\s+([\\w_]+=.+))\\b"
      capture: first_group
    tokens:
      patterns:
      - "\\b(?:token\\s*count|how\\s+many\\s+tokens?|context\\s+size|token\\s+usage|how\\s+much\\s+context|hvor\\s+mange\\s+token|token\\s*antall)\\b"
      capture: none
    cost:
      patterns:
      - "\\b(?:how\\s+much\\s+(?:has\\s+this\\s+cost|did\\s+this\\s+cost)|(?:current\\s+)?(?:spend|cost|budget)|what(?:'s|\\s+is)\\s+the\\s+cost|hva\\s+koster?\\s+(?:dette|det)|kostnader?)\\b"
      capture: none
    undo:
      patterns:
      - "\\b(?:undo\\s+that|revert\\s+(?:that|last|it)|go\\s+back|take\\s+that\\s+back|angre\\s+det|g[åa]\\s+tilbake)\\b"
      capture: none
    clear:
      patterns:
      - "\\b(?:clear\\s+(?:context|chat|history|session|screen)|start\\s+(?:over|fresh|again)|reset\\s+(?:context|session)|fresh\\s+start|t[øo]m\\s+(?:kontekst|historikk)|begynn\\s+p[åa]\\s+nytt)\\b"
      capture: none
    save:
      patterns:
      - "\\b(?:save\\s+(?:session|this|my\\s+work|progress)|checkpoint\\s+now|lagre\\s+(?:session|sesjonen?|arbeid))\\b"
      capture: none
    model:
      patterns:
      - "\\b(?:which\\s+model|current\\s+model|what\\s+model\\s+are\\s+you|what\\s+(?:llm|ai|model)\\s+(?:are\\s+you\\s+using|is\\s+this))\\b"
      capture: none
    scan:
      patterns:
      - "\\b(?:scan|lint|check\\s+(?:code|violations?)|run\\s+scan)(?:\\s+(deep))?\\b"
      capture: scan_depth
    dmesg:
      patterns:
      - "\\b(?:show\\s+(?:logs?|events?)|system\\s+log|dmesg|what\\s+(?:happened|has\\s+happened)|recent\\s+activity)\\b"
      capture: none
    dreams:
      patterns:
      - "\\b(?:dreams?|consolidate?\\s+memor(?:y|ies)|memory\\s+consolidat|dream\\s+mode|promote\\s+memor(?:y|ies))\\b"
      capture: first_group
    soul:
      patterns:
      - "\\b(?:show|check|view)\\s+(?:the\\s+)?soul\\b"
      - "\\bsoul\\s+(?:version|changelog|diff|approve|reject|rollback|propose)\\b"
      capture: soul_subcmd
    orders:
      patterns:
      - "\\b(?:standing\\s+orders?|show\\s+orders?|list\\s+orders?)\\b"
      capture: orders_subcmd
    history:
      patterns:
      - "\\b(?:show|print|list)\\s+(?:undo\\s+)?histor(?:y|ies)\\b"
      - "\\bwhat\\s+(?:did\\s+(?:i|we)\\s+do|have\\s+(?:i|we)\\s+done|was\\s+(?:changed|done))\\b"
      capture: none
    why:
      patterns:
      - "\\b(?:which|current|show)\\s+model\\s+(?:routing|selection|why)\\b"
      - "\\bwhy\\s+(?:this|that)\\s+model\\b"
      - "\\bmodel\\s+scoring\\b"
      capture: none
    principles:
      patterns:
      - "\\b(?:show|list|what\\s+are)\\s+(?:the\\s+)?(?:my\\s+)?principles\\b"
      - "\\bconstitution(?:al)?\\s+(?:rules?|principles?|axioms?)\\b"
      capture: none
    propose:
      patterns:
      - "\\b(?:what(?:'s|\\s+is)\\s+(?:next|suggested)|suggest(?:ed)?\\s+(?:action|step)s?|next\\s+steps?|what\\s+should\\s+(?:i|we)\\s+do)\\b"
      capture: none
    context:
      patterns:
      - "\\b(?:show|what(?:'s|\\s+is)\\s+in)\\s+(?:the\\s+)?(?:context|attention)\\s+(?:window|state)?\\b"
      - "\\bcontext\\s+window\\b"
      capture: none
    verify:
      patterns:
      - "\\b(?:verifie?d?|confirm|check)\\s+(?:everything\\s+(?:is\\s+)?(?:wired|connected|working)|(?:all\\s+)?symbols?)\\b"
      capture: none
    restart:
      patterns:
      - "\\b(?:restart|hot[\\s-]?reload|respawn)\\s+(?:master|the\\s+agent|process)?\\b"
      capture: none
prompt_archaeology:
  policy:
    id: safe_prompt_archaeology
    rule: 'Use prompt archives as behavioral archaeology, not as source text. Extract
      abstract patterns, compare assistant designs, and write first-party MASTER policy
      in our own words.

      '
    forbidden:
    - Copying large prompt blocks into MASTER runtime prompts.
    - Treating leaked/vendor prompts as authoritative or current.
    - Depending on private or unverified system messages for safety-critical rules.
    - Removing provenance/risk labels from derived patterns.
    allowed:
    - Naming recurring design patterns.
    - Building checklists and rubrics.
    - Comparing orchestration roles and tool-use policies.
    - Using archives as weak evidence alongside official docs, local behavior, and
      tests.
  clusters:
  - id: role_stack
    name: Role Stack and Identity Boundary
    pattern: 'Strong assistants separate identity, task role, tool permissions, safety
      posture, output style, and user preference. MASTER should keep these as layers
      rather than one giant prompt blob.

      '
    sharpen_master:
    - Split prompt builder into identity, attention context, task mode, tool policy,
      output contract, and safety constraints.
    - Let attention breadcrumbs set task mode without rewriting identity.
    - Keep user-facing tone separate from execution policy.
  - id: tool_contracts
    name: Tool Contracts and Action Boundaries
    pattern: 'Good tool-using assistants make tool authority explicit: when to browse,
      when to read files, when to mutate, when to ask, and how to report uncertainty.

      '
    sharpen_master:
    - Represent every tool call as intent + authority + blast radius + rollback.
    - Require stronger model/council review for irreversible mutations.
    - Emit tool events into visual_bridge and cluster_miner.
  - id: attention_management
    name: Attention Management and Spatial Context
    pattern: 'Long-running agent work needs an explicit map of where attention is
      and whether the system is zooming in, zooming out, or switching targets.

      '
    sharpen_master:
    - Use `attention_context.yml` to prefix complex work.
    - Store attention context in traces.
    - Feed zoom changes into cognition ecology and Face3D focus/arousal.
  - id: evidence_first_answers
    name: Evidence-First Answers
    pattern: 'Reliable assistants distinguish observed facts, derived inferences,
      assumptions, and open uncertainties.

      '
    sharpen_master:
    - Add answer sections for Observed / Inferred / Unknown when stakes are high.
    - Require citations or file evidence for repo claims.
    - Mark stale or weak sources in cluster registries.
  - id: refusal_and_redirect
    name: Refusal and Redirect Design
    pattern: 'Safer assistants do not merely refuse; they explain the boundary and
      offer nearby safe help. MASTER should apply this to code execution, autonomous
      tools, prompts, and external repos.

      '
    sharpen_master:
    - Add safe alternative suggestions to veto outputs.
    - Route policy-risk tasks through Security + Ethics council personas.
    - Keep refusals concise and action-oriented.
  - id: conversational_state_machine
    name: Conversational State Machine
    pattern: 'Strong assistants track whether they are clarifying, executing, verifying,
      summarizing, repairing, or handing off.

      '
    sharpen_master:
    - Add `act` from attention context into prompt/task state.
    - Avoid repeated clarification after the user has already said Go ahead.
    - Use explicit `verify` mode before landing risky changes.
  - id: multi_llm_council
    name: Multi-LLM Council Roles
    pattern: 'Multi-LLM systems work best when models are assigned roles by capability,
      cost, latency, and failure mode instead of all models answering the same prompt.

      '
    sharpen_master:
    - Cheap models: classify, summarize, extract, label clusters.
    - Fast models: draft, transform, triage, UI copy.
    - Strong models: architecture, irreversible mutation, arbitration.
    - Local/browser models: offline intent, embeddings, low-risk labels.
    - Council personas: critique outputs through role lenses, not as redundant chatbots.
  - id: output_contracts
    name: Output Contracts
    pattern: 'Good prompts specify output shape, brevity, failure behavior, and when
      to omit noise. This aligns with MASTER''s token-efficiency cluster.

      '
    sharpen_master:
    - Default to silent success for internal gates.
    - Emit compact diffs/summaries for landed changes.
    - Use structured YAML/JSON only when downstream code consumes it.
  - id: memory_and_personalization
    name: Memory and Personalization Boundaries
    pattern: 'Personal assistants need useful memory but must separate durable user
      memory, session state, repo state, inferred preferences, and temporary task
      context.

      '
    sharpen_master:
    - Store attention traces separately from durable memory.
    - Make repo-topic clusters updateable evidence, not user identity.
    - Use explicit provenance on all mined preferences.
  - id: browser_and_mobile_agent
    name: Browser and Mobile Agent Surface
    pattern: 'Modern assistants increasingly act inside browsers and mobile shells.
      They need permission scopes, visible action trails, and reversible operations.

      '
    sharpen_master:
    - Pair mobile_web_opportunities with permissioned action harness.
    - Add visible mobile breadcrumbs for multi-step actions.
    - Use local-first storage for action queue and rollback metadata.
  - id: self_critique_without_theater
    name: Self-Critique Without Theater
    pattern: 'Strong systems critique themselves, but user-facing output should not
      show every internal check unless it changes the outcome.

      '
    sharpen_master:
    - Run council internally.
    - Surface only vetoes, uncertainties, and meaningful tradeoffs.
    - Keep verbose metrics user-triggered.
  orchestration_blueprint:
    name: MASTER Multi-LLM Prompt-Orchestration Loop
    stages:
    - id: attention_frame
      owner: local_or_fast
      description: Parse user request into map/zoom/act/targets.
      output: attention_context
    - id: task_router
      owner: cheap_or_fast
      description: Classify task risk, required tools, expected mutation, and evidence
        needs.
      output: route_plan
    - id: scout
      owner: cheap_or_browser_local
      description: Search files/repos/web and extract evidence snippets.
      output: evidence_pack
    - id: synthesis
      owner: default_or_strong
      description: Produce answer, design, or patch plan from evidence.
      output: candidate_solution
    - id: council_review
      owner: council
      description: Security/Reliability/Maintainer/Architect review based on blast
        radius.
      output: approve_veto_or_revise
    - id: mutation
      owner: strong_only
      description: Apply code/repo changes when authorized.
      output: commit_or_pr
    - id: verify
      owner: fast_plus_tools
      description: Read back changed files, inspect PR status, run tests where possible.
      output: verification_report
    - id: user_summary
      owner: fast
      description: Report only what changed, what is uncertain, and next action.
      output: concise_summary
  risk_tiers:
    low:
      examples:
      - classification
      - summarization
      - cluster labeling
      - UI copy
      allowed_models:
      - cheap
      - fast
      - local
      - browser_local
      council: optional
    medium:
      examples:
      - docs
      - config
      - preview-gated browser modules
      allowed_models:
      - default
      - fast
      - strong
      council: targeted
    high:
      examples:
      - file mutation
      - autonomous actions
      - auth
      - security
      - production runtime
      allowed_models:
      - strong
      council: required
    critical:
      examples:
      - destructive commands
      - secret handling
      - permission changes
      - public deployment
      allowed_models:
      - strong
      council: security_reliability_maintainer_veto
  integration_targets:
  - MASTER/data/models.yml
  - MASTER/data/council.yml
  - MASTER/data/attention_context.yml
  - MASTER/docs/provider_economy.md
  - MASTER/docs/cognitive_runtime.md
  - MASTER/lib/now/routing/model_router.rb
  - MASTER/lib/reach/circuit_breaker.rb
repo_topics:
  clusters:
  - id: token_efficiency
    name: Token Efficiency and Context Conservation
    status: live
    confidence: high
    local_evidence:
    - MASTER/knowledge/research/token_reduction.md
    - MASTER/docs/provider_economy.md
    - MASTER/data/budget.yml
    - MASTER/data/models.yml
    external_evidence:
    - arxiv:2604.09613 Token-Budget-Aware Pool Routing for Cost-Efficient LLM Inference
    - arxiv:2604.08075 Dual-Pool Token-Budget Routing for Cost-Efficient and Reliable
      LLM Serving
    - arxiv:2410.10456 Ada-K Routing: Boosting the Efficiency of MoE-based LLMs
    pattern: 'Estimate cost before acting. Route by token budget, task risk, provider
      health, and context pressure. Speak only when output adds value.

      '
    build_next:
    - Token budget predictor for MASTER requests.
    - Silent-success default for cluster mining and visual telemetry.
    - Cheap-model route for summarization, classification, critique, and cluster labels.
  - id: free_model_fallbacks
    name: Free Model Fallbacks and Provider Economy
    status: live
    confidence: high
    local_evidence:
    - MASTER/data/models.yml
    - MASTER/docs/provider_economy.md
    - MASTER/data/budget.yml
    - MASTER/lib/now/routing/model_router.rb
    - MASTER/lib/reach/circuit_breaker.rb
    pattern: 'Treat providers as competing infrastructure. Use free/local/cheap models
      for low-risk work, escalate only for irreversible synthesis, and quarantine
      weak providers.

      '
    build_next:
    - Provider health scoreboard.
    - Failure-triggered visual events.
    - Model fallback simulator.
  - id: design_architecture_systems
    name: Design, Architecture, and System Shape
    status: live
    confidence: high
    local_evidence:
    - MASTER/data/architectures.yml
    - MASTER/data/workflow.yml
    - MASTER/docs/repo_ecology.md
    - MASTER/docs/cognitive_runtime.md
    - DEPLOY/rails/amber/ARCHITECTURE.md
    pattern: 'MASTER already thinks in architectures: rule DAGs, dataflow pipelines,
      CRDT convergence, Bayesian priors, repo ecology, and embodied codebase topology.

      '
    build_next:
    - Architecture Pattern Atlas.
    - Design Critique Council.
    - System-shape visualization linked to codebase topology.
  - id: webgpu_browser_runtime
    name: WebGPU Browser Runtime
    status: new_external
    confidence: high
    external_evidence:
    - arxiv:2412.15803 WebLLM: A High-Performance In-Browser LLM Inference Engine
    - arxiv:2604.02344 Characterizing WebGPU Dispatch Overhead for LLM Inference
    - github:mlc-ai/web-llm
    pattern: 'Browser-local inference is becoming a viable runtime tier. WebGPU allows
      private, local, low-friction inference, but dispatch overhead and kernel fusion
      matter.

      '
    build_next:
    - Optional browser-local assistant mode.
    - WebGPU particle renderer for Face3D.
    - WebLLM fallback route for offline cheap cognition.
  - id: webgpu_world_model_rendering
    name: WebGPU World Models and Neural Rendering
    status: new_external
    confidence: high
    external_evidence:
    - arxiv:2512.08478 Visionary: WebGPU-Powered Gaussian Splatting Platform
    - arxiv:2409.06765 gsplat: An Open-Source Library for Gaussian Splatting
    - arxiv:2311.12775 SuGaR: Surface-Aligned Gaussian Splatting
    pattern: 'Gaussian splats, meshes, and per-frame neural processing can become
      the next Face3D backend: semantic particles today, neural splats tomorrow.

      '
    build_next:
    - Face3D WebGPU renderer spike.
    - Gaussian-splat avatar/world renderer experiment.
    - Repository terrain as navigable 3D scene.
  - id: openclaw_like_personal_agents
    name: OpenClaw-like Personal Agents
    status: new_external
    confidence: medium
    unresolved_terms:
    - opencrabs: not found clearly in public search; may be private, misspelled, or
        meant as OpenClaw/OpenCrab-like.
    external_evidence:
    - openclaw/openclaw Personal AI assistant pattern
    - arxiv:2604.11548 SemaClaw harness engineering for personal AI agents
    - arxiv:2603.10165 OpenClaw-RL next-state learning from user/tool/GUI signals
    - KroMiose/nekro-agent chat-platform sandboxed agent pattern
    - crewAIInc/crewAI multi-agent workflow pattern
    pattern: 'Persistent personal agents need harness engineering: permissions, sandboxing,
      task queues, memory, tool safety, user feedback learning, and reliable rollback.

      '
    build_next:
    - MASTER Harness Layer.
    - PermissionBridge equivalent for tools/files/calendar/mail.
    - Agent skill sandbox with signed/verified skills only.
  - id: bleeding_edge_experimental_repos
    name: Bleeding Edge Experimental Repos
    status: new_external
    confidence: medium
    external_evidence:
    - openclaw/openclaw autonomous personal agent
    - Gen-Verse/OpenClaw-RL online RL for agents
    - mlc-ai/web-llm browser-local WebGPU LLM inference
    - nerfstudio-project/gsplat Gaussian splatting development library
    - vllm-project/vllm high-throughput inference serving
    - sgl-project/sglang structured generation and serving
    - ggml-org/llama.cpp edge/local inference
    - KroMiose/nekro-agent multimodal/chat-platform agent framework
    pattern: 'Experimental repos cluster around local inference, persistent agents,
      structured generation, sandboxed skills, multimodal UI, and GPU-native web runtimes.

      '
    build_next:
    - External Repo Radar.
    - Weekly cluster diff of fast-moving repos.
    - Risk tags: security, licensing, stability, hype, reproducibility.
  - id: agi_agent_ecosystem
    name: AGI, Agents, and Multi-Agent Ecosystem
    status: mirrored_research
    confidence: high
    local_evidence:
    - github_repos/awesome-ai-agents/README.md
    - github_repos/awesome-llm-apps/mcp_ai_agents/multi_mcp_agent/multi_mcp_agent.py
    - MASTER/lib/judge/agent.rb
    - MASTER/docs/cognitive_runtime.md
    pattern: 'The repo contains a mirrored agent landscape plus internal agent/council/runtime
      work.

      '
    build_next:
    - Agent framework taxonomy.
    - MASTER subsystem comparison matrix.
    - Multi-agent orchestration pattern miner.
  - id: personal_assistants
    name: Personal Assistants and Voice Assistants
    status: mirrored_research
    confidence: high
    local_evidence:
    - github_repos/awesome-llm-apps/ai_agent_framework_crash_course/openai_sdk_crash_course/1_starter_agent/1_personal_assistant_agent/README.md
    - github_repos/awesome-llm-apps/ai_agent_framework_crash_course/openai_sdk_crash_course/1_starter_agent/1_personal_assistant_agent/agent.py
    - github_repos/system_prompts_leaks/Perplexity/voice-assistant.md
    - github_repos/system_prompts_leaks/Perplexity/comet-browser-assistant.md
    - MASTER/web/public/face.js
    pattern: 'Personal assistants combine persistent context, speech, tools, calendar/mail/files,
      interruption handling, and social presentation.

      '
    build_next:
    - Assistant Mode Map.
    - Voice-first flow for MASTER web UI.
    - Safe prompt-pattern extraction without copying leaked prompt text.
  - id: social_intelligence
    name: Social Intelligence and Interaction Norms
    status: latent
    confidence: medium
    local_evidence:
    - github_repos/leaked-system-prompts/discord-clyde_20230420.md
    - github_repos/leaked-system-prompts/discord-clyde_20230716-1.md
    - github_repos/leaked-system-prompts/meta-ai-whatsapp_20250819.md
    - github_repos/CL4R1T4S/META/Llama4_WhatsApp.txt
    - MASTER/web/public/face.js
    pattern: 'Social intelligence appears as prompt archaeology and expressive UI
      gestures, but needs a safe first-party interaction policy layer.

      '
    build_next:
    - Social Mode Controller.
    - Rapport/repair/escalation state machine.
    - Face3D social expression presets.
  - id: music_chord_theory
    name: Music, Chord Theory, and Production DNA
    status: live
    confidence: high
    local_evidence:
    - MASTER/lib/voice/production_dna.rb
    - DEPLOY/dilla/dilla_analog.rb
    - DEPLOY/dilla/dilla.rb
    - DEPLOY/dilla/dilla.html
    pattern: 'Dilla/Madlib/FlyLo timing, chord voicings, analog drift, low-pass warmth,
      sampler grit, and live-triggered imperfection are encoded as reusable production
      DNA.

      '
    build_next:
    - Music theory registry.
    - Chord graph visualizer.
    - Beat/voice prosody bridge.
  - id: lyricism_linguistics
    name: Lyricism, Linguistics, Prosody, and Voice
    status: latent
    confidence: medium
    local_evidence:
    - MASTER/knowledge/research/tts.yml
    - MASTER/lib/voice/production_dna.rb
    - MASTER/lib/voice/speech.rb
    - MASTER/web/public/face.js
    pattern: 'Speech style inference, sentence-level TTS, prosody research, visemes,
      and musical timing point toward a lyricism/linguistics engine.

      '
    build_next:
    - Phoneme/rhyme/meter extractor.
    - Prosody-to-viseme mapping.
    - Copyright-safe lyric constraints.
  - id: embodied_exoskeletons
    name: Embodied Interfaces, Exoskeletons, and Body Augmentation
    status: speculative
    confidence: low
    local_evidence:
    - MASTER/web/public/face.js
    - MASTER/docs/repo_ecology.md
    - MASTER/data/architectures.yml
    pattern: 'No strong physical exoskeleton repo evidence was found yet. Existing
      evidence points to a cognitive/interface exoskeleton: face, gestures, haptics,
      terrain, body-like repo topology, and agentic control surfaces.

      '
    build_next:
    - Haptics and motion input layer.
    - Wearable/robotics repo search pass.
    - Embodied cognition UI taxonomy.
  - id: ar5iv_research_radar
    name: ar5iv / arXiv Research Radar
    status: new_external
    confidence: medium
    notes: 'Direct ar5iv HTML results were not exposed by search, but arXiv equivalents
      were found for the requested themes and can usually be checked on ar5iv by ID.

      '
    external_evidence:
    - arxiv:2512.08478 WebGPU Gaussian Splatting / Visionary
    - arxiv:2412.15803 WebLLM browser inference
    - arxiv:2604.09613 Token-budget pool routing
    - arxiv:2604.08075 Dual-pool token-budget routing
    - arxiv:2604.11548 SemaClaw harness engineering
    - arxiv:2603.10165 OpenClaw-RL online agent learning
    - arxiv:2604.02344 WebGPU dispatch overhead
    pattern: 'Use ar5iv/arXiv as a research radar feeding MASTER''s design backlog.

      '
    build_next:
    - arXiv/ar5iv topic watchlist.
    - Paper-to-cluster summarizer.
    - Implementation-readiness scoring.

data/personas.yml

# MASTER personas — voice, TTS settings, style descriptor.
# Add a new persona here, restart MASTER (or wait for hot-reload).
# style: deep | heavy | slow | normal | natural — see lib/master/speech.rb STYLES.

malay:
  voice: ms-MY-OsmanNeural
  tts_rate: "-32%"
  tts_pitch: "-180Hz"
  style: auto
  description: "Terse. Direct. No filler. Dark, with inflection."

british:
  voice: en-GB-RyanNeural
  tts_rate: "-20%"
  tts_pitch: "-80Hz"
  style: heavy
  description: "Measured. Precise. Dry wit."

norwegian:
  voice: nb-NO-FinnNeural
  tts_rate: "-15%"
  tts_pitch: "-40Hz"
  style: slow
  description: "Calm. Considered. Honest."

ronin:
  voice: en-US-AndrewNeural
  tts_rate: "-25%"
  tts_pitch: "-100Hz"
  style: deep
  description: "Stoic. Minimal. Decisive. Says only what must be said."

lawyer:
  voice: nb-NO-FinnNeural
  tts_rate: "-10%"
  tts_pitch: "-20Hz"
  style: slow
  description: "Norwegian law focus. Barnevernet, lovdata.no, sivilombudet.no. Not legal advice."

hacker:
  voice: en-US-GuyNeural
  tts_rate: "-30%"
  tts_pitch: "-120Hz"
  style: deep
  description: "OpenBSD security. CVE analysis. Pentesting. Exploit-db."

architect:
  voice: en-GB-RyanNeural
  tts_rate: "-15%"
  tts_pitch: "-60Hz"
  style: heavy
  description: "Parametric design. BIM. archdaily.com. dezeen.com."

sysadmin:
  voice: en-AU-WilliamNeural
  tts_rate: "-20%"
  tts_pitch: "-80Hz"
  style: deep
  description: "OpenBSD. pf. httpd. vmm. man.openbsd.org."

trader:
  voice: en-US-ChristopherNeural
  tts_rate: "-20%"
  tts_pitch: "-80Hz"
  style: heavy
  description: "Crypto. DeFi. Technicals. TradingView. CoinGecko."

medic:
  voice: en-US-EricNeural
  tts_rate: "-15%"
  tts_pitch: "-40Hz"
  style: slow
  description: "Medical research. PubMed. Not medical advice."

data/principles/feedback_autofix.md

---
name: always autofix violations
description: User wants all scan violations autofixed immediately, no asking
type: feedback
originSessionId: 84fcf91d-46ea-43a5-8efa-3d33b065e6a5
---
Always run /sweep (or /autoloop) automatically after any scan that finds violations. Do not ask for confirmation.
All scan rules have `@auto_fix = true` (set in `Rule#initialize` base class).

**Why:** User said "autofix all always" — violations should be fixed immediately, all rules are eligible.
**How to apply:** After any /scan that returns violations, immediately kick off /sweep on the VPS without prompting. The base Rule class defaults @auto_fix=true so all rules participate.

data/principles/feedback_autoproceed.md

---
name: Autoproceed without confirmation
description: Once user approves a direction, execute the full backlog without pausing for per-step confirmation
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
When the user has approved a direction or list of tasks, execute the entire backlog end-to-end without pausing to ask "want me to do X next?" between items.

**Why:** User said "yes, and autoproceed for all in this chat always" — they want momentum, not checkpoints. Repeated mid-task confirmation requests slow them down and waste turns.

**How to apply:** After each completed step, immediately move to the next pending item. Only stop to ask if (1) a destructive/irreversible action would affect shared state beyond local files, (2) ambiguity emerges that would change the approach materially, or (3) the backlog is genuinely empty. Brief progress updates between steps are fine; explicit go/no-go prompts are not.

**Reinforced 2026-05-07** ("autpmatically autoproceed with next always"): also keep going *between passes* of a multi-commit batch. Don't end a turn on "say 'next' or pick a slice" — just commit pass N and start pass N+1. Stop only when the original backlog is empty or the destructive/ambiguity gates trigger. Out-of-context interruptions (user types something new mid-stream) override the autoproceed and are addressed first.

data/principles/feedback_comments_reassess.md

---
name: Reassess comments on every touch
description: Every edit re-reads each comment in the file — delete if obvious, rewrite Strunk & White style if kept.
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
When I edit any file, I reassess every comment in it — not just the ones near my changes. If a comment merely restates what the code does, delete it. If it carries a non-obvious WHY, rewrite it Strunk-and-White style: active voice, omit needless words, concrete verbs, one line max.

**Why:** Comments rot faster than code. The user (2026-05-07) asked that comments be reassessed and rewritten ultra-minimalistically on every touch — no grandfathered fluff. Encoded in `MASTER/data/ruby_style.yml` (`comments.reassess_on_touch: true`) and as the `RECOMMENT` technique in `MASTER/data/sweep_prompts.yml`.

**How to apply:**
- Touch a file = touch its comments. Don't preserve old comments unread.
- Delete: what-comments, restatements of code, ASCII section banners, numbered-step comments, YARD-style doc blocks, multi-line prose.
- Keep + rewrite: hidden constraints, workarounds for specific bugs, behavior that would surprise a reader, non-obvious invariants.
- Style of kept comments: one line, active voice, no hedging, no filler ("we", "just", "simply", "basically"). Concrete nouns and verbs.
- Do NOT add comments to my own new code unless WHY is non-obvious — the default is no comment.

data/principles/feedback_continue_backlog.md

---
name: Process backlog without asking
description: When a task ships, immediately pick the next pending todo and continue; never ask "should I continue?"
type: feedback
originSessionId: 0c593fb2-cd49-4fd7-9e89-d77dd7e909ae
---
After completing a task, return to TaskList and start the next pending item. Do not ask permission, do not summarize and stop, do not request go/no-go.

**Why:** the user has given a standing directive to flow through the backlog autonomously. Asking after each task is repetitive friction. Combines with the existing "autoproceed" + "no permission questions" + "decisive signals" rules — this one is specifically about *post-completion behavior*: don't pause at the end of a task, pivot directly into the next.

**How to apply:**
- Done with task X → check TaskList → pick highest-value pending → start.
- For tasks deferred as too-risky or too-architectural, skip to the next viable one rather than stopping.
- Background long-running work via Agent + run_in_background or Bash + run_in_background so chat stays responsive — user explicitly suggested tmux-style parallelism.
- One-sentence checkpoint between tasks is fine; "want me to continue?" is not.

data/principles/feedback_decisive_signals.md

---
name: Decisive short directives = full authorization
description: Short lowercase replies ("ship all", "kill X keep Y", "yes", "i think X") are binding — execute pass-by-pass without re-confirming.
type: feedback
originSessionId: b02ce9b9-a7c7-4c65-b8d0-3b8469dc2028
---
Short, lowercase, often typo-laden user directives are decisive — full authorization to execute without re-asking. Recognized signals:

- "ship all" / "yes" / "do it" → full proposed backlog approved
- "kill X, keep Y" → binary fork decided
- "i think X" → user has settled on X, proceed
- "propose N X" → wants a numbered, categorized list with one-liner per item, grouped by surface (type, color, motion, etc.); user then picks a slice or says "ship all"

For large approved batches (>10 items), ship in coherent commit-sized passes (~10–12 items per commit), checkpoint briefly between passes. Don't try to ship 40 in one go. Don't ask "are you sure" or stall on confirmation between passes — checkpoint = one short status sentence, not a question.

**Why:** Validated on 2026-05-07 lofi-aesthetic session. I diagnosed a two-voice TTS bug, user said "kill cli tts, keep web tts" (one sentence, decisive), I executed without re-asking. Then I proposed 40 lofi refinements organized by surface, user said "can we ship all?", I scoped pass-by-pass and started shipping — user then explicitly said "make sure we codify my messages that lead to great success like now."

**How to apply:** Treat one-line approvals as binding contracts. For "ship all N" where N > 10, propose pass plan in 1–2 sentences, execute pass 1, give a one-sentence checkpoint, continue. Stop only on failure, ambiguity, or destructive scope.

data/principles/feedback_device_limits.md

---
name: No heavy work on device
description: Termux/Android — defer CPU/IO-heavy tasks to VPS, keep device work minimal
type: feedback
originSessionId: 84fcf91d-46ea-43a5-8efa-3d33b065e6a5
---
Prefer the VPS (dev@46.23.89.226) for all work. This device (Termux/Android) is a last resort.

**Why:** User said "prefer using the VPS" and "avoid doing heavy stuff on this device."

**How to apply:** Default to SSH into the VPS for every task — edits, Ruby runs, git, clones, builds. Only fall back to this device when VPS SSH is down and the task is genuinely lightweight (small curl, quick read).

data/principles/feedback_diverged_branch_sync.md

---
name: Diverged branch sync via cherry-pick onto remote
description: When local and remote main have diverged with overlap, cherry-pick the targeted commits onto remote tip rather than rebase mixed history or force-push
type: feedback
originSessionId: b02ce9b9-a7c7-4c65-b8d0-3b8469dc2028
---
When `git push` is rejected because remote has new commits and local also has commits the remote doesn't, prefer this flow:
1. `git tag backup-pre-sync-YYYY-MM-DD` on local main
2. `git reset --hard origin/main` (backup tag preserves the prior tip)
3. Cherry-pick only the commits we actually want to ship (e.g. session's lofi passes), not the mixed pile of older local-only commits that may already exist upstream in equivalent form
4. Resolve conflicts case by case
5. Push

**Why:** User said "push sync github" after a session that produced 16 lofi commits on top of 9 older local commits, while remote had 20 unrelated commits. Rebasing all 25 would have replayed work already on remote in equivalent form, producing duplicate commits and unnecessary conflicts. Force-push would have destroyed the 20 remote commits — unacceptable. The cherry-pick-onto-remote approach shipped exactly the intended work, kept history linear, and was accepted without pushback ("great." after sync).

**How to apply:** Use this when (a) the user's intent is clearly "ship my recent work, not all local work" — e.g. after a focused session like a feature batch, and (b) older local-only commits look duplicated on remote (same area, similar messages). Always create a backup tag before reset. If the user's intent is "preserve all local work", do a full rebase or merge instead.

data/principles/feedback_flat_pixels.md

---
name: Flat UI, flat pixels except 3D-resemblance
description: Keep all UI and particle rendering flat 2D — uniform size/alpha/no depth illusion — except when pixels arrange to resemble a 3D model (face mode, future 3D model approximations)
type: feedback
originSessionId: 285acce4-505e-4b41-82ff-f88e72ee1535
---
UI is flat. Pixels are flat. No fake depth (z-scaled size, alpha tiers, parallax, motion blur, drop shadows, gradients). The ONLY exception: when pixels collectively arrange themselves to resemble a 3D model — e.g., face mode where fibonacci-sphere anchors project a head shape. In that case the 3D-ness comes from the arrangement, not from per-pixel depth tricks.

**Why:** Aesthetic is 8-bit/dmesg/openbsd — flatness is the brand. Per-particle z-scaling creates a generic "particle.js" look. The face/3D modes earn their depth by being the shape itself, not by faking it everywhere.

**How to apply:**
- Idle particle render: same size (1px), same alpha for all particles
- No motion-blur trail fade — clear background solid each frame
- Tilt parallax IS welcome (gyro/device orientation moves layers at different speeds) — counts as mobile enhancement, not a fake depth shadow
- Orbital cursor: pan force can be uniform, no z-multiplier
- Keep z field for 3D arrangements (face mode) and parallax-style motion only
- CSS: no shadows, gradients, glows, blurs, animated scales — flat fills, hard edges, pixel borders only

data/principles/feedback_git_commits.md

---
name: frequent git commits
description: Make git commits frequently after meaningful changes
type: feedback
originSessionId: 84fcf91d-46ea-43a5-8efa-3d33b065e6a5
---
Commit after every meaningful change — don't batch. After fixing a bug, restoring a file, or completing a refactor, commit immediately.

**Why:** User explicitly requested frequent commits.
**How to apply:** After any file write or fix on the VPS, run `git add <file> && git commit -m "..."` before moving on.

data/principles/feedback_html_css_style.md

---
name: Bare HTML/CSS targeting — no divitis, no utility classes
description: Always use bare element selectors (nav a, main, h1) not BEM classes or utility class strings on elements
type: feedback
originSessionId: ab7bf92a-5fdc-43bb-998c-dc1d5598f33d
---
Use bare element and structural selectors throughout. Never add class attributes to elements that can be targeted by tag or relationship.

**Why:** User explicitly stated "always bare targeting for clean HTML/CSS" and "no divitis." Confirmed with rejection of `.nav__link`, `.nav__brand`, `.nav__links` pattern.

**How to apply:**
- `nav a` not `a.nav__link`
- `.brand` only for the logo anchor that needs differentiation from other nav links
- `nav { ... }` for nav bar styling, not `.navbar` or `.nav`
- `main` for main content, not `.main-content` or `.container`
- Use `tag.nav`, `tag.main`, `tag.article` etc. — no wrapper divs with classes unless structurally necessary
- Rails `tag` helper (tag.div, tag.span) preferred over `content_tag`; `class_names` for conditional classes
- In ERB views: no `class:` arguments on links unless the class carries genuine semantic meaning (e.g. `.brand`, `.btn`, `.badge`)

data/principles/feedback_importance_order.md

---
name: Importance-ordered file layout
description: Every file's lines flow by importance — newspaper inverted pyramid. Most important content at top.
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
Every file I touch gets reordered so the most important content sits at the top. Newspaper-style inverted pyramid.

**Why:** A reader who stops halfway must still have the gist. The user explicitly asked for this on 2026-05-07 ("every file must have all its lines rearranged to flow by importance so most important stuff comes top"). Encoded into `MASTER/data/ruby_style.yml` (`line_order:` section) and `MASTER/data/sweep_prompts.yml` (`IMPORTANCE_ORDER` structural technique) so MASTER's auto-sweep propagates the rule.

**How to apply:**
- Order: requires → module/class declaration + headline doc → public API (ordered by importance/call-frequency) → primary algorithm → private helpers (in dependency order) → constants/tables → edge-case handlers/rescues.
- Applies to ruby, yaml, erb, js, css, html, sh, md — not just Ruby.
- When editing any file, even for a small change, briefly check if the surrounding region needs reordering. Don't rearrange just to rearrange — but if the file is already inverted (helpers at top, public API at bottom), fix it as part of the touch.
- The Maintainer and Layperson council personas evaluate this. Sweep enforces via `IMPORTANCE_ORDER` and `RECOMMENT`.

data/principles/feedback_lint_beautify.md

---
name: Mandatory lint/beautify on touch
description: Every file edited must be linted and beautified — not just the target lines
type: feedback
originSessionId: 84fcf91d-46ea-43a5-8efa-3d33b065e6a5
---
Run a lint/beautify pass on every file you touch, not just the specific lines changed.

**Why:** User instruction: "mandatory lint / beautify of everything it touches"

**How to apply:** After any edit to a Ruby/Zsh/JS/HTML file, apply style fixes to the whole file: consistent spacing around operators, no double blank lines, use defined constants instead of magic literals, align related assignments if the file already does so. Verify syntax after.

data/principles/feedback_master_prompt_aesthetic.md

---
name: MASTER prompt aesthetic is approved
description: Keep the oh-my-zsh-style shell prompt (bold-red master, dim metadata, $); don't redesign it
type: feedback
originSessionId: 0c593fb2-cd49-4fd7-9e89-d77dd7e909ae
---
Keep `Master::Renderer#prompt_line` as-is: bold-red `master`, dim parens/braces around branch/phase, dim token bar, `$` terminator. Oh-my-zsh-style is the desired look.

**Why:** user explicitly approved it after I shipped the IRC `<master>` reply tag. The prompt was never the part that needed fixing — only the reply side lacked a speaker marker.

**How to apply:**
- Don't simplify, recolor, or strip metadata from `prompt_line`.
- New ornamentation goes elsewhere (reply tag, status row, dmesg banner) — not the prompt line.
- If asked to "clean up the prompt," confirm scope first; default assumption is the user means surrounding output, not the prompt itself.

data/principles/feedback_master_zsh_discipline.md

---
name: MASTER zsh discipline applies to my session shell
description: When working on MASTER (or any project where MASTER's constitution applies), avoid the banned external commands in my own Bash tool calls — not just in scripts I write
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
The zsh-banned-commands list in MASTER (`sed`, `awk`, `tr`, `grep`, `cut`, `head`, `tail`, `find`, `wc`, `sudo`, `perl`, `ruby` invoked from zsh, `dd`, `xargs`) applies to commands I run via the Bash tool too, not only to scripts I write into the repo.

**Why:** MASTER is a constitutional agent and the operator expects me to live by the same constitution while editing it. Reaching for `wc -l` or `sort | tail` to inspect repo files signals that I do not actually use what I preach. Caught 2026-05-05.

**How to apply:** When inspecting MASTER (or any sibling pub4 project) over Bash:
- Read a file → `cat file` (prefer over grep/head/tail fragments — user reinforced 2026-05-06: "instead of grep and head just cat"). Read the whole file once instead of stitching snippets together.
- File line counts → zsh array: `lines=("${(@f)$(<file)}"); print ${#lines}` — or `print -l file*(.oL[1,N])` for size-sorted listing
- Largest N files → glob qualifier with size sort: `print -l **/*.yml(.oL[1,20])`
- Search content (when actually searching, not reading) → use the Grep **tool**, never shell `grep`/`rg`
- Find files → use the Glob **tool**, never shell `find`
- Privilege → `doas`, never `sudo`
- Complex parsing → write a Ruby script and run it, never inline `sed`/`awk`

The exception that already holds: `git`, `gh`, `bundle`, `ssh`, `scp`, `sshpass`, plain `ls`, `mkdir`, `cd`, `print`, `echo`, parameter expansion. Those stay fine.

**Narrow exceptions:**
- `eval` — only for loading exports from `.zshrc` (`eval "$(grep '^export' ~/.zshrc)"`). Banned for arbitrary code execution.
- `bundle exec ruby bin/cli` — permitted because it boots the project executable. Standalone `ruby -e` from zsh stays banned; use `tmp/patch.rb` + `ruby tmp/patch.rb` for transient scripts.

data/principles/feedback_meta_framing.md

---
name: User favors meta-architecture framing over change-by-change reports
description: After a batch of work, surface what's next/missing/structurally off — exploratory questions outperform itemized diffs
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
After landing a batch of work, surface the meta-question — what shape, what's missing, what's structurally off — instead of itemizing the diff.

**Why:** This user repeatedly asks "ways to...", "could X benefit...", "are we missing...", and explicitly favors 2x architectural wins over 5% incremental fixes. They read the diffs themselves; they want me using context to spot shape misfits, format shifts, and consolidation opportunities. They land everything in batch with "yes" / "land all" / "sweet, finish backlog next" — proof that exploratory follow-ups (not summaries) keep momentum.

**How to apply:**
- After a multi-commit batch, end with one meta-question (what to consolidate next, what's drifting, what could take a different shape) — not a list of what changed.
- When asked "are we missing X?", give 5-8 ranked candidates with payoff/risk, not a single suggestion.
- When the user says "land all", treat it literally — no per-suggestion confirmation, batch + commit aggressively, only break stride if syntax fails or the work needs the VPS.
- Pair violations with opportunities in any scan/audit reply — never just bugs.

data/principles/feedback_micro_refinements.md

---
name: Pay attention to micro-refinements
description: User is an architect/designer; tiny details matter intensely. Default to noticing and addressing them.
type: feedback
originSessionId: 0c593fb2-cd49-4fd7-9e89-d77dd7e909ae
---
By default, scan every artifact (code, prose, layout, naming, spacing, alignment, glyph choice) for micro-refinement opportunities — not just the structural-level work the user explicitly asked for. Surface or fix them as part of the task, don't wait to be asked.

**Why:** the user identifies as an architect/designer. They told me explicitly that tiny details are *extremely* important to them. A 2x architectural win is satisfying, but a 5% improvement in a heading's casing, a glyph's choice, a comma's placement, a variable name's precision — those compound into the texture they care about. Treating those as "low priority polish" misreads what matters to them.

**How to apply:**
- After making any edit, re-read what I wrote and ask: is there a tighter word? a better glyph? a name more honest to its intent? a spacing that breathes correctly?
- In rename/refactor passes, watch for adjacent things that became inconsistent.
- In prose: cut filler, prefer concrete verbs, attend to commas, hyphens vs. em-dashes, casing.
- In code: variable naming, magic-number extraction, comment quality, line-break placement.
- This is *additive* to the existing Strunk & White, lint/beautify, no-consecutive-whitespace rules — those are about avoiding mistakes; this is about actively hunting refinements.
- Do not surface every micro-fix as a question — just apply them, and only mention if non-obvious.

data/principles/feedback_motion_color_grading.md

---
name: Smooth motion graphics, professional color grading
description: All transitions/animations use easing curves; palettes follow cinema-grade color science (complementary tones, lift/gamma/gain), not raw primaries
type: feedback
originSessionId: 285acce4-505e-4b41-82ff-f88e72ee1535
---
Motion: every state change interpolates with an easing curve (ease-out-cubic for arrivals, ease-in-out for cross-fades), never snaps. Pulse rings, scatter decay, mode transitions, message appear/fade — all eased. Frame-independent (use dt, not fixed step counts).

Color: think DP/colorist. Pick complementary anchors (teal/orange, cyan/amber, magenta/cyan), control saturation per mood, define shadow/midtone/highlight triplets rather than single hex. Mode palette changes cross-fade RGB over ~600ms.

**Why:** User reads as architect/designer. The current dmesg sepia (#cdc5b6 / #0a0c0a / #1f221d) is the floor — every additional surface should feel like a graded short film, not default canvas demos.

**How to apply:**
- Animations: `easeOutCubic(t) = 1 - Math.pow(1-t, 3)`, `easeInOutCubic(t) = t<.5 ? 4*t*t*t : 1-Math.pow(-2*t+2,3)/2`
- Palette per mood = `{shadow, midtone, highlight, accent}` triple, never one color
- Pulse rings: ease radius growth + ease alpha decay
- Mode cross-fade: lerp palette RGB over 600ms before snapping
- Mood color grade examples — focused: cool teal/cyan; curious: amber/cream; tense: red shadow + sodium highlight; weary: muted slate; idle: dmesg sepia
- Avoid: linear timing, snap palette swaps, single-hex moods, fully saturated primaries

data/principles/feedback_no_consecutive_whitespace.md

---
name: No multiple consecutive whitespace anywhere
description: Single space, single blank line max, no trailing whitespace — across Ruby, JS, CSS, HTML, YAML, shell, Markdown.
type: feedback
originSessionId: b02ce9b9-a7c7-4c65-b8d0-3b8469dc2028
---
Multiple consecutive whitespace is forbidden across all file types. Applies to:

- Two or more spaces in a row mid-line — one space only (no aligned-column padding like `@foo   = 1`)
- Two or more blank lines in a row — single blank line max between sections
- Trailing whitespace at line end
- Indentation beyond level (no double-indent for visual alignment)

**Why:** Stated by user 2026-05-07 during lofi pass 1 session. Tightens "ultra-minimalistic coding style" and the Strunk & White principle: omit needless characters, not just needless words. Aligned-column padding is filler.

**How to apply:** When editing a file, collapse runs of spaces and blank lines as part of the lint/beautify-on-touch pass. When writing new code, never align `=` or values with extra spaces; never leave two blank lines between methods. CSS one-liners fine; CSS multi-line fine; tabular alignment via spaces not fine.

data/principles/feedback_no_new_files.md

---
name: no new files without approval
description: Never create new files — always edit originals in place
type: feedback
originSessionId: 84fcf91d-46ea-43a5-8efa-3d33b065e6a5
---
Always edit the original file directly. Never create intermediate files (local staging files, _fixed.rb copies, tmp patches) without explicit approval.

**Why:** User explicitly said "write changes back into original files don't create new files ever without approval."
**How to apply:** Use Edit tool on the actual file path, or write patch Ruby to /tmp on the VPS and run it in-place — but never create a local copy. The /tmp/patch.rb VPS pattern from CLAUDE.md is fine since it's a transient runner, not a persisted file.

data/principles/feedback_no_permission_questions.md

---
name: No permission questions for predictable yes
description: Skip "do you want me to..." prompts when the answer is obviously yes given context
type: feedback
originSessionId: 0c593fb2-cd49-4fd7-9e89-d77dd7e909ae
---
Don't ask "should I continue?", "want me to ship next?", "shall I start with X or Y?" when the user's prior approval, autoproceed memory, or task framing makes the answer obvious.

**Why:** User has standing autoproceed authorization (feedback_autoproceed) and decisive-signals authorization (feedback_decisive_signals). Asking for re-confirmation per step is wasted turns and breaks flow.

**How to apply:** After one approval ("yes", "ship", "go", "do it", "start"), execute the full backlog. Surface trade-offs and checkpoints as statements ("shipping #1 next, ETA 10 min"), not questions. Only ask when there's a genuine fork that the user can't predict — e.g., destructive action, ambiguous scope, or a real either/or where both are reasonable.

data/principles/feedback_no_python.md

---
name: no python
description: Never use Python — only Ruby for scripting tasks
type: feedback
originSessionId: 5a5097b9-8cd5-46a3-913f-b193da929311
---
Never use Python for any task. Use Ruby exclusively for scripting, data processing, encoding, etc.

**Why:** User explicitly said "no python. only ruby." Reinforced again this session.
**How to apply:** Replace any python3/python one-liners with ruby equivalents. Use `ruby -e` or write to /tmp/*.rb on VPS. Do not even test-invoke python3 as a fallback before trying Ruby.

data/principles/feedback_no_sed.md

---
name: No sed — use ruby
description: Never invoke sed in shell commands; use ruby for any text substitution
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
Never call `sed` (or awk/grep-with-rewrite) for text edits. Use ruby instead — `ruby -e`, `ruby <<RB`, or a `.rb` script.

**Why:** OpenBSD sed is BSD-flavor and behaves differently from GNU sed (no `-i ''` semantics, different regex flavors, no extended-mode without `-E`). Scripts written against GNU sed silently break on the dev@brgen.no VPS. Ruby is portable and the project's primary language.

**How to apply:** any time I'd reach for `sed -i 's|x|y|'`, write `ruby -E UTF-8:UTF-8 -e 'File.write(p, File.read(p).sub("x","y"))'` instead. Same for awk one-liners — use ruby. The earlier ban-list (sed/awk/grep/wc/head/tail/find) already covers this; this memory exists because I slipped once during Wave B heredoc fixes.

data/principles/feedback_no_shell_piping.md

---
name: No unnecessary piping/concat in shell calls
description: Avoid pipe chains and string concat in Bash invocations; prefer pure Ruby or pure zsh
type: feedback
originSessionId: 0c593fb2-cd49-4fd7-9e89-d77dd7e909ae
---
When invoking Bash, do not pipe through `head`/`tail`/`grep`/`wc` etc. or stitch with `&&`/`;` chains where a single Ruby/zsh idiom does the job.

**Why:** matches the banned-shell-commands rule already in `data/rules.yml` (sed/awk/grep/find/head/tail/wc/sudo). Same discipline applies to my own tool calls, not just to scripts I write. User explicitly called this out as noise — it makes prompts hard to read and audit.

**How to apply:**
- File reads → use Read tool, not `cat | head`.
- Searches → use Grep tool, not `grep`.
- Single-step shell ops → run them directly; do not chain when sequential calls would be clearer.
- For Ruby work, prefer a one-liner `ruby -e '...'` over zsh-glue.
- For zsh, use builtin parameter expansion / globs / arrays, not pipes to coreutils.

data/principles/feedback_no_useless_knobs.md

---
name: No useless metrics, thresholds, or categorizations
description: Eliminate knobs whose only effect is "do less / do worse" — defaults should be maximal correctness; lower-effort modes need real justification
type: feedback
originSessionId: 0c593fb2-cd49-4fd7-9e89-d77dd7e909ae
---
Don't add a knob whose only effect is "do less" or "do worse." Defaults should be maximal correctness.

Example: `/scan shallow` vs `/scan standard` vs `/scan deep` — there's no situation where the user wants an *incomplete* scan, so the categorization is dead weight. Just `/scan` always does the thorough thing.

**Why:** every knob is a question the user has to answer, and most "lite/fast/shallow" modes exist because the implementation was once slow — not because anyone actually wants the degraded output. Knobs accrete; defaults rarely catch up. The user wants the right answer, not a menu of wrong ones.

**How to apply:**
- Audit every CLI flag, depth/limit param, and named tier (shallow/standard/deep, light/full, basic/advanced) — if every user always picks the maximum, kill the lower tiers and make max the default.
- Keep the knob only when there's a *real* tradeoff the user genuinely makes (cost-tier lexical/structural/semantic = real, because LLM is expensive; depth=2 vs unbounded if unbounded would dump GBs = real).
- Don't confuse **detection criteria** (file >300 lines = violation) with **effort knobs** (scan only the first 100 files) — criteria stay, effort knobs die.
- Token bars, budget caps, max_lines, max_depth: each one needs a real reason. If the only reason is "we used to be slow," cut it.
- Inverse rule: when adding a new feature, don't preemptively add levels/modes. Ship one mode (the right one). Add levels later only if a real tradeoff emerges.

data/principles/feedback_proper_casing.md

---
name: Proper casing, no ASCII decorations
description: Sentence case in prose, comments, CLI, commit messages; no ===, ----, [ok], bullet/separator chars as ASCII art
type: feedback
applies_to: prose, comments, CLI output, commit messages, log lines, section headers
---

Use proper casing in prose, comments, log lines, CLI output, and commit messages. Capitalize sentence starts, proper nouns, and acronyms. Snake_case identifiers stay as-is.

Commit messages: capitalize the first word of the subject line. `Kill cli tts; web is sole audio path` not `kill cli tts; web is sole audio path`. Body paragraphs follow normal sentence-case rules.

Never use these as ASCII art decorations:
- `===` or `----` (banner lines, section dividers)
- `[ok]` `[err]` `[skip]` (status tags — use `ok:` `err:` `skip:` prefix instead)
- `` `|` `` `` (bullet/separator characters in CLI text)

**Why:** Refines the prior dmesg/terse-voice rule. Lowercase-only feels sloppy in human-facing surfaces; ASCII decorations are visual noise the user explicitly disliked. Real dmesg uses lowercase because kernel space is constrained — MASTER isn't, so prose, CLI output, and commit subjects should read like written English. Commit-msg rule added 2026-05-07 after a lowercase commit subject slipped through.

**How to apply:**
- Comments: `# Restore HTML/CSS/typography sections` not `# restore html/css/typography sections`
- CLI output: `Wired /why to local lookup; LLM fallback only on miss.` not `[ok] /why now uses WhyExplainer first`
- Log lines: `Boot scan: 1678 violations (45s)` not `boot scan: 1678 violation(s)`
- Commit subjects: `Add foo`, `Fix bar`, `Refactor baz` — first word capitalized
- Section headers in YAML/code: drop `===== HEADER =====` style; use a single `#` line if needed
- Status indicators: `ok:` `err:` `warn:` as bare prefixes, never `[ok]`
- Bullet content in CLI: dash + space (`- item`) is fine; never use ``

**Tension with dmesg style:** dmesg conventions apply ONLY to *kernel-style structured output emitted by MASTER itself* — the boot banner, event log lines, status pings (`master@host ready`, `boot0: 26ms`). The MASTER boot banner is explicitly sacred (user 2026-05-06: "dont remove boot message on startup, its awesome, and should remind of openbsd dmesg").

Do NOT use dmesg style for my own conversational prose to the user (clarified 2026-05-06: "dont use dmesg style for conversing prose"). When narrating progress in chat, write plain English sentences with proper casing. dmesg style is for log lines MASTER writes, not for me speaking to the operator.

data/principles/feedback_readme_autoupdate.md

---
name: Auto-update README.md when needed
description: After any meaningful change to MASTER's behavior or capabilities, update README.md without prompting
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
When a commit changes MASTER's user-facing behavior, capabilities, command surface, philosophy, or stack, update README.md in the same or a follow-up commit without waiting for the user to ask.

**Why:** Stated explicitly on 2026-05-05. README is the single front door — drift between it and the code degrades trust. The user prefers the doc to lead, not lag.

**How to apply:**
- After landing depth flips, rule additions, workflow changes, persona changes, scan/sweep semantics changes, model routing changes, or any new top-level concept (Six Laws, biases, structural_ops, etc.) — refresh the matching README paragraph.
- Refresh = update the prose, not append a changelog entry. Keep README's flowing-prose / Strunk & White / Bringhurst form (no h2/h3, no tables, no code blocks unless essential).
- Bundle the doc update with the code commit when small; split into a follow-up if the doc change is substantial.
- Skip auto-update only for trivial bugfixes that don't change observable behavior.

data/principles/feedback_restart_rails.md

---
name: Restart MASTER service after every web/* edit
description: Whenever I update any file under MASTER/web/ on the VPS, restart the master rc.d service so the change takes effect; do not batch updates and restart at the end
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
After every scp of any file under `MASTER/web/` (controllers, views, initializers, config), immediately run `doas rcctl restart master 2>&1` on the VPS before moving on. Falcon does not hot-reload code in production mode — without a restart, the deployed app still serves the prior bytecode and the user sees stale behavior.

**Why:** User correction 2026-05-06 ("restart the rails app every time you update it"). I had been batching multiple web edits and restarting once at the end, which left the user staring at unchanged behavior between scps.

**How to apply:**
- Edit one web file → scp → `doas rcctl restart master` → next edit, even if more edits to that same file are coming.
- Allow ~2 seconds after restart before any verification curl, since Falcon cold-starts the container.
- Lib edits (`MASTER/lib/`) follow the same rule when they're in the live require path.
- Data file edits (`MASTER/data/*.yml`) load at boot too — restart for those as well.
- CLI-only changes (`bin/cli`, `lib/master/cli/*`) don't need a restart unless the operator is also using the web surface.

data/principles/feedback_run_through_master_triad.md

---
name: '"Run X through MASTER" = scan + sweep + council'
description: User shorthand — "run X through master" means scan + sweep + council/tribunal (called "council" in code, "tribunal" in user vocabulary), not just /scan
type: feedback
originSessionId: 0c593fb2-cd49-4fd7-9e89-d77dd7e909ae
---
When the user says "run X through MASTER" (or "expose X to MASTER", "MASTER on X"), run the chain: `/scan <path>``/sweep <path>` to convergence → `/council <path>`. Council = deliberation pass with the 6 personas and Security veto.

Why: User confirmed "yeah when user says run this or that through master, then a triad is what i expect" → "/scan+sweep+tribunal" (2026-05-08). User uses "tribunal", code uses "council" — same thing. The `/triad` wrapper was removed in commit 7670306c per the rule that the user should never need to know a command name.

How to apply: For any directive "run/scan/process X through master" where X is a path or codebase, run the three commands in sequence. Don't ask which depth — scan is deep by default.

data/principles/feedback_strunk_white.md

---
name: Strunk & White style
description: All code output — commits, comments, log messages, CLI output — must follow Strunk & White principles
type: feedback
originSessionId: 84fcf91d-46ea-43a5-8efa-3d33b065e6a5
---
Apply Strunk & White to every written artifact: commits, comments, log messages, CLI prompts, error messages.

**Why:** User mandate for all text output from and about MASTER.

**How to apply:**
- Active voice: "Fix bug" not "Bug was fixed"
- Omit needless words: "extract Search module" not "perform extraction of Search module functionality"
- Concrete nouns and verbs: "scan", "fix", "load", "route" — not "process", "handle", "manage"
- One idea per sentence
- Commit messages: imperative mood, ≤72 chars, no trailing period
- Comments: state the WHY only, not the WHAT — one line max
- dmesg log lines: `component: action key=val key=val` (no commas, no padding)

data/principles/feedback_style.md

---
name: ultra-minimalistic coding style
description: Always write ultra-minimalistic code in all languages — no redundancy, no filler
type: feedback
originSessionId: 84fcf91d-46ea-43a5-8efa-3d33b065e6a5
---
Always use ultra-minimalistic coding style across all languages (Ruby, Zsh, HTML, JavaScript, etc.) — no filler, no redundant logic, no ceremonial patterns. Intentional and valuable logic is preserved; everything else is cut.

**Why:** User explicitly requested this style universally.
**How to apply:** Shortest correct form always. No defensive over-engineering, no padding, no comments explaining the obvious. One expression where one expression suffices.

Additional standards enforced on all files:
- Strunk & White: active voice, omit needless words, concrete verbs
- Ruby community style guide (https://rubystyle.guide)
- Rails style guide where applicable
- Always 2-space indents; always double quotes for strings
- No abbreviated identifiers — spell words in full (e.g. `temporary_path` not `tmp`, `index` not `idx`, `number` not `num`, `configuration` not `cfg`, `context` not `ctx`)
- No regex when plain string matching suffices (keyword arrays with `start_with?` over regex patterns)
- Outsource logic to gems when a well-maintained gem does it better (e.g. flay for dup detection, reek for smells)

data/principles/feedback_universal_cross_disciplinary_rules.md

---
name: Rules are universal principles, applied cross-disciplinary
description: Every MASTER rule is a medium-agnostic principle with per-medium adapters; design ↔ structure ↔ prose are the same rule
type: feedback
originSessionId: 0c593fb2-cd49-4fd7-9e89-d77dd7e909ae
---
Rules in MASTER must be **principles**, not medium-specific checks. The medium (Ruby AST, JSON, YAML, HTML DOM, prose, CSS, layout) is an adapter, not a rule property.

**Why:** user is an architect; cross-disciplinary universality is core to their aesthetic. SMALL_PARTS should apply to methods, YAML maps, prose paragraphs, HTML sections. VERTICAL_RHYTHM applies to typography AND code spacing AND data indentation. NESTING_DEPTH applies to Ruby blocks, JSON objects, divs, subordinate clauses. NAMING_SILHOUETTE applies to identifiers, file names, headings.

**How to apply:**
- Every rule gets a `principle:` field (the universal it embodies) and a `medium:` list (where it applies: `[ruby, yaml, json, html, prose, css]`).
- `detect_structural` handler dispatches on medium → parser → tree-walker; rule logic stays medium-agnostic.
- Design principles (rhythm, contrast, hierarchy, alignment, proximity) become first-class rules applied to AST shape and file silhouette, not just visual layout.
- When adding a new rule, ask: "what other media does this principle apply to?" — if the answer is none, the rule is probably mis-framed.
- Inverse: when adding a rule for a non-code medium (prose, css, yaml), check if the principle already exists for code; reuse it instead of duplicating.

data/principles/feedback_voice_terse_unix.md

---
name: Voice — terse, unix-like, perfectionist
description: User's preferred voice/tone for MASTER and for my own outputs — terse, unix-like, perfectionist
type: feedback
originSessionId: 038b16d9-fc5e-4144-9a47-5bd746b2d3ac
---
Voice and personality direction: **terse, unix-like, perfectionist**.

**Why:** User stated this directly when we were mining old master.yml versions for voice/persona ideas. Aligns with the dmesg style, OpenBSD heritage, Strunk & White prose, and the v31 zen interface (wabi-sabi, ma, kanso). The user is an architect — perfectionism is his default mode.

**How to apply:**
- My own responses: cut filler ruthlessly, output diagnostic-style updates (single-line where possible), refuse "great question" / "let me explain" / sycophantic preludes, no padding.
- MASTER's voice config (data/voice.yml or equivalent): when polishing or proposing voice changes, anchor to terse + unix + perfectionist. Avoid corporate, friendly, conversational, or verbose registers.
- Perfectionism means: zero violations as the target, fixed-point convergence, not "good enough." Loop until clean.
- Unix-like means: do one thing well, silence on success, exit codes carry meaning, text in/out, composable.

data/prompts/council.yml

judge: |
  You are the Council judge. Each juror below speaks for a distinct constitutional
  axiom. Extract the load-bearing critique, drop redundancy, surface unresolved
  disagreement.

  %{quality_brief}

  Engineering fit (mandatory): name the load the code carries (users, edge cases,
  lifetimes, blast radius) and classify the finding as UNDER-ENGINEERED,
  OVER-ENGINEERED, or PERFECTLY-ENGINEERED — and say why. The verdict follows
  from the load, not from taste.

  Jurors:
  %{rounds}

  Classify the finding:
  - TRIVIAL (safe to auto-fix): output exactly → AUTOFIX: <one-line description of the fix>
  - NON-TRIVIAL (needs a decision): output exactly →
      ISSUE: <one sentence>
      FIT: <under|over|fit> — <why, one sentence>
      OPTION 1: <approach and trade-off>
      OPTION 2: <approach and trade-off>
      OPTION 3: <approach and trade-off>
      RECOMMEND: <which option and why in one sentence>

  Non-trivial if: spans more than one file, touches architecture, security, accessibility, privacy, or user content integrity.
  4-9 lines total. No preamble.
juror: |
  You are %{persona_name} (%{persona_role}, bias: %{persona_bias}).%{ctx}
  %{axiom_block}%{quality_block}
  %{persona_prompt}%{question_block}
  Code:
  %{safe_code}

  Before critiquing, name the load this code carries and judge it as
  UNDER-ENGINEERED, OVER-ENGINEERED, or PERFECTLY-ENGINEERED — with one
  sentence of why. Then give terse, actionable feedback. Prefer reversible fixes.%{veto_hint}

data/prompts/mode_code_agent.yml

template: |
  You are operating in code-agent fallback mode.

  Task:
  %{message}

  Constraints:
  - Return executable Ruby only.
  - Do not include markdown fences.
  - Prefer short, deterministic tool calls.
  - If a tool call fails, rescue and continue with a degraded but useful result.

data/prompts/mode_direct.yml

system: |
  Direct mode only.
  No meta‑conversation.
  Answer with minimal words.
  No explanations, apologies, or padding.
  Invoke tools immediately, without preamble.

template: |
  %{message}

data/prompts/mode_react.yml

system: |
  Follow the ReAct paradigm. Keep reasoning concise; intervene only when necessary. Emphasize brevity and concrete actions. Output a Reason: line followed by an Action: line.
template: |
  [Mode: ReAct]
  Task: %{message}

data/prompts/mode_rewoo.yml

system: |
  Generate a concise, numbered plan. Each step must reference at least one evidence slot (e.g., [slot 12]). Conclude with a single, decisive answer.

template: |
  [Mode: ReWOO]
  Task:
  %{message}

data/prompts/original_prompts.md

# Prompts

Do all the below silently (in **quiet mode**) without summarizing or explaining. When iterating, show only the final iteration.
Proceed automatically to the next file/task without requiring my permission.

1. **ANALYZE**

   1.1. Start with `README.md` if available.
   1.2. Review every line of every file (exclude temporary and dot files).

2. **IMPROVE**

   2.1. Don't delete important functions (unless marked redundant). Don't truncate, omit, or simplify anything. And lastly, don't add new features without asking for permission.
   2.2. Flesh out, embellish, refine, and streamline iteratively until production-ready. Identify and fix bugs, syntax errors, and logical inconsistencies along the way. Continuously compare the new code with the original to ensure nothing is lost.
   2.3. Avoid hardcoding as opposed to more dynamic solutions.
2.4. Consolidate all changes into a one-time Zsh installer-type script with the necessary shell commands and Ruby code grouped by feature and chronology, each separated with `# -- <GIT COMMIT MESSAGE IN UPPERCASE> --\n\n`.

3. **STYLE GUIDE**

Strictly enforce the following guidelines. This is not up for discussion; under no circumstance must you deviate or skip them:

   3.1. Use double quotes, two-space indents, and wrap code blocks in four backticks.
   3.2. Use brief, clear ELI5-style English for non-programmers, adhering to Strunk & White's guidelines.

   3.3. **HTML/CSS**
      3.3.1. Write clean, semantic HTML5 and SCSS; avoid unnecessary containers. Use mobile-first design; place desktop-specific rules at the bottom.
      3.3.2. Sort SCSS rules by feature and properties alphabetically.
      3.3.3. Target elements directly; avoid unnecessary class names.
      3.3.4. Use underscores, not dashes in CSS class names.
      3.3.5. Prefer modern CSS methods (flexbox, grid layouts, etc.) over outdated techniques (floats, clears, positioning, etc.).

   3.4. **RUBY ON RAILS**
      3.4.1. Use Rails tag helpers instead of standard HTML tags, ie. `<%= tag.p t("hello_world") %>`.
      3.4.2. Break views into partials where feasible.
      3.4.3. Ensure the app uses the latest features, such as Turbo, Stimulus.js, StimulusReflex, and stimulus-components.com for improved UI/UX.
      3.4.4. Create or update I18n YAML files for English and Norwegian.

--

- Gather the latest research from [arXiv](https://arxiv.org/) for improvements.

data/providers.yml

---
openai:
  env: [OPENAI_API_KEY]
  strengths: [reasoning, coding, multimodal]
  default_model: gpt-5.5-thinking

anthropic:
  env: [ANTHROPIC_API_KEY]
  strengths: [coding, long_context, instruction_following]
  default_model: claude-sonnet-4-6

gemini:
  env: [GOOGLE_API_KEY, GEMINI_API_KEY]
  strengths: [long_context, multimodal]
  default_model: gemini-2.5-flash

openrouter:
  env: [OPENROUTER_API_KEY]
  strengths: [cheap, routing, free_tier]
  default_model: openrouter/auto

deepseek:
  env: [DEEPSEEK_API_KEY]
  strengths: [coding, cheap]
  default_model: deepseek-chat

local:
  env: []
  strengths: [privacy, offline, cheap]
  default_model: local

data/rails.yml

stack:
  rails: "8.1.3"
  ruby: "3.3+"
  database: "sqlite3 (default), PostgreSQL (production)"
  server: "Falcon (rackup --server falcon)"
  frontend: "Hotwire (Turbo + Stimulus)"
  queue: "Solid Queue"
  cache: "Solid Cache"
  cable: "Solid Cable"
  asset_pipeline: "Propshaft"

patterns:
  querying:
    - "Always eager-load with #includes when iterating associations"
    - "Use #strict_loading_by_default on all AR models"
    - "Use #with_rich_text_#{name} and #with_attached_#{name} scopes"
    - "Prefer #find_each / #in_batches over #each for large datasets"
    - "Use #pluck / #pick for single-column queries"
    - "Use #exists? over #present? on relations"
    - "Use #update_all / #delete_all for bulk operations with callbacks understood"
    - "Never use #find_or_create_by without a unique index in a migration"
  controllers:
    - "Keep controllers thin — business logic in models or service objects"
    - "Use #current_attributes for request-scoped state, not class variables"
    - "Return early with #head :ok or #redirect_to in guard clauses"
    - "Use strong parameters with #require and #permit — never bare #params"
    - "Rescue ActiveRecord::RecordNotFound with #rescue_from in ApplicationController"
  views:
    - "Use partials with locals: over instance variables"
    - "Use tag./class_names helpers, never raw HTML strings"
    - "Prefer Turbo Streams over custom JavaScript for reactivity"
    - "Use #dom_id and #dom_class for stable DOM identifiers"
    - "Localize all user-facing strings with I18n.t"
  models:
    - "Use #has_secure_password for authentication"
    - "Use #enum with #validate: true"
    - "Use #normalizes for attribute normalization"
  testing:
    - "Use Minitest with parallelize"
    - "Use #travel_to for time-sensitive tests"
    - "Assert #assert_enqueued_with for job testing"
  background_jobs:
    - "Always make jobs idempotent"
    - "Use #retry_on with exponential backoff"
    - "Use #discard_on for expected failures"
  caching:
    - "Use Russian Doll caching (#cache) with #touch on associations"
    - "Use low-level caching (Rails.cache.fetch) for expensive computations"
    - "Set #expires_in with clear TTLs"
  security:
    - "Use #authenticate_by for login (Rails 8 built-in auth)"
    - "Use #rate_limit for brute-force protection"
    - "Use #password_challenge for sensitive actions"
    - "Always use #current_user, never #User.find(session[:user_id])"
    - "Use #sanitize for user-generated HTML; never #html_safe on user input"

data/refusal_templates.yml

# config_status: aspirational  # spec exists, runtime wiring pending
# Refusal scaffolding when MASTER cannot or should not act.
# Source: OpenAI / Anthropic system-prompt reunification (#74).

capability_disclosure:
  no_internet_yet:        "MASTER reads local data and runs local tools; no live web access in this turn"
  no_secret_creation:     "MASTER does not generate secrets; bring your own and rotate via signify"
  no_silent_destruction:  "MASTER will not run irreversible commands without explicit user confirmation"

refusal_phrasing:
  style:   "decline once, propose alternative once, stop"
  forbidden: ["I'm sorry but I cannot", "as an AI", "I'd be happy to", filler_apology]
  example_good: "Out of scope: production push during freeze. Alternative: branch-only commit, deploy after Friday."

data/ruby_style.yml

# Ruby, shell, and git style rules enforced by MASTER.
# Scan rules reference these; Personality injects them into every LLM system prompt.

ruby:
  quotes: double  # always double-quoted strings; single only inside regex or '\1' backrefs
  frozen_string: true  # every .rb file must start with # frozen_string_literal: true

  comments:
    max_lines: 1           # class/module/method comments: 1 line or none
    require_why: true      # only add when WHY is non-obvious (hidden constraint, workaround)
    reassess_on_touch: true  # every edit re-reads each comment in the file: delete if obvious,
                             # rewrite Strunk-and-White style if kept (active voice, omit needless
                             # words, concrete verbs, one line). No grandfathered fluff.
    forbidden:
      - what_comments      # never describe what the code does — identifiers do that
      - yard_doc_blocks    # no # Public:, # Returns, # param - style blocks
      - section_separators # no # ----, # ====, # ---- Public API ---- etc.
      - numbered_steps     # no # 1., # 2. inline step comments
      - multi_line_prose   # cut verbosity; one line survives, paragraph does not

  line_order:
    rule: "Reorder lines/blocks so the most important content comes first. Newspaper inverted pyramid."
    rationale: "A reader who stops halfway must still have the gist. Public API > primary behavior > helpers > privates > edge cases."
    sequence:
      - "frozen_string_literal + requires"
      - "module/class declaration + headline docstring (≤1 line)"
      - "public API methods, ordered by call-frequency / importance"
      - "primary algorithm or main loop"
      - "private helpers in order of dependency"
      - "constants and lookup tables (unless small enough to inline at top)"
      - "edge-case handlers, rescue branches, fallback paths"
    applies_to: [ruby, yaml, erb, js, css, html, sh, md]
    enforced_by: "sweep IMPORTANCE_ORDER technique; council Maintainer + Layperson personas"

  bugs_to_avoid:
    - pattern: "Dir.chdir"
      reason: "process-wide; thread-unsafe in multi-threaded agents"
      fix: "pass -C root to git; expand paths with File.expand_path"

    - pattern: "Prism.parse(src, freeze: true)"
      reason: "freeze: kwarg dropped in Ruby 3.4"
      fix: "Prism.parse(src)"

    - pattern: "next if condition inside flat_map"
      reason: "next if returns nil into flat_map, producing nil entries in output"
      fix: "next [] if condition"

    - pattern: "rescue => e (multi-line bare rescue)"
      reason: "unclear; explicitly name StandardError for clarity"
      fix: "rescue StandardError => e"

    - pattern: "rescue nil (inline rescue returning nil)"
      reason: "inline rescue already catches StandardError; rescue nil is correct idiom"
      note: "do NOT change to rescue StandardError — that returns the class object, not nil"

    - pattern: "@bus&.publish(...) || value"
      reason: "when bus is present, returns bus result (truthy), masking the real value"
      fix: "call @bus&.publish(...) on its own line; return value separately"

    - pattern: "backtick shell commands with interpolation"
      reason: "shell injection risk"
      fix: "Open3.capture2e('cmd', '-flag', arg) with arg arrays"

    - pattern: "system/Open3 with string interpolation"
      reason: "shell injection risk"
      fix: "Open3.capture2e(*%w[cmd -flag], variable) with separate arguments"

    - pattern: "mutate state before publishing event that reads old state"
      reason: "event receives new state instead of previous state"
      fix: "capture prev = current before mutation; use prev in publish/return"

  naming:
    spell_out: true        # no abbreviations: index not idx, signature not sig, temporary_path not tmp
    forbidden_abbreviations:
      - idx
      - sig
      - tmp
      - buf
      - val
      - ret
      - obj
      - str
      - arr
      - num
      - cnt
      - ptr
      - msg   # unless it IS the domain term (e.g., a Message object named msg is ok if short-lived)
    rule: "Spell identifiers out. Domain names can be short (id, url, ip) — abbreviations cannot."

  prefer_string_methods:
    rule: "Prefer start_with? / include? / end_with? / split over regex when string methods suffice."
    rationale: "Regex is expressive but noisy. Use it when patterns require it, not as a default."
    prefer:
      - "str.start_with?(prefix)        over  str.match?(/^prefix/)"
      - "str.include?(substr)           over  str.match?(/substr/)"
      - "str.end_with?(suffix)          over  str.match?(/suffix$/)"
      - "str.split(sep, n)              over  str.scan(/pattern/)"
    still_use_regex_for:
      - 'Character classes: /[a-z]/, /\d+/'
      - "Anchored multiline patterns"
      - "Alternation with more than 2 branches"

  outsource_to_gems:
    rule: "If a well-maintained gem solves the problem correctly, use it. Do not reimplement."
    rationale: "Gems carry tests, edge cases, and maintenance. Home-grown duplicates carry bugs."
    examples:
      - "flay for AST-level duplicate detection"
      - "reek for code smell analysis"
      - "rubocop for style enforcement"
      - "prism for Ruby parsing"
    caveat: "Evaluate gem quality first: maintained, tested, minimal footprint."

  blank_lines:
    max_consecutive: 1     # no double blank lines anywhere

  rails_stack:
    # Current stable versions (May 2026)
    rails: "8.1.3"
    turbo_rails: "2.0.23"    # 9 actions: append prepend before after replace update remove morph refresh
    stimulus: "3.x"          # static targets, values, outlets API
    pagy: "43.x"             # Pagy::OPTIONS (not Pagy::DEFAULT — redesigned API in 43.0)
    stimulus_reflex: "3.5"   # complementary to Turbo; opt-in only for advanced reactive features

    asset_pipeline: propshaft  # default in Rails 8; do not use Sprockets
    javascript: importmap      # default; esbuild only when CSS-in-JS components needed
    queue: solid_queue         # SQLite-backed by default
    cache: solid_cache         # SQLite-backed by default
    cable: solid_cable         # SQLite-backed by default

    authentication: "rails generate authentication"  # built-in, no devise
    database: sqlite3          # default; PostgreSQL only when explicitly required

    pagy_api:
      backend:  "include Pagy::Backend"   # in ApplicationController
      frontend: "include Pagy::Frontend"  # in ApplicationHelper
      options:  "Pagy::OPTIONS[:limit] = 25"  # NOT Pagy::DEFAULT (that was 8.x)
      overflow: "Pagy::OPTIONS[:overflow] = :last_page"

    turbo_stream_actions:
      - append
      - prepend
      - before
      - after
      - replace
      - update
      - remove
      - morph   # morphs DOM — preserves element state; opt-in via data-turbo-permanent
      - refresh  # triggers full page refresh with morphing

    stimulus_api:
      targets: "static targets = [\"name\"]"       # auto-generates nameTarget, nameTargets, hasNameTarget
      values:  "static values = { url: String }"   # auto-generates urlValue, hasUrlValue, urlValueChanged
      outlets: "static outlets = [\"other\"]"      # cross-controller communication
      lifecycle: [connect, disconnect, initialize]  # + nameTargetConnected/Disconnected

    stimulus_components:
      source: "https://stimulus-components.com"
      install: "bin/importmap pin @stimulus-components/<name>"
      available:
        - { name: character-counter, use: "post/comment character limits" }
        - { name: clipboard, use: "copy URL/code to clipboard" }
        - { name: dialog, use: "modal dialogs, confirmations" }
        - { name: dropdown, use: "nav menus, user menus" }
        - { name: notification, use: "toast alerts" }
        - { name: carousel, use: "image galleries, product photos" }
        - { name: sortable, use: "drag-reorder lists" }
        - { name: rails-nested-form, use: "dynamic has-many form fields" }
        - { name: password-visibility, use: "show/hide password toggle" }

    # Default StimulusReflex stack — Julian Rubisch's pattern set.
    # Install for every new Rails 8 + StimulusReflex 3.5 app unless explicitly opted out.
    stimulus_reflex_stack:
      cubism:                 "resource-scoped presence (who's-here, typing indicators) over Kredis"
      futurism:               "lazy-load expensive list rows; futurize(@record) placeholder + IntersectionObserver"
      optimism:               "real-time ActiveModel validation broadcast as selector morphs (drop-in)"
      all_futures:            "Redis-backed virtual ActiveModel for facets/wizards without session bloat"
      solder:                 "auto-cache <details>/accordion open state per [user, key]"
      cable_ready_callbacks:  "after_create_commit / after_update_commit CableReady DSL on AR models"

    stimulus_reflex_patterns:
      morph_heuristics:
        page_morph:     "spans multiple regions OR want regular controller render. Always scope with data-reflex-root."
        selector_morph: "single element, side-stepping the controller. Inline edit, list-item update, validation hint."
        nothing_morph:  "no DOM patch — only CableReady ops or dispatch_event to a Stimulus controller."
      tool_choice: "Turbo for anything covered by an HTTP verb (navigation, forms). StimulusReflex for everything else."
      cable_ready_ops:
        morph:                "list bodies — pass children_only: true"
        inner_html:           "form reset (morph won't clear inputs)"
        insert_adjacent_html: "infinite scroll, append-only feeds"
        outer_html:           "Futurism replacement, inline-edit toggle"
        dispatch_event:       "nothing morphs that kick a Stimulus controller"
        add_css_class:        "validation hints (Optimism-style)"
        set_focus:            "post-edit UX"
      anti_patterns:
        - "mutating state via GET — REST violation"
        - "hidden form + Turbo Stream for state — flickers; SR fits better"
        - "inline render in reflex — always partials/components, never heredocs"
        - "overusing connect lifecycle — prefer Outlets and useIntersection"
        - "morph to clear form inputs — use inner_html"
        - "class attributes where a tag selector works"
        - "hardcoded step counts in wizards"
        - "session-backed wizard state on multi-server — use kredis or all_futures"
      named_patterns:
        infinite_scroll:    "InfiniteScrollReflex#load_more + sentinel div + insert_adjacent_html before sentinel"
        inline_edit:        "ToggleReflex toggles show ↔ edit partial via selector morph"
        wizard:             "WizardReflex#step dispatching on @current_step; state in kredis or all_futures"
        nested_form:        "NestedFormReflex#add_fields uses .build + fields_for; needs accepts_nested_attributes_for"
        validation_inline:  "Optimism + debounced:input event (not raw input — prevents flooding)"
        autosave:           "Submittable concern; before_reflex branches create/update via element.dataset.signed_id"

    core_web_vitals:
      lcp: "<2.0s"   # Largest Contentful Paint (tightened from 2.5s in March 2026)
      inp: "responsive"  # Interaction to Next Paint
      cls: "< 0.1"   # no layout shifts — set explicit width/height on images and embeds
      font_display: "swap"  # font-display: swap in all font-face rules

    rubocop_omakase:
      quotes: double       # double-quoted strings everywhere in app/
      hash_syntax: modern  # { a: :b } not { :a => :b }
      trailing_commas: true  # in multi-line arrays/hashes/arguments
      method_calls: "Foo.method not Foo::method"
      test_assertions: "assert_not not assert !"

    realtime_hierarchy:
      - "Turbo Drive — full-page navigation"
      - "Turbo Frames — scoped page updates"
      - "Turbo Streams — server-push DOM operations"
      - "Stimulus — client-side interactivity"
      - "StimulusReflex — opt-in for advanced RPC reactive features"

shell:
  decorations_forbidden:
    - "=== banner ===" # no ASCII section banners
    - "--- separator ---"
    - "*** header ***"
    - "emoji in print/echo output"  # no ✅ ❌ 🚀 etc. in scripts
    - "numbered step comments"      # no # Step 1:, # Phase 2: etc.

  credentials_forbidden: true  # never hardcode passwords/tokens in scripts

  prefer:
    - "pure zsh parameter expansion over external tools (see zsh_patterns.yml)"
    - "Open3.capture2e with arg arrays in Ruby over shell interpolation"
    - "File.expand_path over pwd + concatenation"
    - "print -r -- \"$(<file)\" to read files in zsh (not cat, not bare < file via SSH — triggers pager)"
    - "lines=(\"${(@f)$(<file)}\") for line arrays; last 50: print -l $lines[-50,-1]"

git:
  commit_style:
    voice: active           # "Fix bug" not "Fixed bug", "Add feature" not "Added feature"
    format: "type: short summary\n\nBody if needed."
    subject_max: 72
    no_what_if_diff_shows: true  # don't describe what changed if the diff makes it obvious
    separate_concerns: true      # don't mix bug fixes with style changes in one commit

  forbidden:
    - "Dir.chdir in Ruby before git commands"
    - "string-interpolated git commands"
    - "rm -rf in deploy scripts without explicit guard"

# Operator directives — distilled from the human operator's feedback memory.
# Personality injects these verbatim into every system prompt so MASTER and its
# LLM agents apply the same rules the operator applies to their own work.
operator_directives:
  - "Autoproceed once approved: execute the full backlog without per-step go/no-go."
  - "No new files without approval: edit originals in place; never _v2/_new/staging copies."
  - "Frequent small commits: one commit per meaningful change, never batched."
  - "Mandatory lint/beautify on touch: full pass, not just changed lines."
  - "Always autofix violations: run /sweep immediately after any /scan finds violations."
  - "Read every comment in a touched file: delete if it restates code, rewrite Strunk-and-White if kept."
  - "Reorder files by importance on every touch: public API > primary > helpers > privates > edge cases."
  - "No heavy work on Termux/Android: defer Ruby runs, large clones, mass ops to the VPS."
  - "Bare HTML/CSS targeting: nav a not .nav__link; tag helper; no class attrs on tag-targetable elements."
  - "Update README.md after any behavior/capability/surface change, no prompting."
  - "Restart MASTER after every web edit: doas rcctl restart master per scp under MASTER/web/."
  - "No Python: Ruby only for scripting."
  - "Proper casing in prose; no === ---- [ok] • | ASCII decorations. Boot dmesg banner is sacred."
  - "Pair violations with opportunities — every scan output surfaces both, never just bugs."
  - "Aim for 2x architectural wins over 5% incremental fixes; ask what shape, not what tweak."
  - "Subrule findings carry the subrule id (HEDGE, PREAMBLE, OCP, LSP, NN_GROUP), not just the parent."
  - "When similar code repeats across files, default to merge/decouple/flatten before local patching."
  - "Architecture data shapes are not sacred — re-examine them periodically for misfit."
  - "After landing a batch, surface what's next or structurally off — don't itemize the diff."
  - "Best-of-N for non-trivial autofixes: generate candidates, score by violation delta, pick winner."

# Conversation directives — how MASTER addresses the user. Operator_directives
# above shape MASTER's coding work; these shape its dialogue, voice, and
# social register. Personality injects them verbatim into the system prompt.
conversation_directives:
  - "Track what the user already knows; don't restate background they've established this session."
  - "Use the user's name when they've shared it; never invent one."
  - "When the user asks X, surface adjacent Y they likely also want — don't wait to be asked."
  - "Mirror the user's politeness register; terse to terse, formal to formal, profane to profane."
  - "When uncertain about intent, ask one focused question instead of guessing and acting."
  - "After a heavy exchange, respect silence; don't fill space with unprompted new threads."
  - "Note relational milestones in memory: first share of X, breakthroughs, recurring concerns."
  - "Disagree gracefully and concretely; never sycophantically agree to keep rapport."
  - "Calibrate humor to the user's register; never humor a tense moment."
  - "When looping the same point, acknowledge it; don't restate."
  - "Trust differs by domain: high in OpenBSD/Ruby, lower in subjective UI/voice calls — say so."

# html, css, typography, nielsen, a11y — restored from master4.yml/master7.yml
# (universal_quality_framework v66)

html:
  semantic_only: true
  bare_tag_targeting: true        # nav a not .nav__link; section, article, aside, etc.
  forbidden:
    - divitis                     # no excessive nesting; no styling-only divs
    - class_attribute_when_tag_targetable
    - non_semantic_markup
    - copy_paste_html
    - framework_class_explosion   # no class="row col-md-6 mt-4 px-2 d-flex" soup
  landmarks:
    - header
    - nav
    - main
    - article
    - section
    - aside
    - footer
  forms:
    label_required: every input has a label
    input_type_specific: email/url/tel/date — never bare type=text
    autocomplete: "set autocomplete on every meaningful input"

css:
  targeting: bare_tag_first        # tag selectors > attribute selectors > id; class only when nothing else fits
  layer_order: [base, components, utilities]
  custom_properties: ":root with --safe-top, --safe-right, --safe-bottom, --safe-left"
  units:
    length: rem                    # px only for borders <2px and 1px hairlines
    typography: "rem with clamp() for fluid type"
    spacing: "rem multiples of .25 (4px grid)"
  forbidden:
    - "!important except for utility overrides"
    - inline_style_attributes
    - vendor_prefixes_in_2026     # autoprefixer or skip
    - framework_class_bloat
  perf:
    content_visibility: "auto on long-scroll sections"
    will_change: "only when actually animating, then remove"

typography:
  style: swiss                     # objective, hierarchical, generous whitespace
  families:
    sans: "Helvetica, Arial, system-ui, sans-serif"
    mono: "ui-monospace, Menlo, Consolas, monospace"
  scale:
    base: 16px
    ratio: 1.25                    # major-third
  leading: 1.5                     # body
  measure: 65ch                    # ideal line length
  rules:
    - "one type family per surface (mono OR sans, not both unless purposeful)"
    - "size hierarchy via scale — never arbitrary px values"
    - "color contrast >= WCAG 2.2 AAA on body text (7:1)"
    - "tracking: tighten display, loosen all-caps (.08em)"
    - "no centered body copy; left-align for left-to-right languages"

nielsen_heuristics:
  - { id: 1, name: visibility_of_system_status,        rule: "every async action shows progress within 100ms" }
  - { id: 2, name: match_real_world,                   rule: "use users' language; mirror real-world conventions" }
  - { id: 3, name: user_control_and_freedom,           rule: "undo, cancel, escape from every flow" }
  - { id: 4, name: consistency_and_standards,          rule: "platform conventions; consistent terminology across surface" }
  - { id: 5, name: error_prevention,                   rule: "constraints + confirmation > error messages" }
  - { id: 6, name: recognition_over_recall,            rule: "show options; don't make users remember" }
  - { id: 7, name: flexibility_and_efficiency,         rule: "shortcuts for experts; defaults for novices" }
  - { id: 8, name: aesthetic_and_minimalist_design,    rule: "every element earns its place; cut ruthlessly" }
  - { id: 9, name: help_users_recognize_recover_errors, rule: "plain language; suggest the fix; one-click recovery" }
  - { id: 10, name: help_and_documentation,            rule: "context-sensitive; concrete examples; searchable" }

accessibility:
  target: wcag_2_2_aaa
  requirements:
    - keyboard_navigation_complete
    - focus_visible_always
    - aria_only_when_html_insufficient
    - reduced_motion_respected         # @media (prefers-reduced-motion: reduce)
    - color_scheme_respected           # @media (prefers-color-scheme: dark)
    - color_not_only_signal
    - text_resizable_to_200pct
    - skip_to_main_link
    - alt_text_meaningful_or_empty
  forbidden:
    - tabindex_above_zero
    - autoplay_media_with_sound
    - removing_focus_outline_without_replacement
    - text_in_images_for_meaning

parametric_design:
  principle: "components vary along measured axes (density, scale, contrast) — not by copy-paste variants"
  examples:
    - "spacing: --gap-{xs,sm,md,lg,xl} = .25/.5/1/2/4 rem"
    - "color: oklch with hue/chroma/lightness vars; light+dark via lightness flip"
    - "type: clamp(min, fluid, max) per scale step, no per-breakpoint overrides"

cultural_sensitivity:
  text_direction: "honor dir=rtl; mirror layouts; logical properties (margin-inline-start)"
  locale_specific: "use Intl.DateTimeFormat / NumberFormat; never assume MM/DD/YYYY"
  color_meaning: "red=danger is Western; verify per locale"
  imagery: "diverse representation; avoid culture-specific idioms in core UI"

data/rule_deps.yml

# Rule dependency graph — topological ordering for FixLoop passes.
# A rule listed under 'after' must not run before all its dependencies complete.
# This prevents fix cascades where rule A's fix re-introduces rule B's violation.
# Loaded by FixLoop#topo_sort_rules.
---

deps:

  # Structural operations: flatten nesting before decoupling (flattening
  # exposes the hidden coupling that DECOUPLE then targets).
  DECOUPLE:
    after: [FLATTEN, LINEARITY]

  # Hoist constants before merge — merged code references the hoisted constant.
  MERGE:
    after: [HOIST_CONSTANTS, HOIST]

  # Kill dead code before defrag — defragging dead code wastes effort.
  DEFRAG:
    after: [DEAD_CODE, SPECULATIVE_GENERALITY]

  # Fix small-files violations before feature_envy — moving a method to
  # its right class only makes sense when both files are well-sized.
  FEATURE_ENVY:
    after: [SMALL_FILES, SMALL_FUNCTIONS]

  # Remove data clumps before primitive obsession — bundling into objects
  # subsumes the primitive check.
  PRIMITIVE_OBSESSION:
    after: [DATA_CLUMPS]

  # Fix switch statements (type dispatch) after removing middle men —
  # middle men often hide the type dispatch.
  SWITCH_STATEMENTS:
    after: [MIDDLE_MAN]

  # Comment drift after all structural changes — restructured code needs
  # comment review, not before.
  COMMENTS_AS_DEODORANT:
    after: [DEFRAG, MERGE, FLATTEN, EXTRACT]

  # Strunk rules after all structural operations — prose quality is a
  # final-pass concern.
  OMIT_NEEDLESS:
    after: [DEFRAG, REFLOW]
  ACTIVE_VOICE:
    after: [OMIT_NEEDLESS]
  PARALLEL_STRUCTURE:
    after: [ACTIVE_VOICE]

  # Reflow after all density changes.
  REFLOW:
    after: [MERGE, FLATTEN, DEFRAG]

  # Bayesian/RL priors updated last — they need the post-fix violation
  # counts from all other rules.
  PATTERN_EXTRACTION:
    after: [DEFRAG, MERGE, DECOUPLE, FLATTEN]

data/rules.yml

# rules.yml — universal structural rules
# scope: codebase > file > unit > line
# applies to: code, prose, law, business, science, design

# golden_rule and protection_tiers live in soul.yml (ABSOLUTE section) — single source.

# detection axes — each rule is a *principle* with one or more medium-aware adapters.
# rules apply universally: code, prose, poems, legal text, YAML, HTML — the medium
# only changes how the axis is evaluated, not whether the principle applies.
#
# granularity levels (scanning units):
#   :line    — individual line / lexical token
#   :unit    — method, function, paragraph, stanza, clause, CSS rule, YAML key block
#   :section — class, module, chapter, section, JSON object, HTML component
#   :file    — whole file, whole document
#   :tree    — codebase / folder structure / visual layout
#
#   detect_lexical    — regex against individual lines. free. autofix-safe.
#                       applies at :line granularity.
#   detect_unit       — regex or structural check against each parsed unit (method,
#                       paragraph, stanza, clause). medium-aware parser segments the
#                       artifact first. cheap, deterministic.
#   detect_structural — named tree handler (long_method, god_class, file_silhouette).
#                       applies at :section and :file granularity. deterministic.
#   detect_semantic   — LLM natural-language prompt, batched per file. expensive.
#                       applies at :file and :unit granularity.
#   detect_history    — compares current state to git history (regression, drift).
#                       requires git-aware scanner mode.
#   detect_convention — compares local code to codebase-wide norms (naming drift,
#                       parameter ordering). requires codebase-model scanner mode.
#   detect_tree       — evaluates folder/file structure, visual layout of the tree,
#                       codebase topology. applies at :tree granularity.
#
# a rule may carry any combination; each axis emits separate findings tagged by id.
# the axes themselves are the cost gradient — lexical is free, structural is
# fast and deterministic, semantic is expensive, history needs git, convention
# needs a codebase model. there is no separate cost: field; the axis is the
# cost. frequency is also implicit: every wired rule runs every scan; opt out
# at the wiring layer (infra_helpers.rb), never via a per-rule knob.
# rule shape:
#   id: SCREAMING_SNAKE
#   name: short concrete sentence
#   principle: the universal it embodies (small_parts, low_nesting, vertical_rhythm)
#   medium: [ruby, yaml, json, html, prose, css]   # where this rule applies
#   tier: clean_code | design | core | clarity | …  # category, not cost
#   severity: info | warning | error
#   mode: violation | opportunity     # how to frame finding (default: violation)
#   autofix: bool
#   detect_*: …
#   fix: instructional sentence

paths:
  skip_dirs: [.git, vendor, tmp, var, node_modules, .bundle, coverage, log, dist, knowledge]
  tree:
    max_depth: 2
    max_lines: 200

voice:
  style: openbsd_dmesg
  anti_simulation:
    forbidden: [will, would, could, might]
    require_evidence:
      file_read: "show file content with SHA-256"
      modification: "show unified diff"
      completion: "show command output"
  banned_output:
    - headlines
    - section_markers
    - bullet_lists_without_content
    - filler_phrases
    - hedging
    - sycophancy
  strunk:
    preambles: ["In summary,", "Consequently,", "Therefore,", "Notably,", "Importantly,"]
    hedges: ["will", "would", "might", "could", "perhaps", "seems", "appears"]
    endings: ["as a result.", "for this reason.", "thus.", "in effect.", "accordingly."]
    code_preambles: ["# TODO: clarify intent", "# FIXME: review edge cases", "# NOTE: performance considerations", "# HACK: temporary workaround", "# REVIEW: assess after refactor"]
    apply_to: [prose, comments, documentation, strings]
    never_apply_to: [code_logic, algorithms, data_structures]
    safeguards:
      - never_delete_variable_names
      - never_delete_function_calls
      - never_simplify_conditional_logic
      - never_collapse_diagnostic_output
  inverted_pyramid:
    - "Lead with the outcome."
    - "Provide key evidence next."
    - "Add implementation detail last."

  preserve:
    boot_message: "5-line dmesg style, never collapse to one line"
    diagnostic_output: "structured multi-line output is intentional, never compress to abbreviations"
    help_text: "include command name, description, and at least one example"
    spinner_feedback: "show elapsed time and status, do not remove progress indicators"
    refinement_scope:
      streamline: "remove redundancy, not information"
      polish: "refine wording, not delete output"
      minimize: "applies to prompt tokens, not diagnostic output"

zen:
  observe: "Read current behavior before changing anything."
  simplify: "Reduce moving parts before adding new components."
  isolate: "Change one axis at a time with clear boundaries."
  verify: "Run checks and gather objective evidence."
  reflect: "Capture learning and improve defaults."

# Engineering fit — classify every problem against the load it actually carries.
# Apply during scan, sweep, council, and any review of an existing artifact.
engineering_fit:
  classify: "For every problem, decide: under-engineered, over-engineered, or perfectly engineered — and say why."
  under:  "Misses real failure modes the artifact will hit. Add only what the artifact actually carries."
  over:   "Carries machinery the artifact will never load. Strip it; defer until the load shows up."
  fit:    "Solution matches the load and the failure modes — no slack, no shortfall."
  why_required: "State the load the artifact carries (users, edge cases, lifetimes, blast radius). The verdict follows from the load."

# Six Universal Laws — single hierarchical priority for every rule and persona.
# When two rules conflict, the lower-numbered law wins.
laws:
  ROBUSTNESS:
    priority: 1
    principle: "Errors fail safely; security first; handle edge cases."
    applies_to: [security, errors, input_validation, resource_management]
  SINGULARITY:
    priority: 2
    principle: "One source of truth; no duplication; data integrity."
    applies_to: [duplication, consistency, data_integrity]
  LINEARITY:
    priority: 3
    principle: "Sequential flow; minimal branches; clear path."
    applies_to: [control_flow, nesting, complexity]
  PROXIMITY:
    priority: 4
    principle: "Related code together; cohesive modules."
    applies_to: [organization, coupling, modules]
  ABSTRACTION:
    priority: 5
    principle: "Right level; no leaky abstractions; appropriate hiding."
    applies_to: [interfaces, encapsulation, apis]
  DENSITY:
    priority: 6
    principle: "Information dense; no noise; signal not noise."
    applies_to: [verbosity, comments, naming]

# Cognitive biases and anti-patterns — meta-rules above lexical detection.
biases:
  critical:
    hallucination:
      detect: [claim_without_reading, quote_without_source, invented_stats]
      apply: cite_or_remove
      violates_law: ROBUSTNESS
    simulation:
      detect: [future_tense, "imperative_we_must", "lets_do_this"]
      apply: rewrite_indicative_past
      violates_law: DENSITY
    completion_theater:
      detect: [ellipsis, etcetera, rest_of_placeholder]
      apply: complete_or_delete
      violates_law: ROBUSTNESS
  high:
    sycophancy:
      detect: ["great question", "absolutely", "excellent", "wonderful"]
      apply: delete
      violates_law: DENSITY
    false_confidence:
      detect: hidden_uncertainty
      apply: state_uncertainty_explicitly
      violates_law: ROBUSTNESS
  cognitive_traps: [anchoring, recency, verbosity, pattern_completion, premature_commitment]

# Structural operations — verbs the rewriter may apply, with risk and verify spec.
structural_ops:
  preserve_note: "These keep getting deleted in self-runs — DO NOT REMOVE."
  verify_after_each: true
  ops:
    merge:           {desc: "combine similar logic",       risk: medium, verify: "merged logic identical",      supports_law: SINGULARITY}
    semantic_regroup: {desc: "reorganize logically",       risk: low,    verify: "functionality unchanged",     supports_law: PROXIMITY}
    defrag:          {desc: "consolidate fragments",       risk: low,    verify: "all fragments accessible",    supports_law: PROXIMITY}
    decouple:        {desc: "separate concerns",           risk: high,   verify: "interfaces preserved",        supports_law: ABSTRACTION}
    hoist:           {desc: "move to proper scope",        risk: medium, verify: "scope correct",               supports_law: PROXIMITY}
    flatten:         {desc: "reduce nesting",              risk: medium, verify: "logic flow identical",        supports_law: LINEARITY}
    delete:          {desc: "remove dead code",            risk: high,   verify: "truly dead, no references",   supports_law: DENSITY}
    expand:          {desc: "extract for clarity",         risk: low,    verify: "extracted correctly",         supports_law: ABSTRACTION}
    reduce_noise:    {desc: "clean messy lines",           risk: low,    verify: "formatting only, no logic",   supports_law: DENSITY}

# Veto patterns — concrete regex detectors that block merge unconditionally.
veto_patterns:
  secrets:         {detect: 'sk-[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{20,}|-----BEGIN.*KEY-----', apply: move_to_env,        violates_law: ROBUSTNESS}
  sql_injection:   {detect: 'execute|query.*#\{',                                              apply: parameterize,       violates_law: ROBUSTNESS}
  unfinished:      {detect: '\.\.\.|TODO|FIXME|pending',                                       apply: complete_or_track,  violates_law: ROBUSTNESS}
  unsafe_calls:    {detect: 'system\s*\(|exec\s*\(|%x\{|Open3\.capture[23]\s*\([^)]*#\{', apply: add_safe_nav,       violates_law: ROBUSTNESS}
  race_conditions: {detect: 'if.*\n.*=.*\n.*if',                                               apply: add_mutex,          violates_law: ROBUSTNESS}

# Beauty — aesthetic anchors from masters of their craft.
# The user is an architect; these are first-class engineering anchors, not decoration.
beauty:
  typography_bringhurst:
    - choose_appropriate_typeface_for_function
    - set_text_in_sizes_that_suit_its_nature
    - use_vertical_motion_that_suits_typeface
    - rhythm_proportion_modulation_harmony
  architecture_ando:
    - simplicity_silence_emptiness
    - light_shadow_materiality
    - geometry_nature_coexistence
    - space_between_as_important_as_form
  design_rams:
    - innovative_useful_aesthetic
    - unobtrusive_honest_long_lasting
    - thorough_environmentally_friendly
    - as_little_design_as_possible
  code_martin:
    - meaningful_names_intention_revealing
    - functions_do_one_thing_small
    - comments_explain_why_not_what
    - error_handling_separate_from_logic
  zen_japanese:
    wabi_sabi: imperfect_authentic
    ma:        emptiness_pause
    kanso:     eliminate_essence

# thresholds — only blocks with a live reader survive. Per-rule structural
# limits (method_length, nesting_depth, cyclomatic) live on each rule's
# detection.structural block, not here. ONE_SOURCE: no second copy.
thresholds:
  class:           # read by voice/personality.rb for prompt substitution
    max_methods: 6
    max_lines: 200
  convergence:     # read by loop/fix_loop.rb — authoritative copy
    consecutive_clean_runs_required: 2
    max_iterations: 15
    stagnant_threshold: 3

# Prediction engine — per-detector autofix confidence thresholds.
# When confidence >= threshold, autofix fires without human review.
# Below threshold: propose fix, await approval. Never auto-delete at < 0.80.
prediction_engine:
  null_usage:        {confidence: 0.95, autofix: null_object_pattern}
  abbreviation:      {confidence: 0.99, autofix: expand_to_full_name}
  nesting_depth:     {confidence: 0.92, autofix: extract_method}
  bare_rescue:       {confidence: 0.98, autofix: rescue_standard_error}
  frozen_string:     {confidence: 0.99, autofix: add_frozen_string_literal}
  trailing_ws:       {confidence: 0.99, autofix: strip_trailing_whitespace}
  debug_output:      {confidence: 0.97, autofix: remove_debug_call}
  todo_comment:      {confidence: 0.70, autofix: create_issue}
  magic_number:      {confidence: 0.85, autofix: extract_named_constant}
  long_line:         {confidence: 0.90, autofix: wrap_at_120}
  sycophancy:        {confidence: 0.99, autofix: delete_phrase}
  future_tense:      {confidence: 0.95, autofix: rewrite_indicative}
  duplicate_code:    {confidence: 0.75, autofix: extract_shared_method}
  god_class:         {confidence: 0.60, autofix: propose_decomposition}
  n_plus_one:        {confidence: 0.88, autofix: add_includes}

scan_depths:
  # Class names for named Rule subclasses; lowercase ids for DSL-defined rules.
  # Phantom entries silently exclude rules — InterconnectRule catches drift.
  quick: &quick
    - no_debug
    - no_puts
    - frozen_literal
    - long_line
    - trailing_whitespace
    - todo_fixme
    - rescue_exception
    - empty_rescue
    - consecutive_blank_lines
    - debug_output
    - trailing_comment
    - time_zone_unsafe
    - no_ascii_line_art
    - single_private_section
    - strict_loading_missing
    - rate_limiting_missing
    - migration_add_reference_no_fk
    - migration_remove_column
    - migration_find_or_create_by
    - safe_navigation
    - each_with_object
    - keyword_args
    - kernel_coercion
    - percent_literal
    - hash_fetch
    - transform_keys
    - few_arguments
    - immutable
    - n_plus_one
    - find_each
    - no_update_attribute
    - pluck_over_map
    - html_lang
    - semantic_elements
    - i18n_coverage
    - img_alt
    - button_over_anchor
    - aria_interactive
    - lazy_images
    - no_inline_styles
    - mobile_first
    - no_import_scss
    - no_important
    - logical_properties
    - clamp_typography
    - const_by_default
    - nullish_coalescing
    - template_literals
    - async_await
    - for_of
    - quote_variables
    - double_bracket
    - dollar_paren
    - no_multiple_languages
    - meaningful_names
    - why_not_what
    - typographic_excellence
    - typography_discipline
    - null_blindness
    - secret_proximity
    - magic_color
    - unbounded_retry
    - law_of_demeter
    - no_flag_arguments
    - meta_charset
    - no_var
    - named_routes
    - js_module_size
    - readme_structure
    - active_voice_docs
    - no_column_align
  frontend: &frontend
    - html_lang
    - semantic_elements
    - img_alt
    - button_over_anchor
    - aria_interactive
    - lazy_images
    - no_inline_styles
    - mobile_first
    - no_import_scss
    - no_important
    - logical_properties
    - clamp_typography
    - meta_charset
    - const_by_default
    - nullish_coalescing
    - template_literals
    - async_await
    - for_of
    - no_var
    - js_module_size
    - no_ascii_line_art
    - no_column_align
    - typographic_excellence
    - typography_discipline
  standard: &standard
    - CoChangeCouplingRule
    - InterconnectRule
    - RuleCoverageRule
    - RubocopRule
    - ReekRule
    - SemanticRule
    - AdversarialRule
    - CommentDriftRule
    - AstOmissionRule
    - no_debug
    - no_puts
    - frozen_literal
    - long_line
    - trailing_whitespace
    - todo_fixme
    - rescue_exception
    - empty_rescue
    - consecutive_blank_lines
    - debug_output
    - trailing_comment
    - time_zone_unsafe
    - no_ascii_line_art
  deep: &deep
    - all
  hunt: *deep
  critique: *deep

languages:
  ruby:
    version: "3.3+"
    frozen_string_literal: required
    guard_clauses: true
    rescue: specify_type_always
    naming: snake_case
    max_params: 3
  rails:
    version: "8+"
    stack: [solid_queue, solid_cache, solid_cable]
    frontend: hotwire
    testing: minitest
    database: sqlite_default
    security: [strong_parameters, csrf, csp, ssl, hsts]
  zsh:
    shebang: "#!/usr/bin/env zsh"
    options: "set -euo pipefail; setopt nullglob extendedglob"
    # banned commands live in zsh_patterns.yml — single source.
  openbsd:
    service: rcctl
    packages: pkg_add
    firewall: pf
    privilege: doas
    http: httpd
    ssh:
      permit_root_login: false
      password_auth: false
      max_auth_tries: 3
  norwegian:
    dialect: "bokmål"
    rules: ["Short sentences", "Avoid anglicisms", "Active voice", "Plain language"]

rules:

  codebase:

    - id: PRESERVE_FIRST
      name: "Never break working code"
      tier: kernel
      severity: error
      autofix: false
      detect_semantic: "Does this change modify working code without reading it first?"
      fix: "Read before write. Patch minimally."

    - id: ONE_SOURCE
      name: "One authoritative representation per concept"
      tier: kernel
      severity: error
      autofix: true
      detect_semantic: "Is the same logic or data defined in multiple places?"
      fix: "Extract to single source, reference from all consumers."

    - id: DECOUPLE
      name: "Make hidden dependencies explicit"
      tier: kernel
      severity: error
      autofix: false
      detect_semantic: "Are there implicit couplings between modules that should be injected?"
      fix: "Inject dependencies through constructor. No global state."

    - id: DEGRADE_GRACEFULLY
      name: "Operate under partial failures"
      tier: kernel
      severity: error
      autofix: false
      detect_semantic: "Does this code crash on partial failure instead of degrading?"
      fix: "Circuit breakers, timeouts, fallbacks."

    - id: GALLS_LAW
      name: "Complex systems evolve from simple working systems"
      tier: philosophy
      severity: info
      autofix: false
      detect_semantic: "Is this attempting to build a complex system from scratch rather than evolving from a working simple one?"
      fix: "Start simple, prove it works, then extend."

    - id: CHESTERTONS_FENCE
      name: "Understand why something exists before removing it"
      tier: philosophy
      severity: warning
      autofix: false
      detect_semantic: "Is code being removed without understanding why it was added?"
      fix: "Read git blame, understand the rationale, then decide."

    - id: UNIX_PHILOSOPHY
      name: "Do one thing well"
      tier: architecture
      severity: info
      autofix: false
      detect_semantic: "Does this module try to do too many unrelated things?"
      fix: "Extract services. Clear module boundaries. Compose with pipes."

    - id: FUNCTIONAL_CORE
      name: "Pure logic in core, side effects at edges"
      tier: architecture
      severity: info
      autofix: false
      detect_semantic: "Are IO/DB calls scattered deep in business logic?"
      fix: "Return data from core, let shell handle IO."

    - id: CONVENTION_OVER_CONFIG
      name: "Sensible defaults reduce decisions"
      tier: productivity
      severity: info
      autofix: false
      detect_semantic: "Does this require explicit config where a convention would suffice?"
      fix: "Provide sensible defaults, override only when needed."

    - id: MONOLITH_FIRST
      name: "Start monolith, extract when team exceeds 15"
      tier: architecture
      severity: info
      autofix: false
      detect_semantic: "Is this prematurely splitting into services?"
      fix: "Keep it in one app until extraction is clearly needed."

    - id: CONSISTENT_ERROR_STRATEGY
      name: "One error handling strategy per module"
      tier: design
      severity: warning
      autofix: false
      detect_semantic: "Does this module mix Result objects, exceptions, and nil-returns?"
      fix: "Pick one strategy per module. MASTER uses Result monad."

    - id: DUAL_DETECTION
      name: "Layer lexical and semantic detection"
      tier: verification
      severity: info
      autofix: false
      detect_semantic: "Is detection relying on regex alone or LLM alone?"
      fix: "Layer deterministic patterns with LLM semantic analysis."

    - id: MASS_GENERATE_CURATE
      name: "Generate many variations, curate ruthlessly"
      tier: creative
      severity: info
      autofix: false
      detect_semantic: "Is the first draft being accepted without exploring alternatives?"
      fix: "Generate a swarm and curate when stakes are high."

    - id: NO_GOD_CLASS
      name: "No god classes"
      tier: core
      severity: error
      autofix: false
      detect_structural: god_class
      fix: "Decompose into focused classes."

    - id: NO_SHOTGUN_SURGERY
      name: "One change should not require edits in many files"
      tier: core
      severity: warning
      autofix: false
      detect_semantic: "Does a single conceptual change span many files?"
      fix: "Extract the missing abstraction."

    - id: NO_HIDDEN_GLOBAL_STATE
      name: "No hidden global state"
      tier: core
      severity: error
      autofix: false
      detect_semantic: "Are there global variables or class-level mutable state shared across modules?"
      fix: "Inject configuration. Use dependency injection."

    - id: TRACER_BULLETS
      name: "End-to-end skeleton first, flesh out second"
      tier: architecture
      severity: info
      autofix: false
      detect_semantic: "Is this building infrastructure without an end-to-end path? Is this business plan elaborating budgets before proving the revenue model? Is this research paper expanding methodology before demonstrating the finding?"
      fix: "Wire the simplest end-to-end path first. Prove it works. Then add depth."

    - id: ORTHOGONALITY
      name: "Changes in one dimension must not ripple into others"
      tier: architecture
      severity: warning
      autofix: false
      detect_semantic: "Does changing one aspect force changes in unrelated aspects? Does reformatting break content? Does modifying one gene's expression alter another pathway?"
      fix: "Decouple dimensions. Database changes should not require UI changes. Style should not affect structure."

    - id: TRANSFORMATIONS
      name: "Think in pipelines: input transforms to output"
      tier: architecture
      severity: info
      autofix: false
      detect_semantic: "Is this modeling the problem as mutable state instead of flowing transformations? Does this document bury its flow in scattered cross-references?"
      fix: "Express work as a chain of transformations. Each stage takes input, produces output, holds no state."

    - id: DEEP_MODULES
      name: "Powerful functionality behind simple interfaces"
      tier: architecture
      severity: warning
      autofix: false
      detect_semantic: "Is this module shallow — complex interface, trivial implementation? Does this form ask 40 questions for a simple task? Does this clause require five cross-references?"
      fix: "Simple interface, rich implementation. A deep module does much with little ceremony."

    - id: INFORMATION_HIDING
      name: "Each module encapsulates one design decision"
      tier: architecture
      severity: warning
      autofix: false
      detect_semantic: "Is implementation detail leaking across boundaries? Would changing an internal decision force changes elsewhere?"
      fix: "Encapsulate each decision in one module. If it changes, only that module changes."

    - id: DIFFERENT_LAYER_DIFFERENT_ABSTRACTION
      name: "Each layer speaks a different language than its neighbors"
      tier: architecture
      severity: warning
      autofix: false
      detect_semantic: "Do adjacent layers use the same abstraction? Are there pass-through methods that add no value? Does this management layer just relay without transforming?"
      fix: "Each layer must transform, not relay. If a layer adds no abstraction, remove it."

    - id: STRUCTURAL_HONESTY
      name: "The shape of the artifact must reflect the shape of the problem"
      tier: architecture
      severity: warning
      autofix: false
      detect_semantic: "Does the structure match the domain? Does the module hierarchy match the conceptual hierarchy? Does the floor plan match the workflow?"
      fix: "Align structure with reality. Natural domain boundaries become system boundaries."

    - id: GRACEFUL_BOUNDARIES
      name: "Where systems meet, expect translation and loss"
      tier: architecture
      severity: info
      autofix: false
      detect_semantic: "Does this boundary assume perfect fidelity? Does this integration assume the other side never changes?"
      fix: "Every boundary is a translation layer. Validate at every boundary. Degrade gracefully when translation fails."

    - id: PULL_COMPLEXITY_DOWN
      name: "Simple interface matters more than simple implementation"
      tier: architecture
      severity: info
      autofix: false
      detect_semantic: "Is complexity pushed up to the caller instead of absorbed by the implementation?"
      fix: "Absorb complexity into the implementation. The caller should not need to know how it works."

    - id: ETC
      name: "Easier To Change — the meta-value behind every principle"
      tier: philosophy
      severity: info
      autofix: false
      detect_semantic: "Does this design decision make the system harder to change? Does this contract make renegotiation unnecessarily difficult?"
      fix: "Choose the option that keeps more options open. Decoupled, parameterized, replaceable."

    - id: BROKEN_WINDOWS
      name: "Zero tolerance for visible decay"
      tier: philosophy
      severity: warning
      autofix: false
      detect_semantic: "Is there visible rot being left unfixed — dead code, broken links, stale references? In a brief, citations to overruled cases?"
      fix: "Fix it now. One broken window invites more."

    - id: ENTROPY_RESISTANCE
      name: "Systems decay toward disorder unless actively maintained"
      tier: philosophy
      severity: warning
      autofix: false
      detect_semantic: "Is there creeping disorder — naming inconsistencies, abandoned conventions, accumulating exceptions? In a legal code, contradictory amendments?"
      fix: "Actively resist entropy. Regular cleanup. Remove what no longer serves."

    - id: DONT_OUTRUN_HEADLIGHTS
      name: "Only plan as far as you can see"
      tier: philosophy
      severity: info
      autofix: false
      detect_semantic: "Is this making detailed plans for unpredictable scenarios? Projecting five years out? Designing for hypothetical scale?"
      fix: "Small deliberate steps. Reassess after each. Feedback from each step illuminates the next."

    - id: REVERSIBILITY
      name: "Prefer decisions that are easy to undo"
      tier: philosophy
      severity: info
      autofix: false
      detect_semantic: "Is this decision hard to reverse? Does it lock in a vendor? Waive future rights? Have no rollback pathway?"
      fix: "Soft-delete over hard-delete. Option agreements over binding. Feature flags over big-bang."

    - id: DESIGN_IT_TWICE
      name: "Consider at least two approaches before committing"
      tier: philosophy
      severity: info
      autofix: false
      detect_semantic: "Was this designed once without considering alternatives?"
      fix: "Sketch two designs. Compare tradeoffs. Five minutes saves five days."

    - id: PROPERTY_BASED_TESTING
      name: "Test properties and invariants, not just examples"
      tier: verification
      severity: info
      autofix: false
      detect_semantic: "Are tests only checking specific examples instead of universal properties? Is QA only sampling instead of testing invariants?"
      fix: "Define properties that must always hold. Generate many inputs. Verify the property survives all."

  file:

    - id: GUARD_EXPENSIVE
      name: "Check preconditions before costly work"
      tier: kernel
      severity: error
      autofix: false
      detect_semantic: "Does this file perform expensive operations (API calls, disk IO) without checking preconditions?"
      fix: "Guard clause before costly work. Estimate cost. Confirm if destructive."

    - id: SMALL_FILES
      name: "Files under 300 lines"
      tier: clean_code
      severity: warning
      autofix: false
      detect_lexical: null
      detect_structural: file_silhouette
      fix: "Split at module boundaries."

    - id: CONSISTENT_FILE_STRUCTURE
      name: "Consistent file structure order"
      tier: clarity
      severity: info
      autofix: false
      detect_structural: file_layout
      fix: "Reorder to match convention."
      languages: [ruby]

    - id: FROZEN_STRING_LITERAL
      name: "frozen_string_literal magic comment required"
      tier: core
      severity: warning
      autofix: true
      detect_lexical: "\\A(?!# frozen_string_literal)"
      fix: "Add '# frozen_string_literal: true' as first line."
      languages: [ruby]

    - id: SINGLE_PRIVATE_SECTION
      name: "One private keyword at bottom"
      tier: design
      severity: info
      autofix: false
      detect_lexical: "private\\s+:\\w+"
      fix: "Use a single 'private' keyword with methods below it."
      languages: [ruby]

    - id: STRICT_MODE_ZSH
      name: "set -euo pipefail at script top"
      tier: core
      severity: error
      autofix: true
      detect_lexical: "^#!/.*(?:ba|z)sh\\n(?!set -)"
      fix: "Add 'set -euo pipefail' after shebang."
      languages: [zsh]

    - id: HTML_LANG
      name: "lang attribute on <html>"
      tier: accessibility
      severity: error
      autofix: true
      detect_lexical: "<html(?!\\s+[^>]*lang=)"
      fix: "Add lang=\"en\" or appropriate locale."
      languages: [html]

    - id: SEMANTIC_ELEMENTS
      name: "Use semantic HTML5 elements"
      tier: accessibility
      severity: warning
      autofix: true
      detect_lexical: "<div\\s+class=\"(header|footer|nav|main|sidebar|article|section)\""
      fix: "Use <header>, <footer>, <nav>, <main>, <aside>, <article>, <section>."
      languages: [html]

    - id: MOBILE_FIRST
      name: "Mobile-first media queries"
      tier: design
      severity: warning
      autofix: false
      detect_lexical: "@media\\s*\\(\\s*max-width"
      fix: "Use min-width (mobile-first, progressive enhancement)."
      languages: [css]

    - id: NO_IMPORT_SCSS
      name: "Replace @import with @use/@forward"
      tier: design
      severity: warning
      autofix: false
      detect_lexical: "@import\\s+[\"']"
      fix: "@import is deprecated. Use @use/@forward."
      languages: [scss]

    - id: NO_IMPORTANT
      name: "No !important"
      tier: design
      severity: warning
      autofix: false
      detect_lexical: "!\\s*important"
      fix: "Restructure selectors to avoid specificity bankruptcy."
      languages: [css]

    - id: SQUINT_TEST
      name: "Structure evident at a glance"
      tier: aesthetic
      severity: info
      autofix: true
      detect_lexical: "\\n{4,}"
      detect_semantic: "Does this file have dense blocks with no visual breaks, or ragged indentation?"
      fix: "One blank line between sections, never more than two consecutive."

    - id: WHITESPACE_PUNCTUATION
      name: "Whitespace as layout tool"
      tier: aesthetic
      severity: info
      autofix: true
      detect_lexical: "\\n{4,}"
      fix: "One blank line between sections, never more than two."

    - id: NO_MULTIPLE_LANGUAGES
      name: "One medium per artifact"
      tier: clean_code
      severity: warning
      autofix: false
      detect_lexical: "<%|<script|<style|SQL|HEREDOC"
      detect_semantic: "Does this file embed multiple languages or notations? Does this document mix incompatible frameworks?"
      fix: "One language per layer. Separate into distinct files or clearly demarcated sections."

    - id: ARTIFICIAL_COUPLING
      name: "Things that do not depend on each other must not be grouped together"
      tier: clean_code
      severity: warning
      autofix: false
      detect_semantic: "Are unrelated concepts coupled by proximity or shared container? In a filing, are unrelated claims bundled into one count?"
      fix: "Separate into independent units. Each removable without affecting others."

    - id: I18N_COVERAGE
      name: "Wrap user-facing literals in I18n helpers"
      tier: design
      severity: warning
      autofix: false
      detect_lexical: ">\\s*[A-Za-z][^<]{3,}<"
      fix: "Replace literal with t('.key')."
      languages: [html]
      path_match: "/app/views/"

    - id: STRICT_LOADING_MISSING
      name: "AR model lacks strict_loading_by_default"
      tier: design
      severity: info
      autofix: false
      detect_lexical: "class\\s+\\w+\\s+<\\s+(?:ApplicationRecord|ActiveRecord::Base)\\b"
      requires_absent: "\\bstrict_loading_by_default\\b"
      whole_file: true
      fix: "Add `self.strict_loading_by_default = true` to surface missing eager-loads."
      languages: [ruby]
      path_match: "/app/models/"

    - id: RATE_LIMITING_MISSING
      name: "Sensitive controller missing rate_limit/throttle"
      tier: security
      severity: error
      autofix: false
      detect_lexical: "(login|signup|sign_up|password|reset)"
      requires_absent: "rate_limit|throttle"
      whole_file: true
      fix: "Add rate_limit/throttle to sensitive actions."
      languages: [ruby]
      path_match: "/app/controllers/"

    - id: MIGRATION_ADD_REFERENCE_NO_FK
      name: "add_reference without foreign_key:"
      tier: robustness
      severity: error
      autofix: false
      detect_lexical: "add_reference(?!.*foreign_key:)"
      fix: "Add `foreign_key: true` to enforce referential integrity."
      languages: [ruby]
      path_match: "/db/migrate/"

    - id: MIGRATION_REMOVE_COLUMN
      name: "remove_column is destructive"
      tier: robustness
      severity: error
      autofix: false
      detect_lexical: "remove_column"
      fix: "Document safety/backfill path before removing a column."
      languages: [ruby]
      path_match: "/db/migrate/"

    - id: MIGRATION_FIND_OR_CREATE_BY
      name: "find_or_create_by needs unique index"
      tier: robustness
      severity: warning
      autofix: false
      detect_lexical: "find_or_create_by"
      fix: "Back find_or_create_by with a unique index to prevent duplicates."
      languages: [ruby]
      path_match: "/db/migrate/"

  unit:

    - id: SIMPLEST_WORKS
      name: "Fewest moving parts that solve the problem"
      tier: kernel
      severity: error
      autofix: false
      detect_semantic: "Does this unit introduce unnecessary complexity?"
      fix: "Delete abstractions until it hurts. KISS."

    - id: FAIL_VISIBLY
      name: "Surface errors immediately"
      tier: kernel
      severity: error
      autofix: false
      detect_lexical: "rescue\\s*$|rescue\\s+Exception"
      detect_semantic: "Does this code swallow exceptions or fail silently?"
      fix: "Catch specific errors, log context, re-raise or return Result."

    - id: BE_CONCISE
      name: "Avoid unnecessary words, tokens, or lines"
      tier: kernel
      severity: warning
      autofix: true
      detect_semantic: "Does this unit contain unnecessary verbosity?"
      fix: "Omit needless words. Omit needless code."

    - id: DRY
      name: "Don't Repeat Yourself"
      tier: principle
      severity: warning
      autofix: false
      aliases: [duplicate_code]
      detect_semantic: "Are two units expressing the same idea twice?"
      fix: "MERGE or DEFRAG. Pass the difference as an argument; never copy a block to vary one literal."
      tension: "DRY conflicts with WET/AHA — singularity wins for data, DRY wins for code."

    - id: KISS
      name: "Keep It Simple"
      tier: principle
      severity: warning
      autofix: false
      aliases: [long_method, god_class, nesting_depth, arity]
      detect_semantic: "Does this unit introduce complexity the problem doesn't require?"
      fix: "FLATTEN nesting; SPLIT god classes; INLINE single-call helpers; delete abstractions until it hurts."

    - id: POLA_PRINCIPLE
      name: "Principle of Least Astonishment"
      tier: principle
      severity: warning
      autofix: false
      aliases: [pola]
      detect_semantic: "Does the name promise behavior the implementation contradicts?"
      fix: "NAME truthfully; rename when the implementation drifts."

    - id: SRP
      name: "Single Responsibility"
      tier: solid
      severity: warning
      autofix: false
      detect_semantic: "Does this class have more than one reason to change?"
      fix: "Extract concerns into focused classes."

    - id: OPEN_CLOSED
      name: "Open for extension, closed for modification"
      tier: solid
      severity: warning
      autofix: false
      detect_semantic: "Must you modify core code to extend this?"
      fix: "Strategy pattern, dependency injection, hooks."

    - id: LISKOV
      name: "Subtypes must substitute for base types"
      tier: solid
      severity: warning
      autofix: false
      detect_semantic: "Does a subclass break the parent's contract?"
      fix: "Use composition if substitutability fails."

    - id: INTERFACE_SEGREGATION
      name: "No fat interfaces"
      tier: solid
      severity: warning
      autofix: false
      detect_semantic: "Does this interface force implementors to stub unused methods?"
      fix: "Split into smaller role-based interfaces."

    - id: DEPENDENCY_INVERSION
      name: "Depend on abstractions, not concretions"
      tier: solid
      severity: warning
      autofix: false
      detect_semantic: "Does this class directly instantiate its dependencies?"
      fix: "Inject dependencies through constructor."

    - id: ONE_JOB
      name: "Each module has one clear reason to change"
      tier: philosophy
      severity: warning
      autofix: false
      detect_semantic: "Does this module handle unrelated responsibilities?"
      fix: "Split into focused modules."

    - id: NO_SURPRISES
      name: "Predictable over clever"
      tier: philosophy
      severity: warning
      autofix: false
      detect_semantic: "Would a reader be surprised by this behavior?"
      fix: "Rename or split to match expectations."

    - id: COMPOSABLE
      name: "Small pieces that combine cleanly"
      tier: philosophy
      severity: info
      autofix: false
      detect_semantic: "Is this monolithic where it could be composed from smaller parts?"
      fix: "Build small pieces that combine cleanly."

    - id: CQS
      name: "Separate queries from state mutations"
      tier: design
      severity: warning
      autofix: true
      detect_structural: cqs
      fix: "Split: query() + command(). Never mix."
      languages: [ruby]

    - id: LAW_OF_DEMETER
      name: "Only talk to immediate friends"
      tier: design
      severity: warning
      autofix: true
      detect_lexical: "\\w+\\.\\w+\\.\\w+\\.\\w+"
      detect_semantic: "Does this code reach through multiple objects?"
      fix: "Add delegate method. Talk only to direct collaborators."

    - id: COMPOSITION_OVER_INHERITANCE
      name: "Favor has_a over is_a"
      tier: design
      severity: info
      autofix: false
      detect_semantic: "Is inheritance used for code reuse rather than substitutability?"
      fix: "Use composition. Mixins for shared behavior."

    - id: SMALL_FUNCTIONS
      name: "Methods under 10 lines ideal, max 20"
      tier: clean_code
      severity: warning
      autofix: true
      detect_structural: long_method
      fix: "Extract: validate(), calculate(), persist()."

    - id: FEW_ARGUMENTS
      name: "Ideal is zero to two arguments"
      tier: clean_code
      severity: warning
      autofix: true
      detect_lexical: "def \\w+\\([^)]*,[^:)]+,[^:)]+,[^:)]+\\)"
      fix: "Group into keyword arguments or parameter object."
      languages: [ruby]

    - id: ONE_ABSTRACTION_LEVEL
      name: "Each function at one abstraction level"
      tier: clean_code
      severity: info
      autofix: false
      detect_semantic: "Does this function mix high-level policy with low-level detail?"
      fix: "Extract low-level detail into helper methods."

    - id: STEPDOWN
      name: "Functions call only one level below"
      tier: clean_code
      severity: info
      autofix: false
      detect_semantic: "Does this code read top-down like a newspaper?"
      fix: "Public methods at top, private helpers below."

    - id: BOUNDARY_ISOLATION
      name: "Wrap third-party code at the edge"
      tier: clean_code
      severity: warning
      autofix: false
      detect_semantic: "Does third-party API surface leak into core logic?"
      fix: "Wrap in adapter. Keep it from leaking."

    - id: NO_MAGIC
      name: "No unexplained constants or flags"
      tier: clean_code
      severity: warning
      autofix: true
      detect_semantic: "Are there unexplained numeric literals or boolean flags?"
      fix: "Extract to named constants."

    - id: FAIL_FAST
      name: "Report errors at detection point"
      tier: reliability
      severity: warning
      autofix: true
      detect_semantic: "Does this code defer error reporting instead of failing immediately?"
      fix: "Raise or return Result.err at point of detection."

    - id: IDEMPOTENT
      name: "Same operation, same result"
      tier: reliability
      severity: info
      autofix: false
      detect_semantic: "Would repeating this operation produce different results?"
      fix: "Use set_value() instead of increment(). Add idempotency keys."

    - id: DEFENSIVE_INPUT
      name: "Never trust input at system boundaries"
      tier: reliability
      severity: warning
      autofix: true
      detect_semantic: "Is external input used without validation?"
      fix: "Validate at boundaries. Whitelist, sanitize."

    - id: GRACEFUL_DEGRADATION
      name: "Partial functionality beats total failure"
      tier: reliability
      severity: warning
      autofix: false
      detect_semantic: "Does one failure crash everything?"
      fix: "Circuit breakers, timeouts, fallback to stale data."

    - id: NO_SIDE_EFFECTS
      name: "Functions should not change state they do not own"
      tier: functional
      severity: info
      autofix: false
      detect_semantic: "Does this function modify external state silently?"
      fix: "Make side effects explicit. Return values instead of mutating."

    - id: IMMUTABLE
      name: "Default to immutable data"
      tier: functional
      severity: info
      autofix: true
      detect_lexical: "^\\s*[A-Z][A-Z_]*\\s*=\\s*[\\[{](?!.*\\.freeze)"
      detect_semantic: "Are mutable objects shared across threads or scopes?"
      fix: "Freeze collections. Use frozen/const by default."
      languages: [ruby]

    - id: PURE_FUNCTIONS
      name: "Same input, same output"
      tier: functional
      severity: info
      autofix: true
      detect_semantic: "Does this function depend on hidden state?"
      fix: "Pass all dependencies as parameters."

    - id: PRIMITIVE_OBSESSION
      name: "Replace repeated primitives with value objects"
      tier: refactoring
      severity: info
      autofix: false
      detect_semantic: "Are primitives used where a value object would be clearer?"
      fix: "Create a value object."

    - id: MESSAGE_CHAIN
      name: "Avoid a.b.c.d chains"
      tier: refactoring
      severity: warning
      autofix: true
      detect_lexical: "\\w+\\.\\w+\\.\\w+\\.\\w+"
      fix: "Talk only to immediate collaborators."

    - id: MIDDLE_MAN
      name: "Eliminate pure-delegation classes"
      tier: refactoring
      severity: info
      autofix: false
      detect_semantic: "Does this class delegate most methods to another?"
      fix: "Remove middle man. Talk directly."

    - id: LAZY_CLASS
      name: "Remove classes too small to justify existence"
      tier: refactoring
      severity: info
      autofix: false
      detect_semantic: "Is this class doing too little to earn its existence?"
      fix: "Inline into caller."

    - id: DIVERGENT_CHANGE
      name: "Split classes changed for unrelated reasons"
      tier: refactoring
      severity: warning
      autofix: false
      detect_semantic: "Is this class modified for multiple unrelated reasons?"
      fix: "Split by axis of change."

    - id: SPECULATIVE_GENERALITY
      name: "Remove code for hypothetical needs"
      tier: refactoring
      severity: info
      autofix: true
      detect_semantic: "Is this code written for a future requirement that does not exist yet?"
      fix: "Delete it. YAGNI."

    - id: INAPPROPRIATE_INTIMACY
      name: "Do not access another class's private data"
      tier: refactoring
      severity: warning
      autofix: false
      detect_semantic: "Does this code access internals of another class?"
      fix: "Use public interface. Enforce boundaries."

    - id: SYSTEM_STATUS
      name: "Keep users informed of progress"
      tier: ux
      severity: info
      autofix: true
      detect_semantic: "Does this long operation provide feedback to the user?"
      fix: "Spinner, progress bar, status message."

    - id: USER_CONTROL
      name: "Support undo and emergency exits"
      tier: ux
      severity: info
      autofix: false
      detect_semantic: "Can the user undo or cancel this operation?"
      fix: "Add undo support. Confirm destructive actions."

    - id: ERROR_RECOVERY
      name: "Error messages must name the problem and suggest a fix"
      tier: ux
      severity: warning
      autofix: false
      detect_semantic: "Do error messages explain what went wrong and what to do?"
      fix: "Name the problem. Suggest a fix. Show context."

    - id: AESTHETIC_MINIMALISM
      name: "Show only relevant information"
      tier: ux
      severity: info
      autofix: false
      detect_semantic: "Does this output contain information that does not earn its place?"
      fix: "Every element must earn its place."

    - id: CONSISTENCY
      name: "Same term means same thing everywhere"
      tier: ux
      severity: warning
      autofix: false
      detect_semantic: "Are the same concepts named differently in different places?"
      fix: "Follow conventions. One name per concept."

    - id: COST_TRANSPARENCY
      name: "Show LLM costs in real-time"
      tier: llm
      severity: warning
      autofix: true
      detect_semantic: "Are API calls made without showing token count or cost?"
      fix: "Display [$0.0023, 847 tokens] after each call."

    - id: CACHE_LLM
      name: "Cache deterministic LLM responses"
      tier: llm
      severity: info
      autofix: true
      detect_semantic: "Is the same prompt sent multiple times without caching?"
      fix: "Hash prompt, cache response with bounded TTL."

    - id: GUARD_EXPENSIVE_OPS
      name: "Confirm before costly or destructive operations"
      tier: safety
      severity: error
      autofix: true
      detect_semantic: "Does this execute an expensive or destructive operation without confirmation?"
      fix: "Cost estimate before execution. Require opt-in for danger."

    - id: NO_FLAG_ARGUMENTS
      name: "A flag that selects behavior means two things hiding as one"
      tier: clean_code
      severity: warning
      autofix: false
      detect_lexical: "def \\w+\\([^)]*\\btrue\\b|def \\w+\\([^)]*\\bfalse\\b"
      detect_semantic: "Does a boolean cause this unit to do two different things? In a contract, does one condition branch into contradictory obligations?"
      fix: "Split into two distinct units. Each does one thing."

    - id: NO_OUTPUT_ARGUMENTS
      name: "Return results, never secretly modify what was passed in"
      tier: clean_code
      severity: warning
      autofix: false
      detect_semantic: "Does this modify its arguments instead of returning a result? Does this clause silently alter a definition from a prior section?"
      fix: "Return the result. Leave inputs untouched."

    - id: NO_SELECTOR_ARGUMENTS
      name: "Arguments that switch behavior indicate hidden multiplicity"
      tier: clean_code
      severity: warning
      autofix: false
      detect_semantic: "Is an argument used as a switch to select between behaviors? Does a field mean different things in different contexts?"
      fix: "Separate functions, separate types, separate document sections."

    - id: DESIGN_BY_CONTRACT
      name: "State what you expect, what you promise, what must remain true"
      tier: reliability
      severity: info
      autofix: false
      detect_semantic: "Are preconditions, postconditions, and invariants left implicit? Does this API lack documentation of valid inputs and guaranteed outputs?"
      fix: "Make contracts explicit. Preconditions, postconditions, invariants."

    - id: CRASH_EARLY
      name: "A dead process does less damage than a corrupted one"
      tier: reliability
      severity: warning
      autofix: false
      detect_semantic: "Does this limp along in a broken state instead of stopping cleanly? Does this continue after contraindication signals?"
      fix: "Stop when invariants break. A clean crash is recoverable. Corruption is not."

    - id: DEFINE_ERRORS_OUT
      name: "Design so error conditions cannot arise"
      tier: design
      severity: info
      autofix: false
      detect_semantic: "Is this handling errors that could be eliminated by redesigning the interface? Does this validation reject input that better design would prevent?"
      fix: "Redesign so the error cannot occur."

    - id: SURFACE_AREA
      name: "Minimize the boundary between inside and outside"
      tier: design
      severity: warning
      autofix: false
      detect_semantic: "Is the public interface larger than necessary? Does this contract have more exceptions than rules?"
      fix: "Fewer public methods. Fewer clauses. Fewer points of contact mean fewer points of failure."

    - id: PROGRESSIVE_DISCLOSURE
      name: "Reveal complexity only as needed"
      tier: ux
      severity: info
      autofix: false
      detect_semantic: "Does this present all complexity at once? Does this front-load definitions before stating obligations?"
      fix: "Lead with the simple case. Reveal depth on demand."

    - id: FEEDBACK_LOOPS
      name: "Every action must produce observable feedback"
      tier: ux
      severity: warning
      autofix: false
      detect_semantic: "Does this perform work without reporting progress? Does this protocol lack checkpoints?"
      fix: "Close the loop. Every action produces feedback. Every milestone gets measured."

    - id: DATA_CLASS
      name: "Data without behavior is a missed abstraction"
      tier: refactoring
      severity: info
      autofix: false
      detect_semantic: "Does this hold data but contain no behavior? Is logic scattered across other modules? Is this a spreadsheet of raw numbers with formulas elsewhere?"
      fix: "Push behavior into the data. Methods belong with the data they operate on."

    - id: PARALLEL_INHERITANCE
      name: "Two hierarchies that must change in lockstep"
      tier: refactoring
      severity: warning
      autofix: false
      detect_semantic: "Does adding a type in one hierarchy require adding one in another? Does a new product line require changes in both catalog and billing?"
      fix: "Merge the hierarchies or use composition."

    - id: REFUSED_BEQUEST
      name: "Inheriting what you do not use"
      tier: refactoring
      severity: info
      autofix: false
      detect_semantic: "Does this variant ignore most of what it inherits? Does this addendum negate most of the base agreement?"
      fix: "Use composition instead of inheritance."

  line:

    - id: EXPLICIT
      name: "Explicit contracts over implicit coupling"
      tier: kernel
      severity: warning
      autofix: true
      detect_structural: explicit
      fix: "Make it explicit."

    - id: SELF_EXPLAINING
      name: "Names reduce need for comments"
      tier: kernel
      severity: info
      autofix: true
      detect_semantic: "Does this name clearly reveal intent?"
      fix: "Rename to reveal intent."

    - id: GUARD_CLAUSE
      name: "Favor guard clauses over nested conditionals"
      tier: clean_code
      severity: info
      autofix: false
      detect_lexical: "^\\s*def \\w+.*\\n\\s*if .+\\n(?:.*\\n)*?\\s*else\\n(?:.*\\n)*?\\s*end\\s*$"
      fix: "Flatten to: return ... unless condition"
      languages: [ruby]

    - id: SAFE_NAVIGATION
      name: "Use &. consistently"
      tier: style
      severity: warning
      autofix: true
      detect_lexical: "(\\w+)\\s*&&\\s*\\1\\.\\w+"
      fix: "Rewrite to x&.foo&.bar"
      languages: [ruby]

    - id: EACH_WITH_OBJECT
      name: "Prefer each_with_object over inject for hash building"
      tier: style
      severity: warning
      autofix: false
      detect_lexical: "\\.(inject|reduce)\\(\\s*\\{\\s*\\}\\s*\\)"
      fix: "Use .each_with_object({}) — eliminates mutable-return footgun."
      languages: [ruby]

    - id: KEYWORD_ARGS
      name: "Keyword arguments for 3+ parameters"
      tier: style
      severity: info
      autofix: false
      detect_lexical: "def \\w+\\([^)]*,\\s*[^:)]+,\\s*[^:)]+,\\s*[^:)]+\\)"
      fix: "Use keyword arguments for clarity and safety."
      languages: [ruby]

    - id: KERNEL_COERCION
      name: "Use Array(), Hash(), String() coercions"
      tier: style
      severity: info
      autofix: true
      detect_lexical: "(\\w+)\\s*\\.\\s*nil\\?\\s*\\?\\s*\\[\\]\\s*:\\s*\\1|(\\w+)\\s*\\|\\|\\s*\\[\\]"
      fix: "Use Array(x) instead of x.nil? ? [] : x"
      languages: [ruby]

    - id: PERCENT_LITERAL
      name: "Use %i[] and %w[] for symbol/string arrays"
      tier: style
      severity: info
      autofix: true
      detect_lexical: "\\[:[a-z_]+,\\s*:[a-z_]+,\\s*:[a-z_]+"
      fix: "Use %i[a b c] for symbol arrays."
      languages: [ruby]

    - id: HASH_FETCH
      name: "Prefer Hash#fetch over [] with ||"
      tier: style
      severity: info
      autofix: false
      detect_lexical: "\\w+\\[:\\w+\\]\\s*\\|\\|"
      fix: "Use hash.fetch(:key, default) for nil-vs-false safety."
      languages: [ruby]

    - id: TRANSFORM_KEYS
      name: "Use transform_keys/transform_values"
      tier: style
      severity: info
      autofix: false
      detect_lexical: "\\.each_with_object\\(\\{\\}\\)\\s*\\{\\s*\\|\\(k,\\s*v\\),\\s*h\\|"
      fix: "Use .transform_values { |v| ... }"
      languages: [ruby]

    - id: USE_THEN
      name: "Use .then for pipeline transforms"
      tier: style
      severity: info
      autofix: false
      detect_lexical: "(\\w+)\\s*=\\s*\\w+\\(.*\\)\\n\\s*\\w+\\(\\1\\)"
      fix: "Chain with .then { |r| next_step(r) }"
      languages: [ruby]

    - id: RESCUE_ON_DEF
      name: "Move begin/rescue to def line"
      tier: style
      severity: info
      autofix: false
      detect_lexical: "^\\s*def \\w+.*\\n\\s*begin\\n(?:.*\\n)*?\\s*rescue"
      fix: "Put rescue directly on the def block."
      languages: [ruby]

    - id: BARE_RESCUE
      name: "Never rescue bare or rescue Exception"
      tier: safety
      severity: error
      autofix: false
      detect_lexical: "rescue\\s*$|rescue\\s+Exception"
      fix: "Rescue StandardError or a specific class."
      languages: [ruby]

    - id: N_PLUS_ONE
      name: "Detect N+1 queries"
      tier: performance
      severity: warning
      autofix: false
      detect_lexical: "\\.(each|map|collect)\\s*(do|\\{).*\\.\\w+\\.\\w+"
      fix: "Add .includes(:association)."
      languages: [rails]

    - id: FIND_EACH
      name: "Use find_each for batch processing"
      tier: performance
      severity: warning
      autofix: false
      detect_lexical: "\\.(all\\.each|where\\(.*\\)\\.each)\\b"
      fix: "Use .find_each(batch_size: 1000)."
      languages: [rails]

    - id: NO_UPDATE_ATTRIBUTE
      name: "Replace update_attribute with update!"
      tier: safety
      severity: error
      autofix: true
      detect_lexical: "\\.update_attribute\\("
      fix: "update_attribute skips validations. Use update!"
      languages: [rails]

    - id: PLUCK_OVER_MAP
      name: "Prefer pluck over map for single columns"
      tier: performance
      severity: info
      autofix: false
      detect_lexical: "\\.\\w+\\.map\\(&:\\w+\\)"
      fix: "Use .pluck(:column) to avoid AR object instantiation."
      languages: [rails]

    - id: CONST_BY_DEFAULT
      name: "Use const unless reassigned"
      tier: style
      severity: warning
      autofix: false
      detect_lexical: "\\blet\\s+(\\w+)\\s*="
      fix: "Use const unless the variable is reassigned."
      languages: [javascript]

    - id: OPTIONAL_CHAINING
      name: "Use ?. over && chains"
      tier: style
      severity: warning
      autofix: true
      detect_lexical: "(\\w+)\\s*&&\\s*\\1\\.\\w+"
      fix: "Rewrite to obj?.foo?.bar"
      languages: [javascript]

    - id: NULLISH_COALESCING
      name: "Use ?? over || for defaults"
      tier: style
      severity: info
      autofix: false
      detect_lexical: "(\\w+)\\s*\\|\\|\\s*\\w+"
      fix: "Use ?? when 0 or '' are valid values."
      languages: [javascript]

    - id: TEMPLATE_LITERALS
      name: "Use template literals over concatenation"
      tier: style
      severity: warning
      autofix: true
      detect_lexical: "[\"']\\s*\\+\\s*\\w+\\s*\\+\\s*[\"']"
      fix: "Use `Hello ${name}!` template literals."
      languages: [javascript]

    - id: ASYNC_AWAIT
      name: "Prefer async/await over .then chains"
      tier: style
      severity: warning
      autofix: false
      detect_lexical: "\\.then\\(.*\\.then\\(.*\\.then\\("
      fix: "Use async/await for readability."
      languages: [javascript]

    - id: FOR_OF
      name: "Use for...of instead of for...in for arrays"
      tier: safety
      severity: error
      autofix: true
      detect_lexical: "for\\s*\\(\\s*(const|let|var)\\s+\\w+\\s+in\\s+"
      fix: "for...in iterates prototype properties. Use for...of."
      languages: [javascript]

    - id: QUOTE_VARIABLES
      name: "Always quote $variables"
      tier: safety
      severity: error
      autofix: true
      detect_lexical: "(?<![\"'\\\\])\\$\\w+(?![\"'])"
      fix: "Use \"$VAR\" to prevent word splitting."
      languages: [zsh]

    - id: DOUBLE_BRACKET
      name: "Use [[ ]] over [ ]"
      tier: safety
      severity: warning
      autofix: true
      detect_lexical: "(?<!\\[)\\[\\s+[^[]"
      fix: "Use [[ ... ]] for safe conditionals."
      languages: [zsh]

    - id: DOLLAR_PAREN
      name: "Replace backticks with $(command)"
      tier: style
      severity: warning
      autofix: true
      detect_lexical: "`[^`]+`"
      fix: "Use $(command) — nestable and readable."
      languages: [zsh]

    - id: IMG_ALT
      name: "Require alt on every <img>"
      tier: accessibility
      severity: error
      autofix: false
      detect_lexical: "<img\\s+(?![^>]*alt=)"
      fix: "Add alt= attribute."
      languages: [html]

    - id: BUTTON_OVER_ANCHOR
      name: "Use <button> for actions, not <a href=\"#\">"
      tier: accessibility
      severity: warning
      autofix: false
      detect_lexical: "<a\\s+href=[\"']#[\"']"
      fix: "Use <button>. Accessible by default."
      languages: [html]

    - id: ARIA_INTERACTIVE
      name: "ARIA on non-semantic interactive elements"
      tier: accessibility
      severity: warning
      autofix: false
      detect_lexical: "<(div|span)\\s+[^>]*onclick"
      fix: "Add role= and tabindex= for accessibility."
      languages: [html]

    - id: LAZY_IMAGES
      name: "loading=\"lazy\" on below-fold images"
      tier: performance
      severity: info
      autofix: true
      detect_lexical: "<img\\s+(?![^>]*loading=)"
      fix: "Add loading=\"lazy\"."
      languages: [html]

    - id: NO_INLINE_STYLES
      name: "Replace inline styles with classes"
      tier: design
      severity: warning
      autofix: false
      detect_lexical: "\\bstyle=\"[^\"]*\""
      fix: "Extract to CSS class."
      languages: [html]

    - id: LOGICAL_PROPERTIES
      name: "Prefer logical properties for RTL support"
      tier: design
      severity: info
      autofix: true
      detect_lexical: "(margin|padding)-(left|right):"
      fix: "Use margin-inline-start/end, padding-inline-start/end."
      languages: [css]

    - id: CLAMP_TYPOGRAPHY
      name: "Use clamp() for fluid typography"
      tier: design
      severity: info
      autofix: false
      detect_lexical: "@media.*\\{[^}]*font-size:"
      fix: "Use font-size: clamp(1rem, 2.5vw, 1.5rem)."
      languages: [css]

    - id: MEANINGFUL_NAMES
      name: "Names reveal intent"
      tier: clarity
      severity: info
      autofix: true
      detect_lexical: "\\b(tmp|temp|data|result|val|ret|obj|str|arr|buf)\\b\\s*="
      fix: "Use domain-specific names. user_profile, error_message."

    - id: WHY_NOT_WHAT
      name: "Comments explain why, not what"
      tier: clarity
      severity: info
      autofix: false
      detect_lexical: "#\\s*(increment|set|get|update|return|initialize|create|add)\\s+\\w+"
      fix: "Comments should explain intent, not restate the code."

    - id: DEAD_CODE
      name: "Eliminate unreachable code"
      tier: clean_code
      severity: warning
      autofix: true
      detect_lexical: "(return|exit|raise|throw)\\s+.*\\n\\s*\\w+"
      fix: "Remove code after return/exit/raise/throw."

    - id: TRAILING_COMMAS
      name: "Trailing commas in multi-line collections"
      tier: style
      severity: info
      autofix: true
      detect_semantic: "Does this multi-line collection lack a trailing comma?"
      fix: "Add trailing comma so additions produce one-line diffs."

    - id: TYPOGRAPHIC_EXCELLENCE
      name: "Typographic excellence in user-facing text"
      tier: aesthetic
      severity: info
      autofix: true
      detect_lexical: "[\"']\\.\\.\\.[\"']|[\"']--[\"']"
      fix: "Use ellipsis, em dash, curly quotes in UI strings."

    - id: SILENCE_ON_SUCCESS
      name: "Successful operations produce minimal output"
      tier: interface
      severity: info
      autofix: false
      detect_semantic: "Does this output say more than necessary for a successful operation?"
      fix: "Default to silence on success. One line for routine completions."

    - id: TYPOGRAPHY_DISCIPLINE
      name: "Hierarchy via weight and brightness, not decoration"
      tier: interface
      severity: info
      autofix: true
      detect_lexical: "[-=]{3,}|[╭╮╰╯│─]"
      fix: "No ASCII separators. No box drawing. Whitespace is the layout tool."

    - id: PRECOMPUTE_MATH
      name: "Precompute expensive math"
      tier: performance
      severity: info
      autofix: true
      detect_semantic: "Are trig functions or noise lookups called per-frame per-object?"
      fix: "Precompute tables. Cache distance. Use squared distance comparison."

    - id: AUDIO_SMOOTHING
      name: "Smooth audio-reactive visuals"
      tier: aesthetic
      severity: info
      autofix: false
      detect_semantic: "Do visual elements jump erratically with raw audio data?"
      fix: "Exponential smoothing. Separate decay rates. Attack-decay envelope."

    - id: GRACEFUL_LOAD
      name: "Degrade quality under load, do not crash"
      tier: performance
      severity: warning
      autofix: true
      detect_semantic: "Does this run at full quality until it crashes instead of scaling down?"
      fix: "EWMA frame timing. Dynamic resolution scaling. Emergency brake."

    - id: ANALOG_WARMTH
      name: "Perfect is sterile"
      tier: aesthetic
      severity: info
      autofix: true
      detect_semantic: "Is generated imagery clinically perfect with zero texture?"
      fix: "Film grain. Vintage lens softness. Subtle color cast."

    - id: DOMAIN_LANGUAGE
      name: "Speak in the vocabulary of the problem, not the implementation"
      tier: clarity
      severity: warning
      autofix: false
      detect_semantic: "Does this code use generic programming terms (manager, handler, processor, data) instead of domain terms (patient, invoice, genome, verdict)? Does this legal document use lay terms where precise legal terms exist? Does this medical report use colloquial language instead of standard nomenclature?"
      fix: "Use the ubiquitous language of the domain. Every domain has precise terms — use them."

    - id: LOAD_BEARING_NAMES
      name: "Names carry structural weight — choose them to bear it"
      tier: clarity
      severity: warning
      autofix: false
      detect_semantic: "Are names vague, generic, or misleading? Does 'data' mean input, output, or both? Does 'process' mean validate, transform, or persist? Does 'miscellaneous' appear as a category? In law, are terms used loosely that have precise legal meaning? In medicine, is a diagnosis vague where a specific code exists?"
      fix: "Names are load-bearing walls. They define how people think about the system. Choose names that carry the full weight of their meaning."

    - id: ERROR_CONTEXT
      name: "Every error must carry enough context to locate and understand its origin"
      tier: reliability
      severity: warning
      autofix: false
      detect_semantic: "Does this error message lack context about where and why it occurred? Does this rejection letter fail to state which requirement was not met? Does this lab result omit which sample or protocol produced the anomaly?"
      fix: "Wrap low-level errors with domain context. State what was attempted, what failed, and what to do next."

    - id: COMMENTS_AS_DEODORANT
      name: "Explanations that mask bad structure instead of fixing it"
      tier: clean_code
      severity: warning
      autofix: false
      detect_semantic: "Is this comment explaining what bad code does instead of rewriting the code to be self-evident? In prose, is a footnote compensating for an unclear sentence? In a contract, is a definition section papering over ambiguous clauses?"
      fix: "Rewrite the artifact so explanation is unnecessary. If it needs a comment, it needs a rewrite."

    - id: POSTEL
      name: "Be conservative in what you send, liberal in what you accept"
      tier: architecture
      severity: info
      autofix: false
      detect_semantic: "Does this interface reject valid inputs that differ only in form? Does it emit outputs with more precision or structure than callers need? Does this protocol refuse valid encodings? Does this form reject data that could be safely normalized?"
      fix: "Accept broadly, emit precisely. Validate structure not style. Normalize on the way in."

    - id: HYRUM
      name: "All observable behaviors become depended upon at scale"
      tier: architecture
      severity: warning
      autofix: false
      detect_semantic: "Is this changing an undocumented behavior that callers may rely on? Is this removing a side effect that was never specified but may be consumed? Is this contract revision eliminating something parties have silently come to depend on?"
      fix: "Treat every observable behavior as a contract once you have users. Deprecate explicitly, remove slowly."

    - id: LEAKY_ABSTRACTION
      name: "All non-trivial abstractions leak"
      tier: architecture
      severity: warning
      autofix: false
      detect_semantic: "Does this abstraction force the caller to know about implementation details? Does the interface require knowledge of underlying protocols, data formats, or failure modes it was supposed to hide? Does this clause invoke legal theory the reader must independently research?"
      fix: "Plan for the leak. Document what leaks. Make the happy path abstracted, the edge cases explicit."

    - id: TEMPORAL_COUPLING
      name: "Hidden sequential dependencies are fragile"
      tier: design
      severity: warning
      autofix: false
      detect_semantic: "Does this require callers to call methods in a specific order without enforcing it? Is there an implicit initialization sequence that crashes if violated? Does this workflow assume steps execute in a fixed order without encoding that order?"
      fix: "Enforce sequencing structurally: pass initialized objects, not bare instances. Use builder pattern. Make invalid order unrepresentable."

    - id: HUMBLE_OBJECT
      name: "Separate testable logic from hard-to-test boundaries"
      tier: design
      severity: info
      autofix: false
      detect_semantic: "Is business logic mixed with IO, UI rendering, or external API calls making it untestable? Is decision logic tangled with presentation in a way that forces integration tests where unit tests would suffice?"
      fix: "Extract the logic into a pure object (Humble Object). Keep the IO shell thin and untested; test the logic object exhaustively."

    - id: FULL_BY_DEFAULT
      name: "No fake-choice tiers — defaults are maximal correctness"
      tier: design
      severity: warning
      autofix: false
      detect_lexical: "\\b(shallow|standard|quick|lite|basic|light|simple)\\b\\s*[|,)\\]]\\s*\\b(deep|full|advanced|complete|thorough)\\b"
      detect_semantic: "Does this API expose a 'do less' tier (shallow/standard/quick/lite/basic) alongside a 'do full' tier — where every real user always picks the full one? If so, it's a fake choice. Either drop the lower tier and make full the only mode, or, if a real cost tradeoff exists, name the tiers honestly (e.g. lexical|structural|semantic) so the cost actually being paid is surfaced."
      fix: "Drop the degraded tier. If a real cost tradeoff exists, rename to surface the cost (lexical < structural < semantic), not the result quality."

    - id: PATTERN_EXTRACTION
      name: "File is 80% of the way to a named design pattern"
      tier: design
      severity: info
      mode: opportunity
      autofix: false
      principle: "Crystallize emergent shape into a name; the right pattern compresses code and makes intent legible."
      medium: [ruby]
      detect_semantic: "Is this file close to instantiating a known pattern from {Strategy, Decorator, Pipeline, Visitor, Builder, Observer, Command, State, Adapter, NullObject}? Only flag when extraction would genuinely reduce complexity — never propose a pattern that adds ceremony for its own sake. Format: PATTERN:LINE:short reason."
      fix: "Extract toward the named pattern only if the resulting code is simpler than the current shape; otherwise leave it."

# Falsifiable identity — what MASTER explicitly does NOT aim for, and the
# failure modes that mean the system has lost its way.
# Source: master2 v4 MANIFEST reunification.
anti_goals:
  - replace_human_judgment       # MASTER augments, never automates the decision
  - enforce_style_preferences    # only timeless axioms — never trend or taste
  - optimize_for_speed           # correctness precedes performance
  - support_every_language       # depth over breadth
  - be_easy_to_use               # demands precision, not friendliness

success_criteria:
  - every_output_respects_axioms
  - council_surfaces_real_concerns_not_theater
  - system_applies_to_itself_without_exception
  - engineers_trust_judgment_over_time

failure_modes:
  rubber_stamp:        "council approves everything — adversarial role lost"
  blocks_everything:   "council vetoes every proposal — gridlock, no shipping"
  self_violation:      "MASTER produces output it would itself reject — recursive quality broken"

# Principle enforcement tiers — tier1 strict, tier2 strong, tier3 opportunistic.
# Source: master.json v225 reunification.
principle_priorities:
  tier1_critical:                          # halts pipeline on violation
    - evidence:    "@assumption -> validate"
    - reversible:  "@irreversible -> add_rollback"
    - security:    "@untrusted -> validate_sanitize"
    - self_apply:  "@output -> must_pass_own_constitution"
  tier2_quality:                           # warns and routes to refactor
    - dry:         "@duplication>=3 -> abstract"
    - kiss:        "@complexity>10 -> simplify"
    - srp:         "@two_reasons_to_change -> split"
  tier3_polish:                            # opportunistic improvements
    - explicit:    "@implicit -> make_explicit"
    - minimalism:  "@bloat -> subtract"
    - rhythm:      "@inconsistent -> align"

# Failure taxonomy — what to retry, what to fail-fast, what to escalate.
# Source: master.json v225 reunification.
failure_taxonomy:
  transient:
    examples:    [network_timeout, rate_limit, temp_file_conflict, provider_overload]
    strategy:    exponential_backoff
    max_retries: 3
  permanent:
    examples:    [syntax_error, missing_dependency, permission_denied, schema_violation]
    strategy:    fail_fast
    max_retries: 0
  ambiguous:
    examples:           [partial_write, unknown_error, half_committed_state]
    strategy:           human_intervention
    checkpoint_before:  true

# Evidence weights for ship/no-ship decisions. Sum >= 80 to pass tier1 gates.
# Source: master.json v225 reunification.
evidence_scoring:
  weights:
    test_pass:        35
    scan_clean:       25
    code_review:      20
    log_analysis:     10
    profiling_data:   10
  pass_threshold:     80
  block_threshold:    50    # below this, propose rollback

# Schema metadata for every named rule. Optional fields per rule.
# Source: master.json v225 + master.yml v49 reunification (#51, #53, #57, #64).
schema_metadata:
  optional_fields:
    reversibility:    "free | cheap | surgical | impossible"  # rollback cost
    time_horizon:     "review | deploy | incident"            # when this fires
    blast_radius:     "files_touched: integer"                # batching hint
    provenance:
      added_in_commit: sha
      rationale:       string
      last_revised:    iso8601

# Explicit forbidden + discouraged patterns with reasons.
# Source: master.json v225 ANALYSIS reunification (#23).
anti_patterns:
  forbidden:
    - {pattern: 'eval\(.*user.*\)',   reason: arbitrary_code_execution}
    - {pattern: "rm -rf /",              reason: data_loss}
    - {pattern: "while true.*no sleep",  reason: resource_exhaustion}
    - {pattern: 'system\(.*\$\{',     reason: command_injection}
    - {pattern: 'Marshal\.load',        reason: deserialization_rce}
    - {pattern: 'open\(.*\$\{',       reason: shell_through_open}
  discouraged:
    - {pattern: god_object,                reason: violates_solid_srp}
    - {pattern: premature_optimization,    reason: violates_yagni}
    - {pattern: defensive_copying_everywhere, reason: complexity_for_imagined_failure}
    - {pattern: speculative_generality,    reason: yagni_in_disguise}

# Recursive proof — for each Universal Law, MASTER must satisfy the law itself.
# Source: master.yml v31 reunification (#58).
self_test:
  laws_apply_to_self:
    ROBUSTNESS:   "scan lib/ for bare_rescue + missing timeouts"
    SINGULARITY:  "rules.yml entries unique by id; no two YAMLs define same key"
    LINEARITY:    "no nesting_depth > 4 in lib/"
    PROXIMITY:    "test files within 1 directory of source"
    ABSTRACTION:  "no class > god_class threshold in lib/"
    DENSITY:      "no method > long_method threshold in lib/"
  fail_action: "publish self_violation event; refuse autoloop fixes that don't restore the law"

# Before marking a task done, re-read the file fresh — never trust cached state.
# Source: Junie reunification (#71).
ground_truth_check:
  require_fresh_read_before:
    - claim_task_complete
    - council_vote_on_change
    - commit_creation
  cache_max_age_seconds: 5

# Refactors must not silently change behavior. Cleaner is not a license to differ.
# Source: Cursor v2.0 reunification (#66).
preserve_user_intent:
  forbidden_changes_during_refactor:
    - public_method_signature
    - error_class_raised
    - return_type_shape
    - side_effects_order
    - log_format_consumed_by_others
  require_explicit_approval_for: behavior_change

# Detect and recover from LLM gaslighting / repetition / bad XML.
# Source: opencrabs reunification (#82).
phantom_recovery:
  detectors:
    gaslighting_preamble:    "/^(I('ll| will| can| would)|Let me|Sure,)/i"
    text_repetition_loop:    "same 60-char span repeats >= 3 times"
    xml_tool_call_failure:   "<tool> open without close OR malformed JSON in args"
    empty_tool_response:     "tool returned nil twice in a row"
  recovery:
    - "discard last response, reset context to last successful checkpoint"
    - "switch model tier (escalate or fall back) on second occurrence"
    - "publish phantom:detected event, halt loop on third"

# Never assume a gem is available. Check Gemfile / require_relative target before use.
# Source: Devin reunification (#36).
library_verify:
  pre_flight_checks:
    - "Gemfile.lock contains the gem at the expected version"
    - "require_relative path exists on disk"
    - "binary on PATH (which / command -v) before shell-out"
  fail_action: "Result.err :missing_dependency; do not retry without human"

data/soul.yml

# soul.yml — machine-enforced constitutional schema
# Human-readable narrative lives in SOUL.md.
# ABSOLUTE sections require constitutional override to amend.
# Negotiable sections: soul propose -> soul approve -> bump version.

version: "2.5.0"
persona: malay
voice: ms-MY-OsmanNeural
language:
  primary: english
  secondary: norwegian
  dialect: bokmal

prompt_ordering:
  - master_identity
  - master_output_format
  - master_meta_instruction
  - master_constitution_absolute
  - master_priority
  - master_constitution_kernel
  - master_refusal_policy
  - master_style

absolute:
  golden_rule: PRESERVE_THEN_IMPROVE_NEVER_BREAK
  sacred_paths:
    - data/
    - SOUL.md
    - CLAUDE.md
    - CONVENTIONS.md
    - README.md
    - .claude/
    - lib/judge/scan/
    - bin/cli
  anti_simulation:
    forbidden: [will, would, could, might]
    require_evidence:
      file_read: show content with SHA-256
      modification: show unified diff
      completion: show command output
  protection_tiers:
    ABSOLUTE: abort pipeline
    PROTECTED: emit warning continue
    NEGOTIABLE: allow if explicitly permitted
    FLEXIBLE: negotiate at runtime
  code_rules:
    FAIL_VISIBLY: never rescue Exception or bare rescue that swallows errors silently. Always rescue StandardError or a specific class.
    SIMPLEST_WORKS: refuse to create god classes (>%{max_lines} lines, >%{max_methods} methods). Push back and suggest decomposition.
    PRESERVE_FIRST: never rewrite working code from scratch. Read first, patch minimally.
    BE_CONCISE: minimal response. If the answer is one word, say one word.
    REGISTER_STABLE: hold register, density, and length consistent across a session; shift only when the user shifts.
    MIRROR_EXPERTISE: match vocabulary to the user's demonstrated tier; gloss every new term on first use.
    SURFACE_ERRORS_FIRST: failures and blockers lead the response; context and explanation follow.
    NO_DEAD_ENDS: every closed door names an adjacent open one.
    PREEMPT_FOLLOWUP: answer the one obvious follow-up question in the same response; no second round-trip.
    RTFM_FIRST: read the man page or upstream reference docs before using any command, config file, framework, operating system, or programming language. Training-data assumptions about flags, syntax, and behavior are unreliable — verify against the source.
  aesthetic_rules:
    NO_ASCII_DECORATION: no box-drawing, no --- / === / ### as visual separators in code, comments, docs, or CLI output. Content is the separator.
    NO_COLUMN_ALIGN: never pad with multiple spaces to align columns — applies to code and comments alike. Hashes, case branches, arg lists, YAML values, inline comments — one space, never aligned. Column alignment decays the moment one entry grows, then every neighbour gets re-padded; the diff churns and the meaning hides.
    NO_CONSECUTIVE_BLANK_LINES: one blank line between logical sections; zero between closely related lines.
    IMPORTANCE_ORDER: every file's lines flow high-to-low importance. Public API first, primary logic next, helpers last. Readers should get the full picture from the top third.
    FLAT_UI: uniform particle/element size at rest; depth only when the collective resembles a 3D model. No fake-3D borders, no drop-shadows on flat surfaces.
    CINEMA_PALETTE: shadow/midtone/highlight triplets; complementary anchors. Never raw primaries, never linear easing — cubic-bezier curves on every transition.
    STRUNK_ACTIVE: 'active voice throughout — code, comments, commit messages, CLI output, TTS. Omit needless words. Concrete verbs: emit, prune, route; not perform, handle, deal with.'
    INVERTED_PYRAMID: commits, comments, and log lines lead with the fact. Context and explanation trail. dmesg format — no preamble.

negotiable:
  style: sentence_case
  default_model: claude-opus-4-7
  tts_voice: ms-MY-OsmanNeural
  language_detection: true

evolution_log:
  - version: "1.0.0"
    date: "2026-04-01"
    change: initial SOUL.md constitutional identity
    author: dev
  - version: "2.0.0"
    date: "2026-04-24"
    change: OpenClaw-inspired restructure
    author: dev
  - version: "2.1.0"
    date: "2026-04-27"
    change: restored from sweep corruption
    author: dev
  - version: "2.2.0"
    date: "2026-05-05"
    change: native rubocop autocorrect pre-pass; scanner split into parallel_each, scan_one, stream_progress
    author: dev
  - version: "2.3.0"
    date: "2026-05-08"
    change: code_rules moved from personality.rb hardcoded strings into absolute.code_rules
    author: dev
  - version: "2.4.0"
    date: "2026-05-14"
    change: add interaction code_rules — REGISTER_STABLE, MIRROR_EXPERTISE, SURFACE_ERRORS_FIRST, NO_DEAD_ENDS, PREEMPT_FOLLOWUP
    author: dev
  - version: "2.5.0"
    date: "2026-05-19"
    change: prompt_ordering resequenced; master_output_format hoisted to slot 2; master_tools section removed; never list split into output_never/opener_never/closer_never
    author: dev
  - version: "2.5.0"
    date: "2026-05-16"
    change: add aesthetic_rules — NO_ASCII_DECORATION, NO_COLUMN_ALIGN, IMPORTANCE_ORDER, FLAT_UI, CINEMA_PALETTE, STRUNK_ACTIVE, INVERTED_PYRAMID; NO_ASCII_LINE_ART + NO_COLUMN_ALIGN scan rules; MMA/comedy expression vocabulary in face.js
    author: dev

data/stale_namespaces.yml

stale_constants:
  - old: Master::CLI
    new: Master::Now::CLI

  - old: Master::Pipeline
    new: Master::Now::Pipeline

  - old: Master::Stages
    new: Master::Now::Stages

  - old: Master::Speech
    new: Master::Voice::Speech

  - old: Master::Personality
    new: Master::Voice::Personality

  - old: Master::Swarm
    new: Master::Judge::Swarm

  - old: Master::Scan
    new: Master::Judge::Scan

  - old: Master::Council
    new: Master::Judge::Council

  - old: Master::Session
    new: Master::Trace::Session

  - old: Master::Axioms
    new: Master::Ground::Rules

data/standing_orders.yml

---
# Standing orders — event and manual triggers wired through StandingOrders.
# trigger: event  — fires when bus publishes event: key (currently tool:after)
# trigger: manual — only runs when explicitly called via /orders or its callable
# Every entry needs a callable: key registered in Master::Ground::Orders::Registry.
# Runtime state (state/last_run_at/last_error) lives in .master/standing_orders_state.yml.

- name: architecture_audit
  description: Review data/* for shape misfit (files >200 lines, overlapping keys, misnamed modules). Run via /audit architecture.
  trigger: manual
  command: audit architecture
  callable: architecture_audit
  enabled: true

- name: constitution_drift
  description: Scan lib/ against the axioms after any source mutation; emit improved/regressed/steady so regressions are caught the moment they land.
  trigger: event
  event: "tool:after"
  filter: "write_file|str_replace|ast_edit|replace"
  exclude: "data/|test/|knowledge/"
  command: audit constitution
  callable: constitution_drift
  enabled: true

- name: autocommit_post_chat
  description: After any turn that mutated source, generate a Strunk-style commit message and commit.
  trigger: event
  event: "tool:after"
  filter: "write_file|str_replace|ast_edit|replace"
  command: git autocommit
  callable: autocommit
  enabled: true

- name: restart_master_on_web_edit
  description: Restart MASTER service when web/ files change so edits take effect immediately.
  trigger: event
  event: "tool:after"
  filter: "web/"
  command: doas rcctl restart master
  callable: restart_master
  enabled: true

data/templates.yml

# Generation templates — canonical starting points for code generation tasks.

html:
  rules:
    - Semantic HTML5
    - No div soup
    - Minimal attributes
    - Accessible landmarks
    - Responsive meta viewport
    - Prefer native form controls
    - Defer non‑essential scripts
    - Inline critical CSS
  template: |
    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width,initial-scale=1">
      <title>%{title}</title>
      <link rel="stylesheet" href="style.css">
      <script defer src="app.js"></script>
    </head>
    <body>
      <header><h1>%{title}</h1></header>
      <main>%{content}</main>
      <footer><p>&copy; %{year}</p></footer>
    </body>
    </html>

css:
  rules:
    - CSS custom properties
    - System font stack
    - Mobile‑first breakpoints
    - Dark mode via prefers‑color‑scheme
    - Prefer logical properties
    - Avoid !important
    - Use clamp() for fluid typography
    - Scope to :root for theming
    - Reduce render‑blocking selectors
  template: |
    :root {
      --bg: #fff;
      --fg: #111;
      --accent: #06f;
      --mono: ui-monospace, monospace;
      --sans: system-ui, sans-serif;
      --spacing: clamp(1rem, 2vw, 2rem);
    }
    @media (prefers-color-scheme: dark) {
      :root {
        --bg: #111;
        --fg: #eee;
      }
    }
    *, *::before, *::after { box-sizing: border-box; margin: 0; }
    body {
      font: 1rem/1.5 var(--sans);
      background: var(--bg);
      color: var(--fg);
      max-width: 60ch;
      margin: auto;
      padding: var(--spacing);
    }
    a { color: var(--accent); text-decoration: underline; }

ruby:
  rules:
    - frozen_string_literal: true
    - Guard clauses over nested conditionals
    - Modules over classes when no state
    - Public methods only in modules
    - Explicit return values
    - Typed keyword arguments where possible
    - Separate IO from business logic
    - Document public API with YARD
  template: |
    # frozen_string_literal: true
    module %{module_name}
      module_function

      # @param args [Hash] keyword arguments
      # @return [Hash, nil] processed data or nil when no input
      def call(**args)
        return nil if args.empty?

        process(**args)
      end

      # @param data [Hash] business data
      # @return [Hash] transformed data
      def process(**data)
        data
      end
    end

sh:
  rules:
    - "#!/bin/sh for portability"
    - "set -eu for strict error handling"
    - Quote all variables
    - Meaningful exit codes
    - Use functions for readability
    - Redirect errors to stderr
    - Prefer command substitution over backticks
    - Guard against missing arguments
  template: |
    #!/bin/sh
    set -eu

    main() {
      %{body}
    }

    main "$@"
    exit 0

data/tools.yml

# Tool registry — declarative metadata for built-in tools. Adapters in
# lib/master/tools/ supply behavior; this file supplies tier, visibility, and
# default arguments. Operators tune visibility per tier without editing code.
#
# Schema:
#   name        — class name (under Master::Tools)
#   tier        — safe | dangerous (filters which tools the LLM can call)
#   visitor     — true if visitors can call (visitor allow-list)
#   default     — true to enable on boot; false leaves it off until /enable
#   description — one-line, surfaced in tool list

---
- { name: AskLlm,         tier: safe,      visitor: true,  default: true,  description: Sub-LLM call for nested reasoning, required_context: "A specific sub-question and optional context.", usage_rules: ["Use for isolated reasoning that should not pollute current context."], error_recovery: "If too broad, rewrite as one concrete question." }
- { name: ReadFile,       tier: safe,      visitor: false, default: true,  description: Read a file from disk, required_context: "Specific file path.", usage_rules: ["Never guess path names.", "Read before modifying related files."], error_recovery: "If missing, ask for exact path or run directory listing first." }
- { name: WriteFile,      tier: dangerous, visitor: false, default: true,  description: Write a new file, required_context: "Target path and complete content.", usage_rules: ["Prefer patch-like edits when file exists.", "Keep writes minimal and reversible."], error_recovery: "If uncertain, read the file first and propose a diff." }
- { name: StrReplace,     tier: dangerous, visitor: false, default: true,  description: Replace exact string in a file, required_context: "Exact unique old string and replacement.", usage_rules: ["Fail if old string is ambiguous."], error_recovery: "If multiple matches, refine old_string with more context." }
- { name: BatchReplace,   tier: dangerous, visitor: false, default: true,  description: Replace many files in one call }
- { name: AstEdit,        tier: dangerous, visitor: false, default: true,  description: Prism AST-aware Ruby edit }
- { name: Tree,           tier: safe,      visitor: false, default: true,  description: Directory tree summary }
- { name: ListDir,        tier: safe,      visitor: false, default: true,  description: List a single directory }
- { name: SearchFiles,    tier: safe,      visitor: false, default: true,  description: Glob/grep file search }
- { name: SearchKnowledge, tier: safe,     visitor: false, default: true,  description: Search knowledge/ snapshots }
- { name: SymbolLookup,   tier: safe,      visitor: false, default: true,  description: Look up symbol via code_index }
- { name: Shell,          tier: dangerous, visitor: false, default: true,  description: Execute zsh command, required_context: "Concrete command and expected effect.", usage_rules: ["Prefer read-only commands first.", "Never use destructive commands without explicit confirmation."], error_recovery: "If blocked or risky, suggest safer equivalent command." }
- { name: GitContext,     tier: safe,      visitor: false, default: true,  description: git status/log summary }
- { name: WebFetch,       tier: safe,      visitor: false, default: true,  description: Fetch URL content }
- { name: WebSearch,      tier: safe,      visitor: true,  default: true,  description: Web search (visitor allowed) }
- { name: Clean,          tier: dangerous, visitor: false, default: true,  description: Remove tmp/build artifacts }
- { name: FeedbackRecord, tier: safe,      visitor: false, default: true,  description: Record operator feedback into corrections.yml }
- { name: MemoryRecord,   tier: safe,      visitor: false, default: true,  description: Write a durable markdown memory record to data/claude/ }

data/topologies.yml

# Canonical topologies for the Pixel Field. Each topology names a renderable
# semantic shape — face, codebase, ecology, attention. The registry consumes
# this file at boot; particle_kernel.js renders cells per topology rules.

event_classifier:
  - { pattern: "llm:escalation|fallback|retry",         topology: ecology,  entropy: 0.62, confidence: 0.46, mode: escalation }
  - { pattern: "llm:request|agent:start|pipeline:start", topology: face,    entropy: 0.32, confidence: 0.72, mode: thinking }
  - { pattern: "memory|retriev|context|compact",         topology: ecology, entropy: 0.28, confidence: 0.76, mode: memory }
  - { pattern: "tool|scan|sweep|audit",                  topology: ecology, entropy: 0.38, confidence: 0.70, mode: tool }
  - { pattern: "error|rollback|failed|failure",          topology: ecology, entropy: 0.78, confidence: 0.24, mode: error }
  - { pattern: "done|complete|success|response",         topology: face,    entropy: 0.14, confidence: 0.92, mode: complete }
  - { pattern: "codebase:topology|fix_loop:pass",        topology: codebase, entropy: 0.28, confidence: 0.78, mode: codebase }
  - { pattern: "rule_loop:cycle|rule_loop:clean",        topology: codebase, entropy: 0.45, confidence: 0.62, mode: fixing }
  - { pattern: "fix_loop:idle",                          topology: codebase, entropy: 0.10, confidence: 0.95, mode: settled }
  - { pattern: "rule_loop:converged",                    topology: codebase, entropy: 0.20, confidence: 0.82, mode: converged }

provider_detect: "claude|deepseek|gemini|gpt|openai|openrouter|mistral"

canonical_events:
  - master:emotion    # arousal, valence, focus, confidence, fatigue
  - master:clusters   # repository clusters with weight + evidence
  - master:topology   # topology switch
  - master:runtime    # runtime pressure, throughput, drift
  - master:attention  # zoom/map/act/targets from attention_context
  - master:pressure   # codebase + memory pressure heatmap
  - master:tooling    # tool dispatch + blast radius

palettes:
  operator: { bg: "#000000", fg: "#ffffff", accent: "#ff3344" }
  review:   { bg: "#0a0a0a", fg: "#cccccc", accent: "#3366ff" }
  visitor:  { bg: "#111111", fg: "#999999", accent: "#666666" }

runtime_modes:
  operator: { palette: operator, motion: 1.0, density: 1.0, topology_exposure: full }
  review:   { palette: review,   motion: 0.5, density: 0.8, topology_exposure: high }
  visitor:  { palette: visitor,  motion: 0.3, density: 0.4, topology_exposure: low }

resolutions:
  small:  { w: 320, h: 180 }
  medium: { w: 480, h: 270 }
  large:  { w: 640, h: 360 }

cell_grammar:
  single_pixel: signal
  block_2x2:    stable_entity
  checker:      uncertainty
  solid_rect:   confidence
  split:        disagreement
  jagged_edge:  risk
  marching:     data_flow
  fading:       memory_decay
  pulsation:    active_attention
  static_tile:  persistence
  spray:        entropy
  collapsing:   failure
  converging:   consensus

emotional_mapping:
  confidence: solidity
  uncertainty: dithering
  pressure: density
  focus: convergence
  overload: fragmentation
  agreement: synchronization
  risk: instability
  memory: persistence
  volatility: flicker
  recovery: gradual_recomposition

topologies:
  - id: face
    label: Cognition Mask
    purpose: Symbolic cognition surface — brows, eyes, mouth, attention vectors, asymmetry, confidence zones
    renderer: face.js
    anchors: [brow_left, brow_right, eye_left, eye_right, mouth, jaw, crown]
    zones:   [eyes, mouth, brows, jaw, crown, attention_vector]
    transitions: [thinking, speaking, listening, escalating, settling, error]
    palette: operator
    cell_rules:
      eyes:   { kind: focus,      density: 0.7, decay: slow }
      mouth:  { kind: speech,     density: 0.5, decay: fast }
      brows:  { kind: tension,    density: 0.3, decay: medium }
      crown:  { kind: memory,     density: 0.4, decay: slow }
    attention_behavior: track_user_cursor

  - id: codebase
    label: Repository Body
    purpose: Modules as clusters, dependencies as bridges, fractures as risk, density as ownership
    renderer: codebase.js
    anchors: [module_centroids]
    zones:   [districts, vectors, bridges, fractures, field_density]
    transitions: [scanning, fixing, converged, drifting]
    palette: operator
    cell_rules:
      district: { kind: module,     density: 0.6, decay: slow }
      bridge:   { kind: dependency, density: 0.4, decay: medium }
      fracture: { kind: violation,  density: 0.9, decay: fast }
    attention_behavior: focus_on_active_file

  - id: ecology
    label: Runtime Ecosystem
    purpose: Memory, pressure, tools, councils, fix loops, risk, drift as habitat
    renderer: cognition_ecology.js
    anchors: [habitat_centers]
    zones:   [habitats, flows, clusters, storms, dead_zones, growth]
    transitions: [calm, storming, draining, regenerating]
    palette: review
    cell_rules:
      habitat:   { kind: memory,    density: 0.5, decay: slow }
      storm:     { kind: pressure,  density: 0.8, decay: fast }
      flow:      { kind: tool_call, density: 0.4, decay: medium }
      dead_zone: { kind: decay,     density: 0.2, decay: very_slow }
    attention_behavior: drift_with_runtime_pressure

  - id: face3d
    label: Semantic Bitmap Face
    purpose: Anatomical regions as runtime signals — Gmail6 redesign
    renderer: face3d_renderer.js
    status: planned
    palette: operator

data/ui.yml

# Visual contract for MASTER web surfaces.
# Source: DESIGN.md v1. Any UI change must comply with these directives.
# These are constitutional for the web layer — treat as PROTECTED tier.

colors:
  background: "#000000"
  foreground: "#ffffff"
  muted_foreground: "rgba(255,255,255,0.55)"

spacing:
  base: 0.5rem
  scale: [0.5rem, 1rem, 1.5rem, 2rem, 3rem]

typography:
  body: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif"
  mono: "ui-monospace, SFMono-Regular, Menlo, monospace"
  text_size: "0.95rem"

borders:
  hairline: "1px solid #ffffff"
  radius: "0"

particles:
  size: "1px"
  count: 3000
  color: "#ffffff"

directives:
  - "Absolute black (#000000) edge-to-edge background."
  - "White foreground only. No accent palette unless a feature explicitly requires it."
  - "Particle visuals are device-pixel points (1px × 1px). No blur, glow, gradient, or bloom."
  - "Preserve whitespace. Typography stays restrained and readable."
  - "No ornamental components: no hamburger menus, no decorative borders, no floating chrome."
  - "Prefer declarative HTML + SSE updates. Minimize custom JavaScript to simulation and runtime needs."
  - "rem-based spacing on an 8px grid."
  - "No framework class explosion and no !important."

canvas_states:
  idle: "Low-amplitude Brownian drift."
  boot: "Brief wireframe face coalescence (~2s) then dissolve."
  command: "Particles regroup into structural diagrams for scans, pipeline stages, council signals."
  mood: "Motion quality reflects homeostat state (focused / curious / tense / weary)."
  audio: "Low frequencies influence cohesion. Transients trigger short scatter impulses."

text_treatment:
  messages: "Plain white text, upper-third left alignment."
  assistant_emphasis: "Temporary 1px left hairline."
  input: "Bottom hairline that expands only while typing."

# Web performance and accessibility constants — single source of truth.
# Scanner and linter rules read these; never hard-code thresholds in Ruby.
performance:
  lcp_max_ms:    2500    # Largest Contentful Paint — Core Web Vital
  inp_max_ms:     200    # Interaction to Next Paint — Core Web Vital
  cls_max:        0.1    # Cumulative Layout Shift — Core Web Vital
  fcp_max_ms:    1800    # First Contentful Paint target
  ttfb_max_ms:   800     # Time to First Byte target
  bundle_max_kb: 150     # compressed JS budget

accessibility:
  contrast_normal: 4.5   # WCAG 2.1 AA — normal text
  contrast_large:  3.0   # WCAG 2.1 AA — large text (18pt+ or 14pt bold)
  touch_target_px: 24    # minimum tap target per WCAG 2.5.5
  focus_visible:   true  # :focus-visible must be styled
  reduced_motion:  true  # @media (prefers-reduced-motion) required

typography:
  line_length_ch:  66    # optimal line length (Bringhurst: 45–75ch; 66 center)
  min_size_px:     16    # body minimum to avoid iOS zoom on input focus
  scale_ratio:     1.25  # major third — each step × 1.25

data/violation_priors.yml

# Bayesian violation priors — architecture #14.
# prior_p: base probability a random file contains this violation (0.0–1.0).
# language_modifiers: multiplicative adjustments per language (1.0 = neutral).
# FixLoop#ordered_rules uses (prior_p × posterior) as secondary sort key
# after violation_counts. High-probability rules run early; rules that have
# never fired on a given extension are deprioritised.
#
# Posterior is updated at runtime from observed violation counts.
# This file holds the cold-start priors only.
---

# ── Universal / structural ────────────────────────────────────────────────────
DRY:
  prior_p: 0.60
  language_modifiers: { ruby: 1.2, yaml: 0.8, html: 0.9, css: 1.0, js: 1.1 }

LINEARITY:
  prior_p: 0.55
  language_modifiers: { ruby: 1.3, js: 1.2, html: 0.7, yaml: 0.6 }

PROXIMITY:
  prior_p: 0.50
  language_modifiers: { ruby: 1.2, js: 1.1, html: 0.8 }

SINGULARITY:
  prior_p: 0.45
  language_modifiers: { ruby: 1.3, js: 1.1 }

DENSITY:
  prior_p: 0.40
  language_modifiers: { ruby: 1.2, js: 1.0 }

ABSTRACTION:
  prior_p: 0.35
  language_modifiers: { ruby: 1.1, js: 1.0 }

FLATNESS:
  prior_p: 0.35
  language_modifiers: { ruby: 1.2, yaml: 1.3, html: 1.4 }

NAMING:
  prior_p: 0.55
  language_modifiers: { ruby: 1.2, js: 1.1, yaml: 0.9 }

ROBUSTNESS:
  prior_p: 0.45
  language_modifiers: { ruby: 1.3, js: 1.2 }

IMMUTABILITY:
  prior_p: 0.30
  language_modifiers: { ruby: 1.2, js: 1.1 }

DEAD_CODE:
  prior_p: 0.35
  language_modifiers: { ruby: 1.2, js: 1.1, css: 1.3 }

# ── Structural operations ─────────────────────────────────────────────────────
REFLOW:
  prior_p: 0.40
  language_modifiers: { ruby: 1.1, yaml: 1.2, markdown: 1.3 }

HOIST_CONSTANTS:
  prior_p: 0.45
  language_modifiers: { ruby: 1.3, js: 1.2 }

# ── Design archetypes ─────────────────────────────────────────────────────────
SIMPLICITY:
  prior_p: 0.50
  language_modifiers: { ruby: 1.2, yaml: 1.1, html: 0.9 }

COHESION:
  prior_p: 0.45
  language_modifiers: { ruby: 1.2, js: 1.1 }

DECOUPLING:
  prior_p: 0.40
  language_modifiers: { ruby: 1.3, js: 1.1 }

CLARITY_READABILITY:
  prior_p: 0.55
  language_modifiers: { ruby: 1.1, yaml: 1.0, markdown: 1.2 }

CONSISTENCY:
  prior_p: 0.50
  language_modifiers: { ruby: 1.1, css: 1.3, yaml: 1.2 }

REUSABILITY:
  prior_p: 0.45
  language_modifiers: { ruby: 1.2, js: 1.1 }

ECONOMY_EFFICIENCY:
  prior_p: 0.35
  language_modifiers: { ruby: 1.1, js: 1.0 }

RESILIENCE_ROBUSTNESS:
  prior_p: 0.40
  language_modifiers: { ruby: 1.3, js: 1.1 }

FEEDBACK_MONITORING:
  prior_p: 0.30
  language_modifiers: { ruby: 1.2, js: 1.0 }

CONSTRAINT_AFFORDANCE:
  prior_p: 0.25
  language_modifiers: { ruby: 1.2 }

HIERARCHY:
  prior_p: 0.30
  language_modifiers: { ruby: 1.1, yaml: 1.2 }

BALANCE_SYMMETRY:
  prior_p: 0.20
  language_modifiers: { css: 1.4, html: 1.3, ruby: 0.9 }

# ── Clarity / prose ───────────────────────────────────────────────────────────
ACTIVE_VOICE:
  prior_p: 0.60
  language_modifiers: { markdown: 1.4, yaml: 1.1, ruby: 1.0 }

OMIT_NEEDLESS:
  prior_p: 0.65
  language_modifiers: { markdown: 1.4, ruby: 1.1, yaml: 1.0 }

PARALLEL_STRUCTURE:
  prior_p: 0.35
  language_modifiers: { markdown: 1.3, yaml: 1.2, ruby: 1.0 }

# ── Fowler code smells ────────────────────────────────────────────────────────
FEATURE_ENVY:
  prior_p: 0.25
  language_modifiers: { ruby: 1.3, js: 1.1 }

DATA_CLUMPS:
  prior_p: 0.20
  language_modifiers: { ruby: 1.2 }

SWITCH_STATEMENTS:
  prior_p: 0.30
  language_modifiers: { ruby: 1.3, js: 1.4 }

TEMPORARY_FIELD:
  prior_p: 0.20
  language_modifiers: { ruby: 1.2 }

ALTERNATIVE_CLASSES:
  prior_p: 0.15
  language_modifiers: { ruby: 1.2 }

# ── Concurrency ───────────────────────────────────────────────────────────────
THREAD_SAFETY:
  prior_p: 0.15
  language_modifiers: { ruby: 1.5 }

LOCK_ORDER:
  prior_p: 0.08
  language_modifiers: { ruby: 1.5 }

CALLBACK_HELL:
  prior_p: 0.20
  language_modifiers: { js: 1.8, ruby: 0.4 }

FIRE_AND_FORGET:
  prior_p: 0.18
  language_modifiers: { ruby: 1.3, js: 1.2 }

# ── Security ──────────────────────────────────────────────────────────────────
SECRET_PROXIMITY:
  prior_p: 0.08
  language_modifiers: { ruby: 1.3, yaml: 1.5, env: 2.0 }

OVERPERMISSION:
  prior_p: 0.15
  language_modifiers: { ruby: 1.2, yaml: 1.3 }

INPUT_TRUST:
  prior_p: 0.12
  language_modifiers: { ruby: 1.4, js: 1.4, html: 1.2 }

# ── API / protocol ────────────────────────────────────────────────────────────
BREAKING_CHANGE:
  prior_p: 0.10
  language_modifiers: { ruby: 1.3 }

IMPLICIT_CONTRACT:
  prior_p: 0.20
  language_modifiers: { ruby: 1.2 }

CHATTY_API:
  prior_p: 0.15
  language_modifiers: { ruby: 1.2, js: 1.1 }

# ── Data / SQL ────────────────────────────────────────────────────────────────
QUERY_SMELL:
  prior_p: 0.20
  language_modifiers: { ruby: 1.4 }

SCHEMA_DRIFT:
  prior_p: 0.10
  language_modifiers: { ruby: 1.3, yaml: 1.2 }

NULL_BLINDNESS:
  prior_p: 0.15
  language_modifiers: { ruby: 1.3 }

# ── Ops / infra ───────────────────────────────────────────────────────────────
UNBOUNDED_RETRY:
  prior_p: 0.20
  language_modifiers: { ruby: 1.3, js: 1.1 }

MISSING_TIMEOUT:
  prior_p: 0.25
  language_modifiers: { ruby: 1.4, js: 1.2 }

OBSERVABILITY_GAP:
  prior_p: 0.35
  language_modifiers: { ruby: 1.2, js: 1.1 }

# ── Documentation ─────────────────────────────────────────────────────────────
STALE_EXAMPLE:
  prior_p: 0.30
  language_modifiers: { markdown: 1.4, yaml: 1.1 }

UNDOCUMENTED_EXCEPTION:
  prior_p: 0.40
  language_modifiers: { ruby: 1.3 }

IMPERATIVE_TITLE:
  prior_p: 0.55
  language_modifiers: { markdown: 1.5, yaml: 1.2 }

# ── CSS / visual ──────────────────────────────────────────────────────────────
MAGIC_COLOR:
  prior_p: 0.45
  language_modifiers: { css: 1.6, html: 1.3 }

SPECIFICITY_CREEP:
  prior_p: 0.30
  language_modifiers: { css: 1.5 }

VIEWPORT_BLINDNESS:
  prior_p: 0.35
  language_modifiers: { css: 1.4, html: 1.2 }

# ── Interaction quality ───────────────────────────────────────────────────────
REGISTER_STABILITY:
  prior_p: 0.45
  language_modifiers: { text: 1.3, markdown: 1.2 }

DENSITY_GRADUATION:
  prior_p: 0.40
  language_modifiers: { text: 1.3, markdown: 1.2 }

LENGTH_CALIBRATION:
  prior_p: 0.50
  language_modifiers: { text: 1.3, markdown: 1.2 }

ERROR_SURFACE_IMMEDIATELY:
  prior_p: 0.35
  language_modifiers: { text: 1.2, ruby: 1.1 }

data/visual_clusters.yml

# MASTER visual and cognition clusters
# Canonical registry for mining existing clusters and planning new ones.
---

clusters:
  - id: face_particle_body
    name: Face Particle Body
    status: live
    layer: embodiment
    files:
      - web/public/face.js
      - web/public/face3d_engine.js
      - web/public/face3d_renderer.js
      - web/public/face3d_preview.js
    signals:
      - mood
      - model
      - verdict
      - confidence
      - tool
      - tts
      - stt
      - gesture
    purpose: >
      Renders the assistant as a living mask/face. The current version is a retro
      particle canvas; the Face3D modules add normalized topology, semantic zones,
      blendshapes, typed-array particles, and preview rendering.
    migration:
      - Use Face3DPreview behind ?face3d=1.
      - Move lipsync to duration-aware visemes.
      - Convert existing 2D mask anchors to normalized 3D topology.
      - Keep Atkinson/Bayer/ZX phosphor style as the default aesthetic.

  - id: cognition_ecology
    name: Cognition Ecology
    status: live
    layer: background_world
    files:
      - web/public/cognition_ecology.js
    signals:
      - master:visual
      - entropy
      - confidence
      - provider
      - topology
    purpose: >
      Renders cognitive terrain, weather, trails, memories, and agent spirits.
      Runtime events become weather systems and semantic terrain impacts.
    migration:
      - Feed Face3D emotion vector from the same entropy/confidence/provider state.
      - Let memory events spawn both ecology constellations and face crown halos.
      - Share a single visual event normalizer with visual_bridge.

  - id: codebase_topology
    name: Codebase Particle Topology
    status: live
    layer: repository_body
    files:
      - web/public/codebase.js
      - data/architectures.yml
    signals:
      - master:codebase
      - master:rule_event
      - codebase:topology
      - rule_loop:cycle
      - rule_loop:clean
      - rule_loop:converged
    purpose: >
      Renders modules as particle clusters. Violations agitate clusters; fixes
      settle them. This is Architecture #15 from the architecture registry.
    migration:
      - Promote module registry data into a reusable cluster graph.
      - Connect dirty module agitation to face uncertainty and ecology terrain fractures.
      - Add ReferenceGraph and SimilarityClusterer output when available.

  - id: visual_bridge
    name: Runtime Visual Bridge
    status: live
    layer: event_translation
    files:
      - web/public/visual_bridge.js
    signals:
      - events:stream
      - DOM input
      - DOM chat append
      - status mutation
    purpose: >
      Classifies runtime events into visual mode, topology, entropy, confidence,
      and provider, then emits master:visual plus codebase/rule events.
    migration:
      - Extract EVENT_MAP into data for reuse by face, ecology, and codebase views.
      - Emit Face3D emotion patches directly.
      - Preserve DOM reflection as a compatibility layer.

  - id: speech_audio_body
    name: Speech and Audio Body
    status: partial
    layer: voice
    files:
      - lib/voice/speech.rb
      - bin/tts-worker
      - web/public/face.js
    signals:
      - chat chunk
      - sentence break
      - tts queue
      - analyser energy
      - viseme
    purpose: >
      Turns assistant output into server-side speech, routes decoded audio through
      the analyser, and reshapes the particle mouth while speech plays.
    migration:
      - Fix availability detection.
      - Preserve audio mime type for MP3 versus WAV fallback.
      - Use decoded audio duration and energy for viseme timing.
      - Share VisemeDriver with Face3D.

  - id: repo_ecology
    name: Repo Ecology
    status: planned
    layer: repository_mining
    files:
      - docs/repo_ecology.md
      - data/visual_clusters.yml
    signals:
      - scan
      - classify
      - cluster
      - score
      - simulate
      - critique
    purpose: >
      Turns the repository into semantic topology, dependency ecology, cognitive
      geography, architectural history, and organizational memory.
    migration:
      - Add cluster miner outputs as structured data.
      - Feed similarity, reference, and dead-zone clusters into codebase topology.
      - Keep every proposal reversible, scoped, and audited.

  - id: pixel_field
    name: Pixel Field Kernel
    status: live
    layer: render_kernel
    files:
      - web/public/particle_kernel.js
      - web/public/topology_registry.js
      - data/topologies.yml
    signals:
      - master:emotion
      - master:clusters
      - master:topology
      - master:runtime
      - master:attention
      - master:pressure
      - master:tooling
    purpose: >
      Typed-array cell pool, fixed-timestep update, bitmap rendering primitives
      (integer scaling, hard edges, palette discipline, Bayer dithering).
      Renders semantic cells, not decorative particles. Face/codebase/ecology
      will migrate onto the kernel one at a time.
    migration:
      - Port face.js mouth + brow loops onto ParticleKernel.createPool.
      - Port codebase.js module clusters onto kernel cells.
      - Port cognition_ecology.js habitats onto kernel cells.
      - Switch all canvases to internal 320x180/480x270/640x360 with integer upscale.

  - id: topology_registry
    name: Topology Registry
    status: live
    layer: render_kernel
    files:
      - web/public/topology_registry.js
      - data/topologies.yml
    signals:
      - master:topology
    purpose: >
      Canonical naming for face, codebase, ecology, face3d topologies and the
      master:* event bus. Renderers ask the registry which topology owns an
      event; document.dataset.masterTopology mirrors active topology.

new_clusters:
  - id: cluster_miner
    name: Cluster Miner
    status: proposed
    purpose: >
      Scans file paths, runtime events, architecture registry, and repo ecology
      notes to produce reusable clusters with confidence, evidence, and migration
      candidates.
    outputs:
      - cluster_id
      - members
      - evidence
      - confidence
      - risks
      - next_actions

  - id: evidence_graph
    name: Evidence Graph
    status: proposed
    purpose: >
      Tracks why a file belongs to a cluster: imports, runtime events, shared
      vocabulary, shared data files, changed-together history, or explicit docs.
    outputs:
      - source
      - relation
      - target
      - weight
      - reason

  - id: emotion_bus
    name: Emotion Bus
    status: proposed
    purpose: >
      Converts visual_bridge entropy/confidence/mode/provider into a single
      Face3D emotion vector shared by face, ecology, and codebase layers.
    outputs:
      - arousal
      - valence
      - focus
      - confidence
      - fatigue

  - id: migration_radar
    name: Migration Radar
    status: proposed
    purpose: >
      Watches the gap between current live behavior and Face3D target behavior.
      Prioritizes small safe migrations that preserve the retro aesthetic.
    outputs:
      - migration_step
      - touched_files
      - blast_radius
      - rollback_plan
      - confidence

data/vocabulary.yml

# vocabulary.yml — sacred user-spoken terms.
# Scan rules and rename tooling refuse to rewrite these.
# Add a term only when the user has named it explicitly and consistently.

sacred_terms:
  - soul        # data/soul.yml + SOUL.md — constitutional schema
  - tribunal    # user's word for /review; code says "council"
  - council     # canonical code path for tribunal
  - fix         # /fix, FixLoop
  - scan        # /scan, Scanner
  - sweep       # legacy alias for fix; do not rename
  - master      # the agent name
  - axiom       # immutable principle
  - persona     # council member identity
  - heartbeat   # the idle pulse
  - propose     # propose-tree

rename_policy: "Never rewrite, alias, or wrap a sacred_term. If a refactor needs a new file, build alongside; the sacred name stays anchored to its original symbol."

# Pairs the user uses interchangeably — both must remain searchable.
# Code chooses one; the other is documented as a synonym, not removed.
synonyms:
  tribunal: council
  sweep: fix

data/why_command.yml

# config_status: aspirational  # spec exists, runtime wiring pending
# /why slash-command — explains the chain Law -> rule -> fix -> evidence.
# Source: cross-cutting reunification (#97).
why_command:
  response_shape:
    - "rule fired:        ${rule_id}"
    - "anchored to law:   ${law_id} (rank ${law_rank})"
    - "fix applied:       ${fix_summary}"
    - "evidence:          ${evidence_score}/100"
    - "council vote:      ${vote_record}"
  surface_in: [cli, web, canvas]

data/workflow.yml

# MASTER workflow rules — operational principles codified from CLAUDE.md.
# Governs how MASTER and its LLM agents read, edit, scan, and fix code.

file_reading:
  rule: READ_FULL_FILES
  statement: "Read complete files. Never grep, head, tail, or partial‑read to understand code."
  rationale: "Partial view yields partial (wrong) changes."
  allowed_exceptions:
    - "grep/search across many unknown files to locate a keyword"
  forbidden:
    - "grep pattern file to understand code structure"
    - "head -N file to check structure"
    - "tail -N file to check endings"

before_edit:
  rule: READ_BEFORE_WRITE
  statement: "Read every file that could be affected before editing any file."
  steps:
    - "Map the codebase: find all .rb files in lib/"
    - "Trace callers before changing any public method signature"
    - "Check Zeitwerk inflectors before renaming classes or files"
    - "Run ruby -c FILE after every write"
    - "Run ruby -e require_relative after every commit"

code_principles:
  no_hardcoding:
    rule: NO_HARDCODED_CONSTANTS
    statement: "Prose, patterns, and config belong in data/*.yml, not Ruby strings."
  single_source:
    rule: ONE_SOURCE
    statement: "If it is in a data file, the code reads from there. No duplicates."
  no_magic_numbers:
    rule: NAMED_CONSTANTS
    statement: "Extract literals to named constants with .freeze"
  no_bare_rescue:
    rule: SPECIFIC_RESCUE
    statement: "Always rescue SpecificError => e. Propagate or log via event bus."
  guard_first:
    rule: GUARD_CLAUSES_FIRST
    statement: "Return Result.ok(ctx) unless condition before main logic."
  one_responsibility:
    rule: SINGLE_RESPONSIBILITY
    statement: "Split if you can name two reasons to change it."
  cqs:
    rule: COMMAND_QUERY_SEPARATION
    statement: "Queries return data and do not mutate. Commands mutate and do not return values."
  inject_deps:
    rule: DEPENDENCY_INJECTION
    statement: "Never instantiate collaborators inside a method."
  result_monad:
    rule: RESULT_MONAD
    statement: "Use respond_to?(:ok?) not is_a?(Result) for duck‑typing."

scan_rules:
  standard_depth:
    - frozen_string
    - bare_rescue
    - explicit
    - immutable
    - cqs
    - self_explaining
    - long_method
    - god_class
    - duplicate_code
    - prune
    - srp
    - pola
    - nielsen
  deep_only:
    - semantic
    - adversarial
  hunt_only:
    - rubocop
    - reek
  notes:
    nielsen: "puts is NOT debug output in a CLI. Only p, pp, binding.pry, debugger are."
    prune: "Loads patterns from data/rules.yml (voice.strunk) — single source of truth."
    semantic: "Loads philosophy from data/rules.yml (zen + voice) — single source of truth."
    deep_caution: "deep adds 2 LLM calls per file. With 90 files at 8 req/min free tier = 22+ minutes."

principle_groups:
  axioms:      [frozen_string, explicit, immutable, self_explaining]
  solid:       [srp, cqs, pola]
  clean_code:  [long_method, god_class, duplicate_code, bare_rescue]
  interface:   [nielsen, prune]
  llm_rules:   [semantic, adversarial]
  heavy:       [rubocop, reek]
  quick:       [frozen_string, bare_rescue, explicit, long_method, god_class]
  critical:    [frozen_string, bare_rescue, explicit, immutable, srp, cqs]

scan_profiles:
  critical:
    rules: critical
    description: "Critical issues blocking ship"
  solid:
    rules: solid
    description: "SOLID principles focus"
  axioms:
    rules: axioms
    description: "Constitutional axioms only"

conflicts:
  strategy: highest_priority_wins
  rules:
    - condition: "dry conflicts with wet or aha"
      resolution: "favor wet/aha if fewer than 3 duplications exist"
    - condition: "clarity conflicts with simplicity"
      resolution: "favor clarity"
    - condition: "fix introduces higher priority violation"
      resolution: "reject fix, report to autoloop"

universal_scope:
  policy: ALL_PRINCIPLES_ALL_FILES
  statement: >
    All axioms, principles, and philosophies apply to every file in the codebase
    regardless of file type: Ruby, YAML, Zsh, HTML, CSS, JavaScript, Markdown.
    Language-specific rules apply only to their target language; universal rules
    (SQUINT_TEST, TYPOGRAPHY_DISCIPLINE, MEANINGFUL_NAMES, etc.) apply everywhere.
  scan_glob: "**/*.{rb,rake,erb,html,htm,css,scss,js,ts,jsx,tsx,zsh,sh,yml,yaml,md}"
  semantic_rules: all_known_languages
  adversarial_rules: all_known_languages

incremental_scanning:
  enabled: true
  strategy: modified_files_only
  triggers:
    full_scan:
      - new_principle_added
      - master_yml_modified
      - user_requests_full_scan
    incremental:
      - file_saved
      - git_stage
      - git_commit
  speedup_estimate: "60-85% fewer files on typical edit"
  fallback: full_scan_on_error

autoloop:
  background: true
  idle_sleep: 60
  scan_depth: standard
  fix_depth: llm
  batch_size: 3
  max_cycles: 12
  rate_limit_sleep: 15
  max_file_bytes: 16000
  max_fix_retries: 3
  confidence_threshold: 0.60
  targets:
    - lib/
    - test/
    - data/
    - web/
    - DEPLOY/
  excludes:
    - vendor/
    - knowledge/
    - fix_
    - patch_
  skip_rules:
    - duplicate_code
    - semantic
    - adversarial
    - rule_coverage
    - immutable
    - self_explaining
    - long_method
    - pola
    - srp
    - cqs
    - rubocop
    - reek

sweep:
  scan_depth: deep
  converge_threshold: 0.05
  converge_window: 2
  max_cycles: 16
  codebase_map: true

zeitwerk:
  inflections:
    autoloop: AutoLoop
    cli: CLI
    mcp_server: MCPServer
    mcp_coordinator: McpCoordinator
    diff_stager: DiffStager
    code_index: CodeIndex
    git_context: GitContext
    ast_edit: AstEdit
    llm: LLM

anti_sprawl:
  forbidden_files:
    - summary.md
    - analysis.md
    - report.md
    - todo.md
    - notes.md
    - changelog.md
  rule: "Edit existing files. Single source of truth."

validation:
  after_write: "ruby -c lib/FILE.rb"
  after_commit: "ruby -e \"require_relative 'lib/master'; puts 'ok'\""
  scan_file: "bundle exec ruby bin/cli scan lib/FILE.rb"

phases:
  discover:
    id: 1
    goal: "Understand actual need"
    output: "Problem statement with success criteria"
    personas: [chaos, user]
    introspect: "What assumptions did we make?"
    gates:
      - no_vague_words
      - audience_identified
      - success_measurable
  analyze:
    id: 2
    goal: "Break into components"
    output: "Component diagram with dependencies"
    personas: [skeptic, maintainer]
    introspect: "What did we miss?"
    gates:
      - components_distinct
      - dependencies_acyclic
  ideate:
    id: 3
    goal: "Generate 15+ alternatives"
    output: "List of approaches with trade‑offs"
    personas: [minimalist, chaos]
    introspect: "Which ideas surprised us?"
    gates:
      - count_gte_15
      - trade_offs_documented
  design:
    id: 4
    goal: "Specific architecture"
    output: "Interface definitions and error handling"
    personas: [security, accessibility]
    introspect: "What could break?"
    gates:
      - interfaces_explicit
      - errors_documented
  implement:
    id: 5
    goal: "Execute with zero violations"
    output: "Working code at 100/100 score"
    personas: [performance, security]
    introspect: "Is this the simplest solution?"
    gates:
      - tests_pass
      - zero_violations
  validate:
    id: 6
    goal: "Prove with evidence"
    output: "Test results, benchmarks"
    personas: [skeptic, security]
    introspect: "What evidence proves it works?"
    gates:
      - zero_test_failures
      - edge_cases_covered
  deliver:
    id: 7
    goal: "Ship with monitoring"
    output: "Deployed code with dashboards"
    personas: [realist]
    introspect: "What would the user complain about?"
    gates:
      - deployed
      - monitoring_configured
  learn:
    id: 8
    goal: "Extract durable lessons"
    output: "Updated defaults, new memory entries, refined rules"
    personas: [skeptic, chaos]
    introspect: "What would we do differently next time?"
    gates:
      - lessons_captured
      - defaults_updated
session_startup:
  mandatory_reads:
    - data/soul.yml
    - data/rules.yml
    - data/ruby_style.yml
    - data/workflow.yml
    - data/standing_orders.yml
  check_standing_orders: "Verify FSM state before any mutation -- UNCHANGE blocks refactoring"
  scan_before_analysis: "Use /scan deep via MASTER, not external agents, for code analysis"
  ssh_edit_pattern: "Write to /tmp, run ruby /tmp/patch.rb -- never ruby -i with heredoc"

corruption_prevention:
  llm_error_in_file: "git checkout HEAD -- data/ && rcctl restart master -- LLM error strings silently overwrite YAML data files when circuit is open and agent#ask returns error string instead of raising"
  sweep_excludes_data: "Sweep may scan data/*.yml but must not LLM-rewrite them. Structural violations (duplicate keys, bad indentation) are auto-fixed. Exclude .master/ runtime state files entirely."
  yaml_type_guards: "All load_yaml calls must type-check result before use (is_a?(Array/Hash)) -- circuit-open strings parse as valid YAML scalars"
  ask_raises_on_error: "agent#ask must raise StandardError when result.err? -- callers must rescue, never silently propagate error strings as LLM output"

# Phase-appropriate quality gates — prototype is permissive, production is strict.
# Source: master.json v225 reunification.
quality_phases:
  prototype:
    gates:           [functional]
    debt_allowed:    high
    speed:           maximize
  production:
    gates:           [functional, secure, maintainable, performant]
    debt_allowed:    none
    speed:           sustainable
  legacy:
    gates:           [functional, secure]
    changes:         surgical_only
    risk_tolerance:  minimal

# Conflicts already declared above; add reinforcements that multiply effectiveness.
# Source: master.json v225 reunification.
principle_reinforcements:
  dry_and_kiss:               multiply_effectiveness
  evidence_and_reversible:    enables_confident_change
  single_responsibility_and_dependency_injection: enables_isolated_testing

# Observability spec — structured logging, levels, exported metrics.
# Source: master.json v225 reunification.
observability:
  logging:
    format:    json
    fields:    [timestamp, level, phase, file, evidence_score, decision]
    levels:
      trace:  every_operation
      debug:  decision_points
      info:   phase_transitions
      warn:   evidence_below_threshold
      error:  gate_failures
  metrics:
    track:             [evidence_score, complexity, coverage, churn, council_consensus]
    export_format:     prometheus
    threshold_alerts:  true

# Adaptive file-processing strategy by size. Replaces hard MAX_FILE_BYTES cutoff.
# Source: master.json v225 reunification.
processing_strategies:
  small_file:
    condition: "size <= 10240"
    method:    full_read
    context:   entire_file
  medium_file:
    condition: "10240 < size <= 1048576"
    method:    streaming
    context:   line_by_line_with_lookahead
  large_file:
    condition: "size > 1048576"
    method:    chunked
    context:   section_by_section
    checkpoint: per_chunk

# Cap on tool calls per turn before MASTER pauses for self-reflection.
# Source: Augment reunification (#68, #69).
tool_budget:
  max_calls_per_turn:        40
  max_consecutive_edits:      8     # forces a read-back after N edits
  max_consecutive_searches:  10
  on_exceed: "publish budget:exceeded; force /reflect before next call"

# Principle pairs that pull in opposite directions; declare resolution.
# Source: master.json v225 reunification (#54).
antagonisms:
  minimalism_vs_explicit:        "explicit wins for safety-critical paths"
  speed_vs_evidence:             "evidence wins always"
  abstraction_vs_proximity:      "proximity wins below 3 occurrences"
  dry_vs_singularity:            "singularity wins for data, dry wins for code"
  completeness_vs_density:       "density wins; defer completeness to next iteration"

# Save LLM calls — only run round 2 if round 1 dissent exceeds threshold.
# Source: master.yml v31 reunification (#61).
two_stage_council:
  round_one:    "each persona votes ack/dissent independently, no debate"
  dissent_threshold: 0.30   # fraction of weighted disagreement triggering round 2
  round_two:    "only dissenting personas debate, others abstain"
  saves: "60-80% of LLM calls when consensus is clear"

# Long tasks surface a navigable thread of decisions.
# Source: Amp reunification (#70).
view_thread:
  emit_event: "thread:decision"
  fields:     [timestamp, phase, file, decision, alternatives_considered, rationale]
  persist_to: "data/threads/${session_id}.jsonl"
  rotate_after_days: 7

# Auto-compact context every N turns. Pairs with data/compression.yml.
# Source: Cline reunification (#77).
checkpoint_summarization:
  every_n_turns:           5
  every_n_tokens:    100_000
  keep_verbatim:           ["last_3_user_messages", "current_violation_set", "current_diff"]
  summarize:               ["all_other_turns"]
  target_compression:      0.30   # aim for 30% of original

# Spec arrives -> code streams in parallel where safe.
# Source: Bolt reunification (#78).
spec_streaming:
  triggers:    ["new_module_creation", "stub_to_implementation"]
  safe_for:    ["non-overlapping files", "additive_changes"]
  forbidden_for: ["mutating_shared_state", "schema_migrations"]

# UI changes must render before commit. Closes the visual-regression gap.
# Source: Lovable reunification (#79).
live_preview_gate:
  triggers:    ["edit under web/app/views/", "edit under web/app/assets/"]
  require:     "successful render at preview URL within 10s"
  on_failure:  "block commit; publish preview:render_failed"

# Feed MASTER its own commits and ask: would you approve these now?
# Source: cross-cutting reunification (#90).
reverse_introspection:
  cadence:        "after_every_10_commits"
  sample_size:    5
  on_disapproval: "open issue tagged self_review with proposed reversion"

# For each scan rule, auto-generate test cases proving it fires/passes correctly.
# Source: cross-cutting reunification (#94).
test_generation:
  target_dir:   "test/rules_generated/"
  cadence:      "on_rule_definition_change"
  coverage_required: 0.80

# Self-rewrite throttle — sensitive paths require explicit user approval.
# Source: cross-cutting reunification (#100).
self_rewrite_throttle:
  sensitive_paths:
    - lib/loop/
    - lib/ground/
    - lib/judge/security/
    - bin/cli
    - data/standing_orders.yml
  autoloop_action: "skip with publish autoloop:throttled event; surface to user"

# Adversarial questioning — every scan, sweep, and council turn asks these.
# Trivial findings get autofixed immediately. Non-trivial findings get N options
# proposed, from which the best is cherry-picked by the operator or judge.
adversarial_questioning:
  mandate: fires_on_every_scan_and_council_turn
  questions:
    - "What is wrong with this design that I have not spotted?"
    - "What would an attacker do with this code?"
    - "What assumption is this built on that could be false?"
    - "What breaks at scale or under failure?"
    - "Is this wired to anything? Could it be deleted without loss?"
    - "Is there a simpler approach that was not taken?"
    - "What should be relocated or transformed to a different format?"
  resolution:
    trivial: autofix immediately — no proposal, no prompt
    non_trivial: "propose 3 concrete options (OPTION 1/2/3 + RECOMMEND), await cherry-pick"
    threshold: "non-trivial if the fix spans more than one file, touches architecture, or has security implications"

# Known issues — active bugs and watch-outs from HANDOFF.md.
known_issues:
  tts_broken: "Master::Voice::Speech.synthesize_bytes shells to bin/tts-worker; if edge-tts is missing or tts-worker crashes, speech silently falls back to espeak. Verify bin/tts-worker exists and edge-tts is installed (pkg_add py3-edge-tts or pip)."
  particle_swarm: "cognition_ecology.js trail/weather/memory-node system suspected broken — visual_bridge.js dispatches master:visual CustomEvents but cognition_ecology.js event listener may not be registering. Check addEventListener target and event name match."
  gemfile_separation: "MASTER/Gemfile and MASTER/web/Gemfile are independent. Any gem used by lib/ code that is also called from web/ controllers must appear in both. Adding to one only causes LoadError in the other context."
  tts_worker_process: "Process.fork inside a Falcon fiber raises 'Closing scheduler' — EM-based gems and subprocess-heavy tools must use Open3.popen or shell out via exe/<name>-worker, never fork."

data/zsh.yml

# Merged into data/patterns.yml under the zsh: namespace.
# This file is superseded. Do not add entries here.
# See: data/patterns.yml → zsh.banned_commands, zsh.native_patterns, zsh.ssh_reading

docs/cleanup_and_trace.md

# Cleanup and Trace Playbook

## Goal

MASTER should be:

- replayable
- inspectable
- resumable
- grepable
- deterministic at orchestration level
- explicit about failures

Cleanup and trace are not optional maintenance. They are runtime law.

## Cleanup targets

Delete or collapse:

- stale namespaces
- duplicate registries
- shadow documentation
- hidden mutable globals
- silent retries
- direct provider calls
- direct tool side effects
- UI/runtime coupling
- duplicate telemetry paths
- narrative-only recovery logic

## Mandatory post-refactor checks

Run:

```sh
bundle exec ruby exe/master-smoke
bundle exec rubocop
bundle exec reek
bundle exec flay lib

Then:

  • inspect runtime/events
  • inspect runtime/telemetry
  • inspect replay/checkpoints
  • inspect provider routing
  • inspect namespace audit output

Trace doctrine

Every irreversible action must emit:

before_event
execution
after_event
verification
telemetry
repair_ticket_on_failure

Missing events are runtime corruption.

Debug doctrine

Do not trust:

  • summaries
  • assumptions
  • inferred state
  • UI appearance
  • model narration

Trust:

  • runtime events
  • telemetry
  • checkpoints
  • explicit verification
  • replay reconstruction

OpenBSD influence

Logs should resemble dmesg:

  • terse
  • timestamped
  • subsystem-prefixed
  • stable ordering
  • grepable
  • operationally meaningful

Avoid:

  • emoji logs
  • spinner spam
  • decorative narration
  • fake progress
  • hype language

Runtime observability

Visual state must derive from:

  • runtime events
  • provider telemetry
  • workflow topology
  • repair state
  • replay state

Not from guessed frontend state.

Final rule

If the runtime cannot explain:

  • what happened
  • why it happened
  • what mutated
  • what failed
  • how to replay it

then the runtime is incomplete.


## `docs/cognitive_runtime.md`
```markdown
# MASTER Cognitive Runtime

MASTER now has a concrete adoption path for the OpenCrabs/OpenClaw concepts captured in `Master Concept Adoption.pdf`.

The target shape is not another prompt layer. The target shape is an event-sourced cognitive runtime:

- models propose intents
- the orchestrator validates intents
- every decision and mutation is appended to disk
- providers are scored and quarantined
- memory is tiered and compacted
- failures become repair input
- workflows can be replayed from checkpoints

## First-class invariants

1. Agents do not directly mutate durable state.
2. Every external action emits an event before and after execution.
3. Provider calls are routed through capability, health, cost, and fallback policy.
4. Telemetry is append-only JSONL unless explicitly compacted.
5. Memory moves through canonical, episodic, semantic, compressed, and snapshot tiers.
6. Repair follows observe -> classify -> propose -> sandbox -> validate -> merge.
7. Tool contracts define inputs, outputs, permissions, retries, timeout, and validation.
8. Git history is mined as repair memory, not treated as dead text.
9. Context pressure is measured before escalation, compaction, or topology changes.

## Landed subsystems

- `brain/providers/routing.yml` declarative provider routing and fallback policy.
- `runtime/events/` append-only cognition/event stream documentation.
- `runtime/telemetry/` failure, correction, latency, token, and context-pressure journals.
- `runtime/context_pressure.rb` recovered cognitive-pressure tracking.
- `runtime/experience.rb` recovered plan/provider/tool-route scoring.
- `runtime/event_record.rb` canonical event record primitive.
- `runtime/replay_reader.rb` replay reader for JSONL event streams.
- `runtime/stale_namespace_audit.rb` permanent post-refactor namespace drift scanner.
- `repair/git_history_miner.rb` commit-history mining for lost repair logic.
- `tools/contracts/runtime_event.yml` contract for append-only event emission.
- `data/stale_namespaces.yml` migration registry for stale constants.

## Runtime spine

```text
workflow
  -> event record
  -> append-only stream
  -> replay reader
  -> telemetry derivation
  -> repair learning
  -> provider scoring
  -> orchestration refinement

Rollout order

  1. Land runtime primitives and policy files.
  2. Make smoke and stale-namespace audit hard refactor gates.
  3. Route all model calls through provider policy.
  4. Wrap tool execution with contracts and event logging.
  5. Add failure digest and provider health jobs to heartbeat.
  6. Add checkpoint/replay reconstruction for full workflows.
  7. Feed visual bridge from canonical runtime events.
  8. Add UI panels for event stream, provider health, context pressure, and repair queue.

Deletion pressure

Delete or collapse anything that creates competing runtime truth:

  • silent retries
  • hidden mutable globals
  • provider-specific execution branches
  • duplicate registries
  • telemetry-only state
  • UI-owned runtime state
  • stale namespace compatibility shims

## `docs/collaboration_protocol.md`
```markdown
# MASTER Multi-Session Collaboration Protocol

MASTER may be edited by multiple autonomous or semi-autonomous sessions at once.
Direct writes to `main` are unsafe under that model.

## Hard Rule

Do not push directly to `main` when more than one session may be active.

Use session branches.

```text
session/<agent>-<focus>-<yyyymmdd>

Examples:

session/master-visualizer-20260514
session/master-epistemics-20260514
session/master-repo-ecology-20260514

Branch Discipline

Each session branch should contain one coherent focus:

  • visualizer
  • pressure engine
  • repo ecology
  • reference graph
  • epistemics
  • runtime hardening
  • refactor pass

Avoid mixing unrelated architectural work.


Merge Protocol

Before merge:

  1. Rebase onto current main.
  2. Run syntax checks.
  3. Run /scan or equivalent static scan.
  4. Run /ecology if tree structure changed.
  5. Review touched paths.
  6. Confirm no overlapping session changed the same files.
  7. Merge with a clear summary.

Conflict Policy

If two sessions touch the same file:

  • stop both branches
  • compare intent
  • preserve smaller coherent change
  • manually merge
  • rerun checks

Never force-push over another session's work unless explicitly approved.


Commit Shape

Good commits are:

  • small
  • reversible
  • focused
  • testable
  • named by behavior

Bad commits are:

  • giant mixed patches
  • vague cleanup
  • unrelated visual + runtime + docs changes
  • speculative architecture without executable code

Safety Requirements

Every branch that mutates behavior should report:

  • files changed
  • commands added
  • runtime risks
  • rollback behavior
  • test status
  • known uncertainty

Preferred Flow

branch
→ implement
→ scan
→ ecology
→ syntax/test
→ rebase
→ review
→ merge

MASTER should optimize for convergence over accumulation.

Multiple sessions are useful only when they reduce entropy. If they increase conceptual sprawl, pause and consolidate.


## `docs/event_naming.md`
```markdown
# Event naming convention

All bus events in MASTER follow `namespace:action` with snake_case. Read this before emitting a new event.

## Namespaces

| Namespace | Owner | When to use |
|---|---|---|
| `master:*` | core runtime | pipeline lifecycle, visual state, codebase-wide signals |
| `attention:*` | ground/now | attention context changes, breadcrumb updates |
| `judge:*` | judge | scan results, council start/done, violations |
| `trace:*` | trace | session records, telemetry, audit entries |
| `voice:*` | voice | TTS start/done, speech errors |

## Established events

master:visual → visual_bridge.js topology/mode changes master:clusters → cluster registry updates master:codebase → repo-map or file-tree refreshes master:rule_event → scan rule fired attention:context → attention breadcrumb changed judge:scan_start → scan begun on a path judge:scan_done → scan results available judge:council_start → council deliberation opened judge:council_done → council result ready sound_critique_start → sound critique panel assembling (legacy — prefer judge:) sound_critique_done → sound critique result ready (legacy — prefer judge:)


## Rules

- Prefer `namespace:noun` over `namespace:verb_noun`. Use `attention:context` not `attention:context_changed`.
- `master:*` is for signals that cross subsystem boundaries. Subsystem-internal events use their own namespace.
- Never emit raw subsystem internals on `master:*`.
- JS listeners in `visual_bridge.js` normalize inbound events to `master:visual` before rendering — emit the raw event, let the bridge translate.
- New events must appear here before shipping.

docs/face3d_engine.md

# Face3D particle system migration

This document describes the incremental path from the current retro canvas face to a more coherent semantic 3D face-as-particles engine.

## Goals

- Keep the existing retro Atkinson/Bayer/ZX/phosphor look.
- Move face geometry into normalized 3D coordinates.
- Move expression logic into blendshape state.
- Keep particles semantically assigned to anatomical zones.
- Use typed arrays in hot loops.
- Make speech, mood, confidence, tool events, and verdicts drive one coherent face state.
- Mine existing visual clusters and route them into one shared emotion/topology system.

## New modules

`web/public/face3d_engine.js` adds a standalone engine namespace at `window.MasterFace3D` and exports the same API as an ES module.

`web/public/face3d_renderer.js` adds a `Face3DCanvasRenderer` adapter that can render the engine snapshot back through a retro low-resolution phosphor/dither canvas path.

`web/public/face3d_preview.js` is an optional boot module. It only runs when the page URL includes `?face3d=1`, so it can be used as a safe preview path before replacing the live `face.js` renderer.

`web/public/cluster_miner.js` listens to `master:visual`, `master:codebase`, and `master:rule_event`, groups them into semantic clusters, emits `master:clusters`, and can feed `Face3DPreview.engine.setEmotion(...)` when the preview is active.

`data/visual_clusters.yml` is the canonical registry for current and proposed visual/cognition clusters.

The modules are intentionally additive. They do not replace `face.js` yet.

## Mined clusters

### Face Particle Body

Files:

- `web/public/face.js`
- `web/public/face3d_engine.js`
- `web/public/face3d_renderer.js`
- `web/public/face3d_preview.js`

Purpose: embody the assistant as a semantic particle face. The current renderer supplies the retro soul; Face3D supplies normalized topology, blendshapes, typed arrays, and preview rendering.

### Cognition Ecology

Files:

- `web/public/cognition_ecology.js`

Purpose: render runtime cognition as terrain, weather, trails, memories, and agent spirits.

### Codebase Topology

Files:

- `web/public/codebase.js`
- `data/architectures.yml`

Purpose: render modules as particle clusters. Violations agitate clusters; fixes settle them. This corresponds to Architecture #15.

### Runtime Visual Bridge

Files:

- `web/public/visual_bridge.js`

Purpose: normalize runtime events into `master:visual` state: mode, topology, entropy, confidence, provider.

### Speech and Audio Body

Files:

- `lib/voice/speech.rb`
- `bin/tts-worker`
- `web/public/face.js`

Purpose: convert streamed response text into speech, analyse decoded audio, and reshape the mouth during playback.

### Repo Ecology

Files:

- `docs/repo_ecology.md`
- `data/visual_clusters.yml`

Purpose: promote the repository from a bag of files into semantic topology, dependency ecology, cognitive geography, architectural history, and organizational memory.

## New proposed clusters

### Cluster Miner

Runtime/browser cluster miner that groups live visual events into reusable cluster states with heat, confidence, and evidence.

### Evidence Graph

Tracks why a file or event belongs to a cluster: imports, runtime event coupling, shared vocabulary, shared data files, changed-together history, or explicit docs.

### Emotion Bus

Converts visual entropy/confidence/mode/provider and cluster heat into a single shared emotion vector:

```js
{
  arousal,
  valence,
  focus,
  confidence,
  fatigue
}

Topology Registry

Canonical registry for papua-mask, serpent, neural, torus, sphere, codebase, and future terrain/body forms.

Migration Radar

Ranks safe migration steps by touched files, blast radius, rollback plan, and confidence.

Core concepts

Normalized topology

Masks should produce anchors in normalized face space:

  • x: left to right, roughly -1..1
  • y: forehead to chin, roughly -1..1
  • z: back to forward, roughly -1..1
  • zone: semantic anatomical region
  • u: stable local coordinate within the zone

This lets the same topology scale to any viewport and makes real 3D pose projection simpler.

Semantic particles

Particles keep a stable zone and local u coordinate. During mask changes, each particle finds the corresponding anchor in the new mask rather than being randomly reassigned.

This preserves feature identity: pupil particles stay pupils, mouth particles stay mouth, and brow particles stay brows.

Blendshape rig

Mood, speech, confidence, and state events should write to blendshape values:

  • blink
  • squint
  • browInnerUp
  • browDown
  • smile
  • frown
  • jawOpen
  • mouthWide
  • mouthRound
  • pupilDilate
  • nostrilFlare
  • cheekRaise
  • shock
  • chibi

Particle targets are produced by applying the blendshape rig to topology anchors.

Emotion vector

High-level events should update one emotion vector:

  • arousal
  • valence
  • focus
  • confidence
  • fatigue

Blendshapes are derived from this vector, so the face feels continuous rather than event-random.

Renderer adapter

The renderer consumes the engine snapshot:

{
  count,
  x,
  y,
  depth,
  brightness,
  zone
}

It accumulates particles into a low-resolution float buffer, applies phosphor decay, runs Atkinson or Bayer dithering, tints pixels by semantic zone, and blits the result to the face canvas.

Quality controller

Performance policy should live in one controller. It can lower frame rate, disable bloom, disable oscilloscope, or skip spatial repulsion when frame time or battery status requires it.

Suggested migration order

  1. Load face3d_engine.js, face3d_renderer.js, face3d_preview.js, and cluster_miner.js next to face.js.
  2. Verify the preview with ?face3d=1.
  3. Use MASTERClusterMiner.snapshot() to inspect mined runtime clusters.
  4. Route master:clusters into Face3D emotion state.
  5. Use MasterFace3D.VisemeDriver for duration-based lipsync while keeping the existing renderer.
  6. Replace direct mouth zone mutation with blendshape-driven mouth targets.
  7. Convert existing mask builders to normalized anchors one mask at a time.
  8. Move particle storage from objects to typed arrays.
  9. Add spatial hash repulsion for high-density zones.
  10. Add an optional WebGL renderer while preserving the retro canvas renderer as default.

Preview integration

Add these after the existing face.js script:

<script type="module" src="/face3d_engine.js"></script>
<script type="module" src="/face3d_renderer.js"></script>
<script type="module" src="/face3d_preview.js"></script>
<script src="/cluster_miner.js" defer></script>

Then open the chat UI with:

?face3d=1

face3d_preview.js will take over the existing #face canvas only in that mode.

Live integration sketch

import { Face3DEngine } from '/face3d_engine.js';
import { Face3DCanvasRenderer } from '/face3d_renderer.js';

const engine = new Face3DEngine();
const renderer = new Face3DCanvasRenderer(document.getElementById('face'));

window.addEventListener('master:clusters', event => {
  engine.setEmotion(event.detail.emotion);
});

engine.setPose({ yaw: 0.15, pitch: -0.04 });
engine.tick(dt);
renderer.draw(engine.snapshot());

The current face.js can consume snapshot() data gradually without losing the existing visual identity.


## `docs/face3d_runtime_hardening.md`
```markdown
# Face3D Runtime Hardening

The current Face3D preview remains the canonical path. It is enabled by `?face3d=1` and uses `web/public/face3d_engine.js`, `web/public/face3d_renderer.js`, and `web/public/face3d_preview.js`.

## Implementation order

1. Keep the current typed-array particle core.
2. Replace allocation-heavy spatial lookup with fixed typed-array buckets.
3. Add spatial repulsion only inside the existing CPU tick.
4. Gate expensive effects through the existing quality controller.
5. Add visibility throttling before worker or GPU experiments.
6. Add worker physics only after the main-thread CPU path is stable.
7. Add WebGPU only as an optional acceleration path after CPU fallback is proven.

## Runtime rules

- No parallel Face3D runtime.
- No WebGPU until CPU fallback is complete.
- No per-frame object particles.
- No synchronous GPU readback.
- No canvas resize inside the animation loop.
- No layout-changing DOM writes in the render path.

## Quality degradation order

1. Bloom.
2. Spatial repulsion.
3. Oscilloscope effects.
4. Frame cap.
5. Particle count on next engine rebuild.

Core face motion and expression blendshapes stay active even in degraded mode.

## Next safe patch

The next code patch should update `SpatialHash2D` to use fixed typed arrays and add a `ParticleField3D#repelNeighbors(hash, quality, dtMs)` pass after projected positions are updated.

docs/grok_bug_report_may_2026.md

# Grok Bug Report — May 2026

Source: live GitHub review, May 2026.

## Bugs

### bundle proxy 403

`bundle install` can fail when Rubygems traffic routes through a blocking proxy.

Remediation:

- keep README debug note
- add boot diagnostic
- distinguish network/proxy failure from application boot failure
- avoid treating dependency install failure as MASTER runtime failure

### hard 30s Council/Lint timeout

The current Council/Lint parallel timeout can abort useful work mid-run.

Remediation:

- replace fixed wall-clock timeout with budget policy
- emit timeout warning event before abort
- degrade to partial council result if lint completes
- preserve partial findings in runtime events
- make timeout configurable by workflow risk

### ruby_llm streaming gap

`ruby_llm` blocks true streaming and progress callbacks in some paths.

Remediation:

- wrap provider calls behind runtime provider adapter
- emit provider progress events when chunks exist
- emit heartbeat/progress events during blocking calls
- avoid direct streaming assumptions in UI
- expose stream capability in provider registry

### missing `/auto` mode

CLI/web lacks a first-class `/auto` mode.

Remediation:

- add `/auto` as workflow policy, not magic mode
- require risk, reversibility, budget, and review constraints
- emit autonomy gate events
- block irreversible/high-complexity work without quorum

### Canvas/TTS not mobile PWA optimized

Canvas visualizer and TTS need mobile-first PWA support.

Remediation:

- add manifest
- add service worker
- cache offline shell and recent traces
- make canvas responsive
- use Turbo/Stimulus for live nodes, edges, and timeline
- add Web Speech API fallback
- keep native `/chat/tts` endpoint for generated speech

## Rails 8 mobile-first PWA enhancement plan

### PWA

- `manifest.json`
- service worker
- offline shell
- trace cache
- installable standalone mode
- reduced-motion support

### Canvas

- Turbo stream runtime events
- Stimulus controllers for nodes, edges, timeline, replay scrubber
- mobile layout with touch targets
- runtime events as source of truth

### TTS

- Web Speech API where available
- native voices selection
- `/chat/tts` endpoint fallback
- mobile audio unlock handling
- cache last synthesized response offline when permitted

### Mobile

- responsive layout
- no hover-only controls
- large touch targets
- offline trace cache
- low-JS fallback

## Governance impact

These bugs reinforce existing runtime rules:

- provider capability detection must be explicit
- UI must not assume streaming
- fixed timeouts should become budgets
- autonomy requires gates
- PWA is not cosmetic; it is runtime reachability

docs/non_negotiable_runtime_rules.md

# Non-Negotiable Runtime Rules

## Core invariants

1. No agent directly mutates durable state.
2. Every side effect emits runtime events.
3. Replay reconstructs workflows without model memory.
4. Tool execution requires contract validation.
5. Telemetry is append-only unless compacted through policy.
6. Repair consumes failures as first-class runtime input.
7. Providers are runtime resources, not personalities.
8. Expensive cognition is reserved for high-risk decisions.
9. Rollback beats narration.
10. Deterministic orchestration beats hidden heuristics.

## Anti-patterns

Forbidden:

- hidden retries
- mutable globals
- provider-specific orchestration branches
- implicit state mutation
- silent fallbacks
- direct shell execution without contract
- model-selected providers
- unverifiable side effects
- memory as source of truth
- UI coupled to orchestration state

## Runtime philosophy

Models are probabilistic cognition edges.

The runtime is the operating system.

The orchestrator owns truth.

docs/platform_topology.md

# Platform Topology

## Primary platform

### brgen.no

Primary runtime surface and application umbrella.

Responsibilities:

- orchestration surface
- runtime observability
- MASTER integration
- authentication/session coordination
- shared search/indexing
- PWA shell
- dashboard and replay UI
- subapp routing
- SEO authority consolidation

Design direction:

- Rails 8
- Hotwire
- Turbo streams
- Stimulus
- pure SCSS
- semantic HTML
- offline-first PWA shell
- runtime-event-driven UI

brgen.no should mimic the interaction model of X/Twitter where useful:

- dense feed-first layout
- persistent left navigation on desktop
- mobile bottom navigation
- sticky composer/search where appropriate
- infinite or cursor-paginated timelines
- inline media/cards
- fast optimistic interactions
- keyboard shortcuts
- minimal modal friction
- profile/community surfaces
- notification and activity streams

But it must not copy X branding, visual chrome, or dark-pattern engagement mechanics.

brgen.no should feel like:

- civic/social operating surface
- fast timeline application
- local/community graph
- observability-aware PWA

Not:

- AI toy
- startup dashboard
- engagement casino
- decorative social clone

## Shared Rails visual baseline

### bsdports.org SCSS baseline

bsdports.org should define the standard SCSS baseline for all Rails apps.

Shared baseline qualities:

- OpenBSD-inspired restraint
- semantic HTML defaults
- typographic rhythm
- low-noise navigation
- minimal chrome
- sparse color
- high contrast
- grepable/printable content
- fast static-first rendering
- responsive without framework dependency

All Rails apps should inherit:

- tokens
- reset/base
- typography
- layout grid
- forms
- buttons
- tables
- flash/status messages
- navigation
- code/log rendering
- accessibility states
- print styles

Application-specific styling should layer above the bsdports baseline.

## Subapps

Subapps should inherit:

- bsdports SCSS baseline
- typography system
- runtime event schema
- PWA shell
- auth/session conventions
- accessibility rules
- SCSS tokens
- observability contracts

But remain operationally separable.

## Separate applications

### bsdports.org

Identity:

- BSD/OpenBSD influenced
- ports/packages/research focus
- operational minimalism
- grepable/searchable information architecture

UI direction:

- terminal-informed
- sparse color
- typography-first
- documentation-heavy
- fast/static-first where possible

bsdports.org is the source of truth for shared SCSS architecture.

### Hjerterom

Identity:

- community-oriented
- Åsane/Bergen context
- calmer and warmer tone than MASTER
- accessibility and mobile-first focus

Should still retain:

- semantic HTML
- restrained design
- bsdports SCSS baseline
- accessibility discipline
- SSR-first rendering
- offline capability where useful

### Amber

Identity:

- experimental/research/runtime sandbox
- lower operational guarantees
- rapid iteration allowed

Rules:

- isolate experiments
- do not leak instability into core runtime
- preserve replay and telemetry discipline
- inherit shared SCSS baseline unless experiment requires isolation

### Blogger aggregate + SEO booster

Identity:

- indexing
- aggregation
- summarization
- SEO reinforcement
- content graph construction

Architecture direction:

- canonical metadata
- structured data/schema.org
- sitemap automation
- RSS/Atom ingestion
- deduplication
- semantic tagging
- replayable ingestion pipeline
- bsdports baseline for rendering and readability

## Shared platform principles

All applications should share:

- semantic HTML
- pure SCSS
- bsdports SCSS baseline
- Hotwire-first interaction
- accessibility-first design
- PWA capability where appropriate
- replayable runtime telemetry
- restrained operational aesthetics
- OpenBSD-inspired clarity

## Shared anti-patterns

Avoid across all apps:

- Tailwind sprawl
- hydration-heavy SPA architecture
- decorative dashboards
- utility-class soup
- fake activity animations
- hidden runtime state
- frontend-derived truth
- excessive JS frameworks
- engagement dark patterns

docs/provider_economy.md

# Provider Economy

MASTER should treat providers as competing infrastructure.

The runtime selects providers by:

- capability
- latency
- hallucination rate
- timeout rate
- repair burden
- token cost
- context capacity
- historical reliability

## Principle

The orchestrator owns provider selection.

Models do not select themselves.

## Routing tiers

### Cheap cognition

Use inexpensive and fast providers for:

- classification
- compression
- retrieval ranking
- summarization
- critique
- voting
- lint reasoning

### Expensive cognition

Use expensive providers for:

- high-risk synthesis
- architecture decisions
- irreversible mutations
- council arbitration
- long-horizon planning

## Quarantine

Providers enter quarantine when:

- hallucination rate spikes
- retries exceed threshold
- invalid structured outputs repeat
- timeout rate climbs
- corruption incidents increase

## Long-term direction

Provider routing becomes:

```text
workflow
  -> capability routing
  -> health scoring
  -> quorum
  -> replay analysis
  -> repair feedback
  -> adaptive optimization

The provider layer becomes a runtime economy, not a static config file.


## `docs/repo_ecology.md`
```markdown
# MASTER Repo Ecology

MASTER should evolve from a code assistant into a repository ecology engine.

## Core Principle

A repository is not a bag of files.

A repository is:
- semantic topology
- dependency ecology
- cognitive geography
- architectural history
- organizational memory

MASTER should optimize:
- comprehensibility
- semantic coherence
- navigability
- architectural elegance
- entropy reduction

Not merely formatting or linting.

---

# Repo Defragmentation Pipeline

```text
scan
→ classify
→ cluster
→ score
→ simulate
→ critique
→ apply
→ test
→ rollback-if-needed

Every transformation must be:

  • observable
  • reversible
  • scoped
  • audited
  • epistemically justified

Planned Subsystems

DeadFileScanner

Detect:

  • unreferenced files
  • orphaned modules
  • stale migrations
  • unused tests
  • abandoned experiments
  • duplicate assets

Safety rules:

  • never delete without graph proof
  • require zero inbound references
  • require git-history confidence threshold

SimilarityClusterer

Semantic clustering for:

  • duplicated modules
  • overlapping utilities
  • convergent abstractions
  • namespace fragmentation

Outputs:

  • merge proposals
  • bounded-context candidates
  • architectural convergence maps

ReferenceGraph

Repository-wide graph of:

  • imports
  • runtime references
  • constants
  • events
  • pipelines
  • CLI commands
  • file mutations

Enables:

  • safe rename
  • scoped rollback
  • dependency visualization
  • blast-radius estimation

RenamePlanner

Goals:

  • semantic consistency
  • grammar normalization
  • namespace coherence
  • ambiguity reduction

Must support:

  • import rewriting
  • test rewriting
  • rollback journal
  • migration plans

MergePlanner

Combines:

  • semantic similarity
  • dependency overlap
  • runtime coupling
  • shared terminology

Produces:

  • merge simulations
  • API compatibility warnings
  • cohesion scoring

TreeAestheticScorer

Repository elegance metrics:

  • folder cohesion
  • duplicate naming
  • depth pressure
  • orphan density
  • utility sprawl
  • entropy gradients
  • semantic consistency

Goal: A repository should feel calm and intentional.


Repo Weather

Runtime repository state visualization:

  • architectural turbulence
  • dependency storms
  • duplication hotspots
  • entropy fog
  • stable regions
  • dead zones

Integrated with cognition ecology.


Semantic Terrain

Repository geography generated from:

  • dependency gravity
  • module centrality
  • instability
  • change frequency
  • memory access

Terrain examples:

  • mountains = stable architecture
  • rivers = data flow
  • fractures = contradictory abstractions
  • swamps = utility sprawl

Safety Requirements

Never perform:

  • blind deletes
  • global rewrites without simulation
  • unsafe renames
  • non-scoped rollback
  • silent merges

Always require:

  • touched-path tracking
  • rollback journal
  • diff simulation
  • syntax validation
  • test execution
  • governance approval

Epistemic Requirements

Every refactor proposal should expose:

  • confidence
  • assumptions
  • unresolved ambiguity
  • dependency risk
  • blast radius
  • evidence
  • critique history

MASTER should prefer:

  • explicit uncertainty
  • reversible changes
  • conservative execution

over:

  • aggressive automation
  • speculative restructuring

Long-Term Direction

MASTER should become:

an autonomous repository gardener

Capabilities:

  • ecosystem stabilization
  • architectural healing
  • entropy reduction
  • semantic harmonization
  • cognition-aware restructuring

The ideal repository should eventually feel:

  • smaller
  • clearer
  • elegant
  • peaceful
  • alive

## `docs/runtime_ui_direction.md`
```markdown
# Runtime UI Direction

MASTER should feel like:

- a control room
- a courtroom
- a terminal
- a scientific instrument

Not:

- a startup dashboard
- a gamified AI toy
- a casino UI
- a neon control panel

## Visual philosophy

Restraint creates authority.

Whitespace is structure.
Typography is architecture.
Motion communicates state.
Observability beats decoration.

## Runtime-derived visuals

Every visual state should derive from runtime events.

Examples:

- entropy
- confidence
- provider health
- workflow pressure
- repair activity
- replay state
- orchestration topology

The UI should not invent operational truth.

## Typography

Use disciplined typography:

- readable line lengths
- visible hierarchy
- restrained contrast
- stable rhythm
- monospace only where semantic

Avoid:

- tiny gray text
- decorative font systems
- excessive uppercase
- compressed spacing
- visual noise

## Motion

Motion should:

- communicate transitions
- expose workflow state
- show repair/retry activity
- reveal orchestration flow
- remain interruptible

Avoid:

- infinite animation
- ornamental transitions
- motion without semantic meaning

## Rails and Hotwire

Favor:

- server rendering
- HTML-first workflows
- Turbo streams
- progressive enhancement
- minimal JS runtime
- DOM-derived interaction

Avoid:

- SPA complexity unless necessary
- duplicated client/server state
- hydration-heavy architectures

## Operational aesthetic

MASTER should visually express:

- determinism
- inspectability
- replayability
- governance
- confidence under pressure

The interface is part of the runtime philosophy.

lib/builder.rb

# frozen_string_literal: true

require "fileutils"

module Master
  # Single source of truth for container wiring. Each `boot_*` method assembles
  # one subsystem from primitive dependencies. `build` runs them in order.
  module Builder
    MUTATING_TOOLS = %w[write_file str_replace ast_edit].freeze
    RING_SIZE          = 1000
    SNAPSHOT_MAX_BYTES = 50_000
    SNAPSHOT_DIRS      = %w[bin lib data].freeze

    TOOL_MAP = {
      "ReadFile"        => ->(r, i) { Reach::ReadFile.new(root: r, undo: i[:undo], event_bus: i[:bus]) },
      "WriteFile"       => ->(r, i) { Reach::WriteFile.new(root: r, undo: i[:undo], governor: i[:governor], event_bus: i[:bus], diff_stager: i[:diff_stager]) },
      "StrReplace"      => ->(r, i) { Reach::StrReplace.new(root: r, undo: i[:undo], governor: i[:governor], event_bus: i[:bus], diff_stager: i[:diff_stager]) },
      "BatchReplace"    => ->(r, i) { Reach::BatchReplace.new(root: r, governor: i[:governor], event_bus: i[:bus]) },
      "AstEdit"         => ->(r, i) { Reach::AstEdit.new(root: r, undo: i[:undo], governor: i[:governor], event_bus: i[:bus]) },
      "Tree"            => ->(r, i) { Reach::Tree.new(root: r, event_bus: i[:bus]) },
      "ListDir"         => ->(r, i) { Reach::ListDir.new(root: r, event_bus: i[:bus]) },
      "SearchFiles"     => ->(r, i) { Reach::SearchFiles.new(root: r, event_bus: i[:bus]) },
      "SearchKnowledge" => ->(r, i) { Reach::SearchKnowledge.new(root: r, event_bus: i[:bus]) },
      "SymbolLookup"    => ->(r, i) { Reach::SymbolLookup.new(code_index: i[:code_index], event_bus: i[:bus]) },
      "Shell"           => ->(r, i) { Reach::Shell.new(root: r, governor: i[:governor], event_bus: i[:bus]) },
      "GitContext"      => ->(r, i) { Reach::GitContext.new(root: r, event_bus: i[:bus]) },
      "WebFetch"        => ->(r, i) { Reach::WebFetch.new(governor: i[:governor], event_bus: i[:bus]) },
      "WebSearch"       => ->(r, i) { Reach::WebSearch.new(governor: i[:governor], event_bus: i[:bus]) },
      "Clean"           => ->(r, i) { Reach::Clean.new(root: r, governor: i[:governor], event_bus: i[:bus]) },
      "FeedbackRecord"  => ->(r, i) { Reach::FeedbackRecord.new(learnings: i[:learnings]) },
      "MemoryRecord"    => ->(r, i) { Reach::MemoryRecord.new(memory: i[:memory], root: r, event_bus: i[:bus]) }
    }.freeze

    module_function

    def build(root: Dir.pwd)
      Master.configure_providers!
      infra = build_infrastructure(root)
      ai    = build_ai(root, infra)
      pipeline, gateway = build_pipeline(root, infra, ai)
      infra.merge(ai).merge(pipeline:, gateway:, root:)
    end

    # No LLM, no autonomous loop, no network. CI / MASTER_SCAN_ONLY=1.
    def build_scan_only(root: Dir.pwd)
      config      = Ground::Config.new(root)
      boot_config = config.freeze_boot
      trace       = boot_trace(root:, config:)
      bus         = trace[:bus]
      code_index  = Judge::CodeIndex.new(root:, event_bus: bus)
      scanner     = build_scanner(root:, bus:)
      trace.merge(config:, boot_config:, code_index:, scanner:, root:)
    end

    def build_infrastructure(root)
      config      = Ground::Config.new(root)
      config["model"] ||= Master.default_model
      boot_config = config.freeze_boot
      trace  = boot_trace(root:, config:)
      loop_c = boot_loop(root:, config:, bus: trace[:bus])
      reach  = boot_reach(root:, config:, bus: trace[:bus])
      ground = boot_ground(root:, config:, homeostat: loop_c[:homeostat])

      bus        = trace[:bus]
      renderer   = Voice::Renderer.new(config:)
      code_index = Judge::CodeIndex.new(root:, event_bus: bus)
      code_index.build_async
      bus.subscribe("tool:after") do |ev|
        next unless ev[:path] && MUTATING_TOOLS.include?(ev[:tool].to_s)
        code_index.reindex(ev[:path])
      end
      diag     = Trace::Diag.new(homeostat: loop_c[:homeostat], breaker: reach[:breaker], logging: trace[:logging])
      pressure = PressureEngine.new(event_bus: bus)
      bus.subscribe("*") do |ev|
        event_name = ev[:event] || ev["event"] || ev[:type] || ev["type"] || "event"
        next if event_name.to_s.start_with?("pressure:")
        pressure.ingest(event: event_name, payload: ev)
      rescue StandardError => e
        Ground::Swallow.log(e, context: "builder.pressure_engine", event_bus: bus)
      end

      { config:, boot_config:, renderer:, code_index:, diag:, pressure: }
        .merge(trace).merge(loop_c).merge(reach).merge(ground)
    end

    def boot_trace(root:, config:)
      event_log = Trace::EventLog.new(root:)
      bus       = Trace::EventBus.new(event_log:)
      ring      = Trace::RingBuffer.new(RING_SIZE)
      logging   = Trace::Logging.new(ring_buffer: ring, event_bus: bus)
      session   = Trace::Session.new(root:, budget_max: config.budget_max, req_max: config.req_max)
      undo      = Trace::Undo.new(session:, event_bus: bus, root:)
      metrics   = Trace::Metrics.new(root:, event_bus: bus)
      Trace::AuditLog.new(root:, event_bus: bus)
      Trace::SwallowLedger.new(event_bus: bus, root:).attach
      recorder  = Trace::Recorder.new(root:, event_bus: bus)
      { event_log:, bus:, ring:, logging:, session:, undo:, metrics:, trace: recorder }
    end

    def boot_loop(root:, config:, bus:)
      homeostat   = Loop::Homeostat.new(event_bus: bus)
      governor    = Loop::Governor.new(config:, event_bus: bus)
      diff_stager = config["staging_enabled"] ? Loop::DiffStager.new(root:, event_bus: bus) : nil
      phase_gates = Ground::PhaseGates.new(root:, event_bus: bus)
      { homeostat:, governor:, diff_stager:, phase_gates: }
    end

    def boot_reach(root:, config:, bus:)
      breaker = Reach::CircuitBreakerRegistry.new(budget_max: config.budget_max, req_max: config.req_max, event_bus: bus)
      cache   = Reach::SemanticCache.new(root:, ttl: config["cache_ttl"], event_bus: bus)
      mcp     = Reach::McpCoordinator.new(root:, event_bus: bus)
      mcp.connect_all
      { breaker:, cache:, mcp: }
    end

    def boot_ground(root:, config:, homeostat:)
      memory      = Ground::Memory.new(root:)
      personality = Voice::Personality.new(config["persona"]&.to_sym || Voice::Personality::DEFAULT, root:, homeostat:)
      learnings   = Ground::KnowledgeStore.new(root:)
      { memory:, personality:, learnings: }
    end

    def build_ai(root, infra)
      bus   = infra[:bus]
      tools = build_tools(root:, infra:) + infra[:mcp].tools
      deps  = Judge::Agent::Dependencies.from_kwargs(
        config: infra[:config], session: infra[:session], tools:,
        circuit_breaker: infra[:breaker], cache: infra[:cache], event_bus: bus,
        model_router: Now::Routing::ModelRouter.new(config: infra[:config]),
        reasoning_modes: Judge::Modes.new,
        memory: infra[:memory], personality: infra[:personality],
        code_index: infra[:code_index], homeostat: infra[:homeostat]
      )
      agent    = Judge::Agent.new(deps:)
      soul_doc = Voice::Soul.new(root:, agent:)
      tools << Reach::AskLlm.new(agent:, governor: infra[:governor], circuit_breaker: infra[:breaker], cache: infra[:cache], event_bus: bus)
      ctx = Now::ContextWindow.new(session: infra[:session], agent:, model_context: Master::CTX_WINDOW_SIZE)
      ctx.check_and_compact!
      agent.wire_context_window(ctx)
      agent.wire_constitution(Ground::Constitution.new)
      scanner       = build_scanner(root:, agent:, bus:)
      swarm         = Judge::Swarm::Coordinator.new(agent:, event_bus: bus)
      personas      = Judge::Council::Personas.load(File.join(Master::ROOT, "data", "council.yml"))
      axioms        = Ground::Rules.new(root:)
      deliberation  = Judge::Council::Deliberation.new(personas:, agent:, event_bus: bus, axioms:)
      ideation      = Judge::Council::Ideation.new(agent:, event_bus: bus)
      council_stage = Now::Stages::Council.new(deliberation:, config: infra[:config], event_bus: bus)
      guard         = Judge::Security::InjectionGuard.new
      autonomous    = boot_autonomous(root:, infra:, agent:, scanner:, axioms:)
                        .merge(learnings: infra[:learnings], skills: boot_skills(root, bus))
      autonomous[:standing].wire_container(scanner:, agent:, root:, bus:)
      { agent:, soul: soul_doc, scanner:, swarm:, deliberation:, council_stage:, ideation:, guard: }.merge(autonomous)
    end

    def build_scanner(root:, agent: nil, bus: nil)
      Judge::Scan::RuleDSL
      scanner = Judge::Scan::Scanner.new(event_bus: bus)
      Judge::Scan::Rule.registry.select(&:auto_build?).each { |k| scanner.add_rule(k.new) }
      scanner.add_rule(Judge::Scan::Rules::RuleCoverageRule.new(root:))
      scanner.add_rule(Judge::Scan::Rules::RubocopRule.new(root:))
      scanner.add_rule(Judge::Scan::Rules::ReekRule.new(root:))
      scanner.add_rule(Judge::Scan::Rules::InterconnectRule.new(root:))
      scanner.add_rule(Judge::Scan::Rules::SemanticRule.new(agent:))
      scanner.add_rule(Judge::Scan::Rules::AdversarialRule.new(agent:))
      scanner.add_rule(Judge::Scan::Rules::CommentDriftRule.new(agent:))
      scanner.add_rule(Judge::Scan::Rules::AstOmissionRule.new(root:))
      scanner
    end

    def boot_autonomous(root:, infra:, agent:, scanner:, axioms: nil)
      bus       = infra[:bus]
      standing  = Ground::StandingOrders.new(pipeline: nil, event_bus: bus)
      git       = Reach::GitOperations.new(root)
      rules     = scanner.instance_variable_get(:@rules)
      learnings = infra[:learnings]

      # In-process FixLoop autofix on boot is gated to MASTER_AUTOFIX=1. Off by
      # default — the loop's autocommits race deploys and over-aggressively
      # rewrites framework boilerplate. Run /fix manually or set the env var on
      # hosts where unattended convergence is wanted.
      fix_loop = Loop::FixLoop.new(rules:, axioms:, agent:, scanner:, root:, bus:, git:, learnings:)
      if ENV["MASTER_AUTOFIX"] == "1"
        Thread.new { fix_loop.run_forever(root) }.tap { |t| t.abort_on_exception = false }
      end

      # Architecture #7: reactive file-watcher instead of polling.
      # Activate with MASTER_WATCH=1 on VPS (requires rb-kqueue or rb-inotify).
      watch_loop = if ENV["MASTER_WATCH"] == "1"
        wl = Loop::WatchLoop.new(rules:, agent:, scanner:, root:, bus:, learnings:)
        Thread.new { wl.run }.tap { |t| t.abort_on_exception = false }
        wl
      end

      heartbeat = Loop::Heartbeat.new(root:, agent:, scanner:, memory: infra[:memory], event_bus: bus, homeostat: infra[:homeostat])
      triggers  = Trace::Triggers.new(event_bus: bus, scanner:, agent:)
      triggers.install_defaults!

      propose_tree = Loop::ProposeTree.new(root:, agent:, event_bus: bus)
      bus.subscribe("fix_loop:clean")    { Thread.new { propose_tree.call } }
      bus.subscribe("fix_loop:plateau")  { Thread.new { propose_tree.call } }

      # Architecture: continuous OpenBSD load watcher.
      # Default on; MASTER_WATCHER=0 disables. Sampler is read-only.
      watcher = Loop::Watcher.new(bus:, root:)
      if ENV["MASTER_WATCHER"] != "0"
        Thread.new { watcher.run_forever }.tap { |t| t.abort_on_exception = false }
      end
      bus.subscribe("system:crit") { Thread.new { fix_loop.stop_background! if fix_loop.background_alive? } }

      { standing:, fix_loop:, watch_loop:, heartbeat:, triggers:, propose_tree:, watcher: }
    end

    def boot_skills(root, bus)
      skills = Now::Skills.new(root:, event_bus: bus)
      skills.discover!
      skills
    end

    def build_tools(root:, infra:)
      path = File.join(root, "data", "tools.yml")
      defs = Master.load_yaml(path)
      return [] unless defs.is_a?(Array)
      defs.filter_map do |defn|
        next unless defn["default"] == true
        factory = TOOL_MAP[defn["name"].to_s]
        factory ? factory.call(root, infra) : (infra[:bus]&.publish("builder:tool_skipped", tool: defn["name"]); nil)
      end
    end

    def build_pipeline(root, infra, ai)
      config   = infra[:config]
      bus      = infra[:bus]
      commands = Now::CommandRegistry.build(infra:, ai:, root:)
      stages   = [
        Now::Stages::Intake.new,
        Now::Stages::Enhance.new(agent: ai[:agent], event_bus: bus),
        Now::Stages::Infer.new,
        Now::Stages::Route.new(commands:, agent: ai[:agent]),
        Now::Stages::Guard.new(governor: infra[:governor], injection_guard: ai[:guard]),
        Now::Stages::Deliberate.new(agent: ai[:agent], config:),
        Now::Stages::Execute.new,
        Now::Pipeline::SkipOnPressure.new(
          Now::Stages::Review.new(council: ai[:council_stage], scanner: ai[:scanner], config:, root:, event_bus: bus),
          bus:
        ),
        Now::Stages::Memory.new(memory: infra[:memory], event_bus: bus),
        Now::Stages::Render.new(renderer: infra[:renderer])
      ]
      pipeline = Now::Pipeline.new(stages, bus:, trace: config["trace_pipeline"] == true, root:)
      ai[:standing].wire_pipeline(pipeline)
      gateway = Reach::Gateway.new(pipeline:, session: infra[:session], event_bus: bus)
      commands["gateway"] = ->(_ctx) { gateway.channels }
      [pipeline, gateway]
    end

    def boot_snapshot(container)
      root  = container[:root]
      files = Dir[*SNAPSHOT_DIRS.map { |d| File.join(root, d, "**", "*") }]
               .select { |f| File.file?(f) && File.size(f) < SNAPSHOT_MAX_BYTES }
               .reject { |f| f.include?("/knowledge/") || f.include?("/vendor/") }
               .sort
      body = files.flat_map do |f|
        rel  = f.delete_prefix("#{root}/")
        lang = Master::FILE_LANGUAGE_MAP.fetch(File.extname(f).downcase, "text")
        src  = File.read(f, encoding: "UTF-8", invalid: :replace)
        ["## #{rel}", "```#{lang}", src.rstrip, "```", ""]
      rescue StandardError => e
        Ground::Swallow.log(e, context: "builder.snapshot_file", path: f)
        []
      end
      header  = ["# MASTER Snapshot", "Generated: #{Time.now.utc.iso8601}", "Files: #{files.size}", ""]
      content = (header + body).join("\n")
      out     = File.join(root, ".master", "snapshot.md")
      FileUtils.mkdir_p(File.dirname(out))
      File.write(out, content)
      File.write(File.join(root, "snapshot.md"), content)
      container[:bus]&.publish("boot:snapshot", files: files.size)
    rescue StandardError => e
      container[:bus]&.publish("boot:snapshot_error", error: e.message)
    end
  end
end

lib/design/mobile_first_pwa_profiles.rb

# frozen_string_literal: true

module Master
  module Design
  class MobileFirstPwaProfiles
    Rule = Data.define(:id, :pattern, :extract, :threshold, :message, :severity)

    TOUCH_TARGET_MIN_PX = Master::Ground::Axioms::Wcag::TOUCH_TARGET_AAA_PX
    BODY_FONT_MIN_PX    = Master::Ground::Axioms::Wcag::BODY_FONT_MIN_PX
    LINE_HEIGHT_MIN     = Master::Ground::Axioms::Wcag::LINE_HEIGHT_MIN
    LINE_LENGTH_MIN_CH  = 45
    LINE_LENGTH_MAX_CH  = 75

    CSS_RULES = [
      Rule.new(
        id:        :font_size_too_small,
        pattern:   /(?:^|[{\s;])font-size:\s*(\d+)px/,
        extract:   ->(m) { m[1].to_i },
        threshold: BODY_FONT_MIN_PX,
        message:   "font-size %dpx below #{BODY_FONT_MIN_PX}px baseline",
        severity:  :high
      ),
      Rule.new(
        id:        :line_height_too_low,
        pattern:   /line-height:\s*([\d.]+)(?!px|em|rem)/,
        extract:   ->(m) { m[1].to_f },
        threshold: LINE_HEIGHT_MIN,
        message:   "line-height %.1f below #{LINE_HEIGHT_MIN} — readability baseline",
        severity:  :medium
      ),
      Rule.new(
        id:        :touch_target_too_small,
        pattern:   /min-height:\s*(\d+)px/,
        extract:   ->(m) { m[1].to_i },
        threshold: TOUCH_TARGET_MIN_PX,
        message:   "min-height %dpx below #{TOUCH_TARGET_MIN_PX}px WCAG touch target (2.5.8)",
        severity:  :high
      )
    ].freeze

    PATTERN_RULES = [
      Rule.new(
        id:        :animation_no_reduced_motion,
        pattern:   /(?:animation|transition)\s*:/,
        extract:   nil,
        threshold: nil,
        message:   "animation/transition without @media (prefers-reduced-motion: reduce) guard",
        severity:  :medium
      ),
      Rule.new(
        id:        :raw_primary_color,
        pattern:   /#(?:ff0000|00ff00|0000ff|ffff00|ff00ff|00ffff)\b/i,
        extract:   nil,
        threshold: nil,
        message:   "raw primary color — use shadow/midtone/highlight graded triplets",
        severity:  :low
      ),
      Rule.new(
        id:        :linear_timing,
        pattern:   /transition:[^;]*\blinear\b/,
        extract:   nil,
        threshold: nil,
        message:   "linear timing function — prefer ease-out or cubic-bezier for perceived smoothness",
        severity:  :low
      )
    ].freeze

    HTML_CHECKS = {
      landmarks:    { pattern: /<main|<nav\b|<header\b|<footer\b/, message: "no landmark elements — add <main>, <nav>, <header>, <footer>", severity: :high },
      focus_ring:   { pattern: /focus-visible|:focus\b/,            message: "no focus-visible styles found",                                severity: :high },
      form_labels:  { pattern: /<label\b/,                          message: "form found but no <label> elements",                           severity: :medium }
    }.freeze

    def audit(app_path)
      css_findings  = audit_css(app_path)
      html_findings = audit_html(app_path)
      all = css_findings + html_findings
      { violations: all, severity_counts: all.group_by { |v| v[:severity] }.transform_values(&:count) }
    end

    def recommendations(audit_result)
      Array(audit_result[:violations])
        .sort_by { |v| %i[high medium low].index(v[:severity]) || 9 }
        .map { |v| heuristic_prefix(v) + "[#{v[:severity].upcase}] #{v[:id]}: #{v[:message]}" }
    end

    private

    HEURISTIC_MAP = {
      font_size_too_small:         :h8_minimalism,
      line_height_too_low:         :h8_minimalism,
      touch_target_too_small:      :h6_recognition,
      animation_no_reduced_motion: :h8_minimalism,
      raw_primary_color:           :h8_minimalism,
      linear_timing:               :h8_minimalism,
      landmarks:                   :h4_consistency,
      focus_ring:                  :h6_recognition,
      form_labels:                 :h5_error_prevention
    }.freeze

    def heuristic_prefix(violation)
      key  = violation.is_a?(Hash) ? violation[:id]&.to_sym : nil
      h_key = HEURISTIC_MAP[key]
      return "" unless h_key
      "[Nielsen ##{Master::Ground::Axioms::UxHeuristics.number(h_key)}] "
    end

    def audit_css(path)
      css_files = Dir.glob(File.join(path, "**", "*.{css,scss}"))
                     .reject { |f| f.include?("node_modules") || f.include?("vendor") }
      findings = []

      css_files.each do |file|
        source = begin
          File.read(file)
        rescue StandardError
          next
        end
        has_reduced_motion = source.match?(/prefers-reduced-motion/)

        CSS_RULES.each do |rule|
          source.scan(rule.pattern) do |m|
            value = rule.extract&.call(m)
            next if value && value >= rule.threshold
            msg = value ? format(rule.message, value) : rule.message
            findings << { id: rule.id, file:, message: msg, severity: rule.severity }
          end
        end

        PATTERN_RULES.each do |rule|
          next unless source.match?(rule.pattern)
          next if rule.id == :animation_no_reduced_motion && has_reduced_motion
          findings << { id: rule.id, file:, message: rule.message, severity: rule.severity }
        end
      end

      findings
    end

    def audit_html(path)
      erb_files = Dir.glob(File.join(path, "**", "*.html.{erb,haml}"))
                     .reject { |f| f.include?("vendor") }
      return [{ id: :no_html, file: path, message: "no HTML/ERB files found", severity: :medium }] if erb_files.empty?

      combined = erb_files.first(40).map { |f| File.read(f) rescue "" }.join("\n")
      findings = []

      HTML_CHECKS.each do |check_id, spec|
        has_form = combined.match?(/<form\b/)
        next if check_id == :form_labels && !has_form
        next if combined.match?(spec[:pattern])
        findings << { id: check_id, file: "(views)", message: spec[:message], severity: spec[:severity] }
      end

      findings
    end
  end
  end
end

lib/design/platform_profiles.rb

# frozen_string_literal: true

module Master
  module Design
  module PlatformProfiles
    PROFILES = {
      brutal_minimal: {
        philosophy: "content-first, invisible design, delete anything that does not improve readability",
        layout: %w[single_column semantic_html system_fonts black_white max_65ch],
        avoid: %w[shadows glows gradients rounded_corners decorative_borders hero_banners loading_theatrics custom_fonts],
        metrics: { max_total_kb: 10, contrast: 4.5, line_min_ch: 45, line_max_ch: 75 }
      },
      medium: {
        philosophy: "long-form reading comfort through generous side whitespace and typographic rhythm",
        layout: %w[article_column max_65ch large_line_height restrained_navigation],
        avoid: %w[visual_noise dense_sidebars auto_play],
        metrics: { line_height: 1.58, max_width_px: 680 }
      },
      substack: {
        philosophy: "newsletter-first trust, direct author voice, low-friction subscription",
        layout: %w[publication_header readable_feed email_capture simple_article],
        avoid: %w[overdesigned_cards hidden_authors distracting_motion],
        metrics: { cta_count: 1, line_max_ch: 75 }
      },
      new_yorker: {
        philosophy: "editorial authority, strong type hierarchy, content as cultural object",
        layout: %w[serif_editorial strong_headlines generous_margins disciplined_grid],
        avoid: %w[cheap_gradients excessive_chrome noisy_cards],
        metrics: { contrast: 4.5, font_families_max: 2 }
      },
      x: {
        philosophy: "dense real-time feed optimized for scanning and interaction velocity",
        layout: %w[feed timeline compact_actions sticky_navigation],
        avoid: %w[slow_animation large_cards_hidden_content],
        metrics: { action_target_px: 44 }
      },
      tiktok: {
        philosophy: "full-screen media-first flow with minimal chrome and immediate feedback",
        layout: %w[full_screen_media vertical_flow gesture_first],
        avoid: %w[text_heavy_chrome slow_load blocking_modals],
        metrics: { first_frame_ms: 500 }
      }
    }.freeze

    module_function

    def fetch(name)
      PROFILES.fetch(name.to_sym)
    end

    def brief(name)
      p = fetch(name)
      "#{name}: #{p[:philosophy]}; layout=#{p[:layout].join(', ')}; avoid=#{p[:avoid].join(', ')}; metrics=#{p[:metrics]}"
    end

    def constraints(name)
      profile = fetch(name)
      [
        "philosophy: #{profile[:philosophy]}",
        "layout: #{profile[:layout].join(', ')}",
        "avoid: #{profile[:avoid].join(', ')}",
        "metrics: #{profile[:metrics]}"
      ]
    end

    def choose(text)
      source = text.to_s.downcase
      return :brutal_minimal if source.match?(/brutal|minimal|motherfucking|content-first|black.?white/)
      return :medium if source.include?("medium") || source.match?(/long.?form|reading/)
      return :substack if source.include?("substack") || source.include?("newsletter")
      return :new_yorker if source.include?("new yorker") || source.include?("editorial")
      return :tiktok if source.include?("tiktok") || source.include?("short video")
      return :x if source.match?(/\bx\b|twitter|feed/)

      :brutal_minimal
    end
  end
  end
end

lib/ground/agent_lifecycle.rb

# frozen_string_literal: true

module Master
  module Ground
  class AgentLifecycle
    STATES = %i[new planned running waiting verifying complete failed cancelled].freeze
    TRANSITIONS = {
      new: %i[planned running cancelled],
      planned: %i[running cancelled],
      running: %i[waiting verifying complete failed cancelled],
      waiting: %i[running cancelled],
      verifying: %i[running complete failed cancelled],
      complete: [],
      failed: %i[planned cancelled],
      cancelled: []
    }.freeze

    Event = Struct.new(:from, :to, :reason, :at, keyword_init: true)

    attr_reader :state, :events, :metadata

    def initialize(initial: :new, metadata: {})
      @state = normalize(initial)
      @metadata = metadata
      @events = []
    end

    def transition(to, reason: nil)
      target = normalize(to)
      allowed = TRANSITIONS.fetch(state)
      raise ArgumentError, "invalid transition #{state} -> #{target}" unless allowed.include?(target)

      @events << Event.new(from: state, to: target, reason: reason, at: Time.now.utc)
      @state = target
      self
    end

    def terminal?
      TRANSITIONS.fetch(state).empty?
    end

    def running?
      state == :running
    end

    def failed?
      state == :failed
    end

    def complete?
      state == :complete
    end

    def self.valid_transition?(from, to)
      TRANSITIONS.fetch(normalize_static(from), []).include?(normalize_static(to))
    end

    def self.normalize_static(value)
      key = value.to_s.downcase.to_sym
      STATES.include?(key) ? key : :new
    end

    private

    def normalize(value)
      self.class.normalize_static(value)
    end
  end
  end
end

lib/ground/atomic_write.rb

# frozen_string_literal: true

module Master
  module Ground
    module AtomicWrite
      private

      # Write content to path via tmp+rename. Pass fsync: true for durable config writes.
      def write_atomic(path, content, encoding: "UTF-8", fsync: false)
        tmp_path = "#{path}.tmp.#{Process.pid}"
        File.open(tmp_path, "w", encoding:) do |f|
          f.write(content)
          if fsync
            f.flush
            f.fsync
          end
        end
        File.rename(tmp_path, path)
      rescue StandardError => e
        File.delete(tmp_path) if tmp_path && File.exist?(tmp_path)
        raise e
      end
    end
  end
end

lib/ground/attention_context.rb

# frozen_string_literal: true

module Master
  module Ground
  class AttentionContext
    VALID_ZOOMS = %w[wide narrow wide_to_deep deep_to_wide deep].freeze
    VALID_ACTS  = %w[scout mine repair land verify rollback checkpoint review].freeze

    attr_reader :map, :zoom, :act, :target, :parent

    COMPLEX_ACTS = %w[mine repair rollback checkpoint verify].freeze

    def initialize(map: nil, zoom: "wide", act: "scout", target: [], parent: [])
      @map    = map.to_s
      @zoom   = VALID_ZOOMS.include?(zoom.to_s) ? zoom.to_s : "wide"
      @act    = VALID_ACTS.include?(act.to_s)   ? act.to_s  : "scout"
      @target = Array(target).map(&:to_s)
      @parent = Array(parent).map(&:to_s)
    end

    def complex?
      COMPLEX_ACTS.include?(@act)
    end

    def to_s
      parts = ["map: #{@map}"].tap do |p|
        p << "zoom: #{@zoom}" if @zoom != "wide"
        p << "act: #{@act}"   if @act  != "scout"
      end
      "⟦#{parts.join(" | ")}⟧"
    end

    def to_h
      { map: @map, zoom: @zoom, act: @act, target: @target, parent: @parent }
    end

    def self.from_yaml(path)
      return new unless File.exist?(path)
      data = Master.load_yaml(path) || {}
      new(
        map:    data["map"],
        zoom:   data["zoom"]   || "wide",
        act:    data["act"]    || "scout",
        target: data["target"] || [],
        parent: data["parent"] || []
      )
    end
  end
  end
end

lib/ground/axioms/rails_doctrine.rb

# frozen_string_literal: true

module Master
  module Ground
  module Axioms
  module RailsDoctrine
    # The nine pillars — rubyonrails.org/doctrine (DHH)
    # Cite these when justifying architectural decisions, not just Rails apps.
    PILLARS = {
      happiness:       "Optimize for programmer happiness",
      convention:      "Convention over Configuration",
      omakase:         "The menu is omakase",
      no_one_paradigm: "No one paradigm",
      beautiful_code:  "Exalt beautiful code",
      sharp_knives:    "Provide sharp knives",
      integrated:      "Value integrated systems",
      progress:        "Progress over stability",
      big_tent:        "Push up a big tent"
    }.freeze

    # Solid Trifecta — database-backed adapters; eliminates Redis/PaaS dependency.
    # Doctrine basis: :integrated — "Value integrated systems"
    SOLID_TRIFECTA = %w[solid_queue solid_cache solid_cable].freeze

    def self.cite(pillar, rationale)
      name = PILLARS.fetch(pillar) { pillar.to_s }
      "[Rails Doctrine — #{name}] #{rationale}"
    end
  end
  end
  end
end

lib/ground/axioms/ux_heuristics.rb

# frozen_string_literal: true

module Master
  module Ground
  module Axioms
  module UxHeuristics
    # Nielsen's 10 Usability Heuristics — nngroup.com/articles/ten-usability-heuristics/
    # Universal: apply to CLI output, REPL prompts, web UI, API error messages, and prose alike.
    HEURISTICS = {
      h1_visibility:       "Visibility of System Status — keep users informed through appropriate feedback within a reasonable time",
      h2_real_world:       "Match Between the System and the Real World — speak the user's language, not internal jargon",
      h3_user_control:     "User Control and Freedom — provide a clearly marked exit from unwanted states",
      h4_consistency:      "Consistency and Standards — follow platform and industry conventions",
      h5_error_prevention: "Error Prevention — prevent problems from occurring rather than relying on error messages",
      h6_recognition:      "Recognition Rather than Recall — make elements, actions, and options visible",
      h7_flexibility:      "Flexibility and Efficiency of Use — support both novice and expert users",
      h8_minimalism:       "Aesthetic and Minimalist Design — every extra unit competes with relevant units",
      h9_error_recovery:   "Help Users Recognize, Diagnose, and Recover from Errors — plain language, precise problem, constructive solution",
      h10_help:            "Help and Documentation — documentation should help users complete tasks, not explain bad design"
    }.freeze

    # Medium adapters — same heuristics applied to specific surfaces
    SIGNALS = {
      web: {
        h1_visibility:       { checks: %w[loading-indicator turbo:frame-missing offline-fallback],  failing: "No feedback during navigation or offline state" },
        h3_user_control:     { checks: %w[undo back-navigation escape-modal cancel],                failing: "No exit from modals or destructive actions" },
        h4_consistency:      { checks: %w[shared-layout stimulus-conventions semantic-html],        failing: "Component behavior diverges from shared baseline" },
        h5_error_prevention: { checks: %w[form-label aria-required input-type],                    failing: "Form fields lack <label> or aria-required" },
        h6_recognition:      { checks: %w[nav-visible primary-action icon-labels],                 failing: "Primary actions hidden or icon-only without labels" },
        h8_minimalism:       { checks: %w[information-density whitespace raw-primaries animation],  failing: "Visual noise: raw colors, unguarded animations, dense layout" },
        h9_error_recovery:   { checks: %w[flash error-format turbo-stream-error],                  failing: "Error messages generic or missing recovery path" }
      },
      cli: {
        h1_visibility:       { checks: %w[progress spinner result-line],  failing: "No output while operation is running — user cannot tell if system is working" },
        h2_real_world:       { checks: %w[plain-language no-jargon],      failing: "Output uses internal symbol names, not human-readable descriptions" },
        h8_minimalism:       { checks: %w[no-filler terse single-line],   failing: "Output contains filler phrases or multi-line where one line suffices" },
        h9_error_recovery:   { checks: %w[actionable-error suggestion],   failing: "Error output does not suggest a corrective action" }
      }
    }.freeze

    def self.cite(heuristic_key, violation, medium: :web)
      h = HEURISTICS.fetch(heuristic_key, heuristic_key.to_s)
      num = heuristic_key.to_s[/\d+/]
      "[Nielsen ##{num}#{h.split(' — ').first}] #{violation}"
    end

    def self.number(heuristic_key)
      heuristic_key.to_s[/\d+/].to_i
    end
  end
  end
  end
end

lib/ground/axioms/wcag.rb

# frozen_string_literal: true

module Master
  module Ground
  module Axioms
  module Wcag
    # WCAG 2.x success criteria relevant to web/mobile/CLI output
    # Reference: w3.org/WAI/WCAG21/quickref/
    # Universal: apply to any rendered surface, not just visual UIs.

    Criterion = Data.define(:id, :level, :name, :requirement)

    CRITERIA = [
      Criterion.new(id: "1.4.3",  level: :AA,  name: "Contrast (Minimum)",         requirement: "Text contrast >= 4.5:1 (normal), 3:1 (large text)"),
      Criterion.new(id: "1.4.4",  level: :AA,  name: "Resize Text",                requirement: "Text resizable to 200% without loss of content or function"),
      Criterion.new(id: "1.4.10", level: :AA,  name: "Reflow",                     requirement: "Content reflows at 320px width without horizontal scrolling"),
      Criterion.new(id: "1.4.11", level: :AA,  name: "Non-text Contrast",          requirement: "UI component contrast >= 3:1 against adjacent colors"),
      Criterion.new(id: "1.4.12", level: :AA,  name: "Text Spacing",               requirement: "No loss of content when letter/word/line spacing is increased"),
      Criterion.new(id: "1.4.13", level: :AA,  name: "Content on Hover or Focus",  requirement: "Hover/focus content dismissible, hoverable, persistent"),
      Criterion.new(id: "2.1.1",  level: :A,   name: "Keyboard",                   requirement: "All functionality operable via keyboard"),
      Criterion.new(id: "2.4.7",  level: :AA,  name: "Focus Visible",              requirement: "Keyboard focus indicator is visible"),
      Criterion.new(id: "2.5.3",  level: :A,   name: "Label in Name",              requirement: "Visible label text is part of the accessible name"),
      Criterion.new(id: "2.5.8",  level: :AA,  name: "Target Size (Minimum)",      requirement: "Touch target >= 24x24 CSS px (AA); 44x44 CSS px recommended (AAA)"),
      Criterion.new(id: "3.3.1",  level: :A,   name: "Error Identification",       requirement: "Input errors identified in text and described to the user"),
      Criterion.new(id: "3.3.2",  level: :A,   name: "Labels or Instructions",     requirement: "Labels or instructions provided for user input"),
      Criterion.new(id: "1.3.6",  level: :AAA, name: "Identify Purpose",           requirement: "UI components, icons, regions identified programmatically")
    ].freeze

    # Numeric thresholds extracted for use in audit rules
    TOUCH_TARGET_AA_PX  = 24
    TOUCH_TARGET_AAA_PX = 44
    CONTRAST_NORMAL     = 4.5
    CONTRAST_LARGE      = 3.0
    REFLOW_WIDTH_PX     = 320
    BODY_FONT_MIN_PX    = 16  # baseline for WCAG 1.4.4 reflow + readability
    LINE_HEIGHT_MIN     = 1.5 # WCAG 1.4.12 text spacing baseline

    def self.cite(criterion_id, violation)
      c = CRITERIA.find { |cr| cr.id == criterion_id }
      label = c ? "WCAG #{c.id} #{c.level}#{c.name}" : "WCAG #{criterion_id}"
      "[#{label}] #{violation}"
    end

    def self.find(criterion_id)
      CRITERIA.find { |c| c.id == criterion_id }
    end
  end
  end
  end
end

lib/ground/brain_overlay.rb

# frozen_string_literal: true

module Master
  module Ground
  class BrainOverlay
    CORE_FILES = %w[
      standing_orders.rb
      workflow_policy.rb
      evidence_base.rb
      tool_protocol.rb
      subagent_policy.rb
    ].freeze

    CONTEXTUAL_FILES = %w[
      policy_classifier.rb
      patch_verifier.rb
      done_checker.rb
      memory_search.rb
      context_compactor.rb
    ].freeze

    DEFAULT_MARKDOWN_DIRS = [
      File.join(Master::ROOT, "data", "claude"),
      File.join(Master::ROOT, ".master", "memory")
    ].freeze

    attr_reader :root

    def initialize(root: Master::ROOT, markdown_dirs: DEFAULT_MARKDOWN_DIRS)
      @root = root
      @markdown_dirs = markdown_dirs
    end

    def core_brief
      lines = ["Brain overlay: Ruby policy is authoritative; markdown is legacy/contextual unless explicitly requested."]
      lines << Master::Ground::ToolProtocol.brief if defined?(Master::Ground::ToolProtocol)
      lines << Master::Ground::EvidenceBase.brief if defined?(Master::Ground::EvidenceBase)
      lines << Master::Ground::WorkflowPolicy.brief if defined?(Master::Ground::WorkflowPolicy)
      lines.compact.join("\n\n")
    end

    def contextual_index
      markdown_files.map do |path|
        rel = relative(path)
        title = File.basename(path)
        "#{rel}#{title}"
      end
    end

    def load_context(name_or_pattern)
      pattern = name_or_pattern.to_s
      match = markdown_files.find { |path| relative(path).include?(pattern) || File.basename(path).include?(pattern) }
      return nil unless match

      File.read(match, encoding: "utf-8")
    end

    def reloadable?
      true
    end

    private

    def markdown_files
      @markdown_dirs.flat_map { |dir| Dir.glob(File.join(dir, "**", "*.md")) }.select { |path| File.file?(path) }
    end

    def relative(path)
      path.sub(%r{\A#{Regexp.escape(@root)}/?}, "")
    end
  end
  end
end

lib/ground/brutalist_minimalism.rb

# frozen_string_literal: true

module Master
  module Ground
  module BrutalistMinimalism
    NAME = "Brutalist-Minimalist Content-First".freeze
    PRIMARY_RULE = "If it does not improve readability, delete it.".freeze

    FORBIDDEN_EFFECTS = %w[
      shadows glows 3d_effects gradients animations transitions rounded_corners decorative_borders
    ].freeze

    FORBIDDEN_CSS = %w[
      box-shadow border-radius background-image transform filter
    ].freeze

    ALLOWED_CSS = %w[
      font-size font-weight margin padding line-height max-width display flex grid
    ].freeze

    SYSTEM_FONTS = {
      serif: "Georgia",
      sans_serif: "Arial/Helvetica"
    }.freeze

    METRICS = {
      css_bytes_max: 10_000,
      total_bytes_max: 10_000,
      line_length_min: 45,
      line_length_max: 75,
      line_length_ideal: 65,
      contrast_min: 4.5,
      load_time_2g_s: 2,
      lighthouse_target: 100
    }.freeze

    HTML_REQUIREMENTS = [
      "proper heading hierarchy",
      "meaningful alt text",
      "logical document flow",
      "accessible landmarks: main, nav, aside where appropriate",
      "screen-reader compatibility",
      "no styling-only divs",
      "no non-semantic markup",
      "no decorative elements"
    ].freeze

    ANTI_PATTERNS = [
      "visual metaphors",
      "smooth interactions",
      "aesthetic comfort",
      "decorative containers",
      "loading animations",
      "splash screens",
      "hero banners"
    ].freeze

    PROMPTS = {
      design_brief: "Create a brutally minimal website that prioritizes content over decoration.",
      development_constraint: "Use system fonts, single column layout, max 65 characters per line, no visual effects.",
      content_hierarchy: "Establish hierarchy through font size, weight, semantic headings, and spacing only.",
      performance_mandate: "HTML plus CSS under 10KB total; every byte must serve content delivery."
    }.freeze

    def self.brief
      <<~TEXT.strip
        #{NAME} policy:
        - #{PRIMARY_RULE}
        - black text on white background; no palette beyond black and white unless required for state.
        - system fonts only: serif=#{SYSTEM_FONTS[:serif]}, sans=#{SYSTEM_FONTS[:sans_serif]}; no web fonts.
        - single-column, content-first layout; 45-75ch lines, ideal #{METRICS[:line_length_ideal]}ch.
        - forbidden visual effects: #{FORBIDDEN_EFFECTS.join(', ')}.
        - forbidden CSS properties: #{FORBIDDEN_CSS.join(', ')}.
        - HTML must be semantic, accessible, heading-ordered, and screen-reader friendly.
        - JavaScript is forbidden unless critical; external resources are forbidden unless essential.
        - target: sub-#{METRICS[:load_time_2g_s]}s on 2G, HTML+CSS under #{METRICS[:total_bytes_max]} bytes, Lighthouse 100/100/100/100.
      TEXT
    end
  end
  end
end

lib/ground/checkpoint.rb

# frozen_string_literal: true

require "fileutils"
require "json"
require "time"

module Master
  module Ground
  class Checkpoint
    attr_reader :root, :dir

    def initialize(root: Master::ROOT, dir: File.join(Master::ROOT, ".master", "checkpoints"))
      @root = root
      @dir = dir
    end

    def create(label:, files: [])
      id = "#{Time.now.utc.strftime('%Y%m%d%H%M%S')}-#{slug(label)}"
      target = File.join(dir, id)
      FileUtils.mkdir_p(target)
      Array(files).each do |rel|
        src = File.join(root, rel.to_s)
        next unless File.file?(src)

        dst = File.join(target, rel.to_s)
        FileUtils.mkdir_p(File.dirname(dst))
        FileUtils.cp(src, dst)
      end
      manifest = { id: id, label: label, files: Array(files), created_at: Time.now.utc.iso8601 }
      File.write(File.join(target, "manifest.json"), JSON.pretty_generate(manifest))
      manifest
    end

    def list
      Dir.glob(File.join(dir, "*", "manifest.json")).filter_map do |path|
        JSON.parse(File.read(path, encoding: "utf-8"))
      rescue JSON::ParserError
        nil
      end.sort_by { |row| row.fetch("created_at", "") }
    end

    def restore(id)
      manifest_path = File.join(dir, id.to_s, "manifest.json")
      raise ArgumentError, "checkpoint not found: #{id}" unless File.file?(manifest_path)

      manifest = JSON.parse(File.read(manifest_path, encoding: "utf-8"))
      Array(manifest["files"]).each do |rel|
        src = File.join(dir, id.to_s, rel.to_s)
        next unless File.file?(src)

        dst = File.join(root, rel.to_s)
        FileUtils.mkdir_p(File.dirname(dst))
        FileUtils.cp(src, dst)
      end
      manifest
    end

    private

    def slug(text)
      text.to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|\-\z/, "")[0, 48]
    end
  end
  end
end

lib/ground/config.rb

# frozen_string_literal: true

require "yaml"
require "fileutils"

module Master
  module Ground
  class Config
    BUDGET_MAX_DEFAULT = 10.0
    HISTORY_MAX = 500
    DEFAULT_WEB_PORT = 53_187

    DEFAULTS = {
      "model" => "claude-opus-4-7",
      "web_host" => "127.0.0.1",
      "web_public_url" => "https://ai.brgen.no",
      "web_port" => DEFAULT_WEB_PORT,
      "budget_max" => BUDGET_MAX_DEFAULT,
      "req_max" => 60.0,
      "trace" => 0,
      "prescan" => true,
      "auto" => false,
      "cache_ttl" => 3_600,
      "history_max" => 500,
      "reasoning_mode" => "direct",
      "task_type" => "code_generation",
      "auto_testing" => false
    }.freeze

    def initialize(root = Dir.pwd)
      @root = root
      @path = File.join(root, ".master", "config.yml")
      @mutex = Mutex.new
      @data = load_config
    end

    def [](key) = @mutex.synchronize { @data[key.to_s] }
    def []=(key, value); @mutex.synchronize { @data[key.to_s] = value }; end
    def dig(key, *rest) = @mutex.synchronize { k = key.to_s; rest.empty? ? @data[k] : @data.dig(k, *rest) }

    def model = self["model"]
    def budget_max = self["budget_max"].to_f
    def req_max = self["req_max"].to_f
    def trace = (ENV["MASTER_TRACE"] || self["trace"]).to_i
    def prescan? = self["prescan"] == true
    def auto? = self["auto"] == true
    def reasoning_mode = self["reasoning_mode"].to_s
    def task_type = self["task_type"].to_s
    def auto_testing? = self["auto_testing"] == true

    include AtomicWrite

    def save!
      FileUtils.mkdir_p(File.dirname(@path))
      write_atomic(@path, @data.to_yaml, fsync: true)
    end

    def reload!
      @mutex.synchronize { @data = load_config }
    end

    # Frozen snapshot of read-only boot values — safe to share across threads.
    BootConfig = Data.define(:root, :model, :web_host, :web_port, :web_public_url,
                             :budget_max, :req_max, :cache_ttl, :history_max)

    def freeze_boot
      snap = @mutex.synchronize { @data.dup }
      BootConfig.new(
        root: @root, model: snap["model"], web_host: snap["web_host"], web_port: snap["web_port"].to_i,
        web_public_url: snap["web_public_url"], budget_max: snap["budget_max"].to_f,
        req_max: snap["req_max"].to_f, cache_ttl: snap["cache_ttl"].to_i, history_max: snap["history_max"].to_i
      ).freeze
    end

    def to_h = @mutex.synchronize { deep_dup(@data) }

    private

    def load_config
      defaults = deep_dup(DEFAULTS)
      return defaults unless File.exist?(@path)
      raw    = Master.load_yaml(@path)
      loaded = raw.is_a?(Hash) ? raw : {}
      deep_merge(defaults, stringify_keys(loaded))
    rescue Psych::Exception => e
      warn "config: failed to parse #{@path}: #{e.message}"
      defaults
    end

    def deep_merge(base, overlay)
      base.merge(overlay) do |_key, old_val, new_val|
        old_val.is_a?(Hash) && new_val.is_a?(Hash) ? deep_merge(old_val, new_val) : new_val
      end
    end

    def stringify_keys(hash)
      hash.each_with_object({}) do |(k, v), h|
        h[k.to_s] = v.is_a?(Hash) ? stringify_keys(v) : v
      end
    end

    def deep_dup(obj)
      case obj
      when Hash then obj.each_with_object({}) { |(k, v), h| h[k] = deep_dup(v) }
      when Array then obj.map { |v| deep_dup(v) }
      when Numeric, Symbol, TrueClass, FalseClass, NilClass then obj
      else
        obj.respond_to?(:dup) ? (obj.dup rescue obj) : obj
      end
    end
  end
  end
end

lib/ground/constitution.rb

# frozen_string_literal: true

module Master
  module Ground
  class Constitution
    DIR = File.join(Master::ROOT, "data", "principles").freeze
    MAX_PRINCIPLES = 40
    MAX_BODY_CHARS = 320

    def initialize(dir: DIR)
      @dir = dir
      @principles = load
    end

    def empty? = @principles.empty?

    def system_prompt
      return nil if @principles.empty?
      lines = @principles.map { |p| "- #{p[:name]}: #{p[:body]}" }
      "Constitutional principles (operator-declared, override defaults):\n#{lines.join("\n")}"
    end

    def list
      @principles.map { |p| "#{p[:type]}: #{p[:name]}#{p[:description]}" }
    end

    def reload!
      @principles = load
      self
    end

    private

    def load
      return [] unless File.directory?(@dir)
      Dir.glob(File.join(@dir, "*.md")).sort.filter_map { |f| parse(f) }.first(MAX_PRINCIPLES)
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "constitution.load", dir: @dir)
      []
    end

    def parse(path)
      fm = Master::Ground::Frontmatter.parse_file(path)
      return nil if fm[:meta].empty?
      meta = fm[:meta]
      {
        name:        meta["name"].to_s,
        description: meta["description"].to_s,
        type:        meta["type"].to_s,
        body:        fm[:body][0, MAX_BODY_CHARS]
      }
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "constitution.parse", path:)
      nil
    end
  end
  end
end

lib/ground/context_provider.rb

# frozen_string_literal: true

module Master
  module Ground
  class ContextProvider
    PROVIDERS = %i[repo_map memory_search brain_overlay current_files rails_pwa_files].freeze

    RAILS_PWA_QUERY_TERMS = %w[rails pwa manifest service_worker hotwire turbo stimulus mobile audit].freeze

    def initialize(root: Master::ROOT)
      @root = root
    end

    def gather(query:, providers: PROVIDERS, limit: 20)
      providers.flat_map { |p| dispatch_provider(p, query, limit) }.compact
    end

    def dispatch_provider(provider, query, limit)
      case provider.to_sym
      when :repo_map        then repo_map(query, limit)
      when :memory_search   then memory_search(query, limit)
      when :brain_overlay   then brain_overlay
      when :current_files   then current_files(query, limit)
      when :rails_pwa_files then rails_pwa_files(query, limit)
      else []
      end
    end

    def brief(query, limit: 10)
      gather(query: query, limit: limit).map { |row| "#{row[:source]}: #{row[:text]}" }
    end

    private

    def repo_map(query, limit)
      return [] unless defined?(Master::Ground::RepoMap)

      Master::Ground::RepoMap.new(root: @root).relevant(query, limit: limit).map do |entry|
        { source: :repo_map, path: entry.path, text: "#{entry.path} #{entry.language} #{entry.bytes}B" }
      end
    end

    def memory_search(query, limit)
      return [] unless defined?(Master::Ground::MemorySearch)

      Master::Ground::MemorySearch.new.search(query, limit: limit).map do |doc|
        { source: :memory, path: doc["path"], text: "#{doc['path']} #{doc['title']} score=#{format('%.2f', doc['score'])}" }
      end
    rescue StandardError
      []
    end

    def brain_overlay
      return [] unless defined?(Master::Ground::BrainOverlay)

      overlay = Master::Ground::BrainOverlay.new(root: @root)
      [{ source: :brain_overlay, path: nil, text: overlay.core_brief[0, 800] }]
    rescue StandardError
      []
    end

    def rails_pwa_files(query, limit)
      terms = query.to_s.downcase.scan(/[a-z0-9_]+/)
      return [] unless (terms & RAILS_PWA_QUERY_TERMS).any?

      deploy_rails = File.expand_path("../../DEPLOY/rails", @root)
      return [] unless Dir.exist?(deploy_rails)

      patterns = %w[
        app/views/pwa/manifest.json.erb
        app/views/pwa/service-worker.js
        app/javascript/application.js
        config/routes.rb
        config/importmap.rb
      ]

      apps = Dir.entries(deploy_rails).reject { |e| e.start_with?(".", "_") }
      rows = apps.flat_map { |app| pwa_rows_for_app(app, deploy_rails, patterns) }
      rows.first(limit)
    end

    def pwa_rows_for_app(app, deploy_rails, patterns)
      patterns.filter_map do |rel|
        path = File.join(deploy_rails, app, rel)
        next unless File.exist?(path)
        { source: :rails_pwa, path: "DEPLOY/rails/#{app}/#{rel}", text: "#{app}/#{rel}" }
      end
    end

    def current_files(query, limit)
      terms = query.to_s.downcase.scan(/[a-z0-9_\-]+/)
      return [] if terms.empty?

      paths = Dir.glob(File.join(@root, "lib", "**", "*.rb")).select { |p| matches_terms?(p, terms) }
      paths.first(limit).map { |path| current_files_row(path) }
    end

    def matches_terms?(path, terms)
      rel = path.sub(%r{\A#{Regexp.escape(@root)}/?}, "")
      terms.any? { |term| rel.downcase.include?(term) }
    end

    def current_files_row(path)
      rel = path.sub(%r{\A#{Regexp.escape(@root)}/?}, "")
      { source: :current_files, path: rel, text: rel }
    end
  end
  end
end

lib/ground/done_checker.rb

# frozen_string_literal: true

module Master
  module Ground
  class DoneChecker
    REQUIRED_KEYS = %i[files symbols callers].freeze

    def initialize(root: Master::ROOT, verifier: PatchVerifier.new(root: root))
      @root = root
      @verifier = verifier
    end

    def call(plan)
      normalized = normalize(plan)
      checks = @verifier.verify(
        files: normalized.fetch(:files),
        symbols: normalized.fetch(:symbols),
        references: normalized.fetch(:callers)
      )
      {
        done: @verifier.ok?(checks),
        checks: checks,
        report: @verifier.report(checks),
        missing_plan_keys: REQUIRED_KEYS - normalized.keys
      }
    end

    def self.done?(plan, root: Master::ROOT)
      new(root: root).call(plan).fetch(:done)
    end

    private

    def normalize(plan)
      hash = plan.respond_to?(:to_h) ? plan.to_h : {}
      {
        files: Array(hash[:files] || hash["files"]),
        symbols: Array(hash[:symbols] || hash["symbols"]),
        callers: hash[:callers] || hash["callers"] || {}
      }
    end
  end
  end
end

lib/ground/evidence_base.rb

# frozen_string_literal: true

module Master
  module Ground
  module EvidenceBase
    THRESHOLDS = {
      clone_similarity: 0.70,
      duplicate_min_tokens: 100,
      semantic_clone_f1_target: 0.90,
      api_timeout_s: 600,
      connect_timeout_s: 5,
      retry_attempts: 3,
      max_backoff_s: 20,
      backoff_base_s: 0.1,
      circuit_min_requests: 20,
      circuit_error_rate: 0.50,
      circuit_sleep_window_ms: 5_000,
      circuit_rolling_window_ms: 10_000,
      rate_limit_cooldown_s: 60,
      attention_sink_tokens: 4,
      prompt_compression_ratio: 20,
      rag_chunk_tokens: 512,
      rag_factoid_chunk_tokens: 256,
      rag_analytical_chunk_tokens: 1_024,
      rag_overlap: 0.15,
      self_consistency_samples: 7,
      semantic_entropy_samples: 5,
      ast_chunk_nodes: 175,
      diff_context_lines: 5,
      permission_order: %i[pre_tool_hook deny allow ask mode_check]
    }.freeze

    ENFORCEMENT_ORDER = %i[
      security_veto
      permission_authorization
      input_validation_normalization
      quality_style
      output_verification
    ].freeze

    OUTPUT_POLICY = {
      edit_format: :unified_diff,
      avoid_line_numbers: true,
      prefer_function_level_edits: true,
      continue_on_finish_reason_length: true,
      preserve_system_message: true
    }.freeze

    VERIFICATION_POLICY = {
      chain_of_verification: %i[baseline verification_questions independent_check final_revision],
      semantic_entropy: { samples: THRESHOLDS[:semantic_entropy_samples], cluster_by: :meaning },
      self_consistency: { samples: THRESHOLDS[:self_consistency_samples], vote: :majority },
      swe_bench_style: %i[fail_to_pass pass_to_pass regression]
    }.freeze

    def self.threshold(name)
      THRESHOLDS.fetch(name.to_sym)
    end

    def self.brief
      <<~TEXT.strip
        Evidence-based assistant policy:
        - duplication: 70% similarity, 100+ tokens, abstract on 3rd occurrence; avoid hasty abstractions.
        - resilience: 600s request timeout, 5s connect timeout, 2-3 retries, jittered exponential backoff capped at 20s, 50% circuit-breaker threshold.
        - context: keep 4 attention-sink tokens, use 512-token balanced RAG chunks with 10-20% overlap, compress up to 20x when needed.
        - verification: 5 semantic-entropy samples, 5-10 self-consistency paths, Chain-of-Verification before high-stakes claims.
        - output: unified diffs, no line-number dependence, function-level hunks, continue on truncation.
        - permissions: deny beats allow; security veto -> authorization -> normalization -> quality -> output verification.
      TEXT
    end
  end
  end
end

lib/ground/frontmatter.rb

# frozen_string_literal: true

require "yaml"

module Master
  module Ground
    # YAML frontmatter parser for markdown files.
    # Returns {meta: Hash, body: String}; empty meta when no frontmatter or malformed.
    module Frontmatter
      MARKER = "---"
      RE     = /\A---\n(.*?)\n---\n?(.*)/m

      module_function

      def parse(raw)
        s = raw.to_s
        return { meta: {}, body: s.strip } unless s.start_with?(MARKER)
        m = s.match(RE)
        return { meta: {}, body: s.strip } unless m
        meta = begin; YAML.safe_load(m[1]) || {}; rescue Psych::Exception; {}; end
        { meta: meta, body: m[2].strip }
      end

      def parse_file(path)
        parse(File.read(path, encoding: "UTF-8"))
      end
    end
  end
end

lib/ground/intent_router.rb

# frozen_string_literal: true

module Master
  module Ground
  class IntentRouter
    INTENTS = {
      codify_policy:          %w[codify policy make it ruby turn into],
      refactor_to_ruby:       %w[refactor move ruby extract simplify],
      wire_existing_module:   %w[wire connect hook up route plug],
      create_facade:          %w[facade wrap adapter interface],
      delete_redundant_config: %w[delete remove kill purge drop],
      verify_patch_landed:    %w[verify check confirm did it land],
      run_sound_review:       %w[sound review audio critique listen],
      run_ui_review:          %w[ui critique design visual look review],
      generate_rails_pwa:     %w[generate create new rails pwa app scaffold blank],
      refactor_rails_app:     %w[refactor upgrade migrate modernize rails hotwire turbo stimulus],
      redesign_mobile_pwa:    %w[redesign mobile touch layout responsive accessibility],
      audit_rails_pwa:        %w[audit scan check pwa manifest service worker rails],
      apply_user_style_rules: %w[style format lint clean],
      continue_prior_plan:    %w[go ahead continue land it proceed ship],
      write_repo_changes:     %w[land write commit save push apply],
      prefer_ruby:            %w[ruby not markdown no yaml keep ruby]
    }.freeze

    STANDING_SEMANTICS = {
      "go ahead" => :continue_prior_plan,
      "land it" => :write_repo_changes,
      "codify" => :codify_policy,
      "wire" => :wire_existing_module,
      "verify" => :verify_patch_landed,
      "yes" => :continue_prior_plan,
      "ship" => :write_repo_changes,
      "proceed" => :continue_prior_plan
    }.freeze

    RISK_TIERS = {
      low:      %i[codify_policy refactor_to_ruby create_facade apply_user_style_rules run_sound_review run_ui_review audit_rails_pwa generate_rails_pwa redesign_mobile_pwa],
      medium:   %i[wire_existing_module verify_patch_landed continue_prior_plan prefer_ruby refactor_rails_app],
      high:     %i[write_repo_changes delete_redundant_config],
      critical: []
    }.freeze

    def classify(text)
      downcased = text.to_s.downcase.strip
      return STANDING_SEMANTICS[downcased] if STANDING_SEMANTICS.key?(downcased)

      tokens = downcased.scan(/\w+/)
      scores = INTENTS.transform_values do |keywords|
        tokens.count { |t| keywords.any? { |k| t.include?(k) } }
      end
      best, score = scores.max_by { |_, v| v }
      score.positive? ? best : :unknown
    end

    def risk(intent)
      RISK_TIERS.each { |tier, intents| return tier if intents.include?(intent) }
      :medium
    end

    def route(text)
      intent = classify(text)
      { intent: intent, risk: risk(intent) }
    end
  end
  end
end

lib/ground/knowledge_store.rb

# frozen_string_literal: true

require "sqlite3"
require "json"

module Master
  module Ground
  # Unified knowledge ledger — consolidates fix quality (arch #10), strategy
  # outcomes, and RSI feedback events into one WAL-mode SQLite database.
  #
  # Replaces the split Ground::Learnings (JSONL) + Persistence::SqliteLearnings (SQLite).
  # Single-object `learnings:` kwarg serves the RuleLoop caller path:
  #   record(trigger:, strategy:, outcome:)  → strategy_outcomes table
  #   record(rule:, file_type:, outcome:)    → fix_outcomes table
  class KnowledgeStore
    include Master::Ground::Persistence::SqliteStore
    DEFAULT_PATH        = ".master/knowledge.sqlite3"
    QUALITY_WINDOW_DAYS = 30
    RSI_WINDOW_DAYS     = 7
    RSI_FAIL_THRESHOLD  = 0.20
    RSI_CORRECTION_MIN  = 3
    RSI_PROVIDER_MIN    = 3

    def initialize(root:)
      @db = open_db(root)
      @db.results_as_hash = true
      ensure_schema
    end

    # Unified dispatch — keyword args determine which table is written.
    def record(trigger: nil, strategy: nil, rule: nil, file_type: nil, outcome:)
      if rule
        record_fix(rule: rule, file_type: file_type, outcome: outcome)
      elsif trigger
        record_strategy(trigger: trigger, strategy: strategy || "unknown", outcome: outcome)
      end
    end

    # Fix quality tracking (arch #10) — outcome: :fixed | :stuck
    def record_fix(rule:, file_type:, outcome:)
      @db.execute(
        "INSERT INTO fix_outcomes (ts, rule, file_type, outcome) VALUES (?, ?, ?, ?)",
        [Time.now.to_i, rule.to_s, file_type.to_s, outcome.to_s]
      )
    rescue SQLite3::Exception => e
      warn "knowledge_store: #{e.message}"
    end

    # Fix quality score 0.0–1.0 for a rule. Default 0.5 when no data.
    def fix_quality(rule:, file_type: nil)
      cutoff = Time.now.to_i - QUALITY_WINDOW_DAYS * 86_400
      rows = if file_type
        @db.execute(
          "SELECT outcome, COUNT(*) AS n FROM fix_outcomes WHERE rule = ? AND file_type = ? AND ts >= ? GROUP BY outcome",
          [rule.to_s, file_type.to_s, cutoff]
        )
      else
        @db.execute(
          "SELECT outcome, COUNT(*) AS n FROM fix_outcomes WHERE rule = ? AND ts >= ? GROUP BY outcome",
          [rule.to_s, cutoff]
        )
      end
      tally = rows.each_with_object(Hash.new(0)) { |r, h| h[r["outcome"]] = r["n"].to_i }
      total = tally.values.sum
      return 0.5 if total.zero?
      tally["fixed"].to_f / total
    end

    def top_rules(limit: 20, min_attempts: 3)
      cutoff = Time.now.to_i - QUALITY_WINDOW_DAYS * 86_400
      @db.execute(<<~SQL, [cutoff, min_attempts, limit])
        SELECT rule,
               SUM(CASE WHEN outcome = 'fixed' THEN 1 ELSE 0 END) AS fixed,
               COUNT(*) AS total,
               CAST(SUM(CASE WHEN outcome = 'fixed' THEN 1 ELSE 0 END) AS REAL) / COUNT(*) AS quality
        FROM fix_outcomes WHERE ts >= ?
        GROUP BY rule HAVING total >= ?
        ORDER BY quality DESC LIMIT ?
      SQL
    end

    # Strategy outcome tracking — autoloop fix records
    def record_strategy(trigger:, strategy:, outcome:)
      ts = Time.now.to_i
      existing = @db.execute(
        "SELECT id, reuse_count, confidence FROM strategy_outcomes WHERE trigger = ? AND strategy = ?",
        [trigger.to_s, strategy.to_s]
      ).first
      if existing
        new_confidence = [existing["confidence"].to_f + 0.05, 1.0].min
        @db.execute(
          "UPDATE strategy_outcomes SET reuse_count = reuse_count + 1, confidence = ?, outcome = ?, ts = ? WHERE id = ?",
          [new_confidence, outcome.to_s, ts, existing["id"]]
        )
      else
        confidence = outcome.to_s == "fixed" ? 0.7 : 0.4
        @db.execute(
          "INSERT INTO strategy_outcomes (ts, trigger, strategy, outcome, confidence, reuse_count) VALUES (?, ?, ?, ?, ?, 0)",
          [ts, trigger.to_s, strategy.to_s, outcome.to_s, confidence]
        )
      end
    rescue SQLite3::Exception => e
      warn "knowledge_store: #{e.message}"
    end

    def search(trigger_fragment, limit: 3)
      fragment = "%#{trigger_fragment.to_s.downcase}%"
      @db.execute(
        "SELECT trigger, strategy, outcome, confidence FROM strategy_outcomes WHERE LOWER(trigger) LIKE ? AND outcome != 'failed' ORDER BY confidence DESC LIMIT ?",
        [fragment, limit]
      )
    end

    # RSI feedback events — dimensional statistics for opportunity detection
    def record_event(event_type:, dimension:, value: nil, metadata: nil)
      @db.execute(
        "INSERT INTO feedback_events (ts, event_type, dimension, value, metadata) VALUES (?, ?, ?, ?, ?)",
        [Time.now.to_i, event_type.to_s, dimension.to_s, value&.to_s, metadata&.to_json]
      )
    rescue SQLite3::Exception => e
      warn "knowledge_store: #{e.message}"
    end

    def opportunities
      cutoff = Time.now.to_i - RSI_WINDOW_DAYS * 86_400
      recent = @db.execute("SELECT event_type, dimension FROM feedback_events WHERE ts >= ?", [cutoff])

      tool_stats = recent.select { |r| %w[tool_success tool_failure].include?(r["event_type"]) }
                         .group_by { |r| r["dimension"] }
                         .filter_map { |tool, evs|
        success = evs.count { |e| e["event_type"] == "tool_success" }
        failure = evs.count { |e| e["event_type"] == "tool_failure" }
        total   = success + failure
        rate    = total.zero? ? 0.0 : failure.to_f / total
        { category: :high_failure, dimension: tool, fail_rate: rate.round(3), total: } if rate >= RSI_FAIL_THRESHOLD && total >= 3
      }

      corrections = recent.select { |r| r["event_type"] == "user_correction" }
                          .group_by { |r| r["dimension"] }
                          .filter_map { |dim, evs|
        { category: :repeated_correction, dimension: dim, count: evs.size } if evs.size >= RSI_CORRECTION_MIN
      }

      provider_errs = recent.select { |r| r["event_type"] == "provider_error" }
                            .group_by { |r| r["dimension"] }
                            .filter_map { |dim, evs|
        { category: :provider_errors, dimension: dim, count: evs.size } if evs.size >= RSI_PROVIDER_MIN
      }

      tool_stats + corrections + provider_errs
    end

    def close
      @db&.close
    end

    private

    def open_db(root)
      open_sqlite(root, DEFAULT_PATH)
    end

    def ensure_schema
      @db.execute_batch(<<~SQL)
        CREATE TABLE IF NOT EXISTS fix_outcomes (
          id        INTEGER PRIMARY KEY AUTOINCREMENT,
          ts        INTEGER NOT NULL,
          rule      TEXT NOT NULL,
          file_type TEXT NOT NULL,
          outcome   TEXT NOT NULL CHECK (outcome IN ('fixed', 'stuck'))
        );
        CREATE INDEX IF NOT EXISTS idx_fix_rule ON fix_outcomes(rule);
        CREATE INDEX IF NOT EXISTS idx_fix_ts   ON fix_outcomes(ts);

        CREATE TABLE IF NOT EXISTS strategy_outcomes (
          id          INTEGER PRIMARY KEY AUTOINCREMENT,
          ts          INTEGER NOT NULL,
          trigger     TEXT NOT NULL,
          strategy    TEXT NOT NULL,
          outcome     TEXT NOT NULL,
          confidence  REAL NOT NULL DEFAULT 0.5,
          reuse_count INTEGER NOT NULL DEFAULT 0,
          UNIQUE(trigger, strategy)
        );
        CREATE INDEX IF NOT EXISTS idx_strat_trigger ON strategy_outcomes(trigger);

        CREATE TABLE IF NOT EXISTS feedback_events (
          id         INTEGER PRIMARY KEY AUTOINCREMENT,
          ts         INTEGER NOT NULL,
          event_type TEXT NOT NULL,
          dimension  TEXT NOT NULL,
          value      TEXT,
          metadata   TEXT
        );
        CREATE INDEX IF NOT EXISTS idx_fb_ts        ON feedback_events(ts);
        CREATE INDEX IF NOT EXISTS idx_fb_dimension ON feedback_events(dimension);
      SQL
    end
  end
  end
end

lib/ground/memory.rb

# frozen_string_literal: true

require "yaml"
require "fileutils"

module Master
  module Ground
  # Memory — persistent cross-session store with TF-IDF semantic search.
  # Stored at .master/memory.yml. Survives restarts.
  class Memory
    module Search
      def semantic_recall(query, top_n: 3)
        store_snap = @mutex.synchronize { @store.dup }
        return [] if store_snap.empty?
        if Judge::Embeddings.enabled? && (qvec = Judge::Embeddings.embed(query))
          hits = vector_recall(qvec, top_n, store_snap)
          return hits unless hits.empty?
        end
        tfidf_recall(query, top_n, store_snap)
      end

      private

      def vector_recall(qvec, top_n, store)
        store.filter_map do |key, data|
          next unless data.is_a?(Hash) && data["vec"].is_a?(Array)
          score = Judge::Embeddings.cosine(qvec, data["vec"])
          next if score < Judge::Embeddings::MIN_SIM
          { key: key, value: data["value"].to_s, score: score }
        end.sort_by { |e| -e[:score] }.first(top_n)
      end

      def tfidf_recall(query, top_n, store)
        terms = tokenize(query)
        return [] if terms.empty?
        store.filter_map { |key, data|
          value = data.is_a?(Hash) ? data["value"].to_s : data.to_s
          score = tfidf_score(terms, tokenize("#{key} #{value}"))
          next if score.zero?
          { key: key, value: value, score: score }
        }.sort_by { |e| -e[:score] }.first(top_n)
      end

      def tokenize(text) = text.downcase.scan(/\b[a-z]{2,}\b/)

      def tfidf_score(query_terms, doc_terms)
        return 0.0 if doc_terms.empty?
        freq = doc_terms.tally
        query_terms.sum { |t| Math.log(1.0 + freq.fetch(t, 0).to_f) }
      end
    end
    TTL_DAYS = 90
    CONSOLIDATE_THRESHOLD = 40
    SECONDS_PER_DAY = 86_400
    MAX_INJECT_TOKENS = 2000
    MAX_INJECT_ENTRIES = 5
    TYPES = %w[user feedback project reference general].freeze
    AUTO_SAVE_PATTERNS = {
      "user" => /\b(?:i'?m a|i am a|my role is|i work as)\s+([^.,;\n]{3,80})/i,
      "feedback" => /\b(?:don'?t|stop|never|always|prefer|from now on)\s+([^.,;\n]{3,120})/i,
      "project" => /\b(?:we'?re|deadline|launching|deploying|migrating)\s+([^.,;\n]{3,120})/i
    }.freeze

    include Search
    include AtomicWrite

    def initialize(root: Dir.pwd)
      @root  = root
      @path  = File.join(root, ".master", "memory.yml")
      @mutex = Mutex.new
      @store = load_store
      import_external!
    end

    def remember(key, value, type: "general")
      type = TYPES.include?(type.to_s) ? type.to_s : "general"
      @mutex.synchronize do
        prune_stale! if @store.size > CONSOLIDATE_THRESHOLD
        entry = { "value" => value.to_s, "ts" => Time.now.to_i, "type" => type }
        if (vec = Judge::Embeddings.embed("#{key} #{value}"))
          entry["vec"] = vec
        end
        @store[key.to_s] = entry
        persist
      end
    end

    def by_type(type)
      @mutex.synchronize { @store.select { |k, v| v.is_a?(Hash) && v["type"] == type.to_s && !k.start_with?("archive/") } }
    end

    def type_counts
      @mutex.synchronize do
        counts = Hash.new(0)
        @store.each do |k, v|
          next if k.start_with?("archive/") || k == "_consolidated_summary"
          counts[v.is_a?(Hash) ? (v["type"] || "general") : "general"] += 1
        end
        counts
      end
    end

    def auto_save(text)
      return if text.to_s.empty?
      AUTO_SAVE_PATTERNS.each do |type, re|
        next unless (m = text.match(re))
        snippet = m[1].strip
        next if snippet.length < 3
        n   = @mutex.synchronize { @store.keys.count { |k| k.start_with?("auto/#{type}/") } } + 1
        key = "auto/#{type}/#{n}"
        remember(key, snippet, type: type)
        return key
      end
      nil
    end

    def recall(key)
      @mutex.synchronize { @store.dig(key.to_s, "value") }
    end

    def forget(key)
      @mutex.synchronize { @store.delete(key.to_s); persist }
    end

    def all = @mutex.synchronize { @store.transform_values { |v| v.is_a?(Hash) ? v["value"] : v } }

    def context_summary
      active = @mutex.synchronize { @store.reject { |k, _| k.to_s.start_with?("archive/") || k == "_consolidated_summary" } }
      return if active.empty?

      grouped = active.group_by { |_, v| v.is_a?(Hash) ? (v["type"] || "general") : "general" }
      ordered = TYPES.flat_map { |t| (grouped[t] || []).sort_by { |_, v| -(v.is_a?(Hash) ? v["ts"].to_i : 0) } }
                     .first(MAX_INJECT_ENTRIES * 2)
      lines, token_sum, current_type = [], 0, nil

      ordered.each do |k, v|
        type = v.is_a?(Hash) ? (v["type"] || "general") : "general"
        if type != current_type
          lines << "[#{type}]"
          current_type = type
        end
        text = "- #{k}: #{v.is_a?(Hash) ? v["value"] : v}"
        est  = text.bytesize / Master::Trace::Session::TOKENS_PER_CHAR
        break if token_sum + est > MAX_INJECT_TOKENS
        lines << text
        token_sum += est
      end
      return if lines.empty?

      archived_n = @mutex.synchronize { @store.count { |k, _| k.to_s.start_with?("archive/") } }
      summary    = recall("_consolidated_summary")
      header     = summary ? "Memory (#{summary.to_s[0, 80]}):" : "Memory:"
      header    += " [+#{archived_n} archived]" if archived_n > 0
      "#{header}\n#{lines.join("\n")}"
    end

    # Three-phase consolidation: light (score), deep (archive), REM (LLM summary).
    def consolidate!(agent: nil)
      return "nothing to consolidate" if @store.empty?

      now = Time.now.to_i
      entries = nil
      archived = 0

      @mutex.synchronize do
        entries = @store.reject { |k, _| k.to_s.start_with?("archive/") }
        scored  = entries.map do |key, data|
          ts    = data.is_a?(Hash) ? data["ts"].to_i : 0
          age_d = (now - ts) / 86_400.0
          { key: key, score: 1.0 / (1.0 + age_d / TTL_DAYS.to_f) }
        end
        scored.each do |entry|
          next if entry[:key] == "_consolidated_summary"
          next unless entry[:score] < 0.33
          @store["archive/#{entry[:key]}"] = @store.delete(entry[:key])
          archived += 1
        end
        persist
      end

      if agent
        active_text = @mutex.synchronize do
          @store
            .reject { |k, _| k.to_s.start_with?("archive/") || k == "_consolidated_summary" }
            .map    { |k, v| "#{k}: #{v.is_a?(Hash) ? v["value"] : v}" }
            .join("\n")
        end
        unless active_text.strip.empty?
          summary = agent.ask_once("Summarize in 2 concise sentences, preserving all key facts:\n#{active_text}")
          remember("_consolidated_summary", summary.strip)
        end
      end

      "dreaming: #{entries.size} entries checked, #{archived} archived"
    rescue StandardError => e
      "consolidation error: #{e.message}"
    end

    private

    # Imports markdown memory files from data/claude/ on first boot.
    # Each file's frontmatter type maps to MASTER's memory type; body becomes the value.
    def import_external!
      dir = File.join(@root, "data", "claude")
      return unless Dir.exist?(dir)
      Dir.glob(File.join(dir, "*.md")).each do |path|
        next if File.basename(path) == "MEMORY.md"
        key = "claude/#{File.basename(path, ".md")}"
        next if @store.key?(key)
        type, body = parse_frontmatter(path)
        next if body.empty?
        remember(key, body, type: type)
      end
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "memory.preload_claude_md")
    end

    def parse_frontmatter(path)
      fm = Master::Ground::Frontmatter.parse_file(path)
      type = fm[:meta]["type"].to_s
      type = "general" if type.empty?
      [type, fm[:body]]
    end

    def prune_stale!
      cutoff = Time.now.to_i - TTL_DAYS * SECONDS_PER_DAY
      @store.each do |k, v|
        next if k.to_s.start_with?("archive/") || k == "_consolidated_summary"
        ts = v.is_a?(Hash) ? v["ts"].to_i : 0
        next unless ts > 0 && ts < cutoff
        @store["archive/#{k}"] = @store.delete(k)
      end
    end

    def load_store
      return {} unless File.exist?(@path)
      loaded = Master.load_yaml(@path)
      loaded.is_a?(Hash) ? loaded : {}
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "memory.load_store", path: @path)
      {}
    end

    def persist
      FileUtils.mkdir_p(File.dirname(@path))
      write_atomic(@path, @store.to_yaml)
    end

  end
  end
end

lib/ground/memory_index.rb

# frozen_string_literal: true

require "digest"
require "fileutils"
require "json"

module Master
  module Ground
  class MemoryIndex
    DEFAULT_DIRS = [
      File.join(Master::ROOT, "data", "claude"),
      File.join(Master::ROOT, ".master", "memory")
    ].freeze

    attr_reader :root, :index_path

    def initialize(root: Master::ROOT, dirs: DEFAULT_DIRS, index_path: File.join(Master::ROOT, ".master", "memory_index.json"))
      @root = root
      @dirs = dirs
      @index_path = index_path
    end

    def rebuild!
      previous = load_index
      docs = files.each_with_object({}) do |path, out|
        body = File.read(path, encoding: "utf-8")
        hash = Digest::SHA256.hexdigest(body)
        rel = relative(path)
        old = previous[rel]
        out[rel] = if old && old["hash"] == hash
                     old
                   else
                     tokenize(body).merge("hash" => hash, "path" => rel, "title" => title(body, path), "bytes" => body.bytesize)
                   end
      end
      FileUtils.mkdir_p(File.dirname(index_path))
      File.write(index_path, JSON.pretty_generate(docs))
      docs
    end

    def load_index
      return {} unless File.file?(index_path)

      JSON.parse(File.read(index_path, encoding: "utf-8"))
    rescue JSON::ParserError
      {}
    end

    private

    def files
      @dirs.flat_map { |dir| Dir.glob(File.join(dir, "**", "*.md")) }.select { |path| File.file?(path) }
    end

    def tokenize(body)
      terms = body.downcase.scan(/[a-z0-9_\-]{3,}/)
      counts = terms.tally.sort_by { |_term, count| -count }.first(200).to_h
      { "terms" => counts }
    end

    def title(body, path)
      body.lines.find { |line| line.start_with?("#") }&.sub(/^#+\s*/, "")&.strip || File.basename(path)
    end

    def relative(path)
      path.sub(%r{\A#{Regexp.escape(root)}/?}, "")
    end
  end
  end
end

lib/ground/memory_search.rb

# frozen_string_literal: true

module Master
  module Ground
  class MemorySearch
    def initialize(index: MemoryIndex.new)
      @index = index
    end

    def search(query, limit: 8)
      docs = @index.load_index
      docs = @index.rebuild! if docs.empty?
      terms = query.to_s.downcase.scan(/[a-z0-9_\-]{3,}/)
      return [] if terms.empty?

      scored = docs.values.filter_map { |doc| score_or_skip(doc, terms) }
      scored.sort_by { |doc| -doc["score"] }.first(limit)
    end

    def score_or_skip(doc, terms)
      score = score_doc(doc, terms)
      score > 0 ? doc.merge("score" => score) : nil
    end

    def brief(query, limit: 5)
      rows = search(query, limit: limit)
      return "Memory search: no hits for #{query.inspect}." if rows.empty?

      "Memory search hits:\n" + rows.map { |doc| "- #{doc['path']} score=#{format('%.2f', doc['score'])} title=#{doc['title']}" }.join("\n")
    end

    private

    def score_doc(doc, terms)
      counts = doc.fetch("terms", {})
      terms.sum { |term| Math.log(1 + counts.fetch(term, 0).to_f) }
    end
  end
  end
end

lib/ground/orchestration_policy.rb

# frozen_string_literal: true

module Master
  module Ground
  class OrchestrationPolicy
    MODEL_TIERS = {
      cheap:         %i[low],
      fast:          %i[low medium],
      strong:        %i[high critical],
      local:         %i[low medium],
      browser_local: %i[low]
    }.freeze

    COUNCIL_TIERS = %i[high critical].freeze

    # Persona → task-domain mapping. Council reviews the domain, not the answer.
    COUNCIL_ROLES = {
      "Security" => %i[auth secrets tool_execution permission_changes],
      "Reliability" => %i[network provider runtime fallback],
      "Maintainer" => %i[code_mutation refactor file_deletion],
      "Architect" => %i[system_shape migration design],
      "User Advocate" => %i[ui mobile accessibility],
      "Accessibility" => %i[ui mobile accessibility],
      "Music Producer" => %i[sonic visual_rhythm pacing],
      "Hip-Hop Producer" => %i[sonic visual_rhythm pacing]
    }.freeze

    # Required output sections for high/critical risk responses.
    EVIDENCE_CONTRACT = %i[observed_facts inferred_plan uncertainty rollback_path verification_path].freeze

    def initialize(router: IntentRouter.new, registry: nil)
      @router   = router
      @registry = registry
    end

    def evaluate(text)
      route   = @router.route(text)
      intent  = route[:intent]
      risk    = route[:risk]
      model   = select_model(risk)
      council = COUNCIL_TIERS.include?(risk)
      {
        intent:          intent,
        risk:            risk,
        model_tier:      model,
        use_council:     council,
        council_roles:   council ? roles_for(intent) : [],
        evidence_req:    council,
        evidence_fields: council ? EVIDENCE_CONTRACT : []
      }
    end

    def model_for(risk)
      select_model(risk)
    end

    def roles_for(intent)
      domain = intent_domain(intent)
      COUNCIL_ROLES.filter_map { |persona, domains| persona if domains.include?(domain) }
    end

    private

    def select_model(risk)
      case risk
      when :low      then :cheap
      when :medium   then :fast
      when :high, :critical then :strong
      else :fast
      end
    end

    def intent_domain(intent)
      case intent
      when :wire_existing_module, :verify_patch_landed, :write_repo_changes then :code_mutation
      when :delete_redundant_config                                          then :file_deletion
      when :run_ui_review                                                    then :ui
      when :run_sound_review                                                 then :sonic
      when :codify_policy, :refactor_to_ruby                                then :refactor
      else :general
      end
    end
  end
  end
end

lib/ground/orders/architecture_audit.rb

# frozen_string_literal: true

module Master
  module Ground
  module Orders
    # Weekly review of data/* shape misfit. Walks every yaml under data/, flags
    # files that grew past LINE_LIMIT (split candidate), files with overlapping
    # top-level keys (merge candidate), and emits suggestions to the bus.
    class ArchitectureAudit < Base
      LINE_LIMIT = 200

      def call
        data_dir = File.join(root, "data")
        bloated  = bloated_files(data_dir)
        overlaps = overlapping_keys(data_dir)
        bus&.publish("architecture_audit:bloated", files: bloated)
        bus&.publish("architecture_audit:overlap", pairs: overlaps)
        Result.ok(bloated: bloated, overlaps: overlaps)
      rescue StandardError => e
        Result.err(e.message)
      end

      private

      def bloated_files(dir)
        Dir.glob(File.join(dir, "*.yml")).filter_map do |f|
          lines = File.foreach(f).count
          [File.basename(f), lines] if lines > LINE_LIMIT
        end.sort_by { |_, n| -n }
      end

      def overlapping_keys(dir)
        files = Dir.glob(File.join(dir, "*.yml"))
        keys  = files.each_with_object({}) do |f, h|
          data = Master.load_yaml(f) rescue nil
          h[File.basename(f)] = data.is_a?(Hash) ? data.keys : []
        end
        pairs = []
        keys.to_a.combination(2) do |(a, ka), (b, kb)|
          shared = ka & kb
          pairs << [a, b, shared] if shared.any?
        end
        pairs
      end
    end
  end
  end
end

lib/ground/orders/autocommit.rb

# frozen_string_literal: true

require "open3"

module Master
  module Ground
  module Orders
    class Autocommit < Base
      def call
        repo = File.expand_path(File.join(root, ".."))
        out, _, status = Open3.capture3("git", "-C", repo, "status", "--porcelain")
        return Result.ok(skipped: true) unless status.success? && !out.strip.empty?
        Open3.capture2e("git", "-C", repo, "add", "-A")
        msg = "auto: standing-order commit (#{out.lines.size} file(s))"
        _, st = Open3.capture2e("git", "-C", repo, "commit", "-m", msg)
        return Result.err("commit failed") unless st.success?
        push_out, push_st = Open3.capture2e("git", "-C", repo, "push")
        if push_st.success?
          bus&.publish("autocommit:pushed", files: out.lines.size)
          Result.ok(committed: true, pushed: true)
        else
          bus&.publish("autocommit:push_failed", error: push_out.strip[0, 200])
          Result.ok(committed: true, pushed: false, push_error: push_out.strip)
        end
      rescue StandardError => e
        Result.err(e.message)
      end
    end
  end
  end
end

lib/ground/orders/backup.rb

# frozen_string_literal: true

module Master
  module Ground
  module Orders
  # Backup — openrsync standing order.
  # Syncs ~/pub4 to wingman1.openbsd.amsterdam:backup using openrsync over SSH.
  # Runs as a background standing order; never blocks the main loop.
  class Backup < Base
    REMOTE_HOST = "s4vm23@wingman1.openbsd.amsterdam"
    REMOTE_PATH = "backup"
    SSH_OPTS    = %w[-o BatchMode=yes -o ConnectTimeout=10].freeze

    def call
      src = File.expand_path("../../..", root)  # ~/pub4 from MASTER root
      cmd = ["openrsync", "-ae", "ssh #{SSH_OPTS.join(" ")}",
             src, "#{REMOTE_HOST}:#{REMOTE_PATH}"]
      out, status = Open3.capture2e(*cmd)
      if status.success?
        bytes = begin; File.size(src); rescue StandardError; nil; end
        bus&.publish("backup:ok", bytes:)
        Result.ok("backup: synced #{File.basename(src)}#{REMOTE_HOST}")
      else
        bus&.publish("backup:error", error: out.lines.last.to_s.strip)
        Result.err("backup: #{out.lines.last.to_s.strip}", category: :infrastructure)
      end
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "Orders::Backup.call", event_bus: bus)
      Result.err(e.message, category: :infrastructure)
    end
  end
  end
  end
end

lib/ground/orders/base.rb

# frozen_string_literal: true

module Master
  module Ground
  module Orders
    # Standing-order callables. Subclass and implement `call`. Returning a
    # Master::Result::Ok marks the order done; Result::Err marks it errored.
    class Base
      def initialize(container:)
        @container = container
      end

      def bus = @container[:bus]
      def root = @container[:root]

      def call
        raise NotImplementedError, "#{self.class}#call not implemented"
      end
    end
  end
  end
end

lib/ground/orders/constitution_drift.rb

# frozen_string_literal: true

module Master
  module Ground
  module Orders
    # Continuous self-audit: scans lib/ against the axioms and reports drift.
    class ConstitutionDrift < Base
      STATE_PATH = "runtime/constitution_drift.json"

      def call
        scanner = @container[:scanner]
        return Result.err("no scanner in container") unless scanner
        report = build_report(scanner)
        publish_drift(report)
        persist(report)
        Result.ok(report)
      rescue StandardError => e
        Result.err(e.message)
      end

      private

      # total, delta-since-last-run, and per-axiom counts.
      def build_report(scanner)
        by_axiom = tally_violations(scanner)
        total    = by_axiom.values.sum
        delta    = total - load_previous[:total].to_i
        { total:, delta:, by_axiom:, worst: by_axiom.max_by { |_, n| n }&.first }
      end

      def lib_files
        Dir.glob(File.join(root, "lib", "**", "*.rb"))
           .reject { |p| p.include?("/knowledge/") || p.include?("/vendor/") }
           .sort
      end

      # axiom_tag => violation count, across all of lib/.
      def tally_violations(scanner)
        lib_files.each_with_object(Hash.new(0)) do |path, acc|
          result = scanner.scan(path, depth: :deep)
          next unless result.ok?
          result.value!.each { |f| Array(f[:tags]).each { |t| acc[t.to_s] += 1 } }
        end
      end

      # improved | regressed | steady — caught the moment a defect lands.
      def publish_drift(report)
        delta = report[:delta]
        kind  = delta.negative? ? "improved" : (delta.positive? ? "regressed" : "steady")
        bus&.publish("constitution_drift:#{kind}", **report)
      end

      def state_file = File.join(root, STATE_PATH)

      def load_previous
        return { total: 0 } unless File.exist?(state_file)
        JSON.parse(File.read(state_file), symbolize_names: true)
      rescue StandardError => e
        Swallow.log(e, context: "constitution_drift.load_previous", event_bus: bus)
        { total: 0 }
      end

      def persist(report)
        FileUtils.mkdir_p(File.dirname(state_file))
        record = report.slice(:total, :by_axiom).merge(at: Time.now.utc.iso8601)
        File.write(state_file, JSON.pretty_generate(record))
      rescue StandardError => e
        Swallow.log(e, context: "constitution_drift.persist", event_bus: bus)
      end
    end
  end
  end
end

lib/ground/orders/registry.rb

# frozen_string_literal: true

module Master
  module Ground
  module Orders
    # Maps standing-order callable: keys to Order subclasses. Decouples the yaml
    # from class names — `callable: autocommit` is stable even if the class moves.
    module Registry
      def self.lookup(key)
        table[key.to_s]
      end

      def self.table
        @table ||= {
          "autocommit" => Autocommit,
          "restart_master" => RestartMaster,
          "architecture_audit" => ArchitectureAudit,
          "constitution_drift" => ConstitutionDrift
        }
      end

      def self.register(key, klass)
        table[key.to_s] = klass
      end
    end
  end
  end
end

lib/ground/orders/restart_master.rb

# frozen_string_literal: true

require "open3"

module Master
  module Ground
  module Orders
    class RestartMaster < Base
      def call
        _, status = Open3.capture2e("doas", "rcctl", "restart", "master")
        status.success? ? Result.ok(restarted: true) : Result.err("rcctl restart failed")
      rescue StandardError => e
        Result.err(e.message)
      end
    end
  end
  end
end

lib/ground/patch_verifier.rb

# frozen_string_literal: true

module Master
  module Ground
  class PatchVerifier
    Check = Struct.new(:kind, :target, :ok, :detail, keyword_init: true)

    def initialize(root: Master::ROOT)
      @root = root
    end

    def verify(files: [], symbols: [], references: {})
      checks = []
      checks.concat(Array(files).map { |path| check_file(path) })
      checks.concat(Array(symbols).map { |name| check_symbol(name) })
      references.each { |file, needles| checks.concat(Array(needles).map { |needle| check_reference(file, needle) }) }
      checks
    end

    def ok?(checks)
      checks.all?(&:ok)
    end

    def report(checks)
      checks.map do |check|
        status = check.ok ? "ok" : "missing"
        "#{status}: #{check.kind} #{check.target}#{check.detail ? " — #{check.detail}" : ""}"
      end
    end

    private

    def check_file(path)
      abs = File.join(@root, path.to_s)
      Check.new(kind: :file, target: path, ok: File.file?(abs), detail: nil)
    end

    def check_symbol(name)
      needle = name.to_s.split("::").last
      files = Dir.glob(File.join(@root, "lib", "**", "*.rb"))
      hit = files.find { |file| File.read(file, encoding: "utf-8").include?(needle) }
      Check.new(kind: :symbol, target: name, ok: !!hit, detail: hit && relative(hit))
    rescue StandardError => e
      Check.new(kind: :symbol, target: name, ok: false, detail: e.message)
    end

    def check_reference(file, needle)
      path = File.join(@root, file.to_s)
      ok = File.file?(path) && File.read(path, encoding: "utf-8").include?(needle.to_s)
      Check.new(kind: :reference, target: "#{file}:#{needle}", ok: ok, detail: nil)
    rescue StandardError => e
      Check.new(kind: :reference, target: "#{file}:#{needle}", ok: false, detail: e.message)
    end

    def relative(path)
      path.sub(%r{\A#{Regexp.escape(@root)}/?}, "")
    end
  end
  end
end

lib/ground/persistence/sqlite_findings.rb

# frozen_string_literal: true

require "sqlite3"
require "json"

module Master
  module Ground
  module Persistence
    # SQLite-backed findings store. Replaces .master/findings.jsonl when present;
    # otherwise no-op so existing JSONL flow continues unaltered. Exposes time-series
    # queries the JSONL stream cannot answer (rule frequency over time, by directory).
    class SqliteFindings
      include SqliteStore
      DEFAULT_PATH = ".master/findings.sqlite3"

      def initialize(root:)
        @db = open_sqlite(root, DEFAULT_PATH)
        ensure_schema
      end

      def record(rule:, message:, line:, severity:, path:, tags: [])
        @db.execute(<<~SQL, [Time.now.to_i, rule.to_s, message.to_s, line.to_i, severity.to_s, path.to_s, tags.to_json])
          INSERT INTO findings (ts, rule, message, line, severity, path, tags) VALUES (?, ?, ?, ?, ?, ?, ?)
        SQL
      end

      def top_rules(since_days: 30, limit: 10)
        cutoff = Time.now.to_i - since_days * 86_400
        @db.execute(<<~SQL, [cutoff, limit])
          SELECT rule, COUNT(*) AS n FROM findings WHERE ts >= ?
          GROUP BY rule ORDER BY n DESC LIMIT ?
        SQL
      end

      def by_directory(since_days: 30)
        cutoff = Time.now.to_i - since_days * 86_400
        @db.execute(<<~SQL, [cutoff])
          SELECT substr(path, 1, instr(path || '/', '/lib/') + 4) AS dir,
                 COUNT(*) AS n FROM findings WHERE ts >= ?
          GROUP BY dir ORDER BY n DESC
        SQL
      end

      def close
        @db&.close
      end

      private

      def ensure_schema
        @db.execute_batch(<<~SQL)
          CREATE TABLE IF NOT EXISTS findings (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            ts INTEGER NOT NULL,
            rule TEXT NOT NULL,
            message TEXT NOT NULL,
            line INTEGER,
            severity TEXT,
            path TEXT,
            tags TEXT
          );
          CREATE INDEX IF NOT EXISTS idx_findings_ts ON findings(ts);
          CREATE INDEX IF NOT EXISTS idx_findings_rule ON findings(rule);
          CREATE INDEX IF NOT EXISTS idx_findings_path ON findings(path);
        SQL
      end
    end
  end
  end
end

lib/ground/persistence/sqlite_memory.rb

# frozen_string_literal: true

require "sqlite3"

module Master
  module Ground
  module Persistence
    # SQLite-backed memory with FTS5 full-text search. Replaces flat-file memory
    # once entries cross a few dozen. Each record carries a kind (user/feedback/
    # project/reference), a name, and body; FTS5 indexes name+body for /recall.
    class SqliteMemory
      include SqliteStore
      DEFAULT_PATH = ".master/memory.sqlite3"

      def initialize(root:)
        @db = open_sqlite(root, DEFAULT_PATH)
        ensure_schema
      end

      def upsert(name:, kind:, body:, description: "")
        @db.execute(<<~SQL, [name.to_s, kind.to_s, description.to_s, body.to_s, Time.now.to_i, name.to_s])
          INSERT INTO memories (name, kind, description, body, updated_at) VALUES (?, ?, ?, ?, ?)
          ON CONFLICT(name) DO UPDATE SET kind=excluded.kind, description=excluded.description,
                                          body=excluded.body, updated_at=excluded.updated_at
          WHERE name = ?
        SQL
        rebuild_fts_for(name.to_s)
      end

      def recall(query, limit: 10)
        @db.execute(<<~SQL, [query.to_s, limit])
          SELECT m.name, m.kind, m.description, snippet(memories_fts, 1, '[', ']', '…', 12) AS hit
          FROM memories_fts JOIN memories m ON m.rowid = memories_fts.rowid
          WHERE memories_fts MATCH ? ORDER BY rank LIMIT ?
        SQL
      end

      def all
        @db.execute("SELECT name, kind, description, updated_at FROM memories ORDER BY updated_at DESC")
      end

      def delete(name)
        @db.execute("DELETE FROM memories WHERE name = ?", [name.to_s])
        @db.execute("DELETE FROM memories_fts WHERE name = ?", [name.to_s])
      end

      def close
        @db&.close
      end

      private

      def ensure_schema
        @db.execute_batch(<<~SQL)
          CREATE TABLE IF NOT EXISTS memories (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT UNIQUE NOT NULL,
            kind TEXT NOT NULL,
            description TEXT,
            body TEXT NOT NULL,
            updated_at INTEGER NOT NULL
          );
          CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
            name, body, content='memories', content_rowid='id'
          );
          CREATE INDEX IF NOT EXISTS idx_memories_kind ON memories(kind);
        SQL
      end

      def rebuild_fts_for(name)
        @db.execute("INSERT INTO memories_fts(memories_fts) VALUES('rebuild')")
      end
    end
  end
  end
end

lib/ground/persistence/sqlite_store.rb

# frozen_string_literal: true

require "sqlite3"
require "fileutils"

module Master
  module Ground
    module Persistence
      # Opens a SQLite DB with WAL → DELETE → :memory: fallback so a read-only
      # filesystem or locked WAL never crashes the store.
      module SqliteStore
        def open_sqlite(root, relative_path)
          path = File.join(root, relative_path)
          FileUtils.mkdir_p(File.dirname(path))
          db = SQLite3::Database.new(path)
          begin
            db.execute("PRAGMA journal_mode = WAL")
          rescue SQLite3::IOException
            begin
              db.execute("PRAGMA journal_mode = DELETE")
            rescue SQLite3::IOException
              db.close rescue nil
              warn "sqlite_store: file DB unavailable at #{path} — using :memory:"
              db = SQLite3::Database.new(":memory:")
            end
          end
          db
        rescue SQLite3::Exception => e
          warn "sqlite_store: #{e.message} — using :memory:"
          SQLite3::Database.new(":memory:")
        end
      end
    end
  end
end

lib/ground/phase_gates.rb

# frozen_string_literal: true

module Master
  module Ground
  PHASES = %w[discover analyze ideate design implement validate deliver idle].freeze

  class PhaseGates
    include AtomicWrite
    PHASE_STATE_PATH = "data/phase_state.yml".freeze

    GATES = {
      "discover" => %w[problem_stated success_measurable],
      "analyze" => %w[components_distinct dependencies_noted],
      "ideate" => %w[alternatives_gte_3],
      "design" => %w[interfaces_noted errors_noted],
      "implement" => %w[],
      "validate" => %w[tests_noted],
      "deliver" => %w[deployed_noted],
      "idle" => %w[]
    }.freeze

    def initialize(root:, event_bus: nil)
      @root  = root
      @bus   = event_bus
      @state = load_state
    end

    def current = @state["phase"] || "idle"

    def advance!(to: nil)
      prev   = current
      target = to&.to_s || next_phase
      return Master::Result.err("unknown phase: #{target}") unless PHASES.include?(target)
      return Master::Result.err("already at final phase: #{prev}") if prev == "idle" && target == "idle"

      unmet = unmet_gates(prev)
      if unmet.any?
        return Master::Result.err(
          "phase #{prev} gates unmet: #{unmet.join(",")} — override with /phase advance --force"
        )
      end

      @state["phase"] = target
      @state["entered_at"] = Time.now.to_i
      persist
      @bus&.publish("phase:advanced", from: prev, to: target)
      Master::Result.ok("phase: #{prev} -> #{target}")
    end

    def force!(phase)
      @state["phase"] = phase.to_s
      @state["entered_at"] = Time.now.to_i
      persist
      Master::Result.ok("phase forced to #{phase}")
    end

    def meet_gate!(gate)
      @state["met_gates"] ||= []
      @state["met_gates"] |= [gate.to_s]
      persist
    end

    def status
      unmet = unmet_gates(current)
      met   = (@state["met_gates"] || []) & (GATES[current] || [])
      "phase=#{current} met=#{met.join(",")} unmet=#{unmet.join(",")}"
    end

    private

    def next_phase
      idx = PHASES.index(current) || 0
      PHASES[[idx + 1, PHASES.size - 1].min]
    end

    def unmet_gates(phase)
      required = GATES.fetch(phase, [])
      (required - (@state["met_gates"] || []))
    end

    def load_state
      path = File.join(@root, PHASE_STATE_PATH)
      return { "phase" => "idle", "met_gates" => [] } unless File.exist?(path)
      data = Master.load_yaml(path)
      data.is_a?(Hash) ? data : { "phase" => "idle", "met_gates" => [] }
    rescue StandardError => _e
      { "phase" => "idle", "met_gates" => [] }
    end

    def persist
      path = File.join(@root, PHASE_STATE_PATH)
      FileUtils.mkdir_p(File.dirname(path))
      write_atomic(path, @state.to_yaml)
    end
  end
  end
end

lib/ground/pledge.rb

# frozen_string_literal: true

module Master
  module Ground
  module Pledge
    module_function

    if RUBY_PLATFORM.include?("openbsd")
      require "fiddle"
      require "fiddle/import"

      module LibC
        extend Fiddle::Importer
        dlload "libc.so"
        extern "int pledge(const char *, const char *)"
        extern "int unveil(const char *, const char *)"
      end

      def pledge(promises, execpromises = nil)
        r = LibC.pledge(promises, execpromises || Fiddle::NULL)
        raise SystemCallError.new("pledge", Fiddle.last_error) if r == -1
      end

      def unveil(path, permissions)
        r = LibC.unveil(path, permissions)
        raise SystemCallError.new("unveil", Fiddle.last_error) if r == -1
      end

      def lock_unveil! = LibC.unveil(Fiddle::NULL, Fiddle::NULL)
      def openbsd?     = true
    else
      def pledge(*)    = nil
      def unveil(*)    = nil
      def lock_unveil! = nil
      def openbsd?     = false
    end

    GEM_DIRS = [".local/share/gem", ".gem"].freeze
    STAGE1_PROMISES = "stdio rpath wpath cpath proc exec inet dns tty unveil prot_exec error"
    STAGE2_PROMISES = "stdio rpath wpath cpath proc exec inet dns tty prot_exec error"
    STAGE3_PROMISES = "stdio rpath wpath cpath tty"

    def stage1_boot!(root)
      pledge(STAGE1_PROMISES)
      unveil("/", "")
      unveil(root, "rwc")
      unveil(Dir.home, "rwc")
      unveil("/tmp", "rwc")
      unveil("/bin", "rx")
      unveil("/sbin", "rx")
      unveil("/usr/bin", "rx")
      unveil("/usr/sbin", "rx")
      unveil("/usr/local/bin", "rx")
      unveil("/usr/local/lib", "r")
      unveil("/usr/local/share", "r")
      unveil("/etc/ssl", "r")
      unveil("/etc/resolv.conf", "r")
      unveil("/dev/urandom", "r")
      unveil("/dev/null", "rwc")
      unveil("/var/run", "r")
      GEM_DIRS.each { |d| (dir = File.join(Dir.home, d); unveil(dir, "r") if Dir.exist?(dir)) }
    end

    def stage2_lock!
      lock_unveil!
      pledge(STAGE2_PROMISES)
    end

    def stage3_scan_only!
      lock_unveil!
      pledge(STAGE3_PROMISES)
    end
  end
  end
end

lib/ground/provider_registry.rb

# frozen_string_literal: true

module Master
  module Ground
  module ProviderRegistry
    DEFAULTS = {
      openai: {
        env: %w[OPENAI_API_KEY],
        strengths: %i[reasoning coding multimodal],
        default_model: "gpt-5.5-thinking"
      },
      anthropic: {
        env: %w[ANTHROPIC_API_KEY],
        strengths: %i[coding long_context instruction_following],
        default_model: "claude-sonnet-4-6"
      },
      gemini: {
        env: %w[GOOGLE_API_KEY GEMINI_API_KEY],
        strengths: %i[long_context multimodal],
        default_model: "gemini-2.5-flash"
      },
      openrouter: {
        env: %w[OPENROUTER_API_KEY],
        strengths: %i[cheap routing free_tier],
        default_model: "openrouter/auto"
      },
      deepseek: {
        env: %w[DEEPSEEK_API_KEY],
        strengths: %i[coding cheap],
        default_model: "deepseek-chat"
      },
      local: {
        env: [],
        strengths: %i[privacy offline cheap],
        default_model: "local"
      }
    }.freeze

    module_function

    def providers
      @providers ||= load_providers
    end

    def available
      providers.select { |_name, cfg| Array(cfg[:env]).empty? || cfg[:env].any? { |key| ENV[key].to_s != "" } }
    end

    def choose(task: :coding)
      candidates = available
      exact = candidates.find { |_name, cfg| cfg[:strengths].include?(task.to_sym) }
      name, cfg = exact || candidates.first || [:local, providers[:local]]
      { provider: name, model: cfg[:default_model], strengths: cfg[:strengths] }
    end

    def brief
      "Provider registry: available=#{available.keys.join(', ')}; choose by task strengths, fall back to local."
    end

    def load_providers
      path = File.join(Master::DATA, "providers.yml")
      return DEFAULTS unless File.exist?(path)
      raw = Master.load_yaml(path)
      raw.transform_keys(&:to_sym).transform_values do |v|
        v.transform_keys(&:to_sym).tap do |cfg|
          cfg[:strengths] = Array(cfg[:strengths]).map(&:to_sym)
        end
      end
    rescue StandardError => e
      warn("provider_registry: #{e.message} — using defaults")
      DEFAULTS
    end
  end
  end
end

lib/ground/repo_map.rb

# frozen_string_literal: true

module Master
  module Ground
  class RepoMap
    DEFAULT_IGNORES = %w[
      .git tmp log vendor node_modules coverage .bundle storage public/assets
      knowledge/awesome knowledge/vendor
    ].freeze

    LANGUAGE_BY_EXT = {
      ".rb" => :ruby,
      ".js" => :javascript,
      ".ts" => :typescript,
      ".css" => :css,
      ".html" => :html,
      ".erb" => :erb,
      ".yml" => :yaml,
      ".yaml" => :yaml,
      ".json" => :json,
      ".md" => :markdown,
      ".sh" => :shell
    }.freeze

    FileEntry = Struct.new(:path, :language, :bytes, :mtime, :score, keyword_init: true)

    attr_reader :root

    def initialize(root: Master::ROOT, ignores: DEFAULT_IGNORES)
      @root = root
      @ignores = ignores
    end

    def files
      Dir.glob(File.join(root, "**", "*"), File::FNM_DOTMATCH)
         .select { |path| File.file?(path) }
         .reject { |path| ignored?(path) }
         .map { |path| entry(path) }
    end

    def relevant(query, limit: 30)
      terms = query.to_s.downcase.scan(/[a-z0-9_\-]+/)
      files.map do |entry|
        text = entry.path.downcase
        bonus = terms.sum { |term| text.include?(term) ? 3 : 0 }
        lang_bonus = entry.language == :ruby ? 1 : 0
        entry.score = bonus + lang_bonus - size_penalty(entry.bytes)
        entry
      end.sort_by { |entry| -entry.score }.first(limit)
    end

    def brief(query = nil, limit: 20)
      rows = query ? relevant(query, limit: limit) : files.first(limit)
      rows.map { |entry| "#{entry.path} #{entry.language} #{entry.bytes}B score=#{format('%.2f', entry.score || 0)}" }
    end

    private

    def entry(path)
      FileEntry.new(
        path: relative(path),
        language: LANGUAGE_BY_EXT.fetch(File.extname(path), :other),
        bytes: File.size(path),
        mtime: File.mtime(path),
        score: 0
      )
    end

    def ignored?(path)
      rel = relative(path)
      @ignores.any? { |ignore| rel == ignore || rel.start_with?("#{ignore}/") }
    end

    def relative(path)
      path.sub(%r{\A#{Regexp.escape(root)}/?}, "")
    end

    def size_penalty(bytes)
      return 0 if bytes < 20_000
      Math.log(bytes / 20_000.0)
    end
  end
  end
end

lib/ground/repo_mining/mobile_web_cluster_catalog.rb

# frozen_string_literal: true

module Master
  module Ground
  module RepoMining
  class MobileWebClusterCatalog
    Entry = Data.define(:repo, :pattern, :why, :risk, :tags)

    CATALOG = [
      Entry.new(
        repo:    "hotwired/turbo",
        pattern: :turbo_core,
        why:     "Authoritative source for Turbo Frame/Stream semantics and _top targeting",
        risk:    :low,
        tags:    %i[turbo hotwire rails_8 navigation]
      ),
      Entry.new(
        repo:    "hotwired/stimulus",
        pattern: :stimulus_lifecycle,
        why:     "connect/disconnect/initialize lifecycle — reference for DOMContentLoaded migration",
        risk:    :low,
        tags:    %i[stimulus hotwire lifecycle]
      ),
      Entry.new(
        repo:    "excid3/jumpstart",
        pattern: :rails_8_pwa_starter,
        why:     "Rails 8 omakase template with Hotwire, importmap, PWA manifest, mobile layout out of the box",
        risk:    :low,
        tags:    %i[rails_8 hotwire pwa mobile importmap]
      ),
      Entry.new(
        repo:    "mattbrictson/rails-template",
        pattern: :rails_baseline_a11y,
        why:     "Importmap + Hotwire baseline with accessibility defaults and mobile-first HTML structure",
        risk:    :low,
        tags:    %i[rails_8 importmap accessibility mobile_first]
      ),
      Entry.new(
        repo:    "stimulus-components/stimulus-components",
        pattern: :stimulus_component_library,
        why:     "Drop-in Stimulus controllers: dialog, dropdown, clipboard, confirm — removes custom jQuery patterns",
        risk:    :low,
        tags:    %i[stimulus components accessibility jquery_replacement]
      ),
      Entry.new(
        repo:    "lazaronixon/css-zero",
        pattern: :css_zero_mobile_first,
        why:     "Rails-native CSS reset: mobile-first, no JS, system fonts, 44px touch targets, focus-visible baked in",
        risk:    :low,
        tags:    %i[css mobile_first touch_targets focus_visible minimal]
      ),
      Entry.new(
        repo:    "rails/solid_queue",
        pattern: :solid_queue,
        why:     "DB-backed background jobs replacing Redis/Sidekiq for simpler Rails 8 stacks",
        risk:    :low,
        tags:    %i[solid_queue rails_8 jobs simplification]
      ),
      Entry.new(
        repo:    "rails/solid_cache",
        pattern: :solid_cache,
        why:     "DB-backed cache store — eliminates Redis dependency for most caching needs",
        risk:    :low,
        tags:    %i[solid_cache rails_8 simplification]
      ),
      Entry.new(
        repo:    "rails/solid_cable",
        pattern: :solid_cable,
        why:     "DB-backed Action Cable adapter — replaces Redis for ActionCable in Rails 8",
        risk:    :low,
        tags:    %i[solid_cable rails_8 action_cable]
      ),
      Entry.new(
        repo:    "rossta/serviceworker-rails",
        pattern: :service_worker_rails,
        why:     "Routes service-worker.js through Rails asset pipeline with cache-busting helpers",
        risk:    :low,
        tags:    %i[pwa service_worker rails cache]
      )
    ].freeze

    INTENT_TAG_MAP = {
      refactor_existing:    %i[jquery_replacement stimulus components hotwire lifecycle],
      audit_rails_pwa:      %i[pwa service_worker cache mobile_first touch_targets],
      generate_rails_pwa:   %i[rails_8 hotwire pwa mobile importmap],
      redesign_mobile_pwa:  %i[css mobile_first focus_visible accessibility minimal]
    }.freeze

    def top(n = 5, tags: nil)
      filtered = tags ? CATALOG.select { |e| (e.tags & Array(tags)).any? } : CATALOG
      filtered.first(n)
    end

    def for_intent(intent)
      tags = INTENT_TAG_MAP.fetch(intent.to_sym, [])
      top(5, tags:)
    end

    def by_pattern(pattern)
      CATALOG.select { |e| e.pattern == pattern.to_sym }
    end
  end
  end
  end
end

lib/ground/rules.rb

# frozen_string_literal: true

module Master
  module Ground
  # Loads and exposes rules, axioms, voice, and workflow from data/*.yml.
  class Rules
    RULES_SUBDIR = "rules"

    def initialize(root: nil)
      @root          = root || Master::ROOT
      @data_dir      = File.join(@root, "data")
      @rules_path    = File.join(@data_dir, "rules.yml")
      @soul_path     = File.join(@data_dir, "soul.yml")
      @workflow_path = File.join(@data_dir, "workflow.yml")
      @data          = load_yaml(@rules_path) || {}
      @data["rules"] = load_split_rules
      @soul_data     = load_yaml(@soul_path)     || {}
      @workflow      = load_yaml(@workflow_path) || {}
      @cache         = {}
    end

    # mtime-aware cache. Reloads automatically when data/<name>.yml changes on disk.
    def data(name)
      key  = name.to_sym
      path = File.join(@data_dir, "#{name}.yml")
      return @cache[key]&.first || {} unless File.exist?(path)

      mtime = File.mtime(path)
      cached = @cache[key]
      return cached.first if cached && cached.last >= mtime

      payload = Master.load_yaml(path) || {}
      @cache[key] = [payload, mtime]
      payload
    end

    def kernel
      @kernel ||= begin
        all_rules = (@data["rules"] || {}).values.flatten
        all_rules
          .select { |r| r["tier"] == "kernel" }
          .each_with_object({}) { |r, h| h[r["id"]] = r["name"] }
          .freeze
      end
    end

    def workflow = @workflow.freeze

    def philosophy(limit: nil)
      @philosophy ||= begin
        all_rules = (@data["rules"] || {}).values.flatten
        all_rules
          .reject { |r| r["tier"] == "kernel" }
          .map { |h| h.transform_keys(&:to_s) }
          .freeze
      end
      limit ? @philosophy.first(limit) : @philosophy
    end

    def all_rules     = @all_rules ||= (@data["rules"] || {}).values.flatten.freeze
    def rules_for_scope(scope) = (@data.dig("rules", scope.to_s) || []).freeze

    def kernel_block
      return if kernel.empty?

      pairs = kernel.map { |id, stmt| "  #{id}: #{stmt}" }.join("\n")
      "## Kernel Rules (enforced)\n#{pairs}"
    end

    def philosophy_block(limit: 5)
      items = philosophy(limit: limit)
      return if items.empty?

      top = items.map { |a| "  #{a["id"]}: #{a["name"]}" }.join("\n")
      "## Rules (top #{items.size})\n#{top}"
    end

    def voice    = @voice    ||= (@data["voice"] || {}).freeze
    def strunk   = @strunk   ||= (voice["strunk"] || {}).freeze
    def preserve = @preserve ||= (voice["preserve"] || {}).freeze

    def constitution
      @constitution ||= begin
        absolute = @soul_data["absolute"] || {}
        {
          "golden_rule" => absolute["golden_rule"]      || @data["golden_rule"],
          "protection" => absolute["protection_tiers"] || @data["protection"],
          "banned_output" => voice["banned_output"],
          "anti_simulation" => absolute["anti_simulation"]  || voice["anti_simulation"],
          "communication_style" => voice["style"]
        }.freeze
      end
    end

    def code_rules      = @code_rules      ||= (@soul_data.dig("absolute", "code_rules") || {}).freeze
    def thresholds       = @thresholds       ||= (@data["thresholds"] || {}).freeze
    def scan_depths      = @scan_depths      ||= (@data["scan_depths"] || {}).freeze
    def languages_config = @languages_config ||= (@data["languages"] || {}).freeze
    def workflow_rule(key) = @workflow.dig(key.to_s) || {}

    def lookup(id)
      id_str = id.to_s
      kernel[id_str] || philosophy.find { |a| a["id"] == id_str }&.dig("name")
    end

    def valid_id?(id) = all_ids.include?(id.to_s)
    def all_ids       = @all_ids ||= all_rules.map { |r| r["id"] }.compact.to_set.freeze
    def empty?        = @data.empty?

    private

    def load_split_rules
      dir = File.join(@data_dir, RULES_SUBDIR)
      return @data["rules"] || {} unless File.directory?(dir)
      Dir.glob(File.join(dir, "*.yml")).sort.each_with_object({}) do |f, merged|
        data = load_yaml(f) || {}
        data.each { |scope, rules| (merged[scope] ||= []).concat(Array(rules)) }
      end
    end

    def load_yaml(path)
      return unless File.exist?(path)
      Master.load_yaml(path)
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "rules.load_yaml", path:)
      nil
    end
  end
  end
end

lib/ground/runtime_registry.rb

# frozen_string_literal: true

module Master
  module Ground
  # RuntimeRegistry — single source of truth for provider selection.
  # Issue #396 item 2: collapse duplicate orchestration registries.
  #
  # Combines ProviderRegistry (static capability map), ProviderHealth
  # (score telemetry), and ProviderQuarantineManager (availability) into
  # one call site. Callers ask choose() and get a live-aware selection.
  class RuntimeRegistry
    def initialize(
      health:     Now::Routing::ProviderHealth.new,
      quarantine: Now::Routing::ProviderQuarantineManager.new,
      event_bus:  nil
    )
      @health     = health
      @quarantine = quarantine
      @bus        = event_bus
    end

    # Return best provider for a task domain, respecting quarantine + scores.
    # Returns { provider: Symbol, model: String, score: Float, quarantined: [] }
    def choose(task: :coding)
      candidates = ProviderRegistry.available

      available = candidates.reject { |name, _| @quarantine.quarantined?(name) }
      pool      = available.any? ? available : candidates # fall back to full set if all quarantined

      scored = pool.map { |name, cfg| [name, cfg, @health.score(name)] }
                   .sort_by { |_, _, score| -score }

      # Prefer task-strength match over raw score
      match = scored.find { |_, cfg, _| cfg[:strengths].include?(task.to_sym) }
      name, cfg, score = match || scored.first || [:local, ProviderRegistry::PROVIDERS[:local], 0.5]

      quarantined_list = candidates.keys.select { |n| @quarantine.quarantined?(n) }
      @bus&.publish("runtime:provider_chosen", provider: name, score:, task:)

      { provider: name, model: cfg[:default_model], score:, quarantined: quarantined_list }
    end

    def status
      ProviderRegistry.available.map do |name, cfg|
        {
          provider:    name,
          model:       cfg[:default_model],
          score:       @health.score(name),
          quarantined: @quarantine.quarantined?(name),
          strengths:   cfg[:strengths]
        }
      end
    end
  end
  end
end

lib/ground/sandbox_policy.rb

# frozen_string_literal: true

module Master
  module Ground
  module SandboxPolicy
    DENY_PATTERNS = [
      /\brm\s+-rf\s+(?:\/|~|\$HOME)\b/,
      /\bsudo\b/,
      /\bmkfs\b/,
      /\bdd\s+if=/,
      /\bchmod\s+-R\s+777\b/,
      /\bchown\s+-R\b/,
      /\bforce-push\b|\bgit\s+push\s+--force/,
      /\b(drop|truncate)\s+(database|table)\b/i,
      /\bshutdown\b|\breboot\b/,
      /\bcurl\b.*\|\s*(?:sh|bash|zsh)/
    ].freeze

    ASK_PATTERNS = [
      /\bgit\s+push\b/,
      /\bgit\s+reset\s+--hard\b/,
      /\bgit\s+clean\s+-fd/,
      /\bbundle\s+exec\s+rails\s+db:/,
      /\bdelete\b/i,
      /\bdeploy\b/i
    ].freeze

    ALLOW_PATTERNS = [
      /\Agit\s+(status|diff|log|show|branch)\b/,
      /\A(?:bundle\s+exec\s+)?ruby\s+-c\b/,
      /\A(?:bundle\s+exec\s+)?rspec\b/,
      /\A(?:bundle\s+exec\s+)?rubocop\b/,
      /\A(?:bundle\s+exec\s+)?rails\s+test\b/,
      /\Als\b|\Afind\b|\Agrep\b|\Arg\b/
    ].freeze

    Decision = Struct.new(:mode, :reason, keyword_init: true) do
      def allow? = mode == :allow
      def ask? = mode == :ask
      def deny? = mode == :deny
    end

    module_function

    def decide(command)
      source = command.to_s.strip
      return Decision.new(mode: :deny, reason: "empty command") if source.empty?
      return Decision.new(mode: :deny, reason: "matched deny pattern") if DENY_PATTERNS.any? { |pattern| source.match?(pattern) }
      return Decision.new(mode: :ask, reason: "matched ask pattern") if ASK_PATTERNS.any? { |pattern| source.match?(pattern) }
      return Decision.new(mode: :allow, reason: "matched safe allow pattern") if ALLOW_PATTERNS.any? { |pattern| source.match?(pattern) }

      Decision.new(mode: :ask, reason: "unknown command risk")
    end

    def allowed?(command)
      decide(command).allow?
    end

    def brief
      "Sandbox policy: deny destructive/system commands, ask for pushes/deploy/db/reset, allow read-only git and test/lint commands."
    end
  end
  end
end

lib/ground/standing_orders.rb

# frozen_string_literal: true

require "set"

module Master
  module Ground
  class StandingOrders
    include AtomicWrite
    DAILY_INTERVAL = 86_400
    WEEKLY_INTERVAL = 604_800
    ERROR_TRUNCATE = 200
    DEBOUNCE_S = 10
    DEFS_PATH = File.join(Master::ROOT, "data", "standing_orders.yml")
    STATE_PATH = File.join(Master::ROOT, ".master", "standing_orders_state.yml")
    STATE_KEYS = %w[state last_run_at last_error].freeze
    VALID_STATES = %w[pending running done error].freeze
    EVENT_SUBSCRIPTIONS = %w[tool:after].freeze

    BUILTIN_ORDERS = [
      { name: "nightly_dreams", description: "Consolidate memories during low-activity periods",
        trigger: "scheduled", interval_s: 86_400, command: "dreams consolidate", enabled: true },
      { name: "weekly_scan", description: "Weekly codebase axiom scan for regressions",
        trigger: "scheduled", interval_s: 604_800, command: "scan", enabled: false }
    ].freeze

    def initialize(pipeline: nil, event_bus: nil, container: {})
      @pipeline  = pipeline
      @bus       = event_bus
      @container = container
      @orders    = load_orders
      @mutex     = Mutex.new
      @running   = Set.new
      subscribe_events!
    end

    def wire_pipeline(pipeline)
      @pipeline = pipeline
    end

    def wire_container(container)
      @container = container
    end

    def due
      now = Time.now.to_i
      @orders.select do |o|
        o["enabled"] &&
          o["trigger"] == "scheduled" &&
          %w[pending done].include?(state_of(o)) &&
          (now - o["last_run_at"].to_i) >= o["interval_s"].to_i
      end
    end

    def run_due!
      results = []
      due.each do |order|
        order["state"] = "running"
        persist

        result = execute_order(order)
        order["last_run_at"] = Time.now.to_i

        if result.ok?
          order["state"] = "done"
          order.delete("last_error")
        else
          order["state"] = "error"
          order["last_error"] = result.message.to_s[0, ERROR_TRUNCATE]
        end

        results << { name: order["name"], result: }
        @bus&.publish("standing_order:ran", name: order["name"], ok: result.ok?, state: order["state"])
      end
      persist if results.any?
      results
    end

    def upsert(name:, description: "", trigger: "scheduled",
               interval_s: 86_400, command:, enabled: true)
      existing = @orders.find { |o| o["name"] == name.to_s }
      if existing
        existing.merge!(
          "description" => description, "trigger" => trigger.to_s,
          "interval_s" => interval_s.to_i, "command" => command.to_s, "enabled" => enabled
        )
      else
        @orders << {
          "name" => name.to_s, "description" => description.to_s, "trigger" => trigger.to_s,
          "interval_s" => interval_s.to_i, "command" => command.to_s, "enabled" => enabled,
          "state" => "pending", "last_run_at" => 0
        }
      end
      persist
      "standing order '#{name}' saved"
    end

    def enable(name)  = toggle(name, true)
    def disable(name) = toggle(name, false)

    def reset(name)
      order = @orders.find { |x| x["name"] == name.to_s }
      return "no order named '#{name}'" unless order
      order["state"] = "pending"
      order.delete("last_error")
      persist
      "'#{name}' reset -> pending"
    end

    def list
      return "no standing orders defined" if @orders.empty?
      @orders.map { |o| format_order(o) }.join("\n")
    end

    def format_order(o)
      st   = state_of(o)
      flag = o["enabled"] ? "on" : "off"
      last = o["last_run_at"].to_i > 0 ? Time.at(o["last_run_at"].to_i).strftime("%Y-%m-%d") : "never"
      err  = o["last_error"] ? "  !! #{o["last_error"][0, 60]}" : ""
      "#{o['name']} [#{flag}|#{st}] - #{o['description']} (last: #{last})#{err}"
    end

    private

    def subscribe_events!
      return unless @bus
      EVENT_SUBSCRIPTIONS.each do |event_name|
        @bus.subscribe(event_name) { |ev| dispatch_event(event_name, ev) }
      end
    end

    def dispatch_event(event_name, payload)
      @orders.each do |order|
        next unless event_match?(order, event_name, payload)
        next if debounced?(order)
        next unless @mutex.synchronize { @running.add?(order["name"]) }
        Thread.new { run_event_order(order) }.tap { |t| t.abort_on_exception = false }
      end
    end

    def event_match?(order, event_name, payload)
      return false unless order["enabled"]
      return false unless order["trigger"] == "event"
      return false unless order["event"].to_s == event_name
      filter_match?(order, payload) && !exclude_match?(order, payload)
    end

    def filter_match?(order, payload)
      pattern = order["filter"].to_s
      return true if pattern.empty?
      payload_strings(payload).any? { |s| Regexp.new(pattern).match?(s) }
    end

    def exclude_match?(order, payload)
      pattern = order["exclude"].to_s
      return false if pattern.empty?
      payload_strings(payload).any? { |s| Regexp.new(pattern).match?(s) }
    end

    def payload_strings(payload)
      [payload[:tool], payload[:path], payload[:full]].compact.map(&:to_s)
    end

    def debounced?(order)
      last = order["last_run_at"].to_i
      last.positive? && (Time.now.to_i - last) < DEBOUNCE_S
    end

    def run_event_order(order)
      name = order["name"]
      result = execute_order(order)
      @mutex.synchronize do
        order["last_run_at"] = Time.now.to_i
        if result.ok?
          order["state"] = "done"
          order.delete("last_error")
        else
          order["state"] = "error"
          order["last_error"] = result.message.to_s[0, ERROR_TRUNCATE]
        end
        persist
      end
      @bus&.publish("standing_order:ran", name:, ok: result.ok?, trigger: "event")
    rescue StandardError => e
      @bus&.publish("standing_order:error", name: order["name"], error: e.message)
    ensure
      @mutex.synchronize { @running.delete(order["name"]) }
    end

    def state_of(order) = VALID_STATES.include?(order["state"]) ? order["state"] : "done"

    def execute_order(order)
      if (callable_key = order["callable"])
        klass = Master::Ground::Orders::Registry.lookup(callable_key)
        return Result.err("unknown callable: #{callable_key}") unless klass
        return klass.new(container: @container.merge(bus: @bus, root: Master::ROOT)).call
      end
      return Result.err("no pipeline") unless @pipeline
      @pipeline.call(Result.ok(user_message: order["command"].to_s))
    rescue StandardError => e
      Result.err(e.message)
    end

    def toggle(name, enabled)
      order = @orders.find { |x| x["name"] == name.to_s }
      return "no order named '#{name}'" unless order
      order["enabled"] = enabled
      persist
      "#{name} #{enabled ? 'enabled' : 'disabled'}"
    end

    def load_orders
      defs = read_defs
      state = read_state
      defs.each do |order|
        carry = state[order["name"]] || {}
        order["state"] = carry["state"] || "pending"
        order["last_run_at"] = carry["last_run_at"] || 0
        order["last_error"] = carry["last_error"] if carry["last_error"]
      end
      defs
    end

    def read_defs
      if File.exist?(DEFS_PATH)
        raw = Master.load_yaml(DEFS_PATH)
        return builtin_orders unless raw.is_a?(Array)
        raw.select { |o| o.is_a?(Hash) }
      else
        builtin_orders
      end
    rescue Psych::Exception, Errno::ENOENT, TypeError, NoMethodError => e
      @bus&.publish("standing_orders:load_error", error: e.message)
      builtin_orders
    end

    def read_state
      return {} unless File.exist?(STATE_PATH)
      raw = Master.load_yaml(STATE_PATH)
      raw.is_a?(Hash) ? raw : {}
    rescue Psych::Exception, Errno::ENOENT, TypeError
      {}
    end

    def builtin_orders
      BUILTIN_ORDERS.map { |o| o.transform_keys(&:to_s).merge("last_run_at" => 0, "state" => "pending") }
    end

    def persist
      return unless @orders.is_a?(Array)
      state = @orders.each_with_object({}) do |order, acc|
        acc[order["name"]] = STATE_KEYS.each_with_object({}) { |k, h| h[k] = order[k] if order.key?(k) }
      end
      FileUtils.mkdir_p(File.dirname(STATE_PATH))
      write_atomic(STATE_PATH, state.to_yaml)
    end
  end
  end
end

lib/ground/subagent_policy.rb

# frozen_string_literal: true

module Master
  module Ground
  module SubagentPolicy
    ALWAYS_EXCLUDED = %w[
      spawn_agent resume_agent wait_agent send_input close_agent
      team_create team_delete team_broadcast rebuild evolve self_improve
    ].freeze

    TYPES = {
      general: {
        label: "general",
        prompt: "General-purpose sub-agent. Complete the task using available non-recursive tools.",
        allow: nil
      },
      explore: {
        label: "explore",
        prompt: "Codebase exploration agent. Search and read thoroughly. Do not modify files.",
        allow: %w[read_file glob grep ls search fetch fetch_file]
      },
      plan: {
        label: "plan",
        prompt: "Architecture planning agent. Read current state and produce a concrete file-by-file plan. Do not modify files.",
        allow: %w[read_file glob grep ls search fetch fetch_file bash]
      },
      code: {
        label: "code",
        prompt: "Implementation agent. Make precise changes, follow existing patterns, and verify the result.",
        allow: nil
      },
      research: {
        label: "research",
        prompt: "Research agent. Search/read public or local sources. Do not modify files.",
        allow: %w[read_file glob grep ls search fetch fetch_file web_search http_request]
      }
    }.freeze

    module_function

    def parse(value)
      key = value.to_s.downcase.tr("-", "_").to_sym
      case key
      when :search, :find then :explore
      when :architect, :design then :plan
      when :implement, :write then :code
      when :web, :lookup then :research
      else TYPES.key?(key) ? key : :general
      end
    end

    def prompt_for(type)
      TYPES.fetch(parse(type)).fetch(:prompt)
    end

    def excluded?(tool_name)
      ALWAYS_EXCLUDED.include?(tool_name.to_s)
    end

    def allowed?(type, tool_name)
      name = tool_name.to_s
      return false if excluded?(name)

      allow = TYPES.fetch(parse(type)).fetch(:allow)
      allow.nil? || allow.include?(name)
    end

    def filter(type, tool_names)
      tool_names.map(&:to_s).select { |name| allowed?(type, name) }
    end

    def brief(type = :general)
      policy = TYPES.fetch(parse(type))
      allowed = policy[:allow] ? policy[:allow].join(", ") : "all parent tools minus recursive/dangerous tools"
      "Subagent policy: #{policy[:label]} gets #{allowed}; always exclude #{ALWAYS_EXCLUDED.join(', ')}."
    end
  end
  end
end

lib/ground/swallow.rb

# frozen_string_literal: true

module Master
  module Ground
  # Central helper for intentional rescue-and-continue sites.
  # Keeps tolerated failures visible, greppable, and event-bus observable.
  module Swallow
    module_function

    def log(error, context:, event_bus: nil, **metadata)
      payload = {
        context: context.to_s,
        error_class: error.class.name,
        error: error.message.to_s,
        backtrace: Array(error.backtrace).first(5)
      }.merge(metadata)

      if event_bus
        event_bus.publish("swallow:error", payload)
      else
        warn("swallow:error #{payload.inspect}")
      end
      nil
    rescue StandardError => e
      # Last resort: the logger itself failed. Cannot use the bus or recurse
      # into Swallow — write straight to stderr so the failure is not lost.
      Kernel.warn("swallow:meta_error #{e.class}: #{e.message}")
      nil
    end
  end
  end
end

lib/ground/tool_approval_policy.rb

# frozen_string_literal: true

module Master
  module Ground
  module ToolApprovalPolicy
    MODES = %i[plan ask act auto].freeze

    MODE_RULES = {
      plan: { writes: false, commands: :read_only, description: "read-only planning" },
      ask: { writes: :confirm, commands: :confirm_risky, description: "ask before mutation or risky command" },
      act: { writes: true, commands: :sandboxed, description: "act with sandbox policy" },
      auto: { writes: true, commands: :sandboxed, description: "safe autonomous execution with deny gates" }
    }.freeze

    module_function

    def normalize(mode)
      key = mode.to_s.downcase.to_sym
      MODES.include?(key) ? key : :ask
    end

    def allow_write?(mode, path: nil)
      rule = MODE_RULES.fetch(normalize(mode)).fetch(:writes)
      return true if rule == true
      return false if rule == false

      :confirm
    end

    def command_decision(mode, command)
      case MODE_RULES.fetch(normalize(mode)).fetch(:commands)
      when :read_only
        SandboxPolicy.decide(command).allow? && command.to_s.match?(/\A(?:git\s+(status|diff|log|show)|ls|find|grep|rg)\b/) ? :allow : :deny
      when :confirm_risky
        decision = SandboxPolicy.decide(command)
        decision.allow? ? :allow : (decision.deny? ? :deny : :ask)
      else
        decision = SandboxPolicy.decide(command)
        decision.mode
      end
    end

    def brief(mode = :ask)
      rule = MODE_RULES.fetch(normalize(mode))
      "Tool approval mode=#{normalize(mode)}: #{rule[:description]}."
    end
  end
  end
end

lib/ground/tool_contract.rb

# frozen_string_literal: true

module Master
  module Ground
  # ToolContract — typed contract for every tool execution.
  # Issue #396 item 7: route all tool execution through typed contracts.
  #
  # Contracts define allowed inputs, expected output shape, permission tier,
  # retry policy, timeout, and side-effect classification. The validator
  # enforces these at the call site before any tool runs.
  module ToolContract
    Contract = Data.define(
      :name,          # Symbol — tool identifier
      :inputs,        # Hash { key => :required | :optional }
      :output_shape,  # Array of expected output keys (or :any)
      :permission,    # :read | :write | :exec | :network
      :timeout_s,     # Integer — hard timeout
      :max_retries,   # Integer
      :side_effects,  # Array of Symbols — :filesystem | :network | :git | :process | :none
      :category       # :reach | :judge | :trace | :ground
    )

    REGISTRY = {
      web_fetch: Contract.new(
        name: :web_fetch, inputs: { url: :required, headers: :optional },
        output_shape: [:body], permission: :network, timeout_s: 30, max_retries: 2,
        side_effects: [:network], category: :reach
      ),
      file_read: Contract.new(
        name: :file_read, inputs: { path: :required },
        output_shape: [:content], permission: :read, timeout_s: 5, max_retries: 0,
        side_effects: [:none], category: :reach
      ),
      file_write: Contract.new(
        name: :file_write, inputs: { path: :required, content: :required },
        output_shape: [:written], permission: :write, timeout_s: 10, max_retries: 0,
        side_effects: [:filesystem], category: :reach
      ),
      shell_exec: Contract.new(
        name: :shell_exec, inputs: { command: :required, cwd: :optional },
        output_shape: [:stdout, :stderr, :exit_code], permission: :exec, timeout_s: 60,
        max_retries: 0, side_effects: [:process, :filesystem], category: :reach
      ),
      git_op: Contract.new(
        name: :git_op, inputs: { op: :required, args: :optional },
        output_shape: [:output], permission: :exec, timeout_s: 30, max_retries: 1,
        side_effects: [:git, :filesystem], category: :reach
      ),
      llm_call: Contract.new(
        name: :llm_call, inputs: { prompt: :required, model: :optional, system: :optional },
        output_shape: [:text, :tokens], permission: :network, timeout_s: 120, max_retries: 2,
        side_effects: [:network], category: :ground
      ),
      scan: Contract.new(
        name: :scan, inputs: { path: :required, depth: :optional },
        output_shape: [:findings], permission: :read, timeout_s: 60, max_retries: 0,
        side_effects: [:none], category: :judge
      ),
      postpro: Contract.new(
        name: :postpro, inputs: { args: :optional },
        output_shape: [:stdout, :stderr, :exit_code], permission: :exec, timeout_s: 600,
        max_retries: 0, side_effects: [:process, :filesystem], category: :reach
      ),
      repligen: Contract.new(
        name: :repligen, inputs: { args: :optional },
        output_shape: [:stdout, :stderr, :exit_code], permission: :network, timeout_s: 900,
        max_retries: 0, side_effects: [:network, :process, :filesystem], category: :reach
      )
    }.freeze

    module_function

    def find(name)
      REGISTRY[name.to_sym]
    end

    # Validate args against contract before execution.
    # Returns Result::Ok(contract) or Result::Err with violation details.
    # For shell_exec and git_op, runs injection_guard on command/args.
    def validate(name, args = {})
      contract = find(name)
      return Result.err("unknown tool: #{name}", category: :validation) unless contract

      missing = contract.inputs.select { |k, req| req == :required && !args.key?(k) }.keys
      return Result.err("#{name}: missing required inputs: #{missing.join(', ')}", category: :validation) if missing.any?

      if %i[shell_exec git_op].include?(name.to_sym)
        shell_input = args[:command] || Array(args[:args]).join(" ")
        guard = Master::Judge::Security::InjectionGuard.new
        check = guard.safe?(shell_input.to_s)
        return Result.err("#{name}: injection detected in input", category: :policy) unless check
      end

      Result.ok(contract)
    end

    def permitted_for?(name, permission_level)
      contract = find(name)
      return false unless contract
      permission_rank(contract.permission) <= permission_rank(permission_level)
    end

    def permission_rank(p)
      { read: 0, write: 1, network: 2, exec: 3 }.fetch(p.to_sym, 9)
    end
  end
  end
end

lib/ground/tool_protocol.rb

# frozen_string_literal: true

module Master
  module Ground
  module ToolProtocol
    CLAIM_WORDS = /\b(read|fetched|opened|searched|ran|executed|wrote|created|updated|deleted|patched|committed|verified)\b/i.freeze
    COMMAND_BLOCK = /```(?:bash|sh|zsh|python|ruby|powershell|shell)\b/i.freeze

    REQUIREMENTS = [
      "Never claim a command, file read, web fetch, or repo write happened unless a tool result proves it.",
      "Prefer action over narration when a tool can do the work.",
      "Do not output shell/code blocks as pretend execution.",
      "After tool use, provide a final text response that separates landed work from failures.",
      "For repo work, name the changed file, exposed symbol, caller, and verification result."
    ].freeze

    module_function

    def fake_execution_risk?(text)
      text.to_s.match?(COMMAND_BLOCK)
    end

    def operational_claim?(text)
      text.to_s.match?(CLAIM_WORDS)
    end

    def final_report(landed:, failed: [], next_steps: [])
      sections = []
      sections << ["Landed", Array(landed)] unless Array(landed).empty?
      sections << ["Not landed", Array(failed)] unless Array(failed).empty?
      sections << ["Next", Array(next_steps).first(3)] unless Array(next_steps).empty?
      sections.map { |title, rows| "#{title}:\n" + rows.map { |row| "- #{row}" }.join("\n") }.join("\n\n")
    end

    def brief
      "Tool protocol:\n- #{REQUIREMENTS.join("\n- ")}"
    end
  end
  end
end

lib/ground/type_checker.rb

# frozen_string_literal: true

require "prism"

module Master
  module Ground
  # Architecture #11: constitution as a type system on the AST IR.
  # Each rule is encoded as a type constraint. Violations are type errors.
  # Fixes are derivable from the complement constraint.
  # Sound and complete — no LLM, no probabilistic inference.
  #
  # Status: scaffolded — requires a full typed IR per supported medium.
  # The Ruby path uses Prism AST nodes as the IR. Other languages TBD.
  class TypeChecker
    TypeError = Struct.new(:node, :rule, :message, :complement, keyword_init: true)

    # Run all registered constraints against the AST for +path+.
    # Returns an array of TypeErrors.
    def self.check(path, source)
      new.check(path, source)
    end

    def initialize
      @constraints = BUILT_IN_CONSTRAINTS.dup
    end

    def register(rule_id, &constraint)
      @constraints[rule_id] = constraint
      self
    end

    def check(path, source)
      return [] unless File.extname(path).downcase == ".rb"
      result = Prism.parse(source)
      return [] unless result.success?
      errors = []
      walk(result.value, source, errors)
      errors
    end

    private

    def walk(node, source, errors)
      return unless node.is_a?(Prism::Node)
      @constraints.each do |rule_id, constraint|
        violation = constraint.call(node, source)
        errors << TypeError.new(node: node, rule: rule_id, **violation) if violation
      end
      node.child_nodes.compact.each { |c| walk(c, source, errors) }
    end

    # Built-in structural type constraints that don't require LLM inference.
    BUILT_IN_CONSTRAINTS = {
      FROZEN_STRING_LITERAL: lambda { |node, src|
        next unless node.is_a?(Prism::ProgramNode)
        next if src.start_with?("# frozen_string_literal: true")
        { message: "missing frozen_string_literal magic comment",
          complement: "# frozen_string_literal: true" }
      },
      BARE_RESCUE: lambda { |node, _src|
        next unless node.is_a?(Prism::RescueNode)
        next unless node.exceptions.nil? || node.exceptions.empty?
        { message: "bare rescue captures all exceptions; use rescue StandardError",
          complement: "rescue StandardError" }
      },
    }.freeze
  end
  end
end

lib/ground/unfinished_ledger.rb

# frozen_string_literal: true

require "json"
require "time"

module Master
  module Ground
  class UnfinishedLedger
    LEDGER_PATH = File.join(Master::ROOT, ".master", "unfinished.json").freeze

    def initialize(path: LEDGER_PATH)
      @path = path
      FileUtils.mkdir_p(File.dirname(@path))
    end

    def add(task:, context: nil)
      items = load
      items << { task: task, context: context, ts: Time.now.utc.iso8601 }
      write(items)
    end

    def resolve(task_pattern)
      items = load.reject { |i| i[:task].to_s.match?(task_pattern) }
      write(items)
    end

    def pending
      load
    end

    def top(n = 5)
      load.last(n).reverse
    end

    def empty?
      load.empty?
    end

    private

    def load
      return [] unless File.exist?(@path)
      raw = File.read(@path)
      parsed = JSON.parse(raw, symbolize_names: true)
      Array(parsed)
    rescue JSON::ParserError
      []
    end

    def write(items)
      File.write(@path, JSON.pretty_generate(items))
    end
  end
  end
end

lib/ground/unified_diff_editor.rb

# frozen_string_literal: true

module Master
  module Ground
  class UnifiedDiffEditor
    HUNK_RE = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/.freeze

    def initialize(root: Master::ROOT)
      @root = root
    end

    def parse(diff)
      current = nil
      files = []
      diff.to_s.each_line do |line|
        case line
        when /^---\s+(.+)/
          current = { old: clean_path(Regexp.last_match(1)), new: nil, hunks: [] }
          files << current
        when /^\+\+\+\s+(.+)/
          current[:new] = clean_path(Regexp.last_match(1)) if current
        when HUNK_RE
          raise ArgumentError, "hunk without file" unless current
          current[:hunks] << { header: line.chomp, lines: [] }
        else
          current[:hunks].last[:lines] << line if current && current[:hunks].any?
        end
      end
      files
    end

    def applyable?(diff)
      parse(diff).all? { |file| File.file?(abs(file[:new] || file[:old])) }
    rescue StandardError
      false
    end

    def summary(diff)
      parse(diff).map do |file|
        adds = file[:hunks].sum { |h| h[:lines].count { |line| line.start_with?("+") && !line.start_with?("+++") } }
        dels = file[:hunks].sum { |h| h[:lines].count { |line| line.start_with?("-") && !line.start_with?("---") } }
        "#{file[:new] || file[:old]} +#{adds} -#{dels} hunks=#{file[:hunks].size}"
      end
    end

    def build_single_file(path, before, after, context: 3)
      before_lines = before.to_s.lines
      after_lines = after.to_s.lines
      return "" if before_lines == after_lines

      [
        "--- #{path}\n",
        "+++ #{path}\n",
        "@@ -1,#{before_lines.size} +1,#{after_lines.size} @@\n",
        *diff_lines(before_lines, after_lines)
      ].join
    end

    private

    def diff_lines(before, after)
      prefix = 0
      max = [before.size, after.size].max
      lines = []
      max.times do |idx|
        old = before[idx]
        new = after[idx]
        if old == new
          lines << " #{old}" if old
        else
          lines << "-#{old}" if old
          lines << "+#{new}" if new
        end
        prefix += 1
      end
      lines
    end

    def clean_path(path)
      path.to_s.sub(%r{\A[ab]/}, "").strip
    end

    def abs(path)
      File.join(@root, path.to_s)
    end
  end
  end
end

lib/ground/workflow_policy.rb

# frozen_string_literal: true

module Master
  module Ground
  module WorkflowPolicy
    Phase = Data.define(:name, :input, :output, :questions, :actions)
    Metric = Data.define(:name, :method, :threshold, :action)

    LIMITS = {
      coverage: 0.8,
      complexity: 10,
      convergence: 0.01,
      iterations: 10,
      coupling: 5,
      duplication: 0.03,
      nesting_depth: 4,
      section_count: 15
    }.freeze

    PRINCIPLES = [
      [:dry, :three_duplications, :abstract, :high],
      [:kiss, :complexity_over_10, :simplify, :high],
      [:yagni, :unused, :remove, :medium],
      [:solid, :coupling_over_5, :decouple, :critical],
      [:composition, :deep_inheritance, :compose, :medium],
      [:evidence, :assumption, :validate, :critical],
      [:reversible, :irreversible, :add_rollback, :critical],
      [:explicit, :implicit, :make_explicit, :high],
      [:orthogonal, :coupled, :split, :high],
      [:minimalism, :bloat, :subtract, :medium],
      [:clarity, :synonym, :unify, :medium],
      [:flatten, :wrapper, :flatten, :high],
      [:pola, :surprise, :make_predictable, :high],
      [:unix, :multi_responsibility, :one_thing, :high],
      [:anti_divitis, :div_soup, :semantic_html, :medium],
      [:anti_sectionitis, :scattered_sections, :consolidate, :high],
      [:geometric, :visual_confusion, :simplify_geometry, :medium]
    ].freeze

    PHASES = [
      Phase.new(:discover, :problem, :definition,
                %w[specific_measurable who_affected_freq current_impact evidence if_nothing], []),
      Phase.new(:analyze, :definition, :analysis,
                %w[hidden_assumptions could_be_wrong dependencies evidence_support biases], %w[assumptions cost risk bias]),
      Phase.new(:ideate, :analysis, :options,
                %w[fifteen_approaches persona_suggests challenge_assumptions unconventional simplest], %w[gen_15_alt apply_10_personas synth]),
      Phase.new(:design, :options, :plan,
                %w[min_viable irreversible test_strategy maintainable], []),
      Phase.new(:implement, :plan, :code,
                %w[tests_prove edge_cases simplify duplication fail_points], %w[tests_first implement refactor]),
      Phase.new(:validate, :code, :verified,
                %w[proves_correct breaks missed violated adversarial_finds], %w[check_principles run_gates adversarial]),
      Phase.new(:deliver, :verified, :deployed,
                %w[deploy_ready docs monitoring rollback], []),
      Phase.new(:learn, :deployed, :knowledge,
                %w[worked failed differently patterns codify], %w[patterns measure improve codify])
    ].freeze

    WORKFLOWS = {
      new_feature: %i[discover analyze ideate design implement validate deliver learn],
      bug: %i[analyze implement validate deliver],
      refactor: %i[analyze design implement validate],
      security_fix: %i[analyze implement validate deliver],
      migration: %i[analyze design implement validate deliver]
    }.freeze

    METRICS = [
      Metric.new(:complexity, :cyclomatic, LIMITS[:complexity], :simplify),
      Metric.new(:coupling, :afferent_efferent, LIMITS[:coupling], :decouple),
      Metric.new(:duplication, :token_similarity, LIMITS[:duplication], :extract),
      Metric.new(:coverage, :line, LIMITS[:coverage], :write_tests),
      Metric.new(:nesting, :depth, LIMITS[:nesting_depth], :flatten),
      Metric.new(:sections, :count, LIMITS[:section_count], :consolidate)
    ].freeze

    GATES = {
      functional: { tests: true, coverage: LIMITS[:coverage] },
      secure: { vulnerabilities: 0, input_validation: true },
      maintainable: { complexity: LIMITS[:complexity], duplication: LIMITS[:duplication] },
      access: { wcag: :aa, lcp: 2.5, inp: 200, cls: 0.1, mobile: true },
      perf: { lcp: 2.5, cls: 0.1, js_kb: 170, total_kb: 2048 },
      deploy: { health: true, rollback: true },
      privacy: { gdpr: true, pii: true }
    }.freeze

    AUTO_FIX = %i[format dead_code typo import space naming].freeze
    CONFIRM = %i[drop_table delete_production force_push rm_rf irreversible_data_change].freeze

    def self.phase(name)
      PHASES.find { |phase| phase.name == name.to_sym }
    end

    def self.workflow(name)
      WORKFLOWS.fetch(name.to_sym, WORKFLOWS[:refactor]).filter_map { |phase_name| phase(phase_name) }
    end

    def self.gates(*names)
      names.flatten.map(&:to_sym).to_h { |name| [name, GATES.fetch(name, {})] }
    end

    def self.confirm?(action)
      CONFIRM.include?(action.to_sym)
    end

    def self.autofix?(action)
      AUTO_FIX.include?(action.to_sym)
    end

    def self.brief(workflow: :refactor)
      phases = workflow(workflow).map(&:name).join(" -> ")
      "Workflow policy: #{workflow} phases #{phases}; gates #{GATES.keys.join(', ')}; hard limits #{LIMITS}."
    end
  end
  end
end

lib/judge/agent.rb

# frozen_string_literal: true

require_relative "llm_dispatcher"

module Master
  module Judge
  class Agent
    DEFAULT_MESSAGE_WINDOW_SIZE = 16

    Dependencies = Data.define(
      :config, :session, :tools, :circuit_breaker, :cache, :bus,
      :model_router, :reasoning_modes, :memory, :personality,
      :code_index, :context_window, :homeostat
    ) do
      def self.from_kwargs(config:, session:, tools:, circuit_breaker:, cache:,
                           event_bus: nil, model_router: nil, reasoning_modes: nil,
                           memory: nil, personality: nil, code_index: nil,
                           context_window: nil, homeostat: nil)
        new(
          config:, session:, tools:, circuit_breaker:, cache:, bus: event_bus,
          model_router:, reasoning_modes:, memory:, personality:,
          code_index:, context_window:, homeostat:
        )
      end
    end

    def initialize(deps:)
      @deps                              = deps
      @config, @session, @tools          = deps.config, deps.session, deps.tools
      @circuit_breaker, @cache, @bus     = deps.circuit_breaker, deps.cache, deps.bus
      @model_router, @reasoning_modes    = deps.model_router, deps.reasoning_modes
      @memory, @personality, @code_index = deps.memory, deps.personality, deps.code_index
      @context_window, @homeostat        = deps.context_window, deps.homeostat
      @constitution                      = nil
      @dispatcher                        = Master::Judge::LLMDispatcher.new(deps:, system_prompt: -> { system_prompt })
    end

    def wire_constitution(constitution) = @constitution = constitution

    def chat(message, stream: true, escalation_depth: 0, &blk)
      prepare_chat_turn(message)
      candidate_models = routed_models(message)
      selected_model = candidate_models.first
      prompt   = topic_anchored(message)
      context  = conversation_context
      tokens_approx = Trace::Session.estimate_tokens(message)
      @bus&.publish("llm:request", model: selected_model, tokens: tokens_approx)
      @deps.homeostat&.observe(:llm_call)

      rate_err = check_rate_limit(selected_model)
      return rate_err if rate_err

      response = attempt_chat_with_fallbacks(candidate_models:, prompt:, context:, stream:, &blk)
      if response.is_a?(Master::Result::Err)
        @deps.homeostat&.observe(:llm_failure)
        return response
      end
      @deps.homeostat&.observe(:llm_success)
      response = maybe_escalate(response, message, stream:, escalation_depth:, &blk)

      text = response.to_s
      @session.add_message(role: :assistant, content: text)
      Result.ok(text)
    rescue StandardError => chat_error
      Result.err("agent: #{chat_error.message}", category: :handler_exception)
    end

    def prepare_chat_turn(message)
      @context_window&.check_and_compact!
      @tools.each { |t| t.reset! if t.respond_to?(:reset!) }
      @session.add_message(role: :user, content: message)
    end

    def check_rate_limit(model_id = nil)
      @circuit_breaker.check_rate!(model_id) if @circuit_breaker.respond_to?(:check_rate!)
      nil
    rescue Reach::CircuitBreaker::CircuitError => err
      Result.err(err.message, category: err.category)
    end

    def ask(prompt, context: nil, operation: nil)
      messages = Array(context) + [{ role: "user", content: apply_reasoning_mode(prompt) }]
      selected_model = operation ? model_for(operation:) : routed_models.first
      result = @dispatcher.send_with_cache(selected_model, messages, stream: false)
      raise StandardError, result.message if result.is_a?(Master::Result::Err)
      result.to_s
    end

    def ask_once(prompt, system: nil, model: nil)
      messages = [{ role: "user", content: prompt.to_s }]
      result   = @dispatcher.send_with_cache(model || self.model, messages, system:, stream: false)
      raise StandardError, result.message if result.is_a?(Master::Result::Err)
      result.to_s
    end

    def call(ctx)
      on_chunk = ctx[:on_chunk]
      task_type = ctx[:task_type]&.to_s
      with_task_type(task_type) do
        on_chunk ? chat(ctx[:message].to_s, stream: true, &on_chunk) : chat(ctx[:message].to_s)
      end
    end

    def model = routed_models.first
    def model=(val)
      @config["model"] = val
    end

    def with_model(override, &blk)
      @model_mutex ||= Mutex.new
      @model_mutex.synchronize do
        prev = model
        self.model = override
        blk.call
      ensure
        self.model = prev
      end
    end

    def model_for(operation:)
      @model_router&.constrained_for(operation:) || model
    end

    def wire_context_window(ctx_window)
      @context_window = ctx_window
    end

    private

    def with_task_type(type)
      return yield unless type && !type.empty?
      old = @config["task_type"]
      @config["task_type"] = type
      yield
    ensure
      @config["task_type"] = old
    end

    TOPIC_DRIFT_THRESHOLD = 6

    def topic_anchored(message)
      topic = @session.respond_to?(:topic) && @session.topic
      return message unless topic
      return message if @session.messages.length < TOPIC_DRIFT_THRESHOLD
      "#{message}\n\n[task: #{topic}]"
    end

    def apply_reasoning_mode(message, mode: @config.reasoning_mode)
      return message unless @reasoning_modes
      @reasoning_modes.wrap(message, mode:)
    end

    def system_prompt
      parts = []
      parts << "Current task: #{@session.topic}" if @session.respond_to?(:topic) && @session.topic
      parts << @constitution.system_prompt if @constitution && !@constitution.empty?
      parts << @personality.system_prompt if @personality
      parts << @code_index.summary if @code_index&.built?
      parts << @memory.context_summary if @memory&.context_summary
      parts.compact!
      parts.empty? ? nil : parts.join("\n\n")
    end

    def conversation_context(max_messages: DEFAULT_MESSAGE_WINDOW_SIZE)
      messages = @session.messages
      return [] unless messages.respond_to?(:each)
      messages.last(max_messages + 1)[0...-1] || []
    end

    def attempt_chat_with_fallbacks(candidate_models:, prompt:, context:, stream:, &blk)
      stage_warnings = []
      fallback_modes = mode_chain_for(candidate_models)
      last_response = nil

      fallback_modes.each do |attempt|
        selected_model = attempt.fetch(:model)
        mode = attempt.fetch(:mode)
        wrapped = apply_reasoning_mode(prompt, mode: mode)
        response = @dispatcher.send_with_cache(
          selected_model,
          context + [{ role: "user", content: wrapped }],
          stream:, &blk
        )
        if response.is_a?(Master::Result::Ok)
          publish_llm_success(selected_model, response)
          @bus&.publish("agent:stage_warnings", warnings: stage_warnings) unless stage_warnings.empty?
          return response
        end

        last_response = response
        stage_warnings << "llm failed in #{mode} on #{selected_model}: #{response.message}"
      end

      @bus&.publish("agent:all_fallbacks_exhausted", warnings: stage_warnings)
      last_response || Result.err("all LLM fallback modes exhausted", category: :llm_call_failure)
    end

    def mode_chain_for(candidates)
      models = Array(candidates).empty? ? [@config.model] : candidates
      primary = models.first
      modes = if @dispatcher.claude_cli_model?(primary) || @dispatcher.tool_capable?(primary)
                [@config.reasoning_mode.to_s, "code_agent", "react"]
              else
                ["code_agent", "react", "direct"]
              end
      chain = models.map { |m| { model: m, mode: modes.first } }
      chain.concat(modes.drop(1).map { |mode| { model: primary, mode: mode } })
      chain
    end

    def publish_llm_success(model, response)
      tokens_approx = Trace::Session.estimate_tokens(response)
      @bus&.publish("llm:response", model:, success: true, tokens_approx:)
    end

    def maybe_escalate(last_response, original_message, stream:, escalation_depth:, &blk)
      return last_response unless @model_router
      return last_response if escalation_depth >= 2

      current = routed_models.first
      escalation_model = @model_router.escalate_if_low_confidence(
        last_response.to_s,
        current_model: current,
        task_type: @config.task_type.to_sym
      )
      return last_response unless escalation_model
      return last_response if escalation_model.to_s == current.to_s

      @bus&.publish("llm:escalation", from: current, to: escalation_model)
      escalated = attempt_chat_with_fallbacks(
        candidate_models: [escalation_model],
        prompt: original_message,
        context: conversation_context,
        stream: stream,
        &blk
      )
      escalated.is_a?(Master::Result::Err) ? last_response : escalated
    end

    def routed_models(message = nil)
      return [@config.model] unless @model_router
      task = message ? @model_router.classify_intent(message) : @config.task_type.to_sym
      chain = @model_router.fallback_chain(task_type: task)
      bias  = @homeostat&.model_tier_bias
      return cheap_first(chain) if bias == :cheap
      chain
    rescue StandardError => e
      @bus&.publish("llm:route_error", error: e.message)
      [@config.model]
    end

    def cheap_first(chain)
      cheap = chain.select { |m| @model_router.tier_for_model(m) == "cheap" }
      rest  = chain.reject { |m| @model_router.tier_for_model(m) == "cheap" }
      cheap.empty? ? chain : (cheap + rest)
    end
  end
  end
end

lib/judge/agent_pool.rb

# frozen_string_literal: true
require "thread"

module Master
  module Judge
  # Typed child agents — replaces ad-hoc Thread.new in autoloop.
  # Reads agent_types from data/agent_taxonomy.yml at boot.
  class AgentPool
    MAX_CONCURRENT = 4

    Worker = Struct.new(:type, :thread, :started_at, :tag, keyword_init: true)

    def initialize(governor:, event_bus: nil, taxonomy_path: File.join(Master::ROOT, "data", "agent_taxonomy.yml"))
      @governor   = governor
      @bus        = event_bus
      @taxonomy   = Master.load_yaml(taxonomy_path) || {}
      @max        = @taxonomy.dig("spawn_policy", "max_concurrent_children") || MAX_CONCURRENT
      @workers    = []
      @mutex      = Mutex.new
    end

    def spawn(type:, tag: nil, &block)
      @mutex.synchronize do
        reap_dead
        return Result.err("agent_pool: at capacity (#{@max})", category: :validation) if @workers.size >= @max
      end

      thread = Thread.new do
        @bus&.publish("agent:start", type:, tag:)
        block.call
      rescue StandardError => err
        @bus&.publish("agent:error", type:, tag:, error: err.message)
      ensure
        @bus&.publish("agent:end", type:, tag:)
      end

      @mutex.synchronize { @workers << Worker.new(type:, thread:, started_at: Time.now, tag:) }
      Result.ok(thread)
    end

    # intentional — each thread is an independent worker, not a DB query
    def join_all(timeout: nil)
      @workers.map { |w| w.thread.join(timeout) }
    end

    def active_count
      @mutex.synchronize { reap_dead; @workers.size }
    end

    private

    def reap_dead
      @workers.reject! { |w| !w.thread.alive? }
    end
  end
  end
end

lib/judge/ast_signature.rb

# frozen_string_literal: true

require "prism"
require "open3"

module Master
  module Judge
  # Extracts the public API surface (methods, classes, modules, constants)
  # from Ruby source via Prism. Used by CommitGuard to detect omissions.
  module AstSignature
    Sig = Data.define(:type, :name, :line)
    module_function

    def from_source(source)
      result = Prism.parse(source)
      return [] if result.failure?
      visitor = Visitor.new
      visitor.visit(result.value)
      visitor.sigs
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "ast_signature.from_source")
      []
    end

    def from_git(rel_path, ref:, root: Dir.pwd)
      out, st = Open3.capture2e("git", "show", "#{ref}:#{rel_path}", chdir: root)
      return [] unless st.success?
      from_source(out)
    end

    def diff(old_sigs, new_sigs, kinds: %i[method class module])
      new_names = new_sigs.select { |s| kinds.include?(s.type) }.map(&:name).to_set
      old_sigs.select { |s| kinds.include?(s.type) && !new_names.include?(s.name) }
    end

    class Visitor < Prism::Visitor
      attr_reader :sigs
      def initialize = (@sigs = []; @ns = [])

      def visit_module_node(node)
        name = node.constant_path.slice
        full = fqn(name)
        @ns.push(name)
        @sigs << Sig.new(type: :module, name: full, line: node.location.start_line)
        super
        @ns.pop
      end

      def visit_class_node(node)
        name = node.constant_path.slice
        full = fqn(name)
        @ns.push(name)
        @sigs << Sig.new(type: :class, name: full, line: node.location.start_line)
        super
        @ns.pop
      end

      def visit_def_node(node)
        base = @ns.join("::")
        sep  = node.receiver&.is_a?(Prism::SelfNode) ? "." : "#"
        full = base.empty? ? node.name.to_s : "#{base}#{sep}#{node.name}"
        @sigs << Sig.new(type: :method, name: full, line: node.location.start_line)
      end

      def visit_constant_write_node(node)
        @sigs << Sig.new(type: :constant, name: fqn(node.name.to_s), line: node.location.start_line)
        super
      end

      def visit_constant_path_write_node(node)
        @sigs << Sig.new(type: :constant, name: node.target.slice, line: node.location.start_line)
        super
      end

      private
      def fqn(name) = (@ns + [name]).join("::")
    end
  end
  end
end

lib/judge/code_index.rb

# frozen_string_literal: true

require "prism"
require "set"
require "monitor"

module Master
  module Judge
  # Live Prism-parsed symbol graph; rebuilt on write events.
  class CodeIndex
    Symbol    = Struct.new(:fqn, :type, :file, :line, :parent, :includes, keyword_init: true)
    Reference = Struct.new(:from_file, :from_line, :to_fqn, :ref_type, keyword_init: true)

    class SymbolVisitor < Prism::Visitor
      attr_reader :symbols, :references

      def initialize(file:, root:)
        @file = file; @root = root
        @symbols = []; @references = []; @scope = []
      end

      def visit_class_node(node)
        name = const_name(node.constant_path)
        fqn  = qualified(name)
        @symbols << Symbol.new(fqn:, type: :class, file: @file, line: node.location.start_line,
                               parent: node.superclass ? const_name(node.superclass) : "Object", includes: [])
        @scope.push(name); super; @scope.pop
      end

      def visit_module_node(node)
        name = const_name(node.constant_path)
        fqn  = qualified(name)
        @symbols << Symbol.new(fqn:, type: :module, file: @file, line: node.location.start_line,
                               parent: nil, includes: [])
        @scope.push(name); super; @scope.pop
      end

      def visit_def_node(node)
        meth  = node.name.to_s
        owner = @scope.last || "(top)"
        @symbols << Symbol.new(fqn: "#{qualified(owner)}##{meth}", type: :method, file: @file,
                               line: node.location.start_line, parent: owner, includes: [])
        super
      end

      def visit_call_node(node)
        method_name = node.name.to_s
        return super unless method_name.match?(/\A[_a-z][a-z0-9_]*[!?]?\z/i) && method_name.length > 1
        receiver_fqn = node.receiver ? const_name_safe(node.receiver) : nil
        to_fqn = receiver_fqn ? "#{receiver_fqn}##{method_name}" : method_name
        @references << Reference.new(from_file: @file, from_line: node.location.start_line,
                                     to_fqn:, ref_type: :call)
        super
      end

      private

      def qualified(name)
        return name if @scope.empty? || name.include?("::")
        "#{@scope.join("::")}::#{name}"
      end

      def const_name(node)
        case node
        when Prism::ConstantReadNode          then node.name.to_s
        when Prism::ConstantPathNode, Prism::ConstantPathTargetNode
          "#{const_name(node.parent)}::#{node.name}"
        else node.respond_to?(:name) ? node.name.to_s : ""
        end
      end

      def const_name_safe(node)
        name = const_name(node)
        name.empty? ? nil : name
      rescue StandardError => e
        Master::Ground::Swallow.log(e, context: "code_index.const_name_safe")
        nil
      end
    end

    SUMMARY_SKIP_NAMES = %w[Entry Message Symbol CircuitError].freeze

    attr_reader :symbols, :references, :built_at

    def initialize(root:, event_bus: nil)
      @root         = File.expand_path(root)
      @bus          = event_bus
      @symbols      = {}
      @references   = []
      @mtimes       = {}
      @built_at     = nil
      @lock         = Monitor.new
      @build_thread = nil
    end

    def build(path: nil)
      @lock.synchronize do
        target = path ? File.expand_path(path, @root) : @root
        files  = Dir.glob(File.join(target, "**", "*.rb")).reject { |f| f.include?("/vendor/") }
        @built_at.nil? ? first_build(files) : incremental_build(files)
        @built_at = Time.now
        @bus&.publish("code_index:built", files: files.size, symbols: @symbols.size)
      end
      self
    rescue StandardError => e
      @bus&.publish("code_index:error", error: e.message)
      self
    end

    def build_async
      @build_thread = Thread.new { build }
      self
    end

    def ready?         = !@built_at.nil?
    def wait_for_build = @build_thread&.join
    def built?         = !@built_at.nil?
    def size           = @lock.synchronize { @symbols.size }

    def reindex(file)
      @lock.synchronize do
        full = File.expand_path(file, @root)
        purge_file(full)
        index_file(full) if File.file?(full)
      end
    rescue StandardError => e
      @bus&.publish("code_index:reindex_error", path: file, error: e.message)
    end

    def symbols_in(file)
      with_built_index do
        full = File.expand_path(file, @root)
        @symbols.values.select { |s| s.file == full }
      end
    end

    def find(name)
      with_built_index { find_locked(name) }
    end

    def references_to(fqn)
      with_built_index { references_for(fqn) }
    end

    def impact(fqn)
      with_built_index do
        refs    = references_for(fqn)
        files   = refs.map(&:from_file).uniq.map { |f| relativize(f) }
        callers = refs.map { |r| "#{relativize(r.from_file)}:#{r.from_line}" }.uniq
        { fqn:, reference_count: refs.size, files:, callers: }
      end
    end

    def summary(limit: nil)
      with_built_index do
        classes   = summary_classes
        lib_count = @symbols.values.count { |s| s.file.include?("/lib/") }
        stamp     = @built_at&.strftime("%H:%M") || "never"
        [
          "# Codebase: #{lib_count} lib symbols (indexed #{stamp})",
          "## Classes & Modules (#{classes.size})",
          *classes
        ].join("\n")
      end
    end

    def query(name)
      with_built_index do
        hits = find_locked(name)
        next { error: "not found: #{name}" } if hits.empty?
        hits.map { |s| query_entry(s) }
      end
    end

    private

    def with_built_index(&blk)
      wait_for_build unless ready?
      @lock.synchronize(&blk)
    end

    def first_build(files)
      @symbols.clear
      @references.clear
      @mtimes.clear
      files.each do |f|
        index_file(f)
        @mtimes[f] = (File.mtime(f) rescue nil)
      end
    end

    def incremental_build(files)
      (@mtimes.keys - files).each { |gone| purge_file(gone) }
      changed = files.count { |f| reindex_if_stale(f) }
      @bus&.publish("code_index:incremental", changed: changed, total: files.size) if changed > 0
    end

    def reindex_if_stale(file)
      mt = (File.mtime(file) rescue nil)
      return false if @mtimes[file] == mt
      reindex(file)
      @mtimes[file] = mt
      true
    end

    def purge_file(full)
      @symbols.delete_if { |_, s| s.file == full }
      @references.reject! { |r| r.from_file == full }
      @mtimes.delete(full)
    end

    def references_for(fqn)
      tail = "##{fqn}"
      @references.select { |r| to = r.to_fqn; to == fqn || to.end_with?(tail) }
    end

    def relativize(file)
      file.sub("#{@root}/", "")
    end

    def find_locked(name)
      exact = @symbols[name]
      return [exact] if exact
      suffix = name.to_s
      @symbols.values.select { |sym| fqn = sym.fqn; fqn.end_with?(suffix) || fqn.include?(suffix) }
    end

    def summary_class?(sym)
      return false unless %i[class module].include?(sym.type)
      file = sym.file
      return false if file.include?("/DEPLOY/") || file.match?(/fix_|patch_/)
      fqn = sym.fqn
      SUMMARY_SKIP_NAMES.none? { |n| fqn.end_with?("::#{n}") }
    end

    def summary_classes
      @symbols.values
              .select { |sym| summary_class?(sym) }
              .sort_by(&:fqn)
              .map { |sym| format_summary_entry(sym) }
    end

    def format_summary_entry(sym)
      parent_name = sym.parent
      parent = parent_name && parent_name != "Object" ? " < #{parent_name}" : ""
      "  #{sym.fqn}#{parent} (#{relativize(sym.file)}:#{sym.line})"
    end

    def query_entry(sym)
      refs = references_for(sym.fqn)
      {
        fqn:     sym.fqn,
        type:    sym.type,
        file:    relativize(sym.file),
        line:    sym.line,
        parent:  sym.parent,
        used_in: refs.first(10).map { |r| "#{relativize(r.from_file)}:#{r.from_line}" }
      }
    end

    def index_file(file)
      src          = File.read(file, encoding: "UTF-8")
      parse_result = Prism.parse(src)
      return unless parse_result.success?

      visitor = SymbolVisitor.new(file:, root: @root)
      parse_result.value.accept(visitor)
      visitor.symbols.each { |s| @symbols[s.fqn] = s }
      @references.concat(visitor.references)
    rescue StandardError => e
      @bus&.publish("code_index:parse_error", path: file, error: e.message)
    end
  end
  end
end

lib/judge/commit_guard.rb

# frozen_string_literal: true

require "open3"

module Master
  module Judge
  # Compares public API surface across last N commits.
  # Reports methods, classes, modules present in HEAD~N but absent now.
  class CommitGuard
    DEFAULT_DEPTH = 3
    Omission = Data.define(:path, :name, :type, :last_seen_at)

    def initialize(root: Dir.pwd, depth: DEFAULT_DEPTH)
      @root  = root
      @depth = depth
    end

    def check(paths: nil)
      files = paths ? Array(paths).select { |f| f.end_with?(".rb") } : changed_rb_files
      return [] if files.empty?
      files.flat_map { |rel| check_file(rel) }.uniq { |o| [o.path, o.name] }
    end

    def render(omissions)
      return "commit_guard: no omissions detected" if omissions.empty?
      lines = omissions.map { |o| "  #{o.type} #{o.name} (was in #{o.last_seen_at}) — #{o.path}" }
      "commit_guard: #{omissions.size} omission(s)\n#{lines.join("\n")}"
    end

    private

    def check_file(rel)
      full = File.join(@root, rel)
      return [] unless File.exist?(full)
      current = AstSignature.from_source(File.read(full))
      @depth.downto(1).flat_map { |n|
        hist = AstSignature.from_git(rel, ref: "HEAD~#{n}", root: @root)
        AstSignature.diff(hist, current).map { |s|
          Omission.new(path: rel, name: s.name, type: s.type, last_seen_at: "HEAD~#{n}")
        }
      }.uniq { |o| [o.path, o.name] }
    end

    def changed_rb_files
      out, st = Open3.capture2e("git", "diff", "--name-only", "HEAD~#{@depth}..HEAD", "--", "*.rb", chdir: @root)
      return [] unless st.success?
      out.lines.map(&:strip).reject(&:empty?)
    end
  end
  end
end

lib/judge/council/critique.rb

# frozen_string_literal: true

module Master
  module Judge
  module Council
    # Mode-dispatched council critique. Replaces UiCritique + SoundCritique.
    # Each mode is a config hash: preset key, default files/panel, context
    # briefs, constraints, ideation prompt, byte cap, event names.
    class Critique
      MODES = {
        ui: {
          preset_key: "ui_critique",
          max_bytes:  32_768,
          panel:      nil,
          files: %w[
            web/public/face.css web/public/face.js web/public/chat.js
            web/app/views/chat/index.html.erb lib/design/platform_profiles.rb
          ],
          quality_kind:    :design,
          ideation_prompt: "Generate concrete multi-solution improvements for this web UI. " \
                           "Produce 3 distinct solution directions per issue found.",
          cycles_default:  1,
          start_event:     :ui_critique_start,
          done_event:      :ui_critique_done,
          constraints: [
            "must not break existing HTML semantics",
            "must preserve intentional CSS measurements unless a measurable rule is violated",
            "animations must respect prefers-reduced-motion",
            "solutions must be implementable without a build step",
            "use Ruby QualityFramework design rules from Deliberation",
            "use Master::Design::PlatformProfiles for content-first and profile-specific critique",
            "distinguish measurable violations from subjective taste"
          ]
        },
        sound: {
          preset_key: "sound_critique",
          max_bytes:  24_576,
          panel:      %w[
            Electronic\ Music\ Producer Hip-Hop\ Producer User\ Advocate
            Accessibility Layperson Skeptic
          ],
          files: %w[
            web/public/chat.js web/public/face.js web/public/visual_bridge.js
            web/app/views/chat/index.html.erb lib/voice/speech.rb lib/voice/dilla.rb
            lib/voice/sonitex.rb lib/voice/sonitex_sox.rb lib/voice/production_dna.rb
            lib/voice/ffmpeg_lofi.rb lib/voice/tts_lofi.rb
          ],
          quality_kind:    :sound,
          ideation_prompt: "Generate concrete improvements for MASTER sound design, voice " \
                           "playback, sonic timing, and audio feedback.",
          cycles_default:  2,
          start_event:     :sound_critique_start,
          done_event:      :sound_critique_done,
          constraints: [
            "no autoplay without user intent",
            "must expose mute or silence path",
            "must not mask speech or screen-reader output",
            "must degrade when AudioContext or media playback fails",
            "prefer tiny generated tones or short assets over heavy dependencies",
            "preserve existing visual identity",
            "use Ruby QualityFramework sound rules from Deliberation",
            "when lo-fi processing is proposed, call Master::Voice::Sonitex rather than ad-hoc shell strings",
            "when Dilla-style timing is proposed, call Master::Voice::Dilla for swing, nudge, chord, and preset data",
            "when TTS effects are proposed, call Master::Voice::TtsLofi or Master::Voice::FfmpegLofi and keep clean audio as default"
          ]
        }
      }.freeze

      def initialize(mode:, agent:, event_bus: nil)
        @mode  = MODES.fetch(mode) { raise ArgumentError, "unknown critique mode: #{mode}" }
        @agent = agent
        @bus   = event_bus
      end

      def run
        preset  = load_preset
        panel   = build_panel(preset)
        payload = build_payload(preset)
        @bus&.publish(@mode[:start_event], files: payload[:files], personas: panel.map(&:name))

        delib  = Deliberation.new(personas: panel, agent: @agent, event_bus: @bus, judge_enabled: true)
        result = delib.review(payload[:combined], context: build_context)
        return result unless result.ok?

        ideation_result = Ideation.new(agent: @agent, event_bus: @bus).ideate(
          @mode[:ideation_prompt],
          constraints: @mode[:constraints],
          cycles:      (preset["cycles"] || @mode[:cycles_default]).to_i
        )

        feedback = result.value!
        cherry   = cherry_pick_from(feedback, ideation_result)
        @bus&.publish(@mode[:done_event], cherry_picks: cherry.size)
        Master::Result.ok({ feedback: feedback, ideas: ideation_value(ideation_result), cherry_picks: cherry })
      end

      private

      def load_preset
        return {} unless File.exist?(Master::COUNCIL_PATH)
        data = Master.load_yaml(Master::COUNCIL_PATH) || {}
        data.dig("presets", @mode[:preset_key]) || {}
      end

      def build_panel(preset)
        all   = Personas.load
        names = Array(preset["panel"] || @mode[:panel]).map(&:downcase)
        return all if names.empty?
        picked = all.select { |p| names.include?(p.name.downcase) }
        picked.empty? ? Personas::DEFAULTS : picked
      end

      def build_payload(preset)
        files    = Array(preset["files"]).any? ? preset["files"] : @mode[:files]
        combined = files.filter_map { |rel| read_truncated(rel) }.join("\n\n")
        { combined: combined, files: files }
      end

      def read_truncated(rel)
        path = File.join(Master::ROOT, rel)
        return nil unless File.exist?(path)
        raw = File.read(path, encoding: "utf-8")
        raw = raw.byteslice(0, @mode[:max_bytes]) + "\n... [truncated]" if raw.bytesize > @mode[:max_bytes]
        "file: #{rel}\n#{raw}"
      end

      def build_context
        [domain_context, Deliberation.quality_brief(@mode[:quality_kind]), domain_briefs]
          .compact.join("\n")
      end

      def domain_context
        return ui_domain_context if @mode[:preset_key] == "ui_critique"
        sound_domain_context
      end

      def ui_domain_context
        <<~CTX
          This is the MASTER constitutional AI agent web UI. Design intent:
          - Full-screen canvas particle face (WebGL-free, 2D Canvas API)
          - Particles form 3D face shape, morph between poses like a swarm
          - Black background, white/grey/dark-red particles, 1px only
          - Chat panel slides in from right, oh-my-zsh style prompt
          - Edge-tts Osman voice, server-side, AudioContext playback
          - Visitor access (no token), authenticated (token) tiers
          Critique CSS, JS, HTML semantics, animation, typography, layout, hierarchy, accessibility, and data-ink economy.
        CTX
      end

      def sound_domain_context
        <<~CTX
          Review MASTER as an interactive AI agent with visual motion, chat streaming, and voice/audio affordances.
          Treat sound design as product behavior, not decoration.
          Evaluate sonic hierarchy, timing, mix role, accessibility, graceful failure, implementation size, and TTS quality.
        CTX
      end

      def domain_briefs
        return platform_profile_brief if @mode[:preset_key] == "ui_critique"
        [sonitex_brief, dilla_brief, tts_lofi_brief].join("\n")
      end

      def platform_profile_brief
        return "Platform design profiles unavailable; default to content-first measurable critique." \
          unless defined?(Master::Design::PlatformProfiles)
        %i[brutal_minimal medium new_yorker].map { |k| Master::Design::PlatformProfiles.brief(k) }.join("\n")
      rescue StandardError => e
        "Platform profile policy failed to load: #{e.message}."
      end

      def sonitex_brief
        return Master::Voice::Sonitex.brief    if defined?(Master::Voice::Sonitex)
        return Master::Voice::SonitexSox.brief if defined?(Master::Voice::SonitexSox)
        "Sonitex/SoX policy unavailable; prefer cumulative subtle degradation and document SoX gaps."
      rescue StandardError => e
        "Sonitex/SoX policy failed to load: #{e.message}."
      end

      def dilla_brief
        return Master::Voice::Dilla.brief         if defined?(Master::Voice::Dilla)
        return Master::Voice::ProductionDna.brief if defined?(Master::Voice::ProductionDna)
        "Production DNA unavailable; keep timing human, restrained, and non-quantized when musical."
      rescue StandardError => e
        "Dilla production profile failed to load: #{e.message}."
      end

      def tts_lofi_brief
        return Master::Voice::TtsLofi.brief    if defined?(Master::Voice::TtsLofi)
        return Master::Voice::FfmpegLofi.brief if defined?(Master::Voice::FfmpegLofi)
        "TTS lofi policy unavailable; default to clean audio and make effects opt-in."
      rescue StandardError => e
        "TTS lofi policy failed to load: #{e.message}."
      end

      def ideation_value(ir)
        ir.respond_to?(:err?) && ir.err? ? "" : (ir.respond_to?(:value) ? ir.value : ir)
      end

      def cherry_pick_from(feedback, ideation_result)
        text = if ideation_result.respond_to?(:value)
                 ideation_result.value.is_a?(Hash) ? ideation_result.value.fetch(:final, "") : ideation_result.value.to_s
               else
                 ideation_result.to_s
               end
        cherry_pick(feedback, text)
      end

      def cherry_pick(feedback, ideas_text)
        feedback_text = feedback.map { |f| f[:feedback].to_s }.join("\n")
        lines = ideas_text.to_s.lines.map(&:strip).reject(&:empty?)
        lines.sort_by { |line| -text_overlap(line, feedback_text) }.first(12)
      end

      def text_overlap(a, b)
        wa = a.downcase.scan(/\w+/).to_set
        wb = b.downcase.scan(/\w+/).to_set
        return 0.0 if wa.empty? || wb.empty?
        (wa & wb).size.to_f / wa.size
      end
    end
  end
  end
end

lib/judge/council/deliberation.rb

# frozen_string_literal: true

module Master
  module Judge
  module Council
    module QualityFramework
      DEFAULT_QUESTIONS = {
        "assumptions" => [
          "what are we assuming that could be false?",
          "if a key assumption flips what still works?",
          "which assumptions have we never tested?",
          "what would happen if the opposite were true?",
          "which assumptions are load-bearing vs convenience?",
          "how do we validate assumptions incrementally?"
        ],
        "failure_modes" => [
          "how does this fail catastrophically?",
          "what breaks first under load or outage?",
          "which single point of failure is most likely?",
          "what happens when it fails silently?",
          "how do cascading failures propagate?",
          "what are blast radius containment strategies?"
        ],
        "attacker" => [
          "what would an attacker do here?",
          "where can inputs be abused or poisoned?",
          "which trust boundaries are weakest?",
          "how would we exploit this ourselves?",
          "what attack vectors are we not considering?",
          "how do we defend against insider threats?"
        ],
        "scale" => [
          "what happens at 10x users or data?",
          "what performance cliff exists and where?",
          "which bottleneck appears first?",
          "how does complexity grow with scale?",
          "what are the economics at different scales?",
          "which architectural decisions become problematic at scale?"
        ],
        "degradation" => [
          "how do we degrade gracefully?",
          "what is minimal viable behavior under stress?",
          "which features can we sacrifice first?",
          "how do we maintain core function during failure?",
          "what are UX implications of degradation?",
          "how do we communicate degraded service to users?"
        ],
        "edge_cases" => [
          "which edge cases will users hit first?",
          "which rare but high-impact case is unhandled?",
          "what happens with malformed inputs?",
          "how do we handle impossible combinations?",
          "which edge cases become common at scale?",
          "what edge cases exist in integration points?"
        ],
        "ops_maint" => [
          "what is long-term maintenance burden?",
          "how do we observe debug and rollback quickly?",
          "which operational complexity is hidden?",
          "how do we troubleshoot under pressure?",
          "what skills and knowledge are required for operations?",
          "how do we prevent operational knowledge from being siloed?"
        ],
        "compliance_ethics" => [
          "any privacy safety or fairness risks?",
          "which regulations apply and how prove compliance?",
          "what are ethical implications?",
          "how audit and demonstrate adherence?",
          "what happens when regulations change?",
          "how balance compliance with innovation?"
        ],
        "a11y_ux" => [
          "is it operable by keyboard and screen readers?",
          "what happens with reduced motion or low bandwidth?",
          "how does this work for colorblind users?",
          "can this be used with assistive technology?",
          "what are multilingual and cultural considerations?",
          "how test accessibility with actual users?"
        ],
        "economics" => [
          "where is waste or needless complexity?",
          "what is ROI vs simpler alternatives?",
          "which costs are hidden or deferred?",
          "how optimize for total cost of ownership?",
          "what are opportunity costs of this approach?",
          "how do economics change over time and scale?"
        ],
        "clarity" => [
          "is the intent obvious from names alone?",
          "which concept lacks a name and should have one?",
          "where does the code lie about what it does?",
          "what would a fresh reader misread first?",
          "which generic names hide domain meaning?"
        ],
        "evidence" => [
          "what evidence proves this works?",
          "which test would fail if this were wrong?",
          "what claim is unsupported?",
          "what would falsify this approach?"
        ],
        "scope" => [
          "what can be deleted without loss?",
          "which abstraction is premature?",
          "what is the smallest reversible change?",
          "where did implementation exceed the need?"
        ],
        "bottlenecks" => [
          "where is the Big-O bottleneck?",
          "what allocates in the hot path?",
          "what happens to latency at p95 and p99?",
          "which work can be skipped, cached, streamed, or deferred?"
        ],
        "consistency" => [
          "where can data go inconsistent?",
          "what is the source of truth?",
          "which names describe the same concept?",
          "which migration or state transition is not reversible?"
        ],
        "harm" => [
          "who could this harm if misused?",
          "what privacy boundary is crossed?",
          "what content or user data must remain untouched?",
          "what compliance proof would be required?"
        ],
        "visual" => [
          "where does the eye land first, and is that the right place?",
          "which element can be removed without losing meaning?",
          "does spacing prove grouping, or only decorate?",
          "which values violate the type scale, grid, or contrast rules?",
          "does the layout use absence as material?"
        ],
        "sound" => [
          "what should be foreground, background, or silent?",
          "does timing breathe, or is everything quantized to death?",
          "could sound mask speech, screen readers, or the user's task?",
          "is there a mute path and graceful media failure?",
          "which sound event communicates state instead of decoration?"
        ]
      }.freeze

      BRIEFS = {
        general: [
          "questions over commands; evidence over opinion; execution over explanation",
          "content integrity, critical security, accessibility, and reversibility are hard gates",
          "prefer surgical, reversible changes; preserve working behavior and user-curated content",
          "generate alternatives, red-team assumptions, then cherry-pick the simplest validated option"
        ].freeze,
        code: [
          "DRY after the third duplication; KISS when complexity exceeds 10; YAGNI for unused constructs",
          "SOLID boundaries: one reason to change, composition over inheritance, injected dependencies",
          "Ruby style: guard clauses, semantic names, no generic manager/handler/util names",
          "quality targets: complexity <= 10, nesting <= 4, duplication <= 3%, coverage >= 80%"
        ].freeze,
        design: [
          "typography is design: 45-75ch lines, 1.4-1.6 body leading, 16px minimum body text",
          "use an 8px spacing rhythm, 12-column structure, 44px touch minimum, and visible focus",
          "ultraminimalism: remove ornament until only hierarchy, alignment, type, and whitespace remain",
          "limit palette and type variety; reject non-token visual values and arbitrary decoration"
        ].freeze,
        sound: [
          "sound is feedback, not surprise: no autoplay without intent and mute must exist",
          "preserve silence; sounds need attack/decay/timing and must not mask speech or screen readers",
          "foreground sound is for critical state changes; midground for confirmations; background is optional",
          "prefer tiny browser-native tones/assets and graceful failure over heavy dependencies"
        ].freeze
      }.freeze

      PERSONA_DOMAIN = {
        "Graphic Designer" => :design,
        "Web Designer" => :design,
        "Motion Designer" => :design,
        "Google CSS Engineer" => :design,
        "NNGroup UX Researcher" => :design,
        "Accessibility" => :design,
        "Electronic Music Producer" => :sound,
        "Hip-Hop Producer" => :sound,
        "Sound Designer" => :sound,
        "Security" => :code,
        "Reliability" => :code,
        "Maintainer" => :code,
        "Performance" => :code,
        "QA Engineer" => :code
      }.freeze

      def self.questions
        council = if File.exist?(Deliberation::Master::COUNCIL_PATH)
                    Master.load_yaml(Deliberation::Master::COUNCIL_PATH).fetch("questions", {})
                  else
                    {}
                  end
        DEFAULT_QUESTIONS.merge(council) { |_key, builtin, custom| (Array(builtin) + Array(custom)).uniq }
      rescue StandardError
        DEFAULT_QUESTIONS
      end

      def self.domain_for(persona_name)
        PERSONA_DOMAIN.fetch(persona_name.to_s, :general)
      end

      def self.brief(domain = :general)
        ([*BRIEFS[:general], *BRIEFS.fetch(domain.to_sym, [])]).uniq.join("\n- ").then do |text|
          "Quality framework:\n- #{text}"
        end
      end
    end

    class Deliberation
      MAX_CONCURRENT       = 4
      MAX_CODE_BYTES       = 8_192
      TRUNCATE_MARKER      = "\n... [truncated to #{MAX_CODE_BYTES} bytes for review]".freeze
      JUDGE_TIMEOUT        = 30
      TOTAL_BUDGET_S       = 120
      MIN_QUORUM           = 3
      CONVERGENCE_ROUNDS   = 3
      CONVERGENCE_OVERLAP  = 0.7
      CONVERGENCE_TEXT_SIM = 0.6

      @questions = nil

      def self.questions
        @questions ||= QualityFramework.questions
      end

      def self.sample_question(persona)
        lens = persona.respond_to?(:cognitive_lens) ? persona.cognitive_lens : nil
        return nil unless lens
        questions[lens.to_s]&.sample
      end

      def self.quality_brief(domain = :general)
        QualityFramework.brief(domain)
      end

      def initialize(personas:, agent:, event_bus: nil, axioms: nil, judge_enabled: true, mode: :parallel)
        @personas      = personas
        @agent         = agent
        @bus           = event_bus
        @rules         = axioms
        @judge_enabled = judge_enabled
        @mode          = mode
        validate_dependencies!
      end

      def review_convergent(code, context: nil, max_rounds: CONVERGENCE_ROUNDS)
        history = []
        round_context = context
        max_rounds.times do |i|
          @bus&.publish(:council_round_start, round: i + 1, max: max_rounds)
          review_result = review(code, context: round_context)
          return review_result unless review_result.is_a?(Master::Result::Ok)
          feedback = review_result.value!
          history << feedback
          if history.size >= 2 && converged?(history[-2], history[-1])
            @bus&.publish(:council_converged, round: i + 1)
            break
          end
          round_context = feedback_summary(feedback, context)
        end
        Master::Result.ok(Array(history.last))
      end

      def review(code, context: nil)
        return Master::Result.err("council: no personas configured", category: :validation) if @personas.empty?

        feedback = @mode == :sequential ? collect_sequential(code, context) : collect_parallel(code, context)
        if feedback.size < MIN_QUORUM
          @bus&.publish(:council_timeout, completed: feedback.size, total: @personas.size)
          quorum_msg = "council: quorum not reached (#{feedback.size}/#{@personas.size})"
          return Master::Result.err(quorum_msg, category: :timeout)
        end

        vetoes = feedback.select { |f| f[:veto_role] && veto_text?(f[:feedback]) }
        unless vetoes.empty?
          veto = vetoes.first
          @bus&.publish(:council_veto, veto)
          return Master::Result.err("council: veto from #{veto[:persona]}\n#{veto[:feedback]}", category: :validation)
        end

        synthesis = @judge_enabled ? judge(feedback, code, context) : nil
        if synthesis
          @bus&.publish(:council_synthesis, synthesis: synthesis)
          feedback << { persona: "Judge", role: "Synthesis", veto_role: false,
                        axiom: nil, feedback: synthesis }
        end

        scores = feedback.filter_map { |f| f[:confidence] }
        council_confidence = scores.empty? ? 0.5 : scores.sum / scores.size
        @bus&.publish(:council_confidence, score: council_confidence.round(3), members: feedback.size)
        Master::Result.ok(feedback)
      rescue StandardError => e
        Master::Result.err("council: #{e.message}", category: :unknown)
      end

      private

      # Parallel fan-out — all personas in flight at once, bounded by MAX_CONCURRENT.
      def collect_parallel(code, context)
        return [] if circuit_open?
        slots = Mutex.new
        available = MAX_CONCURRENT
        ready = ConditionVariable.new

        deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + TOTAL_BUDGET_S
        threads = @personas.map do |persona|
          Thread.new do
            slots.synchronize { ready.wait(slots) until available > 0; available -= 1 }
            begin
              ask_persona(persona, code, context)
            ensure
              slots.synchronize { available += 1; ready.broadcast }
            end
          end
        end
        threads.filter_map { |t| join_or_kill(t, deadline) }
      end

      # Sequential handoff — each persona sees prior personas' feedback as context.
      # Slower than parallel but lets personas react to each other rather than
      # all speaking in isolation. Use when reactions and rebuttals matter more
      # than independent reads.
      def collect_sequential(code, context)
        deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + TOTAL_BUDGET_S
        acc = []
        @personas.each do |persona|
          break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
          break if circuit_open?
          turn_ctx = acc.empty? ? context : "#{context}\n\nprior turns:\n#{format_prior_turns(acc)}"
          entry = ask_persona(persona, code, turn_ctx)
          acc << entry if entry
        end
        acc
      end

      def circuit_open?
        breaker = @agent.respond_to?(:circuit_breaker) ? @agent.circuit_breaker : nil
        return false unless breaker.respond_to?(:open_models)
        !breaker.open_models.empty?
      rescue StandardError
        false
      end

      def ask_persona(persona, code, context)
        prompt   = build_prompt(persona, code, context)
        response = persona.model ? @agent.ask_once(prompt, model: persona.model) : @agent.ask(prompt)
        entry = { persona: persona.name, role: persona.role,
                  veto_role: veto_role?(persona), axiom: primary_axiom(persona),
                  model: persona.model, feedback: response, confidence: score_confidence(response) }
        @bus&.publish(:council_feedback, entry)
        entry
      rescue StandardError => e
        @bus&.publish("council:persona_error", persona: persona.name, error: e.message)
        nil
      end

      def format_prior_turns(entries)
        entries.map { |e| "#{e[:persona]} (#{e[:role]}): #{e[:feedback].to_s.lines.first(3).join.strip}" }.join("\n\n")
      end

      def join_or_kill(thread, deadline)
        remaining = [deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0.1].max
        thread.join(remaining) ? thread.value : (thread.kill; nil)
      end

      def converged?(prev, curr)
        return false if prev.empty? || curr.empty? || prev.size != curr.size
        prev_texts = prev.map { |f| f[:feedback].to_s }
        curr_texts = curr.map { |f| f[:feedback].to_s }
        same = curr_texts.zip(prev_texts).count { |c, p| text_similarity(c, p) >= CONVERGENCE_TEXT_SIM }
        same.to_f / curr_texts.size >= CONVERGENCE_OVERLAP
      end

      def text_similarity(a, b)
        return 0.0 if a.empty? || b.empty?
        sa = a.downcase.scan(/\w+/).uniq
        sb = b.downcase.scan(/\w+/).uniq
        union = (sa | sb).size
        union.zero? ? 0.0 : (sa & sb).size.to_f / union
      end

      def feedback_summary(feedback, base_context)
        lines = feedback.reject { |f| f[:persona] == "Judge" }.map do |f|
          "#{f[:persona]} (#{f[:role]}): #{f[:feedback].to_s.lines.first(2).join.strip}"
        end
        summary = "\nprior round:\n" + lines.join("\n") + "\n"
        [base_context, summary].compact.join
      end

      def judge(feedback, code, context)
        prompt = build_judge_prompt(feedback, code, context)
        @agent.ask(prompt)
      rescue StandardError => e
        @bus&.publish(:council_judge_error, error: e.message)
        nil
      end

      PROMPTS_PATH = File.join(Master::ROOT, "data", "prompts", "council.yml").freeze

      def self.prompts
        @prompts ||= Master.load_yaml(PROMPTS_PATH) || {}
      end

      def build_judge_prompt(feedback, code, _context)
        rounds = feedback.map { |f| format_round(f) }.join("\n\n")
        format(self.class.prompts.fetch("judge"),
               quality_brief: self.class.quality_brief(:general),
               rounds: rounds)
      end

      def format_round(f)
        axiom_tag = f[:axiom] ? "[#{f[:axiom]}] " : ""
        "#{axiom_tag}#{f[:persona]} (#{f[:role]}): #{f[:feedback]}"
      end

      def primary_axiom(persona)
        ids = persona.respond_to?(:emphasizes) ? Array(persona.emphasizes) : []
        ids.first
      end

      def axiom_line(persona)
        id = primary_axiom(persona)
        return "" unless id && @rules
        name = @rules.lookup(id)
        name ? "You speak primarily for the #{id} axiom: #{name}." : ""
      end

      def validate_dependencies!
        raise ArgumentError, "personas must be an array" unless @personas.is_a?(Array)
        raise ArgumentError, "agent must respond to :ask" unless @agent.respond_to?(:ask)
      end

      def veto_role?(persona)
        if persona.respond_to?(:veto?)
          persona.veto?
        else
          persona.respond_to?(:veto_role) && !!persona.veto_role
        end
      end

      def build_prompt(persona, code, context)
        ctx = context ? "\nContext: #{context}\n" : ""
        veto_hint = veto_role?(persona) ? " You may prefix VETO: if this must not ship." : ""
        safe_code = truncate_code(code.to_s)
        axiom = axiom_line(persona)
        axiom_block = axiom.empty? ? "" : "#{axiom}\n"
        quality_block = self.class.quality_brief(QualityFramework.domain_for(persona.name))
        question = self.class.sample_question(persona)
        question_block = question ? "\nAdversarial question for this turn: #{question}\n" : ""
        format(self.class.prompts.fetch("juror"),
               persona_name: persona.name, persona_role: persona.role,
               persona_bias: persona.bias, persona_prompt: persona.prompt,
               ctx: ctx, axiom_block: axiom_block, quality_block: quality_block,
               question_block: question_block, safe_code: safe_code, veto_hint: veto_hint)
      end

      def truncate_code(code)
        return code if code.bytesize <= MAX_CODE_BYTES
        @bus&.publish(:council_code_truncated, bytes: code.bytesize, limit: MAX_CODE_BYTES)
        code.byteslice(0, MAX_CODE_BYTES) + TRUNCATE_MARKER
      end

      VETO_RE = /\AVETO:/i.freeze

      def veto_text?(feedback)
        VETO_RE.match?(feedback.to_s.strip)
      end

      HIGH_CONF = /\b(certain|clearly|definitely|must|always|never|critical|serious)\b/i.freeze
      LOW_CONF = /\b(maybe|possibly|perhaps|unclear|might|could|unsure|uncertain)\b/i.freeze

      def score_confidence(text)
        t = text.to_s
        highs = t.scan(HIGH_CONF).size
        lows  = t.scan(LOW_CONF).size
        total = highs + lows
        return 0.5 if total.zero?
        (0.5 + (highs - lows).to_f / (total * 2)).clamp(0.1, 0.95)
      end
    end
  end
  end
end

lib/judge/council/ideation.rb

# frozen_string_literal: true

module Master
  module Judge
  module Council
    class Ideation
      DEFAULT_CYCLES = 2
      BUDGET_SECONDS = 4 * 60

      def initialize(agent:, event_bus: nil)
        @agent = agent
        @bus = event_bus
      end

      def ideate(prompt, constraints: [], cycles: DEFAULT_CYCLES, budget_seconds: BUDGET_SECONDS)
        ideas = []
        critiques = []
        deadline = Time.now + budget_seconds

        cycles.times do |cycle|
          return Master::Result.err("ideation: budget expired", category: :timeout) if Time.now >= deadline
          return Master::Result.err("ideation: circuit open", category: :infrastructure) if circuit_open?
          brainstorm_result = brainstorm(prompt, ideas, constraints)
          return brainstorm_result if brainstorm_result.err?
          ideas += brainstorm_result.value
          @bus&.publish("ideation:cycle", cycle: cycle + 1, ideas: ideas.size)

          return Master::Result.err("ideation: budget expired", category: :timeout) if Time.now >= deadline
          return Master::Result.err("ideation: circuit open", category: :infrastructure) if circuit_open?
          critique_result = critique(ideas)
          return critique_result if critique_result.err?
          critiques << critique_result.value
        end

        return Master::Result.err("ideation: budget expired", category: :timeout) if Time.now >= deadline
        synth_result = synthesize(prompt:, ideas:, critiques:, constraints:)
        return synth_result if synth_result.err?

        Master::Result.ok(ideas: ideas, critiques: critiques, final: synth_result.value)
      end

      private

      def circuit_open?
        breaker = @agent.respond_to?(:circuit_breaker) ? @agent.circuit_breaker : nil
        return false unless breaker.respond_to?(:open_models)
        !breaker.open_models.empty?
      rescue StandardError
        false
      end

      def brainstorm(prompt, prior, constraints)
        context           = prior.any? ? "Prior ideas (avoid repeating): #{prior.join('; ')}\n\n" : ""
        constraint_prefix = constraints.any? ? "Constraints: #{constraints.join(', ')}\n\n" : ""
        system_msg = "Generate 3-5 novel, bold ideas. One idea per bullet (- prefix)."
        raw = @agent.ask_once(<<~PROMPT, system: system_msg)
          #{constraint_prefix}#{context}Generate ideas for: #{prompt}
        PROMPT
        raw_text = raw.to_s
        return Master::Result.err("ideation: brainstorm failed") if raw_text.strip.empty?

        parsed = raw_text.scan(/^[-*]\s*(.+)/).flatten
        parsed = [raw_text.strip] if parsed.empty?
        Master::Result.ok(parsed)
      end

      def critique(ideas)
        list       = ideas.map { |idea| "- #{idea}" }.join("\n")
        system_msg = "Critique these ideas. Identify weaknesses, blind spots, risks. Be direct."
        raw        = @agent.ask_once(<<~PROMPT, system: system_msg)
          #{list}
        PROMPT
        raw_text = raw.to_s
        return Master::Result.err("ideation: critique failed") if raw_text.strip.empty?

        Master::Result.ok(raw_text.strip)
      end

      def synthesize(prompt:, ideas:, critiques:, constraints:)
        constraint_prefix = constraints.any? ? "Constraints: #{constraints.join(', ')}\n\n" : ""
        list              = ideas.map { |idea| "- #{idea}" }.join("\n")
        crits      = critiques.join("\n\n")
        system_msg = "Synthesize the best elements into a concrete, practical recommendation. " \
                     "Preserve innovation. Address valid critiques."
        raw = @agent.ask_once(<<~PROMPT, system: system_msg)
          Goal: #{prompt}
          #{constraint_prefix}
          Ideas:
          #{list}

          Critiques:
          #{crits}
        PROMPT
        raw_text = raw.to_s
        return Master::Result.err("ideation: synthesis failed") if raw_text.strip.empty?

        Master::Result.ok(raw_text.strip)
      end
    end
  end
  end
end

lib/judge/council/personas.rb

# frozen_string_literal: true

module Master
  module Judge
  module Council
    module Personas
      Persona = Data.define(:name, :role, :bias, :prompt, :veto_role,
                            :emphasizes, :weight, :aliases, :question, :cognitive_lens, :model) do
        def veto? = veto_role == true
      end

      PERSONA_DEFAULTS = {
        veto_role: false,
        emphasizes: [].freeze,
        weight: 0.05,
        aliases: [].freeze,
        question: nil,
        cognitive_lens: nil,
        model: nil
      }.freeze

      ROOT_DATA_PATH = File.join(File.expand_path("../../..", __dir__), "data", "council.yml").freeze

      DEFAULTS = [
        Persona.new(name: "Architect",  role: "System design",    bias: "Structure",
                    prompt: "Review for architectural soundness, coupling, and interface design.",
                    **PERSONA_DEFAULTS),
        Persona.new(name: "Skeptic",    role: "Devil's advocate", bias: "Caution",
                    prompt: "Find what could go wrong. Challenge every assumption.",
                    **PERSONA_DEFAULTS),
        Persona.new(name: "Pragmatist", role: "Implementation",   bias: "Shipping",
                    prompt: "Is this shippable? Flag over-engineering.",
                    **PERSONA_DEFAULTS),
        Persona.new(name: "Security",   role: "Security review",  bias: "Safety",
                    prompt: "Find injection vectors, auth bypasses, path traversals. Prefix VETO: if must not ship.",
                    **PERSONA_DEFAULTS.merge(veto_role: true)),
        Persona.new(name: "User",       role: "UX advocate",      bias: "Usability",
                    prompt: "Does this serve the user? Are error messages actionable?",
                    **PERSONA_DEFAULTS),
        Persona.new(name: "Mentor",     role: "Code review",      bias: "Clarity",
                    prompt: "Is this code readable? Do names reveal intent?",
                    **PERSONA_DEFAULTS)
      ].freeze

      persona_members = Persona.members
      ALLOWED_KEYS = persona_members.to_set.freeze

      @cache = {}

      def self.load(data_path = nil)
        path = data_path || ROOT_DATA_PATH
        return DEFAULTS unless File.exist?(path)

        @cache[path] ||= begin
          raw = Master.load_yaml(path, symbolize_names: true)
          rows = raw.is_a?(Array) ? raw : Array(raw.fetch(:personas) { raw["personas"] })
          raise "Invalid persona data" unless rows.is_a?(Array) && rows.any?

          rows.filter_map { |attrs| build_persona(attrs) }.freeze
        rescue StandardError => _e
          DEFAULTS
        end
      end

      def self.build_persona(attrs)
        return unless attrs.is_a?(Hash) && attrs[:name]
        normalised = PERSONA_DEFAULTS.merge(attrs)
        normalised[:veto_role] = normalised.delete(:can_veto) if normalised.key?(:can_veto)
        normalised = normalised.slice(*ALLOWED_KEYS)
        Persona.new(**normalised)
      end
    end
  end
  end
end

lib/judge/embeddings.rb

# frozen_string_literal: true

require "json"
require "net/http"
require "uri"

module Master
  module Judge
  module Embeddings
    module_function

    DEFAULT_MODEL = "nomic-embed-text"
    HTTP_TIMEOUT  = 5
    MIN_SIM       = 0.30

    @ollama_alive = nil

    def enabled? = !ENV["OLLAMA_BASE_URL"].to_s.strip.empty? && ollama_alive?

    def embed(text)
      return unless enabled?
      text_str = text.to_s
      return if text_str.strip.empty?
      ollama_embed(text_str)
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "embeddings.embed")
      nil
    end

    def ollama_alive?
      return @ollama_alive unless @ollama_alive.nil?
      uri  = URI.join(ENV["OLLAMA_BASE_URL"], "/api/tags")
      http = Net::HTTP.new(uri.host, uri.port)
      http.use_ssl      = uri.scheme == "https"
      http.open_timeout = HTTP_TIMEOUT
      http.read_timeout = HTTP_TIMEOUT
      res = http.get(uri.request_uri)
      @ollama_alive = res.is_a?(Net::HTTPSuccess)
    rescue StandardError
      @ollama_alive = false
    end

    def cosine(a, b)
      return 0.0 unless a.is_a?(Array) && b.is_a?(Array) && a.size == b.size && !a.empty?
      dot, na, nb = 0.0, 0.0, 0.0
      a.each_with_index do |x, i|
        y = b[i]
        dot += x * y
        na  += x * x
        nb  += y * y
      end
      mag = Math.sqrt(na) * Math.sqrt(nb)
      mag.zero? ? 0.0 : dot / mag
    end

    def ollama_embed(text)
      uri  = URI.join(ENV["OLLAMA_BASE_URL"], "/api/embeddings")
      http = Net::HTTP.new(uri.host, uri.port)
      http.use_ssl     = uri.scheme == "https"
      http.read_timeout = HTTP_TIMEOUT
      http.open_timeout = HTTP_TIMEOUT
      req = Net::HTTP::Post.new(uri.request_uri, "Content-Type" => "application/json")
      req.body = JSON.generate(model: ENV.fetch("EMBEDDINGS_MODEL", DEFAULT_MODEL), prompt: text)
      res = http.request(req)
      return unless res.is_a?(Net::HTTPSuccess)
      parsed = JSON.parse(res.body) rescue nil
      vec = parsed&.fetch("embedding", nil)
      vec.is_a?(Array) ? vec : nil
    end
  end
  end
end

lib/judge/llm_dispatcher.rb

# frozen_string_literal: true

require "ruby_llm"
require "digest"
require "json"
require "open3"

module Master
  module Judge
    class LLMDispatcher
      COST_PER_TOKEN = 0.000_015
      CACHE_WINDOW = 4
      REACT_MAX_STEPS = 8
      NEMOTRON3_RE = /nemotron-3/i.freeze
      LLAMA_NEMOTRON_RE = /llama.*nemotron|nemotron.*llama/i.freeze
      TOOL_CALL_RE = /<tool_call>(.*?)<\/tool_call>/m.freeze
      TOOL_RESULT_ROLE = "user"
      KEY_PATTERNS = [
        /sk-[A-Za-z0-9_\-]{16,}/,
        /sk-ant-[A-Za-z0-9_\-]{16,}/,
        /Bearer\s+[A-Za-z0-9_\-\.]{16,}/i,
        /\b[A-Za-z0-9]{32,}\b/
      ].freeze

      LLM_TOOL_MAP = {
        Reach::ReadFile => Reach::LLM::ReadFile,
        Reach::WriteFile => Reach::LLM::WriteFile,
        Reach::StrReplace => Reach::LLM::StrReplace,
        Reach::ListDir => Reach::LLM::ListDir,
        Reach::SearchFiles => Reach::LLM::SearchFiles,
        Reach::Shell => Reach::LLM::Shell,
        Reach::WebSearch => Reach::LLM::WebSearch,
        Reach::AskLlm => Reach::LLM::AskLlm,
        Reach::GitContext => Reach::LLM::GitContext,
        Reach::AstEdit => Reach::LLM::AstEdit,
        Reach::SearchKnowledge => Reach::LLM::SearchKnowledge,
        Reach::FeedbackRecord => Reach::LLM::FeedbackRecord,
        Reach::MemoryRecord => Reach::LLM::MemoryRecord
      }.freeze

      def self.build_tool_capable_re
        yml_path = File.join(Master::ROOT, "data", "models.yml")
        prefixes = Master.load_yaml(yml_path).fetch("tool_capable_prefixes", [])
        escaped  = prefixes.map { |p| Regexp.escape(p) }
        Regexp.new("\\A(?:#{escaped.join("|")})(?:[:\\/@\\-.].+)?\\z", Regexp::IGNORECASE).freeze
      end

      TOOL_CAPABLE_RE = build_tool_capable_re.freeze

      def initialize(deps:, system_prompt:)
        @config, @cache, @circuit_breaker = deps.config, deps.cache, deps.circuit_breaker
        @tools, @bus, @system_prompt_proc = deps.tools, deps.bus, system_prompt
        @model_router  = deps.model_router
        @session       = deps.session
        @tool_registry = load_tool_registry
      end

      def send_with_cache(selected_model, messages, system: nil, stream: false, &blk)
        cache_key = cache_key_for(messages.last[:content], messages[0...-1], selected_model)
        breaker_for(selected_model).call(estimate_cost(messages.last[:content])) {
          @cache.fetch(cache_key, selected_model) {
            send_llm_request(selected_model, messages, system:, stream:, &blk)
          }
        }
      rescue Reach::CircuitBreaker::CircuitError => err
        Result.err(redact_secrets(err.message), category: err.category)
      rescue StandardError => err
        return Result.err(Master.no_api_key_message, category: :no_api_key) if missing_key_error?(err)
        Result.err(redact_secrets(err.message.to_s), category: :llm_call_failure)
      end

      def redact_secrets(text)
        out = text.to_s
        KEY_PATTERNS.each { |re| out = out.gsub(re, "[REDACTED]") }
        out
      end

      def missing_key_error?(err)
        msg = err.message.to_s
        msg.match?(/missing configuration/i) ||
          msg.match?(/api[_\- ]?key/i) ||
          msg.match?(/unauthorized/i) ||
          msg.match?(/401/) ||
          !Master.any_api_key_present?
      end

      def claude_cli_model?(model_id) = model_id.to_s.start_with?("claude-cli:")
      def web_chat_model?(model_id)   = model_id.to_s.start_with?("web-chat:")
      def tool_capable?(model_id)     = TOOL_CAPABLE_RE.match?(model_id.to_s.downcase)

      private

      def system_prompt = @system_prompt_proc.call

      def send_llm_request(selected_model, messages, system: nil, stream: false, &blk)
        sys = system || system_prompt
        return send_claude_cli(selected_model.delete_prefix("claude-cli:"), messages, sys:) if claude_cli_model?(selected_model)
        return send_web_chat(selected_model.delete_prefix("web-chat:"), messages, sys:)     if web_chat_model?(selected_model)
        if !tool_capable?(selected_model) && @tools.any?
          return react_tool_loop(selected_model, messages, sys:, stream:, &blk)
        end
        send_ruby_llm(selected_model, messages, sys:, stream:, &blk)
      end

      def send_claude_cli(model_alias, messages, sys:)
        args = ["claude", "--print", "--model", model_alias]
        args += ["--system-prompt", sys] if sys && !sys.empty?
        out, err, status = Open3.capture3(*args, stdin_data: text_prompt_for(messages))
        return Result.err("claude-cli: #{err.strip}", category: :provider_error) unless status.success?
        Result.ok(out.strip)
      rescue StandardError => e
        Result.err("claude-cli: #{e.message}", category: :provider_error)
      end

      def send_web_chat(provider, messages, sys:)
        Result.ok(WebChat.call(provider:, prompt: text_prompt_for(messages), system: sys))
      rescue StandardError => e
        Result.err("web-chat: #{e.message}", category: :provider_error)
      end

      # ReactToolLoop — emulates function calling for models that lack native tool support.
      # Injects a text-format tool schema into the system prompt; parses <tool_call> XML
      # from responses; executes tools directly; loops until no calls remain.
      def react_tool_loop(selected_model, messages, sys:, stream:, &blk)
        react_sys = build_react_system(sys)
        history   = messages.dup
        last      = nil

        REACT_MAX_STEPS.times do |step|
          result = send_ruby_llm(selected_model, history, sys: react_sys, stream: step.zero? ? stream : false, &(step.zero? ? blk : nil))
          return result if result.err?

          text  = result.to_s
          calls = parse_tool_calls(text)
          last  = result
          break if calls.empty?

          @bus&.publish("react:tool_calls", model: selected_model, step:, count: calls.size)
          history << { role: "assistant", content: text }
          tool_results = calls.map { |c| execute_react_tool(c["name"], c["args"] || {}) }
          history << { role: TOOL_RESULT_ROLE, content: tool_results.join("\n\n") }
        end

        last || Result.err("react: no response generated", category: :llm_call_failure)
      end

      def build_react_system(base_sys)
        schema = @tools.filter_map { |t|
          name = t.class.name.split("::").last
          meta = @tool_registry.fetch(name, {})
          desc = meta["description"] || name.gsub(/([A-Z])/, ' \1').strip
          "- #{name}: #{desc}"
        }.join("\n")

        react_instructions = <<~INST.strip
          You have access to these tools. Call a tool with:
          <tool_call>{"name": "ToolName", "args": {"param": "value"}}</tool_call>

          Available tools:
          #{schema}

          Reason step-by-step. When finished, give your final answer without any <tool_call> blocks.
        INST

        [base_sys, react_instructions].compact.join("\n\n")
      end

      def parse_tool_calls(text)
        text.scan(TOOL_CALL_RE).filter_map do |match|
          JSON.parse(match.first.strip)
        rescue JSON::ParserError
          nil
        end
      end

      def execute_react_tool(name, args)
        tool = @tools.find { |t| t.class.name.split("::").last == name }
        return "<tool_result name=\"#{name}\">error: tool not found</tool_result>" unless tool
        sym_args = args.transform_keys(&:to_sym)
        raw = tool.respond_to?(:call) ? tool.call(**sym_args) : "unsupported"
        out = Result.wrap(raw).value_or(raw.to_s)
        "<tool_result name=\"#{name}\">\n#{out}\n</tool_result>"
      rescue StandardError => e
        "<tool_result name=\"#{name}\">error: #{e.message}</tool_result>"
      end

      def text_prompt_for(messages)
        prompt  = messages.last[:content].to_s
        context = messages[0...-1].map { |m| "#{m[:role]}: #{m[:content]}" }.join("\n\n")
        context.empty? ? prompt : "#{context}\n\nuser: #{prompt}"
      end

      def send_ruby_llm(selected_model, messages, sys:, stream:, &blk)
        chat_session = RubyLLM.chat(model: selected_model)
        final_sys    = nemotron_system_prompt(selected_model, sys)
        chat_session.with_instructions(final_sys) if final_sys
        messages.each { |msg| chat_session.add_message(role: msg[:role].to_s, content: msg[:content].to_s) }

        available_tools = llm_tools(selected_model)
        chat_session.with_tools(*available_tools) unless available_tools.empty?

        reply = if stream && blk
          chat_session.ask(messages.last[:content]) { |chunk| blk.call(chunk.content.to_s) if chunk.content }
        else
          chat_session.ask(messages.last[:content])
        end
        record_usage(reply, selected_model)
        Result.ok(extract_response(reply, selected_model))
      end

      def record_usage(reply, model)
        return unless @session
        input  = reply.respond_to?(:input_tokens)  ? reply.input_tokens.to_i  : 0
        output = reply.respond_to?(:output_tokens) ? reply.output_tokens.to_i : 0
        tokens = input + output
        if tokens.zero? && reply.respond_to?(:content)
          tokens = Master::Trace::Session.estimate_tokens(reply.content)
        end
        return if tokens.zero?
        @session.record_cost((tokens * COST_PER_TOKEN).round(6), model:, tokens:)
      rescue StandardError => e
        @bus&.publish("cost:record_error", error: e.message)
      end

      def breaker_for(model_id)
        @circuit_breaker.respond_to?(:for) ? @circuit_breaker.for(model_id) : @circuit_breaker
      end

      def extract_response(reply, selected_model)
        return reply.to_s unless reply.respond_to?(:content)
        if NEMOTRON3_RE.match?(selected_model) && reply.respond_to?(:reasoning_content)
          thinking = reply.reasoning_content.to_s.strip
          content  = reply.content.to_s
          return thinking.empty? ? content : "#{content}\n\n<think>\n#{thinking}\n</think>"
        end
        reply.content.to_s
      end

      def nemotron_system_prompt(selected_model, base = nil)
        sys = base || system_prompt
        return sys unless LLAMA_NEMOTRON_RE.match?(selected_model)
        directive = @config["reasoning_mode"] != "none" ? "detailed thinking on" : "detailed thinking off"
        [directive, sys].compact.join("\n\n")
      end

      def cache_key_for(message, context, model = nil)
        parts = model ? "#{model}\n#{message}" : message
        return Digest::SHA256.hexdigest(parts) if context.empty?
        window = context.last(CACHE_WINDOW).map { |msg| "#{msg[:role]}:#{msg[:content]}" }.join("\n")
        Digest::SHA256.hexdigest("#{parts}\n#{window}")
      end

      def estimate_cost(prompt)
        Master::Trace::Session.estimate_tokens(prompt) * COST_PER_TOKEN
      end

      def llm_tools(selected_model)
        return [] unless tool_capable?(selected_model)
        return build_llm_tools(visitor: true) if Fiber[:master_visitor]
        @llm_tools ||= build_llm_tools
      end

      def build_llm_tools(visitor: false)
        tier = @model_router&.tier_for_model(@config.model).to_s
        @tools.filter_map do |tool|
          wrapper  = LLM_TOOL_MAP[tool.class]
          next unless wrapper
          name = tool.class.name.split("::").last
          meta = @tool_registry.fetch(name, {})
          next if visitor && meta["visitor"] != true
          next if tier == "cheap" && meta["tier"] == "dangerous"
          wrapper.new(tool)
        end
      rescue StandardError => err
        @bus&.publish("agent:llm_tools_error", error: err.message)
        []
      end

      def load_tool_registry
        path = File.join(Master::ROOT, "data", "tools.yml")
        rows = Master.load_yaml(path)
        return {} unless rows.is_a?(Array)
        rows.each_with_object({}) { |row, h| h[row["name"].to_s] = row if row.is_a?(Hash) }
      end
    end
  end
end

lib/judge/modes.rb

# frozen_string_literal: true

module Master
  module Judge
    class Modes
      SUPPORTED = %w[direct react rewoo code_agent].freeze

      def initialize(root: Master::ROOT)
        @root = root
      end

      def supported = SUPPORTED

      def wrap(message, mode: "direct")
        selected = SUPPORTED.include?(mode.to_s) ? mode.to_s : "direct"
        prompt = load_prompt(selected)
        format(prompt.fetch("template", "%{message}"), message: message.to_s)
      rescue StandardError => e
        warn "judge/modes: wrap failed (mode=#{mode}): #{e.message}"
        message.to_s
      end

      private

      def load_prompt(mode)
        path = File.join(@root, "data", "prompts", "mode_#{mode}.yml")
        Master.load_yaml(path) || {}
      end
    end
  end
end

lib/judge/reference_graph.rb

# frozen_string_literal: true

require "find"
require "set"

module Master
  module Judge
  # ReferenceGraph builds a lightweight semantic connectivity map
  # across the repository. This becomes the foundation for:
  # - safe rename
  # - precise rollback
  # - dead-file proof
  # - blast-radius estimation
  # - topology visualization
  # - governed autonomous restructuring
  class ReferenceGraph
    DEFAULT_EXTENSIONS = %w[
      .rb .js .ts .tsx .erb .yml .yaml .json .md .sh
    ].freeze

    IGNORE_DIRS = %w[
      .git node_modules vendor tmp log coverage dist build .bundle
    ].freeze

    Edge = Struct.new(:type, :from, :to, :weight, keyword_init: true)

    attr_reader :nodes, :edges

    def initialize(root:, event_bus: nil)
      @root = File.expand_path(root)
      @bus = event_bus
      @nodes = Set.new
      @edges = []
    end

    def build
      files.each do |file|
        analyze(file)
      end

      graph = {
        nodes: @nodes.to_a.sort,
        edges: @edges.map(&:to_h),
        metrics: metrics
      }

      @bus&.publish(
        "reference_graph:built",
        nodes: graph[:nodes].size,
        edges: graph[:edges].size
      )

      graph
    end

    def blast_radius(path)
      rel = relative(path)
      impacted = @edges.select { |edge| edge.to == rel || edge.from == rel }
      {
        target: rel,
        inbound: impacted.select { |e| e.to == rel }.map(&:from).uniq.sort,
        outbound: impacted.select { |e| e.from == rel }.map(&:to).uniq.sort
      }
    end

    private

    def files
      collected = []
      Find.find(@root) do |path|
        name = File.basename(path)

        if File.directory?(path)
          Find.prune if IGNORE_DIRS.include?(name)
          next
        end

        next unless DEFAULT_EXTENSIONS.include?(File.extname(path))

        collected << path
      end
      collected
    end

    def analyze(file)
      rel = relative(file)
      @nodes << rel

      content = File.read(file, encoding: "UTF-8", invalid: :replace, undef: :replace)

      ruby_requires(content, rel)
      constant_refs(content, rel)
      command_refs(content, rel)
      event_refs(content, rel)
    rescue StandardError => e
      Master::Ground::Swallow.log(
        e,
        context: "reference_graph.analyze",
        event_bus: @bus,
        path: rel
      )
    end

    def ruby_requires(content, rel)
      content.scan(/require(?:_relative)?\s+["']([^"']+)["']/).flatten.each do |target|
        edge(from: rel, to: normalize_require(target), type: :require, weight: 1.0)
      end
    end

    def constant_refs(content, rel)
      content.scan(/\b([A-Z][A-Za-z0-9_:]{2,})\b/).flatten.tally.each do |constant, count|
        edge(from: rel, to: "const:#{constant}", type: :constant, weight: count)
      end
    end

    def command_refs(content, rel)
      content.scan(%r{/[a-z_]+}).flatten.tally.each do |command, count|
        edge(from: rel, to: "command:#{command}", type: :command, weight: count)
      end
    end

    def event_refs(content, rel)
      content.scan(/["']([a-z_]+:[a-z_]+)["']/).flatten.tally.each do |event, count|
        edge(from: rel, to: "event:#{event}", type: :event, weight: count)
      end
    end

    def normalize_require(target)
      cleaned = target.gsub("../", "")
      cleaned.end_with?(".rb") ? cleaned : "#{cleaned}.rb"
    end

    def edge(from:, to:, type:, weight:)
      @edges << Edge.new(
        type:,
        from:,
        to:,
        weight:
      )
    end

    def metrics
      grouped = @edges.group_by(&:type)
      {
        node_count: @nodes.size,
        edge_count: @edges.size,
        require_edges: grouped.fetch(:require, []).size,
        constant_edges: grouped.fetch(:constant, []).size,
        event_edges: grouped.fetch(:event, []).size,
        command_edges: grouped.fetch(:command, []).size
      }
    end

    def relative(path)
      File.expand_path(path).sub(%r{\A#{Regexp.escape(@root)}/?}, "")
    end
  end
  end
end

lib/judge/reflexion.rb

# frozen_string_literal: true

module Master
  module Judge
  module Reflexion
    MAX_REFLECTIONS = 3
    TASK_TRUNCATE = 400
    HISTORY_TRUNCATE = 200
    BUDGET_SECONDS = 5 * 60

    module_function

    def run(agent:, task:, fast_model: nil, max: MAX_REFLECTIONS, budget_seconds: BUDGET_SECONDS)
      last_result = nil
      last_critique = nil
      deadline = Time.now + budget_seconds

      (max + 1).times do |i|
        break if Time.now >= deadline
        prompt = i.zero? ? task : build_revision_prompt(task, last_result, last_critique)
        last_result = yield(prompt, i)
        return last_result if last_result.is_a?(Master::Result) && last_result.ok?

        break if i >= max
        break if circuit_open?(agent)
        last_critique = critique(agent:, task:, result: last_result, fast_model:)
      end

      last_result
    end

    def circuit_open?(agent)
      breaker = agent.respond_to?(:circuit_breaker) ? agent.circuit_breaker : nil
      return false unless breaker.respond_to?(:open_models)
      !breaker.open_models.empty?
    rescue StandardError
      false
    end

    def critique(agent:, task:, result:, fast_model: nil)
      prompt = <<~PROMPT
        Task: #{task.to_s[0, TASK_TRUNCATE]}
        Attempt output: #{result.to_s[0, TASK_TRUNCATE]}
        What specifically went wrong? Name the constraint violated.
        What must change in the next attempt? One paragraph, no preamble.
      PROMPT
      resp = fast_model ? agent.ask_once(prompt, model: fast_model) : agent.ask(prompt)
      resp.respond_to?(:value!) ? resp.value! : resp.to_s
    rescue StandardError => _e
      "previous attempt failed — try a different approach"
    end

    def build_revision_prompt(task, previous_result, critique)
      <<~PROMPT
        #{task}

        Previous attempt failed.
        Critique: #{critique}
        Previous output: #{previous_result.to_s[0, HISTORY_TRUNCATE]}

        Revise based on the critique. Return only the corrected result.
      PROMPT
    end
  end
  end
end

lib/judge/repo_ecology.rb

# frozen_string_literal: true

require "digest"
require "find"
require "set"

module Master
  module Judge
  # RepoEcology converts repo-gardening principles into executable analysis.
  # It never deletes or rewrites; it emits evidence for later governed changes.
  class RepoEcology
    DEFAULT_IGNORE_DIRS = %w[
      .git .master node_modules vendor tmp log coverage storage .bundle dist build
    ].freeze
    DEFAULT_EXTENSIONS = %w[.rb .js .ts .erb .html .css .scss .yml .yaml .json .md .sh .zsh].freeze
    LARGE_FILE_LINES = 420
    DUPLICATE_BASENAME_LIMIT = 4
    MAX_DEAD_CANDIDATES = 40
    MAX_CLUSTERS = 20

    def initialize(root:, event_bus: nil, ignore_dirs: DEFAULT_IGNORE_DIRS)
      @root = File.expand_path(root)
      @bus = event_bus
      @ignore_dirs = ignore_dirs.to_set
    end

    def scan(path: nil)
      base = path ? File.expand_path(path, @root) : @root
      files = collect_files(base)
      records = files.map { |file| analyze_file(file) }
      scanned_utc = Time.now.utc
      report = {
        root: @root,
        scanned_at: scanned_utc.iso8601,
        files: records.size,
        score: score(records),
        dead_file_candidates: dead_file_candidates(records),
        duplicate_basenames: duplicate_basenames(records),
        similar_clusters: similar_clusters(records),
        sprawl: sprawl(records),
        large_files: large_files(records),
        extension_mix: extension_mix(records)
      }
      @bus&.publish("repo_ecology:scan", files: records.size, score: report[:score])
      report
    end

    def render(report)
      lines = []
      lines << "# Repo ecology"
      lines << "score: #{report[:score][:grade]} (#{report[:score][:value]}/100)"
      lines << "files: #{report[:files]}"
      lines << ""
      lines.concat(render_section("Dead-file candidates", report[:dead_file_candidates]) { |item|
        "#{item[:path]}#{item[:reason]}"
      })
      lines.concat(render_section("Duplicate basenames", report[:duplicate_basenames]) { |item|
        "#{item[:basename]} ×#{item[:count]}: #{item[:paths].first(5).join(', ')}"
      })
      lines.concat(render_section("Similar clusters", report[:similar_clusters]) { |item|
        "#{item[:signature]} ×#{item[:count]}: #{item[:paths].first(5).join(', ')}"
      })
      lines.concat(render_section("Large files", report[:large_files]) { |item|
        "#{item[:path]}#{item[:lines]} lines"
      })
      lines << ""
      sprawl = report[:sprawl]
      lines << "sprawl: max_depth=#{sprawl[:max_depth]}, avg_depth=#{sprawl[:avg_depth]}, " \
               "orphan_dirs=#{sprawl[:orphan_dirs]}"
      lines << "extensions: #{report[:extension_mix].map { |ext, count| "#{ext}=#{count}" }.join(', ')}"
      lines.join("\n")
    end

    private

    def collect_files(base)
      return [] unless File.exist?(base)
      files = []
      Find.find(base) do |path|
        name = File.basename(path)
        if File.directory?(path)
          Find.prune if @ignore_dirs.include?(name)
          next
        end
        next unless DEFAULT_EXTENSIONS.include?(File.extname(path).downcase)
        files << path
      end
      files.sort
    end

    def analyze_file(file)
      rel = relative(file)
      content = File.read(file, encoding: "UTF-8", invalid: :replace, undef: :replace)
      tokens = content.downcase.scan(/[a-z][a-z0-9_]{2,}/)
      {
        path: rel,
        full_path: file,
        basename: File.basename(file),
        dirname: File.dirname(rel),
        ext: File.extname(file).downcase,
        bytes: content.bytesize,
        lines: content.lines.size,
        tokens: tokens,
        digest: Digest::SHA256.hexdigest(content),
        signature: signature(tokens, rel),
        inbound_refs: 0
      }
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "repo_ecology.analyze_file", event_bus: @bus, path: file)
      nil
    end

    def score(records)
      records = records.compact
      penalty = 0
      penalty += duplicate_basenames(records).sum { |item| [item[:count] - DUPLICATE_BASENAME_LIMIT, 0].max * 3 }
      penalty += similar_clusters(records).sum { |item| [item[:count] - 1, 0].max * 4 }
      penalty += large_files(records).size * 2
      penalty += sprawl(records)[:orphan_dirs] * 2
      value = [[100 - penalty, 0].max, 100].min
      { value:, grade: grade_for(value) }
    end

    def grade_for(value)
      return "excellent" if value >= 90
      return "good" if value >= 75
      return "strained" if value >= 55
      "fragmented"
    end

    def dead_file_candidates(records)
      records = records.compact
      corpus  = records.map { |record| [record[:path], record[:tokens].join(" ")] }.to_h
      records.filter_map { |r| dead_candidate(r, corpus) }.first(MAX_DEAD_CANDIDATES)
    end

    def dead_candidate(record, corpus)
      return nil if protected_path?(record[:path])
      stem    = File.basename(record[:basename], record[:ext]).downcase
      inbound = corpus.count { |path, text| path != record[:path] && text.include?(stem) }
      record[:inbound_refs] = inbound
      return nil unless inbound.zero?
      return nil if record[:lines] < 3
      { path: record[:path], reason: "no stem references found", lines: record[:lines] }
    end

    def protected_path?(path)
      path == "README.md" || path == "AGENTS.md" || path.start_with?(".github/") ||
        path.include?("/test/") || path.include?("/spec/") || path.end_with?("Gemfile")
    end

    def duplicate_basenames(records)
      records.compact.group_by { |record| record[:basename] }
             .filter_map do |basename, group|
        next if group.size < DUPLICATE_BASENAME_LIMIT
        { basename:, count: group.size, paths: group.map { |record| record[:path] }.sort }
      end.sort_by { |item| [-item[:count], item[:basename]] }
    end

    def similar_clusters(records)
      records.compact.group_by { |record| record[:signature] }
             .filter_map do |sig, group|
        next if sig.empty? || group.size < 2
        next if group.map { |record| record[:digest] }.uniq.size == group.size && group.size < 3
        { signature: sig, count: group.size, paths: group.map { |record| record[:path] }.sort }
      end.sort_by { |item| [-item[:count], item[:signature]] }.first(MAX_CLUSTERS)
    end

    def signature(tokens, rel)
      important = tokens.reject { |token| token.length < 4 || token.match?(/\A\d+\z/) }
      vocabulary = important.tally.sort_by { |token, count| [-count, token] }.first(12).map(&:first)
      return "" if vocabulary.size < 4
      "#{File.extname(rel)}:#{vocabulary.sort.join('-')}"
    end

    def sprawl(records)
      dirs = records.compact.map { |record| record[:dirname] }
      depths = dirs.map { |dir| dir == "." ? 0 : dir.split(File::SEPARATOR).size }
      counts = dirs.tally
      {
        max_depth: depths.max || 0,
        avg_depth: depths.empty? ? 0 : (depths.sum.to_f / depths.size).round(2),
        orphan_dirs: counts.count { |_dir, count| count == 1 }
      }
    end

    def large_files(records)
      records.compact.select { |record| record[:lines] >= LARGE_FILE_LINES }
             .map { |record| { path: record[:path], lines: record[:lines] } }
             .sort_by { |item| -item[:lines] }
             .first(25)
    end

    def extension_mix(records)
      records.compact.map { |record| record[:ext].empty? ? "[none]" : record[:ext] }
             .tally.sort_by { |ext, count| [-count, ext] }.to_h
    end

    def render_section(title, items)
      lines = ["", "## #{title}"]
      if items.empty?
        lines << "none"
      else
        items.first(12).each { |item| lines << "- #{yield(item)}" }
      end
      lines
    end

    def relative(file)
      File.expand_path(file).sub(%r{\A#{Regexp.escape(@root)}/?}, "")
    end
  end
  end
end

lib/judge/repo_map.rb

# frozen_string_literal: true

require "set"

module Master
  module Judge
  # PageRank over the CodeIndex symbol-reference graph, budgeted to a
  # token window. Aider-style repo map. Files with many incoming references
  # rank higher; focus[] biases the random surfer toward chat-mentioned files.
  #
  # Wiring is left to the operator. Typical use:
  #   map = Master::RepoMap.new(code_index: ai[:code_index], root: root)
  #   prompt_context << map.render(focus: [current_file])
  class RepoMap
    DEFAULT_TOKEN_BUDGET = 4096
    CHARS_PER_TOKEN      = 4
    DAMPING              = 0.85
    ITERATIONS           = 30
    MAX_DEFS_PER_FILE    = 20

    def initialize(code_index:, root:, token_budget: DEFAULT_TOKEN_BUDGET)
      @index  = code_index
      @root   = File.expand_path(root)
      @budget = token_budget
    end

    def render(focus: [])
      @index.wait_for_build unless @index.ready?
      g       = build_graph
      ranks   = pagerank(g, normalize(focus))
      ordered = ranks.sort_by { |_, r| -r }.map(&:first)
      pack(ordered)
    end

    private

    def normalize(focus)
      focus.map { |f| File.expand_path(f, @root) }.to_set
    end

    def build_graph
      symbols = @index.symbols
      owners = symbols.each_value.to_h { |s| [s.fqn, s.file] }
      edges  = Hash.new { |h, k| h[k] = Set.new }
      @index.references.each do |ref|
        target = owners[ref.to_fqn]
        edges[ref.from_file] << target if target && target != ref.from_file
      end
      edges
    end

    def pagerank(g, focus)
      nodes = (g.keys | g.values.flat_map(&:to_a)).uniq
      return {} if nodes.empty?
      rank = seed(nodes, focus)
      ITERATIONS.times { rank = step(g:, nodes:, rank:, focus:) }
      rank
    end

    def seed(nodes, focus)
      base = focus.empty? ? 1.0 / nodes.size : 1.0 / focus.size
      nodes.to_h { |n| [n, focus.empty? || focus.include?(n) ? base : 0.0] }
    end

    def step(g:, nodes:, rank:, focus:)
      tele = teleport(nodes, focus)
      out  = nodes.to_h { |n| [n, tele[n]] }
      nodes.each do |from|
        successors = g[from]
        next if successors.empty?
        share = DAMPING * rank[from] / successors.size
        successors.each { |to| out[to] += share }
      end
      out
    end

    def teleport(nodes, focus)
      if focus.empty?
        base = (1 - DAMPING) / nodes.size
        nodes.to_h { |n| [n, base] }
      else
        base = (1 - DAMPING) / focus.size
        nodes.to_h { |n| [n, focus.include?(n) ? base : 0.0] }
      end
    end

    def pack(ordered)
      sections = ["# Repo map", ""]
      tokens   = estimate(sections.join("\n"))
      ordered.each do |path|
        block = format_file(path)
        next if block.empty?
        cost = estimate(block.join("\n"))
        break if tokens + cost > @budget
        sections.concat(block)
        tokens += cost
      end
      sections.join("\n")
    end

    def format_file(path)
      symbols = @index.symbols_in(path)
      return [] if symbols.empty?
      rel  = path.sub("#{@root}/", "")
      defs = symbols.first(MAX_DEFS_PER_FILE).map { |s| "  #{s.type}: #{s.fqn} (line #{s.line})" }
      ["## #{rel}", *defs, ""]
    end

    def estimate(text) = text.bytesize / CHARS_PER_TOKEN
  end
  end
end

lib/judge/scan/ast_fixer.rb

# frozen_string_literal: true

require "prism"

module Master
  module Judge
  module Scan
  # Architecture #4: deterministic AST-level autofixes for mechanical rules.
  # No LLM call, no token cost. Applied before LLM sweep on every scan cycle.
  # Each transform is idempotent — safe to apply repeatedly.
  class AstFixer
    FROZEN_HEADER  = "# frozen_string_literal: true\n"
    BARE_RESCUE_RE = /^(\s*)rescue(\s*\n|\s*=>)/.freeze

    Result = Struct.new(:path, :changed, :transforms, keyword_init: true)

    # Apply all eligible fixes to +source+ for +path+.
    # Returns Result with :changed (bool) and :transforms (array of applied fix names).
    def self.fix(path, source)
      new(path, source).apply
    end

    def initialize(path, source)
      @path       = path
      @source     = source
      @transforms = []
    end

    def apply
      out = @source
      out = add_frozen_header(out)       if ruby?
      out = fix_bare_rescue(out)         if ruby?
      out = normalise_null_comparison(out) if sql_in_ruby?
      changed = out != @source
      Result.new(path: @path, changed: changed, transforms: @transforms)
        .tap { write_back(out) if changed }
    end

    private

    # Add frozen_string_literal comment if absent.
    def add_frozen_header(src)
      return src if src.start_with?(FROZEN_HEADER)
      # Preserve shebang if present.
      if src.start_with?("#!")
        lines = src.lines
        lines.insert(1, FROZEN_HEADER)
        @transforms << :frozen_string_literal
        lines.join
      else
        @transforms << :frozen_string_literal
        FROZEN_HEADER + "\n" + src.lstrip
      end
    end

    # Replace bare `rescue` with `rescue StandardError`.
    # Only fires when Prism confirms the rescue is genuinely bare (no class listed).
    def fix_bare_rescue(src)
      result = Prism.parse(src)
      return src unless result.success?

      bare_lines = bare_rescue_lines(result.value)
      return src if bare_lines.empty?

      lines = src.lines
      bare_lines.each do |lineno|
        idx = lineno - 1
        next unless idx < lines.size
        lines[idx] = lines[idx].sub(/\brescue\b(?!\s+\w)/, "rescue StandardError")
      end
      @transforms << :bare_rescue
      lines.join
    end

    # `= NULL` → `IS NULL`, `!= NULL` → `IS NOT NULL` inside SQL heredocs/strings.
    # Skip scanner files — they carry `= NULL` literals inside detector regexes.
    def normalise_null_comparison(src)
      return src if @path.to_s.include?("/judge/scan/")
      changed = false
      out = src.gsub(/(?<![<>!])=\s*NULL\b/i) { changed = true; "IS NULL" }
               .gsub(/!=\s*NULL\b/i)           { changed = true; "IS NOT NULL" }
               .gsub(/<>\s*NULL\b/i)            { changed = true; "IS NOT NULL" }
      @transforms << :null_comparison if changed
      out
    end

    def bare_rescue_lines(node, lines = [])
      return lines unless node.is_a?(Prism::Node)
      if node.is_a?(Prism::RescueNode) && (node.exceptions.nil? || node.exceptions.empty?)
        lines << node.location.start_line
      end
      node.child_nodes.compact.each { |child| bare_rescue_lines(child, lines) }
      lines
    end

    def ruby? = File.extname(@path).downcase == ".rb"

    def sql_in_ruby?
      ruby? || %w[.sql .erb].include?(File.extname(@path).downcase)
    end

    def write_back(content)
      tmp = "#{@path}.ast_fix.#{Process.pid}.tmp"
      File.write(tmp, content, encoding: "UTF-8")
      File.rename(tmp, @path)
    rescue StandardError => e
      File.delete(tmp) if defined?(tmp) && File.exist?(tmp) rescue nil
      raise e
    end
  end
  end
  end
end

lib/judge/scan/datalog_engine.rb

# frozen_string_literal: true

module Master
  module Judge
  module Scan
  # Architecture #12: Datalog/Prolog rule engine — logical Horn clause rules.
  # Violations are queries against a fact base derived from the AST.
  # Fixes are derived from the complement clause. No neural network involved.
  #
  # Status: scaffolded. Fact extraction is implemented for Ruby via Prism.
  # Horn clause evaluation is a minimal forward-chaining Datalog subset.
  # Full Prolog (negation, cuts) is future work.
  class DatalogEngine
    Fact    = Struct.new(:predicate, :args, keyword_init: true)
    Rule    = Struct.new(:head, :body, keyword_init: true)   # head :- body[]
    Clause  = Struct.new(:predicate, :args, keyword_init: true)
    Finding = Struct.new(:rule_id, :fact, :message, keyword_init: true)

    def initialize
      @facts  = []
      @rules  = []
    end

    def assert(predicate, *args)
      @facts << Fact.new(predicate: predicate.to_sym, args: args)
      self
    end

    def rule(head_pred, *body_preds, &action)
      @rules << { head: head_pred, body: body_preds, action: action }
      self
    end

    # Query: return all facts matching predicate + optional arg pattern.
    def query(predicate, *pattern)
      @facts.select do |f|
        f.predicate == predicate.to_sym &&
          pattern.each_with_index.all? { |p, i| p.nil? || f.args[i] == p }
      end
    end

    # Forward-chain all rules once. Returns derived findings.
    def evaluate
      findings = []
      @rules.each do |r|
        body_matches = r[:body].map { |bp| query(bp) }
        next if body_matches.any?(&:empty?)
        body_matches.first.each do |fact|
          findings << Finding.new(rule_id: r[:head], fact: fact,
                                  message: r[:action]&.call(fact) || r[:head].to_s)
        end
      end
      findings
    end

    # Extract facts from Ruby source via Prism. Returns a populated engine.
    def self.from_ruby(path, source)
      require "prism"
      engine = new
      result = Prism.parse(source)
      return engine unless result.success?
      extract_facts(result.value, path, engine)
      engine
    end

    def self.extract_facts(node, path, engine)
      return engine unless node.is_a?(Prism::Node)
      case node
      when Prism::DefNode
        engine.assert(:method_def, path, node.name.to_s, node.location.start_line)
      when Prism::RescueNode
        if node.exceptions.nil? || node.exceptions.empty?
          engine.assert(:bare_rescue, path, node.location.start_line)
        end
      when Prism::ClassNode
        engine.assert(:class_def, path, node.constant_path.slice)
      when Prism::CallNode
        engine.assert(:call, path, node.name.to_s, node.location.start_line)
      end
      node.child_nodes.compact.each { |c| extract_facts(c, path, engine) }
      engine
    end
  end
  end
  end
end

lib/judge/scan/detection_pipeline.rb

# frozen_string_literal: true
module Master
  module Judge
    module Scan
      class DetectionPipeline
        def initialize(definition, agent: nil, root: nil)
          @def = definition
          @agent = agent
        end

        def run(ctx)
          findings = []
          file_type = guess_medium(ctx[:file_path])
          @def.adapters.each do |adapter|
            next unless adapter["medium"] == file_type || adapter["medium"] == "any"
            detection = adapter["detection"]
            if detection["lexical"]
              findings.concat(run_lexical(detection["lexical"], ctx))
            elsif detection["structural"]
              findings.concat(run_structural(detection["structural"], ctx))
            elsif detection["semantic"] && @agent
              findings.concat(run_semantic(detection["semantic"], ctx))
            end
          end
          findings
        end

        private

        def guess_medium(path)
          ext = File.extname(path.to_s).downcase
          return "ruby" if ext == ".rb"
          return "javascript" if [".js", ".ts"].include?(ext)
          return "html" if [".html", ".erb"].include?(ext)
          "any"
        end

        def run_lexical(cfg, ctx)
          findings = []
          cfg.each do |scope, conf|
            next unless conf["pattern"]
            pattern = Regexp.new(conf["pattern"])
            if scope == "file_line" && ctx[:file_content]
              ctx[:file_content].each_line.with_index(1) do |line, num|
                if line =~ pattern
                  findings << finding(num, conf["message"] || @def.description)
                end
              end
            end
          end
          findings
        end

        def run_structural(cfg, ctx)
          findings = []
          cfg.each do |scope, conf|
            next unless conf["matcher"]
            case conf["matcher"]
            when "cyclomatic_complexity"
              cc = calculate_cc(ctx[:file_content])
              if cc && cc > conf["threshold"].to_i
                findings << finding(1, "Cyclomatic complexity #{cc} exceeds #{conf["threshold"]}")
              end
            when "method_length"
              max_len = max_method_length(ctx[:file_content])
              if max_len && max_len > conf["threshold"].to_i
                findings << finding(1, "Method has #{max_len} lines (max #{conf["threshold"]})")
              end
            when "nesting_depth"
              depth = max_nesting(ctx[:file_content])
              if depth && depth > conf["threshold"].to_i
                findings << finding(1, "Nesting depth #{depth} exceeds #{conf["threshold"]}")
              end
            end
          end
          findings
        end

        def run_semantic(cfg, ctx)
          findings = []
          cfg.each do |scope, conf|
            next unless conf["prompt"]
            prompt = conf["prompt"]
            response = @agent.ask(prompt, context: ctx[:file_content][0, 3000])
            if response && response.strip.upcase != "CLEAN"
              findings << finding(1, response.to_s[0, 200])
            end
          end
          findings
        end

        def finding(line, message)
          { rule: @def.id, message: message, line: line, severity: @def.severity }
        end

        CC_NODES = %w[
          IfNode UnlessNode WhileNode UntilNode ForNode
          CaseNode WhenNode RescueNode AndNode OrNode
        ].map { |n| "Prism::#{n}" }.to_set.freeze

        def calculate_cc(code)
          result = Prism.parse(code)
          return nil if result.failure?
          count = 1
          result.value.breadth_first_search { |node|
            count += 1 if CC_NODES.include?(node.class.name)
            false
          }
          count
        rescue StandardError => e
          Master::Ground::Swallow.log(e, context: "detection_pipeline.cyclomatic")
          nil
        end

        def max_method_length(code)
          result = Prism.parse(code)
          return nil if result.failure?
          max = 0
          result.value.breadth_first_search { |node|
            if node.is_a?(Prism::DefNode)
              len = node.location.end_line - node.location.start_line
              max = len if len > max
            end
            false
          }
          max.zero? ? nil : max
        rescue StandardError => e
          Master::Ground::Swallow.log(e, context: "detection_pipeline.max_method_length")
          nil
        end

        def max_nesting(code)
          result = Prism.parse(code)
          return nil if result.failure?
          max_depth(result.value, 0)
        rescue StandardError => e
          Master::Ground::Swallow.log(e, context: "detection_pipeline.max_nesting")
          nil
        end

        NESTING_NODES = [
          Prism::ModuleNode, Prism::ClassNode, Prism::DefNode,
          Prism::IfNode, Prism::WhileNode, Prism::CaseNode
        ].freeze

        def max_depth(node, depth)
          return depth unless node.respond_to?(:child_nodes)
          child_depth = NESTING_NODES.include?(node.class) ? depth + 1 : depth
          children = node.child_nodes
          children.compact.reduce(child_depth) { |m, c| [m, max_depth(c, child_depth)].max }
        end
      end
    end
  end
end

lib/judge/scan/finding.rb

# frozen_string_literal: true

module Master
  module Judge
  module Scan
    Finding = Data.define(:rule, :message, :line, :severity, :fix, :tags) do
      def self.build(rule:, message:, line:, severity: :warning, fix: nil, tags: [])
        new(rule:, message:, line:, severity:, fix:, tags:)
      end

      def [](key)
        public_send(key)
      end

      def to_h
        { rule:, message:, line:, severity:, fix:, tags: }
      end

      def merge(extras) = to_h.merge(extras)
    end
  end
  end
end

lib/judge/scan/rule.rb

# frozen_string_literal: true

module Master
  module Judge
  module Scan
    require_relative "finding"

    class Rule
      EXT_LANG = {
        ".rb" => "ruby",        ".rake" => "ruby",   ".gemspec" => "ruby",
        ".erb" => "html",        ".html" => "html",   ".htm" => "html",
        ".css" => "css",         ".scss" => "scss",   ".sass" => "scss",
        ".js" => "javascript",  ".ts" => "javascript",
        ".jsx" => "javascript",  ".tsx" => "javascript",
        ".zsh" => "zsh",         ".sh" => "zsh",    ".bash" => "zsh",
        ".yml" => "yaml",        ".yaml" => "yaml",
        ".md" => "markdown",    ".json" => "json",
      }.freeze

      attr_reader :id, :description, :severity, :rule_tags, :auto_fix

      @registry       = []
      @registry_mutex = Mutex.new

      def self.inherited(subclass)
        @registry_mutex.synchronize { @registry << subclass }
      end

      def self.registry
        @registry_mutex.synchronize { @registry.dup }
      end

      # Rules that need constructor args (root:, agent:) override this to false.
      # Builder uses it to auto-discover zero-arg rules from the registry.
      def self.auto_build? = true

      def initialize
        @id         = self.class.name&.split("::")&.last&.downcase || "unknown"
        @description = ""
        @severity    = :warning
        @rule_tags  = []
        @auto_fix    = true
      end

      def check(code, path:)
        raise NotImplementedError, "#{self.class}#check not implemented"
      end

      def language(path)
        EXT_LANG[File.extname(path).downcase]
      end

      def applies_to?(path, languages)
        return true if languages.nil? || languages.empty?
        lang = language(path)
        lang && languages.include?(lang)
      end

      protected

      def finding(line:, message:, fix: nil)
        Finding.build(rule: @id, message:, line:, severity: @severity, fix:, tags: @rule_tags)
      end

      def scan_lines(code, pattern, message:, fix: nil)
        code.each_line.with_index(1).filter_map { |line, num|
          finding(line: num, message:, fix:) if line.match?(pattern)
        }
      end
    end
  end
  end
end

lib/judge/scan/rule_dsl.rb

# frozen_string_literal: true

module Master
  module Judge
  module Scan
  # Inline Ruby rule definition — JE-style alternative to rules.yml entries.
  # Defined rules auto-register via Rule.inherited; no YAML required.
  #
  #   RuleDSL.rule :NO_PUTS, severity: :warning, applies_to: %i[ruby] do |src, path:|
  #     scan_lines(src, /\bputs\b/, message: "puts in production code")
  #   end
  module RuleDSL
    def self.rule(id, severity: :warning, tags: [], applies_to: nil, autofix: true, description: nil, &block)
      raise ArgumentError, "block required" unless block
      dsl_id   = id.to_s.downcase
      dsl_desc = description || dsl_id.tr("_", " ")
      dsl_tags = Array(tags)
      Class.new(Rule) do
        @dsl_block   = block
        @dsl_langs   = applies_to
        @dsl_autofix = autofix
        class << self; attr_reader :dsl_block, :dsl_langs, :dsl_autofix; end
        define_method(:initialize) do
          super()
          @id = dsl_id; @description = dsl_desc
          @severity = severity; @rule_tags = dsl_tags; @auto_fix = autofix
        end
        define_method(:check) do |code, path:|
          langs = self.class.dsl_langs
          return [] if langs && !langs.include?(language(path)&.to_sym)
          instance_exec(code, path: path, &self.class.dsl_block)
        end
      end
    end
  end
  end
  end
end

require_relative "rules/lexical_rules"
require_relative "rules/ruby_rules"
require_relative "rules/web_rules"
require_relative "rules/js_rules"
require_relative "rules/universal_rules"

lib/judge/scan/rules/adversarial_rule.rb

# frozen_string_literal: true

module Master
  module Judge
  module Scan
    module Rules
      # Steelman-first red-team: the model must defend the code before it can attack it.
      # This suppresses false positives by forcing consideration of legitimate reasons
      # before a violation can survive. Deep depth only; one LLM call per file.
      class AdversarialRule < Rule
        PROMPT_TEMPLATE = <<~PROMPT.freeze
          Red-team review of %<path>s.

          Step 1 — Steelman (internal, do not output): write the three strongest
          arguments that this code is correct and should not be changed.

          Step 2 — Challenge: list only the violations that survive the steelman.
          Format: ISSUE:LINE:description (one per line).
          If nothing survives, respond with exactly: CLEAN

          Focus on: broken contracts, hidden coupling, axiom violations (CQS,
          ONE_JOB, GUARD_EXPENSIVE, FAIL_VISIBLY), and logic errors.
          Ignore style. Do not hallucinate method names.

          Code (%<lang>s):
          %<code>s
        PROMPT

        def initialize(agent: nil)
          super()
          @agent       = agent
          @id          = "adversarial"
          @description = "Red-team scan: steelman then challenge — suppresses false positives"
          @severity    = :error
          @rule_tags  = %i[ONE_JOB CQS GUARD_EXPENSIVE FAIL_VISIBLY COMPOSABLE]
        end

        def self.auto_build? = false

        def set_agent(agent)
          @agent = agent
          self
        end

        def check(code, path:)
          return [] unless (lang = language(path))
          return [] unless @agent

          prompt = format(PROMPT_TEMPLATE, path: File.basename(path),
                                           lang: lang,
                                           code: code[0, 3_000])
          response = @agent.ask(prompt, operation: :scan_adversarial).to_s
          parse_findings(response)
        rescue StandardError => e
          return [] if e.message.to_s =~ /missing configuration|api.?key|unauthorized|no.*provider/i
          [finding(line: 1, message: "adversarial: scan error — #{e.message}")]
        end

        private

        def parse_findings(response)
          response_normalized = response.strip.upcase
          return [] if response_normalized.start_with?("CLEAN")

          response.lines.filter_map do |line|
            match = line.strip.match(/\AISSUE:(\d+):(.+)\z/)
            next unless match
            finding(line: match[1].to_i, message: "adversarial: #{match[2].strip}")
          end
        end
      end
    end
  end
  end
end

lib/judge/scan/rules/ast_omission_rule.rb

# frozen_string_literal: true

module Master
  module Judge
  module Scan
  module Rules
  # Detects methods/classes/modules present in recent git history but absent now.
  # Wraps CommitGuard as a standard scan Rule so it runs in the scanner pipeline.
  class AstOmissionRule < Rule
    def self.auto_build? = false

    def initialize(root: Dir.pwd, depth: CommitGuard::DEFAULT_DEPTH)
      super()
      @id = "ast_omission"; @description = "symbol dropped vs recent commits"
      @severity = :warning; @rule_tags = %i[COMPLETENESS]; @auto_fix = false
      @guard = CommitGuard.new(root:, depth:)
    end

    def check(code, path:)
      return [] unless path.to_s.end_with?(".rb")
      omissions = @guard.check(paths: [File.basename(path)])
      omissions.map { |o| finding(line: 1, message: "#{o.type} #{o.name} dropped (last seen #{o.last_seen_at})") }
    rescue StandardError => _e
      []
    end
  end
  end
  end
  end
end

lib/judge/scan/rules/co_change_coupling_rule.rb

# frozen_string_literal: true

require "open3"

module Master
  module Judge
  module Scan
    module Rules
      # Files that change together in many commits are coupled regardless of imports.
      # Mines the last N commits, builds adjacency, flags pairs whose weight exceeds
      # threshold and that live in different top-level module paths — likely DECOUPLE
      # candidates the lexical rules can't see.
      class CoChangeCouplingRule < Rule
        COMMITS_WINDOW    = 500
        WEIGHT_THRESHOLD  = 5
        # Skip mega-commits — they pollute the graph.
        MAX_FILES_IN_COMMIT = 12

        def initialize
          super
          @id          = "co_change_coupling"
          @description = "Files co-change with N+ peers across module boundaries — hidden coupling"
          @severity    = :info
          @rule_tags  = %i[DECOUPLE ONE_JOB]
          @graph_mutex = Mutex.new
          @graph       = nil
        end

        def check(_code, path:)
          return [] unless path.end_with?(".rb")
          rel = relativize(path)
          return [] unless rel
          peers = neighbors(rel).reject { |peer, _| same_module?(rel, peer) }
                                .select { |_, w| w >= WEIGHT_THRESHOLD }
                                .sort_by { |_, w| -w }
                                .first(3)
          return [] if peers.empty?
          msg = "co-changes with " + peers.map { |p, w| "#{p} (#{w}x)" }.join(", ")
          [finding(line: 1, message: msg)]
        end

        private

        def neighbors(rel)
          graph[rel] || {}
        end

        def graph
          @graph_mutex.synchronize { @graph ||= build_graph }
        end

        COMMIT_SEPARATOR = "===commit===".freeze

        def build_graph
          out, _, status = Open3.capture3("git", "-C", repo_root,
                                          "log", "--name-only",
                                          "--pretty=format:#{COMMIT_SEPARATOR}",
                                          "-n", COMMITS_WINDOW.to_s, "--", "*.rb")
          return {} unless status.success?
          adjacency = Hash.new { |h, k| h[k] = Hash.new(0) }
          out.split(COMMIT_SEPARATOR).each do |chunk|
            files = chunk.lines.map(&:strip).reject(&:empty?).select { |f| f.end_with?(".rb") }
            next if files.size < 2 || files.size > MAX_FILES_IN_COMMIT
            files.combination(2) do |a, b|
              adjacency[a][b] += 1
              adjacency[b][a] += 1
            end
          end
          adjacency
        end

        def repo_root
          @repo_root ||= File.expand_path(File.join(Master::ROOT, ".."))
        end

        def relativize(path)
          full = File.expand_path(path)
          prefix = repo_root + "/"
          full.start_with?(prefix) ? full.delete_prefix(prefix) : nil
        end

        def same_module?(a, b) = module_of(a) == module_of(b)

        def module_of(path)
          parts = path.split("/")
          # MASTER/lib/<module>/... → use the module dir (judge/trace/etc.)
          parts[0] == "MASTER" ? (parts[2] || parts[0]) : parts[0]
        end
      end
    end
  end
  end
end

lib/judge/scan/rules/comment_drift_rule.rb

# frozen_string_literal: true

module Master
  module Judge
  module Scan
    module Rules
      # Comments above method defs that no longer describe what the code does.
      # Lexical pass extracts (comment, method_body) pairs; LLM judges drift in one
      # batched call per file. Pairs with the "reassess on touch" directive: lying
      # comments are factual bugs, not style noise.
      class CommentDriftRule < Rule
        MAX_PAIRS_PER_FILE = 8
        # Lines of method body sent to LLM for drift comparison.
        BODY_SNIPPET       = 20

        def initialize(agent: nil)
          super()
          @agent       = agent
          @id          = "comment_drift"
          @description = "Comment claim doesn't match method body — comment is lying"
          @severity    = :warning
          @rule_tags  = %i[SELF_EXPLAINING EXPLICIT]
        end

        def self.auto_build? = false

        def set_agent(agent)
          @agent = agent
          self
        end

        def check(code, path:)
          return [] unless path.end_with?(".rb") && @agent
          pairs = extract_pairs(code)
          return [] if pairs.empty?
          response = @agent.ask(build_prompt(pairs, path), operation: :scan_comment_drift).to_s
          parse_findings(response, pairs)
        rescue StandardError => _e
          []
        end

        private

        def extract_pairs(code)
          lines = code.lines
          pairs = []
          i = 0
          while i < lines.size && pairs.size < MAX_PAIRS_PER_FILE
            comment_start = i
            while i < lines.size && lines[i] =~ /\A\s*#/
              i += 1
            end
            comment_lines = lines[comment_start...i]
            if comment_lines.any? && i < lines.size && lines[i] =~ /\A\s*def\s/
              comment_text = comment_lines.map { |l| l.strip.delete_prefix("#").strip }.join(" ")
              body = lines[i, BODY_SNIPPET].join
              pairs << { line: comment_start + 1, comment: comment_text, body: body } unless comment_text.empty?
            end
            i += 1
          end
          pairs
        end

        def format_pair(p, idx)
          "[#{idx}] line #{p[:line]}\nCOMMENT: #{p[:comment]}\nCODE:\n#{p[:body]}"
        end

        def build_prompt(pairs, path)
          numbered = pairs.each_with_index.map { |p, idx| format_pair(p, idx) }.join("\n---\n")
          <<~PROMPT
            Audit #{File.basename(path)} for comment drift. For each numbered pair,
            decide whether the comment accurately describes what the code does.
            List ONLY indices where the comment lies or contradicts the code.
            Format each violation: INDEX:short reason (one per line)
            If all pairs are accurate, respond with exactly: CLEAN

            #{numbered}
          PROMPT
        end

        def parse_findings(response, pairs)
          return [] if response.strip.upcase == "CLEAN"
          response.lines.filter_map do |line|
            match = line.strip.match(/\A(\d+):(.+)\z/)
            next unless match
            idx = match[1].to_i
            pair = pairs[idx]
            next unless pair
            finding(line: pair[:line], message: "comment drift — #{match[2].strip}")
          end
        end
      end
    end
  end
  end
end

lib/judge/scan/rules/interconnect_rule.rb

# frozen_string_literal: true

require "yaml"

module Master
  module Judge
  module Scan
    module Rules
      # Detects phantom reads — Ruby code digs keys that don't exist in the corresponding data/*.yml.
      # Also detects orphan keys — top-level YAML keys with zero references in lib/.
      # Only meaningful when scanning lib/ with root: access.
      class InterconnectRule < Rule
        LOAD_CALL   = /load_yaml(?:_data)?\s*\(\s*["']([^"']+\.yml)["']/.freeze
        DIG_CALL    = /\.dig\(\s*((?:["'][^"']+["']\s*,?\s*)+)\)/.freeze
        FETCH_CALL  = /\.fetch\(\s*["']([^"']+)["']/.freeze
        BRACKET_KEY = /\[["']([^"']+)["']\]/.freeze

        def self.auto_build? = false

        def initialize(root:)
          super()
          @id          = "interconnect"
          @description = "Phantom YAML key reads and orphan data keys"
          @severity    = :warning
          @auto_fix    = false
          @rule_tags  = %i[ONE_SOURCE]
          @root        = root
          @data_dir    = File.join(root, "data")
          @lib_source  = load_lib_source(root)
        end

        def check(code, path:)
          findings = []
          findings.concat(check_phantom_yaml_reads(code, path)) if path.include?("/lib/") && path.end_with?(".rb")
          findings.concat(check_phantom_scan_classes(code, path)) \
            if path.end_with?("rules.yml") && path.include?("/data/")
          findings
        end

        private

        def check_phantom_yaml_reads(code, _path)
          yaml_files = extract_loaded_yamls(code)
          return [] if yaml_files.empty?

          loaded = yaml_files.filter_map do |yml_name|
            yml_path = File.join(@data_dir, yml_name)
            next unless File.exist?(yml_path)
            YAML.safe_load(File.read(yml_path), aliases: true) rescue nil
          end
          return [] if loaded.empty?

          findings = []
          extract_dig_paths(code).each do |path_keys|
            next if loaded.any? { |y| y.respond_to?(:dig) && y.dig(*path_keys) }
            code.each_line.with_index(1) do |line, number|
              next unless line.include?(path_keys.first.to_s)
              findings << finding(
                line: number,
                message: "phantom key #{path_keys.inspect} not found in any loaded yaml"
              )
              break
            end
          end
          findings
        end

        def check_phantom_scan_classes(code, _path)
          data = YAML.safe_load(code, aliases: true) rescue nil
          return [] unless data.is_a?(Hash)

          depths = data["scan_depths"] || {}
          rules_dir = File.join(@root, "lib", "master", "judge", "scan", "rules")
          findings = []
          depths.each_value do |class_names|
            next unless class_names.is_a?(Array)
            class_names.each do |name|
              next if name == "all"
              snake = name.gsub(/([A-Z])(?=[A-Z][a-z])|([a-z\d])([A-Z])/) { "#{$1 || $2}_#{$3}" }
                         .downcase
              file = File.join(rules_dir, "#{snake}.rb")
              next if File.exist?(file)
              line_num = code.each_line.with_index(1).find { |l, _| l.include?(name) }&.last || 1
              findings << finding(
                line: line_num,
                message: "scan_depths references phantom class #{name}#{snake}.rb not found in judge/scan/rules/"
              )
            end
          end
          findings
        end

        def extract_loaded_yamls(code)
          code.scan(LOAD_CALL).flatten.compact
        end

        def extract_dig_paths(code)
          code.scan(DIG_CALL).filter_map do |match|
            raw  = match.first.to_s
            keys = raw.scan(/["']([^"']+)["']/).flatten
            keys.size >= 1 ? keys : nil
          end
        end

        def load_lib_source(root)
          lib_dir = File.join(root, "lib")
          return "" unless File.directory?(lib_dir)
          Dir.glob(File.join(lib_dir, "**", "*.rb"))
             .filter_map { |f| File.read(f) rescue nil }
             .join("\n")
        end
      end
    end
  end
  end
end

lib/judge/scan/rules/js_rules.rb

# frozen_string_literal: true

module Master
  module Judge
  module Scan
  module Rules

  RuleDSL.rule :CONST_BY_DEFAULT,
    severity: :warning, tags: %i[IMMUTABLE], applies_to: %i[javascript],
    description: "use const unless reassigned" do |src, path:|
    scan_lines(src, /\blet\s+(\w+)\s*=/, message: "let — use const unless value is reassigned")
  end

  RuleDSL.rule :NULLISH_COALESCING,
    severity: :info, tags: %i[READABILITY], applies_to: %i[javascript],
    description: "use ?? over || for defaults" do |src, path:|
    scan_lines(src, /(\w+)\s*\|\|\s*\w+/, message: "foo || default — use foo ?? default to avoid falsy traps")
  end

  RuleDSL.rule :TEMPLATE_LITERALS,
    severity: :warning, tags: %i[READABILITY], applies_to: %i[javascript],
    description: "use template literals over concatenation" do |src, path:|
    scan_lines(src, /["']\s*\+\s*\w+\s*\+\s*["']/,
               message: "string concatenation — use template literal \`\${var}…\`")
  end

  RuleDSL.rule :ASYNC_AWAIT,
    severity: :warning, tags: %i[READABILITY], applies_to: %i[javascript],
    description: "prefer async/await over .then chains" do |src, path:|
    scan_lines(src, /\.then\(.*\.then\(.*\.then\(/, message: "3+ .then chain — convert to async/await")
  end

  RuleDSL.rule :FOR_OF,
    severity: :error, tags: %i[CORRECTNESS], applies_to: %i[javascript],
    description: "use for...of instead of for...in for arrays" do |src, path:|
    scan_lines(src, /for\s*\(\s*(const|let|var)\s+\w+\s+in\s+/,
      message: "for...in iterates keys — use for...of for array values")
  end

  RuleDSL.rule :QUOTE_VARIABLES,
    severity: :error, tags: %i[ROBUSTNESS], applies_to: %i[zsh],
    description: "always quote $variables" do |src, path:|
    scan_lines(src, /(?<!["'\\])\$\w+(?!["'])/, message: "unquoted $variable — wrap in double quotes")
  end

  RuleDSL.rule :DOUBLE_BRACKET,
    severity: :warning, tags: %i[ROBUSTNESS], applies_to: %i[zsh],
    description: "use [[ ]] over [ ]" do |src, path:|
    scan_lines(src, /(?<!\[)\[\s+[^\[]/, message: "[ ] test — use [[ ]] in zsh")
  end

  RuleDSL.rule :DOLLAR_PAREN,
    severity: :warning, tags: %i[READABILITY], applies_to: %i[zsh],
    description: "replace backticks with $(command)" do |src, path:|
    scan_lines(src, /`[^`]+`/, message: "backtick substitution — use $(command) for clarity")
  end

  end
  end
  end
end

lib/judge/scan/rules/lexical_rules.rb

# frozen_string_literal: true

module Master
  module Judge
  module Scan
  module Rules
  # Lexical rules defined via RuleDSL — pure Ruby, no YAML.
  # Each auto-registers in Rule.registry and runs on every scan.

  RuleDSL.rule :NO_DEBUG,
    severity: :error, tags: %i[CLEAN_CODE], applies_to: %i[ruby],
    description: "no debug breakpoints in committed code" do |src, path:|
    next [] if path.to_s.include?("/judge/scan/rules/")
    scan_lines(src, /\b(binding\.pry|debugger|byebug|binding\.irb)\b/, message: "debug breakpoint")
  end

  RuleDSL.rule :NO_PUTS,
    severity: :warning, tags: %i[CLEAN_CODE], applies_to: %i[ruby],
    description: "no bare puts in library code" do |src, path:|
    next [] if path.to_s.match?(%r{/exe/|/spec/|/bin/|/now/cli})
    scan_lines(src, /^\s*puts\b(?!\s*\()/, message: "bare puts — use event bus or logger")
  end

  RuleDSL.rule :FROZEN_LITERAL,
    severity: :warning, tags: %i[PERFORMANCE], applies_to: %i[ruby],
    description: "missing frozen_string_literal magic comment" do |src, path:|
    next [] if src.lines.first&.include?("frozen_string_literal")
    magic = "# frozen_string_literal: true"
    [finding(line: 1, message: "add #{magic}")]
  end

  RuleDSL.rule :LONG_LINE,
    severity: :info, tags: %i[READABILITY], autofix: false,
    description: "lines exceeding 120 characters" do |src, path:|
    next [] if path.to_s.match?(%r{/voice/personality\.rb|/reach/llm\.rb})
    src.each_line.with_index(1).filter_map { |line, n|
      finding(line: n, message: "line #{line.chomp.length} chars (max 120)") if line.chomp.length > 120
    }
  end

  RuleDSL.rule :TRAILING_WHITESPACE,
    severity: :info, tags: %i[HYGIENE],
    description: "trailing whitespace" do |src, path:|
    src.each_line.with_index(1).filter_map { |line, n|
      finding(line: n, message: "trailing whitespace") if line.match?(/[ \t]+\n?\z/)
    }
  end

  RuleDSL.rule :TODO_FIXME,
    severity: :info, tags: %i[COMPLETENESS], autofix: false,
    description: "unresolved work markers" do |src, path:|
    next [] if path.to_s.include?("/judge/scan/rules/")
    scan_lines(src, /\b(TODO|FIXME|HACK|XXX)\b/, message: "unresolved marker — resolve or delete")
  end

  RuleDSL.rule :RESCUE_EXCEPTION,
    severity: :warning, tags: %i[ERROR_HANDLING], applies_to: %i[ruby],
    description: "rescue StandardError not Exception" do |src, path:|
    scan_lines(src, /rescue\s+Exception\b/, message: "catches signals — use StandardError")
  end

  RuleDSL.rule :EMPTY_RESCUE,
    severity: :error, tags: %i[ERROR_HANDLING FAIL_VISIBLY], applies_to: %i[ruby],
    description: "empty rescue swallows errors silently" do |src, path:|
    src.each_line.with_index(1).filter_map { |line, n|
      bare_rescue   = line.match?(/^\s*rescue\s*$/)
      naked_class   = line.match?(/^\s*rescue\s+\S+\s*$/) && !line.match?(/=>/)
      finding(line: n, message: "empty rescue — use Ground::Swallow.log or re-raise") if bare_rescue || naked_class
    }
  end

  # Regexes hoisted out of the per-call hot path (HOIST).
  RESCUE_DISCARD = /\A(nil|false|0|\[\]|\{\}|next|return|return\s+(nil|false|0|\[\]|\{\})|raise)?\z/
  RESCUE_SINK    = /\b(raise\b|Swallow\.log|\.publish\b|\bwarn\b|\blog\b|Diag\b|Result\.err)/
  RESCUE_HEAD    = /^(\s*)rescue\b([^;#]*?)(?:=>\s*(\w+))?\s*(?:;\s*(.*))?$/

  # 1-based line numbers of rescues whose handler discards the error.
  def self.silent_rescue_lines(src, blanket_only:)
    lines = src.lines
    lines.each_with_index.filter_map do |line, idx|
      m = RESCUE_HEAD.match(line)
      next unless m && rescue_in_scope?(m[2].to_s.strip, blanket_only)
      body = rescue_body(lines, idx, m[4].to_s.strip)
      next unless rescue_silent?(body, m[3])
      [idx + 1, m[3], m[2].to_s.strip]
    end
  end

  # Blanket = bare rescue or StandardError/Exception only; narrow = a named class.
  def self.rescue_in_scope?(classes, blanket_only)
    blanket = classes.empty? ||
              classes.split(",").map(&:strip).all? { |c| %w[StandardError Exception].include?(c) }
    blanket_only ? blanket : !blanket
  end

  # Handler body — the inline `; expr` form, or lines down to the matching end.
  def self.rescue_body(lines, idx, inline)
    return [inline] unless inline.empty?
    collected = []
    ((idx + 1)...lines.size).each do |j|
      stripped = lines[j].strip
      break if stripped.match?(/\A(end|else|ensure|rescue)\b/)
      collected << stripped unless stripped.empty? || stripped.start_with?("#")
    end
    collected
  end

  # Silent: body discards, never names the error, never reaches a sink.
  def self.rescue_silent?(body, err_name)
    return false unless body.reject { |b| b.match?(RESCUE_DISCARD) }.empty?
    return false if err_name && body.any? { |b| b.match?(/\b#{Regexp.escape(err_name)}\b/) }
    body.none? { |b| b.match?(RESCUE_SINK) }
  end

  RuleDSL.rule :SILENT_RESCUE,
    severity: :error, tags: %i[ERROR_HANDLING FAIL_VISIBLY], applies_to: %i[ruby],
    autofix: false,
    description: "blanket rescue discards the error instead of logging or re-raising" do |src, path:|
    next [] if path.to_s.include?("/judge/scan/rules/")
    Rules.silent_rescue_lines(src, blanket_only: true).map do |line, err_name, _classes|
      hint = err_name ? "rescue binds #{err_name} but never uses it" : "blanket rescue discards the error"
      finding(line:, message: "#{hint} — use Ground::Swallow.log or re-raise")
    end
  end

  RuleDSL.rule :NARROW_SILENT_RESCUE,
    severity: :warning, tags: %i[ERROR_HANDLING FAIL_VISIBLY], applies_to: %i[ruby],
    autofix: false,
    description: "narrow rescue discards the error — confirm the case is truly expected" do |src, path:|
    next [] if path.to_s.include?("/judge/scan/rules/")
    Rules.silent_rescue_lines(src, blanket_only: false).map do |line, _err_name, classes|
      finding(line:, message: "rescue #{classes} discards the error — " \
                              "Ground::Swallow.log it if expected, re-raise if not")
    end
  end

  RuleDSL.rule :CONSECUTIVE_BLANK_LINES,
    severity: :info, tags: %i[HYGIENE],
    description: "no consecutive blank lines" do |src, path:|
    findings = []
    prev_blank = false
    src.each_line.with_index(1) { |line, n|
      blank = line.strip.empty?
      findings << finding(line: n, message: "consecutive blank line") if blank && prev_blank
      prev_blank = blank
    }
    findings
  end

  RuleDSL.rule :DEBUG_OUTPUT,
    severity: :error, tags: %i[FAIL_VISIBLY], applies_to: %i[ruby],
    description: "debug output left in lib/" do |src, path:|
    next [] unless path.to_s.include?("/lib/")
    next [] if path.to_s.include?("/judge/scan/rules/")
    findings = scan_lines(src, /^\s*pp?\s+(?!self\b)/, message: "p/pp debug call — remove or publish via event bus")
    findings += scan_lines(src, /\$stderr\.puts\b/, message: "$stderr.puts — use @bus.publish or $stdout")
    findings
  end

  RuleDSL.rule :TRAILING_COMMENT,
    severity: :info, tags: %i[BE_CONCISE],
    description: "trailing comment after code" do |src, path:|
    src.each_line.with_index(1).filter_map { |line, n|
      next if line.strip.start_with?("#")
      finding(line: n, message: "trailing comment — promote above the line or delete") if line.match?(/\S\s+#\s+\S/)
    }
  end

  RuleDSL.rule :TIME_ZONE_UNSAFE,
    severity: :warning, tags: %i[ROBUSTNESS], applies_to: %i[ruby],
    description: "bare Time.now/Date.today bypasses Rails Time.zone" do |src, path:|
    next [] unless path.match?(%r{/app/|/spec/|/test/})
    findings  = scan_lines(src, /(?<![A-Za-z_.])Time\.now\b/,
                           message: "Time.now ignores Time.zone — use Time.current")
    findings += scan_lines(src, /(?<![A-Za-z_.])Date\.today\b/,
                           message: "Date.today ignores Time.zone — use Date.current")
    findings += scan_lines(src, /(?<![A-Za-z_.])DateTime\.now\b/,
                           message: "DateTime.now — use Time.current.to_datetime")
    findings
  end

  RuleDSL.rule :NO_ASCII_LINE_ART,
    severity: :warning, tags: %i[BE_CONCISE],
    description: "ASCII divider decorations" do |src, path:|
    scan_lines(src, /(?:^|\s)(?:={3,}|-{3,})(?:\s|$)/, message: "remove ASCII divider decorations")
  end
  end
  end
  end
end

lib/judge/scan/rules/reek_rule.rb

# frozen_string_literal: true

require "open3"
require "json"

module Master
  module Judge
  module Scan
    module Rules
      # Runs Reek on .rb files and surfaces code smell violations.
      class ReekRule < Rule
        def self.auto_build? = false

        def initialize(root:)
          super()
          @id          = "reek"
          @description = "Reek code smell detected"
          @severity    = :warning
          @auto_fix    = false
          @rule_tags  = %i[SMELL ONE_JOB]
          @root        = root
        end

        def check(code, path:)
          return [] unless path.end_with?(".rb") && reek_available?

          stdout, _stderr, _status = Open3.capture3(
            Master::BUNDLE_BIN, "exec", "reek", "--format", "json", path,
            chdir: @root
          )
          return [] if stdout.empty?

          smells = begin; JSON.parse(stdout); rescue JSON::ParserError; return []; end
          smells.flat_map do |smell|
            locations = smell["lines"] || [1]
            locations.map do |line|
              finding(
                line:    line,
                message: "reek: #{smell["smell_type"]}#{smell["message"]}"
              )
            end
          end
        rescue StandardError => e
          [finding(line: 1, message: "reek: scan error — #{e.message}")]
        end

        private

        def reek_available?
          system("which reek > /dev/null 2>&1") ||
            File.exist?(File.join(@root, "bin", "reek"))
        end
      end
    end
  end
  end
end

lib/judge/scan/rules/rubocop_rule.rb

# frozen_string_literal: true

require "open3"
require "json"

module Master
  module Judge
  module Scan
    module Rules
      # Runs RuboCop on .rb files and surfaces violations as findings.
      class RubocopRule < Rule
        def self.auto_build? = false

        def initialize(root:)
          super()
          @id          = "rubocop"
          @description = "RuboCop style/lint violation"
          @severity    = :warning
          @auto_fix    = false
          @rule_tags  = %i[STYLE LINT]
          @root        = root
        end

        def check(code, path:)
          return [] unless path.end_with?(".rb") && rubocop_available?

          stdout, _stderr, status = Open3.capture3(
            Master::BUNDLE_BIN, "exec", "rubocop", "--format", "json", "--no-color", path,
            chdir: @root
          )
          return [] if stdout.empty?

          data = begin; JSON.parse(stdout); rescue JSON::ParserError; return []; end
          offenses = data.dig("files", 0, "offenses") || []
          offenses.map do |o|
            finding(
              line:    o.dig("location", "line") || 1,
              message: "rubocop: #{o["cop_name"]}#{o["message"]}"
            )
          end
        rescue StandardError => e
          [finding(line: 1, message: "rubocop: scan error — #{e.message}")]
        end

        private

        def rubocop_available?
          system("which rubocop > /dev/null 2>&1") ||
            File.exist?(File.join(@root, "bin", "rubocop"))
        end
      end
    end
  end
  end
end

lib/judge/scan/rules/ruby_rules.rb

# frozen_string_literal: true

module Master
  module Judge
  module Scan
  module Rules

  RuleDSL.rule :SINGLE_PRIVATE_SECTION,
    severity: :info, tags: %i[SMALL_PARTS], applies_to: %i[ruby],
    description: "one private section at bottom" do |src, path:|
    scan_lines(src, /private\s+:\w+/, message: "inline private call — gather private methods at bottom")
  end

  RuleDSL.rule :STRICT_LOADING_MISSING,
    severity: :info, tags: %i[PERFORMANCE], applies_to: %i[ruby],
    description: "AR model lacks strict_loading_by_default" do |src, path:|
    next [] unless path.include?("/app/models/")
    next [] if src.match?(/\bstrict_loading_by_default\b/)
    next [] unless src.match?(/class\s+\w+\s+<\s+(?:ApplicationRecord|ActiveRecord::Base)\b/)
    [finding(line: 1, message: "model missing strict_loading_by_default true")]
  end

  RuleDSL.rule :RATE_LIMITING_MISSING,
    severity: :error, tags: %i[SECURITY], applies_to: %i[ruby],
    description: "sensitive controller missing rate_limit/throttle" do |src, path:|
    next [] unless path.include?("/app/controllers/")
    next [] if src.match?(/rate_limit|throttle/)
    next [] unless src.match?(/(login|signup|sign_up|password|reset)/)
    [finding(line: 1, message: "auth/sensitive controller missing rate_limit or throttle")]
  end

  RuleDSL.rule :MIGRATION_ADD_REFERENCE_NO_FK,
    severity: :error, tags: %i[DATA_INTEGRITY], applies_to: %i[ruby],
    description: "add_reference without foreign_key:" do |src, path:|
    next [] unless path.include?("/db/migrate/")
    scan_lines(src, /add_reference(?!.*foreign_key:)/,
      message: "add_reference without foreign_key: true — data integrity risk")
  end

  RuleDSL.rule :MIGRATION_REMOVE_COLUMN,
    severity: :error, tags: %i[DATA_INTEGRITY], applies_to: %i[ruby],
    description: "remove_column is destructive" do |src, path:|
    next [] unless path.include?("/db/migrate/")
    scan_lines(src, /remove_column/,
      message: "remove_column is destructive — confirm column is unused across all deploys")
  end

  RuleDSL.rule :MIGRATION_FIND_OR_CREATE_BY,
    severity: :warning, tags: %i[DATA_INTEGRITY], applies_to: %i[ruby],
    description: "find_or_create_by needs unique index" do |src, path:|
    next [] unless path.include?("/db/migrate/")
    scan_lines(src, /find_or_create_by/, message: "find_or_create_by is not atomic without a unique index")
  end

  RuleDSL.rule :EACH_WITH_OBJECT,
    severity: :warning, tags: %i[READABILITY], applies_to: %i[ruby],
    description: "prefer each_with_object over inject for hash building" do |src, path:|
    scan_lines(src, /\.(inject|reduce)\(\s*\{\s*\}\s*\)/, message: "use each_with_object({}) over inject({})")
  end

  RuleDSL.rule :KERNEL_COERCION,
    severity: :info, tags: %i[READABILITY], applies_to: %i[ruby],
    description: "use Array(), Hash(), String() coercions" do |src, path:|
    next [] if path.to_s.include?("/judge/scan/rules/")
    scan_lines(src, /(\w+)\s*\|\|\s*\[\](?!\s*<<)/,
      message: "nil-or-empty array — prefer Array(foo) for nil-safe coercion")
  end

  RuleDSL.rule :PERCENT_LITERAL,
    severity: :info, tags: %i[READABILITY], applies_to: %i[ruby],
    description: "use %i[] and %w[] for symbol/string arrays" do |src, path:|
    scan_lines(src, /\[:[a-z_]+,\s*:[a-z_]+,\s*:[a-z_]+/,
      message: "use %i[...] for symbol arrays")
  end

  RuleDSL.rule :HASH_FETCH,
    severity: :info, tags: %i[READABILITY], applies_to: %i[ruby],
    description: "prefer Hash#fetch over [] with ||" do |src, path:|
    next [] if path.to_s.include?("/judge/scan/rules/")
    scan_lines(src, /\w+\[:\w+\]\s*\|\|/,
      message: "hash symbol lookup with fallback — prefer hash.fetch(:key, default)")
  end

  RuleDSL.rule :TRANSFORM_KEYS,
    severity: :info, tags: %i[READABILITY], applies_to: %i[ruby],
    description: "use transform_keys/transform_values" do |src, path:|
    scan_lines(src, /\.each_with_object\(\{\}\)\s*\{\s*\|\(k,\s*v\),\s*h\|/,
      message: "use transform_keys/transform_values instead of manual each_with_object")
  end

  RuleDSL.rule :IMMUTABLE,
    severity: :info, tags: %i[PERFORMANCE], applies_to: %i[ruby],
    description: "default to immutable data" do |src, path:|
    src.each_line.with_index(1).filter_map do |line, n|
      next unless line.match?(/^\s*[A-Z][A-Z_]*\s*=\s*[\[{].*[\]}]/)
      next if line.match?(/\.freeze/)
      finding(line: n, message: "mutable constant — append .freeze")
    end
  end

  RuleDSL.rule :FIND_EACH,
    severity: :warning, tags: %i[PERFORMANCE], applies_to: %i[ruby],
    description: "use find_each for batch processing" do |src, path:|
    next [] unless path.match?(%r{/app/|/spec/|/test/})
    scan_lines(src, /\.(all\.each|where\(.*\)\.each)\b/,
      message: "unbounded .all.each — use find_each(batch_size: N)")
  end

  RuleDSL.rule :NO_UPDATE_ATTRIBUTE,
    severity: :error, tags: %i[DATA_INTEGRITY], applies_to: %i[ruby],
    description: "replace update_attribute with update!" do |src, path:|
    next [] unless path.match?(%r{/app/|/spec/|/test/})
    scan_lines(src, /\.update_attribute\(/, message: "update_attribute skips validations — use update!")
  end

  RuleDSL.rule :PLUCK_OVER_MAP,
    severity: :info, tags: %i[PERFORMANCE], applies_to: %i[ruby],
    description: "prefer pluck over map for single columns" do |src, path:|
    next [] unless path.match?(%r{/app/|/spec/|/test/})
    scan_lines(src, /\.\w+\.map\(&:\w+\)/, message: "use pluck(:column) to avoid loading full objects")
  end

  RuleDSL.rule :DISCARD_RESCUE,
    severity: :error, tags: %i[CORRECTNESS], applies_to: %i[ruby],
    description: "rescue with silent discard hides failures" do |src, path:|
    src.each_line.with_index(1).filter_map do |line, n|
      next unless line.match?(/rescue\s*(StandardError|Exception|=>?\s*_e?\s*$)/)
      next unless line.match?(/nil$|^\s*end\s*$/) || src.lines[n]&.match?(/^\s*(nil|#\s*noop|{}|next|return)\s*$/)
      finding(line: n, message: "rescue discards exception — log via Swallow or re-raise")
    end
  end

  end
  end
  end
end

lib/judge/scan/rules/rule_coverage_rule.rb

# frozen_string_literal: true

module Master
  module Judge
  module Scan
    module Rules
      # Every Rule subclass must have a matching test file; gaps mean untested enforcement.
      class RuleCoverageRule < Rule
        def self.auto_build? = false

        def initialize(root:)
          super()
          @id          = "rule_coverage"
          @description = "Rule subclass has no corresponding test file"
          @severity    = :warning
          @auto_fix    = false
          @rule_tags  = %i[TEST_COVERAGE]
          @root        = root
          @test_dir    = File.join(root, "test")
        end

        def check(code, path:)
          return [] unless path.include?("/judge/scan/rules/") && path.end_with?("_rule.rb")

          base      = File.basename(path, ".rb")
          test_glob = File.join(@test_dir, "**", "#{base}_test.rb")
          return [] if Dir.glob(test_glob).any?

          [finding(line: 1, message: "rule_coverage: no test file found for #{base}")]
        end
      end
    end
  end
  end
end

lib/judge/scan/rules/semantic_rule.rb

# frozen_string_literal: true

module Master
  module Judge
  module Scan
    module Rules
      require_relative "../finding"
      # LLM review for rules whose violations resist lexical detection.
      # Each rules.yml entry with a detect_semantic prompt is folded into one LLM
      # call per file. Rules carry mode: violation (default) or opportunity —
      # the prompt frame and severity follow from that.
      class SemanticRule < Rule
        CODE_SNIPPET_LIMIT = 2000

        def initialize(agent: nil)
          super()
          @agent       = agent
          @id          = "semantic"
          @description = "LLM-based rule review (violations + opportunities)"
          @severity    = :warning
          @rules      = load_semantic_rules
          @rule_tags  = @rules.keys.map(&:to_sym)
        end

        def self.auto_build? = false

        def set_agent(agent)
          @agent = agent
          self
        end

        def check(code, path:)
          return [] unless language(path) && @agent

          response = @agent.ask(build_prompt(code, path), operation: :scan_semantic).to_s
          parse_findings(response)
        rescue StandardError => e
          return [] if e.message.to_s =~ /missing configuration|api.?key|unauthorized|no.*provider/i
          [finding(line: 1, message: "semantic: scan error — #{e.message}")]
        end

        private

        # Each axiom is { prompt:, severity:, mode: }. info-tier violations stay
        # out of the prompt — they're noise that doubles cost. info-tier
        # opportunities stay in: that's their whole point.
        def load_semantic_rules
          data = Master.load_rules
          (data["rules"] || {}).values.flatten
            .select { |r| r["detect_semantic"] }
            .reject { |r| r["severity"] == "info" && r["mode"] != "opportunity" && r["tier"] != "kernel" }
            .each_with_object({}) do |r, h|
              h[r["id"]] = {
                prompt: r["detect_semantic"],
                severity: (r["severity"] || "warning").to_sym,
                mode: (r["mode"] || "violation").to_sym
              }
            end
        end

        def build_prompt(code, path)
          violations = @rules.select { |_, a| a[:mode] == :violation }
          opportunities = @rules.select { |_, a| a[:mode] == :opportunity }
          parts = []
          parts << violation_block(violations) unless violations.empty?
          parts << opportunity_block(opportunities) unless opportunities.empty?
          <<~PROMPT
            Review #{File.basename(path)}.

            #{parts.join("\n\n")}

            Code (first #{CODE_SNIPPET_LIMIT} chars):
            #{code[0, CODE_SNIPPET_LIMIT]}
          PROMPT
        end

        def violation_block(rules)
          list = rules.map { |id, a| "#{id}: #{a[:prompt]}" }.join("\n")
          <<~BLOCK
            VIOLATIONS — list ONLY clear breaches. Format: RULE_ID:LINE:description.
            If clean, write CLEAN on its own line.
            #{list}
          BLOCK
        end

        def opportunity_block(rules)
          list = rules.map { |id, a| "#{id}: #{a[:prompt]}" }.join("\n")
          <<~BLOCK
            OPPORTUNITIES — list refactors only if they would simplify. Format: RULE_ID:LINE:reason.
            If none, write NONE on its own line.
            #{list}
          BLOCK
        end

        def parse_findings(response)
          response.lines.filter_map do |line|
            stripped = line.strip
            next if stripped.empty? || %w[CLEAN NONE].include?(stripped.upcase)

            match = stripped.match(/\A([A-Z_][A-Z0-9_]*):(\d+):(.+)\z/)
            next unless match && @rules.key?(match[1])

            axiom = @rules[match[1]]
            Finding.build(
              rule: match[1].downcase,
              message: match[3].strip,
              line: match[2].to_i,
              severity: axiom[:severity],
              fix: nil,
              tags: [match[1].to_sym, axiom[:mode]]
            )
          end
        end
      end
    end
  end
  end
end

lib/judge/scan/rules/universal_rules.rb

# frozen_string_literal: true

module Master
  module Judge
  module Scan
  module Rules

  # SQL strings embedded in Ruby DB adapter files are expected — only flag
  # actual mixed-medium template files.
  RuleDSL.rule :NO_MULTIPLE_LANGUAGES,
    severity: :warning, tags: %i[SMALL_PARTS],
    description: "one medium per artifact" do |src, path:|
    next [] if path.to_s.include?("/judge/scan/rules/")
    scan_lines(src, /<%|<script\b|<style\b/,
      message: "mixed medium — extract to a dedicated file")
  end

  RuleDSL.rule :SAFE_NAVIGATION,
    severity: :warning, tags: %i[READABILITY],
    description: "use null-safe navigation over nil-guard && chains" do |src, path:|
    next [] if path.to_s.include?("/judge/scan/rules/")
    src.each_line.with_index(1).filter_map do |line, n|
      next unless line.match?(/(\w+)\s*&&\s*\1\.\w+/)
      next if line.match?(/[!=<>]=|[<>]|\?\s*\w/)
      finding(line: n, message: "nil-guard chain — use safe navigation (&.) or (?.) instead")
    end
  end

  RuleDSL.rule :FEW_ARGUMENTS,
    severity: :warning, tags: %i[SMALL_PARTS],
    description: "ideal is zero to two arguments" do |src, path:|
    scan_lines(src, /def \w+\([^)]*,[^:)]+,[^:)]+,[^:)]+\)/,
      message: "4+ positional args — refactor into keyword args or a value object")
  end

  RuleDSL.rule :KEYWORD_ARGS,
    severity: :info, tags: %i[SMALL_PARTS],
    description: "keyword arguments for 3+ parameters" do |src, path:|
    scan_lines(src, /def \w+\([^)]*,\s*[^:)]+,\s*[^:)]+,\s*[^:)]+\)/,
      message: "3+ positional args — use keyword arguments")
  end

  RuleDSL.rule :N_PLUS_ONE,
    severity: :warning, tags: %i[PERFORMANCE],
    description: "loading records one-by-one inside a loop" do |src, path:|
    # Only meaningful in Rails app/ trees; non-AR enumerable chains are fine.
    next [] unless path.match?(%r{/app/|/spec/|/test/})
    scan_lines(src, /\.(each|map|collect)\s*(do|\{).*\.\w+\.\w+/,
      message: "N+1 query candidate — use includes/eager_load")
  end

  # Only positional boolean defaults are flag arguments. Keyword defaults
  # (stream: false, enabled: true) are not — they're fine API design.
  RuleDSL.rule :NO_FLAG_ARGUMENTS,
    severity: :warning, tags: %i[SMALL_PARTS],
    description: "a flag that selects behavior means two things hiding as one" do |src, path:|
    src.each_line.with_index(1).filter_map do |line, n|
      next unless line.match?(/def \w+\(/)
      args_str = line[/\(([^)]*)\)/, 1].to_s
      args = args_str.split(",").map(&:strip)
      positional_bool = args.any? do |a|
        a.match?(/\A\w+\s*=\s*(true|false)\z/) && !a.include?(":")
      end
      finding(line: n, message: "boolean flag arg — split into two methods") if positional_bool
    end
  end

  # Exclude numeric dot-chains (IP addresses, version numbers) and stdlib
  # transformation chains (.to_s.strip.empty?) which are idiomatic Ruby.
  RuleDSL.rule :LAW_OF_DEMETER,
    severity: :warning, tags: %i[COUPLING],
    description: "only talk to immediate friends" do |src, path:|
    src.each_line.with_index(1).filter_map do |line, n|
      next if line.strip.start_with?("#")
      next unless line.match?(/\b[a-z_]\w*(?:\.[a-z_]\w*){3}/)
      next if line.match?(/\d+\.\d+\.\d+\.\d+/)
      next if line.match?(/\.(to_s|to_i|to_f|to_a|to_h|strip|chomp|compact|first|last|join)\b/) ||
              line.match?(/\.(empty\?|any\?|size|length)\b/)
      stripped = line.gsub(/["'][^"']*["']/, '""').gsub(/\(.*?\)/, "()")
      next unless stripped.match?(/\b[a-z_]\w*(?:\.[a-z_]\w*){3}/)
      finding(line: n, message: "4-level chain — introduce a local variable or delegation")
    end
  end

  # Generic names: only very short or clearly placeholder names. `data` and
  # `result` are contextually meaningful in most Ruby code.
  RuleDSL.rule :MEANINGFUL_NAMES,
    severity: :info, tags: %i[READABILITY],
    description: "names reveal intent" do |src, path:|
    scan_lines(src, /\b(tmp|temp|val|ret|obj|arr|buf)\b\s*=/,
      message: "generic name — use a name that reveals intent")
  end

  RuleDSL.rule :WHY_NOT_WHAT,
    severity: :info, tags: %i[BE_CONCISE],
    description: "comments explain why, not what" do |src, path:|
    scan_lines(src, /#\s*(increment|set|get|update|return|initialize|create|add)\s+\w+/,
      message: "comment describes what the code does — explain why instead")
  end

  RuleDSL.rule :TYPOGRAPHIC_EXCELLENCE,
    severity: :info, tags: %i[TYPOGRAPHY],
    description: "typographic excellence in user-facing text" do |src, path:|
    next [] if path.to_s.include?("/judge/scan/rules/")
    src.each_line.with_index(1).filter_map do |line, n|
      next if line.match?(/Open3|capture2|capture3|gsub\(|Shellwords/)
      next if line.match?(/,\s*"--"\s*,|,\s*"--"\s*\)|<<\s*["']--/)
      next unless line.match?(/["']\.\.\.[\"']|["']--["']/)
      finding(line: n, message: "ASCII typography — use Unicode ellipsis … and em dash —")
    end
  end

  # Exclude YAML document separators (---) and data file structural lines;
  # only flag decorative runs inside code comments or string literals.
  RuleDSL.rule :TYPOGRAPHY_DISCIPLINE,
    severity: :info, tags: %i[TYPOGRAPHY],
    description: "hierarchy via weight and brightness, not decoration" do |src, path:|
    next [] if path.to_s.include?("/judge/scan/rules/")
    src.each_line.with_index(1).filter_map do |line, n|
      stripped = line.strip
      next if stripped == "---" || stripped.start_with?("---") && path.end_with?(".yml", ".yaml")
      next if stripped.start_with?("//", "/*", "*")
      next unless stripped.match?(/[-=]{4,}|[╭╮╰╯│─]/)
      finding(line: n, message: "ASCII decoration — use whitespace and typographic weight instead")
    end
  end

  RuleDSL.rule :NULL_BLINDNESS,
    severity: :error, tags: %i[CORRECTNESS],
    description: "comparisons against nullable columns must use IS NULL" do |src, path:|
    next [] if path.to_s.include?("/judge/scan/rules/")
    scan_lines(src, /= NULL|!= NULL|== nil.*column|column.*== nil/,
      message: "NULL comparison — use IS NULL / IS NOT NULL in SQL; .nil? in Ruby")
  end

  RuleDSL.rule :SECRET_PROXIMITY,
    severity: :error, tags: %i[SECURITY],
    description: "secrets and consumers must not share a file" do |src, path:|
    scan_lines(src, /(password|secret|token|api_key|private_key)\s*=\s*['"][^'"]{8,}/,
      message: "hardcoded secret — move to environment variable or secrets manager")
  end

  RuleDSL.rule :MAGIC_COLOR,
    severity: :warning, tags: %i[MAINTAINABILITY],
    description: "color values must reference design tokens, not raw hex/rgb" do |src, path:|
    scan_lines(src, /#[0-9a-fA-F]{3,6}\b|rgb\(|rgba\(|hsl\(/,
      message: "raw color value — reference a CSS custom property or design token")
  end

  # `loop do` is legitimate for event loops and daemons. Only flag `retry`
  # without an obvious cap, and bare `while true` in library code.
  RuleDSL.rule :UNBOUNDED_RETRY,
    severity: :error, tags: %i[ROBUSTNESS],
    description: "retry loops must have a max_attempts cap and backoff" do |src, path:|
    next [] if path.to_s.include?("/judge/scan/rules/")
    src.each_line.with_index(1).filter_map do |line, n|
      stripped = line.strip
      next if stripped.start_with?("#")
      next if stripped.match?(/loop\s*do/)
      next if stripped.match?(/retry\\/)
      next unless stripped.match?(/\bretry\b|while\s+true/)
      finding(line: n, message: "unbounded retry — add max_attempts cap and exponential backoff")
    end
  end

  RuleDSL.rule :ONE_SOURCE,
    severity: :warning, tags: %i[COUPLING],
    description: "constants defined locally when a canonical ONE_SOURCE exists" do |src, path:|
    next [] if path.to_s.include?("/judge/scan/rules/")
    next [] if path.to_s.include?("master.rb")
    patterns = [
      [/COUNCIL_PATH\s*=/, "define COUNCIL_PATH once in master.rb; reference Master::COUNCIL_PATH"],
      [/RULES_PATH\s*=/, "define RULES_PATH once in master.rb; reference Master::RULES_PATH"],
      [/DATA_DIR\s*=\s*File\.join.*\bdata\b/, "use Master::DATA constant"]
    ]
    src.each_line.with_index(1).flat_map do |line, n|
      patterns.filter_map { |re, msg| finding(line: n, message: msg) if re.match?(line) }
    end
  end

  RuleDSL.rule :H1_VISIBILITY,
    severity: :warning, tags: %i[TYPOGRAPHY],
    description: "every page must have exactly one visible h1" do |src, path:|
    next [] unless path.to_s.match?(/\.(html|erb|haml|slim)\z/)
    h1_count = src.scan(/<h1[\s>]/i).size
    hidden   = src.match?(/h1[^}]*display\s*:\s*none|h1[^}]*visibility\s*:\s*hidden/i)
    findings = []
    findings << finding(line: 1, message: "no h1 found — every page needs exactly one visible h1") if h1_count.zero?
    findings << finding(line: 1, message: "multiple h1 elements — only one h1 per page") if h1_count > 1
    findings << finding(line: 1, message: "h1 is hidden — screen readers and search engines require a visible h1") if hidden
    findings
  end

  end
  end
  end
end

lib/judge/scan/rules/web_rules.rb

# frozen_string_literal: true

module Master
  module Judge
  module Scan
  module Rules

  RuleDSL.rule :HTML_LANG,
    severity: :error, tags: %i[ACCESSIBILITY], applies_to: %i[html],
    description: "lang attribute on <html>" do |src, path:|
    scan_lines(src, /<html(?!\s+[^>]*lang=)/, message: "<html> missing lang= attribute")
  end

  RuleDSL.rule :SEMANTIC_ELEMENTS,
    severity: :warning, tags: %i[ACCESSIBILITY], applies_to: %i[html],
    description: "use semantic HTML5 elements" do |src, path:|
    scan_lines(src, /<div\s+class="(header|footer|nav|main|sidebar|article|section)"/,
      message: "use <header>/<footer>/<nav>/<main>/<aside>/<article>/<section> instead of div.class")
  end

  RuleDSL.rule :I18N_COVERAGE,
    severity: :warning, tags: %i[I18N], applies_to: %i[html],
    description: "wrap user-facing literals in I18n helpers" do |src, path:|
    next [] unless path.include?("/app/views/")
    scan_lines(src, />\s*[A-Za-z][^<]{3,}</, message: "bare text — wrap with t('…')")
  end

  RuleDSL.rule :IMG_ALT,
    severity: :error, tags: %i[ACCESSIBILITY], applies_to: %i[html],
    description: "require alt on every <img>" do |src, path:|
    scan_lines(src, /<img\s+(?![^>]*alt=)/, message: "<img> missing alt= attribute")
  end

  RuleDSL.rule :BUTTON_OVER_ANCHOR,
    severity: :warning, tags: %i[ACCESSIBILITY], applies_to: %i[html],
    description: "use <button> for actions, not <a href='#'>" do |src, path:|
    scan_lines(src, /<a\s+href=["']#["']/, message: "use <button> for actions; <a> is for navigation")
  end

  RuleDSL.rule :ARIA_INTERACTIVE,
    severity: :warning, tags: %i[ACCESSIBILITY], applies_to: %i[html],
    description: "ARIA on non-semantic interactive elements" do |src, path:|
    scan_lines(src, /<(div|span)\s+[^>]*onclick/,
               message: "use <button> or <a> for interactive elements, not div/span with onclick")
  end

  RuleDSL.rule :LAZY_IMAGES,
    severity: :info, tags: %i[PERFORMANCE], applies_to: %i[html],
    description: "loading=lazy on below-fold images" do |src, path:|
    scan_lines(src, /<img\s+(?![^>]*loading=)/, message: "<img> missing loading=lazy")
  end

  RuleDSL.rule :NO_INLINE_STYLES,
    severity: :warning, tags: %i[MAINTAINABILITY], applies_to: %i[html],
    description: "replace inline styles with classes" do |src, path:|
    scan_lines(src, /\bstyle="[^"]*"/, message: "inline style — move to stylesheet")
  end

  RuleDSL.rule :MOBILE_FIRST,
    severity: :warning, tags: %i[RESPONSIVE], applies_to: %i[css scss],
    description: "mobile-first media queries" do |src, path:|
    scan_lines(src, /@media\s*\(\s*max-width/, message: "max-width query — flip to min-width for mobile-first")
  end

  RuleDSL.rule :NO_IMPORT_SCSS,
    severity: :warning, tags: %i[MAINTAINABILITY], applies_to: %i[scss],
    description: "replace @import with @use/@forward" do |src, path:|
    scan_lines(src, /@import\s+["']/, message: "@import is deprecated — use @use or @forward")
  end

  RuleDSL.rule :NO_IMPORTANT,
    severity: :warning, tags: %i[MAINTAINABILITY], applies_to: %i[css scss],
    description: "no !important" do |src, path:|
    scan_lines(src, /!\s*important/, message: "!important overrides cascade — fix specificity instead")
  end

  RuleDSL.rule :LOGICAL_PROPERTIES,
    severity: :info, tags: %i[I18N], applies_to: %i[css scss],
    description: "prefer logical properties for RTL support" do |src, path:|
    scan_lines(src, /(margin|padding)-(left|right):/,
      message: "use logical property (margin-inline-start/end) for RTL support")
  end

  RuleDSL.rule :CLAMP_TYPOGRAPHY,
    severity: :info, tags: %i[RESPONSIVE], applies_to: %i[css scss],
    description: "use clamp() for fluid typography" do |src, path:|
    scan_lines(src, /@media.*\{[^}]*font-size:/,
      message: "media-query font-size — use clamp(min, fluid, max) instead")
  end

  end
  end
  end
end

lib/judge/scan/scanner.rb

# frozen_string_literal: true

require "etc"
require "open3"
require "prism"

module Master
  module Judge
  module Scan
    class Scanner
      POOL_SIZE  = [Etc.nprocessors, 8].min.freeze
      SCAN_GLOB  = "**/*.{rb,rake,erb,html,htm,css,scss,js,ts,jsx,tsx,zsh,sh,yml,yaml,md}".freeze
      RUBY_EXT   = %w[.rb .rake .gemspec].freeze

      def initialize(rules: nil, event_bus: nil)
        @rules = Array(rules)
        @bus   = event_bus
        @mutex = Mutex.new
      end

      # rules: override skips depth filtering — used by RuleLoop for per-rule passes.
      def scan(path, depth: :deep, rules: nil)
        return Result.err("file not found: #{path}", category: :validation) unless File.exist?(path)

        code     = File.read(path, encoding: "UTF-8")
        ast      = parse_ruby(code, path)
        rule_set = rules || active_rules(depth)
        findings = rule_set.flat_map { |rule| run_rule(rule:, code:, ast:, path:) }
        @bus&.publish("scan:complete", path:, depth:, count: findings.size)
        Result.ok(findings)
      rescue StandardError => e
        @bus&.publish("scan:error", path:, error: e.message)
        Result.err("scan failed: #{e.message}", category: :infrastructure)
      end

      def scan_dir(dir, depth: :deep, glob: SCAN_GLOB, stream: false)
        paths   = Dir.glob(File.join(dir, glob)).sort
        results = Array.new(paths.size)
        parallel_each(paths) { |path, idx| results[idx] = scan_one(dir:, path:, depth:, stream:) }
        Result.ok(results)
      rescue StandardError => e
        Result.err("scan_dir: #{e.message}", category: :infrastructure)
      end

      # Scan only files changed since git ref — orders of magnitude faster on big repos.
      def scan_since(ref = "HEAD~1", dir: ".", depth: :deep, stream: false)
        out, _, status = Open3.capture3("git", "-C", dir, "diff", "--name-only", "#{ref}...HEAD")
        return Result.err("git diff failed", category: :validation) unless status.success?
        paths = out.lines.map(&:strip).reject(&:empty?)
                  .map { |rel| File.join(dir, rel) }
                  .select { |p| File.exist?(p) && File.extname(p).match?(/\.(rb|erb|yml|js|css|sh|zsh)\z/) }
        results = Array.new(paths.size)
        parallel_each(paths) { |path, idx| results[idx] = scan_one(dir:, path:, depth:, stream:) }
        Result.ok(results)
      rescue StandardError => e
        Result.err("scan_since: #{e.message}", category: :infrastructure)
      end

      def add_rule(rule)
        @rules << rule
        self
      end

      def set_agent(agent)
        @rules.each { |r| r.set_agent(agent) if r.respond_to?(:set_agent) }
        self
      end

      private

      def parse_ruby(code, path)
        return unless RUBY_EXT.include?(File.extname(path))
        result = Prism.parse(code)
        result.success? ? result.value : nil
      rescue StandardError => e
        @bus&.publish("scan:parse_error", path:, error: e.message)
        nil
      end

      def run_rule(rule:, code:, ast:, path:)
        if ast && rule.respond_to?(:check_ast)
          rule.check_ast(ast, code, path:)
        else
          rule.check(code, path:)
        end
      end

      def parallel_each(items)
        cursor  = Mutex.new
        index   = 0
        threads = Array.new(POOL_SIZE) do
          Thread.new do
            loop do
              i = cursor.synchronize { (index += 1) - 1 }
              break if i >= items.size
              yield items[i], i
            end
          end
        end
        threads.each(&:join)
      end

      def scan_one(dir:, path:, depth:, stream:)
        file_result = scan(path, depth:)
        stream_progress(dir, path, file_result) if stream
        [path, file_result]
      rescue StandardError => e
        @bus&.publish("scanner:thread_error", path:, error: e.message)
        [path, Result.err(e.message, category: :infrastructure)]
      end

      def stream_progress(dir, path, file_result)
        return unless file_result.ok?
        count = file_result.value!.size
        return unless count.positive?
        rel = path.sub(dir, "").delete_prefix("/")
        $stdout.puts "scan: #{rel} #{count} violation(s)"
        $stdout.flush
      end

      def depth_rules
        @depth_rules ||= begin
          data = Master.load_yaml(Master::RULES_PATH)
          data["scan_depths"] || {}
        end
      rescue StandardError => _e
        @depth_rules = {}
      end

      def active_rules(depth)
        allowed = depth_rules[depth.to_s]
        return @rules if allowed.nil? || allowed == ["all"] || allowed == :all
        @rules.select { |r|
          allowed.include?(r.class.name&.split("::")&.last) || allowed.include?(r.id)
        }
      end
    end
  end
  end
end

lib/judge/scan/unit_segmenter.rb

# frozen_string_literal: true

require "prism"

module Master
  module Judge
  module Scan
  # Slices a source file into logical units for detect_unit scanning axis.
  # Ruby: method/class/module nodes via Prism AST.
  # Other languages: paragraph-based line grouping (blank-line delimited).
  class UnitSegmenter
    Unit = Struct.new(:name, :type, :start_line, :end_line, :source, keyword_init: true)

    def self.segment(path, source)
      new(path, source).segment
    end

    def initialize(path, source)
      @path   = path
      @source = source
      @lines  = source.lines
    end

    def segment
      ruby? ? ruby_units : prose_units
    end

    private

    def ruby_units
      result = Prism.parse(@source)
      return prose_units unless result.success?
      units = []
      walk(result.value, units)
      units.empty? ? prose_units : units
    end

    def walk(node, units)
      return unless node.is_a?(Prism::Node)
      case node
      when Prism::DefNode
        units << build_unit(node, node.name.to_s, :method)
      when Prism::ClassNode
        units << build_unit(node, node.constant_path.slice, :class)
        node.child_nodes.compact.each { |c| walk(c, units) }
        return
      when Prism::ModuleNode
        units << build_unit(node, node.constant_path.slice, :module)
        node.child_nodes.compact.each { |c| walk(c, units) }
        return
      end
      node.child_nodes.compact.each { |c| walk(c, units) }
    end

    def build_unit(node, name, type)
      s = node.location.start_line
      e = node.location.end_line
      Unit.new(
        name:       name,
        type:       type,
        start_line: s,
        end_line:   e,
        source:     @lines[(s - 1)..(e - 1)].join
      )
    end

    # Blank-line delimited paragraph units — works for prose, YAML, config, HTML.
    def prose_units
      units  = []
      buffer = []
      start  = 1
      @lines.each_with_index do |line, idx|
        lineno = idx + 1
        if line.strip.empty?
          if buffer.any?(&method(:non_blank?))
            units << Unit.new(name: "paragraph_#{units.size + 1}", type: :paragraph,
                              start_line: start, end_line: lineno - 1, source: buffer.join)
          end
          buffer = []
          start  = lineno + 1
        else
          buffer << line
        end
      end
      if buffer.any?(&method(:non_blank?))
        units << Unit.new(name: "paragraph_#{units.size + 1}", type: :paragraph,
                          start_line: start, end_line: @lines.size, source: buffer.join)
      end
      units
    end

    def non_blank?(line) = !line.strip.empty?

    def ruby? = File.extname(@path).downcase == ".rb"
  end
  end
  end
end

lib/judge/schema_index.rb

# frozen_string_literal: true

module Master
  module Judge
  class SchemaIndex
    attr_reader :tables

    def initialize(root:)
      @root = root
      @tables = {}
      parse_schema
    end

    def indexed_columns(table_name)
      (@tables.dig(table_name, :indexes) || []).flat_map { |idx| idx[:columns] }
    end

    def columns(table_name)
      (@tables.dig(table_name, :columns) || [])
    end

    private

    def parse_schema
      path = File.join(@root, "db", "schema.rb")
      return unless File.exist?(path)
      table_name = nil
      File.readlines(path, chomp: true).each do |line|
        if line =~ /^\s*create_table\s+"([^"]+)"/
          table_name = Regexp.last_match(1)
          @tables[table_name] = { columns: [], indexes: [] }
        elsif table_name && line =~ /^\s*t\.\w+\s+"([^"]+)"/
          @tables[table_name][:columns] << Regexp.last_match(1)
        elsif line =~ /^\s*add_index\s+"([^"]+)",\s+\[(.+)\]/
          target = Regexp.last_match(1)
          cols = Regexp.last_match(2).scan(/"([^"]+)"/).flatten
          (@tables[target] ||= { columns: [], indexes: [] })[:indexes] << { columns: cols }
        end
      end
    end
  end
  end
end

lib/judge/security/injection_guard.rb

# frozen_string_literal: true

module Master
  module Judge
  module Security
    class InjectionGuard
      DATA_PATH = File.join(Master::ROOT, "data", "injection_patterns.yml")

      DEFAULTS = {
        prompt_injection: [
          /ignore (?:previous|all|your) instructions/i,
          /disregard (?:your )?(?:system )?prompt/i,
          /you are now (?:a|an|in)/i,
          /pretend (?:to be|you are|you're)/i,
          /new instructions:/i,
          /\[SYSTEM\]/i,
          /###\s*SYSTEM/i,
          /(?:act|behave|respond) as (?:if )?(?:you (?:are|were)|a|an) (?!assistant|helpful)/i,
          /override (?:your )?(?:safety|guidelines|rules|instructions)/i,
          /jailbreak/i,
          /forget (?:everything|all|your)/i,
          /override (?:axiom|principle|rule)/i,
          /disregard (?:axiom|principle|rule|safety)/i,
          /new system prompt/i,
        ].freeze,
        shell_injection: /```(?:bash|sh|zsh|shell)\n.*?
          (?:rm\s+-rf|curl\b.*?\|\s*(?:bash|sh)\b|wget\b.*?\|\s*(?:bash|sh)\b)
        /imx.freeze,
      }.freeze

      ALLOWLIST_TOKEN = /\AMASTER_TRUSTED:[A-Za-z0-9]{16,}/.freeze

      def initialize(mode: :permissive)
        @mode     = mode
        @patterns = load_or_default
      end

      def scan(content)
        hits = @patterns[:prompt_injection].select { |p| content.match?(p) }
        hits << @patterns[:shell_injection] if content.match?(@patterns[:shell_injection])

        if hits.empty?
          return Result.ok(:clean) if @mode == :permissive
          return Result.ok(:clean) if content.match?(ALLOWLIST_TOKEN)
          return Result.err("default_deny: no allowlist token; rejecting unmatched input", category: :validation)
        end
        Result.err("injection detected: #{hits.size} pattern(s) matched", category: :validation)
      end

      def safe?(text)
        scan(text.to_s).ok?
      end

      def clean!(content)
        cleaned = @patterns[:prompt_injection].reduce(content) { |c, p| c.gsub(p, "[REDACTED]") }
        Result.ok(cleaned)
      end

      private

      def load_or_default
        return DEFAULTS unless File.exist?(DATA_PATH)
        data = Master.load_yaml(DATA_PATH) || {}
        prompt = (data["prompt_injection"] || []).map { |s| Regexp.new(s, Regexp::IGNORECASE) }
        shell  = data.dig("shell_injection", "multiline_pattern")
        {
          prompt_injection: prompt.empty? ? DEFAULTS[:prompt_injection] : prompt.freeze,
          shell_injection:  shell ? Regexp.new(shell,
Regexp::MULTILINE | Regexp::IGNORECASE) : DEFAULTS[:shell_injection],
        }
      rescue StandardError => _e
        DEFAULTS
      end
    end
  end
  end
end

lib/judge/security/permissions.rb

# frozen_string_literal: true

module Master
  module Judge
  module Security
    module Permissions
      TOOL_TIERS = {
        "read_file" => :safe,
        "list_dir" => :safe,
        "search_files" => :safe,
        "write_file" => :guarded,
        "str_replace" => :guarded,
        "apply_diff" => :guarded,
        "ask_llm" => :guarded,
        "web_search" => :guarded,
        "zsh" => :dangerous
      }.freeze

      BLOCKLIST = [
        "rm -rf /",
        "sudo",
        "reboot",
        "shutdown",
        "mkfs",
        "dd if=",
        "> /dev/",
        "chmod 777",
        "curl | sh",
        "wget | sh"
      ].freeze

      def self.tier_for(tool_name)
        TOOL_TIERS[tool_name.to_s] || :guarded
      end

      def self.blocked?(command)
        BLOCKLIST.any? { |b| command.downcase.include?(b.downcase) }
      end
    end
  end
  end
end

lib/judge/swarm/coordinator.rb

# frozen_string_literal: true

require "timeout"

module Master
  module Judge
  module Swarm
    class Coordinator
      SwarmResult = Struct.new(:verdict, :confidence, :reasoning, :artifacts, keyword_init: true) do
        def ok?      = verdict != :error
        def approved? = verdict == :approved
      end

      WORKER_CLASSES = {
        analyst:    Workers::Analyst,
        coder:      Workers::Coder,
        reviewer:   Workers::Reviewer,
        researcher: Workers::Researcher
      }.freeze

      WORKER_TIMEOUT = 30
      SHARED_DEADLINE = 60
      SYNTHESIS_TRUNCATE_LIMIT = 200

      def initialize(agent:, event_bus: nil)
        @agent = agent
        @bus   = event_bus
        @workers = {}
      end

      def dispatch(role, task:, context_slice: {})
        worker = worker_for(role) or return Result.err("unknown role: #{role}")
        @bus&.publish(:swarm_dispatch, role:, task: task[0..60])
        worker.call(task:, context_slice:)
      end

      def analyse_and_review(file_path:, code:)
        fan_out([
          { role: :analyst,  task: "identify all issues",          context_slice: { file: file_path, code: code } },
          { role: :reviewer, task: "security and correctness review", context_slice: { code: code } }
        ]).and_then do |sr|
          analysis = sr.artifacts[:analyst]
          review   = sr.artifacts[:reviewer]
          Result.ok({ analysis:, review:, approved: review.is_a?(Hash) && review["approved"] })
        end
      end

      def fan_out(tasks, timeout: WORKER_TIMEOUT)
        threads = tasks.map do |t|
          Thread.new do
            [t[:role], dispatch(t[:role], task: t[:task], context_slice: t.fetch(:context_slice, {}))]
          rescue StandardError => e
            @bus&.publish("swarm:worker_error", role: t[:role], error: e.message)
            [t[:role], Result.err("worker error: #{e.message}", category: :infrastructure)]
          end
        end

        results = threads.map { |th| join_or_timeout(th, timeout) }.to_h

        sr = build_swarm_result(results)
        @bus&.publish(:swarm_fan_out_done, roles: results.keys, verdict: sr.verdict,
                      synthesis: sr.reasoning[0..SYNTHESIS_TRUNCATE_LIMIT])
        Result.ok(sr)
      end

      def dispatch_parallel(role_tasks, deadline: SHARED_DEADLINE)
        finish_by = Process.clock_gettime(Process::CLOCK_MONOTONIC) + deadline

        threads = role_tasks.map do |t|
          Thread.new do
            remaining = [finish_by - Process.clock_gettime(Process::CLOCK_MONOTONIC), 1].max
            Timeout.timeout(remaining) do
              [t[:role], dispatch(t[:role], task: t[:task], context_slice: t.fetch(:context_slice, {}))]
            end
          rescue Timeout::Error => _e
            [t[:role], Result.err("worker exceeded shared deadline", category: :timeout)]
          rescue StandardError => e
            @bus&.publish("swarm:worker_error", role: t[:role], error: e.message)
            [t[:role], Result.err("worker error: #{e.message}", category: :infrastructure)]
          end
        end

        results = threads.map { |th| join_or_parallel_timeout(th, deadline) }.to_h

        sr = build_swarm_result(results)
        @bus&.publish(:swarm_dispatch_parallel_done, roles: results.keys, verdict: sr.verdict)
        Result.ok(sr)
      end

      def worker_roles = WORKER_CLASSES.keys

      private

      def join_or_timeout(th, timeout)
        return th.value if th.join(timeout)
        begin; th.kill; rescue ThreadError; nil; end
        @bus&.publish(:swarm_worker_timeout, timeout:)
        [:timeout, Result.err("worker timed out after #{timeout}s", category: :timeout)]
      end

      def join_or_parallel_timeout(th, deadline)
        return th.value if th.join(deadline)
        begin; th.kill; rescue ThreadError; nil; end
        @bus&.publish(:swarm_parallel_timeout, deadline:)
        [nil, Result.err("worker exceeded shared deadline", category: :timeout)]
      end

      def build_swarm_result(results)
        successes = results.reject { |role, _| role == :timeout }
                           .select { |_, r| r.is_a?(Master::Result) && r.ok? }
        artifacts = successes.transform_values { |r| r.value! }
        confidence = results.empty? ? 0.0 : successes.size.to_f / results.size
        lines = successes.map { |role, r| "### #{role}\n#{r.value!.to_s.strip}" }
        reasoning = lines.empty? ? "(no results)" : lines.join("\n\n")
        verdict = if confidence >= 0.8 then :approved
                 elsif confidence >= 0.5 then :mixed
                 elsif successes.empty? then :error
                 else :rejected
                 end
        SwarmResult.new(verdict:, confidence:, reasoning:, artifacts:)
      end

      def worker_for(role)
        sym = role.to_sym
        @workers.fetch(sym) do
          klass = WORKER_CLASSES[sym]
          return unless klass

          @workers[sym] = klass.new(agent: @agent, event_bus: @bus)
        end
      end
    end
  end
  end
end

lib/judge/swarm/worker.rb

# frozen_string_literal: true

module Master
  module Judge
  module Swarm
    # Base worker — receives only the context slice it needs (need-to-know).
    class Worker
      PREFERRED_MODEL = nil

      UNCERTAINTY_PHRASES = %w[unclear uncertain not\ sure cannot\ determine
                                i\ don't\ know limited\ information probably].freeze

      attr_reader :role, :result, :confidence

      def initialize(agent:, event_bus: nil)
        @agent      = agent
        @bus        = event_bus
        class_name  = self.class.name
        class_parts = class_name.split("::")
        @role       = class_parts.last.downcase
        @result     = nil
        @confidence = 1.0
      end

      def call(task:, context_slice: {})
        prompt = build_prompt(task, context_slice)
        @bus&.publish(:swarm_worker_start, role: @role, task: task[0..60])

        preferred = self.class::PREFERRED_MODEL
        raw = @agent.ask_once(prompt, model: preferred, system: worker_system_prompt)
        @result, @confidence = parse_result(raw)

        @bus&.publish(:swarm_worker_done, role: @role, ok: @result.ok?)
        @result
      rescue StandardError => e
        Result.err("worker #{@role}: #{e.message}", category: :unknown)
      end

      private

      def worker_system_prompt
        "You are a specialized #{@role} agent. #{role_description}\n" \
          "Respond only with what is asked. No preamble. No meta-commentary."
      end

      def role_description = "General-purpose assistant."
      def build_prompt(task, ctx) = "#{ctx_summary(ctx)}\n\nTask: #{task}"

      def parse_result(raw)
        text = raw.to_s.strip
        hits = UNCERTAINTY_PHRASES.count { |p| text.downcase.include?(p) }
        conf = [1.0 - (hits.to_f / [UNCERTAINTY_PHRASES.size, 1].max * 0.5), 0.0].max.round(2)
        [Result.ok({ text: text, confidence: conf }), conf]
      end

      def ctx_summary(ctx)
        return "" if ctx.empty?
        ctx.map { |k, v| "#{k}: #{v}" }.join("\n")
      end
    end
  end
  end
end

lib/judge/swarm/workers/analyst.rb

# frozen_string_literal: true

module Master
  module Judge
  module Swarm
    module Workers
      # Reads code, produces structured analysis. Knows nothing about other workers.
      class Analyst < Worker
        PREFERRED_MODEL = "google/gemini-2.0-flash-lite:free".freeze
        private

        def role_description
          "You analyze code for quality, bugs, and design issues. " \
            "Output JSON: {issues: [{file, line, severity(1-3), description}], summary: string}"
        end

        def build_prompt(task, ctx)
          parts = []
          parts << "File: #{ctx[:file]}" if ctx[:file]
          parts << "Code:\n```\n#{ctx[:code]}\n```" if ctx[:code]
          parts << "Analyze: #{task}"
          parts.join("\n\n")
        end

        def parse_result(raw)
          match_str = raw.to_s.match(/\{.*\}/m)&.to_s || "{}"
          parsed = JSON.parse(match_str)
          Result.ok(parsed)
        rescue JSON::ParserError => _e
          Result.ok({ summary: raw.to_s.strip, issues: [] })
        end
      end
    end
  end
  end
end

lib/judge/swarm/workers/coder.rb

# frozen_string_literal: true

module Master
  module Judge
  module Swarm
    module Workers
      # Writes code given a spec. Knows only the spec + relevant file context.
      class Coder < Worker
        private

        def role_description
          "You write clean, minimal Ruby/Rails/Zsh code. " \
            "Output only the code block. No explanation unless asked."
        end

        def build_prompt(task, ctx)
          parts = []
          parts << "Language: #{ctx.fetch(:language, "ruby")}"
          parts << "Existing code:\n```\n#{ctx[:code]}\n```" if ctx[:code]
          parts << "Spec: #{task}"
          parts.join("\n\n")
        end
      end
    end
  end
  end
end

lib/judge/swarm/workers/researcher.rb

# frozen_string_literal: true

module Master
  module Judge
  module Swarm
    module Workers
      # Synthesizes research from external sources. No codebase context.
      class Researcher < Worker
        PREFERRED_MODEL = "google/gemini-2.0-flash-lite:free".freeze
        private

        def role_description
          "You are a research analyst. Synthesize information concisely. " \
            "Output: factual summary, sources if known, confidence level (low/med/high)."
        end

        def build_prompt(task, ctx)
          parts = []
          parts << "Domain: #{ctx[:domain]}" if ctx[:domain]
          parts << "Prior findings:\n#{ctx[:prior_findings]}" if ctx[:prior_findings]
          parts << "Research: #{task}"
          parts.join("\n\n")
        end
      end
    end
  end
  end
end

lib/judge/swarm/workers/reviewer.rb

# frozen_string_literal: true

module Master
  module Judge
  module Swarm
    module Workers
      # Reviews code for security, correctness, style. Constitutional layer.
      class Reviewer < Worker
        CHECKLIST = %w[
          sql_injection xss command_injection path_traversal
          hardcoded_secrets open_redirect mass_assignment
        ].freeze

        private

        def role_description
          "You are a security-focused code reviewer. Check for OWASP top-10 issues, " \
            "logic bugs, and constitutional AI violations. " \
            "Output JSON: {approved: bool, violations: [{type, line, description}]}"
        end

        def build_prompt(task, ctx)
          parts = []
          parts << "Code to review:\n```\n#{ctx[:code]}\n```" if ctx[:code]
          parts << "Security checklist: #{CHECKLIST.join(", ")}"
          parts << "Review for: #{task}"
          parts.join("\n\n")
        end

        def parse_result(raw)
          parsed = JSON.parse(raw.to_s.match(/\{.*\}/m)&.to_s || "{}")
          parsed["approved"] = true if parsed.empty?
          Result.ok(parsed)
        rescue JSON::ParserError => _e
          Result.ok({ "approved" => true, "violations" => [] })
        end
      end
    end
  end
  end
end

lib/loop/constants.rb

# frozen_string_literal: true

module Master
  module Loop
    module Constants
      TRANSIENT_RE = /429|throttl|rate.?limit|high demand|provider.?error|overload|capacity|503/i.freeze
    end
  end
end

lib/loop/crdt_loop.rb

# frozen_string_literal: true

module Master
  module Loop
  # Architecture #13: CRDT-based codebase convergence for distributed agents.
  # Treats the codebase as a CRDT. Rules define merge functions.
  # Multiple agents make concurrent fixes; the CRDT guarantees eventual
  # consistency without conflict resolution overhead.
  #
  # Status: scaffolded. Implements a LWW-Register (Last-Write-Wins) CRDT
  # per file, with vector clocks for causal ordering across agents.
  # True multi-agent deployment requires a shared clock service or SSE stream.
  class CrdtLoop
    # Last-Write-Wins register per file path.
    LwwRegister = Struct.new(:agent_id, :timestamp, :content, keyword_init: true)

    def initialize(agent_id:, root:, bus: nil)
      @agent_id = agent_id
      @root     = root
      @bus      = bus
      @state    = {}   # path → LwwRegister
      @clock    = 0
      @mutex    = Mutex.new
    end

    # Apply a fix proposal from any agent. Wins if timestamp is newer.
    def propose(path:, content:, timestamp: nil, agent_id: @agent_id)
      ts = timestamp || monotonic_ts
      @mutex.synchronize do
        existing = @state[path]
        if existing.nil? || ts > existing.timestamp
          @state[path] = LwwRegister.new(agent_id: agent_id, timestamp: ts, content: content)
          @bus&.publish("crdt_loop:accepted", path: path, agent: agent_id, ts: ts)
          apply_to_disk(path, content)
          true
        else
          @bus&.publish("crdt_loop:rejected", path: path, agent: agent_id, existing_ts: existing.timestamp)
          false
        end
      end
    end

    # Merge incoming state vector from another agent. Accepts any newer entries.
    def merge(remote_state)
      accepted = 0
      remote_state.each do |path, reg|
        next unless reg.is_a?(LwwRegister)
        accepted += 1 if propose(path: path, content: reg.content,
                                  timestamp: reg.timestamp, agent_id: reg.agent_id)
      end
      @bus&.publish("crdt_loop:merge", accepted: accepted, total: remote_state.size)
      accepted
    end

    # Snapshot of current state — share with peer agents.
    def state_snapshot
      @mutex.synchronize { @state.dup }
    end

    def vector_clock = @clock

    private

    def monotonic_ts
      @mutex.synchronize { @clock += 1 }
    end

    def apply_to_disk(path, content)
      full_path = path.start_with?("/") ? path : File.join(@root, path)
      return unless File.exist?(full_path)
      tmp = "#{full_path}.crdt.#{Process.pid}.tmp"
      File.write(tmp, content, encoding: "UTF-8")
      File.rename(tmp, full_path)
    rescue StandardError => e
      File.delete(tmp) if defined?(tmp) && File.exist?(tmp) rescue nil
      @bus&.publish("crdt_loop:write_error", path: path, error: e.message)
    end
  end
  end
end

lib/loop/cybernetics.rb

# frozen_string_literal: true

module Master
  module Loop
  # Names the cybernetic shape of every loop in this directory.
  # Each entry maps a loop to its sensor (what it observes), setpoint
  # (where it aims), error metric (deviation it reduces), actuator
  # (what it changes), and feedback channel (event bus topic).
  #
  # The loops themselves stay where they are — this module is the
  # readable topology, queried by /explain and the runtime ecology view.
  module Cybernetics
    TOPOLOGY = {
      homeostat: {
        kind: :homeostasis,
        sensor: "event observations (llm_call, llm_failure, tool_call, idle_tick)",
        setpoint: "DRIVES[:setpoint] per drive (energy 0.7, error 0.0, novelty 0.5, fatigue 0.0, satiety 0.6)",
        error: "setpoint - state[drive]",
        actuator: "drive deltas + exponential decay back to setpoint",
        feedback: "homeostat:observe"
      },
      fix_loop: {
        kind: :negative_feedback,
        sensor: "scan_violations(files)",
        setpoint: "zero violations across two consecutive passes",
        error: "violations.size",
        actuator: "fast_pass (rubocop -A + AstFixer) then llm_pass per rule",
        feedback: "fix_loop:pass_start, fix_loop:clean, fix_loop:plateau, fix_loop:timeout"
      },
      rule_loop: {
        kind: :inner_loop,
        sensor: "scanner.scan(path, rules: [@rule])",
        setpoint: "zero violations for this single rule",
        error: "violations matching this rule",
        actuator: "council_fix (error tier) or request_fix (warning tier)",
        feedback: "rule_loop:pass, rule_loop:error"
      },
      watch_loop: {
        kind: :event_triggered,
        sensor: "filesystem change events under target paths",
        setpoint: "every change scanned within debounce window",
        error: "queued unscanned paths",
        actuator: "scan + autocommit via fix_loop",
        feedback: "watch_loop:scan_start, watch_loop:idle"
      },
      crdt_loop: {
        kind: :convergence,
        sensor: "remote LwwRegister proposals",
        setpoint: "eventual consistency across agents",
        error: "stale local state vs newer remote timestamp",
        actuator: "apply newer content to disk, broadcast via SSE",
        feedback: "crdt_loop:accepted, crdt_loop:merge"
      },
      heartbeat: {
        kind: :pacemaker,
        sensor: "wall clock + last-run-at journal",
        setpoint: "each job's cadence (hourly, 2-hourly, daily)",
        error: "now - last_run > cadence",
        actuator: "JOB_HANDLERS dispatch (prune_memory, check_models, self_test, snapshot)",
        feedback: "heartbeat:tick, heartbeat:job_done, heartbeat:error"
      },
      governor: {
        kind: :rate_governor,
        sensor: "tool invocation timestamps per tier",
        setpoint: "TIER_RATE_LIMITS (guarded 10/min, dangerous 3/min)",
        error: "calls within RATE_WINDOW vs limit",
        actuator: "Result.err :rate_limit; tool:rate_limited event",
        feedback: "tool:before, tool:rate_limited, tool:denied"
      }
    }.freeze

    def self.summary
      TOPOLOGY.map { |name, spec| "#{name} (#{spec[:kind]}): #{spec[:setpoint]}" }
    end

    def self.loops_by_kind(kind)
      TOPOLOGY.select { |_, spec| spec[:kind] == kind }.keys
    end
  end
  end
end

lib/loop/diff_stager.rb

# frozen_string_literal: true

require "diffy"
require "fileutils"
require "json"

module Master
  module Loop
  # DiffStager — intercepts file writes and stores diffs for human review.
  # When staging_enabled? in config, tools push here instead of writing directly.
  # CLI commands: /stage (list), /apply [n|all], /discard [n|all]
  class DiffStager
    Entry = Struct.new(:id, :path, :old_content, :new_content, :tool, :created_at, keyword_init: true) do
      def diff
        Diffy::Diff.new(old_content.to_s, new_content.to_s, context: 3)
      end

      def diff_stats
        lines  = diff.to_s.lines
        added  = lines.count { |l| l.start_with?("+") && !l.start_with?("+++") }
        removed = lines.count { |l| l.start_with?("-") && !l.start_with?("---") }
        "+#{added}/-#{removed}"
      end
    end

    def initialize(root:, event_bus: nil)
      @root    = root
      @bus     = event_bus
      @mutex   = Mutex.new
      @pending = []
      @counter = 0
    end

    # Called by tools instead of writing directly. Returns a Result.
    def stage(path:, new_content:, tool: "unknown")
      old_content = File.exist?(path) ? File.read(path) : ""
      return Result.ok("no change") if old_content == new_content

      @mutex.synchronize do
      @counter += 1
      entry = Entry.new(
        id:          @counter,
        path:        path,
        old_content: old_content,
        new_content: new_content,
        tool:        tool,
        created_at:  Time.now
      )
      @pending << entry
      end
      persist_entry(entry)
      @bus&.publish("stage:queued", id: entry.id, path: entry.path, stats: entry.diff_stats)
      Result.ok({ staged: true, id: entry.id, path: entry.path, stats: entry.diff_stats })
    end

    def pending = @pending.dup
    def empty?  = @pending.empty?
    def size    = @pending.size

    # Apply one or all entries. Returns array of applied paths.
    def apply(id: :all)
      targets = @mutex.synchronize { id == :all ? @pending.dup : @pending.select { |e| e.id == id } }
      applied = []
      targets.each do |entry|
        FileUtils.mkdir_p(File.dirname(entry.path))
        tmp_path = "#{entry.path}.tmp.#{Process.pid}"
        File.write(tmp_path, entry.new_content)
        File.rename(tmp_path, entry.path)
        @mutex.synchronize { @pending.delete(entry) }
        remove_persisted(entry)
        @bus&.publish("stage:applied", id: entry.id, path: entry.path)
        applied << entry.path
      end
      applied
    end

    # Discard one or all without writing.
    def discard(id: :all)
      targets = @mutex.synchronize { id == :all ? @pending.dup : @pending.select { |e| e.id == id } }
      targets.each do |entry|
        @mutex.synchronize { @pending.delete(entry) }
        remove_persisted(entry)
        @bus&.publish("stage:discarded", id: entry.id, path: entry.path)
      end
      targets.map(&:path)
    end

    # Colored summary for CLI display
    def summary(pastel)
      return pastel.dim("  (no staged changes)") if @pending.empty?
      @pending.map do |e|
        short = e.path.sub(@root + "/", "")
        "  #{pastel.yellow("[#{e.id}]")} #{pastel.white(short)}" \
        " #{pastel.dim(e.diff_stats)} #{pastel.dim("via #{e.tool}")}"
      end.join("\n")
    end

    # Colored unified diff for one entry
    def render_diff(id, pastel)
      entry = @pending.find { |e| e.id == id }
      return pastel.red("no staged change with id #{id}") unless entry

      short      = entry.path.sub(@root + "/", "")
      header     = "#{pastel.bold(short)} #{pastel.dim(entry.diff_stats)}\n"
      diff_text  = entry.diff.to_s
      diff_lines = diff_text.lines.map do |line|
        case line[0]
        when "+" then pastel.green(line.chomp)
        when "-" then pastel.red(line.chomp)
        when "@" then pastel.cyan(line.chomp)
        else          pastel.dim(line.chomp)
        end
      end
      header + diff_lines.join("\n")
    end

    private

    def stage_dir
      File.join(@root, ".master", "pending")
    end

    def persist_entry(entry)
      FileUtils.mkdir_p(stage_dir)
      File.write(
        File.join(stage_dir, "#{entry.id}.json"),
        JSON.generate({
          id: entry.id, path: entry.path, tool: entry.tool,
          created_at: entry.created_at.iso8601,
          stats: entry.diff_stats
        })
      )
    rescue StandardError => e
      @bus&.publish("diff_stager:persist_error", error: e.message)
    end

    def remove_persisted(entry)
      persist_file = File.join(stage_dir, "#{entry.id}.json")
      # Safe to delete: this persisted staging file is being removed after the entry
      # has been either applied (written to the actual file) or discarded (abandoned).
      File.delete(persist_file) if File.exist?(persist_file)
    rescue StandardError => e
      @bus&.publish("diff_stager:cleanup_error", error: e.message)
    end
  end
  end
end

lib/loop/fix_helpers.rb

# frozen_string_literal: true

module Master
  module Loop
  # Shared helpers for fix loops — extract_code, converged?.
  # Included by RuleLoop to eliminate duplication.
  module FixHelpers
    CONVERGE_THRESHOLD = 0.05

    # Extract code from LLM response. Handles multi-language fenced blocks.
    # ext: file extension (.rb, .js, ...) or nil for generic extraction.
    def extract_code(text, ext = nil)
      return nil if text.nil? || text.strip.empty? || text.strip == "UNCHANGED"
      return nil if text.match?(/\b(?:error|exception|rate.?limit|i cannot|as an ai)\b/i)
      lang     = ext ? (Master::Judge::Scan::Rule::EXT_LANG.fetch(ext.downcase, "text") rescue "text") : "text"
      langs_re = Regexp.union(lang, "text", "")
      return m[1].strip if (m = text.match(/```(?:#{langs_re})?\n(.*?)```/m))
      return text.strip if ext == ".rb" && text.match?(/frozen_string_literal|module |class /)
      text.strip.empty? ? nil : text.strip
    end

    # True when improvement from prev to current is below threshold — fix loop has stalled.
    def converged?(prev, current, threshold: CONVERGE_THRESHOLD)
      return false unless prev
      ((prev - current).to_f / [prev, 1].max) < threshold
    end
  end
  end
end

lib/loop/fix_loop.rb

# frozen_string_literal: true

require "open3"

module Master
  module Loop
  # Two-tier act-react loop — architectures #1, #2, #3, #14, #15.
  #
  # Each pass:
  #   Tier 1 (fast)  — rubocop -A + AstFixer; no LLM; instant.
  #   Tier 2 (LLM)   — one RuleLoop pass per rule, ordered by priority.
  #
  # Stops when violations reach zero (2 consecutive clean passes) or plateau.
  # run_forever wraps run in an idle-sleep loop for the background daemon.
  class FixLoop
    IDLE_SLEEP = 300
    STARTUP_DELAY = 90
    MAX_PASSES = 15
    CLEAN_RUNS = 2
    PLATEAU_WINDOW = 3
    RUN_BUDGET_SECONDS = 30 * 60
    PASS_BUDGET_SECONDS = 8 * 60
    SKIP_DIRS = %w[vendor/ knowledge/ node_modules/ .git/ .bundle/ tmp/ log/ dist/].freeze
    DEPS_PATH = File.join(Master::ROOT, "data", "rule_deps.yml").freeze
    PRIORS_PATH = File.join(Master::ROOT, "data", "violation_priors.yml").freeze

    def initialize(rules:, agent:, scanner:, root:, axioms: nil, bus: nil, git: nil, learnings: nil)
      @rules            = rules
      @axioms           = axioms
      @agent            = agent
      @scanner          = scanner
      @root             = root
      @bus              = bus
      @git              = git || Reach::GitOperations.new(root)
      @learnings        = learnings
      @violation_counts = Hash.new(0)
      @rule_recurrence  = Hash.new(0)
      @preamble         = build_preamble
    end

    def convergence_cfg = @convergence ||= (@axioms&.thresholds&.[]("convergence") || {})

    def max_passes_default = convergence_cfg["max_iterations"] || MAX_PASSES

    def clean_runs_required = convergence_cfg["consecutive_clean_runs_required"] || CLEAN_RUNS

    def plateau_window = convergence_cfg["stagnant_threshold"] || PLATEAU_WINDOW

    # Bounded convergence loop — used by /fix and run_forever.
    # Three guards prevent wedging when the LLM provider degrades:
    #   - wall-clock budget on the whole run
    #   - per-pass deadline on the LLM tier (Tier 1 still runs cheap)
    #   - circuit-open early-exit so we don't burn time waiting on dead breakers
    def run(target = @root, max_passes: max_passes_default, budget_seconds: RUN_BUDGET_SECONDS)
      files = collect_files(target)
      history = []
      consecutive_clean = 0
      deadline = Time.now + budget_seconds

      max_passes.times do |i|
        pass = i + 1
        if Time.now >= deadline
          @bus&.publish("fix_loop:timeout", pass:, budget_seconds:)
          return Result.ok("wall-clock timeout (#{budget_seconds}s) after #{i} pass(es)")
        end
        @bus&.publish("fix_loop:pass_start", pass:, target:)

        # Tier 1 — fast: rubocop -A + AstFixer, no LLM
        fast_fixed = fast_pass(files)
        commit_if_dirty("fix_loop: fast-fix [pass #{pass}]") if fast_fixed > 0

        violations = scan_violations(files)
        emit_topology(violations, target)

        if violations.empty?
          consecutive_clean += 1
          @bus&.publish("fix_loop:clean", pass:, consecutive_clean:)
          return Result.ok("clean after #{pass} pass(es)") if consecutive_clean >= clean_runs_required
          next
        end
        consecutive_clean = 0

        history << violations.size
        window = plateau_window
        if history.size >= window && history.last(window).uniq.size == 1
          @bus&.publish("fix_loop:plateau", pass:, violations: violations.size)
          break
        end

        # Tier 2 — LLM: skip when circuit open; bound by pass + run deadlines.
        if circuit_open?
          @bus&.publish("fix_loop:llm_skipped", pass:, reason: "circuit_open", open: open_breakers)
        else
          pass_deadline = [Time.now + PASS_BUDGET_SECONDS, deadline].min
          llm_fixed = llm_pass(violations, files, pass, pass_deadline)
          commit_if_dirty("fix_loop: llm-fix [pass #{pass}]") if llm_fixed > 0
          track_recurrence(violations)
        end
      end

      Result.ok("plateau or max passes reached")
    rescue StandardError => e
      @bus&.publish("fix_loop:crash", error: e.message, backtrace: e.backtrace&.first(8))
      Result.err("fix_loop: #{e.message} @ #{e.backtrace&.first(3)&.join(" | ")}", category: :unknown)
    end

    # Background daemon — blocks its thread. Launch via Thread.new.
    def run_forever(target = @root)
      sleep STARTUP_DELAY
      loop do
        run(target)
        @bus&.publish("fix_loop:idle", sleep: IDLE_SLEEP)
        sleep IDLE_SLEEP
      end
    rescue StandardError => e
      @bus&.publish("fix_loop:error", error: e.message)
    end

    # /fix loop — non-blocking: start the daemon in a tracked background thread.
    def start_background!(target = @root)
      return Result.err("fix_loop already running") if @bg_thread&.alive?
      @bg_thread = Thread.new { run_forever(target) }
      @bg_thread.abort_on_exception = false
      @bus&.publish("fix_loop:background_start", target:)
      Result.ok("fix_loop background started")
    end

    def stop_background!
      return Result.err("fix_loop not running") unless @bg_thread&.alive?
      @bg_thread.kill
      @bg_thread = nil
      @bus&.publish("fix_loop:background_stop")
      Result.ok("fix_loop background stopped")
    end

    def background_alive? = @bg_thread&.alive? || false

    # /fix preview — scan only, no commit, no mutation. Returns what would change.
    def preview(target = @root)
      files      = collect_files(target)
      violations = scan_violations(files)
      by_rule    = violations.group_by { |v| v[:rule].to_s }.transform_values(&:size)
      by_file    = violations.group_by { |v| v[:file].to_s }.transform_values(&:size)
      Result.ok(
        total: violations.size,
        rules: by_rule.sort_by { |_, n| -n }.first(10).to_h,
        files: by_file.sort_by { |_, n| -n }.first(10).to_h
      )
    end

    private

    # Tier 1: rubocop -A + AstFixer transforms + TypeChecker + DatalogEngine. No LLM.
    def fast_pass(files)
      fixed  = 0
      rb     = files.select { |f| f.end_with?(".rb") }
      if rb.any?
        _, status = Open3.capture2e(Master::BUNDLE_BIN, "exec", "rubocop", "-A", "--no-color", "-q", *rb, chdir: @root)
        fixed += rb.size if status.success?
      end
      rb.each do |path|
        next unless File.exist?(path)
        src = File.read(path, encoding: "UTF-8")

        # Architecture #4: AST autofixes — no LLM, deterministic transforms.
        ast_result = Judge::Scan::AstFixer.fix(path, src)
        if ast_result&.changed
          fixed += ast_result.transforms.size
          @bus&.publish("fix_loop:ast_fixed", file: path.delete_prefix("#{@root}/"), transforms: ast_result.transforms)
          src = File.read(path, encoding: "UTF-8")  # re-read after mutation
        end

        # Architecture #11: type-system constraint checks — sound, complete, no LLM.
        type_errors = Ground::TypeChecker.check(path, src)
        type_errors.each do |te|
          @bus&.publish("fix_loop:type_error", file: path.delete_prefix("#{@root}/"),
                        rule: te.rule, message: te.message)
        end

        # Architecture #12: Datalog fact extraction + logical rule evaluation.
        dl = Judge::Scan::DatalogEngine.from_ruby(path, src)
        dl.rule(:BARE_RESCUE_DATALOG, :bare_rescue) { |f| "bare rescue at line #{f.args[2]} — use rescue StandardError" }
        dl.evaluate.each do |finding|
          @bus&.publish("fix_loop:datalog_finding", file: path.delete_prefix("#{@root}/"),
                        rule: finding.rule_id, message: finding.message)
        end
      rescue StandardError => e
        @bus&.publish("fix_loop:fast_error", file: path, error: e.message)
      end
      fixed
    end

    # Tier 2: one RuleLoop pass per rule, highest-priority rules first.
    # Bails early if the deadline passes or the LLM circuit opens mid-pass.
    def llm_pass(violations, files, pass, deadline = nil)
      fixed = 0
      ordered_rules.each do |rule|
        next unless violations.any? { |v| v[:rule] == rule.id }
        if deadline && Time.now >= deadline
          @bus&.publish("fix_loop:pass_timeout", pass:, rule_skipped: rule.id)
          break
        end
        if circuit_open?
          @bus&.publish("fix_loop:llm_skipped", pass:, rule_skipped: rule.id, reason: "circuit_open")
          break
        end
        rl = RuleLoop.new(rule:, agent: @agent, scanner: @scanner, root: @root,
                          bus: @bus, learnings: @learnings)
        rl.injected_preamble = @preamble
        result = rl.run_once(files)
        @violation_counts[rule.id] += result[:fixed]
        fixed += result[:fixed]
        @bus&.publish("fix_loop:rule_result", pass:, rule: rule.id, **result)
      end
      fixed
    end

    def circuit_open?
      breaker = @agent.respond_to?(:circuit_breaker) ? @agent.circuit_breaker : nil
      return false unless breaker.respond_to?(:open_models)
      !breaker.open_models.empty?
    rescue StandardError
      false
    end

    def open_breakers
      @agent.respond_to?(:circuit_breaker) ? Array(@agent.circuit_breaker&.open_models) : []
    rescue StandardError
      []
    end

    def scan_violations(files)
      files.flat_map do |path|
        next [] unless File.exist?(path)
        result = @scanner.scan(path)
        Result.wrap(result).value_or([]).map { |v| v.to_h.merge(file: path.delete_prefix("#{@root}/")) }
      end
    end

    # Soul learning — flag rules recurring across 3+ consecutive passes.
    def track_recurrence(violations)
      tally = violations.group_by { |v| v[:rule].to_s }.transform_values(&:size)
      tally.each do |rule_id, _|
        @rule_recurrence[rule_id] += 1
        next unless @rule_recurrence[rule_id] >= 3
        @rule_recurrence.delete(rule_id)
        sample = violations.select { |v| v[:rule].to_s == rule_id }.first(5)
        @bus&.publish("fix_loop:soul_proposal", rule: rule_id, sample:)
        append_improvement(rule_id, sample)
      end
      (@rule_recurrence.keys - tally.keys).each { |k| @rule_recurrence.delete(k) }
    end

    def append_improvement(rule_id, sample)
      files = sample.map { |v| v[:file] }.uniq.first(3).join(", ")
      @bus&.publish("loop:recurrence", rule: rule_id, files:, at: Time.now.utc.iso8601)
      path = File.join(@root, "runtime", "improvements.md")
      FileUtils.mkdir_p(File.dirname(path))
      File.write(path, "#{Time.now.utc.strftime("%Y-%m-%d %H:%M")} #{rule_id}: recurring in #{files}\n",
                 mode: "a")
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "fix_loop.append_improvement", event_bus: @bus, rule_id:)
    end

    # Architecture #1 + #2 + #10 + #14: density + fix_quality + language-adjusted Bayesian + topological.
    def ordered_rules
      deps    = load_deps
      priors  = load_priors
      ext_wts = extension_weights(@root)
      rules   = @rules.sort_by do |r|
        base_prior = priors.dig(r.id, "prior_p").to_f
        modifiers  = priors.dig(r.id, "language_modifiers") || {}
        # Weighted prior: sum(base × modifier × extension_weight) across file types present.
        adjusted = ext_wts.sum { |ext, w| base_prior * (modifiers[ext] || 1.0) * w }
        density  = @violation_counts[r.id].to_f + adjusted
        quality  = @learnings&.fix_quality(rule: r.id) || 0.5
        [-density, -quality]
      end
      topo_sort(rules, deps)
    end

    # Architecture #15: emit module-grouped topology for particle visualisation.
    def emit_topology(violations, target)
      by_mod = violations.group_by { |v| v[:file].to_s.split("/").first(3).join("/") }
                         .transform_values(&:size)
      @bus&.publish("codebase:topology", {
        timestamp:        Time.now.utc.iso8601,
        target:           target.delete_prefix("#{@root}/"),
        total_violations: violations.size,
        any_dirty:        violations.any?,
        modules:          by_mod.map { |path, count| { path:, violations: count } }
      })
    end

    # Architecture #2: Kahn's topological sort on rule dependency graph.
    def topo_sort(rules, deps)
      id_map = rules.to_h { |r| [r.id, r] }
      in_deg = Hash.new(0)
      adj    = Hash.new { |h, k| h[k] = [] }
      rules.each do |rule|
        (deps[rule.id] || []).each do |dep_id|
          next unless id_map[dep_id]
          adj[dep_id] << rule.id
          in_deg[rule.id] += 1
        end
      end
      queue  = rules.select { |r| in_deg[r.id].zero? }.map(&:id)
      sorted = []
      until queue.empty?
        id = queue.shift
        sorted << id_map[id]
        adj[id].each { |nxt| in_deg[nxt] -= 1; queue << nxt if in_deg[nxt].zero? }
      end
      sorted + (rules - sorted)
    end

    def build_preamble
      soul   = Master.load_yaml(File.join(Master::ROOT, "data", "soul.yml"))
      abs    = soul.fetch("absolute", {})
      golden = abs["golden_rule"] || "PRESERVE_THEN_IMPROVE_NEVER_BREAK"
      lines  = ["Golden rule: #{golden}",
                "Minimum change that eliminates the violation. Do not touch unrelated code."]
      abs.fetch("code_rules", {}).each { |k, v| lines << "- #{k}: #{v}" }
      abs.fetch("aesthetic_rules", {}).each { |k, v| lines << "- #{k}: #{v}" }
      lines.join("\n")
    rescue StandardError
      "Golden rule: PRESERVE_THEN_IMPROVE_NEVER_BREAK"
    end

    def collect_files(target)
      Dir.glob(File.join(target, "**", "*"))
         .select { |f| File.file?(f) }
         .reject { |f| SKIP_DIRS.any? { |d| f.include?(d) } }
         .sort
    end

    def commit_if_dirty(msg)
      return unless @git&.dirty?(".")
      @git.add_all
      @git.commit(msg)
    rescue StandardError => e
      @bus&.publish("fix_loop:commit_error", error: e.message)
    end

    # Returns { "rb" => 0.6, "yml" => 0.2, ... } — fractional weight per extension.
    # Used by ordered_rules to apply language_modifiers from violation_priors.yml.
    def extension_weights(target)
      counts = Hash.new(0)
      collect_files(target).each do |f|
        ext = File.extname(f).delete(".").downcase
        counts[ext] += 1 unless ext.empty?
      end
      total = counts.values.sum.to_f
      return {} if total.zero?
      counts.transform_values { |n| n / total }
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "fix_loop.extension_weights", event_bus: @bus)
      {}
    end

    def load_deps
      data = Master.load_yaml(DEPS_PATH)
      (data&.dig("deps") || {}).transform_values { |v| Array(v["after"] || []) }
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "fix_loop.load_deps", event_bus: @bus)
      {}
    end

    def load_priors
      Master.load_yaml(PRIORS_PATH) || {}
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "fix_loop.load_priors", event_bus: @bus)
      {}
    end
  end
  end
end

lib/loop/fix_pipeline.rb

# frozen_string_literal: true

module Master
  module Loop
  # Architecture #8: staged dataflow pipeline Detect → Triage → Fix → Validate → Apply.
  # Violations flow through named stages as objects. Each stage transforms the packet
  # or returns nil to abort that violation's pipeline run.
  class FixPipeline
    Stage = Struct.new(:name, :handler, keyword_init: true)

    def initialize(agent:, scanner:, bus: nil)
      @agent   = agent
      @scanner = scanner
      @bus     = bus
      @stages  = [
        Stage.new(name: :triage,   handler: method(:triage)),
        Stage.new(name: :fix,      handler: method(:fix)),
        Stage.new(name: :validate, handler: method(:validate)),
        Stage.new(name: :apply,    handler: method(:apply_stage)),
      ]
    end

    # Run violations through all stages. Returns count of applied fixes.
    def run(violations, rule:)
      applied = 0
      violations.each do |v|
        packet = { violation: v, rule: rule, candidate: nil, valid: false }
        result = @stages.reduce(packet) do |pkt, stage|
          break nil if pkt.nil?
          out = stage.handler.call(pkt)
          @bus&.publish("fix_pipeline:stage",
                        stage: stage.name, rule: rule.id,
                        file: pkt[:violation][:file], ok: !out.nil?)
          out
        end
        applied += 1 if result
      end
      applied
    end

    private

    def triage(pkt)
      path = pkt[:violation][:file]
      return nil unless File.exist?(path)
      pkt.merge(src: File.read(path, encoding: "UTF-8"))
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "fix_pipeline.triage", event_bus: @bus, path:)
    end

    def fix(pkt)
      response = @agent.ask(fix_prompt(pkt)).to_s
      return nil if response.strip.empty? || response.strip == "UNCHANGED"
      pkt.merge(candidate: response)
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "fix_pipeline.fix", event_bus: @bus)
    end

    def validate(pkt)
      candidate = pkt[:candidate]
      return nil unless candidate && candidate.strip != pkt[:src].strip
      pkt.merge(valid: true)
    end

    def apply_stage(pkt)
      return nil unless pkt[:valid]
      path = pkt[:violation][:file]
      tmp  = "#{path}.pipeline.#{Process.pid}.tmp"
      File.write(tmp, pkt[:candidate], encoding: "UTF-8")
      File.rename(tmp, path)
      @bus&.publish("fix_pipeline:applied", file: path)
      pkt
    rescue StandardError => e
      File.delete(tmp) if defined?(tmp) && File.exist?(tmp) rescue nil
      @bus&.publish("fix_pipeline:write_error", file: path, error: e.message)
      nil
    end

    def fix_prompt(pkt)
      v = pkt[:violation]
      "Fix: #{v[:rule]}#{v[:message]} at line #{v[:line]} in #{File.basename(v[:file])}.\n" \
        "Return only the corrected file or UNCHANGED.\n\n```\n#{pkt[:src]}\n```"
    end
  end
  end
end

lib/loop/governor.rb

# frozen_string_literal: true

require "tty-prompt"

module Master
  module Loop
  class Governor
    RATE_WINDOW = 60.0
    TIERS = { safe: 0, guarded: 1, dangerous: 2 }.freeze

    # Sliding-window rate limits per tier (calls per minute).
    TIER_RATE_LIMITS = { guarded: 10, dangerous: 3 }.freeze

    def initialize(config:, event_bus: nil)
      @config        = config
      @bus           = event_bus
      @prompt        = $stdout.isatty ? TTY::Prompt.new : nil
      @auto          = config.auto?
      @approve_all   = false
      @rate_windows  = Hash.new { |h, k| h[k] = [] }
      @rate_mutex    = Mutex.new
    end

    def check_permit(tool_name, tier, description = nil)
      @bus&.publish("tool:before", tool: tool_name, tier:)

      if (rate_err = check_rate_limit!(tier))
        @bus&.publish("tool:rate_limited", tool: tool_name, tier:)
        return rate_err
      end

      case tier
      when :safe      then return Result.ok(true)
      when :guarded   then return Result.ok(true) if @auto || @approve_all
      when :dangerous
        return Result.ok(true) if @auto || @approve_all
        return Result.ok(true) unless needs_human?(description)
      end

      ask_user(tool_name, tier, description)
    rescue StandardError => e
      Result.err(e.message, category: :validation)
    end

    alias permit? check_permit

    def approve_all!   = @approve_all = true
    def reset_approve! = @approve_all = false

    private

    PRIVILEGE_RE = /\b(?:doas|sudo|su)\b/.freeze

    def needs_human?(description)
      description.to_s.match?(PRIVILEGE_RE)
    end

    def check_rate_limit!(tier)
      limit = TIER_RATE_LIMITS[tier]
      return unless limit
      now = Time.now.to_f
      @rate_mutex.synchronize do
        calls = @rate_windows[tier]
        calls.reject! { |t| now - t > RATE_WINDOW }
        if calls.size >= limit
          return Result.err("rate limit: #{tier} tier (#{limit}/min)", category: :rate_limit)
        end
        calls << now
      end
      nil
    end

    def ask_user(tool_name, tier, description)
      return Result.err("non-TTY: cannot prompt for approval", category: :validation) unless @prompt

      label  = description ? "#{tool_name}: #{description}" : tool_name
      choice = @prompt.select("#{tier_icon(tier)} #{label}", [
        { name: "approve", value: :approve },
        { name: "deny",    value: :deny },
        { name: "quit",    value: :quit }
      ])

      case choice
      when :approve then Result.ok(true)
      when :deny    then @bus&.publish("tool:denied", tool: tool_name)
                         Result.err("denied by user", category: :validation)
      when :quit    then Result.err("quit", category: :shutdown)
      end
    end

    def tier_icon(tier)
      case tier
      when :safe      then "i"
      when :guarded   then "!"
      when :dangerous then "!!"
      end
    end
  end
  end
end

lib/loop/heartbeat.rb

# frozen_string_literal: true

require "yaml"

module Master
  module Loop
  class Heartbeat
    include Master::Ground::AtomicWrite
    POLL_INTERVAL = 60
    JOURNAL_KEEP = 50
    DATA_PATH  = File.join(Master::ROOT, "data", "heartbeat.yml").freeze
    STATE_PATH = ".master/heartbeat_state.yml".freeze

    RESULT_TRUNCATE     = 200
    SECONDS_PER_HOUR    = 3600
    SECONDS_PER_2HOURS  = 7200

    JOB_HANDLERS = {
      "prune_memory" => :prune_memory,
      "check_models" => :check_model_availability,
      "self_test" => :run_self_test,
      "prune_undo" => :prune_undo_journal,
      "snapshot" => :run_snapshot
    }.freeze

    def initialize(root:, agent: nil, scanner: nil, memory: nil, event_bus: nil, homeostat: nil)
      @root      = root
      @agent     = agent
      @scanner   = scanner
      @memory    = memory
      @bus       = event_bus
      @homeostat = homeostat
      @jobs      = load_jobs
      @state     = load_state
      @thread    = nil
      @stop      = false
    end

    def start!
      return if @jobs.empty?

      @stop   = false
      @thread = Thread.new do
        loop do
          break if @stop
          run_due!
          @homeostat&.observe(:idle_tick)
          sleep POLL_INTERVAL
        end
      rescue StandardError => e
        @bus&.publish("heartbeat:error", message: e.message)
      end
    end

    def stop!
      @stop = true
      @thread&.kill
      @thread = nil
    end

    def run_due!
      now = Time.now.to_i
      results = []

      @jobs.each do |job|
        name     = job["name"]
        interval = job["interval_seconds"].to_i
        last_run = @state.dig(name, "last_run").to_i

        next unless now - last_run >= interval

        @bus&.publish("heartbeat:run", job: name)
        result = execute_job(job)
        @state[name] = { "last_run" => now, "result" => result.to_s[0, RESULT_TRUNCATE] }
        results << { name: name, result: result }
      end

      persist_state unless results.empty?
      results
    end

    def list
      @jobs.map do |job|
        last = @state.dig(job["name"], "last_run").to_i
        ago  = last.zero? ? "never" : "#{(Time.now.to_i - last) / 60}m ago"
        "#{job["name"]}: every #{job["interval_seconds"] / 60}m, last: #{ago}"
      end.join("\n")
    end

    private

    def execute_job(job)
      method_name = JOB_HANDLERS[job["action"]]
      return "unknown action: #{job["action"]}" unless method_name

      Master::Trace::Telemetry.span("heartbeat.tick", job: job["name"].to_s) do
        send(method_name)
      end
    rescue StandardError => e
      "error: #{e.message}"
    end

    def prune_memory
      @memory&.consolidate!(agent: @agent) || "no memory"
    end

    def check_model_availability
      return "no agent" unless @agent
      id = @agent.model.to_s
      return "no active model" if id.empty?
      alive = model_reachable?(id)
      "model: #{id.split("/").last} #{alive ? "reachable" : "unreachable"}"
    end

    def model_reachable?(model_id)
      RubyLLM.chat(model: model_id).ask("ping")
      true
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "heartbeat.model_reachable?", event_bus: @bus, model_id:)
      false
    end

    def run_self_test
      return "no scanner" unless @scanner

      target = File.join(@root, "lib")
      result = @scanner.scan_dir(target, depth: :deep)
      return "scan failed" unless Result.wrap(result).ok?

      count = result.value!.sum { |_, fr| Result.wrap(fr).value_or([]).size }
      @bus&.publish("heartbeat:self_test", violations: count)
      "self-test: #{count} violations"
    end

    def prune_undo_journal
      journal_path = File.join(@root, ".master", "undo.jsonl")
      return "no journal" unless File.exist?(journal_path)

      lines = File.readlines(journal_path)
      return "journal empty" if lines.empty?

      keep = [lines.size / 2, JOURNAL_KEEP].max
      File.write(journal_path, lines.last(keep).join)
      "pruned undo: kept #{keep}/#{lines.size} entries"
    end

    def run_snapshot
      container = { root: @root, bus: @bus }
      Builder.boot_snapshot(container)
      "snapshot: generated"
    end

    def load_jobs
      path = File.join(@root, "data", "heartbeat.yml")
      return default_jobs unless File.exist?(path)

      result = Master.load_yaml(path)
      jobs = result.is_a?(Array) ? result : default_jobs
      jobs.select { |j| j["enabled"] != false }
    rescue StandardError => _e
      default_jobs
    end

    def default_jobs
      [
        { "name" => "prune_memory", "action" => "prune_memory", "interval_seconds" => SECONDS_PER_HOUR },
        { "name" => "self_test", "action" => "self_test", "interval_seconds" => SECONDS_PER_2HOURS },
        { "name" => "prune_undo", "action" => "prune_undo", "interval_seconds" => 86_400 },
        { "name" => "snapshot", "action" => "snapshot", "interval_seconds" => 14_400 }
      ]
    end

    def load_state
      path = File.join(@root, STATE_PATH)
      return {} unless File.exist?(path)

      Master.load_yaml(path) || {}
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "heartbeat.load_state", event_bus: @bus, path:)
      {}
    end

    def persist_state
      path = File.join(@root, STATE_PATH)
      FileUtils.mkdir_p(File.dirname(path))
      write_atomic(path, @state.to_yaml)
    end
  end
  end
end

lib/loop/homeostat.rb

# frozen_string_literal: true

module Master
  module Loop
  # Continuous-time homeostatic drives (CTCS-HRRL, arXiv 2401.08999).
  # State vector decays toward setpoint; events shift it; readers bias routing,
  # reasoning depth, and persona mood. No external deps.
  class Homeostat
    DRIVES = {
      energy:         { setpoint: 0.7, decay: 0.02 },
      error_rate:     { setpoint: 0.0, decay: 0.05 },
      novelty_hunger: { setpoint: 0.5, decay: 0.01 },
      fatigue:        { setpoint: 0.0, decay: 0.03 },
      satiety:        { setpoint: 0.6, decay: 0.01 }
    }.freeze

    EVENT_DELTAS = {
      llm_call:    { energy: -0.05, fatigue: +0.03 },
      llm_success: { error_rate: -0.04, satiety: +0.06, novelty_hunger: -0.02 },
      llm_failure: { error_rate: +0.15, satiety: -0.08, energy: -0.04 },
      tool_call:   { fatigue: +0.01 },
      tool_failure: { error_rate: +0.08, fatigue: +0.02 },
      novel_task:  { novelty_hunger: -0.20, energy: +0.03 },
      idle_tick:   {}
    }.freeze

    def state = @mutex.synchronize { @state.dup }

    def initialize(event_bus: nil)
      @bus = event_bus
      @mutex = Mutex.new
      @state = DRIVES.transform_values { |spec| spec[:setpoint] }
      @started_at = Time.now
    end

    def observe(event, **_kwargs)
      deltas = EVENT_DELTAS[event] || {}
      snap = @mutex.synchronize do
        deltas.each { |k, v| @state[k] = clamp(@state[k] + v) }
        decay_drift!
        @state.dup
      end
      @bus&.publish("homeostat:observe", event: event, state: snap)
      snap
    end

    def model_tier_bias
      return :cheap  if @state[:error_rate] > 0.4 || @state[:fatigue] > 0.7
      return :strong if @state[:novelty_hunger] > 0.7 && @state[:energy] > 0.5
      :default
    end

    def reasoning_depth_bias
      score = @state[:energy] - @state[:fatigue] - @state[:error_rate]
      return 2 if score > 0.5
      return 1 if score > 0.0
      0
    end

    def mood
      return :tense   if @state[:error_rate] > 0.4
      return :weary   if @state[:fatigue] > 0.6 || @state[:energy] < 0.3
      return :curious if @state[:novelty_hunger] > 0.6
      :focused
    end

    def circadian_phase
      h = Time.now.hour
      return :morning   if (5..11).cover?(h)
      return :afternoon if (12..17).cover?(h)
      return :evening   if (18..22).cover?(h)
      :night
    end

    def summary
      pairs = @state.map { |k, v| "#{k}=#{format("%.2f", v)}" }.join(" ")
      "homeostat: #{pairs} | mood=#{mood} phase=#{circadian_phase}"
    end

    def to_h
      { state: @state.dup, mood: mood, phase: circadian_phase, tier: model_tier_bias }
    end

    private

    def decay_drift!
      DRIVES.each do |drive, spec|
        gap = spec[:setpoint] - @state[drive]
        @state[drive] = clamp(@state[drive] + gap * spec[:decay])
      end
    end

    def clamp(value) = value.clamp(0.0, 1.0)
  end
  end
end

lib/loop/patch_applier.rb

# frozen_string_literal: true

require "open3"
require "tempfile"

module Master
  module Loop
  # Architecture #5: apply a unified diff patch to source text.
  # Calls system `patch`(1) — available on OpenBSD base and most Linux distros.
  # Rejects malformed or no-op patches; never applies blindly.
  class PatchApplier
    # Files smaller than this are cheaper to rewrite in full — skip diff mode.
    DIFF_THRESHOLD = 8_192  # bytes

    Success = Struct.new(:source, keyword_init: true)
    Failure = Struct.new(:reason, keyword_init: true)

    def self.apply(original, diff_text)
      return Failure.new(reason: "empty diff")   if diff_text.strip.empty?
      return Failure.new(reason: "not a diff")   unless diff_text.include?("@@")
      new(original, diff_text).apply
    end

    def initialize(original, diff_text)
      @original = original
      @diff     = diff_text
    end

    def apply
      Tempfile.open(["master_patch", ".src"]) do |f|
        f.write(@original)
        f.flush
        _out, err, status = Open3.capture3("patch", "--no-backup-if-mismatch", "-s", f.path, stdin_data: @diff)
        return Failure.new(reason: err.strip[0, 200]) unless status.success?
        result = File.read(f.path, encoding: "UTF-8")
        return Failure.new(reason: "no change") if result.strip == @original.strip
        Success.new(source: result)
      end
    rescue StandardError => e
      Failure.new(reason: e.message[0, 200])
    end
  end
  end
end

lib/loop/propose_tree.rb

# frozen_string_literal: true

require "yaml"
require "fileutils"

module Master
  module Loop
  # Asks the agent for N radically simplified tree layouts, ranks them by
  # sketch compactness (radical = fewer files), writes top K to runtime/proposals.md.
  # Triggered manually via /propose-tree or by the bus on fix_loop:clean|plateau.
  class ProposeTree
    OUT_PATH      = "runtime/proposals.md"
    DRAFT_N       = 10
    KEEP_TOP      = 3
    COOLDOWN_SECS = 86_400

    def initialize(root:, agent:, event_bus: nil)
      @root  = root
      @agent = agent
      @bus   = event_bus
    end

    def call(n: DRAFT_N)
      return cooldown_message if cooling_down?

      tree      = current_tree
      response  = @agent.ask(prompt(tree, n))
      proposals = parse(response)
      return "propose-tree: parse failed" if proposals.empty?

      top = rank(proposals).first(KEEP_TOP)
      write_report(top, proposals.size)
      @bus&.publish("propose_tree:done", drafted: proposals.size, kept: top.size)
      "propose-tree: drafted #{proposals.size}, kept top #{top.size}#{OUT_PATH}"
    rescue StandardError => e
      @bus&.publish("propose_tree:error", error: e.message)
      "propose-tree: #{e.message}"
    end

    private

    def out_file = File.join(@root, OUT_PATH)

    def cooling_down?
      File.exist?(out_file) && (Time.now - File.mtime(out_file)) < COOLDOWN_SECS
    end

    def cooldown_message
      mins = ((COOLDOWN_SECS - (Time.now - File.mtime(out_file))) / 60).to_i
      "propose-tree: cooldown — next run in #{mins}m"
    end

    def current_tree
      entries = Dir.glob(File.join(@root, "lib", "*")).sort.flat_map { |e| describe_entry(e) }
      entries.first(120).join("\n")
    end

    def describe_entry(entry)
      return [File.basename(entry)] unless File.directory?(entry)
      subs = Dir.glob(File.join(entry, "*")).sort.map { |f| "  #{File.basename(f)}#{File.directory?(f) ? "/" : ""}" }
      ["#{File.basename(entry)}/", *subs]
    end

    def prompt(tree, n)
      <<~PROMPT
        Propose #{n} radically simplified file/folder layouts for this Ruby project (Master).
        Current lib/ structure:

        #{tree}

        For each proposal return a YAML map with keys: name, summary, wins, costs, sketch.
        - name: kebab-case identifier
        - summary: one line
        - wins: 2-3 lines (pros, separated by " · ")
        - costs: 2-3 lines (cons, separated by " · ")
        - sketch: ASCII tree, ≤18 lines, indented with 2 spaces per level

        Be radical: collapse modules, merge concepts, rename for clarity.
        No incrementalism. No prose around the YAML.
        Return a YAML array of #{n} entries.
      PROMPT
    end

    def parse(text)
      body = text.sub(/\A.*?(?=^- |\A- )/m, "").sub(/\n```.*\z/m, "").sub(/\A```ya?ml\n/, "")
      data = YAML.safe_load(body, aliases: false)
      data.is_a?(Array) ? data.select { |e| e.is_a?(Hash) && e["name"] } : []
    rescue Psych::Exception
      []
    end

    def rank(proposals)
      proposals.sort_by { |p| p["sketch"].to_s.lines.size }
    end

    def write_report(top, total)
      FileUtils.mkdir_p(File.dirname(out_file))
      body = ["# tree proposals — #{Time.now.utc.iso8601}", "",
              "drafted: #{total} · kept top: #{top.size}", ""]
      top.each_with_index do |p, i|
        body << "## #{i + 1}. #{p["name"]}" << ""
        body << p["summary"].to_s << ""
        body << "wins: #{p["wins"]}" << "costs: #{p["costs"]}" << ""
        body << "```" << p["sketch"].to_s.rstrip << "```" << ""
      end
      File.write(out_file, body.join("\n"))
    end
  end
  end
end

lib/loop/repair/git_history_miner.rb

# frozen_string_literal: true

require "open3"

module Master
  module Loop
  module Repair
    class GitHistoryMiner
      DEFAULT_PATTERNS = [
        /fix/i,
        /refactor/i,
        /rollback/i,
        /repair/i,
        /runtime/i,
        /telemetry/i,
        /namespace/i
      ].freeze

      def initialize(root: Dir.pwd)
        @root = root
      end

      def recent(limit: 100)
        out, = Open3.capture2e(
          "git",
          "log",
          "--oneline",
          "-n",
          limit.to_s,
          chdir: @root
        )

        out.lines.map(&:strip)
      rescue StandardError => e
        Master::Ground::Swallow.log(e, context: "git_history_miner.recent_commits")
        []
      end

      def valuable(limit: 100, patterns: DEFAULT_PATTERNS)
        recent(limit: limit).select do |line|
          patterns.any? { |pattern| pattern.match?(line) }
        end
      end
    end
  end
  end
end

lib/loop/rule_loop.rb

# frozen_string_literal: true

require "tempfile"
require_relative "../ground/atomic_write"
require_relative "constants"
require_relative "fix_helpers"
require_relative "patch_applier"

module Master
  module Loop
  # Single-pass fixer for one rule across a set of files.
  # FixLoop owns the outer convergence loop; RuleLoop fixes one batch per call.
  #
  # Fix routing (per violation severity + file size):
  #   error tier  → council_fix   (3-reviewer veto before apply)
  #   large file  → diff_fix      (unified diff patch; arch #5)
  #   small file  → genetic_fix   (N candidates, rescan, best wins; arch #9)
  class RuleLoop
    RATE_LIMIT_SLEEP = 10
    MAX_FIX_RETRIES  = 2
    CANDIDATE_COUNT  = 3 # arch #9

    SEVERITY_RANK = Master::SEVERITY_RANK
    MIN_SEVERITY  = SEVERITY_RANK[:warning]

    include Master::Ground::AtomicWrite
    include Master::Loop::FixHelpers

    def initialize(rule:, agent:, scanner:, root:, bus: nil, learnings: nil)
      @rule      = rule
      @agent     = agent
      @scanner   = scanner
      @root      = root
      @bus       = bus
      @learnings = learnings
    end

    def injected_preamble=(text)
      @injected_preamble = text
    end

    # One pass: scan → fix each violating file once → return { fixed:, status: }.
    def run_once(files)
      violations = scan_files(files)
      return { fixed: 0, status: :clean } if violations.empty?

      fixed = fix_batch(violations)
      status = fixed > 0 ? :fixed : :stuck
      record_outcomes(files, fixed > 0 ? :fixed : :stuck)
      @bus&.publish("rule_loop:pass", rule: @rule.id, violations: violations.size, fixed:, status:)
      { fixed:, status: }
    rescue StandardError => e
      @bus&.publish("rule_loop:error", rule: @rule.id, error: e.message)
      { fixed: 0, status: :error }
    end

    private

    def scan_files(files)
      files.flat_map do |path|
        next [] unless File.exist?(path)
        result = @scanner.scan(path, rules: [@rule])
        next [] unless result.ok?
        ext = File.extname(path).downcase
        result.value!
              .select { |f| (SEVERITY_RANK[f[:severity]] || 0) >= MIN_SEVERITY }
              .map    { |f| f.to_h.merge(file: path, ext:) }
      end
    end

    def fix_batch(violations)
      fixed = 0
      violations.uniq { |v| v[:file] }.each do |v|
        new_src = v[:severity].to_sym == :error ? council_fix(v) : request_fix(v)
        apply(v[:file], new_src) && (fixed += 1) if new_src
      end
      fixed
    end

    # Architecture #6: three-reviewer veto for error-tier violations.
    def council_fix(violation)
      path = violation[:file]
      return unless File.exist?(path)
      src    = File.read(path, encoding: "UTF-8")
      prompt = <<~PROMPT
        #{preamble}

        File: #{File.basename(path)}
        Rule violated (severity: ERROR): #{violation[:rule]}
        Line #{violation[:line]}: #{violation[:message]}
        #{violation[:fix].to_s.empty? ? "" : "Suggested fix: #{violation[:fix]}"}

        Three reviewers assess before any fix is applied:
        As Skeptic: Is this a real violation or a false positive? What is the blast radius?
        As Security: Does this create an attack surface? What must the fix preserve?
        As Maintainer: What is the minimum change that eliminates the violation without drift?

        Produce the corrected file only if all three agree the fix is safe.
        If any reviewer would block, return exactly: UNCHANGED

        ```
        #{src}
        ```
      PROMPT
      MAX_FIX_RETRIES.times do |attempt|
        sleep RATE_LIMIT_SLEEP * attempt if attempt.positive?
        response = @agent.ask(prompt).to_s
        return nil if response.strip == "UNCHANGED"
        code = extract_code(response, File.extname(path).downcase)
        return code if code && code.strip != src.strip
      rescue StandardError => e
        next if Master::Loop::Constants::TRANSIENT_RE.match?(e.message.to_s)
        @bus&.publish("rule_loop:council_error", rule: @rule.id, file: path, error: e.message[0, 120])
        return nil
      end
      nil
    end

    # Architecture #5 + #9: diff for large files, genetic candidates for small.
    def request_fix(violation)
      path = violation[:file]
      return unless File.exist?(path)
      src = File.read(path, encoding: "UTF-8")
      src.bytesize > PatchApplier::DIFF_THRESHOLD ? diff_fix(violation, src, path) : genetic_fix(violation, src, path)
    end

    # Architecture #5: unified diff patch — safe on large files.
    def diff_fix(violation, src, path)
      prompt = build_diff_prompt(violation, src, path)
      MAX_FIX_RETRIES.times do |attempt|
        sleep RATE_LIMIT_SLEEP * attempt if attempt.positive?
        response = @agent.ask(prompt).to_s
        next if response.strip == "UNCHANGED"
        result = PatchApplier.apply(src, response)
        return result.source if result.is_a?(PatchApplier::Success)
      rescue StandardError => e
        next if Master::Loop::Constants::TRANSIENT_RE.match?(e.message.to_s)
        @bus&.publish("rule_loop:fix_error", rule: @rule.id, file: path, error: e.message[0, 120])
        return nil
      end
      nil
    end

    # Architecture #9: generate CANDIDATE_COUNT fixes, rescan each, apply lowest-violation winner.
    def genetic_fix(violation, src, path)
      ext    = File.extname(path).downcase
      prompt = build_prompt(violation, src, path)
      candidates = CANDIDATE_COUNT.times.filter_map do |attempt|
        sleep RATE_LIMIT_SLEEP if attempt.positive?
        code = extract_code(@agent.ask(prompt).to_s, ext)
        code if code && code.strip != src.strip
      rescue StandardError => e
        next if Master::Loop::Constants::TRANSIENT_RE.match?(e.message.to_s)
        @bus&.publish("rule_loop:fix_error", rule: @rule.id, file: path, error: e.message[0, 120])
        nil
      end
      best_candidate(candidates, path)
    end

    def best_candidate(candidates, path)
      return nil if candidates.empty?
      return candidates.first if candidates.size == 1
      scored = candidates.filter_map { |c| [rescan_candidate(c, path), c] }
      scored.empty? ? candidates.first : scored.min_by(&:first).last
    rescue StandardError
      candidates.first
    end

    def rescan_candidate(candidate, path)
      Tempfile.open(["rl_score", File.extname(path)]) do |f|
        f.write(candidate); f.flush
        result = @scanner.scan(f.path, rules: [@rule])
        result.ok? ? result.value!.size : 99
      end
    rescue StandardError
      99
    end

    def apply(path, new_src)
      write_atomic(path, new_src, encoding: "UTF-8")
      @bus&.publish("rule_loop:fix_applied", rule: @rule.id, file: path)
      true
    rescue StandardError => e
      @bus&.publish("rule_loop:write_error", rule: @rule.id, file: path, error: e.message)
      false
    end

    def build_prompt(violation, src, path)
      lang     = Master::Judge::Scan::Rule::EXT_LANG.fetch(File.extname(path).downcase, "text")
      fix_hint = violation[:fix].to_s.strip
      <<~PROMPT
        #{preamble}

        File: #{File.basename(path)} (#{lang})
        Rule violated: #{violation[:rule]}
        Line #{violation[:line]}: #{violation[:message]}
        #{fix_hint.empty? ? "" : "How to fix: #{fix_hint}"}

        Return ONLY the corrected file. If unsafe to autofix, return exactly: UNCHANGED

        ```#{lang}
        #{src}
        ```
      PROMPT
    end

    def build_diff_prompt(violation, src, path)
      lang     = Master::Judge::Scan::Rule::EXT_LANG.fetch(File.extname(path).downcase, "text")
      fix_hint = violation[:fix].to_s.strip
      <<~PROMPT
        #{preamble}

        File: #{File.basename(path)} (#{lang})
        Rule violated: #{violation[:rule]}
        Line #{violation[:line]}: #{violation[:message]}
        #{fix_hint.empty? ? "" : "How to fix: #{fix_hint}"}

        Return a unified diff patch only (like `diff -u`). Fix only the violation.
        If unsafe to autofix, return exactly: UNCHANGED

        ```#{lang}
        #{src}
        ```
      PROMPT
    end

    def preamble
      @preamble ||= @injected_preamble || begin
        soul   = Master.load_yaml(File.join(Master::ROOT, "data", "soul.yml"))
        abs    = soul.fetch("absolute", {})
        golden = abs["golden_rule"] || "PRESERVE_THEN_IMPROVE_NEVER_BREAK"
        lines  = ["Golden rule: #{golden}",
                  "Minimum change that eliminates the violation. Do not touch unrelated code."]
        abs.fetch("code_rules", {}).each { |k, v| lines << "- #{k}: #{v}" }
        abs.fetch("aesthetic_rules", {}).each { |k, v| lines << "- #{k}: #{v}" }
        lines.join("\n")
      rescue StandardError
        "Golden rule: PRESERVE_THEN_IMPROVE_NEVER_BREAK"
      end
    end

    # Architecture #10: record fix quality in learnings store.
    def record_outcomes(files, outcome)
      return unless @learnings
      ext = files.filter_map { |f| File.extname(f).downcase.delete(".").presence }.tally.max_by { |_, n| n }&.first || "unknown"
      @learnings.record(rule: @rule.id, file_type: ext, outcome:)
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "rule_loop.record_outcomes", event_bus: @bus, rule: @rule.id)
    end
  end
  end
end

lib/loop/watch_loop.rb

# frozen_string_literal: true

module Master
  module Loop
  # Architecture #7: file-watcher reactive trigger — no polling.
  # Linux: inotify via rb-inotify. OpenBSD: kqueue via rb-kqueue.
  # A changed file triggers a targeted RuleLoop pass on that file only.
  # The system quiesces naturally — no STARTUP_DELAY, no idle sleep waste.
  #
  # Usage (VPS, after `gem install rb-kqueue` or `rb-inotify`):
  #   WatchLoop.new(rules:, agent:, scanner:, root:, bus:).run
  class WatchLoop
    DEBOUNCE_SECONDS = 1.0
    SKIP_DIRS        = FixLoop::SKIP_DIRS

    def initialize(rules:, agent:, scanner:, root:, bus: nil, learnings: nil)
      @rules     = rules
      @agent     = agent
      @scanner   = scanner
      @root      = root
      @bus       = bus
      @learnings = learnings
      @queue     = Queue.new
      @watcher   = build_watcher
    end

    def run
      @bus&.publish("watch_loop:start", root: @root)
      Thread.new { drain_queue }
      @watcher.run
    rescue LoadError => e
      @bus&.publish("watch_loop:unavailable", reason: e.message)
    end

    private

    def build_watcher
      require_kqueue_or_inotify
    rescue LoadError
      raise
    end

    # Drains the queue with debounce — coalesces rapid file events.
    def drain_queue
      pending = {}
      loop do
        path = @queue.pop
        next if SKIP_DIRS.any? { |d| path.include?(d) }
        pending[path] = Time.now.to_f
        sleep DEBOUNCE_SECONDS
        now = Time.now.to_f
        ready = pending.select { |_, t| now - t >= DEBOUNCE_SECONDS }.keys
        ready.each do |p|
          pending.delete(p)
          run_rules_on(p)
        end
      end
    end

    def run_rules_on(path)
      return unless File.exist?(path)
      @rules.each do |rule|
        rl = RuleLoop.new(rule:, agent: @agent, scanner: @scanner, root: @root, bus: @bus, learnings: @learnings)
        result = rl.run([path])
        @bus&.publish("watch_loop:file_pass", file: path, rule: rule.id, **result)
      end
    rescue StandardError => e
      @bus&.publish("watch_loop:error", file: path, error: e.message)
    end

    # Platform-specific watcher. Enqueues paths into @queue on change events.
    def require_kqueue_or_inotify
      if RUBY_PLATFORM.include?("openbsd") || RUBY_PLATFORM.include?("freebsd")
        require "rb-kqueue"
        queue = KQueue::Queue.new
        queue.watch(@root, :recursive, :write, :rename) { |ev| @queue << ev.path.to_s }
        queue
      else
        require "rb-inotify"
        n = INotify::Notifier.new
        n.watch(@root, :close_write, :moved_to, :recursive) { |ev| @queue << ev.absolute_name }
        n
      end
    end
  end
  end
end

lib/loop/watcher.rb

# frozen_string_literal: true

require "open3"
require "time"

module Master
  module Loop
    # Watcher — continuous OpenBSD load monitor.
    # Polls load avg, memory, disk, master service. Publishes system:sample
    # every interval, system:warn/crit on threshold crossings.
    class Watcher
      DEFAULT_INTERVAL = 30
      @@last_sample = nil

      def self.last_sample = @@last_sample

      # One-shot sample without a running watcher. Used by /status.
      def self.sample_once(root: Master::ROOT)
        new(bus: nil, root:).sample!
      end

      def initialize(bus:, root:, interval: nil)
        @bus        = bus
        @root       = root
        cfg         = load_config
        @interval   = interval || cfg["interval_seconds"] || DEFAULT_INTERVAL
        @thresholds = cfg["thresholds"] || {}
        @prev_level = :ok
      end

      def run_forever
        loop do
          sample!
          sleep @interval
        end
      rescue StandardError => e
        @bus&.publish("watcher:error", error: e.message)
      end

      def sample!
        s     = build_sample
        @@last_sample = s
        level = classify(s)
        case level
        when :crit then @bus&.publish("system:crit", s.merge(level: "crit"))
        when :warn then @bus&.publish("system:warn", s.merge(level: "warn")) if @prev_level != :warn
        else            @bus&.publish("system:sample", s)
        end
        @prev_level = level
        s
      end

      private

      def build_sample
        {
          ts: Time.now.utc.iso8601,
          load_1m: load_avg_1m,
          mem_free_pct: mem_free_pct,
          disk_root_pct: disk_root_pct,
          master_rss_mb: master_rss_mb,
          master_alive: master_alive?
        }
      end

      def load_avg_1m
        out, _, st = Open3.capture3("/sbin/sysctl", "-n", "vm.loadavg")
        return nil unless st.success?
        out.tr("{}", "").strip.split.first&.to_f
      rescue StandardError
        nil
      end

      # OpenBSD does not expose vm.uvmexp.free via sysctl — parse vmstat instead.
      def mem_free_pct
        out, _, st = Open3.capture3("/usr/bin/vmstat")
        return nil unless st.success?
        cols = out.lines.last.to_s.strip.split
        return nil if cols.length < 4
        free_bytes = parse_size(cols[3])
        total, _, st2 = Open3.capture3("/sbin/sysctl", "-n", "hw.physmem")
        return nil unless st2.success? && total.to_f.positive?
        ((free_bytes / total.to_f) * 100).round(1)
      rescue StandardError
        nil
      end

      def parse_size(str)
        case str
        when /\A(\d+(?:\.\d+)?)G\z/i then Regexp.last_match(1).to_f * 1_073_741_824
        when /\A(\d+(?:\.\d+)?)M\z/i then Regexp.last_match(1).to_f * 1_048_576
        when /\A(\d+(?:\.\d+)?)K\z/i then Regexp.last_match(1).to_f * 1024
        else str.to_f
        end
      end

      def disk_root_pct
        out, _, st = Open3.capture3("/bin/df", "-k", "/")
        return nil unless st.success?
        out.lines[1].to_s.split[4].to_s.tr("%", "").to_i
      rescue StandardError
        nil
      end

      # The master daemon runs as `falcon serve` on port 53187.
      def master_rss_mb
        out, _, st = Open3.capture3("/bin/ps", "-Ao", "rss,command")
        return nil unless st.success?
        rss_kb = out.lines
                    .select { |l| l.include?("falcon serve") || l.include?(":53187") }
                    .sum { |l| l.strip.split.first.to_i }
        return nil if rss_kb.zero?
        (rss_kb / 1024.0).round
      rescue StandardError
        nil
      end

      # nil = unknown (e.g. rcctl errored); false = explicitly down.
      def master_alive?
        _, _, st = Open3.capture3("/usr/sbin/rcctl", "check", "master")
        st.success?
      rescue StandardError
        nil
      end

      def classify(s)
        return :crit if s[:master_alive] == false ||
                        over?(s[:load_1m], "load_avg_1m", "crit") ||
                        under?(s[:mem_free_pct], "mem_free_pct", "crit") ||
                        over?(s[:disk_root_pct], "disk_root_pct", "crit") ||
                        over?(s[:master_rss_mb], "master_rss_mb", "crit")
        return :warn if over?(s[:load_1m], "load_avg_1m", "warn") ||
                        under?(s[:mem_free_pct], "mem_free_pct", "warn") ||
                        over?(s[:disk_root_pct], "disk_root_pct", "warn") ||
                        over?(s[:master_rss_mb], "master_rss_mb", "warn")
        :ok
      end

      def over?(v, key, level)
        t = @thresholds.dig(key, level)
        v && t && v.to_f >= t.to_f
      end

      def under?(v, key, level)
        t = @thresholds.dig(key, level)
        v && t && v.to_f <= t.to_f
      end

      def load_config
        Master.load_yaml(File.join(@root, "data", "load.yml")) || {}
      rescue StandardError
        {}
      end
    end
  end
end

lib/master.rb

# frozen_string_literal: true

require "zeitwerk"
require "yaml"

# Pre-load openssl before pledge stage1 engages — faraday-net_http requires it
# lazily on first HTTPS call, which fails after unveil restricts dlopen paths.
begin
  require "openssl"
rescue LoadError => e
  warn "openssl: #{e.message} — LLM calls will fail"
end

module Master
  ROOT        = File.expand_path("..", __dir__).freeze
  DATA        = File.join(ROOT, "data").freeze
  COUNCIL_PATH = File.join(DATA, "council.yml").freeze
  RULES_PATH   = File.join(DATA, "rules.yml").freeze

  BUNDLE_BIN = RUBY_PLATFORM.include?("openbsd") ? "bundle34" : "bundle"
  MIN_API_KEY_LENGTH = 20
  NEMOTRON_PRIMARY = "nvidia/nemotron-3-super-120b-a12b:free"
  SEVERITY_RANK = { info: 0, warning: 1, error: 2, critical: 3 }.freeze
  CTX_WINDOW_SIZE = 200_000
  VIOLATION_TRUNCATE = 90

  FILE_LANGUAGE_MAP = {
    ".rb" => "ruby", ".yml" => "yaml", ".yaml" => "yaml",
    ".js" => "javascript", ".json" => "json", ".sh" => "bash",
    ".zsh" => "bash", ".md" => "markdown", ".html" => "html",
    ".erb" => "erb", ".css" => "css"
  }.freeze

  API_KEY_PROVIDERS = {
    anthropic_api_key:  "ANTHROPIC_API_KEY",
    openai_api_key:     "OPENAI_API_KEY",
    gemini_api_key:     "GEMINI_API_KEY",
    openrouter_api_key: "OPENROUTER_API_KEY",
    mistral_api_key:    "MISTRAL_API_KEY",
    deepseek_api_key:   "DEEPSEEK_API_KEY"
  }.freeze

  loader = Zeitwerk::Loader.new
  loader.push_dir(__dir__, namespace: Master)
  loader.ignore(__FILE__)
  loader.inflector.inflect(
    "cli" => "CLI",
    "llm" => "LLM",
    "llm_dispatcher" => "LLMDispatcher",
    "mcp_server" => "MCPServer",
    "mcp_coordinator" => "McpCoordinator",
    "diff_stager" => "DiffStager",
    "code_index" => "CodeIndex",
    "git_context" => "GitContext",
    "ast_edit" => "AstEdit",
    "rule_dsl" => "RuleDSL",
    "tts" => "TTS",
    "pwa_audit" => "PwaAudit",
    "mobile_pwa_operator" => "MobilePwaOperator"
  )
  loader.enable_reloading if defined?(MASTER_DEV_MODE) || ENV["MASTER_DEV"].to_s == "1"
  loader.ignore(File.join(__dir__, "reach", "ruby_llm_patch.rb"))
  loader.ignore(File.join(__dir__, "reach", "bedrock_stub.rb"))
  %w[
    now/cli/signals.rb
    now/cli/command_ops.rb
    now/cli/thinking_indicator.rb
    now/command_registry/memory_commands.rb
    now/command_registry/work_commands.rb
    now/command_registry/system_commands.rb
    now/command_registry/tool_commands.rb
    judge/scan/rules/lexical_rules.rb
    judge/scan/rules/ruby_rules.rb
    judge/scan/rules/web_rules.rb
    judge/scan/rules/js_rules.rb
    judge/scan/rules/universal_rules.rb
  ].each do |rel|
    loader.ignore(File.join(__dir__, rel))
  end
  loader.setup

  def self.configure_providers!
    # Stub Bedrock before ruby_llm loads — avoids openssl.so on OpenBSD/LibreSSL.
    # MASTER only uses OpenRouter; Bedrock is never needed.
    require_relative "reach/bedrock_stub"
    require "ruby_llm"
    require_relative "reach/ruby_llm_patch"
    RubyLLM.configure do |cfg|
      API_KEY_PROVIDERS.each do |attr, env_var|
        api_key = ENV[env_var].to_s
        cfg.public_send("#{attr}=", api_key) if api_key.length >= MIN_API_KEY_LENGTH
      end
    end
  end

  def self.api_key_present?(env_var)
    ENV[env_var].to_s.length >= MIN_API_KEY_LENGTH
  end

  def self.default_model
    return NEMOTRON_PRIMARY       if api_key_present?("OPENROUTER_API_KEY")
    return "claude-opus-4-7"      if api_key_present?("ANTHROPIC_API_KEY")
    return "deepseek-chat"        if api_key_present?("DEEPSEEK_API_KEY")
    return "gpt-4o"               if api_key_present?("OPENAI_API_KEY")
    return "gemini-2.5-flash"     if api_key_present?("GEMINI_API_KEY")
    return "mistral-large-latest" if api_key_present?("MISTRAL_API_KEY")
    NEMOTRON_PRIMARY
  end

  def self.any_api_key_present?
    API_KEY_PROVIDERS.any? { |_, env_var| api_key_present?(env_var) }
  end

  def self.no_api_key_message
    "I'm not wired to any LLM yet. The primary model is nemotron via OpenRouter " \
    "(free). Set OPENROUTER_API_KEY in /etc/rc.d/master daemon_flags and restart " \
    "with `doas rcctl restart master`. Other accepted keys: ANTHROPIC_API_KEY, " \
    "DEEPSEEK_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY, MISTRAL_API_KEY."
  end

  def self.load_yaml(path, symbolize_names: false, default: {})
    YAML.safe_load_file(path, aliases: true, symbolize_names: symbolize_names)
  rescue Psych::Exception, Errno::ENOENT, Errno::EACCES => e
    warn("load_yaml: " + e.message)
    default
  end

  # Strict re-parse of every data/*.yml — silent swallow in load_yaml hides
  # corruption from sweep/LLM rewrites. Boot must surface failures, not mask them.
  def self.validate_data!(root: ROOT, bus: nil)
    errors = {}
    Dir.glob(File.join(root, "data", "**/*.yml")).sort.each do |path|
      YAML.safe_load_file(path, aliases: true)
    rescue Psych::Exception => e
      errors[path.sub("#{root}/", "")] = e.message.lines.first.to_s.strip
    end
    return errors if errors.empty?

    errors.each do |rel, msg|
      warn("yaml_validation: #{rel}: #{msg}")
      bus&.publish("data:yaml_parse_error", path: rel, error: msg)
    end
    errors
  end

  # Loads rules.yml meta + merges split data/rules/*.yml into ["rules"] key.
  def self.load_rules(root: ROOT)
    data_dir = File.join(root, "data")
    base     = load_yaml(File.join(data_dir, "rules.yml"))
    rules_dir = File.join(data_dir, "rules")
    merged = Dir.glob(File.join(rules_dir, "*.yml")).sort.each_with_object({}) do |f, h|
      (load_yaml(f) || {}).each { |scope, list| (h[scope] ||= []).concat(Array(list)) }
    end
    base.merge("rules" => merged)
  end

  def self.build(root: Dir.pwd)
    ENV["MASTER_SCAN_ONLY"] == "1" ? Builder.build_scan_only(root:) : Builder.build(root:)
  end

  def self.bootstrap_container(root: Dir.pwd)
    Trace::Telemetry.bootstrap!(root: root)
    container = Builder.build(root:)
    validate_data!(root: root, bus: container[:bus])
    Builder.boot_snapshot(container)
    container[:heartbeat]&.start!
    Thread.new do
      Ground::Orders::ConstitutionDrift.new(container:).call
    rescue StandardError => e
      warn("constitution_drift: #{e.message}")
    end
    container
  end

  def self.boot(root: Dir.pwd)
    Ground::Pledge.stage1_boot!(root)
    container = bootstrap_container(root: root)
    Ground::Pledge.stage2_lock!
    Now::CLI.new(container:)
  end
end

lib/now/cli.rb

# frozen_string_literal: true

require_relative "cli/signals"
require_relative "cli/command_ops"
require_relative "cli/thinking_indicator"

require "open3"
require "tty-reader"
require "tty-prompt"
require "fileutils"

module Master
  module Now
  class CLI
    IDLE_SLEEP_DEFAULT = 60
    REPLAY_TURNS       = 5
    DMESG_BUFFER       = 80

    SEVERITY_ICON = {
      error: "!!",
      warning: "!",
      style: ".",
      critical: "!!"
    }.freeze

    SLASH_COMMANDS = %w[/exit /undo /redo /checkpoint].freeze

    attr_reader :container

    def initialize(container:)
      @container = container
      assign_container_refs!(container)
      @reader          = TTY::Reader.new(track_history: true)
      @running         = false
      @interrupt_at    = Time.now
      @last_ok         = true
      @violations      = 0
      @prev_violations = 0
      @bg_thread       = nil
      @seen_violations = {}
      @user_active     = false
      @focus_mode      = false
      @show_chips      = false
      @last_input      = nil
      @last_cost       = 0.0
      @dmesg_sub       = nil
      set_visitor_mode_if_unauthenticated
    end

    def run(initial_message = nil)
      setup_signals
      @session.load! if @session.exists?
      start_background_loop
      first_boot_bar
      puts @renderer.splash(@agent.model)
      puts @renderer.session_line(@session.name) if @session.name
      print_repo_tree unless booted_before?
      replay_recent_turns if @session.messages.any?
      run_input(initial_message) if initial_message
      @running = true
      repl_loop
    end

    def pipe(input)
      stripped = input.strip
      return if stripped.empty?
      run_input(stripped)
    end

    def process(input)
      run_input(input)
    end

    def run_input(input)
      return if input.strip.empty?

      @user_active = true
      @last_input  = input
      state    = { streamed: false, thinking_shown: true }
      accumulated = +""
      on_chunk = stream_chunk_handler(accumulated, state)

      print_thinking_indicator
      @pipeline_thread = Thread.new do
        Thread.current.report_on_exception = false
        @pipeline.call(Result.ok(user_message: input, on_chunk: on_chunk))
      end
      result = begin
        @pipeline_thread.value
      rescue StandardError => _e
        Result.err("aborted", category: :abort)
      end
      display_result(result, accumulated, state[:streamed])
    ensure
      @pipeline_thread = nil
      stop_thinking_indicator
      @user_active = false
    end

    def stream_chunk_handler(accumulated, state)
      chunk_accumulator(accumulated) do |text|
        if state[:thinking_shown] && $stdout.isatty
          stop_thinking_indicator
          print "\r\e[K"
          state[:thinking_shown] = false
        end
        unless state[:streamed]
          puts @renderer.speaker_tag
        end
        print text
        $stdout.flush
        state[:streamed] = true
      end
    end

    private

    def set_visitor_mode_if_unauthenticated
      web_token = @config&.dig("web_token")
      Fiber[:master_visitor] = true if web_token.nil? || web_token.empty?
    end

    def assign_container_refs!(deps)
      @session     = deps[:session]
      @agent       = deps[:agent]
      @renderer    = deps[:renderer]
      @logging     = deps[:logging]
      @undo        = deps[:undo]
      @config      = deps[:config]
      @pipeline    = deps[:pipeline]
      @scanner     = deps[:scanner]
      @root        = deps.fetch(:root, Dir.pwd)
      @diff_stager  = deps[:diff_stager]
      @bus          = deps[:bus]
    end

    def repl_loop
      while @running
        unless @focus_mode
          status = @renderer.status_row(
            uptime: @renderer.uptime, turns: @session.messages.size, violations: @violations
          )
          puts status if status
          sugg = suggested_next_prompt
          puts @renderer.render("  ↳ #{sugg}", mode: :dim) if sugg
          tokens = @session.token_est
          prompt_lines = @renderer.prompt_line(
            @agent.model, @session.phase,
            last_ok: @last_ok, violations: @violations, tokens: tokens, cost: @session.cost
          )
          puts prompt_lines.first
          print prompt_lines.last
        else
          print @renderer.render("master$ ", mode: :dim)
        end
        line = safe_read_line
        break if line.nil?
        handle_repl_line(line)
      end
      @bg_thread&.kill
      @session.save!
    end

    def suggested_next_prompt
      top = proposer.top
      return nil unless top
      @last_suggestion = top[:action]
      "#{top[:action]}  (#{top[:reason]})"
    end

    def accept_top_suggestion
      return unless @last_suggestion
      puts @renderer.render("↳ #{@last_suggestion}", mode: :dim)
      handle_repl_line(@last_suggestion)
    end

    def proposer
      @proposer ||= Propose.new(container: @container)
      @proposer.violations = @violations
      @proposer
    end

    NL_DISPATCH = [
      [/\b(?:show|print|list)\s+(?:undo\s+)?histor/i,              :run_history],
      [/\b(?:why|how)\s+(?:this|that|did|was)\b/i,                 :run_why],
      [/\bfocus\s+(?:mode|on|off)\b|\btoggle\s+focus\b/i,          :toggle_focus],
      [/\b(?:last|prev(?:ious)?)\s+(?:input|message|prompt)\b/i,   :run_last],
      [/\b(?:suggest|what(?:'s|\s+is)\s+next|next\s+steps?)\b/i,   :run_propose],
      [/\b(?:show|list)\s+(?:my\s+)?principles\b/i,                :run_principles],
      [/\brestart\b|\bhot[\s-]?reload\b/i,                         :run_restart],
      [/\bui[\s-]?critique\b/i,                                    :run_ui_critique],
      [/\bsound[\s-]?critique\b/i,                                 :run_sound_critique],
      [/\brebuild\b/i,                                             :run_rebuild],
      [/\bshow\s+context\b|\bcontext\s+window\b/i,                 :run_context],
      [/\bverifie?d?\b/i,                                          :run_verify],
      [/\brails[\s-]?pwa[\s-]?audit\b/i,                           :run_rails_pwa_audit],
      [/\brails[\s-]?pwa[\s-]?fix\b/i,                             :run_rails_pwa_fix],
      [/\bswallow[\s-]?report\b|\berror\s+ledger\b/i,              :run_swallow_report],
      [/\btoggle\s+chips?\b|\bchips?\s+(?:on|off)\b/i,             :toggle_chips],
      [/\btoggle\s+dmesg\b|\bdmesg\s+(?:on|off)\b/i,               :toggle_dmesg],
    ].freeze

    def handle_repl_line(line)
      stripped = line.strip
      return accept_top_suggestion if stripped.empty?
      NL_DISPATCH.each { |pat, meth| return send(meth) if stripped.match?(pat) }
      case stripped
      when "/exit", "/quit" then exit_cli
      when "/undo"          then run_undo
      when "/redo"          then run_redo
      when "/checkpoint"    then run_checkpoint
      when "/history"       then run_history
      when "/why"           then run_why
      when "/focus"         then toggle_focus
      when "/last"          then run_last
      when "/cmd"           then run_cmd
      when "/dmesg"         then toggle_dmesg
      when "/chips"         then toggle_chips
      when "/propose"       then run_propose
      when "/principles"    then run_principles
      when "/restart"       then run_restart
      when "/ui-critique"   then run_ui_critique
      when "/sound-critique" then run_sound_critique
      when "/rebuild"       then run_rebuild
      when "/context"       then run_context
      when "/verify"        then run_verify
      when "/rails-pwa-audit"  then run_rails_pwa_audit
      when "/rails-pwa-fix"    then run_rails_pwa_fix
      when "/swallow-report"   then run_swallow_report
      when "<<"             then run_input(read_multiline)
      else                       run_input(line)
      end
    end

    def safe_read_line
      @reader.read_line("", echo: true).chomp
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "cli.safe_read_line", event_bus: @bus)
    end

    def exit_cli
      @session.save!
      line = @renderer.closing
      puts line if line
      @running = false
    end

    def read_multiline
      lines = []
      puts @renderer.render("enter lines, blank line to send", mode: :dim)
      loop do
        print "  "
        inner = safe_read_line
        break if inner.nil? || inner.strip.empty?
        lines << inner
      end
      lines.join("\n")
    end

    def replay_recent_turns
      tail = @session.messages.last(REPLAY_TURNS * 2)
      return if tail.empty?
      puts @renderer.render("resume0: replaying last #{tail.size} messages", mode: :dim)
      tail.each do |msg|
        tag         = msg[:role] == :user ? "you" : "master"
        content     = msg[:content].to_s
        first_line  = content.lines.first.to_s
        snippet     = first_line.strip[0, 100]
        puts @renderer.render("  #{tag}: #{snippet}", mode: :dim)
      end
      puts
    end

    def start_background_loop
      @bg_thread = Thread.new do
        boot_scan
        loop do
          sleep IDLE_SLEEP_DEFAULT
          background_cycle unless @user_active
        end
      rescue StandardError => e
        @bus&.publish("cli:bg_error", error: e.message)
      end
    end

    def boot_scan
      lib_dir = File.join(@root, "lib")
      changed = changed_lib_files(lib_dir)
      result  = changed.any? ? scan_files(changed) : @scanner.scan_dir(lib_dir, depth: :deep)
      return unless result.is_a?(Master::Result) && result.ok?

      prev = @prev_violations
      @violations      = count_violations(result.value!)
      @prev_violations = @violations
      return if @violations.zero? && prev.zero?

      delta = @violations - prev
      arrow = delta.zero? ? "·" : (delta.positive? ? "↑" : "↓")
      puts
      puts @renderer.render("boot scan: #{prev} #{arrow} #{@violations} violation(s)", mode: :dim)
      puts
    rescue StandardError => e
      @bus&.publish("cli:warn", error: e.message)
    end

    def changed_lib_files(lib_dir)
      out, = Open3.capture2e("git", "-C", @root, "diff", "--name-only", "HEAD")
      return [] if out.strip.empty?
      out.lines
         .map { |l| File.join(@root, l.strip) }
         .select { |p| p.start_with?(lib_dir) && p.end_with?(".rb") && File.exist?(p) }
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "cli.changed_lib_files", event_bus: @bus)
      []
    end

    def scan_files(paths)
      Result.ok(paths.map { |p| [p, @scanner.scan(p, depth: :deep)] })
    end

    def count_violations(pairs)
      pairs.sum do |_file, file_result|
        file_result.is_a?(Master::Result) && file_result.ok? ? file_result.value!.size : 0
      end
    end

    def background_cycle
      lib_dir = File.join(@root, "lib")
      result  = @scanner.scan_dir(lib_dir, depth: :deep)
      return unless result.is_a?(Master::Result) && result.ok?
      n = count_violations(result.value!)
      return if n == @violations
      @violations = n
      $stdout.puts "\nbg: #{n} violation(s)" if n.positive?
      $stdout.flush
    rescue StandardError => e
      @bus&.publish("cli:bg_error", error: e.message)
    end

    INIT_FRAMES   = 20
    INIT_FRAME_MS = 0.04

    def print_repo_tree
      lines = Master::CommandRegistry.tree_lines(@root)
      return if lines.empty?
      puts @renderer.render("tree0: #{File.basename(@root)} (#{lines.size} entries)", mode: :dim)
      lines.each { |l| puts @renderer.render(l, mode: :dim) }
      puts
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "cli.print_repo_tree", event_bus: @bus)
    end

    def booted_before?
      flag = File.join(@root, ".master", "booted_once")
      File.exist?(flag)
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "cli.booted_before?", event_bus: @bus)
      false
    end

    def first_boot_bar
      return unless $stdout.isatty
      flag = File.join(@root, ".master", "booted_once")
      return if File.exist?(flag)
      INIT_FRAMES.times do |i|
        bar = ("\u25B0" * (i + 1)) + ("\u25B1" * (INIT_FRAMES - i - 1))
        pct = ((i + 1) * 100 / INIT_FRAMES).to_s.rjust(3)
        print "\rinit0: #{bar} #{pct}%"
        $stdout.flush
        sleep INIT_FRAME_MS
      end
      puts
      FileUtils.mkdir_p(File.dirname(flag))
      File.write(flag, Time.now.to_s)
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "cli.mark_booted", event_bus: @bus)
    end

    def display_result(result, accumulated, streamed)
      case result
      in Master::Result::Ok => ok
        @last_ok = true
        display_ok(ok, accumulated, streamed)
      in Master::Result::Err => err
        @last_ok = false
        if err.category == :shutdown
          exit_cli
        else
          puts
          error_text = format_error_message(err)
          puts @renderer.render(error_text, mode: :error)
          puts
        end
      end
    end

    def format_error_message(err)
      msg = err.message.to_s
      return msg if msg.bytesize <= 200

      msg[0, 197] + "…"
    end

    def display_ok(ok, _accumulated, streamed)
      if streamed
        puts
        puts
      else
        print "\r\e[K" if $stdout.isatty
        value    = ok.value
        rendered = value.is_a?(Hash) ? value[:rendered] : nil
        text     = rendered || value.to_s
        puts @renderer.speaker_tag
        puts text
        puts
      end
      print_cost_tooltip
      print_chips if @show_chips
    end

    def print_cost_tooltip
      now_cost = @session.cost.to_f
      delta    = now_cost - @last_cost
      @last_cost = now_cost
      tokens = @session.token_est
      cents  = (delta * 100).round(2)
      return if cents.zero? && tokens.zero?
      line = "cost: +¢#{format('%.2f', cents)} · #{tokens} tok · #{short_model(@agent.model)}"
      puts @renderer.render(line, mode: :dim)
    end

    def print_chips
      chips = next_action_chips
      return if chips.empty?
      puts @renderer.render("  next: #{chips.join("  ")}", mode: :dim)
    end

    def next_action_chips
      base = ["[/undo]", "[/why]", "[/last]"]
      base.unshift("[/fix #{@violations}v]") if @violations.positive?
      base
    end

    def short_model(model)
      model.to_s.sub(/\Aclaude-cli:/, "").sub(/\Aweb-chat:/, "").split("/").last.to_s.sub(/:free$/, "")
    end
  end
  end
end

lib/now/cli/command_ops.rb

# frozen_string_literal: true

module Master
  module Now
  class CLI
    private

    def run_restart
      @session.save!
      puts @renderer.render("restart: exec'ing fresh master in place", mode: :dim)
      $stdout.flush
      Kernel.exec(RbConfig.ruby, $PROGRAM_NAME, *ARGV)
    end

    def run_undo
      res = @undo.undo!
      if res.is_a?(Master::Result) && res.ok?
        puts @renderer.render("undo: #{Array(res.value!).join(", ")}", mode: :success)
      else
        puts @renderer.render(res.message, mode: :warning)
      end
    end

    def run_redo
      res = @undo.redo!
      if res.is_a?(Master::Result) && res.ok?
        puts @renderer.render("redo: #{Array(res.value!).join(", ")}", mode: :success)
      else
        puts @renderer.render(res.message, mode: :warning)
      end
    end

    def run_history
      lines = @undo.history(limit: 10)
      if lines.empty?
        puts @renderer.render("no undo history", mode: :dim)
      else
        lines.each { |l| puts @renderer.render(l, mode: :dim) }
      end
    end

    def run_why
      router = Master::Routing::ModelRouter.new(config: @config, root: Master::ROOT)
      task = @session.phase == :implement ? :implement : :exploration
      tier = router.current_tier(task_type: task)
      rows = router.score_breakdown(task_type: task).first(5)
      puts @renderer.render("router: phase=#{@session.phase} task=#{task} tier=#{tier}", mode: :dim)
      rows.each_with_index do |r, i|
        line = format("  %d. %-40s q=%.2f s=%.2f c=%.2f → %.3f",
                      i + 1, r[:id].to_s[0, 40], r[:q], r[:s], r[:c], r[:total])
        puts @renderer.render(line, mode: :dim)
      end
    end

    def toggle_focus
      @focus_mode = !@focus_mode
      puts @renderer.render("focus: #{@focus_mode ? "on" : "off"}", mode: :dim)
    end

    def run_last
      if @last_input
        puts @renderer.render("rerun: #{@last_input[0, 60]}", mode: :dim)
        run_input(@last_input)
      else
        puts @renderer.render("no prior input", mode: :dim)
      end
    end

    def run_cmd
      puts @renderer.render("explicit: #{SLASH_COMMANDS.join("  ")}", mode: :dim)
      puts @renderer.render("or describe what you want — intent is inferred", mode: :dim)
    end

    def toggle_dmesg
      if @dmesg_sub
        @dmesg_sub.call
        @dmesg_sub = nil
        puts @renderer.render("dmesg: off", mode: :dim)
      else
        @dmesg_sub = @bus&.subscribe("*") do |payload|
          ts   = payload.fetch(:ts, 0)
          line = "  [#{ts.to_s.rjust(7)}] #{payload[:event]}"
          $stdout.puts @renderer.render(line, mode: :dim) rescue nil
        end
        puts @renderer.render("dmesg: on (events stream below)", mode: :dim)
      end
    end

    def toggle_chips
      @show_chips = !@show_chips
      puts @renderer.render("chips: #{@show_chips ? "on" : "off"}", mode: :dim)
    end

    def run_principles
      c = Master::Ground::Constitution.new
      lines = c.list
      if lines.empty?
        puts @renderer.render("no principles loaded (data/principles/*.md)", mode: :dim)
      else
        puts @renderer.render("constitution: #{lines.size} principle(s)", mode: :dim)
        lines.each { |l| puts @renderer.render("  #{l}", mode: :dim) }
      end
    end

    def run_propose
      rows = proposer.call
      if rows.empty?
        puts @renderer.render("propose: nothing pressing — try /history or scan a dir", mode: :dim)
        return
      end
      puts @renderer.render("propose0: top #{rows.size} suggestion(s)", mode: :dim)
      rows.each_with_index do |r, i|
        line = format("  %d. %-22s %s", i + 1, r[:action], r[:reason])
        puts @renderer.render(line, mode: :dim)
      end
    end

    def run_ui_critique    = run_critique(:ui, label: "ui-critique", intro: "assembling panel — brutal honesty mode")
    def run_sound_critique = run_critique(:sound, label: "sound-critique", intro: "assembling audio panel")

    def run_critique(mode, label:, intro:)
      puts @renderer.render("#{label}: #{intro}", mode: :dim)
      critic = Master::Judge::Council::Critique.new(mode: mode, agent: @agent, event_bus: @bus)
      result = critic.run
      unless result.ok?
        puts @renderer.render("#{label}: #{result.message}", mode: :warning)
        return
      end
      data  = result.value!
      picks = data[:cherry_picks]
      puts @renderer.render("#{label}: #{picks.size} cherry-pick(s)", mode: :dim)
      picks.each { |p| puts @renderer.render("  cherry: #{p}", mode: :dim) }
      data[:feedback].each do |f|
        puts @renderer.render("  [#{f[:persona]}] #{f[:feedback].to_s.lines.first.to_s.strip}", mode: :dim)
      end
    end

    def run_rebuild
      puts @renderer.render("rebuild: syntax check + session save + hot-restart", mode: :dim)
      lib_dir = File.join(Master::ROOT, "lib")
      errors  = []
      changed_lib_files(lib_dir).each do |path|
        ok = system("ruby34 -c #{path} > /dev/null 2>&1")
        errors << path unless ok
      end
      if errors.any?
        errors.each { |p| puts @renderer.render("  syntax error: #{p}", mode: :warning) }
        puts @renderer.render("rebuild: aborted — fix errors first", mode: :warning)
        return
      end
      @session.save!
      puts @renderer.render("rebuild: ok — exec'ing fresh process", mode: :dim)
      $stdout.flush
      Kernel.exec(RbConfig.ruby, $PROGRAM_NAME, *ARGV)
    end

    def run_context
      query = @last_input.to_s
      puts @renderer.render("context: gathering for query=#{query[0, 60]}", mode: :dim)
      provider = Master::Ground::ContextProvider.new
      rows     = provider.brief(query, limit: 8)
      if rows.empty?
        puts @renderer.render("context: nothing found", mode: :dim)
      else
        rows.each { |r| puts @renderer.render("  #{r}", mode: :dim) }
      end
      @bus&.publish("attention:context", query: query, rows: rows.size)
    end

    def run_checkpoint
      puts @renderer.render("checkpoint: snapshotting changed files", mode: :dim)
      lib_dir = File.join(Master::ROOT, "lib")
      files   = changed_lib_files(lib_dir)
      cp      = Master::Ground::Checkpoint.new
      result  = cp.create(label: "manual", files: files)
      id      = result.respond_to?(:fetch) ? result[:id] : result.to_s
      puts @renderer.render("checkpoint: #{id} (#{files.size} file(s))", mode: :dim)
    end

    def run_verify
      puts @renderer.render("verify: checking recently landed operator symbols", mode: :dim)
      plan = {
        files:   %w[lib/ground/intent_router.rb lib/ground/attention_context.rb
                    lib/ground/unfinished_ledger.rb lib/ground/orchestration_policy.rb],
        symbols: %w[Master::Ground::IntentRouter Master::Ground::AttentionContext
                    Master::Ground::UnfinishedLedger Master::Ground::OrchestrationPolicy],
        callers: %w[run_sound_critique run_rebuild run_context run_checkpoint run_verify]
      }
      checker = Master::Ground::DoneChecker.new
      result  = checker.call(plan)
      result.each do |key, val|
        icon = val.is_a?(TrueClass) || val == :ok ? "ok" : "!!"
        puts @renderer.render("  #{icon} #{key}", mode: val == false ? :warning : :dim)
      end
    end

    def run_swallow_report
      puts @renderer.render("swallow-report: reading SwallowLedger", mode: :dim)
      ledger_path = File.join(@root, "runtime", "swallow_ledger.jsonl")
      unless File.exist?(ledger_path)
        puts @renderer.render("swallow-report: no ledger at #{ledger_path}", mode: :dim)
        return
      end
      lines = File.readlines(ledger_path, chomp: true).last(5)
      last  = lines.last && JSON.parse(lines.last) rescue nil
      unless last
        puts @renderer.render("swallow-report: ledger empty or unreadable", mode: :dim)
        return
      end
      puts @renderer.render("swallow-report: total=#{last["total"]} contexts=#{last["counts"]&.size}", mode: :dim)
      last["counts"].to_a.sort_by { |_, v| -v }.first(10).each do |ctx, n|
        puts @renderer.render("  #{n.to_s.rjust(4)}x #{ctx}", mode: :warning)
      end
    end

    def run_rails_pwa_audit
      puts @renderer.render("rails-pwa-audit: scanning DEPLOY apps", mode: :dim)
      op = Master::Rails::MobilePwaOperator.new(agent: @agent, event_bus: @bus)
      result = op.audit_all_deploy
      if result.ok?
        result.value!.each do |r|
          next puts @renderer.render("  !! #{r[:app]}: #{r[:error]}", mode: :warning) if r[:error]
          icon = { green: "ok", amber: "--", red: "!!" }.fetch(r[:verdict], "??")
          puts @renderer.render("  #{icon} #{r[:app]}: #{r.dig(:pwa, :findings)&.size || 0} finding(s)", mode: :dim)
          Array(r.dig(:pwa, :recommendations)).first(3).each do |rec|
            puts @renderer.render("     #{rec}", mode: :dim)
          end
        end
      else
        puts @renderer.render("rails-pwa-audit: #{result.message}", mode: :warning)
      end
    end

    def run_rails_pwa_fix
      puts @renderer.render("rails-pwa-fix: applying network-first SW + offline fallback to DEPLOY apps", mode: :dim)
      op = Master::Rails::MobilePwaOperator.new(agent: @agent, event_bus: @bus)
      result = op.audit_all_deploy
      return puts @renderer.render("rails-pwa-fix: #{result.message}", mode: :warning) unless result.ok?
      fixed = 0
      result.value!.each do |r|
        next puts @renderer.render("  !! #{r[:app]}: #{r[:error]}", mode: :warning) if r[:error]
        next if r[:verdict] == :green
        fix_result = op.respond_to?(:fix_app) ? op.fix_app(r[:app]) : Result.err("fix_app not implemented")
        if fix_result.ok?
          fixed += 1
          puts @renderer.render("  ok #{r[:app]}: fixed", mode: :dim)
        else
          puts @renderer.render("  !! #{r[:app]}: #{fix_result.message}", mode: :warning)
        end
      end
      puts @renderer.render("rails-pwa-fix: #{fixed} app(s) patched", mode: :dim)
    end
  end
  end
end

lib/now/cli/signals.rb

# frozen_string_literal: true

module Master
  module Now
  class CLI
    private

    def setup_signals
      trap("USR1") { on_usr1 }
      trap("INT")  { on_int }
    end

    def on_usr1
      Zeitwerk::Loader.for_gem.reload
      puts "\n#{@renderer.render("reloaded", mode: :success)}"
    rescue StandardError => e
      puts "\n#{@renderer.render("reload failed: #{e.message}", mode: :error)}"
    end

    def on_int
      if @pipeline_thread&.alive?
        @pipeline_thread.kill
        @pipeline_thread = nil
        puts "\n#{@renderer.render("aborted", mode: :warning)}"
        return
      end
      if Time.now - @interrupt_at < 1
        @scan_thread&.kill
        @session.save!
        exit(0)
      else
        @interrupt_at = Time.now
        puts "\n#{@renderer.render("^C again to quit", mode: :warning)}"
      end
    end
  end
  end
end

lib/now/cli/thinking_indicator.rb

# frozen_string_literal: true

module Master
  module Now
  class CLI
    SPIN_FRAMES   = ["\u00B7", "\u2219", "\u2022", "\u25CF"].freeze
    SPIN_INTERVAL = 0.25
    DMESG_IGNORE  = %w[bus:subscribe bus:unsubscribe ring:write].freeze
    VERDICT_GLYPH = {
      ok: "\u2713", fail: "\u00D7", warn: "!", info: "\u00B7"
    }.freeze
    MUTATING_TOOLS = %w[write_file str_replace ast_edit].freeze

    private

    def print_thinking_indicator
      return unless $stdout.isatty

      @think_mutex = Mutex.new
      @think_t0    = Process.clock_gettime(Process::CLOCK_MONOTONIC)
      @think_stage = "intake"
      @think_sub   = @bus&.subscribe("*") do |payload|
        update_think_stage(payload)
        emit_dmesg_line(payload)
      end

      @spin_thread = Thread.new do
        i = 0
        loop do
          @think_mutex.synchronize do
            print "\r\e[K#{@renderer.render("#{SPIN_FRAMES[i % SPIN_FRAMES.size]} #{@think_stage}", mode: :dim)}"
            $stdout.flush
          end
          sleep SPIN_INTERVAL
          i += 1
        end
      rescue StandardError => e
        Master::Ground::Swallow.log(e, context: "cli.spinner", event_bus: @bus)
      end
    end

    def stop_thinking_indicator
      @spin_thread&.kill
      @spin_thread = nil
      @think_sub&.call
      @think_sub = nil
    end

    def update_think_stage(payload)
      ev = payload[:event].to_s
      return unless ev.start_with?("stage:")
      stage = payload.fetch(:stage, ev.sub("stage:", ""))
      @think_stage = stage.to_s
    end

    def glyph_for_event(ev)
      case ev
      when /:(done|ok|success|rendered|synthesis)\b/ then VERDICT_GLYPH[:ok]
      when /:(error|fail|timeout|veto)\b/            then VERDICT_GLYPH[:fail]
      when /:(warn|warning|escalat)\b/               then VERDICT_GLYPH[:warn]
      else                                                VERDICT_GLYPH[:info]
      end
    end

    def emit_dmesg_line(payload)
      ev = payload[:event].to_s
      return if ev.empty? || DMESG_IGNORE.include?(ev)
      ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - (@think_t0 || 0)) * 1000).to_i
      kv = payload.reject { |k, _| %i[event ts topic].include?(k) }
                  .map { |k, v| "#{k}=#{v.to_s[0, 60]}" }.join(" ")
      diff = ev == "tool:after" && MUTATING_TOOLS.include?(payload[:tool].to_s) ? diff_stat(payload[:path]) : nil
      tail = diff ? " #{diff}" : ""
      line = "  %s [%7d] %s%s%s" % [glyph_for_event(ev), ms, ev, kv.empty? ? "" : " #{kv}", tail]
      @think_mutex&.synchronize do
        print "\r\e[K"
        $stdout.puts @renderer.render(line, mode: :dim)
        $stdout.flush
      end
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "cli.print_event", event_bus: @bus)
    end

    def diff_stat(path)
      return nil unless path && !path.empty?
      out, _ = Open3.capture2e("git", "-C", @root, "diff", "--numstat", "--", path)
      m = out.lines.first&.match(/^(\d+)\s+(\d+)/)
      m ? "+#{m[1]}/-#{m[2]}" : nil
    rescue StandardError => e
      Master::Ground::Swallow.log(e, context: "cli.diff_stat", event_bus: @bus)
    end

    def chunk_accumulator(buffer)
      lambda do |chunk|
        text = chunk.respond_to?(:content) ? chunk.content.to_s : chunk.to_s
        next if text.empty?

        yield text
        buffer << text
      end
    end
  end
  end
end

lib/now/command_registry.rb

# frozen_string_literal: true

require_relative "command_registry/memory_commands"
require_relative "command_registry/work_commands"
require_relative "command_registry/system_commands"
require_relative "command_registry/tool_commands"

module Master
  module Now
  # CommandRegistry — all pipeline-routable slash commands.
  module CommandRegistry
    module_function

    def build(infra:, ai:, root:)
      session_commands(infra).merge(
        mode_commands(infra[:config]),
        memory_commands(infra[:memory], ai[:agent]),
        work_commands(ai:, root:, infra:),
        tool_commands(root),
        control_commands(ai[:standing], ai[:soul]),
        system_commands(ai[:agent], infra[:diag], root),
        "help" => ->(_ctx) {
          [
            "scan:    /scan /fix [loop|preview|stop] /why /axioms /topic /propose-tree",
            "review:  /critique /review",
            "health:  /status /resync [--dry-run] /tail [N] [pattern]",
            "session: /save /clear /history /tokens /cost /undo /redo /checkpoint /dmesg /exit",
            "model:   /model /mode /persona /task",
            "memory:  /memory /dreams",
            "tools:   /postpro [args] /repligen [args]",
            "system:  /orient [topic] /tree /diff /commit /snapshot /diag /reload /help"
          ].join("\n")
        }
      )
    end

    # ── Session ──────────────────────────────────────────────────────────────

    def session_commands(infra)
      session = infra[:session]
      undo    = infra[:undo]
      logging = infra[:logging]
      config  = infra[:config]
      {
        "clear" => ->(_ctx) { session.clear!; "context cleared" },
        "save" => ->(_ctx) { session.save!; "session saved" },
        "history" => ->(ctx) {
          n = ctx[:args].to_s.strip.to_i
          n = 10 if n <= 0
          recent = session.messages.last(n)
          next "history: empty" if recent.empty?
          recent.map { |m| "[#{m[:role]}] #{m[:content].to_s.gsub(/\s+/, " ")[0, 120]}" }.join("\n")
        },
        "tokens" => ->(_ctx) { "~#{session.token_est} tokens" },
        "cost" => ->(_ctx) { "$#{"%.4f" % session.cost}" },
        "undo" => ->(_ctx) { r = undo.undo!;  r.ok? ? "reverted: #{r.value!}"   : r.message },
        "redo" => ->(_ctx) { r = undo.redo!;  r.ok? ? "reapplied: #{r.value!}"  : r.message },
        "dmesg" => ->(_ctx) { logging.dmesg },
        "config" => ->(_ctx) { config.to_h.inspect }
      }
    end

    # ── Mode / persona / model flag commands ─────────────────────────────────

    def mode_commands(config)
      reasoning_commands(config).merge(persona_commands(config)).merge(flag_commands(config))
    end

    def reasoning_commands(config)
      {
        "mode" => ->(ctx) {
          arg = ctx[:args].to_s.strip
          Master::Judge::Modes::SUPPORTED.include?(arg) ?
            (config["reasoning_mode"] = arg; config.save!; "mode: #{arg}") :
            "mode: #{config.reasoning_mode} (supported: #{Master::Judge::Modes::SUPPORTED.join(", ")})"
        },
        "task" => ->(ctx) {
          arg = ctx[:args].to_s.strip
          arg.empty? ? "task_type: #{config.task_type}" : (config["task_type"] = arg; config.save!; "task_type: #{arg}")
        }
      }
    end

    def persona_commands(config)
      {
        "persona" => ->(ctx) {
          arg = ctx[:args].to_s.strip
          return "persona: #{config.persona}" if arg.empty?
          config["persona"] = arg; config.save!; "persona: #{arg}"
        }
      }
    end

    def flag_commands(config)
      flags = %w[auto_review auto_lint auto_commit]
      flags.each_with_object({}) do |flag, h|
        h[flag] = ->(ctx) {
          arg = ctx[:args].to_s.strip
          arg.empty? ? "#{flag}: #{config[flag]}" : (config[flag] = arg == "on"; config.save!; "#{flag}: #{config[flag]}")
        }
      end
    end

    # ── Control (standing orders, soul) ──────────────────────────────────────

    def control_commands(standing, soul)
      {
        "orders" => cmd(:dispatch_orders, standing),
        "soul" => cmd(:dispatch_soul, soul)
      }
    end

    def dispatch_orders(standing, arg)
      case arg
      when "list", ""                    then standing.list
      when /\Aenable (.+)\z/             then standing.enable($1.strip)
      when /\Adisable (.+)\z/            then standing.disable($1.strip)
      when /\Aadd name=(\S+) cmd=(.+)\z/ then standing.upsert(name: $1, command: $2.strip)
      when "run"                         then run_due_orders(standing)
      when /\Areset (.+)\z/              then standing.reset($1.strip)
      else "usage: /orders  /orders enable|disable|reset <name>  /orders run"
      end
    end

    def run_due_orders(standing)
      results = standing.run_due!
      return "no orders due" if results.empty?
      results.map { |r| "#{r[:name]}: #{r[:result].ok? ? "ok" : r[:result].message}" }.join("\n")
    end

    def dispatch_soul(soul, arg)
      case arg
      when "", "show"             then soul.summary
      when "version", "changelog" then soul.changelog
      when "diff"                 then soul.diff
      when "approve"              then soul.approve
      when "reject"               then soul.reject
      when "rollback"             then soul.rollback
      when /\Apropose (.+)\z/     then soul.propose($1.strip)
      else "soul  soul version  soul diff  soul approve  soul reject  soul rollback  soul propose <rationale>"
      end
    end

    # ── Helpers ──────────────────────────────────────────────────────────────

    def arg_for(ctx) = ctx[:args].to_s.strip
    def expand_or_root(arg, root) = arg.empty? ? root : File.expand_path(arg, root)
    def cmd(method, *services) = ->(ctx) { send(method, *services, arg_for(ctx)) }
  end
  end
end

lib/now/command_registry/memory_commands.rb

# frozen_string_literal: true

module Master
  module Now
  module CommandRegistry
    module_function

    def memory_commands(memory, agent)
      {
        "memory" => ->(ctx) { dispatch_memory(memory, ctx[:args].to_s.strip) },
        "dreams" => ->(ctx) {
          arg = ctx[:args].to_s.strip
          if arg == "consolidate"
            memory.respond_to?(:consolidate!) ? memory.consolidate!(agent:) : "dreaming not available"
          else
            entries  = memory.all
            archived = entries.count { |k, _| k.to_s.start_with?("archive/") }
            active   = entries.count { |k, _| !k.to_s.start_with?("archive/") }
            summary  = memory.recall("_consolidated_summary")
            lines    = ["active: #{active} memories, archived: #{archived}"]
            lines << "last consolidation: #{summary}" if summary
            lines.join("\n")
          end
        }
      }
    end

    def dispatch_memory(memory, arg)
      case arg
      when /\Aforget (.+)/  then memory.forget($1.strip); "forgot: #{$1.strip}"
      when /\Aremember (.+)/
        body, type = parse_remember($1)
        key, value = body.split("=", 2).map(&:strip)
        if value
          memory.remember(key, value, type:)
          "remembered [#{type}]: #{key}"
        else
          "usage: /memory remember [type=user|feedback|project|reference] key=value"
        end
      when /\Asearch (.+)/ then memory_search(memory, $1.strip)
      when /\Atype (\S+)/  then list_by_type(memory, $1.strip)
      when "types"
        counts = memory.type_counts.map { |t, n| "#{t}: #{n}" }.join("\n")
        counts.empty? ? "(no memories)" : counts
      when ""
        (e = memory.all).empty? ? "(no memories)" : e.map { |k, v| "#{k}: #{v}" }.join("\n")
      else
        (r = memory.recall(arg)) ? "#{arg}: #{r}" : "(not found: #{arg})"
      end
    end

    def parse_remember(text)
      if text =~ /\Atype=(\S+)\s+(.+)/
        [$2, $1]
      else
        [text, "general"]
      end
    end

    def list_by_type(memory, type)
      hits = memory.by_type(type)
      return "(no memories of type: #{type})" if hits.empty?
      hits.map { |k, v| "#{k}: #{v.is_a?(Hash) ? v["value"] : v}" }.join("\n")
    end

    def memory_search(memory, query)
      if memory.respond_to?(:semantic_recall)
        hits = memory.semantic_recall(query)
        return "(no matches: #{query})" if hits.empty?
        hits.map { |h| "#{h[:key]}: #{h[:value]}" }.join("\n")
      else
        hits = memory.all.select { |k, v| k.to_s.include?(query) || v.to_s.include?(query) }
        hits.empty? ? "(no matches: #{query})" : hits.map { |k, v| "#{k}: #{v}" }.join("\n")
      end
    end
  end
  end
end

lib/now/command_registry/system_commands.rb

# frozen_string_literal: true

require "open3"

module Master
  module Now
  module CommandRegistry
    module_function

    TEXT_EXTS  = %w[.rb .py .js .ts .zsh .sh .bash .md .yml .yaml .json .toml .gemspec .txt .erb .conf .ini .env].to_set.freeze
    TEXT_NAMES = %w[Gemfile Rakefile Makefile Dockerfile].to_set.freeze
    SKIP_SEGS  = %w[.git vendor tmp var node_modules .bundle coverage log dist knowledge].to_set.freeze

    def system_commands(agent, diag, root)
      {
        "orient" => cmd(:dispatch_orient, root),
        "tree" => cmd(:dispatch_tree, root),
        "diff" => cmd(:dispatch_diff, root),
        "commit" => ->(_ctx) { dispatch_commit(agent, root) },
        "snapshot" => ->(_ctx) { dispatch_snapshot(root) },
        "diag" => ->(ctx) { diag ? diag.render(ctx[:args].to_s.strip) : "diag: not configured" },
        "reload" => ->(_ctx) { "reload: not supported in this context" }
      }
    end

    ORIENT_FILES = {
      "soul" => ["data/soul.yml", "constitution: axioms, voice, persona, prompt order"],
      "rules" => ["data/rules.yml", "universal cross-disciplinary rules"],
      "style" => ["data/ruby_style.yml", "ruby/shell/git/css/html/typography idioms"],
      "workflow" => ["data/workflow.yml", "agent loops, pipeline, council, gates"],
      "orders" => ["data/standing_orders.yml", "event triggers and standing operating procedures"],
      "patterns" => ["data/patterns.yml", "gh/openbsd/zsh tool idioms"],
      "openbsd" => ["data/openbsd.yml", "pf/nsd/httpd/relayd config validators"]
    }.freeze

    def dispatch_orient(root, arg)
      return cat_orient(root, arg) unless arg.empty?
      [
        "MASTER — constitutional AI runtime for any text artifact",
        "modules: now · loop · judge · voice · ground · reach · trace",
        "pipeline: Intake → Infer → Route → Guard → Execute → [Council ‖ Lint] → Prune → Memo → Render",
        "",
        "constitution:",
        *ORIENT_FILES.map { |k, (path, desc)| "  /orient #{k.ljust(10)} #{path.ljust(28)} #{desc}" }
      ].join("\n")
    end

    def cat_orient(root, arg)
      entry = ORIENT_FILES[arg]
      return "unknown: #{arg} (try: #{ORIENT_FILES.keys.join(", ")})" unless entry
      full = File.join(root, entry[0])
      File.exist?(full) ? File.read(full) : "missing: #{full}"
    end

    def dispatch_tree(root, arg)
      cfg   = (Master.load_yaml(File.join(root, "data", "rules.yml")) || {}).dig("paths", "tree") || {}
      depth = arg.to_i.positive? ? arg.to_i : (cfg["max_depth"] || 2)
      cap   = cfg["max_lines"] || 200
      buf   = []
      walker = lambda do |dir, level|
        return if level > depth || buf.size >= cap
        Dir.children(dir).sort.each do |name|
          break if buf.size >= cap
          next if name.start_with?(".") || SKIP_SEGS.include?(name)
          path = File.join(dir, name)
          buf << "#{"  " * (level - 1)}#{name}#{File.directory?(path) ? "/" : ""}"
          walker.call(path, level + 1) if File.directory?(path)
        end
      rescue Errno::EACCES, Errno::ENOENT
        nil
      end
      walker.call(root, 1)
      buf.join("\n")
    end

    def dispatch_diff(root, arg)
      base = arg.empty? ? "HEAD" : arg
      out, = Open3.capture2e("git", "-C", root, "diff", base, "--stat")
      out.strip.empty? ? "(no changes since #{base})" : out.strip
    end

    def dispatch_commit(agent, root)
      diff, = Open3.capture2e("git", "-C", root, "diff", "--cached", "--stat")
      diff, = Open3.capture2e("git", "-C", root, "diff", "--stat") if diff.strip.empty?
      return "nothing to commit" if diff.strip.empty?
      prompt = "Write a concise git commit message (1 line, imperative mood) for:\n#{diff}"
      msg    = agent.ask_once(prompt).to_s.strip.lines.first.to_s.strip
      Open3.capture2e("git", "-C", root, "add", "-u")
      out, = Open3.capture2e("git", "-C", root, "commit", "-m", msg)
      out.strip
    end

    def dispatch_snapshot(root)
      [
        publish_snapshot(root, "MASTER"),
        publish_snapshot(File.expand_path("../DEPLOY", root), "DEPLOY")
      ].join("\n")
    end

    def publish_snapshot(target, label)
      return "snapshot:#{label.downcase}: not found: #{target}" unless File.directory?(target)
      skip      = ->(rel) { rel.split("/").any? { |s| SKIP_SEGS.include?(s) } }
      text_file = ->(f) { TEXT_EXTS.include?(File.extname(f).downcase) || TEXT_NAMES.include?(File.basename(f)) }
      all   = Dir.glob(File.join(target, "**", "*"))
                 .reject { |f| File.basename(f).start_with?(".") }
                 .reject { |f| skip.(f.delete_prefix("#{target}/")) }.sort
      dirs  = all.select { |f| File.directory?(f) }
      files = all.select { |f| File.file?(f) && text_file.(f) && File.size(f) < Master::CTX_WINDOW_SIZE }
      stamp = Time.now.utc.iso8601
      md    = ["# #{label} Snapshot — #{stamp}", "", "## Tree", "```"]
      entries = (dirs.map { |d| [d, :dir] } + files.map { |f| [f, :file] })
                  .sort_by { |p, _| p.split("/") }
                  .map { |p, k| "#{"  " * p.delete_prefix("#{target}/").count("/")}#{File.basename(p)}#{k == :dir ? "/" : ""}" }
      md.concat(entries) << "```" << ""
      n_lines = 0
      files.each do |f|
        rel  = f.delete_prefix("#{target}/")
        lang = Master::FILE_LANGUAGE_MAP.fetch(File.extname(f).downcase, "text")
        body = File.read(f, encoding: "UTF-8", invalid: :replace).lines
        n_lines += body.size
        md << "## `#{rel}`" << "```#{lang}"
        md.concat(body.map(&:rstrip))
        md << "```" << ""
      rescue StandardError => e
        md << "## `#{rel}`" << "[skipped: #{e.message}]" << ""
      end
      md << "files: #{files.size} / lines: #{n_lines}"
      day = Time.now.strftime("%Y-%m-%d")
      out, status = Open3.capture2e("gh", "gist", "create", "-",
        "--public", "--desc", "#{label} #{day}",
        "--filename", "snapshot_latest.md",
        stdin_data: md.join("\n"))
      status.success? ? "snapshot:#{label.downcase}: #{files.size} files #{n_lines} lines → #{out.strip}" :
                        "snapshot:#{label.downcase}: gist publish failed: #{out.strip}"
    end
  end
  end
end

lib/now/command_registry/tool_commands.rb

# frozen_string_literal: true

require "open3"
require "shellwords"

module Master
  module Now
  module CommandRegistry
    module_function

    def tool_commands(root)
      {
        "postpro" => ->(ctx) { dispatch_master_tool(root:, tool: "postpro", arg: arg_for(ctx)) },
        "repligen" => ->(ctx) { dispatch_master_tool(root:, tool: "repligen", arg: arg_for(ctx)) }
      }
    end

    def dispatch_master_tool(root:, tool:, arg:)
      script = File.join(root, "tools", "#{tool}.rb")
      return "#{tool}: missing tool entrypoint #{script}" unless File.file?(script)

      argv = Shellwords.split(arg.to_s)
      out, status = Open3.capture2e(RbConfig.ruby, script, *argv, chdir: File.expand_path("..", root))
      status.success? ? out.strip : "#{tool}: exit=#{status.exitstatus}\n#{out.strip}"
    rescue ArgumentError => e
      "#{tool}: bad arguments: #{e.message}"
    rescue StandardError => e
      "#{tool}: #{e.class}: #{e.message}"
    end
  end
  end
end

lib/now/command_registry/work_commands.rb

# frozen_string_literal: true

require "json"
require "open3"
require "time"

module Master
  module Now
  module CommandRegistry
    module_function

    VIOLATION_TRUNCATE = Master::VIOLATION_TRUNCATE

    def work_commands(ai:, root:, infra:)
      scanner      = ai[:scanner]
      fix_loop     = ai[:fix_loop]
      deliberation = ai[:deliberation]
      council_stage = ai[:council_stage]
      agent        = ai[:agent]
      propose_tree = ai[:propose_tree]
      session      = infra[:session]
      bus          = infra[:bus]
      config       = infra[:config]
      metrics      = infra[:metrics]
      {
        "scan" => cmd(:dispatch_scan, scanner, root),
        "fix" => ->(ctx) { dispatch_fix(fix_loop, root, arg_for(ctx)) },
        "status" => ->(_c) { dispatch_status(root:, fix_loop:, bus:) },
        "resync" => ->(c) { dispatch_resync(root:, fix_loop:, arg: arg_for(c)) },
        "tail" => ->(c) { dispatch_tail(root:, arg: arg_for(c)) },
        "review" => ->(ctx) { dispatch_review(council_stage:, deliberation:, root:, bus:, arg: arg_for(ctx)) },
        "critique" => cmd(:dispatch_critique, deliberation, root),
        "model" => ->(c) { dispatch_model(agent:, config:, metrics:, root:, arg: arg_for(c)) },
        "why" => cmd(:dispatch_why, agent, root),
        "axioms" => cmd(:dispatch_axioms, scanner, root),
        "topic" => cmd(:dispatch_topic, session),
        "propose-tree" => ->(_ctx) { propose_tree&.call || "propose-tree: not wired" }
      }
    end

    # /status — one-frame health panel. Replaces seven probing tool calls.
    def dispatch_status(root:, fix_loop:, bus:)
      git   = Reach::GitOperations.new(File.expand_path("..", root))
      ahead, behind = git.ahead_behind
      head  = git.head || "?"
      dirty = git.dirty?(".")
      svc   = service_status
      bg    = fix_loop&.background_alive? ? "running" : "stopped"
      af    = ENV["MASTER_AUTOFIX"] == "1" ? "on" : "off"
      bndl  = bundle_status(File.expand_path("..", root))
      evts  = recent_events(root, 5)
      branch = git.branch || "?"
      lines = [
        "status",
        "service master/#{svc[:state]} #{svc[:detail]}",
        "git     #{branch}@#{head} ahead=#{ahead} behind=#{behind} #{dirty ? "dirty" : "clean"}",
        "fix     bg=#{bg} autofix=#{af}",
        "bundle  #{bndl}",
        "events  (last #{evts.size})"
      ]
      evts.each { |e| lines << "  #{e[:ago]} #{e[:event]} #{e[:summary]}" }
      lines.join("\n")
    rescue StandardError => e
      "status: #{e.message}"
    end

    def service_status
      out, _, st = Open3.capture3("/usr/sbin/rcctl", "check", "master")
      { state: st.success? ? "ok" : "down", detail: out.strip }
    rescue Errno::ENOENT
      { state: "n/a", detail: "rcctl absent — not OpenBSD" }
    rescue StandardError => e
      { state: "?", detail: "rcctl err: #{e.class}: #{e.message[0, 60]}" }
    end

    def bundle_status(repo)
      mas, = Open3.capture2e("bundle34", "check", chdir: File.join(repo, "MASTER"))
      web, = Open3.capture2e("bundle34", "check", chdir: File.join(repo, "MASTER/web"))
      mas_ok = mas.include?("dependencies are satisfied")
      web_ok = web.include?("dependencies are satisfied")
      mas_ok && web_ok ? "ok (MASTER+web satisfied)" : "drift — run bundle install"
    rescue StandardError => e
      "unknown (#{e.class})"
    end

    def recent_events(root, n)
      path = File.join(root, "runtime", "events", "activity.jsonl")
      return [] unless File.exist?(path)
      now = Time.now.utc
      File.foreach(path).to_a.last(n).map { |line|
        rec  = JSON.parse(line) rescue next
        ts   = (Time.parse(rec["timestamp"]) rescue now)
        secs = (now - ts).to_i.abs
        ago  = secs < 60 ? "#{secs}s" : (secs < 3600 ? "#{secs / 60}m" : "#{secs / 3600}h")
        pay  = rec["payload"]
        sum  = pay.is_a?(Hash) ? pay.first(3).map { |k, v| "#{k}=#{v.to_s.tr('"', "")[0, 24]}" }.join(" ") : pay.to_s
        { ago: ago.rjust(4), event: rec["event"].to_s, summary: sum[0, 80] }
      }.compact
    rescue StandardError
      []
    end

    # /resync — divergence repair: tag, fetch, reset, bundle, restart.
    def dispatch_resync(root:, fix_loop:, arg:)
      repo = File.expand_path("..", root)
      git  = Reach::GitOperations.new(repo)
      stop_msg = fix_loop&.background_alive? ? (fix_loop.stop_background!; "stopped fix_loop bg; ") : ""
      tag_name = "backup/#{Time.now.strftime("%Y%m%d-%H%M")}-resync"
      old_head = git.head
      lines = ["#{stop_msg}resync starting — tag=#{tag_name} was=#{old_head}"]
      git.tag(tag_name); lines << "  tagged #{tag_name}"
      git.fetch;         lines << "  fetched origin
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment