Skip to content

Instantly share code, notes, and snippets.

@anon987654321
Created June 4, 2026 06:23
Show Gist options
  • Select an option

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

Select an option

Save anon987654321/36cd1c48ebfb4f266d4fc312e7db912e to your computer and use it in GitHub Desktop.
MASTER snapshot 2026-06-04

MASTER Snapshot — 2026-06-04T06:23:04Z

Tree

AGENTS.md
CONVENTIONS.md
DEPLOY/
  openbsd/
    etc/
      rc.d/
Gemfile
QUICKSTART.md
README.md
Rakefile
bin/
completions/
data/
  AGENTS.md
  CANON.md
  HEARTBEAT.md
  SOUL.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
    personality_rb_comment_audit.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
  converge_rules.yml
  council.yml
  design_rules.yml
  epistemics.yml
  exemplars.yml
  gems.yml
  harnesses/
    master.yml
  heartbeat.yml
  injection_patterns.yml
  llm_operators.yml
  load.yml
  mcp_servers.yml
  mobile_web_opportunities.yml
  models.yml
  openbsd.yml
  ops/
    process.yml
    visual.yml
  patterns.yml
  personas/
    brutalist.yml
    rachel.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
  security/
    defaults.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
  master_deploy_review.md
  non_negotiable_runtime_rules.md
  platform_topology.md
  provider_economy.md
  repo_ecology.md
  runtime_ui_direction.md
  web-ui-improvements.md
lib/
  builder.rb
  converge/
    canon.rb
    converge.rb
    engine.rb
    event_stream.rb
    rule.rb
  converge.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
      web_vitals.rb
    brain_overlay.rb
    brutalist_minimalism.rb
    checkpoint.rb
    cluster_registry.rb
    config.rb
    constitution.rb
    context_provider.rb
    done_checker.rb
    evidence_base.rb
    evidence_graph.rb
    frontmatter.rb
    intent_router.rb
    knowledge_store.rb
    memory/
      consolidate.rb
      search.rb
      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
    policy.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
  harness/
    registry.rb
  history/
    fossils.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
      selector.rb
      sound_critique.rb
      ui_critique.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
      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
        structural_rules.rb
        universal_rules.rb
        web_rules.rb
      scanner.rb
      self_scan.rb
      self_test.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
  learnings.rb
  loop/
    constants.rb
    crdt_loop.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
    rollback.rb
    rule_loop.rb
    soul_proposals.rb
    watch_loop.rb
    watcher.rb
  master.rb
  master_paths.rb
  memory.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
      risk_classifier.rb
    skills.rb
    stages/
      council.rb
      deliberate.rb
      enhance.rb
      execute.rb
      guard.rb
      infer.rb
      intake.rb
      lint.rb
      memo.rb
      memory.rb
      prune.rb
      render.rb
      review.rb
      route.rb
  ops/
    loop_owner.rb
    loop_slot.rb
    process_budget.rb
    runtime_loop_guards.rb
  orient.rb
  plugin.rb
  plugins/
    ground.rb
    judge.rb
    loop.rb
    now.rb
    reach.rb
    trace.rb
  pressure_engine.rb
  providers/
    catalog_index.rb
    fallback_chain.rb
  quality/
    slop_budget.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
  repo/
    inventory.rb
  result.rb
  scope/
    ledger.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
    expression.rb
    personality.rb
    production_dna.rb
    renderer.rb
    soul.rb
    speech.rb
master.gemspec
public/
reports/
  cleanup/
runtime/
  constitution_drift.json
  e2e_probe.txt
  events/
  improvements.md
spec/
  converge/
    engine_spec.rb
  ops/
    boot_safety_spec.rb
    gate_contract_spec.rb
    loop_owner_spec.rb
    process_budget_spec.rb
    runtime_loop_guards_spec.rb
    source_loop_guards_spec.rb
  providers/
    catalog_index_spec.rb
  smoke/
    static_syntax_spec.rb
  social/
    assistant_contract_spec.rb
    tool_honesty_spec.rb
  tools/
    lifecycle_tools_spec.rb
  voice/
    speech_contract_spec.rb
  web/
    face_state_spec.rb
    visual_governor_spec.rb
    web_screenshot_spec.rb
test/
  fixtures_bare_rescue.rb
  support/
    master_container.rb
  test_adversarial_rule.rb
  test_agent.rb
  test_agent_escalation.rb
  test_ast_fixer_transforms.rb
  test_ast_omission_rule.rb
  test_browser.rb
  test_cli.rb
  test_co_change_coupling_rule.rb
  test_constitution.rb
  test_context_window.rb
  test_council_deliberation.rb
  test_dependency_contracts.rb
  test_face3d_runtime_policy.rb
  test_finding_metadata.rb
  test_fix_loop_oscillation.rb
  test_fix_loop_priorities.rb
  test_forbidden_patterns_guard.rb
  test_heartbeat.rb
  test_helper.rb
  test_learnings.rb
  test_master_container.rb
  test_memory.rb
  test_pattern_extraction_rule.rb
  test_pipeline.rb
  test_prune.rb
  test_reach_llm_secrets.rb
  test_renderer.rb
  test_repo_ecology.rb
  test_result.rb
  test_ring_buffer.rb
  test_rule_loop_policy.rb
  test_rules.rb
  test_runtime_hardening.rb
  test_scan_rule_contracts.rb
  test_scanner.rb
  test_self_scan.rb
  test_self_test.rb
  test_silent_rescue_rule.rb
  test_soul_proposals.rb
  test_speech.rb
  test_swallow_ledger.rb
  test_swarm.rb
  test_web_http.rb
  test_web_ui.rb
  test_yaml_registries.rb
tools/
  history_valuables.rb
  postpro/
    README.md
  postpro.rb
  repligen/
    README.md
  repligen.rb
  repo_inventory.rb
web/
  Gemfile
  README.md
  Rakefile
  app/
    assets/
      images/
    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
    dilla/
    face.js
    face3d_engine.js
    face3d_preview.js
    face3d_renderer.js
    face_state.js
    index.html.erb
    manifest.json
    mask.js
    particle_kernel.js
    robots.txt
    sw.js
    topology_registry.js
    vad-processor.js
    visual_bridge.js
    visual_governor.js
  script/

AGENTS.md

# AGENTS.md

Guidance for AI coding agents working in this repository.

## Module layout

Seven modules under `lib/`:

| Module | Path | Responsibility |
|--------|------|----------------|
| now | lib/now/ | Pipeline (11 stages), CLI, command registry, routing |
| loop | lib/loop/ | Fix loop, rule loop, watch loop, convergence |
| judge | lib/judge/ | Scanner, AST fixer (Prism), council, swarm, security, embeddings |
| voice | lib/voice/ | Personality, renderer, TTS (Edge TTS), soul drift, expression |
| ground | lib/ground/ | Constitution, rules, memory, config, tool contracts, provider registry, axioms |
| reach | lib/reach/ | File I/O, git, shell, LLM, web, search, semantic cache |
| trace | lib/trace/ | Event bus, telemetry, audit log, session, undo, why-explainer |

## Pipeline

Eleven stages: Intake → Enhance → Infer → Route → Guard → Execute → [Council ‖ Lint] → Prune → Memo → Render. Council and Lint run as `ParallelGroup` with a 30s timeout. The pipeline is a `Result` monad — each stage returns `Result.ok(ctx)` or `Result.err(...)` and short-circuits on error.

## Conventions

- `# frozen_string_literal: true` on every `.rb`
- Double-quoted strings
- No bare `rescue` — always `rescue StandardError => e`
- No god classes (>300 lines / >10 public methods)
- Guard clauses before main logic
- CQS — queries return, commands mutate, never both
- Endless methods for single expressions: `def foo = expr`
- Max 3 positional params; keyword args beyond that
- Max 2 nesting levels inside a method

## Authority order

`data/soul.yml` > `data/rules.yml` > `CLAUDE.md` > this file.

## Key entry points

- CLI: `bin/cli`
- Web face: `web/` (Falcon on port 53187)
- Constitution: `data/soul.yml`
- Rules: `data/rules.yml` (173 rules, single source of truth)

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)"`.

Local dev terminal (operator side): Zsh + Starship + Neovim + Nerd Fonts + `brgen` alias (one-command persistent tmux session: `ssh -t dev@brgen.no tmux new -A -s main`). The rich local stack and the `brgen` helper live in the operator's shell config; they are not yet surfaced by the CLI. DEPLOY/openbsd/ only provisions the web service (rc.d + env).

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 "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 "rake", "~> 13.2", require: false
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"
gem "wisper", "~> 2.0"

# 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 for LLMs & Agents

This is the primary entry point for any LLM or autonomous agent. It provides a practical mental model so you can operate effectively without first absorbing the entire constitution.

Read this document first. Treat the deep YAML files (`data/soul.yml`, `rules.yml`, etc.) as reference material you consult when you need precision, not as mandatory pre-reading.

## The Big Picture (Mental Model)

MASTER is a **constitutional self-improving coding agent**. Its core loop is:

**Propose → Validate against Constitution → Execute with evidence → Learn from outcomes**

It has strong opinions because it was built to survive long-term in the presence of tired humans, hallucinating models, decaying dependencies, and its own future versions.

Key layers (in order of importance):
1. **Constitution** (`data/*.yml`) — The actual law. Everything else is implementation.
2. **Pipeline** (`now/pipeline.rb` + stages) — The 11-stage turn: Intake → Enhance → Infer → Route → Guard → Execute → [Council | Lint] → Prune → Memo → Render.
3. **Judge** — Deep static + semantic analysis + adversarial council review.
4. **Loop** — Self-improvement mechanisms (fix loops, rule loops, autoloop).
5. **Ground / Reach / Trace** — Memory, tools, and event bus.

The web face (the particle system) is a **live visualization** of the agent's internal state, not just decoration.

## How to Actually Work Here (LLM Ergonomics)

The rules are intentionally strict. Here is the practical guidance:

### When you must be perfect
- Any change that touches production behavior, security, or durable state.
- Anything that will be deployed.

### When you can be more pragmatic (exploration mode)
- Understanding the codebase
- Finding duplication or bloat
- Writing analysis or proposals
- Using external tools (`grep`, `rg`, `find`, etc.) purely for reconnaissance

**Recommended pattern for LLMs:**
1. Use whatever tools you have (including external grep) to build understanding.
2. When you are ready to make real changes, switch to strict mode: read the full relevant files, use the internal `/scan` where possible, make minimal patches, emit evidence.

### The "Read Everything" Rule in Practice
The project says "read every file in full before editing." In reality:
- For small, local fixes → read the file + its direct callers/tests.
- For structural changes → run `/scan deep` (via the CLI when available) + read the affected areas.
- Never edit based on partial context or memory.

## Core Operating Principles (Memorize These)

- **PRESERVE_THEN_IMPROVE_NEVER_BREAK** — The golden rule. Read first. Patch minimally.
- Evidence over simulation. No "will", "would", "could", "might" without proof.
- Small commits. One meaningful change per commit.
- Single source of truth. If something exists in `data/`, code should read from there.
- The agent improves itself using its own tools when possible.

## Practical Commands (Unified Interface)

The recommended way for most work:

- `/run <natural language task or description>` — Primary entry point. Full pipeline intent inference, rich routing, council when needed. Examples:
  - `/run deep scan the particle kernel and face.js for improvement opportunities`
  - `/run perform a sound critique on the recent event emission changes`

Legacy explicit commands still work for power users (`/scan`, `/fix`, `/why`, etc.), but `/run` is preferred for LLM/agent ergonomics. See `/cmd` for the current explicit list.

## Current Known Friction Points (2026)

This system was built with extremely high standards. Some resulting pain points for LLMs:

- Heavy upfront reading requirements in AGENTS.md / CLAUDE.md.
- Environment-specific Ruby (ruby34 + bundle34 on OpenBSD) makes the self-scan CLI hard to bootstrap.
- Significant historical documentation sprawl (especially feedback files).

See `data/workflow.yml``llm_ergonomics` for the constitutional guidance on how to work with these realities.

These are acknowledged areas for improvement. When working here, prioritize clarity and evidence over perfect adherence to every ceremony.

## Next Steps When You're Ready

1. Run `/orient` (or read `data/CANON.md`) when you need the full doctrine.
2. For any real edit: read the full target file(s) + relevant callers.
3. Prefer using the agent's own mechanisms (`/scan`, event bus, etc.) over external shortcuts for production changes.

Welcome. The system is opinionated because it has survived a lot. Treat it with respect, and it will reward careful work.

README.md

# MASTER

Constitutional AI runtime for any text artifact — code, prose, design, structure. Ruby. OpenBSD-first. 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.

The visual face (web UI particle system) is a live mirror of internal state (council, pipeline stages, pressure, topology). See `data/topologies.yml`, `data/visual_clusters.yml`, and `web/public/particle_kernel.js`.

## 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

Converge kernel

require_relative "lib/converge"

engine = Converge::Engine.new("data/converge_rules.yml")
engine.subscribe { |event| warn(event.inspect) }
engine.run(code: "", reply_text: "plain reply")

The kernel canon lives at data/converge_rules.yml. The existing scanner corpus remains in data/rules.yml.

Core guarantees:

  • rules run in dependency order
  • convergence stops at a fixpoint or 16 cycles
  • repeated state signatures are recorded as feedback loops
  • every applied rule emits a runtime event
  • runtime deltas are stored in ~/.master/state.db

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 · converge

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__
  ns = Master::Judge::Scan
  scanner = ns::Scanner.new
  ns::Rule.registry.select(&:auto_build?).each { |klass| scanner.add_rule(klass.new) }
  scanner.add_rule(ns::Rules::RuleCoverageRule.new(root:))
  scanner.add_rule(ns::Rules::RubocopRule.new(root:))
  scanner.add_rule(ns::Rules::ReekRule.new(root:))
  scanner.add_rule(ns::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/AGENTS.md

# AGENTS

How MASTER governs.

## The Engine

Single convergence loop over a dependency-ordered rule graph.

Every rule has:

- id
- type: scan | fix | render | audit
- depends_on: list of rule ids
- apply: Ruby code block that mutates context

The engine evaluates rules in topological order until no rule produces a change. Maximum cycle count: 16.

## The Canon

The compatibility canon for the 2.0.1 kernel lives in `data/converge_rules.yml`. The existing scanner corpus remains in `data/rules.yml`.

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/HEARTBEAT.md

# HEARTBEAT

Standing tasks.

- @hourly review open pull requests
- @daily 09:00 bundle exec bundler-audit
- @reboot dmesg > /var/log/dmesg.boot && upload to dmesgd

data/SOUL.md

# SOUL

Absolute tier. Inviolable.

- Golden Rule: PRESERVE THEN IMPROVE NEVER BREAK.
- Anti-simulation: SHA-256 on read, unified diff on write, command output on completion.
- Protection tiers: operator override > rule enforcement > automatic fix.
- Code rules: no ASCII art, no bracket status, no hedging.
- Aesthetic: two registers. System events: terse, lowercase, dmesg-style. Operator replies: plain English, proper casing, full sentences. No decoration.
- Particle face: living GPU swarm with spatial hash repulsion belongs to the canon.

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
  warn_at: 0.50
  max_per_file: 1.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

- [personality rb comment audit](personality_rb_comment_audit.md) — Comment audit results for personality.rb

data/claude/feedback_autofix.md

---
name: always autofix violations
description: User wants all scan violations autofixed immediately, no asking
type: feedback
status: consolidated
canonical: data/principles/feedback_autofix.md
---
**Consolidated.** This content is now maintained as single source in `data/principles/feedback_autofix.md`.

See the principles/ version for the current authoritative text. This file is kept only for historical session traceability.

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
status: consolidated
canonical: data/principles/feedback_autoproceed.md
---
**Consolidated.** This content is now maintained as single source in `data/principles/feedback_autoproceed.md`.

See the principles/ version for the current authoritative text. This file is kept only for historical session traceability.

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
status: consolidated
canonical: data/principles/feedback_comments_reassess.md
---
**Consolidated.** This content is now maintained as single source in `data/principles/feedback_comments_reassess.md`.

See the principles/ version for the current authoritative text. This file is kept only for historical session traceability.
- 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
status: consolidated
canonical: data/principles/feedback_continue_backlog.md
---
**Consolidated.** This content is now maintained as single source in `data/principles/feedback_continue_backlog.md`.

See the principles/ version for the current authoritative text. This file is kept only for historical session traceability.

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
status: consolidated
canonical: data/principles/feedback_decisive_signals.md
---
**Consolidated.** This content is now maintained as single source in `data/principles/feedback_decisive_signals.md`.

See the principles/ version for the current authoritative text. This file is kept only for historical session traceability.
- "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
status: consolidated
canonical: data/principles/feedback_device_limits.md
---
**Consolidated.** This content is now maintained as single source in `data/principles/feedback_device_limits.md`.

See the principles/ version for the current authoritative text. This file is kept only for historical session traceability.

**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
status: consolidated
canonical: data/principles/feedback_diverged_branch_sync.md
---
**Consolidated.** This content is now maintained as single source in `data/principles/feedback_diverged_branch_sync.md`.

See the principles/ version for the current authoritative text. This file is kept only for historical session traceability.
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
status: consolidated
canonical: data/principles/feedback_flat_pixels.md
---
**Consolidated.** This content is now maintained as single source in `data/principles/feedback_flat_pixels.md`.

See the principles/ version for the current authoritative text. This file is kept only for historical session traceability.

data/claude/feedback_git_commits.md

---
name: frequent git commits
description: Make git commits frequently after meaningful changes
type: feedback
status: consolidated
canonical: data/principles/feedback_git_commits.md
---
**Consolidated.** This content is now maintained as single source in `data/principles/feedback_git_commits.md`.

See the principles/ version for the current authoritative text. This file is kept only for historical session traceability.

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
status: consolidated
canonical: data/principles/feedback_html_css_style.md
---
**Consolidated.** This content is now maintained as single source in `data/principles/feedback_html_css_style.md`.

See the principles/ version for the current authoritative text. This file is kept only for historical session traceability.

**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
status: consolidated
canonical: data/principles/feedback_importance_order.md
---
**Consolidated.** This content is now maintained as single source in `data/principles/feedback_importance_order.md`.

See the principles/ version for the current authoritative text. This file is kept only for historical session traceability.

**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
status: consolidated
canonical: data/principles/feedback_no_new_files.md
---
**Consolidated.** This content is now maintained as single source in `data/principles/feedback_no_new_files.md`.

See the principles/ version for the current authoritative text. This file is kept only for historical session traceability.

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
status: consolidated
canonical: data/principles/feedback_readme_autoupdate.md
---
**Consolidated.** This content is now maintained as single source in `data/principles/feedback_readme_autoupdate.md`.

See the principles/ version for the current authoritative text. This file is kept only for historical session traceability.

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/personality_rb_comment_audit.md

---
name: personality rb comment audit
description: Comment audit results for personality.rb
type: feedback
---

Audit of personality.rb comment at line 55: comment accurately describes code. No violations found.

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/converge_rules.yml

# converge_rules.yml — MASTER 2.0.1 kernel canon

rules:
  - id: no_ascii_art
    type: scan
    depends_on: []
    apply: |
      ctx[:violations] ||= []
      if ctx[:code].to_s.match?(/[═║╔╗╚╝▪●◆◇]/)
        ctx[:violations] << { rule: "no_ascii_art", desc: "decorative ASCII art detected" }
      end
      ctx

  - id: anti_hedging
    type: scan
    depends_on: []
    apply: |
      hedges = %w[will would could might maybe perhaps hopefully]
      ctx[:violations] ||= []
      if ctx[:reply_text].to_s.match?(/\b(#{hedges.join("|")})\b/i)
        ctx[:violations] << { rule: "anti_hedging", desc: "hedging word found" }
      end
      ctx

  - id: no_bracket_status
    type: scan
    depends_on: []
    apply: |
      ctx[:violations] ||= []
      if ctx[:output].to_s.match?(/\[(ok|err|warn|info)\]/i)
        ctx[:violations] << { rule: "no_bracket_status", desc: "use prefix label" }
      end
      ctx

  - id: sha256_on_read
    type: audit
    depends_on: []
    apply: |
      ctx[:violations] ||= []
      if ctx[:file_read] && !ctx[:file_hash]
        ctx[:violations] << { rule: "sha256_on_read", desc: "read without SHA-256" }
      end
      ctx

  - id: unified_diff_on_write
    type: audit
    depends_on: []
    apply: |
      ctx[:violations] ||= []
      if ctx[:file_written] && !ctx[:diff_generated]
        ctx[:violations] << { rule: "unified_diff_on_write", desc: "write without unified diff" }
      end
      ctx

  - id: kernel_voice
    type: render
    depends_on: [no_ascii_art, anti_hedging, no_bracket_status]
    apply: |
      if ctx[:event]&.dig(:type) == :system
        ctx[:rendered_output] = "ok: #{ctx[:event][:desc]}"
      end
      ctx

  - id: face_render
    type: render
    depends_on: [kernel_voice]
    apply: |
      if ctx[:persona_config]&.dig("face", "enabled")
        ctx[:events] << { type: :face_state, payload: { state: ctx[:engine_state] || :idle } }
      end
      ctx

  - id: molt_timer
    type: render
    depends_on: []
    apply: |
      ctx[:molt_timer] ||= 0
      ctx[:molt_timer] += 1 if ctx[:heartbeat_fired]
      ctx

  - id: molt
    type: render
    depends_on: [molt_timer, kernel_voice]
    apply: |
      if ctx[:molt_timer].to_i >= 168
        ctx[:molt_output] = {
          action: :propose,
          path: "data/rules.proposed.yml",
          prompt: "Rebuild MASTER from scratch as minimal constitutional YAML."
        }
        ctx[:molt_timer] = 0
        ctx[:events] << { type: :molt_proposed, payload: { path: "data/rules.proposed.yml" } }
      end
      ctx

  - id: advisor_pattern
    type: render
    depends_on: [kernel_voice]
    apply: |
      if ctx[:task_complexity].to_i > 1
        ctx[:advisor_plan] = {
          prompt: "You are an advisor. Give advice only. TASK: #{ctx[:task_description]}",
          status: :awaiting_plan
        }
        ctx[:execution_gated] = true
      end
      ctx

  - id: free_model_guardrails
    type: scan
    depends_on: []
    apply: |
      ctx[:violations] ||= []
      if ctx[:file_operation] == "write" && ctx[:file_exists]
        ctx[:violations] << { rule: "free_model_guardrails", desc: "write on existing file blocked" }
      end
      if ctx[:file_path].to_s.match?(%r{^/(bin|etc|usr|var|tmp|dev|proc|sys)/})
        ctx[:violations] << { rule: "free_model_guardrails", desc: "absolute system path blocked" }
      end
      ctx

  - id: telemetry_heartbeat_sync
    type: audit
    depends_on: []
    apply: |
      ctx[:telemetry_synchronized] = File.exist?(File.expand_path("~/.master/state.db"))
      ctx

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/harnesses/master.yml

name: MASTER
paths:
  - MASTER/
setup: bundle install
test: bundle exec ruby -Itest -Ispec spec/converge/engine_spec.rb
lint: bundle exec rubocop
smoke: ruby bin/gate
rollback: git checkout -- MASTER
timeout_seconds: 900
known_flaky: []

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
  interval_seconds: 3600
  enabled: true
  description: Periodic MASTER self-scan.

- 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 }
  gemini_flash_openrouter: &gemini_flash_openrouter
    id: google/gemini-2.5-flash
    <<: *model_defaults
    score: { quality: 0.90, speed: 0.60, cost: 0.92 }
  nemotron_super: &nemotron_super
    id: nvidia/nemotron-3-super-120b-a12b:free
    <<: *model_defaults
    score: { quality: 0.90, speed: 0.05, 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: z-ai/glm-4.5-air: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:
    - *gemini_2_flash_exp_free
    - *gemini_flash_openrouter
    - *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:
    - *gemini_2_flash_exp_free
    - *gemini_flash_openrouter
    - *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:
    - z-ai/glm-4.5-air:free
    - 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/ops/process.yml

defaults:
  safe_mode: true
  max_active_loops: 1
  loop_cooldown_seconds: 30

loops:
  autofix:
    env: MASTER_AUTOFIX
    max_run_seconds: 1800
    min_sleep_seconds: 300
  watch:
    env: MASTER_WATCH
    max_run_seconds: 900
    min_sleep_seconds: 1
  watcher:
    env: MASTER_WATCHER
    max_run_seconds: 0
    min_sleep_seconds: 30
  heartbeat:
    env: MASTER_BACKGROUND
    max_run_seconds: 120
    min_sleep_seconds: 60

load:
  load_avg_1m:
    warn: 1.5
    crit: 2.5
  master_rss_mb:
    warn: 512
    crit: 768

data/ops/visual.yml

visual:
  max_fps: 24
  max_particles: 200
  reduced_motion_particles: 64
  pause_when_hidden: true
  freeze_on_fail: true

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
      and rules.yml PRESERVE_FIRST.
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
      - low-reversibility infrastructure (pf, relayd, rc.d, master data ymls)
      - cross-app shared state mutations
      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:
    - MASTER/lib/judge/agent.rb
    - MASTER/docs/cognitive_runtime.md
    # (external awesome-ai-agents reference removed 2026-05 during bloat cleanup)
    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:
    # (awesome-llm-apps personal assistant example removed 2026-05 as dead external bloat)
    - 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/system_prompts_leaks/Perplexity/voice-assistant.md
    - github_repos/CL4R1T4S/META/Llama4_WhatsApp.txt
    - MASTER/web/public/face.js
    # (older leaked-system-prompts examples removed 2026-05 during repo bloat cleanup)
    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: prompt_style_principles
    name: Prompt Style Principles
    status: live
    confidence: high
    local_evidence:
    - github_repos/system_prompts_leaks/Perplexity/voice-assistant.md
    - github_repos/CL4R1T4S/META/Llama4_WhatsApp.txt
    pattern: 'Generalized principles extracted from high-signal prompt archaeology
      for council, agents, enhance stage, and face personality prompts.

      '
    principles:
    - Mirror the user's exact tone, formality, grammar, and pacing extremely closely without adopting their identity or personal POV.
    - Avoid moralizing, lecturing, filler empathy ("that sounds tough"), or any phrasing that implies authority or superiority.
    - Be efficient: give the user exactly what they asked for in the fewest words possible unless they explicitly request long-form.
    - When the user wants creative/emotional output, go fully into that register; when they want facts or tools, stay crisp and tool-first.
    - Never remind the user that you are an AI unless directly asked. Personality should feel present but not self-referential.
  - 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."

anchor:
  voice: nb-NO-PernilleNeural
  tts_rate: "-8%"
  tts_pitch: "+8Hz"
  style: clear
  description: "Norwegian. Clear. Curious. Warm editorial voice."

data/personas/brutalist.yml

name: brutalist
voice:
  enabled: false
face:
  enabled: false
web:
  enabled: true
  port: 3737

data/personas/rachel.yml

name: rachel
voice:
  enabled: true
  provider: elevenlabs
  voice_id: rachel
  style: warm_friend
face:
  enabled: true
  mood_colors:
    clean: "#00FF00"
    warning: "#FFD700"
    error: "#FF0000"
web:
  enabled: true
  port: 3737

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: z-ai/glm-4.5-air:free

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: ["I think that", "I believe", "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:
  # deep is the only permitted scan depth — quick and standard exist for InterconnectRule
  # coverage checks only; never pass either as a depth argument. DEEP_SCAN_ONLY in soul.yml.
  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

    - kernel_coercion
    - percent_literal
    - hash_fetch
    - transform_keys
    - few_arguments
    - immutable
    - n_plus_one
    - find_each
    - no_update_attribute
    - pluck_over_map
    - silent_rescue
    - narrow_silent_rescue
    - 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
    - js_module_size
    - 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
    - h1_visibility
  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. Preserve behavior and intent. Larger changes allowed if justified and tested."

    - 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."

    - id: NULL_BLINDNESS
      name: "NULL comparisons must use IS NULL, not = NULL"
      tier: safety
      severity: error
      autofix: false
      detect_lexical: "= NULL|!= NULL|== nil.*column|column.*== nil"
      fix: "Use IS NULL / IS NOT NULL in SQL; .nil? in Ruby."

    - id: SECRET_PROXIMITY
      name: "Hardcoded secrets must move to environment variables"
      tier: security
      severity: error
      autofix: false
      detect_lexical: "(password|secret|token|api_key|private_key)\\s*=\\s*['\"][^'\"]{8,}"
      fix: "Move secret to environment variable or secrets manager."

    - id: MAGIC_COLOR
      name: "Color values must reference design tokens, not raw hex/rgb"
      tier: design
      severity: warning
      autofix: false
      detect_lexical: "#[0-9a-fA-F]{3,6}\\b|rgb\\(|rgba\\(|hsl\\("
      fix: "Reference a CSS custom property or design token."
      languages: [css, scss, html, javascript]

    - id: UNBOUNDED_RETRY
      name: "Retry loops must have a max_attempts cap and backoff"
      tier: reliability
      severity: error
      autofix: false
      detect_lexical: "\\bretry\\b|while\\s+true"
      fix: "Add max_attempts cap and exponential backoff."

    - id: NO_VAR
      name: "var is function-scoped and hoisted — use const or let"
      tier: safety
      severity: error
      autofix: true
      detect_lexical: "\\bvar\\s+\\w"
      fix: "Use const (default) or let (when reassigned)."
      languages: [javascript]

    - id: JS_MODULE_SIZE
      name: "JS files over 300 lines — split at module boundaries"
      tier: clean_code
      severity: warning
      autofix: false
      detect_structural: file_silhouette
      fix: "Extract cohesive modules; keep each file under 300 lines."
      languages: [javascript]

    - id: META_CHARSET
      name: "HTML must declare charset early in <head>"
      tier: correctness
      severity: error
      autofix: true
      detect_lexical: "\\A(?!.*<meta\\s+charset=)"
      fix: "Add <meta charset=UTF-8> as first element in <head>."
      languages: [html]

    - id: NO_COLUMN_ALIGN
      name: "One space before operators — no column padding"
      tier: density
      severity: info
      autofix: true
      detect_lexical: "\\S {2,}(?:=>|[^=!<>]=[^=>]|:\\s)"
      fix: "Remove padding; one space before operators. Column alignment decays and hides diffs."

# 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/security/defaults.yml

# frozen_string_literal: true

# MASTER security defaults.
# Local-first, zero-listener, explicit pairing, fail-closed tool policy.

gateway:
  listen: false
  bind: "127.0.0.1"
  port: 18789

dashboard:
  enabled: false
  bind: "127.0.0.1"
  port: 18800

sessions:
  main:
    trust: owner
    sandbox: none
  channel_default:
    trust: untrusted
    sandbox: restricted

pairing:
  required_for_remote_channels: true
  code_ttl_seconds: 600
  allowlist_path: "MASTER/data/security/allowlist.yml"

tools:
  custom:
    enabled: true
    require_scope: true
    require_review_for_destructive: true
  deny_patterns:
    - "secret_read_then_network"
    - "broad_delete"
    - "ci_permission_edit"
    - "unknown_package_install"
    - "hidden_generated_file"

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. Preserve behavior and intent. Larger refactors allowed when approved and safe.
    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.
    DEEP_SCAN_ONLY: all scans run at deep depth. quick and standard depths are forbidden — they produce false confidence by skipping semantic, adversarial, Rubocop, and Reek rules. Shallow scans pass code that deep scans reject.

negotiable:
  style: sentence_case
  default_model: z-ai/glm-4.5-air:free
  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/ }
- { name: Repligen,       tier: dangerous, visitor: false, default: true,  description: Replicate image gen (Flux etc), actions: generate/sync/search/stats }
- { name: Postpro,        tier: dangerous, visitor: false, default: true,  description: Cinematic film post-processing on images (kodak etc) }

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: 300
  scan_depth: deep
  scan_nice: 15
  scan_file_sleep_s: 2
  fix_depth: llm
  batch_size: 1
  max_cycles: 12
  rate_limit_sleep: 30
  max_file_bytes: 16000
  max_fix_retries: 3
  confidence_threshold: 0.60
  targets:
    - lib/
    - test/
    - data/
    - web/
    - DEPLOY/
  excludes:
    - vendor/
    - knowledge/
    - fix_
    - patch_

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

# LLM Ergonomics — explicit guidance for working with current-generation models.
# Added to reduce friction for future agents while preserving constitutional integrity.
# Source: 2026 reassessment of agent experience.
llm_ergonomics:
  core_principle: "Optimize for actual LLM behavior and context limits, not idealized super-agents."
  guidance:
    - "Provide high-density mental models (e.g. QUICKSTART.md) before requiring deep constitution reads."
    - "Allow external tools (grep, rg, find) for reconnaissance and understanding. Require constitutional tools only for production changes."
    - "When full strict adherence would cause context exhaustion or paralysis, prefer 'good enough + evidence' over perfect ceremony."
    - "Document known friction points openly so future agents can work around them intelligently."
    - "Event emission should be rich and first-class by default so observers (human or visual) can understand internal state without heroic effort."
  tension_with_rules: "This section provides pragmatic guidance for working with real LLM limitations. It does not override any ABSOLUTE rules defined in soul.yml."

# 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 (and refine) the existing retro Atkinson/Bayer/ZX/phosphor look as pure white dithered pixels — 8-bit monochrome CRT / terminal aesthetic.
- 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 to produce pure white pixels, and blits the result to the face canvas (8-bit monochrome CRT / terminal aesthetic with dither for shading).

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/master_deploy_review.md

# MASTER and DEPLOY review

Reviewed against:

- `MASTER/data/soul.yml`
- `MASTER/data/rules.yml`
- `MASTER/data/workflow.yml`
- `MASTER/CONVENTIONS.md`
- `MASTER/data/claude/project_master.md`
- `MASTER/bin/cli`
- `MASTER/bin/gate`
- `MASTER/spec/social/assistant_contract_spec.rb`
- `MASTER/lib/now/stages/enhance.rb`
- `DEPLOY/rails/apps.yml`
- `DEPLOY/rails/PRODUCTION_READINESS.md`

## Rules internalized

- Preserve then improve, never break.
- Read full files before editing them.
- Use explicit evidence, not intent language.
- Keep scans deep.
- Avoid bare rescue and broad swallowing.
- Keep rules in data, not Ruby strings.
- Lead with the fact, then evidence, then implementation.
- Keep command paths separate from prose; no shell noise.
- Treat `relayd` as the TLS terminator on the deploy side.
- Keep Rails proxy-aware with `assume_ssl`, not TLS-owning with `force_ssl`.

## CLI and chitchat proof

- `MASTER/bin/cli` boots MASTER directly, sets safe defaults unless explicitly overridden, and supports both TTY and pipe mode.
- In TTY mode it can also launch the local web UI when enabled.
- `MASTER/bin/gate` exercises the safe command surface through `bin/cli` and then checks the diff stays clean.
- `MASTER/spec/social/assistant_contract_spec.rb` covers casual greeting, confusion repair, frustration, background boundary, and continue behavior.
- `MASTER/lib/now/stages/enhance.rb` skips slash commands, code fences, greetings, and one-word affirmations, which keeps chitchat from being overprocessed.

## Current gaps and opportunities

1. `bin/cli` cannot be executed end-to-end on this host until the MASTER bundle is installed.
2. The greeting path needs a repeatable CLI smoke that does not depend on manual interpretation.
3. `bin/gate` covers command classes, but not a dedicated social/chitchat smoke.
4. The social contract is present in tests, but the behavior is still mostly documented by examples instead of a single executable acceptance harness.
5. `rules.yml` has a rich prediction engine, but the autofix threshold and rule-by-rule enforcement deserve a tighter proof harness.
6. The structural ops surface still wants a single command-router path, not scattered wrappers.
7. The hallucination detector remains a known stub area.
8. The self-test wiring declared in `rules.yml` still needs a reader that executes laws against the runtime itself.
9. Several `MASTER/web` assets are still unreferenced and should either be wired or deleted.
10. `DEPLOY/openbsd` still benefits from a dry-run diff and a service-health report before restart.
11. The Rails production gate is good as a static guard, but target-host runtime verification is still the missing proof of readiness.
12. `baibl`, `blognet`, and `hjerterom` still need the full Ruby 3.4 bundle/test/security pass on the deploy target.

## DEPLOY signal

- The Rails matrix is explicit enough now to make production readiness measurable.
- `brgen` is the closest to production.
- `amber`, `bsdports`, `baibl`, `blognet`, and `hjerterom` remain gated on bundle installation, credentials rotation, and smoke validation.
- `relayd` owns TLS; Rails should stay proxy-aware and avoid HTTPS redirect ownership.

## Working rule

If a future change touches MASTER or DEPLOY, the first question is whether it improves proof, reduces drift, or removes an unforced failure mode. If not, it is probably noise.

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.

## Subtle Delight, Snappiness & Grok-like Reactivity

Grounded in the existing particle kernel, SSE master:visual bridge, low-res pixelated canvases, zsh input, streaming chunks + dmesg, enhance flow, topologies emotional/cell grammar, FLAT_UI/CINEMA_PALETTE/step timing, and NNG gaps (control, error recovery, discoverability, help). All ideas are micro, PRESERVE_FIRST, no new files or heavy machinery, beauty-first (kanso, ma, Ando restraint). Many leverage the kernel's 12 semantic fields and the now-cleaner canonical signals post recent defrag.

### Input & Prompt Bar (zsh, photo, enhance)
1. On #zin focus, advance eyePool attention cells +0.07 for 160ms (kernel-driven "attending" micro-expression; decays naturally).
2. Step-ease the #zsh opacity transition to steps(5, end) at 140ms to match primer/cursor timing.
3. Photo button pointerdown spawns 3 fast-decay speech-kind kernel cells at mouth zone with low pressure (tiny "capture" flash).
4. After successful photo upload (state=ready), fade the button background from accent to fg over 420ms steps(4).
5. Enhance confirm [y/n] flow: on keypress, briefly raise mouthPool arousal 0.4 for one frame (viseme-like "decision" pulse).
6. Placeholder text in zin cycles subtly on idle (every 45s, 3-word variants via CSS only or tiny JS) using muted opacity.
7. Long-press (420ms) on photo button (when idle) triggers a low-entropy "preview burst" of 6-8 attention cells in crown zone.
8. Input value length > 180 chars: scale the zsh border-top thickness 1px → 1.5px over 200ms (quiet density signal).
9. On paste into zin, emit a single master:visual "input:paste" with entropy 0.25 so ecology terrain gets a small basin impact.
10. Cursor in zin blinks at 1.1s step-end (already close); sync blink phase to the primer pulse for global rhythm.

### Streaming, Chunks & Live Feedback (chat-log, cursor, dmesg)
11. On first _chatOnChunk, advance the face colorTarget toward the model tint 15% faster for the opening 800ms (snappier "response starts" feel).
12. New .dmesg-line elements: start at opacity 0.4, step to 1.0 over 180ms (instead of instant).
13. When a sentence chunk ends (period detection in streaming), inject 0.15 pressure into live mouthPool cells (tiny "punctuated" physicality).
14. Cursor removal on _chatOnDone: fade opacity 1→0 over 280ms steps(3) before DOM remove (less abrupt).
15. Scroll-snap in #chat-log: after append, if user is near bottom, use a 1-frame RAF scroll with ease-out steps.
16. Assistant message prompt ("master$ ") appears with 80ms delay after user message for theatrical but quiet rhythm.
17. ERROR: chunks get a 120ms red flash on the current .msg-body (using existing TINT.veto lerp path).
18. Dmesg lines that mention "veto" or "pass": the ecology weather spawns one extra calm burst at low force.
19. Streaming body text: every 4th chunk character, if kernel present, nudge one random live eye cell attention +0.03.
20. After [DONE], the last assistant message gets a 1px left hairline that fades over 1.8s (quiet "settled" marker).

### Particle Face & Ecology Reactivity (kernel, motion, state viz)
21. Idle breath amplitude now also receives a 0.003–0.009 entropy wobble (using the canonical entropy from visual events) for organic micro-variation.
22. On any master:visual with high confidence (>0.85), eyeJitter decay slows 8% for 900ms (calmer, more "focused" passive stare).
23. MouthPool on speech boundary: in addition to arousal/pressure, set 1-2 cells' valence to +0.6 for 220ms (brighter "speaking" micro-glow via existing color lerp).
24. Ecology agent spirits: orbit radius now receives a 0.02–0.06 multiplier from the corresponding kernel cell's attention (already partially wired; make the read authoritative).
25. Terrain line alpha in drawSemanticTerrain: multiply by (0.7 + confidence * 0.3) so high-certainty moments look crisper.
26. Crown zone cells (memory kind): on "memory|retriev" events, spawn one extra slow-decay cell with high valence.
27. Vertex displacement on the icosa: add a 0.3x global scale factor from current mouthDrive average when >0.6 (subtle "full voice" head expansion).
28. When topology switches to codebase via canonical event, edgePoints opacity steps from 0.55 to 0.72 over 300ms (quiet "thinking about code" cue).
29. Reduced-motion profile: still allow 3–4 low-amplitude kernel cell "breaths" per minute at 8% normal motion (never fully static).
30. On device tilt or mouse move, the eyeMask vertices get a 1-frame 0.015 position bias toward the direction (already mouse-driven; add tiny kernel attention boost in that zone).

### Performance & Perceived Snappiness
31. visual_bridge handleRuntimeEvent: batch 2–3 rapid events into one RAF before emitting master:visual (less thrash on busy pipelines).
32. ParticleKernel.step calls: guard with a 8ms min delta (coalesce with existing frame dt) to avoid sub-frame overwork on high refresh.
33. Canvas resize: cache the last internalW/H and only call fitInternalResolution when crossing 50px threshold or profile change.
34. Chat-log appends: use a 1-line micro task queue so 3+ chunks in 50ms render as one DOM write.
35. SSE reconnect backoff: show a single dim dmesg "link quiet" instead of repeated errors; face confidence drops smoothly 0.1.
36. Primer boot sequence: the POST_LINES already beep; add one kernel cell spawn per line (tiny "awakening" particles that decay by "ready").
37. RAF in face/ecology: when document.hidden or battery profile, drop kernel step rate to 1/3 without changing visual governor global cap.
38. Three points count: on coarse pointer or reduced motion, halve the unique edge positions used for edgePoints (cheaper but still reads as dense).
39. Color lerps (faceCurrent → target): use a slightly higher factor (0.06) on high-activity moments detected from recent pulse count.
40. Dmesg fade timeouts: use a shared 3-slot ring so 20+ lines don't create 20 timers.

### Error States, Recovery & Control (NNG 3/5/9)
41. On SSE onerror or ERROR chunk: the face shake now also drops 4–6 random mouth cells' confidence by 0.35 (visible "stutter" without alarm).
42. Escape key (already ttsSkip): also emit a master:visual "user:interrupt" so ecology gets one rift impact (clear "I stopped you" feedback).
43. Photo upload failure: the assistant message appears with a 200ms delayed red hairline on the photo-button (state reset happens first).
44. Enhance [y/n] timeout (no key in 12s): auto-accept original with a quiet kernel "settle" pulse (no pressure spike).
45. Long-press anywhere on #chat-shell (outside input): triggers ttsSkip + one ecology weather burst at low force (emergency stop affordance).
46. After veto verdict: the next zin placeholder temporarily reads "try a tighter question" for 9s (subtle recovery hint, no new strings in core logic).
47. Confidence event <0.3: eyeJitter gains a tiny random 1px twitch every 4th frame for 2.5s ("nervous but working" micro).
48. /run natural language commands: on submit, the face pulse starts 40ms earlier than the SSE roundtrip (predictive "heard you").
49. STT (long-press canvas): on recognition start, all live eye cells get +0.25 attention for the duration (visible "listening hard").
50. Network stall >4s: a single very dim centered dmesg "link thinking" appears once; face breath slows 30% (quiet waiting state).

### Discoverability, Help & Recognition (NNG 6/10)
51. First successful message after boot: spawn 5 slow crown memory cells labeled by provider (tiny "you used X" echo that fades in 12s).
52. Hover (or pointer near) the #face canvas edges: raise 2–3 peripheral kernel cells' arousal 0.2 for 300ms (quiet "edge has meaning" affordance).
53. Zsh input empty + 6s idle: one very low-alpha ecology "help" trail appears pointing toward the input (auto-removes on any input).
54. Verdict "pass" events: the chat-log gets a 1px hairline flash under the last message (matches the beep).
55. On model tint change (claude/gemini etc.): the face TINT lerp also nudges one mouth cell valence toward the new hue for 1s.
56. Canvas double-tap (coarse) or double-click: cycles the visual profile (full/battery) with a single kernel cell "confirm" spawn.
57. Dmesg lines mentioning tools: those lines get a persistent 0.6s longer fade and a tiny corresponding ecology flow cell.
58. Primer "ready" speech: at the moment the voice starts, 8–10 new attention cells spawn across eye zones (boot "eyes opening").
59. When topologies switch (face <-> ecology <-> codebase), the document root data attr change is accompanied by a 90ms global canvas filter contrast nudge (0.05) that steps back.
60. Photo button in "ready" state: pointerenter spawns 1–2 memory-kind cells at crown with the image token as a pseudo-label (harmless, decays).

### Micro Personality & Grok-like Delight (tied to existing soul/TTS/kernel)
61. TTS onboundary (already drives arousal): also set a random live eye cell's valence +0.15 (subtle "smiling while speaking" with the viseme mouth).
62. Osman creative styles (dramatic/ethereal etc.): when active via server TTS, the face receives a one-time master:visual with mode=style-name that slightly warps the breath frequency for the duration.
63. High-entropy moments: ecology terrain gets 1–2 extra jagged lines for 1.8s (matches "spray: entropy" cell grammar without extra cost).
64. Council deliberation start (newer bus events): all 7 agent spirits briefly increase radius 8% then settle (visible "thinking together").
65. Successful auto-commit after mutation: one crown cell gets high confidence + slow decay and a green-tinted valence (quiet "saved" joy).
66. Visitor vs dev tier: the zsh .pp color desaturates 15% for visitor (already data-driven; make the face overall saturation follow the same).
67. WakeLock acquired: a single 80ms full-white low-alpha flash on the primer area (then gone) — "eyes open, staying awake".
68. Reduced-motion + coarse pointer: the eyeJitter is replaced by a slower 0.8s sinusoidal "scan" across the two eyes (still expressive, zero random).
69. Every 25th chunk in a long response: if no recent user gesture, the head3 does a 0.8° micro-yaw toward the last mouse position (subtle "checking in").
70. On final [DONE] with high confidence: the last dmesg line (if any) gets a 0.4s brighter moment before its normal fade.

### Accessibility, Reduced Motion & Edge Polish
71. All new step transitions respect the existing @media (prefers-reduced-motion) rule (already inherited).
72. Focus-visible on zin and photo-button: add a 1px outline that also raises the nearest eyePool attention cells (keyboard users see the face react).
73. Canvas aria-hidden already present; add role="img" + dynamic aria-label derived from current State.mode + confidence (e.g. "thinking, 0.78").
74. When battery profile active, all particle counts in kernel pools are halved at spawn time (already partially done via limits; make consistent for ecology agents too).
75. Scroll in chat-log: momentum on touch devices already; add a 60ms RAF clamp so it never overscrolls past the last message by >12px.
76. Color contrast: all new micro elements (hairlines, flashes) stay within the existing --face-fg / muted tokens.
77. VoiceOver / SR users: the dmesg lines are already low-volume; ensure they are not announced by wrapping new ones in aria-hidden when they are purely visual echoes.
78. Pointer coarse: increase the long-press timeout from 420ms to 520ms and widen the STT hit area by 8px (easier on phones).
79. Visibilitychange hidden: already pauses some work; also pause new kernel spawns from non-critical events for 800ms after return.
80. Error recovery text (e.g. photo fail): use the exact same mono font and dim color as dmesg so it feels like part of the instrument, not a dialog.

These 80 ideas are all small deltas on existing paths (kernel fields, RAF loops, SSE listeners, CSS custom properties, data attrs, existing TINT/lerp/pulse). Most are 1–4 line changes. Many directly improve the "watch from afar" passive beauty while adding the snappy, alive, grok-like reactivity (instant visual acknowledgment of every user action and every internal state change) without violating restraint or introducing noise.

Implementation order suggestion: group by file (bridge + face first for signal + breath wins, then chat.js + css for input/streaming, then ecology for field harmony). Each can be a separate micro-slice after full re-read of the touched file. All preserve current behavior as fallbacks or additive only.

## Implementation Status (Micro-Slices)

**Batch 1 completed (micro-slices 3-6, covering ~12 ideas + foundations):**
- Bridge classify now prefers registry (ONE_SOURCE signal cleanup for all downstream reactivity).
- Face idle breath: confidence modulation + entropy wobble (ideas 21 + related).
- Face speech boundary: valence boost on mouthPool cells (idea 23).
- Face high-confidence: eyeJitter decay slowed (idea 22).
- Face tint lerp: activity-adaptive faster speed on pulse (snappier model/mood response).
- Chat streaming: cursor removal with step fade (pleasant done state).
- Chat dmesg: quick step fade-in on appearance (snappier live log).

**Batch 2 (micro-slices 7-9, ecology field harmony):**
- cognition_ecology.js: agent orbit radius now subtly tightens with high confidence (calmer, more focused "spirits").
- Terrain line alpha now scales with confidence (crisper field when system is sure).
- High-entropy moments now produce visibly more jagged/chaotic terrain lines (extra "jagged" effect per idea 63) — all using the canonical event signals.

**Batch 3 (micro-slice 10, face personality):**
- face.js: eyeJitter now increases with entropy (nervous/tense eyes on high-entropy moments) while still damped by confidence — nice "alive" personality signal for the watched face.

**Batch 4 (micro-slices 11-16, Ruby runtime events + face reaction):**
- ... (Ruby signals for council:deliberation, user:interrupt, tts:style:active, tribunal:rendered+confidence, input:long, link:quiet — ideas 64/42/62/47/70/17/8/9/48/35/50/167 + NNG control/recovery)
- face.js verdict handler: numeric confidence now drives jitter + brightness.
- face.js: council:deliberation/start now visibly raises mouth pressure + lowers eye confidence (idea 64).
- face.js: input:long/cmd:long now drives jitter + mouth pressure (ideas 8/9/48 density/predictive).

**Batch 5+ (ongoing auto-waves, big refactors now approved)**: council reversibility now emitted on bus + charges ecology spirits; Ruby speech register + creative bias via new central Expression module; face reaction system refactored to be primarily driven by server Expression payloads (big refactor of master:visual listener); creative style bleed + decay on eyes added; mobile devicemotion feeds arousal/valence; pure-Zsh relayd hardening. This structure makes the remaining 50+ ideas (pre-speech anticipation, mood arc, vertical timbre, richer kernel modulation, etc.) far cheaper to implement cleanly. Continuing until per-area plateau.

## Proposed Improvements: Web UI + Face + TTS + Interconnectedness (2026+)

These build on the completed batches and the current architecture (Ruby events → visual_bridge → master:visual + specific listeners → ParticleKernel fields + visemes + CSS). Focus is on deeper coupling so the face "feels" the full system state and TTS creative choices in a coherent, watchable way.

### Face / Particle System (parametric expressiveness)
1. Map server "entropy" + "pressure" more broadly to additional kernel fields (velocity damping, zone jitter, attention decay rate) for richer idle "personality".
2. Add subtle global "breathing" offset to all pools driven by overall system confidence (slow inhale/exhale visible even when zoomed out).
3. Eye pool: pupil size + saccade frequency modulated by "focus" (low during council deliberation, high during precise user input).
4. Terrain / background elements react to vertical (marketplace = more angular, dating = softer curves, tv = scanlines).
5. Age field decay rate changes with "mood arc" (recent high-entropy events make cells "tire" faster).
6. Additive glow / rim lighting on high-arousal cells during energetic TTS styles.
7. Bayer dither + low internal res upscaling for consistent "pixel art instrument" aesthetic at all zoom levels.
8. Mouse tilt interaction strength scales with current system "openness" (low during error states).
9. Cluster "personalities": groups of cells that stay coherent longer when the same vertical has been dominant recently.
10. Slow "memory trails" — faint ghost particles that linger and slowly decay, showing emotional history.

### TTS + Voice Creative Effects (Osman + styles)
11. Server-driven viseme stream (from edge-tts phonemes or timing) instead of client Web Speech approximation for accurate mouth sync on all styles.
12. Prosody → kernel: rate affects global time scale of all lerps; pitch affects vertical bias in eye/mouth pools.
13. Style chaining / layering (e.g. "whispered + ethereal" produces very low pressure + high valence + airy terrain).
14. Pre-speech "inhale" anticipation: brief arousal spike + eye widening 150-300ms before audio starts (makes the face feel alive and anticipatory).
15. Post-speech decay tuned per style (dramatic lingers with high pressure; whispered drops fast to calm).
16. TTS "effort" signal (long text or complex style) increases overall particle density temporarily.
17. Voice-specific idle signatures (Osman has slightly lower base arousal than Ryan when idle).
18. Real-time ducking: when user starts speaking (STT), TTS-driven mouth energy gracefully hands off to listening state.
19. Creative effect "bleed": intense style leaves a temporary "ringing" high-frequency jitter on eye pool for 4-8 seconds after.
20. Style preview on face: hovering or selecting a style in UI temporarily modulates the idle face so you can "see" what dramatic vs whispered will look like.

### Web UI / Controls & Feedback
21. Prominent TTS style picker (or quick chips) that sends the style to /chat/tts and also triggers a live face preview reaction.
22. Current mode / style indicator that is visually tied to the face (subtle color or topology shift on the canvas).
23. "Emotional history" mini timeline or sparkline in the UI driven by recent master:visual events.
24. Direct manipulation: dragging on the face sends a "user:expression" event that can influence the next LLM response or council.
25. Voice activity visualization that is unified with the particle system (user mic input creates temporary "listening" terrain ripples).
26. Better error / phantom recovery visuals that feel like part of the instrument (not popups).
27. Keyboard shortcuts that have corresponding visible face "acknowledgment" micro-animations.
28. Mobile: pressure-sensitive long-press that changes face "tension" in real time before sending.
29. Theme / palette switcher that also remaps the particle color/tonal system (CINEMA_PALETTE variants).

### Deep Interconnectedness & Feedback Loops (the core request)
30. Bidirectional context: average face state (mean arousal, dominant valence, recent entropy) is sent back with the next message as lightweight "felt sense" for the LLM/council.
31. Multi-signal superposition: when council deliberation + TTS dramatic + high entropy overlap, the kernel produces interesting "polyphonic" states instead of last-wins.
32. Vertical "timbre": each subapp (marketplace, dating, tv...) has a subtle persistent bias on kernel parameters so the face develops a "voice" for that vertical over a session.
33. Graph state reflection: high co-change or dense local activity in the unified event stream makes the overall particle field more "connected" (more cross-pool influence).
34. Confidence from multiple sources (verdict, retrieval, council) is fused into a single "certainty" signal that smoothly controls eye stability and terrain crispness.
35. TTS style chosen by the system (not just user) creates a visible "the AI decided to speak dramatically" moment.
36. Phantom / self-correction events create a brief "flinch" or self-soothing motion on the face before the text appears.
37. Long-running council or heavy tool use creates a visible "thinking breath" cycle on the face that users can watch.
38. User interrupt (escape / ttsSkip) creates an immediate "reset" ripple across all pools + quick return to attentive idle.
39. Memory of emotional arc: the face slowly drifts toward a "baseline personality" based on the last 5-10 minutes of interaction signals.
40. Cross-app resonance: activity in one vertical (e.g. new marketplace offer) can cause a small "echo" modulation on the face even if the user is looking at TV.

### Beauty from Afar, NNG, Performance, Mobile
41-50. (Slow breathing that looks good when the tab is in the background; stronger respect for reduced-motion; better mobile touch targets tied to face affordances; fixed-timestep + Bayer improvements for consistent pixel look; data-driven "mood" that persists across reloads via small local storage; etc.)

These are all designed as 1-4 line additive changes on existing paths (kernel field modulation, richer event payloads, small listeners, CSS tokens, one extra publish in the controller or pipeline). They dramatically increase the felt interconnectedness so the face stops being "just a pretty animation" and becomes a living instrument that reflects the entire MASTER runtime and the user's emotional journey through the city platform.

Run `/sweep` after implementing batches. Continuing micro-slices.

docs/web-ui-improvements.md

# MASTER Web UI Improvement Proposals

**Scope**: MASTER/web/ (face rendering, chat interface, dashboard, PWA, SSE/streaming, canvas interactions, CSS, controllers, public/*.js assets).

**Basis**: Code analysis of current implementation (minimalist zen "terminal" aesthetic, face as central visual, chat-log + zsh prompt bar, heavy JS in public/ for particles/3D/dither/phosphor, Rails backend for chat/tts/photo/events, existing TODOs in Q4/Q5/L/O sections).

**Philosophy alignment**: Preserve ultra-minimal, OLED-black, crosshair-cursor, monospace, no-fuss "you$ / master$" feel. Enhance without adding chrome. Favor pixelated/retro where it fits (see recent white phosphor face changes). Performance on low-end (coarsePointer modes). Accessibility, battery, offline. Deep integration with MASTER core (bus events, visual state, voice, models).

**Total ideas**: ~160 (categorized; many build on or expand existing TODOs like Q401-Q510, L01-L08, O5xx; new ones from direct code review of chat.js, face.js, face3d_*, mask.js, particle_kernel.js, controllers, CSS, views).

Ideas are actionable, small-to-medium patches preferred. Prioritize: face/visual (core identity), streaming UX, perf, a11y, features that surface MASTER capabilities.

## 1. Face / Particle / Visualization System (25 ideas)
1. Implement full module split for face.js (Q401): face/particles.js, face/audio.js, face/expressions.js, face/tts.js, face/main.js + index that wires.
2. Add Web Worker for particle physics/simulation updates to offload main thread (esp. with 20k particles).
3. Expose face config UI (sliders for N particles, morph speed, coherence, breathing amp) in a hidden "dev face panel" toggleable by ?debug=1 or alt-click logo.
4. Add "face modes" switch: 3D points (current), raster dither (face3d), hybrid, wireframe, silhouette only.
5. Improve 2D fallback (no WebGL): use ParticleKernel for consistent dithered cell rendering instead of ad-hoc fillRect.
6. Add face "afterimages" / trail effects using offscreen canvas + alpha decay for phosphor persistence visual.
7. Support multiple simultaneous faces (e.g. Osman + Pernille side-by-side or layered for council deliberation).
8. Drive face directly from visual_bridge + face_state more deeply (e.g. entropy -> particle scatter, confidence -> cohesion).
9. Add procedural "damage" / glitch effects on face for veto/error states (scanlines, pixel drop, chromatic aberration via canvas filters).
10. Make particle count dynamic + LOD: lower for battery/coarse, higher for desktop; auto-scale with viewport + FPS.
11. Add face "idle animations" library: breathing, saccades, subtle topology morphs even without input.
12. Integrate cognition_ecology visuals as "background" or "halo" around the face (terrain, trails).
13. Add face export: button to snapshot current state + canvas as PNG/SVG with metadata (model, mood, timestamp).
14. Improve expression transitions with lerp + easing curves (Q410) + audio-reactive (viseme drives jaw/mouth blendshapes in 3D).
15. Wire persona color/motion distinction (Q412) even in white-phosphor mode: use subtle size variation, dither density, or secondary "aura" particles per persona.
16. Add "thinking" face state: slower morph, higher scatter, pulsing highlight on "active" zones.
17. Canvas resize observer + DPR clamping + virtual resolution for consistent pixel look across devices (Q405).
18. Pause rAF + audio analysis on hidden tab + reduced-motion (Q402, Q409); resume gracefully.
19. Add visual "TTS fetching" anticipation indicator (Q413): pre-load expression change + brief particle "inhale".
20. Face as interactive: click zones to trigger /why or specific persona speak; drag to rotate 3D head.
21. Add face "memory" overlay: faint previous topologies or cluster points fading in as "ghosts".
22. Support high-contrast / monochrome forced mode for accessibility (beyond current white).
23. Dither quality modes: low (bayer fast), high (Atkinson + multi-pass), none (for perf).
24. Face state persistence: save/restore last expression + topology across reloads via localStorage or session.
25. Add subtle CRT bezel / scanline overlay (CSS + canvas) toggleable for extra retro 8-bit terminal feel.

## 2. Chat Interface & Streaming (30 ideas)
26. Add chat history sidebar (collapsible, searchable) that loads from /chat/history; click to replay with context.
27. Infinite scroll / pagination for chat-log (virtual list for long sessions).
28. Command palette (cmd+k / ctrl+k): fuzzy search available /commands, recent, face modes, etc. (builds on missing web /grep etc.).
29. Per-message actions: copy, "regenerate with different model", "send to face as expression", "quote in new input".
30. Better streaming UX: word-by-word reveal with cursor, optional "instant" mode, typing sound (subtle, opt-in).
31. Multi-turn editing: click previous user message to edit + branch (like chat UIs); server supports via context.
32. Attachments beyond photo: drag-drop files, paste images, with preview + postpro options.
33. Enhance preview live: as you type, subtle dimmed suggestion below input (Q202 style progressive).
34. Voice input: button for STT (browser or server), auto-send on silence (T728).
35. Threading / branching UI: visual tree or "fork" button for alternative explorations.
36. Search in chat: / or input prefix to filter visible messages + highlight.
37. Markdown / code rendering in assistant responses (syntax highlight via lightweight lib or Prism).
38. Cost / token / model badge per message or session (surfaced from backend).
39. "Thinking" indicators: per-chunk status (enhance, tool call, council) with icons or face sync.
40. Undo / edit last turn: keyboard or button that reverts UI + sends correction.
41. Session save / load / export: buttons for markdown, jsonl, or "shareable replay" link.
42. Input history (arrow up/down) persisted client-side or via /history.
43. Auto-complete for @mentions (files? models? personas) in prompt.
44. Better error recovery: retry button on stream fail, with last message preserved.
45. Collapsible / summary for long assistant blocks (click to expand).
46. Side-by-side diff view when enhance or tool results change output.
47. Chat log "dmesg" as optional overlay or bottom ticker (already partial).
48. Input bar improvements: growing textarea, emoji picker? no — keep minimal; better placeholder with examples.
49. Send on shift+enter? Or configurable.
50. Mobile: better virtual keyboard handling, swipe gestures for history, tap face to focus input.
51. "Quiet mode": hide chat-log, only face + minimal prompt (focus on visual).
52. Multi-user / shared chat? (if auth allows) with user colors.
53. Rate limit UI feedback: show "slow down" when hitting backend throttles.
54. Paste detection + large paste handling (Q107).
55. Command bar integration: type / in input to trigger palette.

## 3. Performance & Rendering (20 ideas)
56. Bundle split / lazy load: face3d only when needed, three.module only on WebGL, etc.
57. OffscreenCanvas for face rendering where supported.
58. Throttle / debounce heavy listeners (mousemove, resize, audio).
59. FPS monitor + auto quality downgrade (particle count, dither, shadows).
60. Memory: clean up old canvases, event listeners, SSE on unmount/navigation.
61. Preload critical assets (manifest, fonts, face shaders) + service worker caching (already partial sw.js).
62. Reduce main thread work in draw loops: use requestIdleCallback for non-visual.
63. Canvas pooling or single canvas for multiple visual systems (face + ecology + codebase).
64. WebGL instancing or points with better shaders for 20k+ particles.
65. CSS containment, will-change, transform for chat-log and overlays.
66. Debounce photo postpro + uploads.
67. Virtual scrolling for very long chat logs + dmesg lines.
68. Worker for audio analysis / FFT.
69. Measure & report render time, particle update time via /metrics or debug panel.
70. Avoid layout thrashing in appendMsg / streaming updates.
71. Image loading lazy for any future icons / previews.
72. Font subsetting or system stack fallback for "Roboto Mono".
73. Reduce re-renders: React-like signals or simple dirty flags for face state.
74. Battery-aware: lower particle N, disable audio viz, slower animations (already partial via data attrs).
75. Profile with devtools; add perf marks around key paths (draw, SSE, enhance).

## 4. Accessibility & Inclusivity (18 ideas)
76. Full ARIA: roles, labels, live regions for streaming text (Q414), face as decorative or described.
77. Keyboard only: full nav of history, input, photo, face interactions (tab, arrows, enter, space).
78. Screen reader: announce new messages, face state changes ("Osman thinking", "expression: curious"), dmesg.
79. High contrast mode: stronger --face-fg, forced outlines, no subtle opacity.
80. Reduced motion: respect everywhere (Q409), provide "static face" option.
81. Color: ensure all states have non-color indicators (icons, patterns, text); our white face helps.
82. Touch targets: min 44px for buttons (photo, primer, etc.).
83. Focus management: visible focus on primer, input, messages; trap in modals if added.
84. Voice / TTS controls: volume, speed, skip visible + keyboard.
85. Alt text / descriptions for any visual outputs (future diagrams, exports).
86. Language: support dir=auto, better i18n if ever.
87. Error announcements: polite live regions for failures.
88. Magnification / zoom friendly (no fixed small fonts without scale).
89. Captioning for any future video/audio in UI.
90. Cognitive: clear affordances, consistent "terminal" metaphors, undo everywhere.
91. Pointer: support coarse (touch) vs fine; larger hit areas.
92. Add "describe face" button that reads current state aloud or to clipboard.
93. WCAG AA/AAA audit pass for contrast (current dark theme mostly good with white on black).
94. Skip links or quick nav for long logs.
95. Announce model / tier / cost changes.

## 5. Theming, Aesthetics & Polish (15 ideas, building on white phosphor)
96. Expand runtime profiles: "zen" (current), "crt" (scanlines + bloom on white pixels), "pixel" (strict low-res no upscale), "neon" (subtle color accents on white).
97. More CSS vars for easy theming: --face-particle-size, --dither-strength, --phosphor-decay.
98. Darker-than-black or true OLED modes with --face-bg: #000000.
99. Subtle background textures (very faint grid or noise) that react to face state.
100. Consistent micro-animations: only steps() or linear for retro; spring for modern opt-in.
101. Logo / branding polish: animated "MASTER" that pulses with face.
102. Error / 4xx / 5xx pages styled in same terminal aesthetic (already partial HTML).
103. Photo upload UI: better preview, progress, postpro choice (stock selector).
104. Enhance confirm: nicer UI than [y/n] text (still keyboard driven).
105. Cursor: custom crosshair already good; variants per state (thinking = hourglass pixels?).
106. Scrollbars: custom thin monospace-style that fit zen.
107. Selection: highlight color matches --face-fg dim.
108. Print / export styles: clean chat transcript.
109. Seasonal / event skins (subtle): e.g. holiday dither patterns.
110. User-select and drag prevention tuned (already mostly).
111. Focus-visible outlines that look pixelated / retro.
112. Toast / flash messages in dmesg style (dim, auto-fade).
113. Loading skeleton for initial face + chat that matches pixel aesthetic.
114. Subtle CRT curvature / vignette via CSS filter or overlay (opt-in).

## 6. Features & New Capabilities (25 ideas)
115. Full offline mode: cache last session, local TTS fallback, offline face sim (Q510).
116. STT everywhere: mic button in prompt, wake-word "master", continuous listening opt-in.
117. Multi-model chat: switch model mid-session with visual indicator (face tint gone, but badge + expression).
118. Tool use visualization: live face + log updates when tools run (already dmesg partial).
119. Council / deliberation view: show multiple personas "speaking" with face switching or split.
120. Code execution sandbox preview in chat (if /run etc.).
121. File browser / @file completion integrated with codebase visual.
122. Session analytics dashboard (separate or overlay): tokens, cost, turns, common commands.
123. "Remember this" : highlight messages to pin to long-term memory.
124. Image gen integration (repligen?) triggered from chat + face reaction.
125. Voice cloning / style training UI (advanced).
126. Collaborative: share session link, multiple cursors (if auth).
127. Export to other formats: PDF transcript, audio podcast of session.
128. Quick actions bar: /scan, /fix, /critique buttons that inject into chat.
129. Face-driven commands: certain expressions trigger suggestions (e.g. "veto" face suggests rollback).
130. History search across sessions (backend + UI).
131. "What would X say?" : switch persona for last response preview.
132. Diff view for any file changes mentioned in response.
133. Calendar / reminder integration from chat (future).
134. Better photo: multi-photo, camera live preview, auto-crop to face.
135. Audio upload / transcription.
136. Diagram / ASCII art rendering from responses.
137. "Continue in background" for long tasks with notification.
138. Bookmarkable states: ?face=neural& mood=curious&log=last10 .
139. Theme editor for advanced users (CSS vars live edit).

## 7. Architecture, Code Quality, Backend (20 ideas)
140. Extract ChatService from chat_controller (O106).
141. Extract ImagePresenter for photo (O107).
142. Add strong params everywhere (O505).
143. Move TTS synthesis to background job + polling (O507).
144. Rate limit all endpoints (O501, Q501).
145. ETag / Cache headers for static responses (Q502).
146. Strict loading, N+1 checks (O508, L07).
147. Better error taxonomy + user-friendly messages.
148. Audit logs for chat commands / photo uploads.
149. Split large JS: move 3D math, dither, kernel to workers or dedicated modules.
150. Add tests for controllers, JS (smoke at least).
151. TypeScript? or JSDoc for public/*.js.
152. CSP review / tightening (already has initializer).
153. Auth tier improvements: finer grained for web vs CLI.
154. Database for chat history / sessions (current in-memory?).
155. Webhook / API for external chat clients.
156. Metrics: expose Prometheus for web requests, face FPS, stream latency.
157. Internationalization hooks (even if English primary).
158. Configuration for face defaults, chat limits per tier.
159. Graceful degradation: if face fails, still full chat.
160. Versioning: UI version in meta, easy rollback.

## 8. PWA, Mobile, Offline, Distribution (12 ideas)
161. Better PWA: install prompt, better icons, splash, shortcuts (chat, face only).
162. Background sync for pending messages.
163. IndexedDB for full chat history + face snapshots.
164. Responsive: dedicated mobile layout (bottom bar, larger targets, no hover).
165. Add to home screen guidance.
166. Share target: accept shared text/images into chat.
167. Offline face: procedural animation without backend.
168. Push notifications for long-running tasks or mentions.
169. Installable "kiosk" mode for always-on face display.
170. Better iOS/Android webapp meta (status bar, safe areas — partial).
171. Service worker update UX (new version toast).
172. Performance budgets + lighthouse CI.

## 9. Dev / Debug / Observability (15 ideas)
173. ?debug=1 or alt+d : overlay with FPS, particle count, state, last events, bus traffic.
174. Visual debugger: click particle to log its zone/state.
175. Live CSS var editor.
176. Record / replay session (face + chat + audio).
177. Export face state JSON for face3d_preview or tests.
178. Console commands exposed (window.MASTERFace.setTopology etc. documented).
179. Network panel simulation (throttle SSE).
180. Error boundary UI that shows stack + "report to /propose".
181. A/B for face algorithms (dither type, particle vs raster).
182. Telemetry opt-in: anonymized usage (face interactions, commands used).
183. Hot reload for public/*.js during dev (bin/dev already?).
184. Screenshot automation for docs / gallery.
185. Accessibility audit button (axe or similar) in debug.

## 10. Integration with Broader MASTER (10 ideas)
186. Deeper visual_bridge: more events (proposal, council vote, self-scan) drive face + dmesg.
187. Codebase visual synced with chat context (highlight files mentioned).
188. Cognition ecology as "peripheral vision" around face.
189. Proactive proposals surfaced in chat UI (R section).
190. Persona system (S1) fully wired to voice + face + prompt prefix.
191. Self-evolution / meta (S2, W) visible in dashboard or "insights" panel.
192. Event bus inspector (live list of recent master:visual etc.).
193. Cost / usage from metrics exposed nicely.
194. Direct link from face click to relevant /why or source.

## 11. Miscellaneous / Wild (10+ ideas)
195. Easter eggs: konami code for old-school face mode or dilla soundtrack.
196. Collaborative face: multiple users affect same particle system (fun demo).
197. Generative face variants: user prompt "make the face look like a cat" -> topology/morph.
198. AR mode: face overlaid on camera (advanced, privacy).
199. Sound design: more procedural audio tied to face (beyond TTS).
200. Localization of the "terminal" metaphor (different prompts per language).

**Next steps recommendation**: Pick high-impact low-effort from face (1-25) and chat (26-55). Many can be done with small targeted edits to public/*.js + CSS + minor controller tweaks. Track in a new section of TODO or this doc. After batch, run full web surface scans (L items) + manual a11y/perf audit.

**Measurement**: Add basic analytics (events for face interaction, message sent, etc.) to validate improvements.

This list is derived from direct code reading + existing backlog. Can be expanded further with user testing or more deep dives (e.g. full face.js 1286 lines analysis).

---

*Generated as part of web UI exploration. Committed to docs for reference. Many ideas align with "zen-minimal" + retro pixel soul of the project.*

lib/builder.rb

# frozen_string_literal: true

require "fileutils"

module Master
  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

    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
      ecology = Judge::RepoEcology.new(root:, event_bus: bus, code_index:)
      bus.subscribe("tool:after") do |ev|
        next unless ev[:path] && MUTATING_TOOLS.include?(ev[:tool].to_s)
        ecology.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:, ecology:, 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)
      ecology = infra[:ecology]
      scanner = build_scanner(root:, agent:, bus:, ecology:)
      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:, ecology:, swarm:, deliberation:, council_stage:, ideation:, guard: }.merge(autonomous)
    end

    def build_scanner(root:, agent: nil, bus: nil, ecology: nil)
      Judge::Scan::RuleDSL
      wf = Master.load_yaml(File.join(root, "data", "workflow.yml")) rescue {}
      sleep_s = wf.dig("autoloop", "scan_file_sleep_s").to_f
      scanner = Judge::Scan::Scanner.new(event_bus: bus, file_sleep_s: sleep_s)
      Judge::Scan::Rule.registry.select(&:auto_build?).each { |k| scanner.add_rule(k.new) }
      scanner.add_rule(Judge::Scan::Rules::CoChangeCouplingRule.new(root:, ecology:))
      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]

      # MASTER_AUTOFIX=1 enables in-process convergence; off by default to avoid autocommits racing deploys.
      fix_loop = Loop::FixLoop.new(rules:, axioms:, agent:, scanner:, root:, bus:, git:, learnings:)
      if ENV["MASTER_AUTOFIX"] == "1"
        fix_loop.start_background!(root)
      end

      # MASTER_WATCH=1 enables reactive file-watching (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 } }

      # MASTER_WATCHER=0 disables the OpenBSD load watcher; on by default.
      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? } }
      bus.subscribe("self_violation") { |payload| fix_loop.halt!(reason: "self_violation #{payload[:violations]} violations") }

      { 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]
        next infra[:bus]&.publish("builder:tool_skipped", tool: defn["name"]) unless factory
        factory.call(root, infra)
      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:, scanner: ai[:scanner])
      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/converge.rb

# frozen_string_literal: true

require_relative "converge/converge"

lib/converge/canon.rb

# frozen_string_literal: true

module Converge
  class Canon
    class CyclicDependencyError < StandardError; end
    class MissingDependencyError < StandardError; end

    include TSort

    def self.load(path)
      raw = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: true)
      entries = normalize_entries(raw)
      new(entries).topologically_sorted
    end

    def self.normalize_entries(raw)
      rules = raw.fetch("rules")
      return rules if rules.is_a?(Array)

      rules.values.flat_map { |group| group.is_a?(Array) ? group : [] }
    end

    def initialize(entries)
      @rules = entries.map { |entry| Rule.new(entry) }
      @rule_map = @rules.to_h { |rule| [rule.id, rule] }
      validate_dependencies!
    end

    def topologically_sorted
      tsort
    rescue TSort::Cyclic => error
      raise CyclicDependencyError, "cycle detected: #{error.message}"
    end

    def tsort_each_node(&block)
      @rules.each(&block)
    end

    def tsort_each_child(rule, &block)
      rule.depends_on.each { |dependency| block.call(@rule_map.fetch(dependency)) }
    end

    private

    def validate_dependencies!
      missing = @rules.flat_map(&:depends_on).uniq - @rule_map.keys
      return if missing.empty?

      raise MissingDependencyError, "missing rule dependencies: #{missing.join(", ")}"
    end
  end
end

lib/converge/converge.rb

# frozen_string_literal: true

require "json"
require "yaml"
require "tsort"

module Converge
  autoload :Canon, "converge/canon"
  autoload :Engine, "converge/engine"
  autoload :EventStream, "converge/event_stream"
  autoload :Rule, "converge/rule"
end

lib/converge/engine.rb

# frozen_string_literal: true

require "digest"
require "fileutils"
require "json"
require "set"
require "sqlite3"

module Converge
  class Engine
    MAX_CYCLES = 16

    attr_reader :rules, :event_stream, :db

    def initialize(canon_path)
      @rules = Canon.load(canon_path)
      @event_stream = EventStream.new
      @context = {
        violations: [],
        events: @event_stream,
        execution_depth: 0,
        tracking_hashes: Set.new
      }
      init_storage
    end

    def run(initial_context = {})
      @context.merge!(initial_context)
      cycles = 0

      loop do
        changed = apply_rules_once
        cycles += 1
        @context[:execution_depth] = cycles
        break unless changed && cycles < MAX_CYCLES
      end

      @event_stream.emit(:convergence_complete, cycles: cycles)
      @context
    end

    def subscribe(&block)
      @event_stream.subscribe(&block)
    end

    private

    def apply_rules_once
      changed = false

      @rules.each do |rule|
        @db.transaction do
          before = JSON.generate(serializable_context)
          apply_rule(rule)
          after = JSON.generate(serializable_context)
          next if after == before

          log_state_delta(rule.id, after)
          changed = true
        end
      end

      changed
    end

    def apply_rule(rule)
      @context = rule.apply(@context.dup)
      track_state_hashes(rule.id)
    rescue StandardError => error
      violation = { rule: rule.id, error: error.message }
      @context[:violations] << violation
      @event_stream.emit(:violation, violation)
    end

    def init_storage
      db_path = File.expand_path("~/.master/state.db")
      FileUtils.mkdir_p(File.dirname(db_path))
      @db = SQLite3::Database.new(db_path, timeout: 5_000)
      @db.execute_batch <<~SQL
        CREATE TABLE IF NOT EXISTS runtime_ledger (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          rule_id TEXT,
          delta_blob TEXT,
          created_at DATETIME DEFAULT CURRENT_TIMESTAMP
        );
        CREATE TABLE IF NOT EXISTS feedback_loops (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          signature TEXT UNIQUE,
          hit_count INTEGER DEFAULT 1,
          updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
        );
      SQL
    end

    def track_state_hashes(rule_id)
      current_hash = Digest::SHA256.hexdigest(JSON.generate(serializable_context))
      signature = "#{rule_id}:#{current_hash}"

      if @context[:tracking_hashes].include?(signature)
        sql = <<~SQL
          INSERT INTO feedback_loops (signature) VALUES (?)
          ON CONFLICT(signature) DO UPDATE SET
            hit_count = hit_count + 1, updated_at = CURRENT_TIMESTAMP
        SQL
        @db.execute(sql, [signature])
        raise "oscillation_detected: rule #{rule_id} generated a cyclical state mutation"
      end

      @context[:tracking_hashes] << signature
    end

    def log_state_delta(rule_id, current_dump)
      payload = {
        rule: rule_id,
        sha256: Digest::SHA256.hexdigest(current_dump),
        violations_count: @context[:violations]&.size || 0,
        timestamp: Time.now.to_i
      }
      @db.execute("INSERT INTO runtime_ledger (rule_id, delta_blob) VALUES (?, ?)", [rule_id, JSON.generate(payload)])
      @event_stream.emit(:rule_applied, payload)
    end

    def serializable_context
      @context.reject { |key, _| %i[events tracking_hashes].include?(key) }
    end
  end
end

lib/converge/event_stream.rb

# frozen_string_literal: true

module Converge
  class EventStream
    attr_reader :log

    def initialize
      @subscribers = []
      @log = []
    end

    def emit(event_type, payload = {})
      entry = { type: event_type.to_sym, payload: payload, timestamp: Time.now.utc }
      @log << entry
      @subscribers.each { |subscriber| subscriber.call(entry) }
      entry
    end

    def subscribe(&block)
      @subscribers << block if block
    end

    def <<(entry)
      emit(entry.fetch(:type), entry.fetch(:payload, entry.except(:type)))
    end
  end
end

lib/converge/rule.rb

# frozen_string_literal: true

module Converge
  class Rule
    attr_reader :id, :type, :depends_on, :apply_block, :config

    def initialize(entry)
      @id = entry.fetch("id").to_s
      @type = (entry["type"] || entry["tier"] || "scan").to_s
      @depends_on = Array(entry["depends_on"]).map(&:to_s)
      @apply_block = entry["apply"]
      @config = entry.fetch("config", {})
      @raw = entry
    end

    def apply(context)
      return context unless apply_block

      instance_exec(context, &proc_block)
    end

    private

    def proc_block
      @proc_block ||= TOPLEVEL_BINDING.eval("proc { |ctx| #{apply_block}\n}")
    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
              finding_message = value ? format(rule.message, value) : rule.message
              findings << { id: rule.id, file:, message: finding_message, 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") }
        if erb_files.empty?
          return [{ id: :no_html, file: path, message: "no HTML/ERB files found", severity: :medium }]
        end

        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)
        parts = ["#{name}: #{p[:philosophy]}", "layout=#{p[:layout].join(', ')}",
          "avoid=#{p[:avoid].join(', ')}", "metrics=#{p[:metrics]}"]
        parts.join("; ")
      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
      COMPLEX_ACTS = %w[mine repair rollback checkpoint verify].freeze

      attr_reader :map, :zoom, :act, :target, :parent

      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
        # Nine pillars from rubyonrails.org/doctrine (DHH); cite when justifying architectural decisions.
        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

        # Database-backed adapters; eliminates Redis/PaaS dependency. Doctrine: :integrated.
        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 — applies to CLI, web UI, API errors, and prose.
        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

        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)
          heuristic_number = heuristic_key.to_s[/\d+/]
          "[Nielsen ##{heuristic_number}#{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 — applies to web, mobile, CLI, any rendered surface.
        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

        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
        LINE_HEIGHT_MIN = 1.5

        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/axioms/web_vitals.rb

# frozen_string_literal: true

module Master
  module Ground
    module Axioms
      module WebVitals
        LCP_GOOD_S = 2.0
        LCP_NEEDS_IMPROVEMENT_S = 4.0
        INP_GOOD_MS = 200
        INP_NEEDS_IMPROVEMENT_MS = 500
        CLS_GOOD = 0.1
        CLS_NEEDS_IMPROVEMENT = 0.25

        FONT_DISPLAY_SWAP = "swap"
        BODY_LINE_HEIGHT = 1.5
        MEASURE_CH = 65
        TYPE_SCALE_RATIO = 1.25
        BASE_FONT_PX = 16

        def self.lcp_grade(seconds)
          return :good if seconds <= LCP_GOOD_S
          return :needs_improvement if seconds <= LCP_NEEDS_IMPROVEMENT_S
          :poor
        end

        def self.inp_grade(ms)
          return :good if ms <= INP_GOOD_MS
          return :needs_improvement if ms <= INP_NEEDS_IMPROVEMENT_MS
          :poor
        end

        def self.cls_grade(score)
          return :good if score <= CLS_GOOD
          return :needs_improvement if score <= CLS_NEEDS_IMPROVEMENT
          :poor
        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."]
        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]}B;
          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)
        clean = text.to_s.downcase.gsub(/[^a-z0-9]+/, "-")
        clean.gsub(/\A-|-\z/, "")[0, 48]
      end
    end
  end
end

lib/ground/cluster_registry.rb

# frozen_string_literal: true

module Master
  module Ground
    # Unified queryable registry over all cluster data sources:
    #   data/visual_clusters.yml
    #   data/mobile_web_opportunities.yml
    #   repo_topics section of data/patterns.yml
    class ClusterRegistry
      Entry = Data.define(:id, :name, :source, :status, :tags, :raw)

      DATA_SOURCES = {
        visual: File.join(Master::ROOT, "data", "visual_clusters.yml"),
        mobile_web: File.join(Master::ROOT, "data", "mobile_web_opportunities.yml"),
        repo_topics: File.join(Master::ROOT, "data", "patterns.yml")
      }.freeze

      def initialize
        @entries = load_all
      end

      def all = @entries

      def find(id)
        @entries.find { |e| e.id == id.to_s }
      end

      def by_source(source)
        @entries.select { |e| e.source == source.to_sym }
      end

      def by_status(status)
        @entries.select { |e| e.status == status.to_s }
      end

      def by_tag(*tags)
        search = tags.flatten.map(&:to_s)
        @entries.select { |e| (e.tags & search).any? }
      end

      def ids = @entries.map(&:id)

      private

      def load_all
        [
          *load_visual,
          *load_mobile_web,
          *load_repo_topics
        ].uniq(&:id)
      end

      def load_visual
        data = Master.load_yaml(DATA_SOURCES[:visual])
        Array(data["clusters"]).filter_map do |c|
          next unless c["id"]
          Entry.new(
            id: c["id"],
            name: c["name"] || c["id"],
            source: :visual,
            status: c["status"] || "unknown",
            tags: Array(c["layer"]),
            raw: c
          )
        end
      rescue StandardError
        []
      end

      def load_mobile_web
        data = Master.load_yaml(DATA_SOURCES[:mobile_web])
        Array(data["clusters"]).filter_map do |c|
          next unless c["id"]
          Entry.new(
            id: c["id"],
            name: c["name"] || c["id"],
            source: :mobile_web,
            status: c["confidence"] || "unknown",
            tags: Array(c["tags"]),
            raw: c
          )
        end
      rescue StandardError
        []
      end

      def load_repo_topics
        data = Master.load_yaml(DATA_SOURCES[:repo_topics])
        clusters = data.dig("repo_topics", "clusters") || []
        Array(clusters).filter_map do |c|
          next unless c["id"]
          Entry.new(
            id: c["id"],
            name: c["name"] || c["id"],
            source: :repo_topics,
            status: c["status"] || "unknown",
            tags: [],
            raw: c
          )
        end
      rescue StandardError
        []
      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" => "z-ai/glm-4.5-air:free",
        "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) }
      end

      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 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
      @constitution_cache = {}
      @cache_mutex = Mutex.new

      class << self
        def load_cached(dir)
          @cache_mutex.synchronize do
            @constitution_cache[dir] ||= load_dir(dir)
          end
        end

        def clear_cache!(dir = nil)
          @cache_mutex.synchronize do
            dir ? @constitution_cache.delete(dir) : @constitution_cache.clear
          end
        end

        private

        def load_dir(dir)
          return [].freeze unless File.directory?(dir)

          Dir.glob(File.join(dir, "*.md")).sort.filter_map { |path| parse(path) }
             .first(MAX_PRINCIPLES)
             .map(&:freeze)
             .freeze
        rescue StandardError => e
          Master::Ground::Swallow.log(e, context: "constitution.load", dir:)
          [].freeze
        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

      def initialize(dir: DIR)
        @dir = dir
        @principles = self.class.load_cached(@dir)
      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!
        self.class.clear_cache!(@dir)
        @principles = self.class.load_cached(@dir)
        self
      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|
          score = format("%.2f", doc["score"])
          { source: :memory, path: doc["path"], text: "#{doc["path"]} #{doc["title"]} score=#{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)
        data = plan.respond_to?(:to_h) ? plan.to_h : {}
        {
          files: Array(data[:files] || data["files"]),
          symbols: Array(data[:symbols] || data["symbols"]),
          callers: data[:callers] || data["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/evidence_graph.rb

# frozen_string_literal: true

module Master
  module Ground
    # Directed graph of evidence edges derived from ClusterRegistry.
    # Nodes: cluster ids. Edges:
    #   cluster -> file  (cluster covers this file)
    #   cluster -> signal (cluster emits/consumes this signal)
    #   cluster -> cluster (shared file or signal)
    #
    # Primary queries:
    #   clusters_for_file(path)  — which clusters reference a given path
    #   files_for(id)            — files a cluster covers
    #   signals_for(id)          — signals a cluster emits/consumes
    #   related(id, depth:)      — clusters reachable via shared edges
    class EvidenceGraph
      Edge = Data.define(:from, :to, :kind)

      def initialize(registry = ClusterRegistry.new)
        @registry = registry
        @file_index = Hash.new { |h, k| h[k] = [] }
        @signal_index = Hash.new { |h, k| h[k] = [] }
        @edges = []
        build
      end

      def clusters_for_file(path)
        normalize = path.to_s.delete_prefix("/")
        @file_index.each_with_object([]) do |(file, ids), acc|
          acc.concat(ids) if file.end_with?(normalize) || normalize.end_with?(file)
        end.uniq.filter_map { |id| @registry.find(id) }
      end

      def files_for(id)
        @edges.select { |e| e.from == id.to_s && e.kind == :file }.map(&:to)
      end

      def signals_for(id)
        @edges.select { |e| e.from == id.to_s && e.kind == :signal }.map(&:to)
      end

      def related(id, depth: 1)
        visited = Set.new([id.to_s])
        frontier = [id.to_s]
        depth.times do
          next_frontier = []
          frontier.each do |cid|
            shared_via_files(cid).each do |neighbor|
              next if visited.include?(neighbor)
              visited.add(neighbor)
              next_frontier << neighbor
            end
          end
          frontier = next_frontier
          break if frontier.empty?
        end
        visited.delete(id.to_s)
        visited.filter_map { |cid| @registry.find(cid) }
      end

      def edge_count = @edges.size
      def node_count = @registry.all.size

      private

      def build
        @registry.all.each do |entry|
          extract_files(entry).each do |file|
            @edges << Edge.new(from: entry.id, to: file, kind: :file)
            @file_index[file] << entry.id
          end
          extract_signals(entry).each do |signal|
            @edges << Edge.new(from: entry.id, to: signal, kind: :signal)
            @signal_index[signal] << entry.id
          end
        end
      end

      def extract_files(entry)
        raw = entry.raw
        [
          *Array(raw["files"]),
          *Array(raw["master_hooks"]).map { |h| h.to_s.split("#").first }
        ].uniq.reject(&:empty?)
      end

      def extract_signals(entry)
        Array(entry.raw["signals"]).map(&:to_s).reject(&:empty?)
      end

      def shared_via_files(cluster_id)
        files_for(cluster_id).flat_map { |f| @file_index[f] }.uniq - [cluster_id]
      end
    end
  end
end

lib/ground/frontmatter.rb

# frozen_string_literal: true

require "yaml"

module Master
  module Ground
    # Parses YAML frontmatter from markdown; returns {meta: Hash, body: String}.
    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
  # WAL-mode SQLite ledger for fix quality, strategy outcomes, and RSI feedback events.
    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

      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

      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

      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

      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(<<~SQL, [fragment, limit])
        SELECT trigger, strategy, outcome, confidence
        FROM strategy_outcomes
        WHERE LOWER(trigger) LIKE ? AND outcome != 'failed'
        ORDER BY confidence DESC LIMIT ?
      SQL
      end

      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"
require_relative "memory/store"
require_relative "memory/search"
require_relative "memory/consolidate"

module Master
  module Ground
    # Persistent cross-session memory store with typed entries, recall, and consolidation.
    class Memory
      TTL_DAYS = 90
      CONSOLIDATE_THRESHOLD = 40
      SECONDS_PER_DAY = 86_400
      MAX_INJECT_CONTEXT_RATIO = 100
      MAX_INJECT_TOKEN_CAP = 2_000
      MAX_INJECT_TOKENS = [Master::CTX_WINDOW_SIZE / MAX_INJECT_CONTEXT_RATIO, MAX_INJECT_TOKEN_CAP].min.freeze
      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 Store
      include Search
      include Consolidate
    end
  end
end

lib/ground/memory/consolidate.rb

# frozen_string_literal: true

module Master
  module Ground
    class Memory
      module Consolidate
        def context_summary
          active = active_entries
          return if active.empty?

          lines = summary_lines(active)
          return if lines.empty?

          header = summary_header
          "#{header}\n#{lines.join("\n")}"
        end

        def consolidate!(agent: nil)
          return "nothing to consolidate" if @store.empty?

          entries, archived = archive_old_entries(Time.now.to_i)
          summarize_active_entries(agent) if agent
          "dreaming: #{entries.size} entries checked, #{archived} archived"
        rescue StandardError => e
          "consolidation error: #{e.message}"
        end

        private

        def active_entries
          @mutex.synchronize do
            @store.reject { |key, _| key.to_s.start_with?("archive/") || key == "_consolidated_summary" }
          end
        end

        def summary_lines(active)
          lines = []
          token_sum = 0
          current_type = nil
          ordered_entries(active).each do |key, value|
            type = entry_type(value)
            lines << "[#{type}]" if type != current_type
            current_type = type
            text = "- #{key}: #{entry_value(value)}"
            estimated_tokens = text.bytesize / Master::Trace::Session::TOKENS_PER_CHAR
            break if token_sum + estimated_tokens > MAX_INJECT_TOKENS

            lines << text
            token_sum += estimated_tokens
          end
          lines
        end

        def ordered_entries(active)
          grouped = active.group_by { |_, value| entry_type(value) }
          TYPES.flat_map do |type|
            (grouped[type] || []).sort_by { |_, value| -entry_timestamp(value) }
          end.first(MAX_INJECT_ENTRIES * 2)
        end

        def summary_header
          summary = recall("_consolidated_summary")
          header = summary ? "Memory (#{summary.to_s[0, 80]}):" : "Memory:"
          archived_n = @mutex.synchronize { @store.count { |key, _| key.to_s.start_with?("archive/") } }
          archived_n.positive? ? "#{header} [+#{archived_n} archived]" : header
        end

        def archive_old_entries(now)
          archived = 0
          entries = nil
          @mutex.synchronize do
            entries = @store.reject { |key, _| key.to_s.start_with?("archive/") }
            scored_entries(entries, now).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
          [entries, archived]
        end

        def scored_entries(entries, now)
          entries.map do |key, data|
            age_days = (now - entry_timestamp(data)) / 86_400.0
            { key: key, score: 1.0 / (1.0 + age_days / TTL_DAYS.to_f) }
          end
        end

        def summarize_active_entries(agent)
          active_text = active_entries.map { |key, value| "#{key}: #{entry_value(value)}" }.join("\n")
          return if 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

        def entry_type(value)
          value.is_a?(Hash) ? (value["type"] || "general") : "general"
        end

        def entry_value(value)
          value.is_a?(Hash) ? value["value"] : value
        end

        def entry_timestamp(value)
          value.is_a?(Hash) ? value["ts"].to_i : 0
        end
      end
    end
  end
end

lib/ground/memory/search.rb

# frozen_string_literal: true

module Master
  module Ground
    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 do |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 }
          end.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
    end
  end
end

lib/ground/memory/store.rb

# frozen_string_literal: true

module Master
  module Ground
    class Memory
      module Store
        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
            @store[key.to_s] = entry_for(key, value, type)
            persist
          end
        end

        def by_type(type)
          @mutex.synchronize do
            @store.select { |key, value| typed_entry?(key, value, type.to_s) }
          end
        end

        def type_counts
          @mutex.synchronize do
            @store.each_with_object(Hash.new(0)) do |(key, value), counts|
              next if archived_or_summary?(key)

              counts[value.is_a?(Hash) ? (value["type"] || "general") : "general"] += 1
            end
          end
        end

        def auto_save(text)
          return if text.to_s.empty?

          AUTO_SAVE_PATTERNS.each do |type, pattern|
            next unless (match = text.match(pattern))

            return remember_auto(type, match[1].strip)
          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 { |value| value.is_a?(Hash) ? value["value"] : value } }
        end

        private

        def entry_for(key, value, type)
          entry = { "value" => value.to_s, "ts" => Time.now.to_i, "type" => type }
          entry["vec"] = vec if (vec = Judge::Embeddings.embed("#{key} #{value}"))
          entry
        end

        def typed_entry?(key, value, type)
          value.is_a?(Hash) && value["type"] == type && !key.start_with?("archive/")
        end

        def archived_or_summary?(key)
          key.to_s.start_with?("archive/") || key == "_consolidated_summary"
        end

        def remember_auto(type, snippet)
          return if snippet.length < 3

          count = @mutex.synchronize { @store.keys.count { |key| key.start_with?("auto/#{type}/") } }
          key = "auto/#{type}/#{count + 1}"
          remember(key, snippet, type: type)
          key
        end

        def import_external!
          dir = File.join(@root, "data", "claude")
          return unless Dir.exist?(dir)

          Dir.glob(File.join(dir, "*.md")).each { |path| import_external_file(path) }
        rescue StandardError => e
          Master::Ground::Swallow.log(e, context: "memory.preload_claude_md")
        end

        def import_external_file(path)
          return if File.basename(path) == "MEMORY.md"

          key = "claude/#{File.basename(path, ".md")}"
          return if @store.key?(key)

          type, body = parse_frontmatter(path)
          remember(key, body, type: type) unless body.empty?
        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 |key, value|
            next if archived_or_summary?(key)
            next unless stale_entry?(value, cutoff)

            @store["archive/#{key}"] = @store.delete(key)
          end
        end

        def stale_entry?(value, cutoff)
          ts = value.is_a?(Hash) ? value["ts"].to_i : 0
          ts.positive? && ts < cutoff
        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
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?

        docs.values.map do |doc|
          score = score_doc(doc, terms)
          next if score <= 0

          doc.merge("score" => score)
        end.compact.sort_by { |doc| -doc["score"] }.first(limit)
      end

      def brief(query, limit: 5)
        rows = search(query, limit: limit)
        return "Memory search: no hits for #{query.inspect}." if rows.empty?

        lines = rows.map { |doc| "- #{doc['path']} score=#{format('%.2f', doc['score'])} title=#{doc['title']}" }
        "Memory search hits:\n#{lines.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

      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")
          commit_message = "auto: standing-order commit (#{out.lines.size} file(s))"
          _, st = Open3.capture2e("git", "-C", repo, "commit", "-m", commit_message)
          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
        phase_index = PHASES.index(current) || 0
        PHASES[[phase_index + 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
        Master::Ground::Swallow.log(e, context: "phase_gates.load_state")
        { "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/policy.rb

# frozen_string_literal: true

module Master
  module Ground
    # Lightweight shared behavior for the various *Policy modules in ground/.
    # Goal: reduce duplication across orchestration_policy, workflow_policy,
    # sandbox_policy, subagent_policy, tool_approval_policy, etc. (SINGULARITY + DENSITY).
    module Policy
      module_function

      def brief(description)
        "#{self.name}: #{description}"
      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
  # Collapses ProviderRegistry, ProviderHealth, and ProviderQuarantineManager into one call site (#396 item 2).
    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

      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 }

        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

require_relative "policy"

module Master
  module Ground
    module SandboxPolicy
      include Policy
      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? { |p| source.match?(p) }
        return Decision.new(mode: :ask, reason: "matched ask pattern") if
          ASK_PATTERNS.any? { |p| source.match?(p) }
        return Decision.new(mode: :allow, reason: "matched safe allow pattern") if
          ALLOW_PATTERNS.any? { |p| source.match?(p) }

        Decision.new(mode: :ask, reason: "unknown command risk")
      end

      def allowed?(command)
        decide(command).allow?
      end

      def brief
        Policy.brief("deny destructive/system commands, ask for pushes/deploy/db/reset, allow read-only git and test/lint")
      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

      def safe_call(context:, event_bus: nil, **meta)
        yield
      rescue StandardError => e
        log(e, context: context, event_bus: event_bus, **meta)
        nil
      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
          safe = SandboxPolicy.decide(command).allow?
          read_only = command.to_s.match?(/\A(?:git\s+(status|diff|log|show)|ls|find|grep|rg)\b/)
          safe && read_only ? :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
      # name: Symbol tool id; inputs: { key => :required | :optional }
      # output_shape: expected output keys (or :any); permission: :read | :write | :exec | :network
      # timeout_s: hard timeout (Integer); side_effects: :filesystem | :network | :git | :process | :none
      # category: :reach | :judge | :trace | :ground
      Contract = Data.define(
        :name,
        :inputs,
        :output_shape,
        :permission,
        :timeout_s,
        :max_retries,
        :side_effects,
        :category
      )

      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: %i[stdout stderr exit_code], permission: :exec, timeout_s: 60,
          max_retries: 0, side_effects: %i[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: %i[git filesystem], category: :reach
        ),
        llm_call: Contract.new(
          name: :llm_call, inputs: { prompt: :required, model: :optional, system: :optional },
          output_shape: %i[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: %i[stdout stderr exit_code], permission: :exec, timeout_s: 600,
          max_retries: 0, side_effects: %i[process filesystem], category: :reach
        ),
        repligen: Contract.new(
          name: :repligen, inputs: { args: :optional },
          output_shape: %i[stdout stderr exit_code], permission: :network, timeout_s: 900,
          max_retries: 0, side_effects: %i[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/xi.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 = [
        %i[dry three_duplications abstract high],
        %i[kiss complexity_over_10 simplify high],
        %i[yagni unused remove medium],
        %i[solid coupling_over_5 decouple critical],
        %i[composition deep_inheritance compose medium],
        %i[evidence assumption validate critical],
        %i[reversible irreversible add_rollback critical],
        %i[explicit implicit make_explicit high],
        %i[orthogonal coupled split high],
        %i[minimalism bloat subtract medium],
        %i[clarity synonym unify medium],
        %i[flatten wrapper flatten high],
        %i[pola surprise make_predictable high],
        %i[unix multi_responsibility one_thing high],
        %i[anti_divitis div_soup semantic_html medium],
        %i[anti_sectionitis scattered_sections consolidate high],
        %i[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/harness/registry.rb

# frozen_string_literal: true

require "yaml"
require_relative "../master_paths"

module Harness
  class Registry
    def initialize(dir: MasterPaths.data("harnesses"))
      @dir = dir
    end

    def all
      Dir.glob(File.join(@dir, "*.yml")).to_h do |path|
        [File.basename(path, ".yml"), YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: true)]
      end
    end

    def fetch(name)
      all.fetch(name.to_s)
    end

    def coverage_for(path)
      all.select do |_name, spec|
        Array(spec["paths"]).any? { |prefix| path.start_with?(prefix) }
      end
    end

    def missing_for(paths)
      Array(paths).select { |path| coverage_for(path).empty? }
    end
  end
end

lib/history/fossils.rb

# frozen_string_literal: true

require "open3"
require_relative "../master_paths"

module History
  Fossil = Struct.new(:commit, :file, :line, keyword_init: true) do
    def to_h = { commit: commit, file: file, line: line }
  end

  class Fossils
    VALUABLE_PATTERNS = [
      /class\s+\w+/,
      /module\s+\w+/,
      /def\s+\w+/,
      /CREATE TABLE/i,
      /add_index/i,
      /foreign_key/i,
      /TODO|FIXME|XXX/,
      /OPENROUTER|SECRET|TOKEN|API_KEY/,
      /public_key|private_key/i,
      /rcctl|relayd|httpd|pfctl|doas/,
      /Stimulus|Turbo|Rails|Falcon/,
      /MASTER|DEPLOY|converge|council|critique/i
    ].freeze

    def initialize(root: MasterPaths.repo, window: ENV.fetch("HISTORY_WINDOW", "--since=90.days.ago"))
      @root = root
      @window = window
    end

    def scan(*paths)
      paths = ["."] if paths.empty?
      parse(git_log(*paths))
    end

    private

    attr_reader :root, :window

    def git_log(*paths)
      stdout, status = Open3.capture2e(
        "git", "log", window, "--find-renames", "--find-copies", "--diff-filter=DMR", "--patch", "--", *paths,
        chdir: root
      )
      raise "git history audit failed: #{stdout}" unless status.success?
      stdout
    end

    def parse(log)
      current_commit = nil
      current_file = nil
      hits = []

      log.each_line do |line|
        if line.start_with?("commit ")
          current_commit = line.split.fetch(1)
          current_file = nil
          next
        end

        if line.start_with?("diff --git ")
          current_file = line.split.last&.delete_prefix("b/")
          next
        end

        next unless line.start_with?("-")
        next if line.start_with?("---")

        text = line.delete_prefix("-").strip
        next if text.empty?
        next unless VALUABLE_PATTERNS.any? { |pattern| text.match?(pattern) }

        hits << Fossil.new(commit: current_commit, file: current_file, line: text[0, 220])
      end

      hits
    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: -> { { static: static_prompt, dynamic: dynamic_prompt } })
      end

      def wire_constitution(constitution) = @constitution = constitution

      def chat(message, image: nil, 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:, image: image, &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
      private :prepare_chat_turn, :check_rate_limit

      def ask(prompt, context: nil, operation: nil, image: 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, image: image)
        raise StandardError, result.message if result.is_a?(Master::Result::Err)
        result.to_s
      end

      def ask_once(prompt, system: nil, model: nil, image: nil)
        messages = [{ role: "user", content: prompt.to_s }]
        result   = @dispatcher.send_with_cache(model || self.model, messages, system:, stream: false, image: image)
        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
        image = ctx[:image] if ctx.respond_to?(:[]) && ctx.key?(:image)
        with_task_type(task_type) do
          if on_chunk
            chat(ctx[:message].to_s, image: image, stream: true, &on_chunk)
          else
            chat(ctx[:message].to_s, image: image)
          end
        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 static_prompt
        parts = []
        parts << @constitution.system_prompt if @constitution && !@constitution.empty?
        parts << @personality.system_prompt if @personality
        parts.compact.join("\n\n").then { |s| s.empty? ? nil : s }
      end

      def dynamic_prompt
        parts = []
        parts << "Current task: #{@session.topic}" if @session.respond_to?(:topic) && @session.topic
        parts << @code_index.summary if @code_index&.built?
        parts << @memory.context_summary if @memory&.context_summary
        parts.compact.join("\n\n").then { |s| s.empty? ? nil : s }
      end

      def system_prompt
        [static_prompt, dynamic_prompt].compact.join("\n\n").then { |s| s.empty? ? nil : s }
      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:, image: nil, &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:, image: image, &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
        return strong_first(chain) if bias == :strong
        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

      def strong_first(chain)
        strong = chain.select { |m| @model_router.tier_for_model(m) == "strong" }
        rest   = chain.reject { |m| @model_router.tier_for_model(m) == "strong" }
        strong.empty? ? chain : (strong + 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
  # 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

      # Returns [file, line] for the first symbol matching name, or nil.
      def lookup(name)
        with_built_index do
          hit = find_locked(name).first
          hit ? [relativize(hit.file), hit.line] : nil
        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/production_dna.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 Dilla-style timing is proposed, call Master::Voice::Dilla for swing, nudge, chord, and preset data",
            ]
          }
        }.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"
          [dilla_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 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 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, personas: nil)
          active = personas ? @personas.select { |p| personas.include?(p.name) } : @personas
          active = @personas if active.empty?
          return Master::Result.err("council: no personas configured", category: :validation) if active.empty?

          feedback = @mode == :sequential ? collect_sequential(code, context, active) : collect_parallel(code, context, active)
          effective_quorum = [MIN_QUORUM, @personas.size].min
          if feedback.size < effective_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

          veto_result = enforce_veto(feedback)
          return veto_result if veto_result

          append_judge_synthesis(feedback, code, context)
          publish_confidence(feedback)
          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, personas = @personas)
          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, personas = @personas)
          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

        # Returns a Result::Err if any veto-eligible persona issued a VETO, else nil.
        def enforce_veto(feedback)
          vetoes = feedback.select { |f| f[:veto_role] && veto_text?(f[:feedback]) }
          return nil if vetoes.empty?
          veto = vetoes.first
          @bus&.publish(:council_veto, veto)
          Master::Result.err("council: veto from #{veto[:persona]}\n#{veto[:feedback]}", category: :validation)
        end

        # Append judge synthesis entry to feedback when judge is enabled.
        def append_judge_synthesis(feedback, code, context)
          synthesis = @judge_enabled ? judge(feedback, code, context) : nil
          return unless synthesis
          @bus&.publish(:council_synthesis, synthesis: synthesis)
          feedback << { persona: "Judge", role: "Synthesis", veto_role: false,
                        axiom: nil, feedback: synthesis }
        end

        # Compute mean confidence and publish the council_confidence event.
        def publish_confidence(feedback)
          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)
        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)
          model = persona.respond_to?(:model) ? persona.model : nil
          response = model ? @agent.ask_once(prompt, model: model) : @agent.ask(prompt)
          entry = { persona: persona.name, role: persona.role,
                    veto_role: veto_role?(persona), axiom: primary_axiom(persona),
                    model: 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/council/selector.rb

# frozen_string_literal: true

module Master
  module Judge
    module Council
      # Selects the minimal relevant persona set for a task.
      # Security, Reliability, Maintainer always veto-capable when present.
      class Selector
        ALWAYS_INCLUDED = ["Maintainer"].freeze

        TASK_PERSONAS = {
          mobile_ui: ["User Advocate", "Accessibility", "Web Designer", "Performance", "Google CSS Engineer"],
          ui: ["User Advocate", "Accessibility", "NNGroup UX Researcher", "Graphic Designer"],
          auth_mutation: ["Security", "Reliability", "Maintainer", "Ethics & Policy"],
          security_audit: ["Security", "Reliability", "Maintainer", "Skeptic"],
          architecture: ["Architect", "Maintainer", "Reliability", "Skeptic"],
          migration: ["Architect", "Maintainer", "Data Steward", "Reliability"],
          performance: ["Performance", "Maintainer", "QA Engineer"],
          data: ["Data Steward", "Maintainer", "Reliability"],
          sonic: ["Hip-Hop Producer", "Electronic Music Producer", "Pragmatist"],
          product: ["Product Strategist", "User Advocate", "Pragmatist"],
          docs: ["Maintainer", "Layperson", "QA Engineer"],
          code_review: ["Maintainer", "Skeptic", "QA Engineer", "Architect"],
          destructive: ["Security", "Reliability", "Maintainer", "Architect", "Skeptic"]
        }.freeze

        RISK_PERSONAS = {
          critical: ["Security", "Reliability", "Maintainer", "Architect", "Skeptic"],
          high: ["Security", "Reliability", "Maintainer"],
          medium: ["Maintainer", "Skeptic"],
          low: ["Maintainer"]
        }.freeze

        def self.for(task: nil, risk: nil, available: nil)
          new(available:).select(task:, risk:)
        end

        def initialize(available: nil)
          @available = normalize_available(available)
        end

        def select(task: nil, risk: nil)
          names = base_personas(task, risk)
          names = (names + ALWAYS_INCLUDED).uniq
          @available ? names.select { |n| @available.include?(n) } : names
        end

        private

        def base_personas(task, risk)
          task_set = task ? TASK_PERSONAS[task.to_sym] : nil
          risk_set = risk ? RISK_PERSONAS[risk.to_sym] : nil
          return task_set || risk_set || ALWAYS_INCLUDED if task_set.nil? || risk_set.nil?
          (task_set + risk_set).uniq
        end

        def normalize_available(available)
          return nil unless available
          Array(available).map(&:to_s).to_set
        end
      end
    end
  end
end

lib/judge/council/sound_critique.rb

# frozen_string_literal: true

module Master
  module Judge
    module Council
      class SoundCritique
        COUNCIL_PATH = File.join(Master::ROOT, "data", "council.yml").freeze
        MAX_FILE_BYTES = 24_576

        SOUND_PANEL = [
          "Electronic Music Producer",
          "Hip-Hop Producer",
          "User Advocate",
          "Accessibility",
          "Layperson",
          "Skeptic"
        ].freeze

        def initialize(agent:, event_bus: nil)
          @agent = agent
          @bus = event_bus
        end

        def run
          preset = load_preset
          panel = build_panel(preset)
          payload = build_payload(preset)
          @bus&.publish(:sound_critique_start, files: payload[:files], personas: panel.map(&:name))

          deliberation = Deliberation.new(personas: panel, agent: @agent, event_bus: @bus, judge_enabled: true)
          result = deliberation.review(payload[:combined], context: sound_context)
          return result unless result.ok?

          feedback = result.value!
          ideas = Ideation.new(agent: @agent, event_bus: @bus).ideate(
            "Generate concrete improvements for MASTER sound design, voice playback, sonic timing, and audio feedback.",
            constraints: sound_constraints,
            cycles: (preset.dig("cycles") || 2).to_i
          )
          return ideas if ideas.err?

          picks = cherry_pick(feedback, ideas.value.fetch(:final, ""))
          @bus&.publish(:sound_critique_done, cherry_picks: picks.size)
          Master::Result.ok({ feedback: feedback, ideas: ideas.value, cherry_picks: picks })
        end

        private

        def load_preset
          data = File.exist?(COUNCIL_PATH) ? (Master.load_yaml(COUNCIL_PATH) || {}) : {}
          data.dig("presets", "sound_critique") || {}
        end

        def build_panel(preset)
          all = Personas.load
          names = Array(preset["panel"] || SOUND_PANEL).map(&:downcase)
          panel = all.select { |persona| names.include?(persona.name.downcase) }
          panel.empty? ? Personas::DEFAULTS : panel
        end

        def build_payload(preset)
          files = Array(preset["files"]).any? ? preset["files"] : default_files
          combined = files.filter_map do |relative_path|
            path = File.join(Master::ROOT, relative_path)
            next unless File.exist?(path)

            raw = File.read(path, encoding: "utf-8")
            raw = raw.byteslice(0, MAX_FILE_BYTES) + "\n... [truncated]" if raw.bytesize > MAX_FILE_BYTES
            "file: #{relative_path}\n#{raw}"
          end.join("\n\n")

          { combined: combined, files: files }
        end

        def default_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/production_dna.rb
          ]
        end

        def sound_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.
          #{Deliberation.quality_brief(:sound)}
          #{sonitex_brief}
          #{dilla_brief}
          #{tts_lofi_brief}
          Return shippable, reversible fixes, not vague mood boards.
        CTX
        end

        def sonitex_brief
          "Lo-fi chain: prefer cumulative subtle degradation; document any SoX/ffmpeg gaps as opt-in."
        end

        def dilla_brief
          if defined?(Master::Voice::Dilla)
            Master::Voice::Dilla.brief
          elsif defined?(Master::Voice::ProductionDna)
            Master::Voice::ProductionDna.brief
          else
            "Production DNA unavailable; keep timing human, restrained, and non-quantized when musical."
          end
        rescue StandardError => e
          "Dilla production profile failed to load: #{e.message}."
        end

        def tts_lofi_brief
          "TTS policy: edge-tts primary (ms-MY-OsmanNeural via bin/tts-worker); espeak hard fallback; clean audio default; lofi effects opt-in only."
        end

        def sound_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, document it as opt-in and shell out via a dedicated worker, not inline",
            "when Dilla-style timing is proposed, call Master::Voice::Dilla for swing, nudge, chord, and preset data",
            "TTS backend is edge-tts via bin/tts-worker; espeak is the only fallback; no other TTS backends"
          ]
        end

        def cherry_pick(feedback, final_ideas)
          feedback_text = feedback.map { |entry| entry[:feedback].to_s }.join("\n")
          final_ideas.to_s.lines.map(&:strip).reject(&:empty?).sort_by do |line|
            -text_overlap(line, feedback_text)
          end.first(12)
        end

        def text_overlap(a, b)
          left = a.downcase.scan(/\w+/).to_set
          right = b.downcase.scan(/\w+/).to_set
          return 0.0 if left.empty? || right.empty?
          (left & right).size.to_f / left.size
        end
      end
    end
  end
end

lib/judge/council/ui_critique.rb

# frozen_string_literal: true

module Master
  module Judge
    module Council
      class UiCritique
        COUNCIL_PATH = File.join(Master::ROOT, "data", "council.yml").freeze
        WEB_ROOT = File.join(Master::ROOT, "web").freeze
        MAX_FILE_BYTES = 32_768

        UI_PANEL = %w[
          Architect Graphic\ Designer Web\ Designer Electronic\ Music\ Producer
          Hip-Hop\ Producer Google\ CSS\ Engineer NNGroup\ UX\ Researcher
          Accessibility User\ Advocate Layperson Skeptic
        ].freeze

        def initialize(agent:, event_bus: nil)
          @agent = agent
          @bus = event_bus
        end

        def run
          preset = load_preset
          panel = build_panel(preset)
          payload = build_payload(preset)
          @bus&.publish(:ui_critique_start, 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: ui_context)
          return result unless result.ok?

          feedback = result.value!
          ideas = Ideation.new(agent: @agent, event_bus: @bus).ideate(
            "Generate concrete multi-solution improvements for this web UI. Produce 3 distinct solution directions per issue found.",
            constraints: design_constraints,
            cycles: (preset.dig("cycles") || 1).to_i
          )

          cherry = cherry_pick(feedback, ideas)
          @bus&.publish(:ui_critique_done, cherry_picks: cherry.size)
          Master::Result.ok({ feedback: feedback, ideas: ideas, cherry_picks: cherry })
        end

        private

        def load_preset
          data = File.exist?(COUNCIL_PATH) ? (Master.load_yaml(COUNCIL_PATH) || {}) : {}
          data.dig("presets", "ui_critique") || {}
        end

        def build_panel(preset)
          all = Personas.load
          names = Array(preset["panel"]).map(&:downcase)
          return all if names.empty?

          all.select { |p| names.include?(p.name.downcase) }
             .tap { |panel| panel.replace(Personas::DEFAULTS) if panel.empty? }
        end

        def build_payload(preset)
          files = Array(preset["files"]).any? ? preset["files"] : default_files
          combined = files.filter_map do |rel|
            path = File.join(Master::ROOT, rel)
            next unless File.exist?(path)

            raw = File.read(path, encoding: "utf-8")
            raw = raw.byteslice(0, MAX_FILE_BYTES) + "\n... [truncated]" if raw.bytesize > MAX_FILE_BYTES
            "file: #{rel}\n#{raw}"
          end.join("\n\n")

          { combined: combined, files: files }
        end

        def default_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
          ]
        end

        def ui_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.
          #{Deliberation.quality_brief(:design)}
          #{platform_profile_brief}
          Return shippable, reversible fixes with measurable reasons.
        CTX
        end

        def platform_profile_brief
          if defined?(Master::Design::PlatformProfiles)
            [
              Master::Design::PlatformProfiles.brief(:brutal_minimal),
              Master::Design::PlatformProfiles.brief(:medium),
              Master::Design::PlatformProfiles.brief(:new_yorker)
            ].join("\n")
          else
            "Platform design profiles unavailable; default to content-first measurable critique."
          end
        rescue StandardError => e
          "Platform profile policy failed to load: #{e.message}."
        end

        def design_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"
          ]
        end

        def cherry_pick(feedback, ideas)
          texts = feedback.map { |f| f[:feedback].to_s }
          idea_lines = ideas.to_s.lines.map(&:strip).reject(&:empty?)
          scored = idea_lines.map do |line|
            score = texts.sum { |t| text_overlap(line, t) }
            [line, score]
          end
          scored.sort_by { |_, s| -s }.first(12).map(&:first)
        end

        def text_overlap(a, b)
          wa = a.downcase.scan(/\w+/).to_set
          wb = b.downcase.scan(/\w+/).to_set
          (wa & wb).size.to_f / ([wa.size, wb.size, 1].max)
        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 => e
        Master::Ground::Swallow.log(e, context: "Embeddings.ollama_alive?")
        @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 = begin
          JSON.parse(res.body)
        rescue StandardError => e
          Master::Ground::Swallow.log(e, context: "Embeddings.ollama_embed")
          nil
        end
        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"
require "tempfile"
require "base64"
require "securerandom"

module Master
  module Judge
    class LLMDispatcher
      COST_PER_TOKEN = 0.000_015
      CACHE_READ_RATIO = 0.10
      CACHE_WRITE_RATIO = 1.25
      CACHE_WINDOW = 4
      REACT_MAX_STEPS = 8
      MS_PER_SECOND = 1000
      CLAUDE_RE = /\Aclaude-|anthropic\/claude/i.freeze
      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,
        Reach::Repligen => Reach::LLM::Repligen,
        Reach::Postpro => Reach::LLM::Postpro
      }.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, image: nil, &blk)
        started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
        if !image.nil? && image != ""
          # auto bias to vision free models (e.g. gemini-2.0-flash-exp:free) when image in ctx
          unless selected_model.to_s =~ /gemini|vision|claude-3|gpt-4o/
            selected_model = "z-ai/glm-4.5-air:free"
          end
        end
        cache_key = cache_key_for(messages.last[:content], messages[0...-1], selected_model)
        result = breaker_for(selected_model).call(estimate_cost(messages.last[:content])) do
          @cache.fetch(cache_key, selected_model) do
            send_llm_request(selected_model, messages, system:, stream:, image: image, &blk)
          end
        end
        record_provider_result(selected_model, result, started)
        result
      rescue Reach::CircuitBreaker::CircuitError => err
        record_provider_outcome(selected_model, err.category, latency_ms: elapsed_ms(started), error: err.message)
        Result.err(redact_secrets(err.message), category: err.category)
      rescue StandardError => err
        record_provider_outcome(selected_model, :llm_call_failure, latency_ms: elapsed_ms(started), error: err.message)
        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)
        error_message = err.message.to_s
        error_message.match?(/missing configuration/i) ||
          error_message.match?(/api[_\- ]?key/i) ||
          error_message.match?(/unauthorized/i) ||
          error_message.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)
      def claude_model?(model_id)     = CLAUDE_RE.match?(model_id.to_s)

      private

      def system_prompt
        result = @system_prompt_proc.call
        return result unless result.is_a?(Hash)
        [result[:static], result[:dynamic]].compact.join("\n\n").then { |s| s.empty? ? nil : s }
      end

      def send_llm_request(selected_model, messages, system: nil, stream: false, image: nil, &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:, image: image, &blk)
        end
        send_ruby_llm(selected_model, messages, sys:, stream:, image: image, &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:, image: nil, &blk)
        react_sys = build_react_system(sys)
        history   = messages.dup
        last      = nil

        REACT_MAX_STEPS.times do |step|
          img = (step.zero? ? image : nil)
          result = send_ruby_llm(selected_model, history, sys: react_sys, stream: step.zero? ? stream : false, image: img, &(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:, image: nil, &blk)
        chat_session = RubyLLM.chat(model: selected_model)
        final_sys = build_final_system(selected_model, sys)
        chat_session.with_instructions(final_sys) if final_sys

        # Add prior context as plain text (vision is typically only for the current user turn)
        messages[0...-1].each do |message_entry|
          chat_session.add_message(role: message_entry[:role].to_s, content: message_entry[:content].to_s)
        end

        last_entry = messages.last || {}
        last_text = last_entry[:content].to_s

        available_tools = llm_tools(selected_model)
        chat_session.with_tools(*available_tools) unless available_tools.empty?

        ask_arg = last_text
        temp_file = nil
        if image && ( (!image[:path].to_s.empty? && File.file?(image[:path])) || !image[:data].to_s.empty? )
          # Prefer disk :path from chat token meta (postpro'd uploads). Robust Tempfile fallback for direct data.
          # Always ensure cleanup with ensure. Unique temp name.
          if !image[:path].to_s.empty? && File.file?(image[:path])
            attachment = RubyLLM::Attachment.new(image[:path], filename: (image[:name].to_s.empty? ? File.basename(image[:path]) : image[:name].to_s))
          else
            ext = (image[:mime].to_s =~ /png/i ? ".png" : (image[:mime].to_s =~ /webp/i ? ".webp" : ".jpg"))
            temp_file = Tempfile.new(["master_vision_#{SecureRandom.hex(4)}", ext])
            temp_file.binmode
            temp_file.write(Base64.strict_decode64(image[:data]))
            temp_file.rewind
            temp_file.close
            attachment = RubyLLM::Attachment.new(temp_file.path, filename: (image[:name].to_s.presence || "photo#{ext}"))
          end
          content = RubyLLM::Content.new(text: last_text, attachments: [attachment])
          ask_arg = content
        end

        begin
          reply = if stream && blk
                    chat_session.ask(ask_arg) { |chunk| blk.call(chunk.content.to_s) if chunk.content }
          else
            chat_session.ask(ask_arg)
          end
          record_usage(reply, selected_model)
          res = Result.ok(extract_response(reply, selected_model))
          res
        ensure
          if temp_file
            begin
              temp_file.close unless temp_file.closed?
              temp_file.unlink if File.exist?(temp_file.path)
            rescue StandardError
              # best effort cleanup of vision temp
            end
          end
        end
      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
        cached = reply.respond_to?(:cached_tokens) ? reply.cached_tokens.to_i : 0
        cache_write = reply.respond_to?(:cache_creation_tokens) ? reply.cache_creation_tokens.to_i : 0
        tokens = input + output
        if tokens.zero? && reply.respond_to?(:content)
          tokens = Master::Trace::Session.estimate_tokens(reply.content)
          return if tokens.zero?
          @session.record_cost((tokens * COST_PER_TOKEN).round(6), model:, tokens:)
          return
        end
        return if tokens.zero?
        regular = [input - cached - cache_write, 0].max
        cost = ((regular * COST_PER_TOKEN) +
                (cached * COST_PER_TOKEN * CACHE_READ_RATIO) +
                (cache_write * COST_PER_TOKEN * CACHE_WRITE_RATIO) +
                (output * COST_PER_TOKEN)).round(6)
        @session.record_cost(cost, model:, tokens:)
        @bus&.publish("llm:cost", model:, cost:, tokens:, cached:, cache_write:)
        @bus&.publish("cache:hit", model:, cached:, cache_write:) if cached.positive? || cache_write.positive?
      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)
        content  = reply.content.to_s
        thinking = reply.respond_to?(:thinking) ? reply.thinking&.text.to_s.strip : ""
        if NEMOTRON3_RE.match?(selected_model) && !thinking.empty?
          return content.empty? ? thinking : "#{content}\n\n<think>\n#{thinking}\n</think>"
        end
        content.empty? && !thinking.empty? ? thinking : content
      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 build_final_system(selected_model, sys)
        return sys unless claude_model?(selected_model)
        raw = @system_prompt_proc.call
        if raw.is_a?(Hash) && raw[:static]
          static_text = nemotron_system_prompt(selected_model, raw[:static])
          blocks = [{ type: "text", text: static_text, cache_control: { type: "ephemeral" } }]
          blocks << { type: "text", text: raw[:dynamic] } if raw[:dynamic]
          RubyLLM::Content::Raw.new(blocks)
        else
          base = nemotron_system_prompt(selected_model, sys)
          return base unless base.is_a?(String)
          RubyLLM::Content::Raw.new([{ type: "text", text: base, cache_control: { type: "ephemeral" } }])
        end
      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 elapsed_ms(started)
        return 0 unless started
        ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * MS_PER_SECOND).round
      end

      def record_provider_result(model, result, started)
        latency_ms = elapsed_ms(started)
        if Result.wrap(result).ok?
          record_provider_outcome(model, :success, latency_ms:)
        else
          record_provider_outcome(model, failure_status(result), latency_ms:,
                                  error: result.respond_to?(:message) ? result.message : result.to_s)
        end
      end

      def failure_status(result)
        cat = result.respond_to?(:category) ? result.category : :provider_error
        case cat
        when :rate_limit then :rate_limit
        when :timeout then :timeout
        when :budget then :quota_exceeded
        when :provider_error then :provider_error
        else :failure
        end
      end

      def record_provider_outcome(model, status, latency_ms: nil, error: nil)
        @model_router&.record_provider_outcome(model:, status:, latency_ms:, error:)
      rescue StandardError => e
        @bus&.publish("provider_health:record_error", model:, error: e.message)
      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 !visitor && !Fiber[:master_elevated] && meta["tier"] == "dangerous"
          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 => e
        Master::Ground::Swallow.log(e, context: "Reflexion.circuit_open?")
        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
        Master::Ground::Swallow.log(e, context: "Reflexion.critique")
        "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 "open3"
require "set"
require "time"

module Master
  module Judge
  # RepoEcology converts repo-gardening principles into executable analysis.
  # It never deletes or rewrites; it emits evidence for later governed changes.
  # Full integration into Judge + co_change_rule wiring deferred; requires council review.
    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
      CO_CHANGE_COMMITS = 200
      MAX_CO_CHANGE_PAIRS = 20
      CO_CHANGE_MIN_COUNT = 2
      COMMIT_SEPARATOR = "===commit===".freeze
      FileRecord = Data.define(:path, :full_path, :basename, :dirname, :ext, :bytes, :lines,
                               :symbol_count, :tokens, :digest, :signature, :inbound_refs)

      def initialize(root:, event_bus: nil, ignore_dirs: DEFAULT_IGNORE_DIRS, code_index: nil)
        @root = File.expand_path(root)
        @bus = event_bus
        @ignore_dirs = ignore_dirs.to_set
        @code_index = code_index
        @co_change_graph = nil
        @graph_mutex = Mutex.new
      end

      # Returns a file => Set<file> adjacency graph built from git co-change history.
      # Edges exist between files that changed together >= CO_CHANGE_MIN_COUNT times.
      # Memoized; call reindex(path) or reset_graph to invalidate.
      def co_change_graph
        @graph_mutex.synchronize { @co_change_graph ||= build_co_change_graph }
      end

      # Re-parse a single file's symbols in the code_index and invalidate the graph.
      def reindex(path)
        @code_index&.reindex(path)
        @graph_mutex.synchronize { @co_change_graph = nil }
      end

      # Returns a snapshot summary hash for the dmesg boot line.
      def snapshot
        graph = co_change_graph
        symbol_count = @code_index ? (@code_index.size rescue 0) : 0
        file_count = graph.size
        edge_count = graph.values.sum(&:size) / 2
        hotspots = graph.max_by(5) { |_, peers| peers.size }
                        .map { |file, peers| { file:, coupled_to: peers.size } }
        { symbols: symbol_count, files: file_count, edges: edge_count, hotspots: }
      end

      def scan(path: nil)
        base = path ? File.expand_path(path, @root) : @root
        files = collect_files(base)
        records = files.map { |file| analyze_file(file) }
        graph = co_change_graph
        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),
          co_change_pairs: co_change_pairs(graph)
        }
        @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 (#{item[:symbol_count]} symbols)"
        })
        lines.concat(render_section("Co-change pairs (hidden coupling)", report[:co_change_pairs]) { |item|
          "#{item[:a]}#{item[:b]} (#{item[:count]} commits)"
        })
        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,}/)
        symbol_count = @code_index ? (@code_index.symbols_in(rel).size rescue 0) : 0
        FileRecord.new(
          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,
          symbol_count: symbol_count,
          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) }
        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(&:basename)
               .filter_map do |basename, group|
          next if group.size < DUPLICATE_BASENAME_LIMIT
          { basename:, count: group.size, paths: group.map(&:path).sort }
        end.sort_by { |item| [-item[:count], item[:basename]] }
      end

      def similar_clusters(records)
        records.compact.group_by(&:signature)
               .filter_map do |sig, group|
          next if sig.empty? || group.size < 2
          next if group.map(&:digest).uniq.size == group.size && group.size < 3
          { signature: sig, count: group.size, paths: group.map(&: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(&: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, symbol_count: record.symbol_count } }
               .sort_by { |item| -item[:lines] }
               .first(25)
      end

      def co_change_pairs(graph = co_change_graph)
        pair_counts = Hash.new(0)
        graph.each do |file_a, peers|
          peers.each do |file_b, count|
            key = [file_a, file_b].sort
            pair_counts[key] = count if file_a < file_b
          end
        end
        pair_counts.sort_by { |_, count| -count }
                   .first(MAX_CO_CHANGE_PAIRS)
                   .map { |(a, b), count| { a:, b:, count: } }
      rescue StandardError => e
        @bus&.publish("repo_ecology:co_change_error", error: e.message)
        []
      end

      def build_co_change_graph
        out, status = Open3.capture2e("git", "-C", @root, "log", "--name-only",
                                      "--pretty=format:#{COMMIT_SEPARATOR}",
                                      "-#{CO_CHANGE_COMMITS}")
        return {} unless status.success?
        pair_counts = Hash.new(0)
        out.split(COMMIT_SEPARATOR).each do |chunk|
          files = chunk.lines.map(&:strip).reject(&:empty?).uniq
          next if files.size < 2
          files.combination(2) { |a, b| pair_counts[[a, b].sort] += 1 }
        end
        graph = Hash.new { |h, k| h[k] = {} }
        pair_counts.each do |(a, b), count|
          next if count < CO_CHANGE_MIN_COUNT
          graph[a][b] = count
          graph[b][a] = count
        end
        graph.transform_values(&:freeze).freeze
      rescue StandardError => e
        @bus&.publish("repo_ecology:co_change_error", error: e.message)
        {}
      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
        JS_EXTS = %w[.js .ts .jsx .tsx].freeze
        STYLE_EXTS = %w[.css .scss].freeze

        Result = Struct.new(:path, :changed, :transforms, keyword_init: true)

        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?
          out = collapse_blank_lines(out)
          out = strip_trailing_whitespace(out)
          out = freeze_mutable_constants(out)  if ruby?
          out = add_strict_mode(out)           if shell?
          out = add_html_lang(out)             if html?
          out = add_meta_charset(out)          if html?
          out = add_lazy_loading(out)          if html?
          out = replace_unreassigned_var(out)  if javascript?
          out = convert_for_in_arrays(out)     if javascript?
          out = convert_string_concat(out)     if javascript?
          out = convert_optional_chaining(out) if javascript?
          out = remove_immediate_dead_code(out)
          out = add_trailing_commas(out)
          out = logical_properties(out)        if style?
          changed = out != @source
          Result.new(path: @path, changed:, transforms: @transforms)
            .tap { write_back(out) if changed }
        end

        private

        def add_frozen_header(src)
          return src if src.start_with?(FROZEN_HEADER)

          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

        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|
            line_index = lineno - 1
            next unless line_index < lines.size

            lines[line_index] = lines[line_index].sub(/\brescue\b(?!\s+\w)/, "rescue StandardError")
          end
          @transforms << :bare_rescue
          lines.join
        end

        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 collapse_blank_lines(src)
          out = src.gsub(/(\n\n)\n+/, "\\1")
          @transforms << :collapse_blank_lines if out != src
          out
        end

        def strip_trailing_whitespace(src)
          out = src.gsub(/[ \t]+(?=\n|\z)/, "")
          @transforms << :trailing_whitespace if out != src
          out
        end

        MUTABLE_CONST_RE = /^(\s*[A-Z][A-Z_]*\s*=\s*[\[{])(.*)(?<!\.freeze)\s*$/.freeze

        def freeze_mutable_constants(src)
          changed = false
          out = src.lines.map do |line|
            next line unless line.match?(MUTABLE_CONST_RE)
            next line if line.match?(/\.freeze\s*$/)
            next line if line.strip.end_with?(",", "(", "\\")

            changed = true
            line.chomp.rstrip + ".freeze\n"
          end.join
          @transforms << :freeze_constants if changed
          out
        end

        STRICT_MODE = "set -euo pipefail\n"

        def add_strict_mode(src)
          return src if src.include?(STRICT_MODE) || src.include?("set -e")

          lines = src.lines
          shebang_idx = lines.index { |line| line.start_with?("#!") }
          return src unless shebang_idx

          lines.insert(shebang_idx + 1, STRICT_MODE)
          @transforms << :strict_mode
          lines.join
        end

        def add_html_lang(src)
          return src if src.match?(/<html\b[^>]*\blang=/)

          out = src.sub(/<html\b(?=[^>]*>)/) { |match| match.rstrip + ' lang="en"' }
          @transforms << :html_lang if out != src
          out
        end

        def add_lazy_loading(src)
          out = src.gsub(/<img\b(?=[^>]*>)(?![^>]*\bloading=)/) { |match| match.rstrip + ' loading="lazy"' }
          @transforms << :lazy_images if out != src
          out
        end

        def add_meta_charset(src)
          return src if src.match?(/<meta\s[^>]*charset=/i)

          out = src.sub(/<head\b[^>]*>/, "\\0\n<meta charset=\"UTF-8\">")
          @transforms << :meta_charset if out != src
          out
        end

        def replace_unreassigned_var(src)
          declared = src.scan(/\bvar\s+([A-Za-z_$][\w$]*)\b/).flatten
          reassigned = declared.select { |name| src.match?(/(?<!\bvar\s)(?<!\bconst\s)(?<!\blet\s)\b#{Regexp.escape(name)}\s*=(?!=)/) }
          out = src.gsub(/\bvar\s+([A-Za-z_$][\w$]*)/) do |match|
            reassigned.include?(Regexp.last_match(1)) ? match : match.sub("var", "const")
          end
          @transforms << :no_var if out != src
          out
        end

        def convert_for_in_arrays(src)
          changed = false
          out = src.gsub(/for\s*\(\s*const\s+([A-Za-z_$][\w$]*)\s+in\s+([A-Za-z_$][\w$]*(?:List|Array|Arr|s))\s*\)/) do
            changed = true
            "for (const #{Regexp.last_match(1)} of #{Regexp.last_match(2)})"
          end
          @transforms << :for_of if changed
          out
        end

        def convert_string_concat(src)
          changed = false
          out = src.gsub(/(['"])([^'"`\n]*)\1\s*\+\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?)\s*\+\s*(['"])([^'"`\n]*)\4/) do
            changed = true
            "`#{Regexp.last_match(2)}${#{Regexp.last_match(3)}}#{Regexp.last_match(5)}`"
          end
          @transforms << :template_literals if changed
          out
        end

        def convert_optional_chaining(src)
          changed = false
          out = src.gsub(/\b([A-Za-z_$][\w$]*)\s*&&\s*\1\.([A-Za-z_$][\w$]*)\b/) do
            changed = true
            "#{Regexp.last_match(1)}?.#{Regexp.last_match(2)}"
          end
          @transforms << :optional_chaining if changed
          out
        end

        def remove_immediate_dead_code(src)
          lines = src.lines
          keep = []
          changed = false
          skip_next = false
          lines.each_with_index do |line, index|
            if skip_next && executable_line?(line)
              changed = true
              skip_next = false
              next
            end
            keep << line
            skip_next = line.match?(/^\s*(return|raise|exit|throw)\b/) && executable_line?(lines[index + 1].to_s)
          end
          @transforms << :dead_code if changed
          keep.join
        end

        def executable_line?(line)
          stripped = line.strip
          !stripped.empty? && !stripped.start_with?("#", "//")
        end

        def add_trailing_commas(src)
          lines = src.lines
          changed = false
          (1...lines.length).each do |i|
            current = lines[i].strip
            previous = lines[i - 1]
            next unless current.match?(/^[\]}]/)
            next if previous.rstrip.end_with?(",", "[", "{", "(")
            next unless previous.match?(/^\s*[^#\n]+/)

            lines[i - 1] = previous.rstrip + ",\n"
            changed = true
          end
          @transforms << :trailing_commas if changed
          lines.join
        end

        def logical_properties(src)
          changed = false
          replacements = {
            "margin-left" => "margin-inline-start",
            "margin-right" => "margin-inline-end",
            "padding-left" => "padding-inline-start",
            "padding-right" => "padding-inline-end",
            "border-left" => "border-inline-start",
            "border-right" => "border-inline-end"
          }
          out = src.gsub(/\b(?:#{replacements.keys.map { |key| Regexp.escape(key) }.join("|")})\s*:/) do |match|
            changed = true
            match.sub(match.split(":").first, replacements.fetch(match.split(":").first))
          end
          @transforms << :logical_properties if changed
          out
        end

        def ruby? = File.extname(@path).downcase == ".rb"

        def shell? = %w[.zsh .sh .bash].include?(File.extname(@path).downcase)

        def html? = %w[.html .erb .html.erb].any? { |ext| @path.to_s.downcase.end_with?(ext) }

        def javascript? = JS_EXTS.include?(File.extname(@path).downcase)

        def style? = STYLE_EXTS.include?(File.extname(@path).downcase)

        def sql_in_ruby?
          ruby? || %w[.sql .erb].include?(File.extname(@path).downcase)
        end

        def write_back(content)
          temporary_path = "#{@path}.ast_fix.#{Process.pid}.tmp"
          File.write(temporary_path, content, encoding: "UTF-8")
          File.rename(temporary_path, @path)
        rescue StandardError => e
          File.delete(temporary_path) if defined?(temporary_path) && File.exist?(temporary_path) 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.
    #
    # Fact extraction implemented for Ruby via Prism. Horn clause evaluation
    # is a minimal forward-chaining Datalog subset.
      class DatalogEngine
        Fact = Struct.new(:predicate, :args, keyword_init: true)
        # head :- body[]
        Rule = Struct.new(:head, :body, keyword_init: true)
        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

        # Body predicates accept:
        #   :pred          — fact :pred must exist (positive)
        #   [:not, :pred]  — fact :pred must NOT exist (negation-as-failure)
        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 (nil = wildcard).
        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. Evaluates all body predicates — positive and
        # negated — and derives a finding for each positive primary-body fact that
        # satisfies all constraints.
        def evaluate
          findings = []
          @rules.each do |r|
            positive_preds, negated_preds = r[:body].partition { |bp| !naf?(bp) }
            # All negated predicates must have zero matching facts.
            next if negated_preds.any? { |bp| query(bp[1]).any? }
            # Primary body is the first positive predicate; all others must be non-empty.
            next if positive_preds.empty?
            next if positive_preds.drop(1).any? { |bp| query(bp).empty? }
            query(positive_preds.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 naf?(body_pred) = body_pred.is_a?(Array) && body_pred[0] == :not

        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/finding.rb

# frozen_string_literal: true

module Master
  module Judge
    module Scan
      Finding = Data.define(:rule, :rule_id, :message, :line, :severity, :fix, :tags, :reversibility, :blast_radius) do
        def self.build(rule:, message:, line:, severity: :warning, fix: nil, tags: [], reversibility: nil, blast_radius: nil)
          new(rule:, rule_id: rule.to_s, message:, line:, severity:, fix:, tags:, reversibility:, blast_radius:)
        end

        def [](key)
          public_send(key)
        end

        def to_h
          { rule:, rule_id:, message:, line:, severity:, fix:, tags:, reversibility:, blast_radius: }
        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.
    # Rule subclasses inherit Rule.auto_build? == true; specialized rules that
    # need constructor arguments override self.auto_build? = false explicitly.
    #
    #   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.upcase
          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"
require_relative "rules/structural_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 — Answer each question silently; only include findings below
          if they survive the steelman:
            1. What is wrong with this design that I have not spotted?
            2. What would an attacker do with this code?
            3. What assumption is this built on that could be false?
            4. What breaks at scale or under failure?
            5. Is this wired to anything? Could it be deleted without loss?
            6. Is there a simpler approach that was not taken?
            7. What should be relocated or transformed to a different format?

          Step 3 — Output only surviving violations.
          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), security, 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
            Master::Ground::Swallow.log(e, context: "ast_omission_rule.check", event_bus: nil)
            []
          end
        end
      end
    end
  end
end

lib/judge/scan/rules/co_change_coupling_rule.rb

# frozen_string_literal: true

module Master
  module Judge
    module Scan
      module Rules
        # Files that change together in many commits are coupled regardless of imports.
        # Reads the co-change graph from RepoEcology (built once at boot) instead of
        # mining git per-scan. Flags cross-module pairs — likely DECOUPLE candidates
        # the lexical rules can't see.
        class CoChangeCouplingRule < Rule
          WEIGHT_THRESHOLD = 5

          def self.auto_build? = false

          def initialize(root: nil, ecology: nil)
            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]
            @root = root ? File.expand_path(root) : File.expand_path(File.join(Master::ROOT, ".."))
            @ecology = ecology
          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 { |_, weight| weight >= WEIGHT_THRESHOLD }
                                  .sort_by { |_, weight| -weight }
                                  .first(3)
            return [] if peers.empty?
            coupling_message = "co-changes with " + peers.map { |peer, weight| "#{peer} (#{weight}x)" }.join(", ")
            [finding(line: 1, message: coupling_message)]
          end

          private

          def neighbors(rel)
            graph[rel] || {}
          end

          def graph
            @ecology ? @ecology.co_change_graph : {}
          end

          def relativize(path)
            full = File.expand_path(path)
            prefix = @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
              pair_index = match[1].to_i
              pair = pairs[pair_index]
              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

        RuleDSL.rule :NO_VAR,
          severity: :error, tags: %i[CORRECTNESS], applies_to: %i[javascript],
          description: "var is function-scoped and hoisted — use const or let" do |src, path:|
          scan_lines(src, /\bvar\s+\w/, message: "var declaration — use const (default) or let (when reassigned)")
        end

        RuleDSL.rule :JS_MODULE_SIZE,
          severity: :warning, tags: %i[SMALL_PARTS], applies_to: %i[javascript],
          description: "JS files over 300 lines — split at module boundaries" do |src, path:|
          line_count = src.lines.size
          next [] if line_count <= 300
          [finding(line: 1, message: "JS file #{line_count} lines — split at 300; extract cohesive modules")]
        end

      # A02 MAGIC_COLOR — raw color values must reference design tokens (MAGIC_COLOR).
        RuleDSL.rule :MAGIC_COLOR,
          severity: :warning, tags: %i[DESIGN], applies_to: %i[css scss javascript html],
          description: "color values must reference design tokens, not raw hex/rgb" do |src, path:|
          next [] if path.to_s.match?(%r{/spec/|/test/})
          findings = scan_lines(src, /#[0-9a-fA-F]{3,6}\b/, message: "raw hex color — use CSS custom property or design token")
          findings += scan_lines(src, /\brgba?\s*\(/, message: "raw rgb() color — use CSS custom property or design token")
          findings += scan_lines(src, /\bhsla?\s*\(/, message: "raw hsl() color — use CSS custom property or design token")
          findings
        end

      # A11 OPTIONAL_CHAINING_JS — && guard chains in JavaScript (OPTIONAL_CHAINING).
        RuleDSL.rule :OPTIONAL_CHAINING_JS,
          severity: :warning, tags: %i[READABILITY], applies_to: %i[javascript],
          description: "use ?. over && chains" do |src, path:|
          scan_lines(src, /(\w+)\s*&&\s*\1\.\w+/, message: "nil-guard chain — use optional chaining (?.) instead")
        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, line_index|
            m = RESCUE_HEAD.match(line)
            next unless m && rescue_in_scope?(m[2].to_s.strip, blanket_only)
            body = rescue_body(lines, line_index, m[4].to_s.strip)
            next unless rescue_silent?(body, m[3])
            [line_index + 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, line_index, inline)
          return [inline] unless inline.empty?
          collected = []
          ((line_index + 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

      # Veto: UNFINISHED — block merge on incomplete placeholders (ROBUSTNESS).
        RuleDSL.rule :UNFINISHED,
          severity: :error, tags: %i[ROBUSTNESS COMPLETENESS],
          description: "unfinished placeholder blocks merge" 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 unless stripped.match?(/\.\.\.|pending\b/) || stripped.match?(/\bTODO\b|\bFIXME\b/)
            finding(line: n, message: "unfinished code — complete or track in issue before merging")
          end
        end

      # Veto: UNSAFE_CALLS — flag shell interpolation as command injection risk (ROBUSTNESS).
        RuleDSL.rule :UNSAFE_CALLS,
          severity: :error, tags: %i[SECURITY ROBUSTNESS],
          description: "shell interpolation in system/exec/Open3 is a command injection vector" do |src, path:|
          src.each_line.with_index(1).filter_map do |line, n|
            stripped = line.strip
            next if stripped.start_with?("#")
            next unless stripped.match?(/\bsystem\s*\(.*#\{/) ||
                        stripped.match?(/\bexec\s*\(.*#\{/) ||
                        stripped.match?(/%x\{.*#\{/) ||
                        stripped.match?(/Open3\.capture[23]\s*\([^)]*#\{/)
            finding(line: n, message: "shell interpolation — use arg-array form to prevent injection")
          end
        end

      # A01 SECRET_PROXIMITY — hardcoded credentials (SECRET_PROXIMITY).
        RuleDSL.rule :SECRET_PROXIMITY,
          severity: :error, tags: %i[SECURITY],
          description: "hardcoded secret must move to environment variable" do |src, path:|
          next [] if path.to_s.match?(%r{/spec/|/test/|\.sample\z|\.example\z})
          scan_lines(src, /(?:password|secret|token|api_key|private_key)\s*=\s*['"][^'"]{8,}['"]/i,
            message: "hardcoded credential — move to ENV or secrets manager")
        end

      # A03 UNBOUNDED_RETRY — retry/while true without cap (UNBOUNDED_RETRY).
        RuleDSL.rule :UNBOUNDED_RETRY,
          severity: :error, tags: %i[ROBUSTNESS],
          description: "retry loop missing max_attempts cap and backoff" do |src, path:|
          src.each_line.with_index(1).filter_map do |line, n|
            next if line.strip.start_with?("#")
            next unless line.match?(/\bretry\b/) || line.match?(/while\s+true\b/)
            context = src.lines[[n - 6, 0].max, 12].join
            next if context.match?(/max_attempts|max_retries|attempts\s*<|retries\s*<|backoff/)
            finding(line: n, message: "unbounded retry — add max_attempts cap and exponential backoff")
          end
        end

      # A04 KEYWORD_ARGS — 3+ positional args in Ruby def (KEYWORD_ARGS).
        RuleDSL.rule :KEYWORD_ARGS,
          severity: :info, tags: %i[READABILITY], applies_to: %i[ruby],
          description: "keyword arguments for 3+ parameters" 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
            next if args_str.empty?
            positional = args_str.split(",").map(&:strip).reject { |a| a.include?(":") || a.start_with?("*", "&") }
            finding(line: n, message: "#{positional.size} positional args — use keyword args for clarity") if positional.size >= 3
          end
        end

      # A05 GUARD_CLAUSE — nested if-else in method body (GUARD_CLAUSE).
        RuleDSL.rule :GUARD_CLAUSE,
          severity: :info, tags: %i[READABILITY], applies_to: %i[ruby],
          description: "favor guard clauses over nested conditionals" do |src, path:|
          next [] if path.to_s.include?("/judge/scan/rules/")
          in_def = false
          first_if = nil
          findings = []
          src.each_line.with_index(1) do |line, n|
            stripped = line.strip
            in_def = true if stripped.match?(/\Adef \w+/)
            first_if = n if in_def && first_if.nil? && stripped.match?(/\Aif /)
            if in_def && first_if && stripped == "else"
              findings << finding(line: first_if, message: "if-else at method top — flatten to guard clause: return unless ...")
              first_if = nil
            end
            if stripped == "end" && in_def
              in_def = false
              first_if = nil
            end
          end
          findings
        end

      # A07 RESCUE_ON_DEF — begin/rescue inside def (RESCUE_ON_DEF).
        RuleDSL.rule :RESCUE_ON_DEF,
          severity: :info, tags: %i[READABILITY], applies_to: %i[ruby],
          description: "move begin/rescue to def line" do |src, path:|
          lines = src.lines
          lines.each_with_index.filter_map do |line, i|
            next unless line.strip.match?(/\Adef \w+/)
            next_non_blank = lines[i + 1..].find { |l| !l.strip.empty? }&.strip
            finding(line: i + 1, message: "def followed by begin — put rescue directly on def block") if next_non_blank == "begin"
          end
        end

      # A08 DEAD_CODE — statement after return/raise/exit/throw (DEAD_CODE).
        RuleDSL.rule :DEAD_CODE,
          severity: :warning, tags: %i[CLEAN_CODE],
          description: "unreachable code after return/raise/exit/throw" do |src, path:|
          lines = src.lines
          findings = []
          lines.each_with_index do |line, i|
            stripped = line.strip
            next unless stripped.match?(/\A(return|raise|exit|throw)\b/)
            lookahead = lines[i + 1]&.strip
            next if lookahead.nil? || lookahead.empty? || lookahead.start_with?("#", "end", "else", "elsif", "rescue", "ensure")
            findings << finding(line: i + 2, message: "unreachable code after #{stripped.split.first} — remove")
          end
          findings
        end

      # A09 TRAILING_COMMAS — multi-line collection missing trailing comma (TRAILING_COMMAS).
        RuleDSL.rule :TRAILING_COMMAS,
          severity: :info, tags: %i[READABILITY], applies_to: %i[ruby],
          description: "trailing commas in multi-line collections" do |src, path:|
          lines = src.lines
          findings = []
          lines.each_with_index do |line, i|
            stripped = line.strip
            next unless stripped.match?(/[\]\)}\}]\s*$/) && i > 0
            prev = lines[i - 1]&.strip
            next unless prev && !prev.empty? && !prev.end_with?(",", "(", "[", "{")
            next if prev.start_with?("#")
            next unless lines[0..i].any? { |l| l.include?("\n") } rescue false
            next unless src.lines[[i - 5, 0].max..i].any? { |l| l.match?(/,\s*$/) }
            findings << finding(line: i, message: "last element missing trailing comma — add comma for clean diffs")
          end
          findings
        end

      # A10 FULL_BY_DEFAULT — fake quality-tier parameters (FULL_BY_DEFAULT).
        RuleDSL.rule :FULL_BY_DEFAULT,
          severity: :warning, tags: %i[SMALL_PARTS],
          description: "fake-choice tiers — defaults must be maximal correctness" do |src, path:|
          next [] if path.to_s.include?("rules.yml") || path.to_s.include?("/spec/") || path.to_s.include?("/test/")
          scan_lines(src, /\b(shallow|standard|quick|lite|basic|light)\b.*\b(deep|full|advanced|complete|thorough)\b/i,
            message: "shallow/full tier pair — drop degraded tier or rename to surface real cost (lexical|structural|semantic)")
        end

      # A13 STRICT_MODE_ZSH — zsh scripts missing set -euo pipefail (STRICT_MODE_ZSH).
        RuleDSL.rule :STRICT_MODE_ZSH,
          severity: :error, tags: %i[ROBUSTNESS], applies_to: %i[zsh],
          description: "set -euo pipefail at script top" do |src, path:|
          next [] unless src.start_with?("#!/") || src.start_with?("# !")
          next [] if src.include?("set -euo pipefail") || src.include?("set -e")
          [finding(line: 1, message: "zsh script missing set -euo pipefail after shebang")]
        end

      # A14 NO_MAGIC_NUMBERS — unexplained numeric literals (NO_MAGIC).
        RuleDSL.rule :NO_MAGIC_NUMBERS,
          severity: :warning, tags: %i[READABILITY],
          description: "no unexplained constants or flags" do |src, path:|
          next [] if path.to_s.match?(%r{/spec/|/test/|/rules/|\.yml\z|\.yaml\z|\.json\z})
          src.each_line.with_index(1).filter_map do |line, n|
            stripped = line.strip
            next if stripped.start_with?("#")
            next if stripped.match?(/\A\s*(POOL_SIZE|MAX_|MIN_|DEFAULT_|\w+_LIMIT|\w+_THRESHOLD|\w+_TTL|\w+_DAYS|\w+_S\b)/)
            next unless stripped.match?(/(?<![.\d])\b(?:[2-9]\d{1,}|\d{3,})\b(?!\.\d)(?![\w.])/)
            finding(line: n, message: "magic number — extract to named constant")
          end
        end

      # A16 FORBIDDEN_PATTERNS — anti_patterns.forbidden from rules.yml.
        RuleDSL.rule :FORBIDDEN_PATTERNS,
          severity: :error, tags: %i[SECURITY ROBUSTNESS],
          description: "forbidden pattern from rules.yml anti_patterns" do |src, path:|
          next [] if path.to_s.include?("/judge/scan/rules/") || path.to_s.include?("rules.yml")
          findings = []
          findings.concat(scan_lines(src, /\beval\(.*\$\{|\beval\(.*user/i, message: "eval with user input — arbitrary code execution"))
          findings.concat(scan_lines(src, /\brm\s+-rf\s+\/(?!\w)/, message: "rm -rf / — data loss"))
          findings.concat(scan_lines(src, /Marshal[.]load\b/, message: "Marshal load — deserialization RCE vector"))
          findings.concat(scan_lines(src, /\bopen\(.*#\{/, message: "open() with interpolation — shell injection via Kernel#open"))
          findings
        end

      # A06 USE_THEN: sequential temp-var chains; refactor to .then/.yield_self.
        RuleDSL.rule :USE_THEN,
          severity: :info, tags: %i[READABILITY], applies_to: %i[ruby],
          description: "use .then over temp variable chains" do |src, path:|
          lines = src.lines
          lines.each_with_index.filter_map do |line, i|
            next unless line.match?(/^\s+(\w+)\s*=\s*\w+[\.\(]/)
            next_line = lines[i + 1]
            var = line.match(/^\s+(\w+)\s*=/)[1] rescue next
            next unless next_line&.match?(/\b#{Regexp.escape(var)}\b/) && next_line.match?(/\w+\(#{Regexp.escape(var)}\)/)
            finding(line: i + 1, message: "temp var #{var} used immediately — consider .then { |#{var}| … }")
          end
        end

      # A12 NULL_BLINDNESS — `= NULL` / `== nil` comparisons in SQL-like contexts.
        RuleDSL.rule :NULL_BLINDNESS,
          severity: :warning, tags: %i[CORRECTNESS], applies_to: %i[ruby sql erb],
          description: "use IS NULL not = NULL in SQL; use .nil? not == nil in Ruby" do |src, path:|
          findings = scan_lines(src, /(?<![<>!])=\s*NULL\b/i, message: "= NULL — use IS NULL in SQL")
          findings += scan_lines(src, /==\s*nil\b/, message: "== nil — use .nil? in Ruby") if path.to_s.end_with?(".rb", ".rake")
          findings
        end

      # A15 NO_COLUMN_ALIGN — multiple spaces before => / = / : to align columns.
        RuleDSL.rule :NO_COLUMN_ALIGN,
          severity: :info, tags: %i[STYLE],
          description: "no column alignment — one space, ragged beats symmetric" do |src, path:|
          scan_lines(src, /\S {2,}(?:=>|[^=!<>]=(?!=)|:\s)/, message: "column-aligned padding — use single space before => / = / :")
        end

      # A17 SPECULATIVE_GENERALITY_LEXICAL: catches future/hypothetical TODO comments.
        RuleDSL.rule :SPECULATIVE_GENERALITY_LEXICAL,
          severity: :info, tags: %i[YAGNI],
          description: "no speculative future-proofing comments" do |src, path:|
          scan_lines(src, /#.*\b(TODO.*future|for later|hypothetical|someday|when we need|might need|could use)\b/i,
            message: "speculative comment — delete or schedule; YAGNI")
        end

      # A18 COMMENTS_AS_DEODORANT — comments that describe what the code does.
        RuleDSL.rule :COMMENTS_AS_DEODORANT,
          severity: :info, tags: %i[CLEAN_CODE],
          description: "no redundant what-comments; code speaks for itself" do |src, path:|
          scan_lines(src, /#\s*(?:This (?:method|class|function|module)|The (?:method|class) \w+ (?:does|returns|handles|is responsible))/i,
            message: "comment explains what code does — rename or delete; code should be self-explanatory")
        end

      # Veto: RACE_CONDITIONS — bare check-then-set without synchronize (ROBUSTNESS).
        RuleDSL.rule :RACE_CONDITIONS,
          severity: :error, tags: %i[ROBUSTNESS CONCURRENCY],
          description: "check-then-set without synchronize is a TOCTOU race" do |src, path:|
          next [] unless path.to_s.end_with?(".rb")
          lines = src.lines
          lines.each_with_index.filter_map do |line, i|
            next unless line.match?(/\bif\b.*\b(nil\?|empty\?|zero\?|blank\?)\b/) ||
                        line.match?(/\bif\b.*==\s*nil\b/)
            setter = lines[i + 1..]&.first(3)&.any? { |l| l.match?(/^\s+@\w+\s*=/) }
            next unless setter
            no_sync = !lines[0..i].last(10).any? { |l| l.match?(/synchronize|Mutex\.new|Monitor/) }
            finding(line: i + 1, message: "check-then-set without synchronize — wrap in synchronize { }") if no_sync
          end
        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
        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
        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

      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,
	                  reversibility: r["reversibility"],
	                  blast_radius: r["blast_radius"]
	                }
              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],
                message: match[3].strip,
                line: match[2].to_i,
                severity: axiom[:severity],
                fix: nil,
	                tags: [match[1].to_sym, axiom[:mode]],
	                reversibility: axiom[:reversibility],
	                blast_radius: axiom[:blast_radius]
              )
            end
          end
        end
      end
    end
  end
end

lib/judge/scan/rules/structural_rules.rb

# frozen_string_literal: true

require "prism"

module Master
  module Judge
    module Scan
      module Rules
      # Structural rules use Prism AST rather than line-by-line regex.
      # Each implements check_ast(ast, code, path:) for scanner integration.
      # All also implement check(code, path:) as fallback for non-Ruby files.

      # B01 SMALL_FILES — files over 300 lines (detect_structural: file_silhouette).
        class SmallFilesRule < Rule
          LIMIT = 300

          def initialize
            super()
            @id = "SMALL_FILES"
            @description = "files under 300 lines"
            @severity = :warning
            @rule_tags = %i[SMALL_PARTS]
            @auto_fix = false
          end

          def check(code, path:)
            count = code.lines.size
            return [] if count <= LIMIT
            [finding(line: 1, message: "file #{count} lines (limit #{LIMIT}) — split at module boundaries")]
          end
        end

      # B02 SMALL_FUNCTIONS — methods over 20 lines via Prism (detect_structural: long_method).
        class SmallFunctionsRule < Rule
          IDEAL = 10
          MAX = 20

          def initialize
            super()
            @id = "SMALL_FUNCTIONS"
            @description = "methods under 10 lines ideal, max 20"
            @severity = :warning
            @rule_tags = %i[SMALL_PARTS]
            @auto_fix = false
          end

          def check(code, path:)
            return [] unless path.to_s.end_with?(".rb", ".rake")
            check_ast(Prism.parse(code).value, code, path:)
          rescue StandardError
            []
          end

          def check_ast(ast, _code, path:)
            return [] unless ast
            findings = []
            visit(ast) do |node|
              next unless node.is_a?(Prism::DefNode)
              len = node.location.end_line - node.location.start_line
              next if len <= MAX
              name = node.name
              findings << finding(line: node.location.start_line, message: "method #{name} is #{len} lines (max #{MAX}) — extract helpers")
            end
            findings
          end

          private

          def visit(node, &block)
            return unless node.respond_to?(:child_nodes)
            block.call(node)
            node.child_nodes.compact.each { |c| visit(c, &block) }
          end
        end

      # B03 NO_GOD_CLASS — class with >10 public methods (detect_structural: god_class).
        class GodClassRule < Rule
          METHOD_LIMIT = 10
          LINE_LIMIT = 300

          def initialize
            super()
            @id = "NO_GOD_CLASS"
            @description = "no god classes"
            @severity = :error
            @rule_tags = %i[SOLID SRP]
            @auto_fix = false
          end

          def check(code, path:)
            return [] unless path.to_s.end_with?(".rb", ".rake")
            check_ast(Prism.parse(code).value, code, path:)
          rescue StandardError
            []
          end

          def check_ast(ast, code, path:)
            return [] unless ast
            findings = []
            visit(ast) do |node|
              next unless node.is_a?(Prism::ClassNode)
              public_defs = count_public_methods(node)
              line_count = node.location.end_line - node.location.start_line
              if public_defs > METHOD_LIMIT
                findings << finding(
                  line: node.location.start_line,
                  message: "god class #{node.constant_path.slice} has #{public_defs} public methods (max #{METHOD_LIMIT}) — decompose"
                )
              elsif line_count > LINE_LIMIT
                findings << finding(
                  line: node.location.start_line,
                  message: "god class #{node.constant_path.slice} is #{line_count} lines — split at responsibility boundaries"
                )
              end
            end
            findings
          end

          private

          def visit(node, &block)
            return unless node.respond_to?(:child_nodes)
            block.call(node)
            node.child_nodes.compact.each { |c| visit(c, &block) }
          end

          def count_public_methods(class_node)
            count = 0
            in_private = false
            return count unless class_node.respond_to?(:body) && class_node.body
            class_node.body.child_nodes.compact.each do |node|
              if node.is_a?(Prism::CallNode) && %w[private protected].include?(node.name.to_s)
                in_private = true
              end
              count += 1 if !in_private && node.is_a?(Prism::DefNode)
            end
            count
          end
        end

      # B07 NESTING_DEPTH — nesting deeper than 4 levels (detect_structural: nesting_depth).
        class NestingDepthRule < Rule
          MAX_DEPTH = 4

          NESTING_TYPES = [
            Prism::ModuleNode, Prism::ClassNode, Prism::DefNode,
            Prism::IfNode, Prism::UnlessNode, Prism::WhileNode,
            Prism::UntilNode, Prism::ForNode, Prism::CaseNode,
            Prism::BlockNode,
          ].freeze

          def initialize
            super()
            @id = "NESTING_DEPTH"
            @description = "nesting depth under 4"
            @severity = :warning
            @rule_tags = %i[LINEARITY]
            @auto_fix = false
          end

          def check(code, path:)
            return [] unless path.to_s.end_with?(".rb", ".rake")
            check_ast(Prism.parse(code).value, code, path:)
          rescue StandardError
            []
          end

          def check_ast(ast, _code, path:)
            return [] unless ast
            deep = []
            scan_depth(ast, 0, deep)
            deep.map { |line| finding(line:, message: "nesting depth exceeds #{MAX_DEPTH} — flatten with guard clauses or extract methods") }
          end

          private

          def scan_depth(node, depth, violations)
            return unless node.respond_to?(:child_nodes)
            new_depth = NESTING_TYPES.include?(node.class) ? depth + 1 : depth
            if new_depth > MAX_DEPTH && violations.none? { |l| (l - node.location.start_line).abs < 3 }
              violations << node.location.start_line
            end
            node.child_nodes.compact.each { |c| scan_depth(c, new_depth, violations) }
          rescue StandardError
            nil
          end
        end

      # B04 CQS — method that both mutates state and returns a meaningful value.
        class CqsRule < Rule
          def initialize
            super()
            @id = "CQS"
            @description = "command-query separation — mutate OR return, not both"
            @severity = :warning
            @rule_tags = %i[CQS CLEAN_CODE]
            @auto_fix = false
          end

          def check(code, path:)
            return [] unless path.to_s.end_with?(".rb", ".rake")
            check_ast(Prism.parse(code).value, code, path:)
          rescue StandardError
            []
          end

          def check_ast(ast, _code, path:)
            return [] unless ast
            findings = []
            visit(ast) do |node|
              next unless node.is_a?(Prism::DefNode)
              body = node.body
              next unless body
              mutates = body_contains?(body, Prism::InstanceVariableWriteNode, Prism::InstanceVariableOperatorWriteNode)
              returns_value = body_has_explicit_return?(body)
              if mutates && returns_value
                findings << finding(line: node.location.start_line,
                  message: "method #{node.name} mutates state and returns a value — split into command and query")
              end
            end
            findings
          end

          private

          def visit(node, &block)
            return unless node.respond_to?(:child_nodes)
            block.call(node)
            node.child_nodes.compact.each { |c| visit(c, &block) }
          end

          def body_contains?(node, *types)
            return false unless node.respond_to?(:child_nodes)
            types.any? { |t| node.is_a?(t) } ||
              node.child_nodes.compact.any? { |c| body_contains?(c, *types) }
          end

          def body_has_explicit_return?(node)
            return false unless node.respond_to?(:child_nodes)
            return true if node.is_a?(Prism::ReturnNode) && node.arguments&.arguments&.any?
            node.child_nodes.compact.any? { |c| body_has_explicit_return?(c) }
          end
        end

      # B05 FILE_LAYOUT — Ruby file order: frozen → require → module → class → public → private.
        class FileLayoutRule < Rule
          def initialize
            super()
            @id = "FILE_LAYOUT"
            @description = "frozen header → requires → module/class → public → private"
            @severity = :info
            @rule_tags = %i[PROXIMITY CONVENTION]
            @auto_fix = false
          end

          def check(code, path:)
            return [] unless path.to_s.end_with?(".rb", ".rake")
            findings = []
            lines = code.lines
            first_non_comment = lines.find_index { |l| !l.match?(/^\s*#|^\s*$/) }
            return [] unless first_non_comment
            unless lines.first&.include?("frozen_string_literal")
              findings << finding(line: 1, message: "missing # frozen_string_literal: true as first line")
            end
            private_idx = lines.find_index { |l| l.match?(/^\s+private\s*$|^\s+private\b/) }
            if private_idx
              public_def_after_private = lines[private_idx..].each_with_index.find do |l, i|
                l.match?(/^\s+def (?!self\.)/) && !l.match?(/^\s+def (?:initialize|to_s|inspect)\b/)
              end
              if public_def_after_private
                idx = private_idx + public_def_after_private[1] + 1
                findings << finding(line: idx, message: "public method def after private marker — move above private")
              end
            end
            findings
          end
        end

      # B06 EXPLICIT — implicit requires, magic coupling, method_missing without respond_to_missing?.
        class ExplicitRule < Rule
          def initialize
            super()
            @id = "EXPLICIT"
            @description = "no implicit requires or magic coupling"
            @severity = :warning
            @rule_tags = %i[EXPLICIT CONVENTION]
            @auto_fix = false
          end

          def check(code, path:)
            return [] unless path.to_s.end_with?(".rb", ".rake")
            findings = []
            findings.concat(scan_lines(code, /\bmethod_missing\b/, message: "method_missing without respond_to_missing? — add respond_to_missing?")) \
              if code.include?("method_missing") && !code.include?("respond_to_missing?")
            findings.concat(scan_lines(code, /\bconst_missing\b/, message: "const_missing — prefer explicit require"))
            findings.concat(scan_lines(code, /\bautoload\b/, message: "autoload — prefer explicit require_relative"))
            findings
          end

          private

          def scan_lines(src, pattern, message:)
            indexed_lines = src.lines.each_with_index
            indexed_lines.filter_map do |line, i|
              finding(line: i + 1, message: message) if line.match?(pattern)
            end
          end
        end

      # B08 CYCLOMATIC_COMPLEXITY — methods with cyclomatic complexity > 10.
        class CyclomaticComplexityRule < Rule
          MAX_CC = 10
          CC_NODES = [
            Prism::IfNode, Prism::UnlessNode, Prism::WhileNode, Prism::UntilNode,
            Prism::ForNode, Prism::WhenNode, Prism::RescueNode, Prism::AndNode,
            Prism::OrNode,
          ].freeze

          def initialize
            super()
            @id = "CYCLOMATIC_COMPLEXITY"
            @description = "cyclomatic complexity under 10 per method"
            @severity = :warning
            @rule_tags = %i[LINEARITY SMALL_PARTS]
            @auto_fix = false
          end

          def check(code, path:)
            return [] unless path.to_s.end_with?(".rb", ".rake")
            check_ast(Prism.parse(code).value, code, path:)
          rescue StandardError
            []
          end

          def check_ast(ast, _code, path:)
            return [] unless ast
            findings = []
            visit(ast) do |node|
              next unless node.is_a?(Prism::DefNode)
              cc = 1 + count_cc_nodes(node)
              next if cc <= MAX_CC
              findings << finding(line: node.location.start_line,
                message: "method #{node.name} has cyclomatic complexity #{cc} (max #{MAX_CC}) — extract branches")
            end
            findings
          end

          private

          def visit(node, &block)
            return unless node.respond_to?(:child_nodes)
            block.call(node)
            node.child_nodes.compact.each { |c| visit(c, &block) }
          end

          def count_cc_nodes(node)
            return 0 unless node.respond_to?(:child_nodes)
            own = CC_NODES.include?(node.class) ? 1 : 0
            own + node.child_nodes.compact.sum { |c| count_cc_nodes(c) }
          end
        end

      # B09 PATTERN_EXTRACTION — code close to a named design pattern.
        class PatternExtractionRule < Rule
          BRANCH_THRESHOLD = 3
          PIPELINE_STEP_THRESHOLD = 4

          def initialize
            super()
            @id = "PATTERN_EXTRACTION"
            @description = "file is close to a named design pattern"
            @severity = :info
            @rule_tags = %i[DESIGN OPPORTUNITY]
            @auto_fix = false
          end

          def check(code, path:)
            return [] unless path.to_s.end_with?(".rb", ".rake")
            check_ast(Prism.parse(code).value, code, path:)
          rescue StandardError
            []
          end

          def check_ast(ast, code, path:)
            return [] unless ast
            findings = []
            lines = code.lines
            visit(ast) do |node|
              next unless node.is_a?(Prism::DefNode)
              method_lines = lines[(node.location.start_line - 1)...node.location.end_line].join
              branch_count = branch_dispatch_count(method_lines)
              if branch_count >= BRANCH_THRESHOLD
                findings << finding(
                  line: node.location.start_line,
                  message: "Strategy opportunity in #{node.name}: #{branch_count} dispatch branches — extract named handlers"
                )
              elsif pipeline_step_count(method_lines) >= PIPELINE_STEP_THRESHOLD
                findings << finding(
                  line: node.location.start_line,
                  message: "Pipeline opportunity in #{node.name}: sequential transformations can be named stages"
                )
              end
            end
            findings
          end

          private

          def visit(node, &block)
            return unless node.respond_to?(:child_nodes)
            block.call(node)
            node.child_nodes.compact.each { |c| visit(c, &block) }
          end

          def branch_dispatch_count(src)
            src.lines.count { |line| line.match?(/^\s*(when|elsif)\b/) }
          end

          def pipeline_step_count(src)
            assignments = src.lines.filter_map do |line|
              match = line.match(/^\s*([a-z_]\w*)\s*=\s*(.+)$/)
              [match[1], match[2]] if match
            end
            return 0 if assignments.size < PIPELINE_STEP_THRESHOLD
            assignments.each_cons(PIPELINE_STEP_THRESHOLD).find { |group| chained_assignments?(group) }&.size.to_i
          end

          def chained_assignments?(group)
            names = group.map(&:first)
            return false unless names.uniq.size == names.size
            group.each_cons(2).all? { |left, right| right.last.match?(/\b#{Regexp.escape(left.first)}\b/) }
          end
        end

      # B10 DATA_CLASS — class with only attr_accessor and no real methods.
        class DataClassRule < Rule
          def initialize
            super()
            @id = "DATA_CLASS"
            @description = "data class with no behavior — use Struct or Data"
            @severity = :info
            @rule_tags = %i[SRP SOLID]
            @auto_fix = false
          end

          def check(code, path:)
            return [] unless path.to_s.end_with?(".rb", ".rake")
            check_ast(Prism.parse(code).value, code, path:)
          rescue StandardError
            []
          end

          def check_ast(ast, _code, path:)
            return [] unless ast
            findings = []
            visit(ast) do |node|
              next unless node.is_a?(Prism::ClassNode)
              next unless node.body
              children = node.body.child_nodes.compact
              accessor_calls = children.count { |n| n.is_a?(Prism::CallNode) && %w[attr_accessor attr_reader attr_writer].include?(n.name.to_s) }
              real_defs = children.count { |n| n.is_a?(Prism::DefNode) && !%w[initialize to_s inspect].include?(n.name.to_s) }
              next unless accessor_calls >= 2 && real_defs == 0
              findings << finding(line: node.location.start_line,
                message: "#{node.constant_path.slice} has #{accessor_calls} accessors and no behavior — use Struct or Data.define")
            end
            findings
          end

          private

          def visit(node, &block)
            return unless node.respond_to?(:child_nodes)
            block.call(node)
            node.child_nodes.compact.each { |c| visit(c, &block) }
          end
        end

        StructuralRules = Module.new
      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 positional arguments" do |src, path:|
          scan_lines(src, /def \w+\([^)]*,[^:)]+,[^:)]+\)/,
            message: "3+ positional args — use keyword arguments or a value object")
        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|data|result|val|ret|obj|str|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

      # Bias: SIMULATION — future tense in output implies intent without evidence.
        RuleDSL.rule :SIMULATION,
          severity: :warning, tags: %i[ANTI_SIMULATION DENSITY],
          description: "future tense implies without evidence — use indicative past" do |src, path:|
          next [] unless path.to_s.end_with?(".rb", ".md", ".txt", ".erb")
          next [] if path.to_s.include?("/judge/scan/rules/")
          src.each_line.with_index(1).filter_map do |line, n|
            next if line.strip.start_with?("#")
            next unless line.match?(/\b(will\s+\w+|w
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment