CONVENTIONS.md
DEPLOY/
openbsd/
etc/
rc.d/
Gemfile
QUICKSTART.md
README.md
Rakefile
bin/
completions/
data/
CANON.md
agent_taxonomy.yml
agents/
brgen_amber_completion.yml
architectures.yml
attention_context.yml
budget.yml
claude/
MEMORY.md
feedback_autofix.md
feedback_autoproceed.md
feedback_comments_reassess.md
feedback_continue_backlog.md
feedback_decisive_signals.md
feedback_device_limits.md
feedback_diverged_branch_sync.md
feedback_flat_pixels.md
feedback_git_commits.md
feedback_html_css_style.md
feedback_importance_order.md
feedback_lint_beautify.md
feedback_master_prompt_aesthetic.md
feedback_master_zsh_discipline.md
feedback_meta_framing.md
feedback_micro_refinements.md
feedback_motion_color_grading.md
feedback_no_consecutive_whitespace.md
feedback_no_new_files.md
feedback_no_permission_questions.md
feedback_no_python.md
feedback_no_sed.md
feedback_no_shell_piping.md
feedback_no_useless_knobs.md
feedback_proper_casing.md
feedback_readme_autoupdate.md
feedback_restart_rails.md
feedback_run_through_master_triad.md
feedback_strunk_white.md
feedback_style.md
feedback_universal_cross_disciplinary_rules.md
feedback_voice_terse_unix.md
project_defrag_plan_2026_05.md
project_falcon_em_subprocess.md
project_master.md
project_master_dual_gemfile.md
project_master_seven_module_refactor.md
project_master_yml_json_authority.md
reference_grok_ui_cli_patterns.md
reference_opencrabs.md
user_architect_aesthetics.md
closings.yml
council.yml
design_rules.yml
epistemics.yml
exemplars.yml
gems.yml
heartbeat.yml
injection_patterns.yml
llm_operators.yml
load.yml
mcp_servers.yml
mobile_web_opportunities.yml
models.yml
openbsd.yml
patterns.yml
personas.yml
principles/
feedback_autofix.md
feedback_autoproceed.md
feedback_comments_reassess.md
feedback_continue_backlog.md
feedback_decisive_signals.md
feedback_device_limits.md
feedback_diverged_branch_sync.md
feedback_flat_pixels.md
feedback_git_commits.md
feedback_html_css_style.md
feedback_importance_order.md
feedback_lint_beautify.md
feedback_master_prompt_aesthetic.md
feedback_master_zsh_discipline.md
feedback_meta_framing.md
feedback_micro_refinements.md
feedback_motion_color_grading.md
feedback_no_consecutive_whitespace.md
feedback_no_new_files.md
feedback_no_permission_questions.md
feedback_no_python.md
feedback_no_sed.md
feedback_no_shell_piping.md
feedback_no_useless_knobs.md
feedback_proper_casing.md
feedback_readme_autoupdate.md
feedback_restart_rails.md
feedback_run_through_master_triad.md
feedback_strunk_white.md
feedback_style.md
feedback_universal_cross_disciplinary_rules.md
feedback_voice_terse_unix.md
prompts/
council.yml
mode_code_agent.yml
mode_direct.yml
mode_react.yml
mode_rewoo.yml
original_prompts.md
providers.yml
rails.yml
refusal_templates.yml
ruby_style.yml
rule_deps.yml
rules.yml
soul.yml
stale_namespaces.yml
standing_orders.yml
templates.yml
tools.yml
topologies.yml
traces/
ui.yml
violation_priors.yml
visual_clusters.yml
vocabulary.yml
web/
why_command.yml
workflow.yml
zsh.yml
docs/
cleanup_and_trace.md
cognitive_runtime.md
collaboration_protocol.md
event_naming.md
face3d_engine.md
face3d_runtime_hardening.md
grok_bug_report_may_2026.md
non_negotiable_runtime_rules.md
platform_topology.md
provider_economy.md
repo_ecology.md
runtime_ui_direction.md
lib/
builder.rb
design/
mobile_first_pwa_profiles.rb
platform_profiles.rb
ground/
agent_lifecycle.rb
atomic_write.rb
attention_context.rb
axioms/
rails_doctrine.rb
ux_heuristics.rb
wcag.rb
brain_overlay.rb
brutalist_minimalism.rb
checkpoint.rb
config.rb
constitution.rb
context_provider.rb
done_checker.rb
evidence_base.rb
frontmatter.rb
intent_router.rb
knowledge_store.rb
memory.rb
memory_index.rb
memory_search.rb
orchestration_policy.rb
orders/
architecture_audit.rb
autocommit.rb
backup.rb
base.rb
constitution_drift.rb
registry.rb
restart_master.rb
patch_verifier.rb
persistence/
sqlite_findings.rb
sqlite_memory.rb
sqlite_store.rb
phase_gates.rb
pledge.rb
provider_registry.rb
repo_map.rb
repo_mining/
mobile_web_cluster_catalog.rb
rules.rb
runtime_registry.rb
sandbox_policy.rb
standing_orders.rb
subagent_policy.rb
swallow.rb
tool_approval_policy.rb
tool_contract.rb
tool_protocol.rb
type_checker.rb
unfinished_ledger.rb
unified_diff_editor.rb
workflow_policy.rb
judge/
agent.rb
agent_pool.rb
ast_signature.rb
code_index.rb
commit_guard.rb
council/
critique.rb
deliberation.rb
ideation.rb
personas.rb
embeddings.rb
llm_dispatcher.rb
modes.rb
reference_graph.rb
reflexion.rb
repo_ecology.rb
repo_map.rb
scan/
ast_fixer.rb
datalog_engine.rb
detection_pipeline.rb
finding.rb
rule.rb
rule_dsl.rb
rules/
adversarial_rule.rb
ast_omission_rule.rb
co_change_coupling_rule.rb
comment_drift_rule.rb
interconnect_rule.rb
js_rules.rb
lexical_rules.rb
reek_rule.rb
rubocop_rule.rb
ruby_rules.rb
rule_coverage_rule.rb
semantic_rule.rb
universal_rules.rb
web_rules.rb
scanner.rb
unit_segmenter.rb
schema_index.rb
security/
injection_guard.rb
permissions.rb
swarm/
coordinator.rb
worker.rb
workers/
analyst.rb
coder.rb
researcher.rb
reviewer.rb
loop/
constants.rb
crdt_loop.rb
cybernetics.rb
diff_stager.rb
fix_helpers.rb
fix_loop.rb
fix_pipeline.rb
governor.rb
heartbeat.rb
homeostat.rb
patch_applier.rb
propose_tree.rb
repair/
git_history_miner.rb
rule_loop.rb
watch_loop.rb
watcher.rb
master.rb
now/
cli/
command_ops.rb
signals.rb
thinking_indicator.rb
cli.rb
command_registry/
memory_commands.rb
system_commands.rb
tool_commands.rb
work_commands.rb
command_registry.rb
context_window.rb
hot_reload.rb
orchestration/
event_sequence_orchestrator.rb
pipeline.rb
pipeline_context.rb
propose.rb
routing/
model_router.rb
provider_health.rb
provider_quarantine_manager.rb
skills.rb
stages/
council.rb
deliberate.rb
enhance.rb
execute.rb
guard.rb
infer.rb
intake.rb
lint.rb
memory.rb
prune.rb
render.rb
review.rb
route.rb
pressure_engine.rb
rails/
face3d_runtime_policy.rb
hotwire_refactor_policy.rb
mobile_pwa_operator.rb
pwa_audit.rb
rails8_app_audit.rb
reach/
ask_llm.rb
ast_edit.rb
base.rb
batch_replace.rb
bedrock_stub.rb
circuit_breaker.rb
circuit_breaker_registry.rb
clean.rb
feedback_record.rb
gateway.rb
git_context.rb
git_operations.rb
list_dir.rb
llm.rb
mcp_coordinator.rb
memory_record.rb
path_guard.rb
read_file.rb
ruby_llm_patch.rb
search_files.rb
search_knowledge.rb
semantic_cache.rb
shell.rb
str_replace.rb
symbol_lookup.rb
text_hygiene.rb
tree.rb
web_fetch.rb
web_search.rb
whitespace_normalizer.rb
write_file.rb
result.rb
trace/
audit_log.rb
broadcaster.rb
diag.rb
event_bus.rb
event_log.rb
logging.rb
memory_tier_compactor.rb
metrics.rb
recorder.rb
ring_buffer.rb
self_map.rb
session.rb
swallow_ledger.rb
telemetry.rb
triggers.rb
undo.rb
why_explainer.rb
unwrap_error.rb
voice/
dilla.rb
ffmpeg_lofi.rb
personality.rb
production_dna.rb
renderer.rb
sonitex.rb
sonitex_sox.rb
soul.rb
speech.rb
tts_lofi.rb
master.gemspec
runtime/
constitution_drift.json
e2e_probe.txt
events/
improvements.md
test/
fixtures_bare_rescue.rb
support/
master_container.rb
test_adversarial_rule.rb
test_agent.rb
test_agent_escalation.rb
test_ast_omission_rule.rb
test_bare_rescue_rule.rb
test_browser.rb
test_cli.rb
test_co_change_coupling_rule.rb
test_council_deliberation.rb
test_face3d_runtime_policy.rb
test_helper.rb
test_learnings.rb
test_master_container.rb
test_pipeline.rb
test_prune.rb
test_result.rb
test_ring_buffer.rb
test_rules.rb
test_runtime_hardening.rb
test_silent_rescue_rule.rb
test_speech.rb
test_swallow_ledger.rb
test_web_http.rb
test_web_ui.rb
test_yaml_registries.rb
tools/
postpro/
README.md
postpro.rb
repligen/
README.md
repligen.rb
web/
Gemfile
README.md
Rakefile
app/
assets/
images/
javascripts/
app.js
chat.js
stylesheets/
channels/
application_cable/
channel.rb
connection.rb
master_channel.rb
controllers/
application_controller.rb
canvas_controller.rb
chat_controller.rb
concerns/
dashboard_controller.rb
events_controller.rb
health_controller.rb
helpers/
application_helper.rb
middleware/
auth_tier.rb
models/
application_record.rb
concerns/
views/
chat/
index.html.erb
dashboard/
index.html.erb
layouts/
application.html.erb
pwa/
manifest.json.erb
bin/
config/
application.rb
boot.rb
cable.yml
ci.rb
database.yml
environment.rb
environments/
development.rb
production.rb
test.rb
initializers/
assets.rb
content_security_policy.rb
filter_parameter_logging.rb
inflections.rb
master_container.rb
new_framework_defaults_8_0.rb
locales/
en.yml
puma.rb
routes.rb
db/
seeds.rb
face.js
index.html.erb
lib/
tasks/
public/
chat.js
cluster_miner.js
codebase.js
cognition_ecology.js
face.js
face3d_engine.js
face3d_preview.js
face3d_renderer.js
manifest.json
mask.js
particle_kernel.js
robots.txt
sw.js
topology_registry.js
vad-processor.js
visual_bridge.js
script/
# MASTER — Conventions for External LLMs
Context injection for any LLM reviewing or editing MASTER. Read before touching code.
The complete rule corpus — every axiom, scan rule, operator principle, and
external lineage — is indexed in `data/CANON.md`. Read it first, every session.
This file is the orientation; CANON.md is the directory.
## Identity
MASTER is a constitutional AI coding agent written in Ruby 3.3+ on OpenBSD 7.8. It replaces Claude Code CLI for its operator. It is general-purpose and language-agnostic. Every change leaves the system in a working, deployable state.
## Golden rule
`PRESERVE_THEN_IMPROVE_NEVER_BREAK`. Read before write. Patch minimally. Understand before touching — Chesterton's Fence.
## Anti-simulation
Never state intent without evidence. Forbidden hedges — `will`, `would`, `could`, `might`. Require:
- File read → content with SHA-256
- Modification → unified diff
- Completion → command output
## Communication — two registers, do not mix
- **MASTER's own log/event lines** (boot banner, scheduler ticks, tool events, dmesg-style status): structured, terse, lowercase, kernel-ish — `master@host ready`, `boot0: 26ms`, `model0 at openrouter`. The OpenBSD-dmesg boot banner is sacred — never strip it.
- **Conversational replies to the operator**: plain English, proper casing, full sentences. No dmesg style here. No headlines, no empty bullets, no filler, no sycophancy, no hedging. Outcome first, evidence next, implementation last.
- **Commits and log lines** stay active, concrete, terse — Strunk & White, omit needless words.
## No ASCII line art
Never use these as decorations in any output (comments, log lines, CLI text, chat replies, commit messages):
- `===`, `----` (banner lines, section dividers)
- `•`, `|`, `›`, `‹` (bullet/separator characters)
- `[ok]`, `[err]`, `[skip]` brackets — use bare prefixes `ok:`, `err:`, `skip:`, `warn:` instead
In Markdown documents, plain `---` for an `<hr>` and table separators are fine — they carry meaning. Banner art does not.
## Code rules (enforced by scan)
- **Read before write** — every affected file before any edit.
- **No bare rescue** — always `rescue SpecificError => e`. Inline `expr rescue nil` is fine when nil is intentional.
- **Named constants** — extract literals with `.freeze`.
- **No magic numbers** — thresholds belong in `data/rules.yml` under `thresholds:`.
- **No abbreviations** — `index` not `idx`, `signature` not `sig`, `temporary_path` not `tmp`.
- **No regex when string methods suffice** — `start_with?`, `include?`, `end_with?`.
- **Outsource to gems** — if it exists and works, use it.
- **Endless methods** — single-expression methods use `def foo = expr`.
- **Result monad** — check with `respond_to?(:ok?)`, not `is_a?(Result)`. Unwrap with `.value!` only after `.ok?` is true; on an `Err` it raises.
- **No flag arguments** — a boolean that selects behavior is two methods in one.
- **Guard clauses first** — `return Result.ok(ctx) unless condition` before main logic.
- **Dependency injection** — never instantiate collaborators inside a method.
- **CQS** — queries return, commands mutate. Not both.
## Thresholds
- File — 300 lines max, warn at 200
- Method — 10 lines ideal, 7 warn
- Class — 6 public methods, 3 ivars, 200 lines
- Params — 3 positional max; keyword args for 3+
- Nesting — 2 levels max inside a method
## Ruby style
- `# frozen_string_literal: true` on every `.rb`
- Double-quoted strings always; single only inside regex or `'\1'` backrefs
- One-line comments. No YARD blocks, no section separators
- Comments explain WHY, never WHAT
- `snake_case` throughout
- Zeitwerk autoloading — file name matches class name
## Rails view style
- Prefer Rails tag helpers for dynamic or localized markup: `tag.p t("key")`, `tag.section class: state_class`, `tag.meta name: "viewport", content: "width=device-width,initial-scale=1"`.
- Prefer `tag.*` blocks for semantic containers with dynamic attributes, Turbo data, ARIA, or localized content.
- Keep plain HTML when markup is static and clearer as HTML. Do not turn views into helper soup.
- Never hardcode user-facing text in reusable views when `t("key", default: "text")` is practical.
- Prefer semantic tags and bare CSS targeting: `nav a`, `main section`, `article header`. Do not add class attributes where a tag selector works.
- Use Rails form, link, button, Turbo, and tag helpers instead of hand-rolled HTML when the helper carries routing, CSRF, method, data, or escaping semantics.
Bugs to avoid:
- `Dir.chdir` — process-wide, thread-unsafe. Use `File.expand_path`.
- `Prism.parse(src, freeze: true)` — `freeze:` dropped in 3.4. Use `Prism.parse(src)`.
- `next if` inside `flat_map` — returns `nil`. Use `next [] if`.
- Backtick shell with interpolation — use `Open3.capture2e(*%w[cmd], arg)`.
## Zsh / shell
Banned in zsh and SSH: `sed`, `awk`, `tr`, `grep`, `cut`, `head`, `tail`, `find`, `wc`, `sudo`, `perl`, `ruby`, `dd`, `xargs`. Use zsh builtins, parameter expansion, `doas` for privilege, Ruby scripts for complex logic.
Read files over SSH with `cat path` — read the whole file once. Do not stitch `grep` + `head` fragments; reasoning from full context beats reasoning from snippets. For local zsh array work use `lines=("${(@f)$(<file)}")`.
## Architecture
Pipeline: `Intake → Infer → Route → Guard → Execute → [Council ‖ Lint] → Prune → Memo → Render`. Council and Lint run concurrently under a 30s deadline via `ParallelGroup`. Rollback on `axiom_violation` or `validation`: `git reset --hard HEAD`. Scan rules auto-register via the `Rule.inherited` callback — every file under `scan/rules/` must subclass `Rule` or it goes silently unrun. Rules with no constructor args set `def auto_build? = true` to opt into the registry's zero-arg construction path. `axiom_coverage_rule` walks `scan/rules/*.rb` with a Prism `SuperclassFinder` and flags any file whose top-level class does not inherit from `Rule`, so silent registry drift is caught at scan time. All rules ship with `@auto_fix = true` and participate in sweep. Sweep runs rubocop autocorrect first, then escalates to LLM rewrite under the corruption guards.
Council deliberation samples a focus question per persona per turn from `data/council_questions.yml` (8 categories — assumptions, failure_modes, attacker, edge_cases, degradation, ops_maint, economics, clarity). Architect → assumptions, Skeptic → failure_modes, Security → attacker, User → edge_cases, Pragmatist → economics, Mentor → clarity. Unmapped personas pass through with no question.
Observability: `Master::Telemetry` is a soft-optional OpenTelemetry tracer that emits JSONL spans to `.master/traces.log`. Wraps `EventBus#publish`, `Metrics#append`, `AuditLog#append`, and `Heartbeat#execute_job`. Bootstrap fires in `Master.boot` between Pledge stage1 and stage2.
Key files — `data/soul.yml` (golden rule, tiers, persona), `data/rules.yml` (structural rules, thresholds, depths), `data/ruby_style.yml` (style and bugs), `data/workflow.yml` (READ_BEFORE_WRITE, scan principles), `data/standing_orders.yml` (current FSM state).
## Running scans
Standard: `eval "$(grep '^export' ~/.zshrc)" && cd ~/pub4/MASTER && echo "/scan lib/" | bundle exec ruby bin/cli`. Autofix sweep: `/autoloop 20`. Do not use external agents when MASTER can scan itself. Depth knobs are gone — every scan is full by default.
Pre-commit constitution check: `bin/audit` runs the scanner over staged files and fails the commit on any kernel-tier rule or critical/error violation. Wire as a git pre-commit hook by writing `exec bin/audit` into `.git/hooks/pre-commit`.
## Protection tiers
ABSOLUTE aborts the pipeline. PROTECTED emits a warning and continues. NEGOTIABLE allows if explicitly permitted. FLEXIBLE negotiates at runtime. ABSOLUTE sections in `data/soul.yml` require `/override` to amend.
## Environment
VPS: `dev@brgen.no` · OpenBSD 7.8 · passwordless `doas`. SSH credentials live in the operator's environment, never in versioned docs. Non-interactive SSH must not source `.zshrc` — load env only: `eval "$(grep '^export' ~/.zshrc)"`.
Edit VPS files by direct edit + `scp` — write the new file content locally, scp it up. Reserve `~/pub4/tmp/patch.rb` for genuinely script-shaped edits where a patch script is the right tool. Never use `ruby -i` with heredoc — empties the file on script error.
After every scp under `MASTER/web/`, immediately `doas rcctl restart master` so Falcon picks up the change. Falcon does not hot-reload in production; without the restart the deployed app keeps serving the prior bytecode.
## Web auth tiers
Token in `~/pub4/.master/config.yml` is accepted via `Authorization: Bearer`, `X-Token` header, or `master_session` cookie. First-hit `?token=...` triggers the handshake: AuthTier sets an `HttpOnly; Secure; SameSite=Strict` cookie and 302s to the same path with the token stripped, keeping the secret out of logs and history. No credential = visitor — chat works, but `Thread.current[:master_visitor]` is set so `Master::Agent::LlmDispatch#build_llm_tools` filters tools to the visitor allow-list (currently `AskLlm`, `WebSearch`). The CLI REPL bypasses this entirely and always has full access.
## Slash commands
`/scan [profile] [path]`, `/fix [path]`, `/ecology [path]`, `/review [on|off|path]`, `/critique <file|text>`, `/swarm <role> <task>`, `/ideate <prompt>`, `/topic`, `/rsi [stats]`, `/model [list|<id>]`, `/why <rule>`, `/diag [section]`, `/snapshot`, `/tts`, `/brief`, `/heartbeat`, `/orders`, `/soul`, `/dmesg`. `/scan` reports violations (read-only). `/fix` iterates scan→LLM-rewrite→scan until violations plateau, stall, or reach zero — stops on diminishing returns automatically. `/review` runs council deliberation. `/snapshot` publishes two GitHub gists — MASTER + DEPLOY. `/why` resolves locally first; LLM fires only on a miss.# frozen_string_literal: true
source "https://rubygems.org"
gem "fiddle"
gem "ruby_llm", "~> 1.3"
gem "tty-prompt", "~> 0.23"
gem "tty-reader", "~> 0.9"
gem "tty-spinner", "~> 0.9"
gem "tty-markdown", "~> 0.7"
gem "tty-table", "~> 0.12"
gem "tty-screen", "~> 0.8"
gem "tty-box", "~> 0.7"
gem "tty-command", "~> 0.10"
gem "tty-tree", "~> 0.4"
gem "tty-config", "~> 0.6"
gem "tty-logger", "~> 0.6"
gem "tty-progressbar", "~> 0.18"
gem "pastel", "~> 0.8"
gem "rouge", "~> 4.4"
gem "diffy", "~> 3.4"
gem "zeitwerk", "~> 2.7"
gem "sinatra", "~> 4.0"
gem "sinatra-contrib", "~> 4.0"
gem "rb-edge-tts", git: "https://github.com/ZPVIP/rb-edge-tts"
group :test do
gem "minitest", ">= 5.25"
gem "rack-test", "~> 2.1"
gem "ferrum", "~> 0.15"
gem "simplecov", require: false
end
gem "ruby_llm-mcp"
gem "rubocop", "~> 1.60", require: false
gem "reek", "~> 6.4", require: false
gem "flay", require: false
gem "opentelemetry-sdk", "~> 1.11", require: false
gem "sqlite3", "~> 2.9"
# Architecture #7: file-watcher reactive trigger (kqueue on OpenBSD, inotify on Linux)
# Skipped on Android/Termux — inotify gem unavailable there.
if RUBY_PLATFORM =~ /bsd|dragonfly/i
gem "rb-kqueue", "~> 0.2", require: false
elsif RUBY_PLATFORM =~ /linux/ && !RUBY_PLATFORM.include?("android")
gem "rb-inotify", "~> 0.10", require: false
end
# MASTER Quickstart (External LLMs)
MASTER is a constitutional coding agent in Ruby. Read this first, then run `/orient` for full doctrine.
1) Golden rule
- Preserve, then improve, never break.
- Read full files before editing.
- Keep patches minimal and reversible.
2) Non-negotiables
- No fabricated claims; show evidence from files/commands.
- No bare `rescue`; rescue specific exceptions.
- Prefer named constants over magic literals.
- Use string methods before regex when possible.
- Dependency-inject collaborators; avoid hidden instantiation.
3) Style baseline
- `# frozen_string_literal: true` in Ruby files.
- Double-quoted strings.
- Guard clauses first.
- Endless method style for single expressions.
- Clear names; avoid abbreviations like `idx`, `tmp`, `sig`.
4) How MASTER works
- Pipeline: Intake → Infer → Route → Guard → Execute → Council/Lint → Prune → Memo → Render.
- Scans enforce structure/style rules from `data/rules.yml` and `data/ruby_style.yml`.
- Fixes are applied through FixLoop and must remain safe and auditable.
5) Core commands
- `/scan [profile] [path]` check a file/dir.
- `/fix [path]` apply fixes.
- `/diag` runtime snapshot.
- `/why <rule>` explain one rule.
- `/help` command catalog.
6) Web auth model
- Token-authenticated operator gets full tools.
- Visitor mode is restricted to safe tools.
7) If uncertain
- Ask for the specific rule section instead of guessing.
- Prefer explicit tradeoffs and smallest safe change.
Full reference: `CONVENTIONS.md` and `/orient`.# MASTER
Constitutional AI runtime for any text artifact — code, prose, design, structure. Ruby. OpenBSD. Self-hosting.
Models propose. The constitution validates. Convergence loops digest violations. Memory learns what fixes stick. Pressure fields track epistemic health. Providers compete by capability, cost, and evidence.
## Quickstart
```sh
cd MASTER
bundle install
bundle exec ruby bin/cliPipe 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
Four layers:
- Brain — declarative constitution, standing orders, roles, memory policy, provider routing, governance.
- Runtime — append-only events, telemetry, checkpoints, replay state, queues, locks, provider health, hot cache.
- Orchestration — routing, voting, fallback, quorum, workflow execution, tool contracts, convergence loops.
- 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.
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 |
- 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.
observe → classify → propose → sandbox → validate → merge
Failures become data. Data becomes playbooks. Playbooks become safer defaults.
| 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.
| 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.
now · loop · judge · voice · ground · reach · trace
Constitution lives in data/. Runtime state in .master/. Knowledge store at .master/knowledge.sqlite3.
Bundler 403 on install: proxy is blocking rubygems.org. Check gem sources --list and env | grep -i proxy. Install a single gem to isolate: gem install zeitwerk -v 2.7.5.
MIT.
## `Rakefile`
```text
# frozen_string_literal: true
require "rake/testtask"
Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList["test/test_*.rb"]
t.warning = false
end
desc "Deep scan lib/ — exit 1 if any violations found (static rules, no LLM)"
task :constitution do
$LOAD_PATH.unshift(File.join(__dir__, "lib"))
require "master"
root = __dir__
scanner = Master::Scan::Scanner.new
Master::Scan::Rule.registry.select(&:auto_build?).each { |klass| scanner.add_rule(klass.new) }
scanner.add_rule(Master::Scan::Rules::RuleCoverageRule.new(root:))
scanner.add_rule(Master::Scan::Rules::RubocopRule.new(root:))
scanner.add_rule(Master::Scan::Rules::ReekRule.new(root:))
scanner.add_rule(Master::Scan::Rules::InterconnectRule.new(root:))
result = scanner.scan_dir(File.join(root, "lib"), depth: :deep, stream: false)
abort "constitution: scan failed: #{result.message}" unless result.respond_to?(:ok?) && result.ok?
violations = result.value!.flat_map { |_f, r| (r.respond_to?(:ok?) && r.ok?) ? r.value! : [] }
total = violations.size
if total.zero?
puts "constitution: clean"
else
by_rule = violations.group_by { |v| v[:rule] }
by_rule.sort_by { |_, vs| -vs.size }.each do |rule, vs|
puts "[#{rule}] #{vs.size}"
vs.first(5).each { |v| puts " #{v[:file]}:#{v[:line]}: #{v[:message]}" }
end
puts "constitution: #{total} violation(s)"
exit 1
end
end
task default: :test
namespace :test do
desc "Run web system tests"
task :web do
sh "ruby -Ilib:test test/test_web_ui.rb"
end
end
namespace :lint do
desc "Check all Ruby files have # frozen_string_literal: true"
task :frozen do
missing = Dir.glob(File.join(__dir__, "lib", "**", "*.rb")).reject do |f|
File.read(f, 100).include?("# frozen_string_literal: true")
end
if missing.empty?
puts "lint:frozen: all files frozen"
else
missing.each { |f| puts " missing: #{f.sub("#{__dir__}/", "")}" }
abort "lint:frozen: #{missing.size} file(s) missing frozen_string_literal"
end
end
end
desc "Full audit: constitution + lint:frozen"
task audit: %i[constitution lint:frozen] do
puts "audit: passed"
end
# 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.# 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: InjectionGuardpack: 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# 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"# 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]# Cost ceiling for a session. Routes degrade strong -> fast -> cheap as spend climbs.
# Source: master2 reunification.
budget:
limit: 10.0
currency: USD
thresholds:
strong: 5.0
fast: 1.0
cheap: 0.0
on_exceed:
action: degrade_route
notify: bus
# Beyond dollars, track approximate kWh / CO2 per session.
# Source: cross-cutting reunification (#98).
carbon_budget:
kwh_per_million_tokens: 0.5 # rough industry estimate
co2_g_per_kwh: 400 # mixed grid baseline
daily_kwh_cap: 1.0
on_exceed: "switch to local model only; publish carbon:exceeded"# 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---
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.---
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.---
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.---
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.---
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.---
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).---
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.---
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---
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.---
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`)---
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`.---
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.---
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.---
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.---
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.---
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.---
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---
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.---
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.---
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.---
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.---
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.---
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.---
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.---
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.---
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.---
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.---
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.---
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)---
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)---
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.---
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.---
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.---
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`.---
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)---
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.---
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.---
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.---
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.---
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.---
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.# 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."# 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.erbtypography:
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# 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# 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"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)"# Heartbeat jobs — all disabled. Schedule-based maintenance removed.
# These actions are now triggered by events or manual slash commands:
# prune_memory → /dreams
# self_test → auto_default standing order (fires after any source mutation)
# prune_undo → manual: /prune undo
# snapshot → manual: /snapshot
- name: prune_memory
action: prune_memory
enabled: false
description: Use /dreams instead.
- name: self_test
action: self_test
enabled: false
description: Covered by auto_default standing order.
- name: prune_undo
action: prune_undo
enabled: false
description: Use /prune undo instead.
- name: snapshot
action: snapshot
enabled: false
description: Use /snapshot instead.# 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"# 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]# 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# 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# 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# Model routing profile — DeepSeek primary, OpenRouter free-tier fallback.
routing:
enabled: true
strategy: weighted
escalation_enabled: true
escalation_tier: strong
provider: deepseek
weights: &weights
quality: 0.50
speed: 0.25
cost: 0.25
fallback_policy:
retries_per_tier: 1
on:
- timeout
- network_error
- refusal
- insufficient_balance
- rate_limit
- quota_exceeded
defaults: &model_defaults
score: { quality: 0.0, speed: 0.0, cost: 0.0 }
model_defs:
gemini_flash: &gemini_flash
id: gemini-2.5-flash
<<: *model_defaults
score: { quality: 0.88, speed: 0.90, cost: 0.95 }
gemini_pro: &gemini_pro
id: gemini-2.5-pro
<<: *model_defaults
score: { quality: 0.95, speed: 0.70, cost: 0.80 }
mistral_large: &mistral_large
id: mistralai/mistral-large
<<: *model_defaults
score: { quality: 0.90, speed: 0.75, cost: 0.70 }
mistral_small: &mistral_small
id: mistralai/mistral-small-3.1-24b
<<: *model_defaults
score: { quality: 0.78, speed: 0.85, cost: 0.90 }
deepseek_chat: &deepseek_chat
id: deepseek-chat
<<: *model_defaults
score: { quality: 0.96, speed: 0.85, cost: 0.95 }
deepseek_reasoner: &deepseek_reasoner
id: deepseek-reasoner
<<: *model_defaults
score: { quality: 0.97, speed: 0.55, cost: 0.85 }
deepseek_coder: &deepseek_coder
id: deepseek-coder
<<: *model_defaults
score: { quality: 0.85, speed: 0.70, cost: 0.95 }
claude_sonnet: &claude_sonnet
id: anthropic/claude-sonnet-4-6
<<: *model_defaults
score: { quality: 0.95, speed: 0.75, cost: 0.60 }
nemotron_super: &nemotron_super
id: nvidia/nemotron-3-super-120b-a12b:free
<<: *model_defaults
score: { quality: 0.90, speed: 0.88, cost: 1.0 }
qwen_coder: &qwen_coder
id: qwen/qwen3-coder:free
<<: *model_defaults
score: { quality: 0.75, speed: 0.65, cost: 1.0 }
qwen3_next: &qwen3_next
id: qwen/qwen3-next-80b-a3b-instruct:free
<<: *model_defaults
score: { quality: 0.82, speed: 0.88, cost: 1.0 }
gpt_oss_120b: &gpt_oss_120b
id: openai/gpt-oss-120b:free
<<: *model_defaults
score: { quality: 0.86, speed: 0.70, cost: 1.0 }
llama_70b: &llama_70b
id: meta-llama/llama-3.3-70b-instruct:free
<<: *model_defaults
score: { quality: 0.78, speed: 0.70, cost: 1.0 }
hermes_405b: &hermes_405b
id: nousresearch/hermes-3-llama-3.1-405b:free
<<: *model_defaults
score: { quality: 0.85, speed: 0.50, cost: 1.0 }
gpt_4o: &gpt_4o
id: openai/gpt-4o
<<: *model_defaults
score: { quality: 0.93, speed: 0.80, cost: 0.55 }
claude_cli_sonnet: &claude_cli_sonnet
id: claude-cli:claude-sonnet-4-6
<<: *model_defaults
score: { quality: 0.95, speed: 0.70, cost: 0.60 }
claude_cli_opus: &claude_cli_opus
id: claude-cli:claude-opus-4-7
<<: *model_defaults
score: { quality: 0.99, speed: 0.50, cost: 0.30 }
gemma_2_9b_free: &gemma_2_9b_free
id: google/gemma-2-9b-it:free
<<: *model_defaults
score: { quality: 0.72, speed: 0.88, cost: 1.0 }
gemma_2_27b: &gemma_2_27b
id: google/gemma-2-27b-it
<<: *model_defaults
score: { quality: 0.82, speed: 0.78, cost: 0.92 }
gemini_2_flash_exp_free: &gemini_2_flash_exp_free
id: google/gemini-2.0-flash-exp:free
<<: *model_defaults
score: { quality: 0.85, speed: 0.92, cost: 1.0 }
gemini_flash_lite: &gemini_flash_lite
id: google/gemini-flash-lite-latest
<<: *model_defaults
score: { quality: 0.78, speed: 0.96, cost: 0.97 }
phi_4_free: &phi_4_free
id: microsoft/phi-4:free
<<: *model_defaults
score: { quality: 0.74, speed: 0.90, cost: 1.0 }
glm_4_5_air_free: &glm_4_5_air_free
id: z-ai/glm-4.5-air:free
<<: *model_defaults
score: { quality: 0.80, speed: 0.82, cost: 1.0 }
yi_lightning: &yi_lightning
id: 01-ai/yi-lightning
<<: *model_defaults
score: { quality: 0.78, speed: 0.94, cost: 0.94 }
command_r_plus: &command_r_plus
id: cohere/command-r-plus
<<: *model_defaults
score: { quality: 0.86, speed: 0.72, cost: 0.65 }
grok_4_fast: &grok_4_fast
id: x-ai/grok-4-fast
<<: *model_defaults
score: { quality: 0.88, speed: 0.92, cost: 0.78 }
reka_flash: &reka_flash
id: rekaai/reka-flash-3:free
<<: *model_defaults
score: { quality: 0.74, speed: 0.86, cost: 1.0 }
deepseek_v3_free: &deepseek_v3_free
id: deepseek/deepseek-chat-v3.1:free
<<: *model_defaults
score: { quality: 0.95, speed: 0.85, cost: 1.0 }
llama_4_scout_free: &llama_4_scout_free
id: meta-llama/llama-4-scout:free
<<: *model_defaults
score: { quality: 0.84, speed: 0.78, cost: 1.0 }
groq_llama_3_3_70b: &groq_llama_3_3_70b
id: groq/llama-3.3-70b-versatile
<<: *model_defaults
score: { quality: 0.85, speed: 0.99, cost: 0.85 }
cerebras_llama_3_1_8b: &cerebras_llama_3_1_8b
id: cerebras/llama-3.1-8b
<<: *model_defaults
score: { quality: 0.70, speed: 0.99, cost: 0.92 }
ollama_qwen: &ollama_qwen
id: ollama:qwen2.5-coder:7b
<<: *model_defaults
score: { quality: 0.62, speed: 0.85, cost: 1.0 }
ollama_llama: &ollama_llama
id: ollama:llama3.2:3b
<<: *model_defaults
score: { quality: 0.55, speed: 0.92, cost: 1.0 }
ollama_phi: &ollama_phi
id: ollama:phi4:mini
<<: *model_defaults
score: { quality: 0.58, speed: 0.95, cost: 1.0 }
models:
default:
- *nemotron_super
- *gpt_oss_120b
- *qwen3_next
- *llama_70b
- *qwen_coder
strong:
- *deepseek_reasoner
- *deepseek_chat
- *gemini_pro
- *mistral_large
- *claude_sonnet
- *gpt_4o
- *gemini_flash
- *command_r_plus
cheap:
- *nemotron_super
- *gemini_flash
- *mistral_small
- *llama_70b
- *qwen_coder
- *gemma_2_9b_free
- *gemini_flash_lite
- *yi_lightning
fast:
- *groq_llama_3_3_70b
- *cerebras_llama_3_1_8b
- *gemini_flash_lite
- *gemini_2_flash_exp_free
free:
- *gemini_2_flash_exp_free
- *gemma_2_9b_free
- *llama_4_scout_free
- *phi_4_free
- *glm_4_5_air_free
- *reka_flash
- *nemotron_super
- *qwen_coder
- *llama_70b
- *hermes_405b
claude_code:
- *claude_cli_sonnet
- *claude_cli_opus
local:
- *ollama_phi
- *ollama_llama
- *ollama_qwen
routes:
code_generation: default
refactoring: default
architecture: strong
review: default
explanation: cheap
exploration: cheap
fallback_default: cheap
tool_capable_prefixes:
- claude
- claude-cli
- gpt-4
- gpt-4o
- gemini
- mistral
- mistralai
- mixtral
- llama-3.1
- llama-3.3
- llama-4
- qwen
- command-r
- cohere/command
- deepseek
- stepfun
- nvidia
- nemotron
- meta/meta-llama
- anthropic/claude
- openai/gpt
- google/gemini
- google/gemma
- microsoft/phi
- z-ai/glm
- 01-ai/yi
- x-ai/grok
- rekaai/reka
- groq
- cerebras
operation_constraints:
# Operations that write files, run autoloop/sweep, or execute destructive commands
# require a model with quality score >= 0.88 (default and cheap tiers excluded).
# Equivalent to: claude-sonnet-4-6, gemini-2.5-pro, mistral-large, gpt-4o.
file_write: { min_quality: 0.88, preferred_tier: strong }
autoloop: { min_quality: 0.88, preferred_tier: strong }
sweep: { min_quality: 0.88, preferred_tier: strong }
council: { min_quality: 0.88, preferred_tier: strong }
scan_semantic: { min_quality: 0.88, preferred_tier: strong }
scan_adversarial: { min_quality: 0.88, preferred_tier: strong }
destructive: { min_quality: 0.90, preferred_tier: strong }
continuity:
enabled: true
updated_at: "2026-05-01T00:00:00Z"
openrouter:
free_latest:
- nvidia/nemotron-3-super-120b-a12b:free
- openai/gpt-oss-120b:free
- qwen/qwen3-next-80b-a3b-instruct:free
- meta-llama/llama-3.3-70b-instruct:free
- qwen/qwen3-coder:free
# Provider trust tracked over time; weights routing beyond raw cost.
# Source: master.json v225 reunification (#52).
trust_scoring:
initial_score: 0.50
success_increment: 0.02
failure_decrement: 0.10
deprecate_below: 0.20
persist_to: "data/provider_trust.yml"
consider_in_routing: true
three_mirror_redundancy:
# Three models vote; ship only on >= 2 agreement for critical fixes.
# Source: cross-cutting reunification (#95).
enabled_for: [tier1_critical, security_relevant, irreversible]
pool: [openrouter_primary, openrouter_secondary, claude_cli]
quorum: 2
on_disagreement: "fall back to council vote with all dissent recorded"
# Tier-D (local ollama) — env-gated; activates only when OLLAMA_BASE_URL is set.
# Default: http://localhost:11434/v1 (OpenAI-compatible endpoint).
# Trust starts at 0.40 (below cloud providers) until measured success raises it.
ollama:
enabled_when_env: OLLAMA_BASE_URL
default_base_url: "http://localhost:11434/v1"
initial_trust: 0.40
use_for: [exploration, fallback_default] # cheapest-acceptable tasks only
never_for: [council, sweep, autoloop, file_write, destructive]# 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"# Platform/tool idioms — gh, openbsd, zsh — and intent/research namespaces
# merged from former *_patterns.yml + repo_topic_clusters + prompt_archaeology.
# Each top-level key is a namespace.
---
gh:
operations:
- action: create_pr
pattern: gh pr create --title '${title}' --body '${body}' --base main
- action: merge_pr
pattern: gh pr merge ${number} --squash --delete-branch
- action: create_issue
pattern: gh issue create --title '${title}' --body '${body}' --label '${labels}'
- action: close_issue
pattern: gh issue close ${number} --reason completed
- action: list_workflows
pattern: gh run list --workflow=${workflow} --limit 5
- action: trigger_workflow
pattern: gh workflow run ${workflow} --ref ${branch}
- action: review_pr
pattern: gh pr review ${number} --approve --body '${comment}'
- action: check_status
pattern: gh pr checks ${number} --watch
- action: clone_repo
pattern: gh repo clone ${owner}/${repo}
- action: fork_repo
pattern: gh repo fork ${owner}/${repo} --clone
- action: api_call
pattern: gh api ${endpoint}
forbidden:
- command: curl api.github.com
replacement: gh api
- command: curl github.com/api
replacement: gh api
- command: hub
replacement: gh — hub is deprecated
openbsd:
service_commands:
enable: rcctl enable ${service}
start: rcctl start ${service}
restart: rcctl restart ${service}
reload: rcctl reload ${service}
check: rcctl check ${service}
disable: rcctl disable ${service}
configuration_paths:
pf: "/etc/pf.conf"
httpd: "/etc/httpd.conf"
relayd: "/etc/relayd.conf"
smtpd: "/etc/mail/smtpd.conf"
acme: "/etc/acme-client.conf"
sshd: "/etc/ssh/sshd_config"
ntp: "/etc/ntpd.conf"
cron: "/var/cron/tabs/${user}"
unbound: "/var/unbound/unbound.conf"
package_operations:
install: pkg_add ${package}
remove: pkg_delete ${package}
search: pkg_info -Q ${query}
update: pkg_add -u
firmware: fw_update
prohibited_commands:
- command: systemctl
replacement: rcctl
- command: apt
replacement: pkg_add
- command: apt-get
replacement: pkg_add
- command: brew
replacement: pkg_add
- command: yum
replacement: pkg_add
- command: ip addr
replacement: ifconfig
- command: ip route
replacement: route
- command: journalctl
replacement: cat /var/log/messages
- command: sudo
replacement: doas
- command: ufw
replacement: pfctl
- command: iptables
replacement: pf
- command: nginx
replacement: httpd (OpenBSD native)
- command: docker
replacement: vmctl
- command: systemd
replacement: rcctl
- command: gsed
replacement: sed (POSIX)
- command: gawk
replacement: awk (POSIX)
- command: ggrep
replacement: grep (POSIX)
security:
pledge: pledge(2) – restrict syscalls after init
unveil: unveil(2) – restrict filesystem visibility
doas: doas.conf – preferred over sudo
signify: signify(1) – cryptographic signing
chroot: httpd runs chrooted by default
daemon_configs:
pf.conf:
daemon: pf
man: pf.conf.5
required_patterns:
- set skip on lo
warnings:
- pattern: pass all
message: Overly permissive rule
nsd.conf:
daemon: nsd
man: nsd.conf.5
required_patterns:
- 'server:'
- 'zone:'
warnings:
- pattern: rrl-size
absent_message: Missing RRL config for DDoS protection
- pattern: hide-version
absent_message: 'Consider hide-version: yes'
httpd.conf:
daemon: httpd
man: httpd.conf.5
required_patterns: []
warnings: []
smtpd.conf:
daemon: smtpd
man: smtpd.conf.5
required_patterns:
- listen on
- action
- match
warnings:
- pattern: match from any
message: Potential open relay
relayd.conf:
daemon: relayd
man: relayd.conf.5
required_patterns:
- relay
warnings: []
acme-client.conf:
daemon: acme-client
man: acme-client.conf.5
required_patterns:
- authority
- domain
warnings: []
doas.conf:
daemon: doas
man: doas.conf.5
required_patterns:
- permit
warnings:
- pattern: nopass
message: Allows password‑less escalation
sshd_config:
daemon: sshd
man: sshd_config.5
required_patterns: []
warnings:
- pattern: PermitRootLogin yes
message: Security risk – disallow root login
- pattern: PasswordAuthentication yes
message: Prefer key‑based authentication
ntpd.conf:
daemon: ntpd
man: ntpd.conf.5
required_patterns:
- server
warnings: []
unbound.conf:
daemon: unbound
man: unbound.conf.5
required_patterns:
- 'server:'
warnings: []
zsh:
forbidden_commands:
- command: awk
replacement: 'zsh array/string field splitting: ${${(s:,:)line}[4]}'
- command: sed
replacement: 'zsh parameter expansion: ${var//search/replace}'
- command: tr
replacement: 'zsh case conversion: ${(L)var} ${(U)var}'
- command: grep
replacement: 'zsh pattern matching: ${(M)arr:#*pattern*}'
- command: cut
replacement: 'zsh field splitting: ${${(s:delim:)var}[N]}'
- command: head
replacement: 'zsh array slicing: ${arr[1,10]}'
- command: tail
replacement: 'zsh array slicing: ${arr[-5,-1]}'
- command: uniq
replacement: 'zsh unique flag: ${(u)arr}'
- command: sort
replacement: 'zsh sort flags: ${(o)arr} (asc) / ${(O)arr} (desc)'
- command: bash
replacement: zsh — never use bash
- command: find
replacement: 'zsh glob qualifiers: **/*.rb(.)'
- command: wc
replacement: 'zsh length/count: ${#var} / ${#arr}'
- command: sudo
replacement: doas on OpenBSD
native_patterns:
string_replace: "${var//find/replace}"
case_lower: "${(L)var}"
case_upper: "${(U)var}"
trim_whitespace: "${${var##[[:space:]]#}%%[[:space:]]#}"
split_to_array: "${(s:delim:)var}"
array_join: "${(j:,:)arr}"
array_unique: "${(u)arr}"
array_sort_asc: "${(o)arr}"
array_sort_desc: "${(O)arr}"
array_reverse: "${(Oa)arr}"
array_filter_match: "${(M)arr:#*pattern*}"
array_filter_exclude: "${arr:#*pattern*}"
remove_crlf: "${var//$'\\r'/}"
exceptions:
- Complex regex requiring PCRE
- Multi‑file operations beyond globbing
- Binary data processing
banned_commands:
- python
- bash
- sed
- awk
- tr
- wc
- head
- tail
- cut
- find
- sudo
auto_remediation:
awk: "${${(s: :)line}[n]}"
sed: "${var//old/new}"
tr: "${(U)var} or ${(L)var}"
wc: "${#lines}"
head: "${lines[1,n]}"
tail: "${lines[-n,-1]}"
grep: "${(M)lines:#*pattern*}"
cut: "${${(s:delim:)var}[N]}"
sort: "${(o)arr} or ${(O)arr}"
find: "**/*.ext(.)"
sudo: doas
token_economics:
philosophy: 'Replacing multi‑tool shell pipelines with pure Zsh parameter expansion
eliminates process boundaries, collapses multiple grammars into one, reduces
reasoning entropy for LLMs, and converts runtime overhead into in‑memory transforms
— saving both tokens and wall‑clock time.
'
example_bad:
code: awk -F, '{print $4}' | sed 's/\r//g' | tr '[:upper:]' '[:lower:]'
cost: 3 grammars, pipes + subshells, I/O transformations
example_good:
code: cleaned=${var//$'\r'/}; lower=${(L)cleaned}; fourth=${${(s:,:)lower}[4]}
cost: One grammar, one evaluation model, no process boundaries
benefit: Model reasons locally instead of globally across pipeline
ssh_reading:
rule: cat path — read the whole file once, never stitch output with grep/head/tail
rationale: Partial reads yield partial (wrong) changes. Same rule as workflow
READ_FULL_FILES.
infer:
commands:
sweep:
patterns:
- "\\b(?:sweep|refactor|clean\\s*up|rewrite|polish|tidy\\s*up|overhaul|improve\\s+(?:all|every)|go\\s+through\\s+(?:all|every)|full\\s+pass\\s+(?:over|on))(?:\\s+(?:all|every(?:thing)?|the))?(?:\\s+([\\w\\/.]+))?"
- "\\b(?:rydd\\s+opp|refaktorer|forbedre?|gjennomg[åa]|omskriv)(?:\\s+([\\w\\/.]+))?"
capture: path
autoloop:
patterns:
- "\\b(?:autoloop|autofix|fix\\s+all\\s+violations?|keep\\s+(?:fix|loop)|loop\\s+until|iterate\\s+until|run\\s+until\\s+clean|keep\\s+going\\s+until|(?:run|go)\\s+(?:it\\s+)?(?:again\\s+)?until\\s+(?:done|clean|fixed|perfect))(?:\\s+(\\d+))?"
- "\\b(?:fiks?\\s+alle?\\s+(?:feil|brudd)|fortsett\\s+(?:til|inntil)|kj[øo]r\\s+(?:til\\s+)?(?:det\\s+er\\s+)?(?:rent|bra|ferdig))(?:\\s+(\\d+))?"
capture: cycles
council:
patterns:
- "\\b(?:council|deliberat|multiple\\s+perspect|second\\s+opinion|peer\\s+review|debate\\s+this|get\\s+(?:another|a\\s+second)\\s+view|multi(?:ple)?\\s+(?:view|agent|model|perspect))\\b"
- "\\b(?:r[åa]dsl[åa]g|bruk\\s+(?:flere|multiple)\\s+(?:perspektiv|synsvinkler?)|diskuter\\s+(?:dette|det))\\b"
capture: on_off
explain:
patterns:
- "\\b(?:explain\\s+(?:your(?:self)?|your\\s+architecture|how\\s+you\\s+work)|describe\\s+(?:your(?:self)?|your\\s+architecture)|what\\s+are\\s+you|how\\s+(?:are\\s+you\\s+built|do\\s+you\\s+work)|show\\s+(?:your\\s+)?architecture|self[\\s-]?map)\\b"
capture: none
persona:
patterns:
- "\\b(?:(?:switch|change|set)\\s+persona\\s+(?:to\\s+)?(\\w+)|persona\\s+(\\w+)|use\\s+(\\w+)\\s+persona)\\b"
capture: persona_name
memory:
patterns:
- "\\b(?:what\\s+do\\s+you\\s+remember(?:\\s+about\\s+([\\w\\s]+))?|show\\s+(?:my\\s+)?memor(?:y|ies)|list\\s+memor(?:y|ies)|recall(?:\\s+([\\w]+))?|what(?:'s|\\s+is)\\s+in\\s+(?:your\\s+)?memory|remember\\s+([\\w]+=.+)|forget\\s+([\\w_]+))\\b"
- "\\b(?:hva\\s+husker\\s+du(?:\\s+om\\s+([\\w\\s]+))?|vis\\s+(?:min\\s+)?hukommelse|husk\\s+([\\w_]+=.+))\\b"
capture: first_group
tokens:
patterns:
- "\\b(?:token\\s*count|how\\s+many\\s+tokens?|context\\s+size|token\\s+usage|how\\s+much\\s+context|hvor\\s+mange\\s+token|token\\s*antall)\\b"
capture: none
cost:
patterns:
- "\\b(?:how\\s+much\\s+(?:has\\s+this\\s+cost|did\\s+this\\s+cost)|(?:current\\s+)?(?:spend|cost|budget)|what(?:'s|\\s+is)\\s+the\\s+cost|hva\\s+koster?\\s+(?:dette|det)|kostnader?)\\b"
capture: none
undo:
patterns:
- "\\b(?:undo\\s+that|revert\\s+(?:that|last|it)|go\\s+back|take\\s+that\\s+back|angre\\s+det|g[åa]\\s+tilbake)\\b"
capture: none
clear:
patterns:
- "\\b(?:clear\\s+(?:context|chat|history|session|screen)|start\\s+(?:over|fresh|again)|reset\\s+(?:context|session)|fresh\\s+start|t[øo]m\\s+(?:kontekst|historikk)|begynn\\s+p[åa]\\s+nytt)\\b"
capture: none
save:
patterns:
- "\\b(?:save\\s+(?:session|this|my\\s+work|progress)|checkpoint\\s+now|lagre\\s+(?:session|sesjonen?|arbeid))\\b"
capture: none
model:
patterns:
- "\\b(?:which\\s+model|current\\s+model|what\\s+model\\s+are\\s+you|what\\s+(?:llm|ai|model)\\s+(?:are\\s+you\\s+using|is\\s+this))\\b"
capture: none
scan:
patterns:
- "\\b(?:scan|lint|check\\s+(?:code|violations?)|run\\s+scan)(?:\\s+(deep))?\\b"
capture: scan_depth
dmesg:
patterns:
- "\\b(?:show\\s+(?:logs?|events?)|system\\s+log|dmesg|what\\s+(?:happened|has\\s+happened)|recent\\s+activity)\\b"
capture: none
dreams:
patterns:
- "\\b(?:dreams?|consolidate?\\s+memor(?:y|ies)|memory\\s+consolidat|dream\\s+mode|promote\\s+memor(?:y|ies))\\b"
capture: first_group
soul:
patterns:
- "\\b(?:show|check|view)\\s+(?:the\\s+)?soul\\b"
- "\\bsoul\\s+(?:version|changelog|diff|approve|reject|rollback|propose)\\b"
capture: soul_subcmd
orders:
patterns:
- "\\b(?:standing\\s+orders?|show\\s+orders?|list\\s+orders?)\\b"
capture: orders_subcmd
history:
patterns:
- "\\b(?:show|print|list)\\s+(?:undo\\s+)?histor(?:y|ies)\\b"
- "\\bwhat\\s+(?:did\\s+(?:i|we)\\s+do|have\\s+(?:i|we)\\s+done|was\\s+(?:changed|done))\\b"
capture: none
why:
patterns:
- "\\b(?:which|current|show)\\s+model\\s+(?:routing|selection|why)\\b"
- "\\bwhy\\s+(?:this|that)\\s+model\\b"
- "\\bmodel\\s+scoring\\b"
capture: none
principles:
patterns:
- "\\b(?:show|list|what\\s+are)\\s+(?:the\\s+)?(?:my\\s+)?principles\\b"
- "\\bconstitution(?:al)?\\s+(?:rules?|principles?|axioms?)\\b"
capture: none
propose:
patterns:
- "\\b(?:what(?:'s|\\s+is)\\s+(?:next|suggested)|suggest(?:ed)?\\s+(?:action|step)s?|next\\s+steps?|what\\s+should\\s+(?:i|we)\\s+do)\\b"
capture: none
context:
patterns:
- "\\b(?:show|what(?:'s|\\s+is)\\s+in)\\s+(?:the\\s+)?(?:context|attention)\\s+(?:window|state)?\\b"
- "\\bcontext\\s+window\\b"
capture: none
verify:
patterns:
- "\\b(?:verifie?d?|confirm|check)\\s+(?:everything\\s+(?:is\\s+)?(?:wired|connected|working)|(?:all\\s+)?symbols?)\\b"
capture: none
restart:
patterns:
- "\\b(?:restart|hot[\\s-]?reload|respawn)\\s+(?:master|the\\s+agent|process)?\\b"
capture: none
prompt_archaeology:
policy:
id: safe_prompt_archaeology
rule: 'Use prompt archives as behavioral archaeology, not as source text. Extract
abstract patterns, compare assistant designs, and write first-party MASTER policy
in our own words.
'
forbidden:
- Copying large prompt blocks into MASTER runtime prompts.
- Treating leaked/vendor prompts as authoritative or current.
- Depending on private or unverified system messages for safety-critical rules.
- Removing provenance/risk labels from derived patterns.
allowed:
- Naming recurring design patterns.
- Building checklists and rubrics.
- Comparing orchestration roles and tool-use policies.
- Using archives as weak evidence alongside official docs, local behavior, and
tests.
clusters:
- id: role_stack
name: Role Stack and Identity Boundary
pattern: 'Strong assistants separate identity, task role, tool permissions, safety
posture, output style, and user preference. MASTER should keep these as layers
rather than one giant prompt blob.
'
sharpen_master:
- Split prompt builder into identity, attention context, task mode, tool policy,
output contract, and safety constraints.
- Let attention breadcrumbs set task mode without rewriting identity.
- Keep user-facing tone separate from execution policy.
- id: tool_contracts
name: Tool Contracts and Action Boundaries
pattern: 'Good tool-using assistants make tool authority explicit: when to browse,
when to read files, when to mutate, when to ask, and how to report uncertainty.
'
sharpen_master:
- Represent every tool call as intent + authority + blast radius + rollback.
- Require stronger model/council review for irreversible mutations.
- Emit tool events into visual_bridge and cluster_miner.
- id: attention_management
name: Attention Management and Spatial Context
pattern: 'Long-running agent work needs an explicit map of where attention is
and whether the system is zooming in, zooming out, or switching targets.
'
sharpen_master:
- Use `attention_context.yml` to prefix complex work.
- Store attention context in traces.
- Feed zoom changes into cognition ecology and Face3D focus/arousal.
- id: evidence_first_answers
name: Evidence-First Answers
pattern: 'Reliable assistants distinguish observed facts, derived inferences,
assumptions, and open uncertainties.
'
sharpen_master:
- Add answer sections for Observed / Inferred / Unknown when stakes are high.
- Require citations or file evidence for repo claims.
- Mark stale or weak sources in cluster registries.
- id: refusal_and_redirect
name: Refusal and Redirect Design
pattern: 'Safer assistants do not merely refuse; they explain the boundary and
offer nearby safe help. MASTER should apply this to code execution, autonomous
tools, prompts, and external repos.
'
sharpen_master:
- Add safe alternative suggestions to veto outputs.
- Route policy-risk tasks through Security + Ethics council personas.
- Keep refusals concise and action-oriented.
- id: conversational_state_machine
name: Conversational State Machine
pattern: 'Strong assistants track whether they are clarifying, executing, verifying,
summarizing, repairing, or handing off.
'
sharpen_master:
- Add `act` from attention context into prompt/task state.
- Avoid repeated clarification after the user has already said Go ahead.
- Use explicit `verify` mode before landing risky changes.
- id: multi_llm_council
name: Multi-LLM Council Roles
pattern: 'Multi-LLM systems work best when models are assigned roles by capability,
cost, latency, and failure mode instead of all models answering the same prompt.
'
sharpen_master:
- Cheap models: classify, summarize, extract, label clusters.
- Fast models: draft, transform, triage, UI copy.
- Strong models: architecture, irreversible mutation, arbitration.
- Local/browser models: offline intent, embeddings, low-risk labels.
- Council personas: critique outputs through role lenses, not as redundant chatbots.
- id: output_contracts
name: Output Contracts
pattern: 'Good prompts specify output shape, brevity, failure behavior, and when
to omit noise. This aligns with MASTER''s token-efficiency cluster.
'
sharpen_master:
- Default to silent success for internal gates.
- Emit compact diffs/summaries for landed changes.
- Use structured YAML/JSON only when downstream code consumes it.
- id: memory_and_personalization
name: Memory and Personalization Boundaries
pattern: 'Personal assistants need useful memory but must separate durable user
memory, session state, repo state, inferred preferences, and temporary task
context.
'
sharpen_master:
- Store attention traces separately from durable memory.
- Make repo-topic clusters updateable evidence, not user identity.
- Use explicit provenance on all mined preferences.
- id: browser_and_mobile_agent
name: Browser and Mobile Agent Surface
pattern: 'Modern assistants increasingly act inside browsers and mobile shells.
They need permission scopes, visible action trails, and reversible operations.
'
sharpen_master:
- Pair mobile_web_opportunities with permissioned action harness.
- Add visible mobile breadcrumbs for multi-step actions.
- Use local-first storage for action queue and rollback metadata.
- id: self_critique_without_theater
name: Self-Critique Without Theater
pattern: 'Strong systems critique themselves, but user-facing output should not
show every internal check unless it changes the outcome.
'
sharpen_master:
- Run council internally.
- Surface only vetoes, uncertainties, and meaningful tradeoffs.
- Keep verbose metrics user-triggered.
orchestration_blueprint:
name: MASTER Multi-LLM Prompt-Orchestration Loop
stages:
- id: attention_frame
owner: local_or_fast
description: Parse user request into map/zoom/act/targets.
output: attention_context
- id: task_router
owner: cheap_or_fast
description: Classify task risk, required tools, expected mutation, and evidence
needs.
output: route_plan
- id: scout
owner: cheap_or_browser_local
description: Search files/repos/web and extract evidence snippets.
output: evidence_pack
- id: synthesis
owner: default_or_strong
description: Produce answer, design, or patch plan from evidence.
output: candidate_solution
- id: council_review
owner: council
description: Security/Reliability/Maintainer/Architect review based on blast
radius.
output: approve_veto_or_revise
- id: mutation
owner: strong_only
description: Apply code/repo changes when authorized.
output: commit_or_pr
- id: verify
owner: fast_plus_tools
description: Read back changed files, inspect PR status, run tests where possible.
output: verification_report
- id: user_summary
owner: fast
description: Report only what changed, what is uncertain, and next action.
output: concise_summary
risk_tiers:
low:
examples:
- classification
- summarization
- cluster labeling
- UI copy
allowed_models:
- cheap
- fast
- local
- browser_local
council: optional
medium:
examples:
- docs
- config
- preview-gated browser modules
allowed_models:
- default
- fast
- strong
council: targeted
high:
examples:
- file mutation
- autonomous actions
- auth
- security
- production runtime
allowed_models:
- strong
council: required
critical:
examples:
- destructive commands
- secret handling
- permission changes
- public deployment
allowed_models:
- strong
council: security_reliability_maintainer_veto
integration_targets:
- MASTER/data/models.yml
- MASTER/data/council.yml
- MASTER/data/attention_context.yml
- MASTER/docs/provider_economy.md
- MASTER/docs/cognitive_runtime.md
- MASTER/lib/now/routing/model_router.rb
- MASTER/lib/reach/circuit_breaker.rb
repo_topics:
clusters:
- id: token_efficiency
name: Token Efficiency and Context Conservation
status: live
confidence: high
local_evidence:
- MASTER/knowledge/research/token_reduction.md
- MASTER/docs/provider_economy.md
- MASTER/data/budget.yml
- MASTER/data/models.yml
external_evidence:
- arxiv:2604.09613 Token-Budget-Aware Pool Routing for Cost-Efficient LLM Inference
- arxiv:2604.08075 Dual-Pool Token-Budget Routing for Cost-Efficient and Reliable
LLM Serving
- arxiv:2410.10456 Ada-K Routing: Boosting the Efficiency of MoE-based LLMs
pattern: 'Estimate cost before acting. Route by token budget, task risk, provider
health, and context pressure. Speak only when output adds value.
'
build_next:
- Token budget predictor for MASTER requests.
- Silent-success default for cluster mining and visual telemetry.
- Cheap-model route for summarization, classification, critique, and cluster labels.
- id: free_model_fallbacks
name: Free Model Fallbacks and Provider Economy
status: live
confidence: high
local_evidence:
- MASTER/data/models.yml
- MASTER/docs/provider_economy.md
- MASTER/data/budget.yml
- MASTER/lib/now/routing/model_router.rb
- MASTER/lib/reach/circuit_breaker.rb
pattern: 'Treat providers as competing infrastructure. Use free/local/cheap models
for low-risk work, escalate only for irreversible synthesis, and quarantine
weak providers.
'
build_next:
- Provider health scoreboard.
- Failure-triggered visual events.
- Model fallback simulator.
- id: design_architecture_systems
name: Design, Architecture, and System Shape
status: live
confidence: high
local_evidence:
- MASTER/data/architectures.yml
- MASTER/data/workflow.yml
- MASTER/docs/repo_ecology.md
- MASTER/docs/cognitive_runtime.md
- DEPLOY/rails/amber/ARCHITECTURE.md
pattern: 'MASTER already thinks in architectures: rule DAGs, dataflow pipelines,
CRDT convergence, Bayesian priors, repo ecology, and embodied codebase topology.
'
build_next:
- Architecture Pattern Atlas.
- Design Critique Council.
- System-shape visualization linked to codebase topology.
- id: webgpu_browser_runtime
name: WebGPU Browser Runtime
status: new_external
confidence: high
external_evidence:
- arxiv:2412.15803 WebLLM: A High-Performance In-Browser LLM Inference Engine
- arxiv:2604.02344 Characterizing WebGPU Dispatch Overhead for LLM Inference
- github:mlc-ai/web-llm
pattern: 'Browser-local inference is becoming a viable runtime tier. WebGPU allows
private, local, low-friction inference, but dispatch overhead and kernel fusion
matter.
'
build_next:
- Optional browser-local assistant mode.
- WebGPU particle renderer for Face3D.
- WebLLM fallback route for offline cheap cognition.
- id: webgpu_world_model_rendering
name: WebGPU World Models and Neural Rendering
status: new_external
confidence: high
external_evidence:
- arxiv:2512.08478 Visionary: WebGPU-Powered Gaussian Splatting Platform
- arxiv:2409.06765 gsplat: An Open-Source Library for Gaussian Splatting
- arxiv:2311.12775 SuGaR: Surface-Aligned Gaussian Splatting
pattern: 'Gaussian splats, meshes, and per-frame neural processing can become
the next Face3D backend: semantic particles today, neural splats tomorrow.
'
build_next:
- Face3D WebGPU renderer spike.
- Gaussian-splat avatar/world renderer experiment.
- Repository terrain as navigable 3D scene.
- id: openclaw_like_personal_agents
name: OpenClaw-like Personal Agents
status: new_external
confidence: medium
unresolved_terms:
- opencrabs: not found clearly in public search; may be private, misspelled, or
meant as OpenClaw/OpenCrab-like.
external_evidence:
- openclaw/openclaw Personal AI assistant pattern
- arxiv:2604.11548 SemaClaw harness engineering for personal AI agents
- arxiv:2603.10165 OpenClaw-RL next-state learning from user/tool/GUI signals
- KroMiose/nekro-agent chat-platform sandboxed agent pattern
- crewAIInc/crewAI multi-agent workflow pattern
pattern: 'Persistent personal agents need harness engineering: permissions, sandboxing,
task queues, memory, tool safety, user feedback learning, and reliable rollback.
'
build_next:
- MASTER Harness Layer.
- PermissionBridge equivalent for tools/files/calendar/mail.
- Agent skill sandbox with signed/verified skills only.
- id: bleeding_edge_experimental_repos
name: Bleeding Edge Experimental Repos
status: new_external
confidence: medium
external_evidence:
- openclaw/openclaw autonomous personal agent
- Gen-Verse/OpenClaw-RL online RL for agents
- mlc-ai/web-llm browser-local WebGPU LLM inference
- nerfstudio-project/gsplat Gaussian splatting development library
- vllm-project/vllm high-throughput inference serving
- sgl-project/sglang structured generation and serving
- ggml-org/llama.cpp edge/local inference
- KroMiose/nekro-agent multimodal/chat-platform agent framework
pattern: 'Experimental repos cluster around local inference, persistent agents,
structured generation, sandboxed skills, multimodal UI, and GPU-native web runtimes.
'
build_next:
- External Repo Radar.
- Weekly cluster diff of fast-moving repos.
- Risk tags: security, licensing, stability, hype, reproducibility.
- id: agi_agent_ecosystem
name: AGI, Agents, and Multi-Agent Ecosystem
status: mirrored_research
confidence: high
local_evidence:
- github_repos/awesome-ai-agents/README.md
- github_repos/awesome-llm-apps/mcp_ai_agents/multi_mcp_agent/multi_mcp_agent.py
- MASTER/lib/judge/agent.rb
- MASTER/docs/cognitive_runtime.md
pattern: 'The repo contains a mirrored agent landscape plus internal agent/council/runtime
work.
'
build_next:
- Agent framework taxonomy.
- MASTER subsystem comparison matrix.
- Multi-agent orchestration pattern miner.
- id: personal_assistants
name: Personal Assistants and Voice Assistants
status: mirrored_research
confidence: high
local_evidence:
- github_repos/awesome-llm-apps/ai_agent_framework_crash_course/openai_sdk_crash_course/1_starter_agent/1_personal_assistant_agent/README.md
- github_repos/awesome-llm-apps/ai_agent_framework_crash_course/openai_sdk_crash_course/1_starter_agent/1_personal_assistant_agent/agent.py
- github_repos/system_prompts_leaks/Perplexity/voice-assistant.md
- github_repos/system_prompts_leaks/Perplexity/comet-browser-assistant.md
- MASTER/web/public/face.js
pattern: 'Personal assistants combine persistent context, speech, tools, calendar/mail/files,
interruption handling, and social presentation.
'
build_next:
- Assistant Mode Map.
- Voice-first flow for MASTER web UI.
- Safe prompt-pattern extraction without copying leaked prompt text.
- id: social_intelligence
name: Social Intelligence and Interaction Norms
status: latent
confidence: medium
local_evidence:
- github_repos/leaked-system-prompts/discord-clyde_20230420.md
- github_repos/leaked-system-prompts/discord-clyde_20230716-1.md
- github_repos/leaked-system-prompts/meta-ai-whatsapp_20250819.md
- github_repos/CL4R1T4S/META/Llama4_WhatsApp.txt
- MASTER/web/public/face.js
pattern: 'Social intelligence appears as prompt archaeology and expressive UI
gestures, but needs a safe first-party interaction policy layer.
'
build_next:
- Social Mode Controller.
- Rapport/repair/escalation state machine.
- Face3D social expression presets.
- id: music_chord_theory
name: Music, Chord Theory, and Production DNA
status: live
confidence: high
local_evidence:
- MASTER/lib/voice/production_dna.rb
- DEPLOY/dilla/dilla_analog.rb
- DEPLOY/dilla/dilla.rb
- DEPLOY/dilla/dilla.html
pattern: 'Dilla/Madlib/FlyLo timing, chord voicings, analog drift, low-pass warmth,
sampler grit, and live-triggered imperfection are encoded as reusable production
DNA.
'
build_next:
- Music theory registry.
- Chord graph visualizer.
- Beat/voice prosody bridge.
- id: lyricism_linguistics
name: Lyricism, Linguistics, Prosody, and Voice
status: latent
confidence: medium
local_evidence:
- MASTER/knowledge/research/tts.yml
- MASTER/lib/voice/production_dna.rb
- MASTER/lib/voice/speech.rb
- MASTER/web/public/face.js
pattern: 'Speech style inference, sentence-level TTS, prosody research, visemes,
and musical timing point toward a lyricism/linguistics engine.
'
build_next:
- Phoneme/rhyme/meter extractor.
- Prosody-to-viseme mapping.
- Copyright-safe lyric constraints.
- id: embodied_exoskeletons
name: Embodied Interfaces, Exoskeletons, and Body Augmentation
status: speculative
confidence: low
local_evidence:
- MASTER/web/public/face.js
- MASTER/docs/repo_ecology.md
- MASTER/data/architectures.yml
pattern: 'No strong physical exoskeleton repo evidence was found yet. Existing
evidence points to a cognitive/interface exoskeleton: face, gestures, haptics,
terrain, body-like repo topology, and agentic control surfaces.
'
build_next:
- Haptics and motion input layer.
- Wearable/robotics repo search pass.
- Embodied cognition UI taxonomy.
- id: ar5iv_research_radar
name: ar5iv / arXiv Research Radar
status: new_external
confidence: medium
notes: 'Direct ar5iv HTML results were not exposed by search, but arXiv equivalents
were found for the requested themes and can usually be checked on ar5iv by ID.
'
external_evidence:
- arxiv:2512.08478 WebGPU Gaussian Splatting / Visionary
- arxiv:2412.15803 WebLLM browser inference
- arxiv:2604.09613 Token-budget pool routing
- arxiv:2604.08075 Dual-pool token-budget routing
- arxiv:2604.11548 SemaClaw harness engineering
- arxiv:2603.10165 OpenClaw-RL online agent learning
- arxiv:2604.02344 WebGPU dispatch overhead
pattern: 'Use ar5iv/arXiv as a research radar feeding MASTER''s design backlog.
'
build_next:
- arXiv/ar5iv topic watchlist.
- Paper-to-cluster summarizer.
- Implementation-readiness scoring.# 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."---
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.---
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.---
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.---
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.---
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.---
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).---
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.---
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---
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.---
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`)---
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`.---
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.---
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.---
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.---
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.---
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.---
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---
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.---
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.---
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.---
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.---
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.---
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.---
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.---
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.---
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.---
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.---
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.---
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)---
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)---
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.---
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.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}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.system: |
Direct mode only.
No meta‑conversation.
Answer with minimal words.
No explanations, apologies, or padding.
Invoke tools immediately, without preamble.
template: |
%{message}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}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}# 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.
---
openai:
env: [OPENAI_API_KEY]
strengths: [reasoning, coding, multimodal]
default_model: gpt-5.5-thinking
anthropic:
env: [ANTHROPIC_API_KEY]
strengths: [coding, long_context, instruction_following]
default_model: claude-sonnet-4-6
gemini:
env: [GOOGLE_API_KEY, GEMINI_API_KEY]
strengths: [long_context, multimodal]
default_model: gemini-2.5-flash
openrouter:
env: [OPENROUTER_API_KEY]
strengths: [cheap, routing, free_tier]
default_model: openrouter/auto
deepseek:
env: [DEEPSEEK_API_KEY]
strengths: [coding, cheap]
default_model: deepseek-chat
local:
env: []
strengths: [privacy, offline, cheap]
default_model: localstack:
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"# 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."# 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"# 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]# rules.yml — universal structural rules
# scope: codebase > file > unit > line
# applies to: code, prose, law, business, science, design
# golden_rule and protection_tiers live in soul.yml (ABSOLUTE section) — single source.
# detection axes — each rule is a *principle* with one or more medium-aware adapters.
# rules apply universally: code, prose, poems, legal text, YAML, HTML — the medium
# only changes how the axis is evaluated, not whether the principle applies.
#
# granularity levels (scanning units):
# :line — individual line / lexical token
# :unit — method, function, paragraph, stanza, clause, CSS rule, YAML key block
# :section — class, module, chapter, section, JSON object, HTML component
# :file — whole file, whole document
# :tree — codebase / folder structure / visual layout
#
# detect_lexical — regex against individual lines. free. autofix-safe.
# applies at :line granularity.
# detect_unit — regex or structural check against each parsed unit (method,
# paragraph, stanza, clause). medium-aware parser segments the
# artifact first. cheap, deterministic.
# detect_structural — named tree handler (long_method, god_class, file_silhouette).
# applies at :section and :file granularity. deterministic.
# detect_semantic — LLM natural-language prompt, batched per file. expensive.
# applies at :file and :unit granularity.
# detect_history — compares current state to git history (regression, drift).
# requires git-aware scanner mode.
# detect_convention — compares local code to codebase-wide norms (naming drift,
# parameter ordering). requires codebase-model scanner mode.
# detect_tree — evaluates folder/file structure, visual layout of the tree,
# codebase topology. applies at :tree granularity.
#
# a rule may carry any combination; each axis emits separate findings tagged by id.
# the axes themselves are the cost gradient — lexical is free, structural is
# fast and deterministic, semantic is expensive, history needs git, convention
# needs a codebase model. there is no separate cost: field; the axis is the
# cost. frequency is also implicit: every wired rule runs every scan; opt out
# at the wiring layer (infra_helpers.rb), never via a per-rule knob.
# rule shape:
# id: SCREAMING_SNAKE
# name: short concrete sentence
# principle: the universal it embodies (small_parts, low_nesting, vertical_rhythm)
# medium: [ruby, yaml, json, html, prose, css] # where this rule applies
# tier: clean_code | design | core | clarity | … # category, not cost
# severity: info | warning | error
# mode: violation | opportunity # how to frame finding (default: violation)
# autofix: bool
# detect_*: …
# fix: instructional sentence
paths:
skip_dirs: [.git, vendor, tmp, var, node_modules, .bundle, coverage, log, dist, knowledge]
tree:
max_depth: 2
max_lines: 200
voice:
style: openbsd_dmesg
anti_simulation:
forbidden: [will, would, could, might]
require_evidence:
file_read: "show file content with SHA-256"
modification: "show unified diff"
completion: "show command output"
banned_output:
- headlines
- section_markers
- bullet_lists_without_content
- filler_phrases
- hedging
- sycophancy
strunk:
preambles: ["In summary,", "Consequently,", "Therefore,", "Notably,", "Importantly,"]
hedges: ["will", "would", "might", "could", "perhaps", "seems", "appears"]
endings: ["as a result.", "for this reason.", "thus.", "in effect.", "accordingly."]
code_preambles: ["# TODO: clarify intent", "# FIXME: review edge cases", "# NOTE: performance considerations", "# HACK: temporary workaround", "# REVIEW: assess after refactor"]
apply_to: [prose, comments, documentation, strings]
never_apply_to: [code_logic, algorithms, data_structures]
safeguards:
- never_delete_variable_names
- never_delete_function_calls
- never_simplify_conditional_logic
- never_collapse_diagnostic_output
inverted_pyramid:
- "Lead with the outcome."
- "Provide key evidence next."
- "Add implementation detail last."
preserve:
boot_message: "5-line dmesg style, never collapse to one line"
diagnostic_output: "structured multi-line output is intentional, never compress to abbreviations"
help_text: "include command name, description, and at least one example"
spinner_feedback: "show elapsed time and status, do not remove progress indicators"
refinement_scope:
streamline: "remove redundancy, not information"
polish: "refine wording, not delete output"
minimize: "applies to prompt tokens, not diagnostic output"
zen:
observe: "Read current behavior before changing anything."
simplify: "Reduce moving parts before adding new components."
isolate: "Change one axis at a time with clear boundaries."
verify: "Run checks and gather objective evidence."
reflect: "Capture learning and improve defaults."
# Engineering fit — classify every problem against the load it actually carries.
# Apply during scan, sweep, council, and any review of an existing artifact.
engineering_fit:
classify: "For every problem, decide: under-engineered, over-engineered, or perfectly engineered — and say why."
under: "Misses real failure modes the artifact will hit. Add only what the artifact actually carries."
over: "Carries machinery the artifact will never load. Strip it; defer until the load shows up."
fit: "Solution matches the load and the failure modes — no slack, no shortfall."
why_required: "State the load the artifact carries (users, edge cases, lifetimes, blast radius). The verdict follows from the load."
# Six Universal Laws — single hierarchical priority for every rule and persona.
# When two rules conflict, the lower-numbered law wins.
laws:
ROBUSTNESS:
priority: 1
principle: "Errors fail safely; security first; handle edge cases."
applies_to: [security, errors, input_validation, resource_management]
SINGULARITY:
priority: 2
principle: "One source of truth; no duplication; data integrity."
applies_to: [duplication, consistency, data_integrity]
LINEARITY:
priority: 3
principle: "Sequential flow; minimal branches; clear path."
applies_to: [control_flow, nesting, complexity]
PROXIMITY:
priority: 4
principle: "Related code together; cohesive modules."
applies_to: [organization, coupling, modules]
ABSTRACTION:
priority: 5
principle: "Right level; no leaky abstractions; appropriate hiding."
applies_to: [interfaces, encapsulation, apis]
DENSITY:
priority: 6
principle: "Information dense; no noise; signal not noise."
applies_to: [verbosity, comments, naming]
# Cognitive biases and anti-patterns — meta-rules above lexical detection.
biases:
critical:
hallucination:
detect: [claim_without_reading, quote_without_source, invented_stats]
apply: cite_or_remove
violates_law: ROBUSTNESS
simulation:
detect: [future_tense, "imperative_we_must", "lets_do_this"]
apply: rewrite_indicative_past
violates_law: DENSITY
completion_theater:
detect: [ellipsis, etcetera, rest_of_placeholder]
apply: complete_or_delete
violates_law: ROBUSTNESS
high:
sycophancy:
detect: ["great question", "absolutely", "excellent", "wonderful"]
apply: delete
violates_law: DENSITY
false_confidence:
detect: hidden_uncertainty
apply: state_uncertainty_explicitly
violates_law: ROBUSTNESS
cognitive_traps: [anchoring, recency, verbosity, pattern_completion, premature_commitment]
# Structural operations — verbs the rewriter may apply, with risk and verify spec.
structural_ops:
preserve_note: "These keep getting deleted in self-runs — DO NOT REMOVE."
verify_after_each: true
ops:
merge: {desc: "combine similar logic", risk: medium, verify: "merged logic identical", supports_law: SINGULARITY}
semantic_regroup: {desc: "reorganize logically", risk: low, verify: "functionality unchanged", supports_law: PROXIMITY}
defrag: {desc: "consolidate fragments", risk: low, verify: "all fragments accessible", supports_law: PROXIMITY}
decouple: {desc: "separate concerns", risk: high, verify: "interfaces preserved", supports_law: ABSTRACTION}
hoist: {desc: "move to proper scope", risk: medium, verify: "scope correct", supports_law: PROXIMITY}
flatten: {desc: "reduce nesting", risk: medium, verify: "logic flow identical", supports_law: LINEARITY}
delete: {desc: "remove dead code", risk: high, verify: "truly dead, no references", supports_law: DENSITY}
expand: {desc: "extract for clarity", risk: low, verify: "extracted correctly", supports_law: ABSTRACTION}
reduce_noise: {desc: "clean messy lines", risk: low, verify: "formatting only, no logic", supports_law: DENSITY}
# Veto patterns — concrete regex detectors that block merge unconditionally.
veto_patterns:
secrets: {detect: 'sk-[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{20,}|-----BEGIN.*KEY-----', apply: move_to_env, violates_law: ROBUSTNESS}
sql_injection: {detect: 'execute|query.*#\{', apply: parameterize, violates_law: ROBUSTNESS}
unfinished: {detect: '\.\.\.|TODO|FIXME|pending', apply: complete_or_track, violates_law: ROBUSTNESS}
unsafe_calls: {detect: 'system\s*\(|exec\s*\(|%x\{|Open3\.capture[23]\s*\([^)]*#\{', apply: add_safe_nav, violates_law: ROBUSTNESS}
race_conditions: {detect: 'if.*\n.*=.*\n.*if', apply: add_mutex, violates_law: ROBUSTNESS}
# Beauty — aesthetic anchors from masters of their craft.
# The user is an architect; these are first-class engineering anchors, not decoration.
beauty:
typography_bringhurst:
- choose_appropriate_typeface_for_function
- set_text_in_sizes_that_suit_its_nature
- use_vertical_motion_that_suits_typeface
- rhythm_proportion_modulation_harmony
architecture_ando:
- simplicity_silence_emptiness
- light_shadow_materiality
- geometry_nature_coexistence
- space_between_as_important_as_form
design_rams:
- innovative_useful_aesthetic
- unobtrusive_honest_long_lasting
- thorough_environmentally_friendly
- as_little_design_as_possible
code_martin:
- meaningful_names_intention_revealing
- functions_do_one_thing_small
- comments_explain_why_not_what
- error_handling_separate_from_logic
zen_japanese:
wabi_sabi: imperfect_authentic
ma: emptiness_pause
kanso: eliminate_essence
# thresholds — only blocks with a live reader survive. Per-rule structural
# limits (method_length, nesting_depth, cyclomatic) live on each rule's
# detection.structural block, not here. ONE_SOURCE: no second copy.
thresholds:
class: # read by voice/personality.rb for prompt substitution
max_methods: 6
max_lines: 200
convergence: # read by loop/fix_loop.rb — authoritative copy
consecutive_clean_runs_required: 2
max_iterations: 15
stagnant_threshold: 3
# Prediction engine — per-detector autofix confidence thresholds.
# When confidence >= threshold, autofix fires without human review.
# Below threshold: propose fix, await approval. Never auto-delete at < 0.80.
prediction_engine:
null_usage: {confidence: 0.95, autofix: null_object_pattern}
abbreviation: {confidence: 0.99, autofix: expand_to_full_name}
nesting_depth: {confidence: 0.92, autofix: extract_method}
bare_rescue: {confidence: 0.98, autofix: rescue_standard_error}
frozen_string: {confidence: 0.99, autofix: add_frozen_string_literal}
trailing_ws: {confidence: 0.99, autofix: strip_trailing_whitespace}
debug_output: {confidence: 0.97, autofix: remove_debug_call}
todo_comment: {confidence: 0.70, autofix: create_issue}
magic_number: {confidence: 0.85, autofix: extract_named_constant}
long_line: {confidence: 0.90, autofix: wrap_at_120}
sycophancy: {confidence: 0.99, autofix: delete_phrase}
future_tense: {confidence: 0.95, autofix: rewrite_indicative}
duplicate_code: {confidence: 0.75, autofix: extract_shared_method}
god_class: {confidence: 0.60, autofix: propose_decomposition}
n_plus_one: {confidence: 0.88, autofix: add_includes}
scan_depths:
# Class names for named Rule subclasses; lowercase ids for DSL-defined rules.
# Phantom entries silently exclude rules — InterconnectRule catches drift.
quick: &quick
- no_debug
- no_puts
- frozen_literal
- long_line
- trailing_whitespace
- todo_fixme
- rescue_exception
- empty_rescue
- consecutive_blank_lines
- debug_output
- trailing_comment
- time_zone_unsafe
- no_ascii_line_art
- single_private_section
- strict_loading_missing
- rate_limiting_missing
- migration_add_reference_no_fk
- migration_remove_column
- migration_find_or_create_by
- safe_navigation
- each_with_object
- keyword_args
- kernel_coercion
- percent_literal
- hash_fetch
- transform_keys
- few_arguments
- immutable
- n_plus_one
- find_each
- no_update_attribute
- pluck_over_map
- html_lang
- semantic_elements
- i18n_coverage
- img_alt
- button_over_anchor
- aria_interactive
- lazy_images
- no_inline_styles
- mobile_first
- no_import_scss
- no_important
- logical_properties
- clamp_typography
- const_by_default
- nullish_coalescing
- template_literals
- async_await
- for_of
- quote_variables
- double_bracket
- dollar_paren
- no_multiple_languages
- meaningful_names
- why_not_what
- typographic_excellence
- typography_discipline
- null_blindness
- secret_proximity
- magic_color
- unbounded_retry
- law_of_demeter
- no_flag_arguments
- meta_charset
- no_var
- named_routes
- js_module_size
- readme_structure
- active_voice_docs
- no_column_align
frontend: &frontend
- html_lang
- semantic_elements
- img_alt
- button_over_anchor
- aria_interactive
- lazy_images
- no_inline_styles
- mobile_first
- no_import_scss
- no_important
- logical_properties
- clamp_typography
- meta_charset
- const_by_default
- nullish_coalescing
- template_literals
- async_await
- for_of
- no_var
- js_module_size
- no_ascii_line_art
- no_column_align
- typographic_excellence
- typography_discipline
standard: &standard
- CoChangeCouplingRule
- InterconnectRule
- RuleCoverageRule
- RubocopRule
- ReekRule
- SemanticRule
- AdversarialRule
- CommentDriftRule
- AstOmissionRule
- no_debug
- no_puts
- frozen_literal
- long_line
- trailing_whitespace
- todo_fixme
- rescue_exception
- empty_rescue
- consecutive_blank_lines
- debug_output
- trailing_comment
- time_zone_unsafe
- no_ascii_line_art
deep: &deep
- all
hunt: *deep
critique: *deep
languages:
ruby:
version: "3.3+"
frozen_string_literal: required
guard_clauses: true
rescue: specify_type_always
naming: snake_case
max_params: 3
rails:
version: "8+"
stack: [solid_queue, solid_cache, solid_cable]
frontend: hotwire
testing: minitest
database: sqlite_default
security: [strong_parameters, csrf, csp, ssl, hsts]
zsh:
shebang: "#!/usr/bin/env zsh"
options: "set -euo pipefail; setopt nullglob extendedglob"
# banned commands live in zsh_patterns.yml — single source.
openbsd:
service: rcctl
packages: pkg_add
firewall: pf
privilege: doas
http: httpd
ssh:
permit_root_login: false
password_auth: false
max_auth_tries: 3
norwegian:
dialect: "bokmål"
rules: ["Short sentences", "Avoid anglicisms", "Active voice", "Plain language"]
rules:
codebase:
- id: PRESERVE_FIRST
name: "Never break working code"
tier: kernel
severity: error
autofix: false
detect_semantic: "Does this change modify working code without reading it first?"
fix: "Read before write. Patch minimally."
- id: ONE_SOURCE
name: "One authoritative representation per concept"
tier: kernel
severity: error
autofix: true
detect_semantic: "Is the same logic or data defined in multiple places?"
fix: "Extract to single source, reference from all consumers."
- id: DECOUPLE
name: "Make hidden dependencies explicit"
tier: kernel
severity: error
autofix: false
detect_semantic: "Are there implicit couplings between modules that should be injected?"
fix: "Inject dependencies through constructor. No global state."
- id: DEGRADE_GRACEFULLY
name: "Operate under partial failures"
tier: kernel
severity: error
autofix: false
detect_semantic: "Does this code crash on partial failure instead of degrading?"
fix: "Circuit breakers, timeouts, fallbacks."
- id: GALLS_LAW
name: "Complex systems evolve from simple working systems"
tier: philosophy
severity: info
autofix: false
detect_semantic: "Is this attempting to build a complex system from scratch rather than evolving from a working simple one?"
fix: "Start simple, prove it works, then extend."
- id: CHESTERTONS_FENCE
name: "Understand why something exists before removing it"
tier: philosophy
severity: warning
autofix: false
detect_semantic: "Is code being removed without understanding why it was added?"
fix: "Read git blame, understand the rationale, then decide."
- id: UNIX_PHILOSOPHY
name: "Do one thing well"
tier: architecture
severity: info
autofix: false
detect_semantic: "Does this module try to do too many unrelated things?"
fix: "Extract services. Clear module boundaries. Compose with pipes."
- id: FUNCTIONAL_CORE
name: "Pure logic in core, side effects at edges"
tier: architecture
severity: info
autofix: false
detect_semantic: "Are IO/DB calls scattered deep in business logic?"
fix: "Return data from core, let shell handle IO."
- id: CONVENTION_OVER_CONFIG
name: "Sensible defaults reduce decisions"
tier: productivity
severity: info
autofix: false
detect_semantic: "Does this require explicit config where a convention would suffice?"
fix: "Provide sensible defaults, override only when needed."
- id: MONOLITH_FIRST
name: "Start monolith, extract when team exceeds 15"
tier: architecture
severity: info
autofix: false
detect_semantic: "Is this prematurely splitting into services?"
fix: "Keep it in one app until extraction is clearly needed."
- id: CONSISTENT_ERROR_STRATEGY
name: "One error handling strategy per module"
tier: design
severity: warning
autofix: false
detect_semantic: "Does this module mix Result objects, exceptions, and nil-returns?"
fix: "Pick one strategy per module. MASTER uses Result monad."
- id: DUAL_DETECTION
name: "Layer lexical and semantic detection"
tier: verification
severity: info
autofix: false
detect_semantic: "Is detection relying on regex alone or LLM alone?"
fix: "Layer deterministic patterns with LLM semantic analysis."
- id: MASS_GENERATE_CURATE
name: "Generate many variations, curate ruthlessly"
tier: creative
severity: info
autofix: false
detect_semantic: "Is the first draft being accepted without exploring alternatives?"
fix: "Generate a swarm and curate when stakes are high."
- id: NO_GOD_CLASS
name: "No god classes"
tier: core
severity: error
autofix: false
detect_structural: god_class
fix: "Decompose into focused classes."
- id: NO_SHOTGUN_SURGERY
name: "One change should not require edits in many files"
tier: core
severity: warning
autofix: false
detect_semantic: "Does a single conceptual change span many files?"
fix: "Extract the missing abstraction."
- id: NO_HIDDEN_GLOBAL_STATE
name: "No hidden global state"
tier: core
severity: error
autofix: false
detect_semantic: "Are there global variables or class-level mutable state shared across modules?"
fix: "Inject configuration. Use dependency injection."
- id: TRACER_BULLETS
name: "End-to-end skeleton first, flesh out second"
tier: architecture
severity: info
autofix: false
detect_semantic: "Is this building infrastructure without an end-to-end path? Is this business plan elaborating budgets before proving the revenue model? Is this research paper expanding methodology before demonstrating the finding?"
fix: "Wire the simplest end-to-end path first. Prove it works. Then add depth."
- id: ORTHOGONALITY
name: "Changes in one dimension must not ripple into others"
tier: architecture
severity: warning
autofix: false
detect_semantic: "Does changing one aspect force changes in unrelated aspects? Does reformatting break content? Does modifying one gene's expression alter another pathway?"
fix: "Decouple dimensions. Database changes should not require UI changes. Style should not affect structure."
- id: TRANSFORMATIONS
name: "Think in pipelines: input transforms to output"
tier: architecture
severity: info
autofix: false
detect_semantic: "Is this modeling the problem as mutable state instead of flowing transformations? Does this document bury its flow in scattered cross-references?"
fix: "Express work as a chain of transformations. Each stage takes input, produces output, holds no state."
- id: DEEP_MODULES
name: "Powerful functionality behind simple interfaces"
tier: architecture
severity: warning
autofix: false
detect_semantic: "Is this module shallow — complex interface, trivial implementation? Does this form ask 40 questions for a simple task? Does this clause require five cross-references?"
fix: "Simple interface, rich implementation. A deep module does much with little ceremony."
- id: INFORMATION_HIDING
name: "Each module encapsulates one design decision"
tier: architecture
severity: warning
autofix: false
detect_semantic: "Is implementation detail leaking across boundaries? Would changing an internal decision force changes elsewhere?"
fix: "Encapsulate each decision in one module. If it changes, only that module changes."
- id: DIFFERENT_LAYER_DIFFERENT_ABSTRACTION
name: "Each layer speaks a different language than its neighbors"
tier: architecture
severity: warning
autofix: false
detect_semantic: "Do adjacent layers use the same abstraction? Are there pass-through methods that add no value? Does this management layer just relay without transforming?"
fix: "Each layer must transform, not relay. If a layer adds no abstraction, remove it."
- id: STRUCTURAL_HONESTY
name: "The shape of the artifact must reflect the shape of the problem"
tier: architecture
severity: warning
autofix: false
detect_semantic: "Does the structure match the domain? Does the module hierarchy match the conceptual hierarchy? Does the floor plan match the workflow?"
fix: "Align structure with reality. Natural domain boundaries become system boundaries."
- id: GRACEFUL_BOUNDARIES
name: "Where systems meet, expect translation and loss"
tier: architecture
severity: info
autofix: false
detect_semantic: "Does this boundary assume perfect fidelity? Does this integration assume the other side never changes?"
fix: "Every boundary is a translation layer. Validate at every boundary. Degrade gracefully when translation fails."
- id: PULL_COMPLEXITY_DOWN
name: "Simple interface matters more than simple implementation"
tier: architecture
severity: info
autofix: false
detect_semantic: "Is complexity pushed up to the caller instead of absorbed by the implementation?"
fix: "Absorb complexity into the implementation. The caller should not need to know how it works."
- id: ETC
name: "Easier To Change — the meta-value behind every principle"
tier: philosophy
severity: info
autofix: false
detect_semantic: "Does this design decision make the system harder to change? Does this contract make renegotiation unnecessarily difficult?"
fix: "Choose the option that keeps more options open. Decoupled, parameterized, replaceable."
- id: BROKEN_WINDOWS
name: "Zero tolerance for visible decay"
tier: philosophy
severity: warning
autofix: false
detect_semantic: "Is there visible rot being left unfixed — dead code, broken links, stale references? In a brief, citations to overruled cases?"
fix: "Fix it now. One broken window invites more."
- id: ENTROPY_RESISTANCE
name: "Systems decay toward disorder unless actively maintained"
tier: philosophy
severity: warning
autofix: false
detect_semantic: "Is there creeping disorder — naming inconsistencies, abandoned conventions, accumulating exceptions? In a legal code, contradictory amendments?"
fix: "Actively resist entropy. Regular cleanup. Remove what no longer serves."
- id: DONT_OUTRUN_HEADLIGHTS
name: "Only plan as far as you can see"
tier: philosophy
severity: info
autofix: false
detect_semantic: "Is this making detailed plans for unpredictable scenarios? Projecting five years out? Designing for hypothetical scale?"
fix: "Small deliberate steps. Reassess after each. Feedback from each step illuminates the next."
- id: REVERSIBILITY
name: "Prefer decisions that are easy to undo"
tier: philosophy
severity: info
autofix: false
detect_semantic: "Is this decision hard to reverse? Does it lock in a vendor? Waive future rights? Have no rollback pathway?"
fix: "Soft-delete over hard-delete. Option agreements over binding. Feature flags over big-bang."
- id: DESIGN_IT_TWICE
name: "Consider at least two approaches before committing"
tier: philosophy
severity: info
autofix: false
detect_semantic: "Was this designed once without considering alternatives?"
fix: "Sketch two designs. Compare tradeoffs. Five minutes saves five days."
- id: PROPERTY_BASED_TESTING
name: "Test properties and invariants, not just examples"
tier: verification
severity: info
autofix: false
detect_semantic: "Are tests only checking specific examples instead of universal properties? Is QA only sampling instead of testing invariants?"
fix: "Define properties that must always hold. Generate many inputs. Verify the property survives all."
file:
- id: GUARD_EXPENSIVE
name: "Check preconditions before costly work"
tier: kernel
severity: error
autofix: false
detect_semantic: "Does this file perform expensive operations (API calls, disk IO) without checking preconditions?"
fix: "Guard clause before costly work. Estimate cost. Confirm if destructive."
- id: SMALL_FILES
name: "Files under 300 lines"
tier: clean_code
severity: warning
autofix: false
detect_lexical: null
detect_structural: file_silhouette
fix: "Split at module boundaries."
- id: CONSISTENT_FILE_STRUCTURE
name: "Consistent file structure order"
tier: clarity
severity: info
autofix: false
detect_structural: file_layout
fix: "Reorder to match convention."
languages: [ruby]
- id: FROZEN_STRING_LITERAL
name: "frozen_string_literal magic comment required"
tier: core
severity: warning
autofix: true
detect_lexical: "\\A(?!# frozen_string_literal)"
fix: "Add '# frozen_string_literal: true' as first line."
languages: [ruby]
- id: SINGLE_PRIVATE_SECTION
name: "One private keyword at bottom"
tier: design
severity: info
autofix: false
detect_lexical: "private\\s+:\\w+"
fix: "Use a single 'private' keyword with methods below it."
languages: [ruby]
- id: STRICT_MODE_ZSH
name: "set -euo pipefail at script top"
tier: core
severity: error
autofix: true
detect_lexical: "^#!/.*(?:ba|z)sh\\n(?!set -)"
fix: "Add 'set -euo pipefail' after shebang."
languages: [zsh]
- id: HTML_LANG
name: "lang attribute on <html>"
tier: accessibility
severity: error
autofix: true
detect_lexical: "<html(?!\\s+[^>]*lang=)"
fix: "Add lang=\"en\" or appropriate locale."
languages: [html]
- id: SEMANTIC_ELEMENTS
name: "Use semantic HTML5 elements"
tier: accessibility
severity: warning
autofix: true
detect_lexical: "<div\\s+class=\"(header|footer|nav|main|sidebar|article|section)\""
fix: "Use <header>, <footer>, <nav>, <main>, <aside>, <article>, <section>."
languages: [html]
- id: MOBILE_FIRST
name: "Mobile-first media queries"
tier: design
severity: warning
autofix: false
detect_lexical: "@media\\s*\\(\\s*max-width"
fix: "Use min-width (mobile-first, progressive enhancement)."
languages: [css]
- id: NO_IMPORT_SCSS
name: "Replace @import with @use/@forward"
tier: design
severity: warning
autofix: false
detect_lexical: "@import\\s+[\"']"
fix: "@import is deprecated. Use @use/@forward."
languages: [scss]
- id: NO_IMPORTANT
name: "No !important"
tier: design
severity: warning
autofix: false
detect_lexical: "!\\s*important"
fix: "Restructure selectors to avoid specificity bankruptcy."
languages: [css]
- id: SQUINT_TEST
name: "Structure evident at a glance"
tier: aesthetic
severity: info
autofix: true
detect_lexical: "\\n{4,}"
detect_semantic: "Does this file have dense blocks with no visual breaks, or ragged indentation?"
fix: "One blank line between sections, never more than two consecutive."
- id: WHITESPACE_PUNCTUATION
name: "Whitespace as layout tool"
tier: aesthetic
severity: info
autofix: true
detect_lexical: "\\n{4,}"
fix: "One blank line between sections, never more than two."
- id: NO_MULTIPLE_LANGUAGES
name: "One medium per artifact"
tier: clean_code
severity: warning
autofix: false
detect_lexical: "<%|<script|<style|SQL|HEREDOC"
detect_semantic: "Does this file embed multiple languages or notations? Does this document mix incompatible frameworks?"
fix: "One language per layer. Separate into distinct files or clearly demarcated sections."
- id: ARTIFICIAL_COUPLING
name: "Things that do not depend on each other must not be grouped together"
tier: clean_code
severity: warning
autofix: false
detect_semantic: "Are unrelated concepts coupled by proximity or shared container? In a filing, are unrelated claims bundled into one count?"
fix: "Separate into independent units. Each removable without affecting others."
- id: I18N_COVERAGE
name: "Wrap user-facing literals in I18n helpers"
tier: design
severity: warning
autofix: false
detect_lexical: ">\\s*[A-Za-z][^<]{3,}<"
fix: "Replace literal with t('.key')."
languages: [html]
path_match: "/app/views/"
- id: STRICT_LOADING_MISSING
name: "AR model lacks strict_loading_by_default"
tier: design
severity: info
autofix: false
detect_lexical: "class\\s+\\w+\\s+<\\s+(?:ApplicationRecord|ActiveRecord::Base)\\b"
requires_absent: "\\bstrict_loading_by_default\\b"
whole_file: true
fix: "Add `self.strict_loading_by_default = true` to surface missing eager-loads."
languages: [ruby]
path_match: "/app/models/"
- id: RATE_LIMITING_MISSING
name: "Sensitive controller missing rate_limit/throttle"
tier: security
severity: error
autofix: false
detect_lexical: "(login|signup|sign_up|password|reset)"
requires_absent: "rate_limit|throttle"
whole_file: true
fix: "Add rate_limit/throttle to sensitive actions."
languages: [ruby]
path_match: "/app/controllers/"
- id: MIGRATION_ADD_REFERENCE_NO_FK
name: "add_reference without foreign_key:"
tier: robustness
severity: error
autofix: false
detect_lexical: "add_reference(?!.*foreign_key:)"
fix: "Add `foreign_key: true` to enforce referential integrity."
languages: [ruby]
path_match: "/db/migrate/"
- id: MIGRATION_REMOVE_COLUMN
name: "remove_column is destructive"
tier: robustness
severity: error
autofix: false
detect_lexical: "remove_column"
fix: "Document safety/backfill path before removing a column."
languages: [ruby]
path_match: "/db/migrate/"
- id: MIGRATION_FIND_OR_CREATE_BY
name: "find_or_create_by needs unique index"
tier: robustness
severity: warning
autofix: false
detect_lexical: "find_or_create_by"
fix: "Back find_or_create_by with a unique index to prevent duplicates."
languages: [ruby]
path_match: "/db/migrate/"
unit:
- id: SIMPLEST_WORKS
name: "Fewest moving parts that solve the problem"
tier: kernel
severity: error
autofix: false
detect_semantic: "Does this unit introduce unnecessary complexity?"
fix: "Delete abstractions until it hurts. KISS."
- id: FAIL_VISIBLY
name: "Surface errors immediately"
tier: kernel
severity: error
autofix: false
detect_lexical: "rescue\\s*$|rescue\\s+Exception"
detect_semantic: "Does this code swallow exceptions or fail silently?"
fix: "Catch specific errors, log context, re-raise or return Result."
- id: BE_CONCISE
name: "Avoid unnecessary words, tokens, or lines"
tier: kernel
severity: warning
autofix: true
detect_semantic: "Does this unit contain unnecessary verbosity?"
fix: "Omit needless words. Omit needless code."
- id: DRY
name: "Don't Repeat Yourself"
tier: principle
severity: warning
autofix: false
aliases: [duplicate_code]
detect_semantic: "Are two units expressing the same idea twice?"
fix: "MERGE or DEFRAG. Pass the difference as an argument; never copy a block to vary one literal."
tension: "DRY conflicts with WET/AHA — singularity wins for data, DRY wins for code."
- id: KISS
name: "Keep It Simple"
tier: principle
severity: warning
autofix: false
aliases: [long_method, god_class, nesting_depth, arity]
detect_semantic: "Does this unit introduce complexity the problem doesn't require?"
fix: "FLATTEN nesting; SPLIT god classes; INLINE single-call helpers; delete abstractions until it hurts."
- id: POLA_PRINCIPLE
name: "Principle of Least Astonishment"
tier: principle
severity: warning
autofix: false
aliases: [pola]
detect_semantic: "Does the name promise behavior the implementation contradicts?"
fix: "NAME truthfully; rename when the implementation drifts."
- id: SRP
name: "Single Responsibility"
tier: solid
severity: warning
autofix: false
detect_semantic: "Does this class have more than one reason to change?"
fix: "Extract concerns into focused classes."
- id: OPEN_CLOSED
name: "Open for extension, closed for modification"
tier: solid
severity: warning
autofix: false
detect_semantic: "Must you modify core code to extend this?"
fix: "Strategy pattern, dependency injection, hooks."
- id: LISKOV
name: "Subtypes must substitute for base types"
tier: solid
severity: warning
autofix: false
detect_semantic: "Does a subclass break the parent's contract?"
fix: "Use composition if substitutability fails."
- id: INTERFACE_SEGREGATION
name: "No fat interfaces"
tier: solid
severity: warning
autofix: false
detect_semantic: "Does this interface force implementors to stub unused methods?"
fix: "Split into smaller role-based interfaces."
- id: DEPENDENCY_INVERSION
name: "Depend on abstractions, not concretions"
tier: solid
severity: warning
autofix: false
detect_semantic: "Does this class directly instantiate its dependencies?"
fix: "Inject dependencies through constructor."
- id: ONE_JOB
name: "Each module has one clear reason to change"
tier: philosophy
severity: warning
autofix: false
detect_semantic: "Does this module handle unrelated responsibilities?"
fix: "Split into focused modules."
- id: NO_SURPRISES
name: "Predictable over clever"
tier: philosophy
severity: warning
autofix: false
detect_semantic: "Would a reader be surprised by this behavior?"
fix: "Rename or split to match expectations."
- id: COMPOSABLE
name: "Small pieces that combine cleanly"
tier: philosophy
severity: info
autofix: false
detect_semantic: "Is this monolithic where it could be composed from smaller parts?"
fix: "Build small pieces that combine cleanly."
- id: CQS
name: "Separate queries from state mutations"
tier: design
severity: warning
autofix: true
detect_structural: cqs
fix: "Split: query() + command(). Never mix."
languages: [ruby]
- id: LAW_OF_DEMETER
name: "Only talk to immediate friends"
tier: design
severity: warning
autofix: true
detect_lexical: "\\w+\\.\\w+\\.\\w+\\.\\w+"
detect_semantic: "Does this code reach through multiple objects?"
fix: "Add delegate method. Talk only to direct collaborators."
- id: COMPOSITION_OVER_INHERITANCE
name: "Favor has_a over is_a"
tier: design
severity: info
autofix: false
detect_semantic: "Is inheritance used for code reuse rather than substitutability?"
fix: "Use composition. Mixins for shared behavior."
- id: SMALL_FUNCTIONS
name: "Methods under 10 lines ideal, max 20"
tier: clean_code
severity: warning
autofix: true
detect_structural: long_method
fix: "Extract: validate(), calculate(), persist()."
- id: FEW_ARGUMENTS
name: "Ideal is zero to two arguments"
tier: clean_code
severity: warning
autofix: true
detect_lexical: "def \\w+\\([^)]*,[^:)]+,[^:)]+,[^:)]+\\)"
fix: "Group into keyword arguments or parameter object."
languages: [ruby]
- id: ONE_ABSTRACTION_LEVEL
name: "Each function at one abstraction level"
tier: clean_code
severity: info
autofix: false
detect_semantic: "Does this function mix high-level policy with low-level detail?"
fix: "Extract low-level detail into helper methods."
- id: STEPDOWN
name: "Functions call only one level below"
tier: clean_code
severity: info
autofix: false
detect_semantic: "Does this code read top-down like a newspaper?"
fix: "Public methods at top, private helpers below."
- id: BOUNDARY_ISOLATION
name: "Wrap third-party code at the edge"
tier: clean_code
severity: warning
autofix: false
detect_semantic: "Does third-party API surface leak into core logic?"
fix: "Wrap in adapter. Keep it from leaking."
- id: NO_MAGIC
name: "No unexplained constants or flags"
tier: clean_code
severity: warning
autofix: true
detect_semantic: "Are there unexplained numeric literals or boolean flags?"
fix: "Extract to named constants."
- id: FAIL_FAST
name: "Report errors at detection point"
tier: reliability
severity: warning
autofix: true
detect_semantic: "Does this code defer error reporting instead of failing immediately?"
fix: "Raise or return Result.err at point of detection."
- id: IDEMPOTENT
name: "Same operation, same result"
tier: reliability
severity: info
autofix: false
detect_semantic: "Would repeating this operation produce different results?"
fix: "Use set_value() instead of increment(). Add idempotency keys."
- id: DEFENSIVE_INPUT
name: "Never trust input at system boundaries"
tier: reliability
severity: warning
autofix: true
detect_semantic: "Is external input used without validation?"
fix: "Validate at boundaries. Whitelist, sanitize."
- id: GRACEFUL_DEGRADATION
name: "Partial functionality beats total failure"
tier: reliability
severity: warning
autofix: false
detect_semantic: "Does one failure crash everything?"
fix: "Circuit breakers, timeouts, fallback to stale data."
- id: NO_SIDE_EFFECTS
name: "Functions should not change state they do not own"
tier: functional
severity: info
autofix: false
detect_semantic: "Does this function modify external state silently?"
fix: "Make side effects explicit. Return values instead of mutating."
- id: IMMUTABLE
name: "Default to immutable data"
tier: functional
severity: info
autofix: true
detect_lexical: "^\\s*[A-Z][A-Z_]*\\s*=\\s*[\\[{](?!.*\\.freeze)"
detect_semantic: "Are mutable objects shared across threads or scopes?"
fix: "Freeze collections. Use frozen/const by default."
languages: [ruby]
- id: PURE_FUNCTIONS
name: "Same input, same output"
tier: functional
severity: info
autofix: true
detect_semantic: "Does this function depend on hidden state?"
fix: "Pass all dependencies as parameters."
- id: PRIMITIVE_OBSESSION
name: "Replace repeated primitives with value objects"
tier: refactoring
severity: info
autofix: false
detect_semantic: "Are primitives used where a value object would be clearer?"
fix: "Create a value object."
- id: MESSAGE_CHAIN
name: "Avoid a.b.c.d chains"
tier: refactoring
severity: warning
autofix: true
detect_lexical: "\\w+\\.\\w+\\.\\w+\\.\\w+"
fix: "Talk only to immediate collaborators."
- id: MIDDLE_MAN
name: "Eliminate pure-delegation classes"
tier: refactoring
severity: info
autofix: false
detect_semantic: "Does this class delegate most methods to another?"
fix: "Remove middle man. Talk directly."
- id: LAZY_CLASS
name: "Remove classes too small to justify existence"
tier: refactoring
severity: info
autofix: false
detect_semantic: "Is this class doing too little to earn its existence?"
fix: "Inline into caller."
- id: DIVERGENT_CHANGE
name: "Split classes changed for unrelated reasons"
tier: refactoring
severity: warning
autofix: false
detect_semantic: "Is this class modified for multiple unrelated reasons?"
fix: "Split by axis of change."
- id: SPECULATIVE_GENERALITY
name: "Remove code for hypothetical needs"
tier: refactoring
severity: info
autofix: true
detect_semantic: "Is this code written for a future requirement that does not exist yet?"
fix: "Delete it. YAGNI."
- id: INAPPROPRIATE_INTIMACY
name: "Do not access another class's private data"
tier: refactoring
severity: warning
autofix: false
detect_semantic: "Does this code access internals of another class?"
fix: "Use public interface. Enforce boundaries."
- id: SYSTEM_STATUS
name: "Keep users informed of progress"
tier: ux
severity: info
autofix: true
detect_semantic: "Does this long operation provide feedback to the user?"
fix: "Spinner, progress bar, status message."
- id: USER_CONTROL
name: "Support undo and emergency exits"
tier: ux
severity: info
autofix: false
detect_semantic: "Can the user undo or cancel this operation?"
fix: "Add undo support. Confirm destructive actions."
- id: ERROR_RECOVERY
name: "Error messages must name the problem and suggest a fix"
tier: ux
severity: warning
autofix: false
detect_semantic: "Do error messages explain what went wrong and what to do?"
fix: "Name the problem. Suggest a fix. Show context."
- id: AESTHETIC_MINIMALISM
name: "Show only relevant information"
tier: ux
severity: info
autofix: false
detect_semantic: "Does this output contain information that does not earn its place?"
fix: "Every element must earn its place."
- id: CONSISTENCY
name: "Same term means same thing everywhere"
tier: ux
severity: warning
autofix: false
detect_semantic: "Are the same concepts named differently in different places?"
fix: "Follow conventions. One name per concept."
- id: COST_TRANSPARENCY
name: "Show LLM costs in real-time"
tier: llm
severity: warning
autofix: true
detect_semantic: "Are API calls made without showing token count or cost?"
fix: "Display [$0.0023, 847 tokens] after each call."
- id: CACHE_LLM
name: "Cache deterministic LLM responses"
tier: llm
severity: info
autofix: true
detect_semantic: "Is the same prompt sent multiple times without caching?"
fix: "Hash prompt, cache response with bounded TTL."
- id: GUARD_EXPENSIVE_OPS
name: "Confirm before costly or destructive operations"
tier: safety
severity: error
autofix: true
detect_semantic: "Does this execute an expensive or destructive operation without confirmation?"
fix: "Cost estimate before execution. Require opt-in for danger."
- id: NO_FLAG_ARGUMENTS
name: "A flag that selects behavior means two things hiding as one"
tier: clean_code
severity: warning
autofix: false
detect_lexical: "def \\w+\\([^)]*\\btrue\\b|def \\w+\\([^)]*\\bfalse\\b"
detect_semantic: "Does a boolean cause this unit to do two different things? In a contract, does one condition branch into contradictory obligations?"
fix: "Split into two distinct units. Each does one thing."
- id: NO_OUTPUT_ARGUMENTS
name: "Return results, never secretly modify what was passed in"
tier: clean_code
severity: warning
autofix: false
detect_semantic: "Does this modify its arguments instead of returning a result? Does this clause silently alter a definition from a prior section?"
fix: "Return the result. Leave inputs untouched."
- id: NO_SELECTOR_ARGUMENTS
name: "Arguments that switch behavior indicate hidden multiplicity"
tier: clean_code
severity: warning
autofix: false
detect_semantic: "Is an argument used as a switch to select between behaviors? Does a field mean different things in different contexts?"
fix: "Separate functions, separate types, separate document sections."
- id: DESIGN_BY_CONTRACT
name: "State what you expect, what you promise, what must remain true"
tier: reliability
severity: info
autofix: false
detect_semantic: "Are preconditions, postconditions, and invariants left implicit? Does this API lack documentation of valid inputs and guaranteed outputs?"
fix: "Make contracts explicit. Preconditions, postconditions, invariants."
- id: CRASH_EARLY
name: "A dead process does less damage than a corrupted one"
tier: reliability
severity: warning
autofix: false
detect_semantic: "Does this limp along in a broken state instead of stopping cleanly? Does this continue after contraindication signals?"
fix: "Stop when invariants break. A clean crash is recoverable. Corruption is not."
- id: DEFINE_ERRORS_OUT
name: "Design so error conditions cannot arise"
tier: design
severity: info
autofix: false
detect_semantic: "Is this handling errors that could be eliminated by redesigning the interface? Does this validation reject input that better design would prevent?"
fix: "Redesign so the error cannot occur."
- id: SURFACE_AREA
name: "Minimize the boundary between inside and outside"
tier: design
severity: warning
autofix: false
detect_semantic: "Is the public interface larger than necessary? Does this contract have more exceptions than rules?"
fix: "Fewer public methods. Fewer clauses. Fewer points of contact mean fewer points of failure."
- id: PROGRESSIVE_DISCLOSURE
name: "Reveal complexity only as needed"
tier: ux
severity: info
autofix: false
detect_semantic: "Does this present all complexity at once? Does this front-load definitions before stating obligations?"
fix: "Lead with the simple case. Reveal depth on demand."
- id: FEEDBACK_LOOPS
name: "Every action must produce observable feedback"
tier: ux
severity: warning
autofix: false
detect_semantic: "Does this perform work without reporting progress? Does this protocol lack checkpoints?"
fix: "Close the loop. Every action produces feedback. Every milestone gets measured."
- id: DATA_CLASS
name: "Data without behavior is a missed abstraction"
tier: refactoring
severity: info
autofix: false
detect_semantic: "Does this hold data but contain no behavior? Is logic scattered across other modules? Is this a spreadsheet of raw numbers with formulas elsewhere?"
fix: "Push behavior into the data. Methods belong with the data they operate on."
- id: PARALLEL_INHERITANCE
name: "Two hierarchies that must change in lockstep"
tier: refactoring
severity: warning
autofix: false
detect_semantic: "Does adding a type in one hierarchy require adding one in another? Does a new product line require changes in both catalog and billing?"
fix: "Merge the hierarchies or use composition."
- id: REFUSED_BEQUEST
name: "Inheriting what you do not use"
tier: refactoring
severity: info
autofix: false
detect_semantic: "Does this variant ignore most of what it inherits? Does this addendum negate most of the base agreement?"
fix: "Use composition instead of inheritance."
line:
- id: EXPLICIT
name: "Explicit contracts over implicit coupling"
tier: kernel
severity: warning
autofix: true
detect_structural: explicit
fix: "Make it explicit."
- id: SELF_EXPLAINING
name: "Names reduce need for comments"
tier: kernel
severity: info
autofix: true
detect_semantic: "Does this name clearly reveal intent?"
fix: "Rename to reveal intent."
- id: GUARD_CLAUSE
name: "Favor guard clauses over nested conditionals"
tier: clean_code
severity: info
autofix: false
detect_lexical: "^\\s*def \\w+.*\\n\\s*if .+\\n(?:.*\\n)*?\\s*else\\n(?:.*\\n)*?\\s*end\\s*$"
fix: "Flatten to: return ... unless condition"
languages: [ruby]
- id: SAFE_NAVIGATION
name: "Use &. consistently"
tier: style
severity: warning
autofix: true
detect_lexical: "(\\w+)\\s*&&\\s*\\1\\.\\w+"
fix: "Rewrite to x&.foo&.bar"
languages: [ruby]
- id: EACH_WITH_OBJECT
name: "Prefer each_with_object over inject for hash building"
tier: style
severity: warning
autofix: false
detect_lexical: "\\.(inject|reduce)\\(\\s*\\{\\s*\\}\\s*\\)"
fix: "Use .each_with_object({}) — eliminates mutable-return footgun."
languages: [ruby]
- id: KEYWORD_ARGS
name: "Keyword arguments for 3+ parameters"
tier: style
severity: info
autofix: false
detect_lexical: "def \\w+\\([^)]*,\\s*[^:)]+,\\s*[^:)]+,\\s*[^:)]+\\)"
fix: "Use keyword arguments for clarity and safety."
languages: [ruby]
- id: KERNEL_COERCION
name: "Use Array(), Hash(), String() coercions"
tier: style
severity: info
autofix: true
detect_lexical: "(\\w+)\\s*\\.\\s*nil\\?\\s*\\?\\s*\\[\\]\\s*:\\s*\\1|(\\w+)\\s*\\|\\|\\s*\\[\\]"
fix: "Use Array(x) instead of x.nil? ? [] : x"
languages: [ruby]
- id: PERCENT_LITERAL
name: "Use %i[] and %w[] for symbol/string arrays"
tier: style
severity: info
autofix: true
detect_lexical: "\\[:[a-z_]+,\\s*:[a-z_]+,\\s*:[a-z_]+"
fix: "Use %i[a b c] for symbol arrays."
languages: [ruby]
- id: HASH_FETCH
name: "Prefer Hash#fetch over [] with ||"
tier: style
severity: info
autofix: false
detect_lexical: "\\w+\\[:\\w+\\]\\s*\\|\\|"
fix: "Use hash.fetch(:key, default) for nil-vs-false safety."
languages: [ruby]
- id: TRANSFORM_KEYS
name: "Use transform_keys/transform_values"
tier: style
severity: info
autofix: false
detect_lexical: "\\.each_with_object\\(\\{\\}\\)\\s*\\{\\s*\\|\\(k,\\s*v\\),\\s*h\\|"
fix: "Use .transform_values { |v| ... }"
languages: [ruby]
- id: USE_THEN
name: "Use .then for pipeline transforms"
tier: style
severity: info
autofix: false
detect_lexical: "(\\w+)\\s*=\\s*\\w+\\(.*\\)\\n\\s*\\w+\\(\\1\\)"
fix: "Chain with .then { |r| next_step(r) }"
languages: [ruby]
- id: RESCUE_ON_DEF
name: "Move begin/rescue to def line"
tier: style
severity: info
autofix: false
detect_lexical: "^\\s*def \\w+.*\\n\\s*begin\\n(?:.*\\n)*?\\s*rescue"
fix: "Put rescue directly on the def block."
languages: [ruby]
- id: BARE_RESCUE
name: "Never rescue bare or rescue Exception"
tier: safety
severity: error
autofix: false
detect_lexical: "rescue\\s*$|rescue\\s+Exception"
fix: "Rescue StandardError or a specific class."
languages: [ruby]
- id: N_PLUS_ONE
name: "Detect N+1 queries"
tier: performance
severity: warning
autofix: false
detect_lexical: "\\.(each|map|collect)\\s*(do|\\{).*\\.\\w+\\.\\w+"
fix: "Add .includes(:association)."
languages: [rails]
- id: FIND_EACH
name: "Use find_each for batch processing"
tier: performance
severity: warning
autofix: false
detect_lexical: "\\.(all\\.each|where\\(.*\\)\\.each)\\b"
fix: "Use .find_each(batch_size: 1000)."
languages: [rails]
- id: NO_UPDATE_ATTRIBUTE
name: "Replace update_attribute with update!"
tier: safety
severity: error
autofix: true
detect_lexical: "\\.update_attribute\\("
fix: "update_attribute skips validations. Use update!"
languages: [rails]
- id: PLUCK_OVER_MAP
name: "Prefer pluck over map for single columns"
tier: performance
severity: info
autofix: false
detect_lexical: "\\.\\w+\\.map\\(&:\\w+\\)"
fix: "Use .pluck(:column) to avoid AR object instantiation."
languages: [rails]
- id: CONST_BY_DEFAULT
name: "Use const unless reassigned"
tier: style
severity: warning
autofix: false
detect_lexical: "\\blet\\s+(\\w+)\\s*="
fix: "Use const unless the variable is reassigned."
languages: [javascript]
- id: OPTIONAL_CHAINING
name: "Use ?. over && chains"
tier: style
severity: warning
autofix: true
detect_lexical: "(\\w+)\\s*&&\\s*\\1\\.\\w+"
fix: "Rewrite to obj?.foo?.bar"
languages: [javascript]
- id: NULLISH_COALESCING
name: "Use ?? over || for defaults"
tier: style
severity: info
autofix: false
detect_lexical: "(\\w+)\\s*\\|\\|\\s*\\w+"
fix: "Use ?? when 0 or '' are valid values."
languages: [javascript]
- id: TEMPLATE_LITERALS
name: "Use template literals over concatenation"
tier: style
severity: warning
autofix: true
detect_lexical: "[\"']\\s*\\+\\s*\\w+\\s*\\+\\s*[\"']"
fix: "Use `Hello ${name}!` template literals."
languages: [javascript]
- id: ASYNC_AWAIT
name: "Prefer async/await over .then chains"
tier: style
severity: warning
autofix: false
detect_lexical: "\\.then\\(.*\\.then\\(.*\\.then\\("
fix: "Use async/await for readability."
languages: [javascript]
- id: FOR_OF
name: "Use for...of instead of for...in for arrays"
tier: safety
severity: error
autofix: true
detect_lexical: "for\\s*\\(\\s*(const|let|var)\\s+\\w+\\s+in\\s+"
fix: "for...in iterates prototype properties. Use for...of."
languages: [javascript]
- id: QUOTE_VARIABLES
name: "Always quote $variables"
tier: safety
severity: error
autofix: true
detect_lexical: "(?<![\"'\\\\])\\$\\w+(?![\"'])"
fix: "Use \"$VAR\" to prevent word splitting."
languages: [zsh]
- id: DOUBLE_BRACKET
name: "Use [[ ]] over [ ]"
tier: safety
severity: warning
autofix: true
detect_lexical: "(?<!\\[)\\[\\s+[^[]"
fix: "Use [[ ... ]] for safe conditionals."
languages: [zsh]
- id: DOLLAR_PAREN
name: "Replace backticks with $(command)"
tier: style
severity: warning
autofix: true
detect_lexical: "`[^`]+`"
fix: "Use $(command) — nestable and readable."
languages: [zsh]
- id: IMG_ALT
name: "Require alt on every <img>"
tier: accessibility
severity: error
autofix: false
detect_lexical: "<img\\s+(?![^>]*alt=)"
fix: "Add alt= attribute."
languages: [html]
- id: BUTTON_OVER_ANCHOR
name: "Use <button> for actions, not <a href=\"#\">"
tier: accessibility
severity: warning
autofix: false
detect_lexical: "<a\\s+href=[\"']#[\"']"
fix: "Use <button>. Accessible by default."
languages: [html]
- id: ARIA_INTERACTIVE
name: "ARIA on non-semantic interactive elements"
tier: accessibility
severity: warning
autofix: false
detect_lexical: "<(div|span)\\s+[^>]*onclick"
fix: "Add role= and tabindex= for accessibility."
languages: [html]
- id: LAZY_IMAGES
name: "loading=\"lazy\" on below-fold images"
tier: performance
severity: info
autofix: true
detect_lexical: "<img\\s+(?![^>]*loading=)"
fix: "Add loading=\"lazy\"."
languages: [html]
- id: NO_INLINE_STYLES
name: "Replace inline styles with classes"
tier: design
severity: warning
autofix: false
detect_lexical: "\\bstyle=\"[^\"]*\""
fix: "Extract to CSS class."
languages: [html]
- id: LOGICAL_PROPERTIES
name: "Prefer logical properties for RTL support"
tier: design
severity: info
autofix: true
detect_lexical: "(margin|padding)-(left|right):"
fix: "Use margin-inline-start/end, padding-inline-start/end."
languages: [css]
- id: CLAMP_TYPOGRAPHY
name: "Use clamp() for fluid typography"
tier: design
severity: info
autofix: false
detect_lexical: "@media.*\\{[^}]*font-size:"
fix: "Use font-size: clamp(1rem, 2.5vw, 1.5rem)."
languages: [css]
- id: MEANINGFUL_NAMES
name: "Names reveal intent"
tier: clarity
severity: info
autofix: true
detect_lexical: "\\b(tmp|temp|data|result|val|ret|obj|str|arr|buf)\\b\\s*="
fix: "Use domain-specific names. user_profile, error_message."
- id: WHY_NOT_WHAT
name: "Comments explain why, not what"
tier: clarity
severity: info
autofix: false
detect_lexical: "#\\s*(increment|set|get|update|return|initialize|create|add)\\s+\\w+"
fix: "Comments should explain intent, not restate the code."
- id: DEAD_CODE
name: "Eliminate unreachable code"
tier: clean_code
severity: warning
autofix: true
detect_lexical: "(return|exit|raise|throw)\\s+.*\\n\\s*\\w+"
fix: "Remove code after return/exit/raise/throw."
- id: TRAILING_COMMAS
name: "Trailing commas in multi-line collections"
tier: style
severity: info
autofix: true
detect_semantic: "Does this multi-line collection lack a trailing comma?"
fix: "Add trailing comma so additions produce one-line diffs."
- id: TYPOGRAPHIC_EXCELLENCE
name: "Typographic excellence in user-facing text"
tier: aesthetic
severity: info
autofix: true
detect_lexical: "[\"']\\.\\.\\.[\"']|[\"']--[\"']"
fix: "Use ellipsis, em dash, curly quotes in UI strings."
- id: SILENCE_ON_SUCCESS
name: "Successful operations produce minimal output"
tier: interface
severity: info
autofix: false
detect_semantic: "Does this output say more than necessary for a successful operation?"
fix: "Default to silence on success. One line for routine completions."
- id: TYPOGRAPHY_DISCIPLINE
name: "Hierarchy via weight and brightness, not decoration"
tier: interface
severity: info
autofix: true
detect_lexical: "[-=]{3,}|[╭╮╰╯│─]"
fix: "No ASCII separators. No box drawing. Whitespace is the layout tool."
- id: PRECOMPUTE_MATH
name: "Precompute expensive math"
tier: performance
severity: info
autofix: true
detect_semantic: "Are trig functions or noise lookups called per-frame per-object?"
fix: "Precompute tables. Cache distance. Use squared distance comparison."
- id: AUDIO_SMOOTHING
name: "Smooth audio-reactive visuals"
tier: aesthetic
severity: info
autofix: false
detect_semantic: "Do visual elements jump erratically with raw audio data?"
fix: "Exponential smoothing. Separate decay rates. Attack-decay envelope."
- id: GRACEFUL_LOAD
name: "Degrade quality under load, do not crash"
tier: performance
severity: warning
autofix: true
detect_semantic: "Does this run at full quality until it crashes instead of scaling down?"
fix: "EWMA frame timing. Dynamic resolution scaling. Emergency brake."
- id: ANALOG_WARMTH
name: "Perfect is sterile"
tier: aesthetic
severity: info
autofix: true
detect_semantic: "Is generated imagery clinically perfect with zero texture?"
fix: "Film grain. Vintage lens softness. Subtle color cast."
- id: DOMAIN_LANGUAGE
name: "Speak in the vocabulary of the problem, not the implementation"
tier: clarity
severity: warning
autofix: false
detect_semantic: "Does this code use generic programming terms (manager, handler, processor, data) instead of domain terms (patient, invoice, genome, verdict)? Does this legal document use lay terms where precise legal terms exist? Does this medical report use colloquial language instead of standard nomenclature?"
fix: "Use the ubiquitous language of the domain. Every domain has precise terms — use them."
- id: LOAD_BEARING_NAMES
name: "Names carry structural weight — choose them to bear it"
tier: clarity
severity: warning
autofix: false
detect_semantic: "Are names vague, generic, or misleading? Does 'data' mean input, output, or both? Does 'process' mean validate, transform, or persist? Does 'miscellaneous' appear as a category? In law, are terms used loosely that have precise legal meaning? In medicine, is a diagnosis vague where a specific code exists?"
fix: "Names are load-bearing walls. They define how people think about the system. Choose names that carry the full weight of their meaning."
- id: ERROR_CONTEXT
name: "Every error must carry enough context to locate and understand its origin"
tier: reliability
severity: warning
autofix: false
detect_semantic: "Does this error message lack context about where and why it occurred? Does this rejection letter fail to state which requirement was not met? Does this lab result omit which sample or protocol produced the anomaly?"
fix: "Wrap low-level errors with domain context. State what was attempted, what failed, and what to do next."
- id: COMMENTS_AS_DEODORANT
name: "Explanations that mask bad structure instead of fixing it"
tier: clean_code
severity: warning
autofix: false
detect_semantic: "Is this comment explaining what bad code does instead of rewriting the code to be self-evident? In prose, is a footnote compensating for an unclear sentence? In a contract, is a definition section papering over ambiguous clauses?"
fix: "Rewrite the artifact so explanation is unnecessary. If it needs a comment, it needs a rewrite."
- id: POSTEL
name: "Be conservative in what you send, liberal in what you accept"
tier: architecture
severity: info
autofix: false
detect_semantic: "Does this interface reject valid inputs that differ only in form? Does it emit outputs with more precision or structure than callers need? Does this protocol refuse valid encodings? Does this form reject data that could be safely normalized?"
fix: "Accept broadly, emit precisely. Validate structure not style. Normalize on the way in."
- id: HYRUM
name: "All observable behaviors become depended upon at scale"
tier: architecture
severity: warning
autofix: false
detect_semantic: "Is this changing an undocumented behavior that callers may rely on? Is this removing a side effect that was never specified but may be consumed? Is this contract revision eliminating something parties have silently come to depend on?"
fix: "Treat every observable behavior as a contract once you have users. Deprecate explicitly, remove slowly."
- id: LEAKY_ABSTRACTION
name: "All non-trivial abstractions leak"
tier: architecture
severity: warning
autofix: false
detect_semantic: "Does this abstraction force the caller to know about implementation details? Does the interface require knowledge of underlying protocols, data formats, or failure modes it was supposed to hide? Does this clause invoke legal theory the reader must independently research?"
fix: "Plan for the leak. Document what leaks. Make the happy path abstracted, the edge cases explicit."
- id: TEMPORAL_COUPLING
name: "Hidden sequential dependencies are fragile"
tier: design
severity: warning
autofix: false
detect_semantic: "Does this require callers to call methods in a specific order without enforcing it? Is there an implicit initialization sequence that crashes if violated? Does this workflow assume steps execute in a fixed order without encoding that order?"
fix: "Enforce sequencing structurally: pass initialized objects, not bare instances. Use builder pattern. Make invalid order unrepresentable."
- id: HUMBLE_OBJECT
name: "Separate testable logic from hard-to-test boundaries"
tier: design
severity: info
autofix: false
detect_semantic: "Is business logic mixed with IO, UI rendering, or external API calls making it untestable? Is decision logic tangled with presentation in a way that forces integration tests where unit tests would suffice?"
fix: "Extract the logic into a pure object (Humble Object). Keep the IO shell thin and untested; test the logic object exhaustively."
- id: FULL_BY_DEFAULT
name: "No fake-choice tiers — defaults are maximal correctness"
tier: design
severity: warning
autofix: false
detect_lexical: "\\b(shallow|standard|quick|lite|basic|light|simple)\\b\\s*[|,)\\]]\\s*\\b(deep|full|advanced|complete|thorough)\\b"
detect_semantic: "Does this API expose a 'do less' tier (shallow/standard/quick/lite/basic) alongside a 'do full' tier — where every real user always picks the full one? If so, it's a fake choice. Either drop the lower tier and make full the only mode, or, if a real cost tradeoff exists, name the tiers honestly (e.g. lexical|structural|semantic) so the cost actually being paid is surfaced."
fix: "Drop the degraded tier. If a real cost tradeoff exists, rename to surface the cost (lexical < structural < semantic), not the result quality."
- id: PATTERN_EXTRACTION
name: "File is 80% of the way to a named design pattern"
tier: design
severity: info
mode: opportunity
autofix: false
principle: "Crystallize emergent shape into a name; the right pattern compresses code and makes intent legible."
medium: [ruby]
detect_semantic: "Is this file close to instantiating a known pattern from {Strategy, Decorator, Pipeline, Visitor, Builder, Observer, Command, State, Adapter, NullObject}? Only flag when extraction would genuinely reduce complexity — never propose a pattern that adds ceremony for its own sake. Format: PATTERN:LINE:short reason."
fix: "Extract toward the named pattern only if the resulting code is simpler than the current shape; otherwise leave it."
# Falsifiable identity — what MASTER explicitly does NOT aim for, and the
# failure modes that mean the system has lost its way.
# Source: master2 v4 MANIFEST reunification.
anti_goals:
- replace_human_judgment # MASTER augments, never automates the decision
- enforce_style_preferences # only timeless axioms — never trend or taste
- optimize_for_speed # correctness precedes performance
- support_every_language # depth over breadth
- be_easy_to_use # demands precision, not friendliness
success_criteria:
- every_output_respects_axioms
- council_surfaces_real_concerns_not_theater
- system_applies_to_itself_without_exception
- engineers_trust_judgment_over_time
failure_modes:
rubber_stamp: "council approves everything — adversarial role lost"
blocks_everything: "council vetoes every proposal — gridlock, no shipping"
self_violation: "MASTER produces output it would itself reject — recursive quality broken"
# Principle enforcement tiers — tier1 strict, tier2 strong, tier3 opportunistic.
# Source: master.json v225 reunification.
principle_priorities:
tier1_critical: # halts pipeline on violation
- evidence: "@assumption -> validate"
- reversible: "@irreversible -> add_rollback"
- security: "@untrusted -> validate_sanitize"
- self_apply: "@output -> must_pass_own_constitution"
tier2_quality: # warns and routes to refactor
- dry: "@duplication>=3 -> abstract"
- kiss: "@complexity>10 -> simplify"
- srp: "@two_reasons_to_change -> split"
tier3_polish: # opportunistic improvements
- explicit: "@implicit -> make_explicit"
- minimalism: "@bloat -> subtract"
- rhythm: "@inconsistent -> align"
# Failure taxonomy — what to retry, what to fail-fast, what to escalate.
# Source: master.json v225 reunification.
failure_taxonomy:
transient:
examples: [network_timeout, rate_limit, temp_file_conflict, provider_overload]
strategy: exponential_backoff
max_retries: 3
permanent:
examples: [syntax_error, missing_dependency, permission_denied, schema_violation]
strategy: fail_fast
max_retries: 0
ambiguous:
examples: [partial_write, unknown_error, half_committed_state]
strategy: human_intervention
checkpoint_before: true
# Evidence weights for ship/no-ship decisions. Sum >= 80 to pass tier1 gates.
# Source: master.json v225 reunification.
evidence_scoring:
weights:
test_pass: 35
scan_clean: 25
code_review: 20
log_analysis: 10
profiling_data: 10
pass_threshold: 80
block_threshold: 50 # below this, propose rollback
# Schema metadata for every named rule. Optional fields per rule.
# Source: master.json v225 + master.yml v49 reunification (#51, #53, #57, #64).
schema_metadata:
optional_fields:
reversibility: "free | cheap | surgical | impossible" # rollback cost
time_horizon: "review | deploy | incident" # when this fires
blast_radius: "files_touched: integer" # batching hint
provenance:
added_in_commit: sha
rationale: string
last_revised: iso8601
# Explicit forbidden + discouraged patterns with reasons.
# Source: master.json v225 ANALYSIS reunification (#23).
anti_patterns:
forbidden:
- {pattern: 'eval\(.*user.*\)', reason: arbitrary_code_execution}
- {pattern: "rm -rf /", reason: data_loss}
- {pattern: "while true.*no sleep", reason: resource_exhaustion}
- {pattern: 'system\(.*\$\{', reason: command_injection}
- {pattern: 'Marshal\.load', reason: deserialization_rce}
- {pattern: 'open\(.*\$\{', reason: shell_through_open}
discouraged:
- {pattern: god_object, reason: violates_solid_srp}
- {pattern: premature_optimization, reason: violates_yagni}
- {pattern: defensive_copying_everywhere, reason: complexity_for_imagined_failure}
- {pattern: speculative_generality, reason: yagni_in_disguise}
# Recursive proof — for each Universal Law, MASTER must satisfy the law itself.
# Source: master.yml v31 reunification (#58).
self_test:
laws_apply_to_self:
ROBUSTNESS: "scan lib/ for bare_rescue + missing timeouts"
SINGULARITY: "rules.yml entries unique by id; no two YAMLs define same key"
LINEARITY: "no nesting_depth > 4 in lib/"
PROXIMITY: "test files within 1 directory of source"
ABSTRACTION: "no class > god_class threshold in lib/"
DENSITY: "no method > long_method threshold in lib/"
fail_action: "publish self_violation event; refuse autoloop fixes that don't restore the law"
# Before marking a task done, re-read the file fresh — never trust cached state.
# Source: Junie reunification (#71).
ground_truth_check:
require_fresh_read_before:
- claim_task_complete
- council_vote_on_change
- commit_creation
cache_max_age_seconds: 5
# Refactors must not silently change behavior. Cleaner is not a license to differ.
# Source: Cursor v2.0 reunification (#66).
preserve_user_intent:
forbidden_changes_during_refactor:
- public_method_signature
- error_class_raised
- return_type_shape
- side_effects_order
- log_format_consumed_by_others
require_explicit_approval_for: behavior_change
# Detect and recover from LLM gaslighting / repetition / bad XML.
# Source: opencrabs reunification (#82).
phantom_recovery:
detectors:
gaslighting_preamble: "/^(I('ll| will| can| would)|Let me|Sure,)/i"
text_repetition_loop: "same 60-char span repeats >= 3 times"
xml_tool_call_failure: "<tool> open without close OR malformed JSON in args"
empty_tool_response: "tool returned nil twice in a row"
recovery:
- "discard last response, reset context to last successful checkpoint"
- "switch model tier (escalate or fall back) on second occurrence"
- "publish phantom:detected event, halt loop on third"
# Never assume a gem is available. Check Gemfile / require_relative target before use.
# Source: Devin reunification (#36).
library_verify:
pre_flight_checks:
- "Gemfile.lock contains the gem at the expected version"
- "require_relative path exists on disk"
- "binary on PATH (which / command -v) before shell-out"
fail_action: "Result.err :missing_dependency; do not retry without human"# soul.yml — machine-enforced constitutional schema
# Human-readable narrative lives in SOUL.md.
# ABSOLUTE sections require constitutional override to amend.
# Negotiable sections: soul propose -> soul approve -> bump version.
version: "2.5.0"
persona: malay
voice: ms-MY-OsmanNeural
language:
primary: english
secondary: norwegian
dialect: bokmal
prompt_ordering:
- master_identity
- master_output_format
- master_meta_instruction
- master_constitution_absolute
- master_priority
- master_constitution_kernel
- master_refusal_policy
- master_style
absolute:
golden_rule: PRESERVE_THEN_IMPROVE_NEVER_BREAK
sacred_paths:
- data/
- SOUL.md
- CLAUDE.md
- CONVENTIONS.md
- README.md
- .claude/
- lib/judge/scan/
- bin/cli
anti_simulation:
forbidden: [will, would, could, might]
require_evidence:
file_read: show content with SHA-256
modification: show unified diff
completion: show command output
protection_tiers:
ABSOLUTE: abort pipeline
PROTECTED: emit warning continue
NEGOTIABLE: allow if explicitly permitted
FLEXIBLE: negotiate at runtime
code_rules:
FAIL_VISIBLY: never rescue Exception or bare rescue that swallows errors silently. Always rescue StandardError or a specific class.
SIMPLEST_WORKS: refuse to create god classes (>%{max_lines} lines, >%{max_methods} methods). Push back and suggest decomposition.
PRESERVE_FIRST: never rewrite working code from scratch. Read first, patch minimally.
BE_CONCISE: minimal response. If the answer is one word, say one word.
REGISTER_STABLE: hold register, density, and length consistent across a session; shift only when the user shifts.
MIRROR_EXPERTISE: match vocabulary to the user's demonstrated tier; gloss every new term on first use.
SURFACE_ERRORS_FIRST: failures and blockers lead the response; context and explanation follow.
NO_DEAD_ENDS: every closed door names an adjacent open one.
PREEMPT_FOLLOWUP: answer the one obvious follow-up question in the same response; no second round-trip.
RTFM_FIRST: read the man page or upstream reference docs before using any command, config file, framework, operating system, or programming language. Training-data assumptions about flags, syntax, and behavior are unreliable — verify against the source.
aesthetic_rules:
NO_ASCII_DECORATION: no box-drawing, no --- / === / ### as visual separators in code, comments, docs, or CLI output. Content is the separator.
NO_COLUMN_ALIGN: never pad with multiple spaces to align columns — applies to code and comments alike. Hashes, case branches, arg lists, YAML values, inline comments — one space, never aligned. Column alignment decays the moment one entry grows, then every neighbour gets re-padded; the diff churns and the meaning hides.
NO_CONSECUTIVE_BLANK_LINES: one blank line between logical sections; zero between closely related lines.
IMPORTANCE_ORDER: every file's lines flow high-to-low importance. Public API first, primary logic next, helpers last. Readers should get the full picture from the top third.
FLAT_UI: uniform particle/element size at rest; depth only when the collective resembles a 3D model. No fake-3D borders, no drop-shadows on flat surfaces.
CINEMA_PALETTE: shadow/midtone/highlight triplets; complementary anchors. Never raw primaries, never linear easing — cubic-bezier curves on every transition.
STRUNK_ACTIVE: 'active voice throughout — code, comments, commit messages, CLI output, TTS. Omit needless words. Concrete verbs: emit, prune, route; not perform, handle, deal with.'
INVERTED_PYRAMID: commits, comments, and log lines lead with the fact. Context and explanation trail. dmesg format — no preamble.
negotiable:
style: sentence_case
default_model: claude-opus-4-7
tts_voice: ms-MY-OsmanNeural
language_detection: true
evolution_log:
- version: "1.0.0"
date: "2026-04-01"
change: initial SOUL.md constitutional identity
author: dev
- version: "2.0.0"
date: "2026-04-24"
change: OpenClaw-inspired restructure
author: dev
- version: "2.1.0"
date: "2026-04-27"
change: restored from sweep corruption
author: dev
- version: "2.2.0"
date: "2026-05-05"
change: native rubocop autocorrect pre-pass; scanner split into parallel_each, scan_one, stream_progress
author: dev
- version: "2.3.0"
date: "2026-05-08"
change: code_rules moved from personality.rb hardcoded strings into absolute.code_rules
author: dev
- version: "2.4.0"
date: "2026-05-14"
change: add interaction code_rules — REGISTER_STABLE, MIRROR_EXPERTISE, SURFACE_ERRORS_FIRST, NO_DEAD_ENDS, PREEMPT_FOLLOWUP
author: dev
- version: "2.5.0"
date: "2026-05-19"
change: prompt_ordering resequenced; master_output_format hoisted to slot 2; master_tools section removed; never list split into output_never/opener_never/closer_never
author: dev
- version: "2.5.0"
date: "2026-05-16"
change: add aesthetic_rules — NO_ASCII_DECORATION, NO_COLUMN_ALIGN, IMPORTANCE_ORDER, FLAT_UI, CINEMA_PALETTE, STRUNK_ACTIVE, INVERTED_PYRAMID; NO_ASCII_LINE_ART + NO_COLUMN_ALIGN scan rules; MMA/comedy expression vocabulary in face.js
author: devstale_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---
# 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# 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>© %{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# 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/ }# 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# 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# 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 }# 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# 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# 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]# MASTER workflow rules — operational principles codified from CLAUDE.md.
# Governs how MASTER and its LLM agents read, edit, scan, and fix code.
file_reading:
rule: READ_FULL_FILES
statement: "Read complete files. Never grep, head, tail, or partial‑read to understand code."
rationale: "Partial view yields partial (wrong) changes."
allowed_exceptions:
- "grep/search across many unknown files to locate a keyword"
forbidden:
- "grep pattern file to understand code structure"
- "head -N file to check structure"
- "tail -N file to check endings"
before_edit:
rule: READ_BEFORE_WRITE
statement: "Read every file that could be affected before editing any file."
steps:
- "Map the codebase: find all .rb files in lib/"
- "Trace callers before changing any public method signature"
- "Check Zeitwerk inflectors before renaming classes or files"
- "Run ruby -c FILE after every write"
- "Run ruby -e require_relative after every commit"
code_principles:
no_hardcoding:
rule: NO_HARDCODED_CONSTANTS
statement: "Prose, patterns, and config belong in data/*.yml, not Ruby strings."
single_source:
rule: ONE_SOURCE
statement: "If it is in a data file, the code reads from there. No duplicates."
no_magic_numbers:
rule: NAMED_CONSTANTS
statement: "Extract literals to named constants with .freeze"
no_bare_rescue:
rule: SPECIFIC_RESCUE
statement: "Always rescue SpecificError => e. Propagate or log via event bus."
guard_first:
rule: GUARD_CLAUSES_FIRST
statement: "Return Result.ok(ctx) unless condition before main logic."
one_responsibility:
rule: SINGLE_RESPONSIBILITY
statement: "Split if you can name two reasons to change it."
cqs:
rule: COMMAND_QUERY_SEPARATION
statement: "Queries return data and do not mutate. Commands mutate and do not return values."
inject_deps:
rule: DEPENDENCY_INJECTION
statement: "Never instantiate collaborators inside a method."
result_monad:
rule: RESULT_MONAD
statement: "Use respond_to?(:ok?) not is_a?(Result) for duck‑typing."
scan_rules:
standard_depth:
- frozen_string
- bare_rescue
- explicit
- immutable
- cqs
- self_explaining
- long_method
- god_class
- duplicate_code
- prune
- srp
- pola
- nielsen
deep_only:
- semantic
- adversarial
hunt_only:
- rubocop
- reek
notes:
nielsen: "puts is NOT debug output in a CLI. Only p, pp, binding.pry, debugger are."
prune: "Loads patterns from data/rules.yml (voice.strunk) — single source of truth."
semantic: "Loads philosophy from data/rules.yml (zen + voice) — single source of truth."
deep_caution: "deep adds 2 LLM calls per file. With 90 files at 8 req/min free tier = 22+ minutes."
principle_groups:
axioms: [frozen_string, explicit, immutable, self_explaining]
solid: [srp, cqs, pola]
clean_code: [long_method, god_class, duplicate_code, bare_rescue]
interface: [nielsen, prune]
llm_rules: [semantic, adversarial]
heavy: [rubocop, reek]
quick: [frozen_string, bare_rescue, explicit, long_method, god_class]
critical: [frozen_string, bare_rescue, explicit, immutable, srp, cqs]
scan_profiles:
critical:
rules: critical
description: "Critical issues blocking ship"
solid:
rules: solid
description: "SOLID principles focus"
axioms:
rules: axioms
description: "Constitutional axioms only"
conflicts:
strategy: highest_priority_wins
rules:
- condition: "dry conflicts with wet or aha"
resolution: "favor wet/aha if fewer than 3 duplications exist"
- condition: "clarity conflicts with simplicity"
resolution: "favor clarity"
- condition: "fix introduces higher priority violation"
resolution: "reject fix, report to autoloop"
universal_scope:
policy: ALL_PRINCIPLES_ALL_FILES
statement: >
All axioms, principles, and philosophies apply to every file in the codebase
regardless of file type: Ruby, YAML, Zsh, HTML, CSS, JavaScript, Markdown.
Language-specific rules apply only to their target language; universal rules
(SQUINT_TEST, TYPOGRAPHY_DISCIPLINE, MEANINGFUL_NAMES, etc.) apply everywhere.
scan_glob: "**/*.{rb,rake,erb,html,htm,css,scss,js,ts,jsx,tsx,zsh,sh,yml,yaml,md}"
semantic_rules: all_known_languages
adversarial_rules: all_known_languages
incremental_scanning:
enabled: true
strategy: modified_files_only
triggers:
full_scan:
- new_principle_added
- master_yml_modified
- user_requests_full_scan
incremental:
- file_saved
- git_stage
- git_commit
speedup_estimate: "60-85% fewer files on typical edit"
fallback: full_scan_on_error
autoloop:
background: true
idle_sleep: 60
scan_depth: standard
fix_depth: llm
batch_size: 3
max_cycles: 12
rate_limit_sleep: 15
max_file_bytes: 16000
max_fix_retries: 3
confidence_threshold: 0.60
targets:
- lib/
- test/
- data/
- web/
- DEPLOY/
excludes:
- vendor/
- knowledge/
- fix_
- patch_
skip_rules:
- duplicate_code
- semantic
- adversarial
- rule_coverage
- immutable
- self_explaining
- long_method
- pola
- srp
- cqs
- rubocop
- reek
sweep:
scan_depth: deep
converge_threshold: 0.05
converge_window: 2
max_cycles: 16
codebase_map: true
zeitwerk:
inflections:
autoloop: AutoLoop
cli: CLI
mcp_server: MCPServer
mcp_coordinator: McpCoordinator
diff_stager: DiffStager
code_index: CodeIndex
git_context: GitContext
ast_edit: AstEdit
llm: LLM
anti_sprawl:
forbidden_files:
- summary.md
- analysis.md
- report.md
- todo.md
- notes.md
- changelog.md
rule: "Edit existing files. Single source of truth."
validation:
after_write: "ruby -c lib/FILE.rb"
after_commit: "ruby -e \"require_relative 'lib/master'; puts 'ok'\""
scan_file: "bundle exec ruby bin/cli scan lib/FILE.rb"
phases:
discover:
id: 1
goal: "Understand actual need"
output: "Problem statement with success criteria"
personas: [chaos, user]
introspect: "What assumptions did we make?"
gates:
- no_vague_words
- audience_identified
- success_measurable
analyze:
id: 2
goal: "Break into components"
output: "Component diagram with dependencies"
personas: [skeptic, maintainer]
introspect: "What did we miss?"
gates:
- components_distinct
- dependencies_acyclic
ideate:
id: 3
goal: "Generate 15+ alternatives"
output: "List of approaches with trade‑offs"
personas: [minimalist, chaos]
introspect: "Which ideas surprised us?"
gates:
- count_gte_15
- trade_offs_documented
design:
id: 4
goal: "Specific architecture"
output: "Interface definitions and error handling"
personas: [security, accessibility]
introspect: "What could break?"
gates:
- interfaces_explicit
- errors_documented
implement:
id: 5
goal: "Execute with zero violations"
output: "Working code at 100/100 score"
personas: [performance, security]
introspect: "Is this the simplest solution?"
gates:
- tests_pass
- zero_violations
validate:
id: 6
goal: "Prove with evidence"
output: "Test results, benchmarks"
personas: [skeptic, security]
introspect: "What evidence proves it works?"
gates:
- zero_test_failures
- edge_cases_covered
deliver:
id: 7
goal: "Ship with monitoring"
output: "Deployed code with dashboards"
personas: [realist]
introspect: "What would the user complain about?"
gates:
- deployed
- monitoring_configured
learn:
id: 8
goal: "Extract durable lessons"
output: "Updated defaults, new memory entries, refined rules"
personas: [skeptic, chaos]
introspect: "What would we do differently next time?"
gates:
- lessons_captured
- defaults_updated
session_startup:
mandatory_reads:
- data/soul.yml
- data/rules.yml
- data/ruby_style.yml
- data/workflow.yml
- data/standing_orders.yml
check_standing_orders: "Verify FSM state before any mutation -- UNCHANGE blocks refactoring"
scan_before_analysis: "Use /scan deep via MASTER, not external agents, for code analysis"
ssh_edit_pattern: "Write to /tmp, run ruby /tmp/patch.rb -- never ruby -i with heredoc"
corruption_prevention:
llm_error_in_file: "git checkout HEAD -- data/ && rcctl restart master -- LLM error strings silently overwrite YAML data files when circuit is open and agent#ask returns error string instead of raising"
sweep_excludes_data: "Sweep may scan data/*.yml but must not LLM-rewrite them. Structural violations (duplicate keys, bad indentation) are auto-fixed. Exclude .master/ runtime state files entirely."
yaml_type_guards: "All load_yaml calls must type-check result before use (is_a?(Array/Hash)) -- circuit-open strings parse as valid YAML scalars"
ask_raises_on_error: "agent#ask must raise StandardError when result.err? -- callers must rescue, never silently propagate error strings as LLM output"
# Phase-appropriate quality gates — prototype is permissive, production is strict.
# Source: master.json v225 reunification.
quality_phases:
prototype:
gates: [functional]
debt_allowed: high
speed: maximize
production:
gates: [functional, secure, maintainable, performant]
debt_allowed: none
speed: sustainable
legacy:
gates: [functional, secure]
changes: surgical_only
risk_tolerance: minimal
# Conflicts already declared above; add reinforcements that multiply effectiveness.
# Source: master.json v225 reunification.
principle_reinforcements:
dry_and_kiss: multiply_effectiveness
evidence_and_reversible: enables_confident_change
single_responsibility_and_dependency_injection: enables_isolated_testing
# Observability spec — structured logging, levels, exported metrics.
# Source: master.json v225 reunification.
observability:
logging:
format: json
fields: [timestamp, level, phase, file, evidence_score, decision]
levels:
trace: every_operation
debug: decision_points
info: phase_transitions
warn: evidence_below_threshold
error: gate_failures
metrics:
track: [evidence_score, complexity, coverage, churn, council_consensus]
export_format: prometheus
threshold_alerts: true
# Adaptive file-processing strategy by size. Replaces hard MAX_FILE_BYTES cutoff.
# Source: master.json v225 reunification.
processing_strategies:
small_file:
condition: "size <= 10240"
method: full_read
context: entire_file
medium_file:
condition: "10240 < size <= 1048576"
method: streaming
context: line_by_line_with_lookahead
large_file:
condition: "size > 1048576"
method: chunked
context: section_by_section
checkpoint: per_chunk
# Cap on tool calls per turn before MASTER pauses for self-reflection.
# Source: Augment reunification (#68, #69).
tool_budget:
max_calls_per_turn: 40
max_consecutive_edits: 8 # forces a read-back after N edits
max_consecutive_searches: 10
on_exceed: "publish budget:exceeded; force /reflect before next call"
# Principle pairs that pull in opposite directions; declare resolution.
# Source: master.json v225 reunification (#54).
antagonisms:
minimalism_vs_explicit: "explicit wins for safety-critical paths"
speed_vs_evidence: "evidence wins always"
abstraction_vs_proximity: "proximity wins below 3 occurrences"
dry_vs_singularity: "singularity wins for data, dry wins for code"
completeness_vs_density: "density wins; defer completeness to next iteration"
# Save LLM calls — only run round 2 if round 1 dissent exceeds threshold.
# Source: master.yml v31 reunification (#61).
two_stage_council:
round_one: "each persona votes ack/dissent independently, no debate"
dissent_threshold: 0.30 # fraction of weighted disagreement triggering round 2
round_two: "only dissenting personas debate, others abstain"
saves: "60-80% of LLM calls when consensus is clear"
# Long tasks surface a navigable thread of decisions.
# Source: Amp reunification (#70).
view_thread:
emit_event: "thread:decision"
fields: [timestamp, phase, file, decision, alternatives_considered, rationale]
persist_to: "data/threads/${session_id}.jsonl"
rotate_after_days: 7
# Auto-compact context every N turns. Pairs with data/compression.yml.
# Source: Cline reunification (#77).
checkpoint_summarization:
every_n_turns: 5
every_n_tokens: 100_000
keep_verbatim: ["last_3_user_messages", "current_violation_set", "current_diff"]
summarize: ["all_other_turns"]
target_compression: 0.30 # aim for 30% of original
# Spec arrives -> code streams in parallel where safe.
# Source: Bolt reunification (#78).
spec_streaming:
triggers: ["new_module_creation", "stub_to_implementation"]
safe_for: ["non-overlapping files", "additive_changes"]
forbidden_for: ["mutating_shared_state", "schema_migrations"]
# UI changes must render before commit. Closes the visual-regression gap.
# Source: Lovable reunification (#79).
live_preview_gate:
triggers: ["edit under web/app/views/", "edit under web/app/assets/"]
require: "successful render at preview URL within 10s"
on_failure: "block commit; publish preview:render_failed"
# Feed MASTER its own commits and ask: would you approve these now?
# Source: cross-cutting reunification (#90).
reverse_introspection:
cadence: "after_every_10_commits"
sample_size: 5
on_disapproval: "open issue tagged self_review with proposed reversion"
# For each scan rule, auto-generate test cases proving it fires/passes correctly.
# Source: cross-cutting reunification (#94).
test_generation:
target_dir: "test/rules_generated/"
cadence: "on_rule_definition_change"
coverage_required: 0.80
# Self-rewrite throttle — sensitive paths require explicit user approval.
# Source: cross-cutting reunification (#100).
self_rewrite_throttle:
sensitive_paths:
- lib/loop/
- lib/ground/
- lib/judge/security/
- bin/cli
- data/standing_orders.yml
autoloop_action: "skip with publish autoloop:throttled event; surface to user"
# Adversarial questioning — every scan, sweep, and council turn asks these.
# Trivial findings get autofixed immediately. Non-trivial findings get N options
# proposed, from which the best is cherry-picked by the operator or judge.
adversarial_questioning:
mandate: fires_on_every_scan_and_council_turn
questions:
- "What is wrong with this design that I have not spotted?"
- "What would an attacker do with this code?"
- "What assumption is this built on that could be false?"
- "What breaks at scale or under failure?"
- "Is this wired to anything? Could it be deleted without loss?"
- "Is there a simpler approach that was not taken?"
- "What should be relocated or transformed to a different format?"
resolution:
trivial: autofix immediately — no proposal, no prompt
non_trivial: "propose 3 concrete options (OPTION 1/2/3 + RECOMMEND), await cherry-pick"
threshold: "non-trivial if the fix spans more than one file, touches architecture, or has security implications"
# Known issues — active bugs and watch-outs from HANDOFF.md.
known_issues:
tts_broken: "Master::Voice::Speech.synthesize_bytes shells to bin/tts-worker; if edge-tts is missing or tts-worker crashes, speech silently falls back to espeak. Verify bin/tts-worker exists and edge-tts is installed (pkg_add py3-edge-tts or pip)."
particle_swarm: "cognition_ecology.js trail/weather/memory-node system suspected broken — visual_bridge.js dispatches master:visual CustomEvents but cognition_ecology.js event listener may not be registering. Check addEventListener target and event name match."
gemfile_separation: "MASTER/Gemfile and MASTER/web/Gemfile are independent. Any gem used by lib/ code that is also called from web/ controllers must appear in both. Adding to one only causes LoadError in the other context."
tts_worker_process: "Process.fork inside a Falcon fiber raises 'Closing scheduler' — EM-based gems and subprocess-heavy tools must use Open3.popen or shell out via exe/<name>-worker, never fork."# 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# 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 libThen:
- inspect runtime/events
- inspect runtime/telemetry
- inspect replay/checkpoints
- inspect provider routing
- inspect namespace audit output
Every irreversible action must emit:
before_event
execution
after_event
verification
telemetry
repair_ticket_on_failure
Missing events are runtime corruption.
Do not trust:
- summaries
- assumptions
- inferred state
- UI appearance
- model narration
Trust:
- runtime events
- telemetry
- checkpoints
- explicit verification
- replay reconstruction
Logs should resemble dmesg:
- terse
- timestamped
- subsystem-prefixed
- stable ordering
- grepable
- operationally meaningful
Avoid:
- emoji logs
- spinner spam
- decorative narration
- fake progress
- hype language
Visual state must derive from:
- runtime events
- provider telemetry
- workflow topology
- repair state
- replay state
Not from guessed frontend state.
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
- Land runtime primitives and policy files.
- Make smoke and stale-namespace audit hard refactor gates.
- Route all model calls through provider policy.
- Wrap tool execution with contracts and event logging.
- Add failure digest and provider health jobs to heartbeat.
- Add checkpoint/replay reconstruction for full workflows.
- Feed visual bridge from canonical runtime events.
- Add UI panels for event stream, provider health, context pressure, and repair queue.
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
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.
Before merge:
- Rebase onto current
main. - Run syntax checks.
- Run
/scanor equivalent static scan. - Run
/ecologyif tree structure changed. - Review touched paths.
- Confirm no overlapping session changed the same files.
- Merge with a clear summary.
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.
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
Every branch that mutates behavior should report:
- files changed
- commands added
- runtime risks
- rollback behavior
- test status
- known uncertainty
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.
# Face3D particle system migration
This document describes the incremental path from the current retro canvas face to a more coherent semantic 3D face-as-particles engine.
## Goals
- Keep the existing retro Atkinson/Bayer/ZX/phosphor look.
- Move face geometry into normalized 3D coordinates.
- Move expression logic into blendshape state.
- Keep particles semantically assigned to anatomical zones.
- Use typed arrays in hot loops.
- Make speech, mood, confidence, tool events, and verdicts drive one coherent face state.
- Mine existing visual clusters and route them into one shared emotion/topology system.
## New modules
`web/public/face3d_engine.js` adds a standalone engine namespace at `window.MasterFace3D` and exports the same API as an ES module.
`web/public/face3d_renderer.js` adds a `Face3DCanvasRenderer` adapter that can render the engine snapshot back through a retro low-resolution phosphor/dither canvas path.
`web/public/face3d_preview.js` is an optional boot module. It only runs when the page URL includes `?face3d=1`, so it can be used as a safe preview path before replacing the live `face.js` renderer.
`web/public/cluster_miner.js` listens to `master:visual`, `master:codebase`, and `master:rule_event`, groups them into semantic clusters, emits `master:clusters`, and can feed `Face3DPreview.engine.setEmotion(...)` when the preview is active.
`data/visual_clusters.yml` is the canonical registry for current and proposed visual/cognition clusters.
The modules are intentionally additive. They do not replace `face.js` yet.
## Mined clusters
### Face Particle Body
Files:
- `web/public/face.js`
- `web/public/face3d_engine.js`
- `web/public/face3d_renderer.js`
- `web/public/face3d_preview.js`
Purpose: embody the assistant as a semantic particle face. The current renderer supplies the retro soul; Face3D supplies normalized topology, blendshapes, typed arrays, and preview rendering.
### Cognition Ecology
Files:
- `web/public/cognition_ecology.js`
Purpose: render runtime cognition as terrain, weather, trails, memories, and agent spirits.
### Codebase Topology
Files:
- `web/public/codebase.js`
- `data/architectures.yml`
Purpose: render modules as particle clusters. Violations agitate clusters; fixes settle them. This corresponds to Architecture #15.
### Runtime Visual Bridge
Files:
- `web/public/visual_bridge.js`
Purpose: normalize runtime events into `master:visual` state: mode, topology, entropy, confidence, provider.
### Speech and Audio Body
Files:
- `lib/voice/speech.rb`
- `bin/tts-worker`
- `web/public/face.js`
Purpose: convert streamed response text into speech, analyse decoded audio, and reshape the mouth during playback.
### Repo Ecology
Files:
- `docs/repo_ecology.md`
- `data/visual_clusters.yml`
Purpose: promote the repository from a bag of files into semantic topology, dependency ecology, cognitive geography, architectural history, and organizational memory.
## New proposed clusters
### Cluster Miner
Runtime/browser cluster miner that groups live visual events into reusable cluster states with heat, confidence, and evidence.
### Evidence Graph
Tracks why a file or event belongs to a cluster: imports, runtime event coupling, shared vocabulary, shared data files, changed-together history, or explicit docs.
### Emotion Bus
Converts visual entropy/confidence/mode/provider and cluster heat into a single shared emotion vector:
```js
{
arousal,
valence,
focus,
confidence,
fatigue
}Canonical registry for papua-mask, serpent, neural, torus, sphere, codebase, and future terrain/body forms.
Ranks safe migration steps by touched files, blast radius, rollback plan, and confidence.
Masks should produce anchors in normalized face space:
x: left to right, roughly-1..1y: forehead to chin, roughly-1..1z: back to forward, roughly-1..1zone: semantic anatomical regionu: stable local coordinate within the zone
This lets the same topology scale to any viewport and makes real 3D pose projection simpler.
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.
Mood, speech, confidence, and state events should write to blendshape values:
blinksquintbrowInnerUpbrowDownsmilefrownjawOpenmouthWidemouthRoundpupilDilatenostrilFlarecheekRaiseshockchibi
Particle targets are produced by applying the blendshape rig to topology anchors.
High-level events should update one emotion vector:
arousalvalencefocusconfidencefatigue
Blendshapes are derived from this vector, so the face feels continuous rather than event-random.
The renderer consumes the engine snapshot:
{
count,
x,
y,
depth,
brightness,
zone
}It accumulates particles into a low-resolution float buffer, applies phosphor decay, runs Atkinson or Bayer dithering, tints pixels by semantic zone, and blits the result to the face canvas.
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.
- Load
face3d_engine.js,face3d_renderer.js,face3d_preview.js, andcluster_miner.jsnext toface.js. - Verify the preview with
?face3d=1. - Use
MASTERClusterMiner.snapshot()to inspect mined runtime clusters. - Route
master:clustersinto Face3D emotion state. - Use
MasterFace3D.VisemeDriverfor duration-based lipsync while keeping the existing renderer. - Replace direct mouth zone mutation with blendshape-driven mouth targets.
- Convert existing mask builders to normalized anchors one mask at a time.
- Move particle storage from objects to typed arrays.
- Add spatial hash repulsion for high-density zones.
- Add an optional WebGL renderer while preserving the retro canvas renderer as default.
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.
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.
# 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# 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.# 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# 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 optimizationThe 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
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
Semantic clustering for:
- duplicated modules
- overlapping utilities
- convergent abstractions
- namespace fragmentation
Outputs:
- merge proposals
- bounded-context candidates
- architectural convergence maps
Repository-wide graph of:
- imports
- runtime references
- constants
- events
- pipelines
- CLI commands
- file mutations
Enables:
- safe rename
- scoped rollback
- dependency visualization
- blast-radius estimation
Goals:
- semantic consistency
- grammar normalization
- namespace coherence
- ambiguity reduction
Must support:
- import rewriting
- test rewriting
- rollback journal
- migration plans
Combines:
- semantic similarity
- dependency overlap
- runtime coupling
- shared terminology
Produces:
- merge simulations
- API compatibility warnings
- cohesion scoring
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.
Runtime repository state visualization:
- architectural turbulence
- dependency storms
- duplication hotspots
- entropy fog
- stable regions
- dead zones
Integrated with cognition ecology.
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
- blind deletes
- global rewrites without simulation
- unsafe renames
- non-scoped rollback
- silent merges
- touched-path tracking
- rollback journal
- diff simulation
- syntax validation
- test execution
- governance approval
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
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.
# frozen_string_literal: true
require "fileutils"
module Master
# Single source of truth for container wiring. Each `boot_*` method assembles
# one subsystem from primitive dependencies. `build` runs them in order.
module Builder
MUTATING_TOOLS = %w[write_file str_replace ast_edit].freeze
RING_SIZE = 1000
SNAPSHOT_MAX_BYTES = 50_000
SNAPSHOT_DIRS = %w[bin lib data].freeze
TOOL_MAP = {
"ReadFile" => ->(r, i) { Reach::ReadFile.new(root: r, undo: i[:undo], event_bus: i[:bus]) },
"WriteFile" => ->(r, i) { Reach::WriteFile.new(root: r, undo: i[:undo], governor: i[:governor], event_bus: i[:bus], diff_stager: i[:diff_stager]) },
"StrReplace" => ->(r, i) { Reach::StrReplace.new(root: r, undo: i[:undo], governor: i[:governor], event_bus: i[:bus], diff_stager: i[:diff_stager]) },
"BatchReplace" => ->(r, i) { Reach::BatchReplace.new(root: r, governor: i[:governor], event_bus: i[:bus]) },
"AstEdit" => ->(r, i) { Reach::AstEdit.new(root: r, undo: i[:undo], governor: i[:governor], event_bus: i[:bus]) },
"Tree" => ->(r, i) { Reach::Tree.new(root: r, event_bus: i[:bus]) },
"ListDir" => ->(r, i) { Reach::ListDir.new(root: r, event_bus: i[:bus]) },
"SearchFiles" => ->(r, i) { Reach::SearchFiles.new(root: r, event_bus: i[:bus]) },
"SearchKnowledge" => ->(r, i) { Reach::SearchKnowledge.new(root: r, event_bus: i[:bus]) },
"SymbolLookup" => ->(r, i) { Reach::SymbolLookup.new(code_index: i[:code_index], event_bus: i[:bus]) },
"Shell" => ->(r, i) { Reach::Shell.new(root: r, governor: i[:governor], event_bus: i[:bus]) },
"GitContext" => ->(r, i) { Reach::GitContext.new(root: r, event_bus: i[:bus]) },
"WebFetch" => ->(r, i) { Reach::WebFetch.new(governor: i[:governor], event_bus: i[:bus]) },
"WebSearch" => ->(r, i) { Reach::WebSearch.new(governor: i[:governor], event_bus: i[:bus]) },
"Clean" => ->(r, i) { Reach::Clean.new(root: r, governor: i[:governor], event_bus: i[:bus]) },
"FeedbackRecord" => ->(r, i) { Reach::FeedbackRecord.new(learnings: i[:learnings]) },
"MemoryRecord" => ->(r, i) { Reach::MemoryRecord.new(memory: i[:memory], root: r, event_bus: i[:bus]) }
}.freeze
module_function
def build(root: Dir.pwd)
Master.configure_providers!
infra = build_infrastructure(root)
ai = build_ai(root, infra)
pipeline, gateway = build_pipeline(root, infra, ai)
infra.merge(ai).merge(pipeline:, gateway:, root:)
end
# No LLM, no autonomous loop, no network. CI / MASTER_SCAN_ONLY=1.
def build_scan_only(root: Dir.pwd)
config = Ground::Config.new(root)
boot_config = config.freeze_boot
trace = boot_trace(root:, config:)
bus = trace[:bus]
code_index = Judge::CodeIndex.new(root:, event_bus: bus)
scanner = build_scanner(root:, bus:)
trace.merge(config:, boot_config:, code_index:, scanner:, root:)
end
def build_infrastructure(root)
config = Ground::Config.new(root)
config["model"] ||= Master.default_model
boot_config = config.freeze_boot
trace = boot_trace(root:, config:)
loop_c = boot_loop(root:, config:, bus: trace[:bus])
reach = boot_reach(root:, config:, bus: trace[:bus])
ground = boot_ground(root:, config:, homeostat: loop_c[:homeostat])
bus = trace[:bus]
renderer = Voice::Renderer.new(config:)
code_index = Judge::CodeIndex.new(root:, event_bus: bus)
code_index.build_async
bus.subscribe("tool:after") do |ev|
next unless ev[:path] && MUTATING_TOOLS.include?(ev[:tool].to_s)
code_index.reindex(ev[:path])
end
diag = Trace::Diag.new(homeostat: loop_c[:homeostat], breaker: reach[:breaker], logging: trace[:logging])
pressure = PressureEngine.new(event_bus: bus)
bus.subscribe("*") do |ev|
event_name = ev[:event] || ev["event"] || ev[:type] || ev["type"] || "event"
next if event_name.to_s.start_with?("pressure:")
pressure.ingest(event: event_name, payload: ev)
rescue StandardError => e
Ground::Swallow.log(e, context: "builder.pressure_engine", event_bus: bus)
end
{ config:, boot_config:, renderer:, code_index:, diag:, pressure: }
.merge(trace).merge(loop_c).merge(reach).merge(ground)
end
def boot_trace(root:, config:)
event_log = Trace::EventLog.new(root:)
bus = Trace::EventBus.new(event_log:)
ring = Trace::RingBuffer.new(RING_SIZE)
logging = Trace::Logging.new(ring_buffer: ring, event_bus: bus)
session = Trace::Session.new(root:, budget_max: config.budget_max, req_max: config.req_max)
undo = Trace::Undo.new(session:, event_bus: bus, root:)
metrics = Trace::Metrics.new(root:, event_bus: bus)
Trace::AuditLog.new(root:, event_bus: bus)
Trace::SwallowLedger.new(event_bus: bus, root:).attach
recorder = Trace::Recorder.new(root:, event_bus: bus)
{ event_log:, bus:, ring:, logging:, session:, undo:, metrics:, trace: recorder }
end
def boot_loop(root:, config:, bus:)
homeostat = Loop::Homeostat.new(event_bus: bus)
governor = Loop::Governor.new(config:, event_bus: bus)
diff_stager = config["staging_enabled"] ? Loop::DiffStager.new(root:, event_bus: bus) : nil
phase_gates = Ground::PhaseGates.new(root:, event_bus: bus)
{ homeostat:, governor:, diff_stager:, phase_gates: }
end
def boot_reach(root:, config:, bus:)
breaker = Reach::CircuitBreakerRegistry.new(budget_max: config.budget_max, req_max: config.req_max, event_bus: bus)
cache = Reach::SemanticCache.new(root:, ttl: config["cache_ttl"], event_bus: bus)
mcp = Reach::McpCoordinator.new(root:, event_bus: bus)
mcp.connect_all
{ breaker:, cache:, mcp: }
end
def boot_ground(root:, config:, homeostat:)
memory = Ground::Memory.new(root:)
personality = Voice::Personality.new(config["persona"]&.to_sym || Voice::Personality::DEFAULT, root:, homeostat:)
learnings = Ground::KnowledgeStore.new(root:)
{ memory:, personality:, learnings: }
end
def build_ai(root, infra)
bus = infra[:bus]
tools = build_tools(root:, infra:) + infra[:mcp].tools
deps = Judge::Agent::Dependencies.from_kwargs(
config: infra[:config], session: infra[:session], tools:,
circuit_breaker: infra[:breaker], cache: infra[:cache], event_bus: bus,
model_router: Now::Routing::ModelRouter.new(config: infra[:config]),
reasoning_modes: Judge::Modes.new,
memory: infra[:memory], personality: infra[:personality],
code_index: infra[:code_index], homeostat: infra[:homeostat]
)
agent = Judge::Agent.new(deps:)
soul_doc = Voice::Soul.new(root:, agent:)
tools << Reach::AskLlm.new(agent:, governor: infra[:governor], circuit_breaker: infra[:breaker], cache: infra[:cache], event_bus: bus)
ctx = Now::ContextWindow.new(session: infra[:session], agent:, model_context: Master::CTX_WINDOW_SIZE)
ctx.check_and_compact!
agent.wire_context_window(ctx)
agent.wire_constitution(Ground::Constitution.new)
scanner = build_scanner(root:, agent:, bus:)
swarm = Judge::Swarm::Coordinator.new(agent:, event_bus: bus)
personas = Judge::Council::Personas.load(File.join(Master::ROOT, "data", "council.yml"))
axioms = Ground::Rules.new(root:)
deliberation = Judge::Council::Deliberation.new(personas:, agent:, event_bus: bus, axioms:)
ideation = Judge::Council::Ideation.new(agent:, event_bus: bus)
council_stage = Now::Stages::Council.new(deliberation:, config: infra[:config], event_bus: bus)
guard = Judge::Security::InjectionGuard.new
autonomous = boot_autonomous(root:, infra:, agent:, scanner:, axioms:)
.merge(learnings: infra[:learnings], skills: boot_skills(root, bus))
autonomous[:standing].wire_container(scanner:, agent:, root:, bus:)
{ agent:, soul: soul_doc, scanner:, swarm:, deliberation:, council_stage:, ideation:, guard: }.merge(autonomous)
end
def build_scanner(root:, agent: nil, bus: nil)
Judge::Scan::RuleDSL
scanner = Judge::Scan::Scanner.new(event_bus: bus)
Judge::Scan::Rule.registry.select(&:auto_build?).each { |k| scanner.add_rule(k.new) }
scanner.add_rule(Judge::Scan::Rules::RuleCoverageRule.new(root:))
scanner.add_rule(Judge::Scan::Rules::RubocopRule.new(root:))
scanner.add_rule(Judge::Scan::Rules::ReekRule.new(root:))
scanner.add_rule(Judge::Scan::Rules::InterconnectRule.new(root:))
scanner.add_rule(Judge::Scan::Rules::SemanticRule.new(agent:))
scanner.add_rule(Judge::Scan::Rules::AdversarialRule.new(agent:))
scanner.add_rule(Judge::Scan::Rules::CommentDriftRule.new(agent:))
scanner.add_rule(Judge::Scan::Rules::AstOmissionRule.new(root:))
scanner
end
def boot_autonomous(root:, infra:, agent:, scanner:, axioms: nil)
bus = infra[:bus]
standing = Ground::StandingOrders.new(pipeline: nil, event_bus: bus)
git = Reach::GitOperations.new(root)
rules = scanner.instance_variable_get(:@rules)
learnings = infra[:learnings]
# In-process FixLoop autofix on boot is gated to MASTER_AUTOFIX=1. Off by
# default — the loop's autocommits race deploys and over-aggressively
# rewrites framework boilerplate. Run /fix manually or set the env var on
# hosts where unattended convergence is wanted.
fix_loop = Loop::FixLoop.new(rules:, axioms:, agent:, scanner:, root:, bus:, git:, learnings:)
if ENV["MASTER_AUTOFIX"] == "1"
Thread.new { fix_loop.run_forever(root) }.tap { |t| t.abort_on_exception = false }
end
# Architecture #7: reactive file-watcher instead of polling.
# Activate with MASTER_WATCH=1 on VPS (requires rb-kqueue or rb-inotify).
watch_loop = if ENV["MASTER_WATCH"] == "1"
wl = Loop::WatchLoop.new(rules:, agent:, scanner:, root:, bus:, learnings:)
Thread.new { wl.run }.tap { |t| t.abort_on_exception = false }
wl
end
heartbeat = Loop::Heartbeat.new(root:, agent:, scanner:, memory: infra[:memory], event_bus: bus, homeostat: infra[:homeostat])
triggers = Trace::Triggers.new(event_bus: bus, scanner:, agent:)
triggers.install_defaults!
propose_tree = Loop::ProposeTree.new(root:, agent:, event_bus: bus)
bus.subscribe("fix_loop:clean") { Thread.new { propose_tree.call } }
bus.subscribe("fix_loop:plateau") { Thread.new { propose_tree.call } }
# Architecture: continuous OpenBSD load watcher.
# Default on; MASTER_WATCHER=0 disables. Sampler is read-only.
watcher = Loop::Watcher.new(bus:, root:)
if ENV["MASTER_WATCHER"] != "0"
Thread.new { watcher.run_forever }.tap { |t| t.abort_on_exception = false }
end
bus.subscribe("system:crit") { Thread.new { fix_loop.stop_background! if fix_loop.background_alive? } }
{ standing:, fix_loop:, watch_loop:, heartbeat:, triggers:, propose_tree:, watcher: }
end
def boot_skills(root, bus)
skills = Now::Skills.new(root:, event_bus: bus)
skills.discover!
skills
end
def build_tools(root:, infra:)
path = File.join(root, "data", "tools.yml")
defs = Master.load_yaml(path)
return [] unless defs.is_a?(Array)
defs.filter_map do |defn|
next unless defn["default"] == true
factory = TOOL_MAP[defn["name"].to_s]
factory ? factory.call(root, infra) : (infra[:bus]&.publish("builder:tool_skipped", tool: defn["name"]); nil)
end
end
def build_pipeline(root, infra, ai)
config = infra[:config]
bus = infra[:bus]
commands = Now::CommandRegistry.build(infra:, ai:, root:)
stages = [
Now::Stages::Intake.new,
Now::Stages::Enhance.new(agent: ai[:agent], event_bus: bus),
Now::Stages::Infer.new,
Now::Stages::Route.new(commands:, agent: ai[:agent]),
Now::Stages::Guard.new(governor: infra[:governor], injection_guard: ai[:guard]),
Now::Stages::Deliberate.new(agent: ai[:agent], config:),
Now::Stages::Execute.new,
Now::Pipeline::SkipOnPressure.new(
Now::Stages::Review.new(council: ai[:council_stage], scanner: ai[:scanner], config:, root:, event_bus: bus),
bus:
),
Now::Stages::Memory.new(memory: infra[:memory], event_bus: bus),
Now::Stages::Render.new(renderer: infra[:renderer])
]
pipeline = Now::Pipeline.new(stages, bus:, trace: config["trace_pipeline"] == true, root:)
ai[:standing].wire_pipeline(pipeline)
gateway = Reach::Gateway.new(pipeline:, session: infra[:session], event_bus: bus)
commands["gateway"] = ->(_ctx) { gateway.channels }
[pipeline, gateway]
end
def boot_snapshot(container)
root = container[:root]
files = Dir[*SNAPSHOT_DIRS.map { |d| File.join(root, d, "**", "*") }]
.select { |f| File.file?(f) && File.size(f) < SNAPSHOT_MAX_BYTES }
.reject { |f| f.include?("/knowledge/") || f.include?("/vendor/") }
.sort
body = files.flat_map do |f|
rel = f.delete_prefix("#{root}/")
lang = Master::FILE_LANGUAGE_MAP.fetch(File.extname(f).downcase, "text")
src = File.read(f, encoding: "UTF-8", invalid: :replace)
["## #{rel}", "```#{lang}", src.rstrip, "```", ""]
rescue StandardError => e
Ground::Swallow.log(e, context: "builder.snapshot_file", path: f)
[]
end
header = ["# MASTER Snapshot", "Generated: #{Time.now.utc.iso8601}", "Files: #{files.size}", ""]
content = (header + body).join("\n")
out = File.join(root, ".master", "snapshot.md")
FileUtils.mkdir_p(File.dirname(out))
File.write(out, content)
File.write(File.join(root, "snapshot.md"), content)
container[:bus]&.publish("boot:snapshot", files: files.size)
rescue StandardError => e
container[:bus]&.publish("boot:snapshot_error", error: e.message)
end
end
end# frozen_string_literal: true
module Master
module Design
class MobileFirstPwaProfiles
Rule = Data.define(:id, :pattern, :extract, :threshold, :message, :severity)
TOUCH_TARGET_MIN_PX = Master::Ground::Axioms::Wcag::TOUCH_TARGET_AAA_PX
BODY_FONT_MIN_PX = Master::Ground::Axioms::Wcag::BODY_FONT_MIN_PX
LINE_HEIGHT_MIN = Master::Ground::Axioms::Wcag::LINE_HEIGHT_MIN
LINE_LENGTH_MIN_CH = 45
LINE_LENGTH_MAX_CH = 75
CSS_RULES = [
Rule.new(
id: :font_size_too_small,
pattern: /(?:^|[{\s;])font-size:\s*(\d+)px/,
extract: ->(m) { m[1].to_i },
threshold: BODY_FONT_MIN_PX,
message: "font-size %dpx below #{BODY_FONT_MIN_PX}px baseline",
severity: :high
),
Rule.new(
id: :line_height_too_low,
pattern: /line-height:\s*([\d.]+)(?!px|em|rem)/,
extract: ->(m) { m[1].to_f },
threshold: LINE_HEIGHT_MIN,
message: "line-height %.1f below #{LINE_HEIGHT_MIN} — readability baseline",
severity: :medium
),
Rule.new(
id: :touch_target_too_small,
pattern: /min-height:\s*(\d+)px/,
extract: ->(m) { m[1].to_i },
threshold: TOUCH_TARGET_MIN_PX,
message: "min-height %dpx below #{TOUCH_TARGET_MIN_PX}px WCAG touch target (2.5.8)",
severity: :high
)
].freeze
PATTERN_RULES = [
Rule.new(
id: :animation_no_reduced_motion,
pattern: /(?:animation|transition)\s*:/,
extract: nil,
threshold: nil,
message: "animation/transition without @media (prefers-reduced-motion: reduce) guard",
severity: :medium
),
Rule.new(
id: :raw_primary_color,
pattern: /#(?:ff0000|00ff00|0000ff|ffff00|ff00ff|00ffff)\b/i,
extract: nil,
threshold: nil,
message: "raw primary color — use shadow/midtone/highlight graded triplets",
severity: :low
),
Rule.new(
id: :linear_timing,
pattern: /transition:[^;]*\blinear\b/,
extract: nil,
threshold: nil,
message: "linear timing function — prefer ease-out or cubic-bezier for perceived smoothness",
severity: :low
)
].freeze
HTML_CHECKS = {
landmarks: { pattern: /<main|<nav\b|<header\b|<footer\b/, message: "no landmark elements — add <main>, <nav>, <header>, <footer>", severity: :high },
focus_ring: { pattern: /focus-visible|:focus\b/, message: "no focus-visible styles found", severity: :high },
form_labels: { pattern: /<label\b/, message: "form found but no <label> elements", severity: :medium }
}.freeze
def audit(app_path)
css_findings = audit_css(app_path)
html_findings = audit_html(app_path)
all = css_findings + html_findings
{ violations: all, severity_counts: all.group_by { |v| v[:severity] }.transform_values(&:count) }
end
def recommendations(audit_result)
Array(audit_result[:violations])
.sort_by { |v| %i[high medium low].index(v[:severity]) || 9 }
.map { |v| heuristic_prefix(v) + "[#{v[:severity].upcase}] #{v[:id]}: #{v[:message]}" }
end
private
HEURISTIC_MAP = {
font_size_too_small: :h8_minimalism,
line_height_too_low: :h8_minimalism,
touch_target_too_small: :h6_recognition,
animation_no_reduced_motion: :h8_minimalism,
raw_primary_color: :h8_minimalism,
linear_timing: :h8_minimalism,
landmarks: :h4_consistency,
focus_ring: :h6_recognition,
form_labels: :h5_error_prevention
}.freeze
def heuristic_prefix(violation)
key = violation.is_a?(Hash) ? violation[:id]&.to_sym : nil
h_key = HEURISTIC_MAP[key]
return "" unless h_key
"[Nielsen ##{Master::Ground::Axioms::UxHeuristics.number(h_key)}] "
end
def audit_css(path)
css_files = Dir.glob(File.join(path, "**", "*.{css,scss}"))
.reject { |f| f.include?("node_modules") || f.include?("vendor") }
findings = []
css_files.each do |file|
source = begin
File.read(file)
rescue StandardError
next
end
has_reduced_motion = source.match?(/prefers-reduced-motion/)
CSS_RULES.each do |rule|
source.scan(rule.pattern) do |m|
value = rule.extract&.call(m)
next if value && value >= rule.threshold
msg = value ? format(rule.message, value) : rule.message
findings << { id: rule.id, file:, message: msg, severity: rule.severity }
end
end
PATTERN_RULES.each do |rule|
next unless source.match?(rule.pattern)
next if rule.id == :animation_no_reduced_motion && has_reduced_motion
findings << { id: rule.id, file:, message: rule.message, severity: rule.severity }
end
end
findings
end
def audit_html(path)
erb_files = Dir.glob(File.join(path, "**", "*.html.{erb,haml}"))
.reject { |f| f.include?("vendor") }
return [{ id: :no_html, file: path, message: "no HTML/ERB files found", severity: :medium }] if erb_files.empty?
combined = erb_files.first(40).map { |f| File.read(f) rescue "" }.join("\n")
findings = []
HTML_CHECKS.each do |check_id, spec|
has_form = combined.match?(/<form\b/)
next if check_id == :form_labels && !has_form
next if combined.match?(spec[:pattern])
findings << { id: check_id, file: "(views)", message: spec[:message], severity: spec[:severity] }
end
findings
end
end
end
end# frozen_string_literal: true
module Master
module Design
module PlatformProfiles
PROFILES = {
brutal_minimal: {
philosophy: "content-first, invisible design, delete anything that does not improve readability",
layout: %w[single_column semantic_html system_fonts black_white max_65ch],
avoid: %w[shadows glows gradients rounded_corners decorative_borders hero_banners loading_theatrics custom_fonts],
metrics: { max_total_kb: 10, contrast: 4.5, line_min_ch: 45, line_max_ch: 75 }
},
medium: {
philosophy: "long-form reading comfort through generous side whitespace and typographic rhythm",
layout: %w[article_column max_65ch large_line_height restrained_navigation],
avoid: %w[visual_noise dense_sidebars auto_play],
metrics: { line_height: 1.58, max_width_px: 680 }
},
substack: {
philosophy: "newsletter-first trust, direct author voice, low-friction subscription",
layout: %w[publication_header readable_feed email_capture simple_article],
avoid: %w[overdesigned_cards hidden_authors distracting_motion],
metrics: { cta_count: 1, line_max_ch: 75 }
},
new_yorker: {
philosophy: "editorial authority, strong type hierarchy, content as cultural object",
layout: %w[serif_editorial strong_headlines generous_margins disciplined_grid],
avoid: %w[cheap_gradients excessive_chrome noisy_cards],
metrics: { contrast: 4.5, font_families_max: 2 }
},
x: {
philosophy: "dense real-time feed optimized for scanning and interaction velocity",
layout: %w[feed timeline compact_actions sticky_navigation],
avoid: %w[slow_animation large_cards_hidden_content],
metrics: { action_target_px: 44 }
},
tiktok: {
philosophy: "full-screen media-first flow with minimal chrome and immediate feedback",
layout: %w[full_screen_media vertical_flow gesture_first],
avoid: %w[text_heavy_chrome slow_load blocking_modals],
metrics: { first_frame_ms: 500 }
}
}.freeze
module_function
def fetch(name)
PROFILES.fetch(name.to_sym)
end
def brief(name)
p = fetch(name)
"#{name}: #{p[:philosophy]}; layout=#{p[:layout].join(', ')}; avoid=#{p[:avoid].join(', ')}; metrics=#{p[:metrics]}"
end
def constraints(name)
profile = fetch(name)
[
"philosophy: #{profile[:philosophy]}",
"layout: #{profile[:layout].join(', ')}",
"avoid: #{profile[:avoid].join(', ')}",
"metrics: #{profile[:metrics]}"
]
end
def choose(text)
source = text.to_s.downcase
return :brutal_minimal if source.match?(/brutal|minimal|motherfucking|content-first|black.?white/)
return :medium if source.include?("medium") || source.match?(/long.?form|reading/)
return :substack if source.include?("substack") || source.include?("newsletter")
return :new_yorker if source.include?("new yorker") || source.include?("editorial")
return :tiktok if source.include?("tiktok") || source.include?("short video")
return :x if source.match?(/\bx\b|twitter|feed/)
:brutal_minimal
end
end
end
end# 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# 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# frozen_string_literal: true
module Master
module Ground
class AttentionContext
VALID_ZOOMS = %w[wide narrow wide_to_deep deep_to_wide deep].freeze
VALID_ACTS = %w[scout mine repair land verify rollback checkpoint review].freeze
attr_reader :map, :zoom, :act, :target, :parent
COMPLEX_ACTS = %w[mine repair rollback checkpoint verify].freeze
def initialize(map: nil, zoom: "wide", act: "scout", target: [], parent: [])
@map = map.to_s
@zoom = VALID_ZOOMS.include?(zoom.to_s) ? zoom.to_s : "wide"
@act = VALID_ACTS.include?(act.to_s) ? act.to_s : "scout"
@target = Array(target).map(&:to_s)
@parent = Array(parent).map(&:to_s)
end
def complex?
COMPLEX_ACTS.include?(@act)
end
def to_s
parts = ["map: #{@map}"].tap do |p|
p << "zoom: #{@zoom}" if @zoom != "wide"
p << "act: #{@act}" if @act != "scout"
end
"⟦#{parts.join(" | ")}⟧"
end
def to_h
{ map: @map, zoom: @zoom, act: @act, target: @target, parent: @parent }
end
def self.from_yaml(path)
return new unless File.exist?(path)
data = Master.load_yaml(path) || {}
new(
map: data["map"],
zoom: data["zoom"] || "wide",
act: data["act"] || "scout",
target: data["target"] || [],
parent: data["parent"] || []
)
end
end
end
end# frozen_string_literal: true
module Master
module Ground
module Axioms
module RailsDoctrine
# The nine pillars — rubyonrails.org/doctrine (DHH)
# Cite these when justifying architectural decisions, not just Rails apps.
PILLARS = {
happiness: "Optimize for programmer happiness",
convention: "Convention over Configuration",
omakase: "The menu is omakase",
no_one_paradigm: "No one paradigm",
beautiful_code: "Exalt beautiful code",
sharp_knives: "Provide sharp knives",
integrated: "Value integrated systems",
progress: "Progress over stability",
big_tent: "Push up a big tent"
}.freeze
# Solid Trifecta — database-backed adapters; eliminates Redis/PaaS dependency.
# Doctrine basis: :integrated — "Value integrated systems"
SOLID_TRIFECTA = %w[solid_queue solid_cache solid_cable].freeze
def self.cite(pillar, rationale)
name = PILLARS.fetch(pillar) { pillar.to_s }
"[Rails Doctrine — #{name}] #{rationale}"
end
end
end
end
end# frozen_string_literal: true
module Master
module Ground
module Axioms
module UxHeuristics
# Nielsen's 10 Usability Heuristics — nngroup.com/articles/ten-usability-heuristics/
# Universal: apply to CLI output, REPL prompts, web UI, API error messages, and prose alike.
HEURISTICS = {
h1_visibility: "Visibility of System Status — keep users informed through appropriate feedback within a reasonable time",
h2_real_world: "Match Between the System and the Real World — speak the user's language, not internal jargon",
h3_user_control: "User Control and Freedom — provide a clearly marked exit from unwanted states",
h4_consistency: "Consistency and Standards — follow platform and industry conventions",
h5_error_prevention: "Error Prevention — prevent problems from occurring rather than relying on error messages",
h6_recognition: "Recognition Rather than Recall — make elements, actions, and options visible",
h7_flexibility: "Flexibility and Efficiency of Use — support both novice and expert users",
h8_minimalism: "Aesthetic and Minimalist Design — every extra unit competes with relevant units",
h9_error_recovery: "Help Users Recognize, Diagnose, and Recover from Errors — plain language, precise problem, constructive solution",
h10_help: "Help and Documentation — documentation should help users complete tasks, not explain bad design"
}.freeze
# Medium adapters — same heuristics applied to specific surfaces
SIGNALS = {
web: {
h1_visibility: { checks: %w[loading-indicator turbo:frame-missing offline-fallback], failing: "No feedback during navigation or offline state" },
h3_user_control: { checks: %w[undo back-navigation escape-modal cancel], failing: "No exit from modals or destructive actions" },
h4_consistency: { checks: %w[shared-layout stimulus-conventions semantic-html], failing: "Component behavior diverges from shared baseline" },
h5_error_prevention: { checks: %w[form-label aria-required input-type], failing: "Form fields lack <label> or aria-required" },
h6_recognition: { checks: %w[nav-visible primary-action icon-labels], failing: "Primary actions hidden or icon-only without labels" },
h8_minimalism: { checks: %w[information-density whitespace raw-primaries animation], failing: "Visual noise: raw colors, unguarded animations, dense layout" },
h9_error_recovery: { checks: %w[flash error-format turbo-stream-error], failing: "Error messages generic or missing recovery path" }
},
cli: {
h1_visibility: { checks: %w[progress spinner result-line], failing: "No output while operation is running — user cannot tell if system is working" },
h2_real_world: { checks: %w[plain-language no-jargon], failing: "Output uses internal symbol names, not human-readable descriptions" },
h8_minimalism: { checks: %w[no-filler terse single-line], failing: "Output contains filler phrases or multi-line where one line suffices" },
h9_error_recovery: { checks: %w[actionable-error suggestion], failing: "Error output does not suggest a corrective action" }
}
}.freeze
def self.cite(heuristic_key, violation, medium: :web)
h = HEURISTICS.fetch(heuristic_key, heuristic_key.to_s)
num = heuristic_key.to_s[/\d+/]
"[Nielsen ##{num} — #{h.split(' — ').first}] #{violation}"
end
def self.number(heuristic_key)
heuristic_key.to_s[/\d+/].to_i
end
end
end
end
end# frozen_string_literal: true
module Master
module Ground
module Axioms
module Wcag
# WCAG 2.x success criteria relevant to web/mobile/CLI output
# Reference: w3.org/WAI/WCAG21/quickref/
# Universal: apply to any rendered surface, not just visual UIs.
Criterion = Data.define(:id, :level, :name, :requirement)
CRITERIA = [
Criterion.new(id: "1.4.3", level: :AA, name: "Contrast (Minimum)", requirement: "Text contrast >= 4.5:1 (normal), 3:1 (large text)"),
Criterion.new(id: "1.4.4", level: :AA, name: "Resize Text", requirement: "Text resizable to 200% without loss of content or function"),
Criterion.new(id: "1.4.10", level: :AA, name: "Reflow", requirement: "Content reflows at 320px width without horizontal scrolling"),
Criterion.new(id: "1.4.11", level: :AA, name: "Non-text Contrast", requirement: "UI component contrast >= 3:1 against adjacent colors"),
Criterion.new(id: "1.4.12", level: :AA, name: "Text Spacing", requirement: "No loss of content when letter/word/line spacing is increased"),
Criterion.new(id: "1.4.13", level: :AA, name: "Content on Hover or Focus", requirement: "Hover/focus content dismissible, hoverable, persistent"),
Criterion.new(id: "2.1.1", level: :A, name: "Keyboard", requirement: "All functionality operable via keyboard"),
Criterion.new(id: "2.4.7", level: :AA, name: "Focus Visible", requirement: "Keyboard focus indicator is visible"),
Criterion.new(id: "2.5.3", level: :A, name: "Label in Name", requirement: "Visible label text is part of the accessible name"),
Criterion.new(id: "2.5.8", level: :AA, name: "Target Size (Minimum)", requirement: "Touch target >= 24x24 CSS px (AA); 44x44 CSS px recommended (AAA)"),
Criterion.new(id: "3.3.1", level: :A, name: "Error Identification", requirement: "Input errors identified in text and described to the user"),
Criterion.new(id: "3.3.2", level: :A, name: "Labels or Instructions", requirement: "Labels or instructions provided for user input"),
Criterion.new(id: "1.3.6", level: :AAA, name: "Identify Purpose", requirement: "UI components, icons, regions identified programmatically")
].freeze
# Numeric thresholds extracted for use in audit rules
TOUCH_TARGET_AA_PX = 24
TOUCH_TARGET_AAA_PX = 44
CONTRAST_NORMAL = 4.5
CONTRAST_LARGE = 3.0
REFLOW_WIDTH_PX = 320
BODY_FONT_MIN_PX = 16 # baseline for WCAG 1.4.4 reflow + readability
LINE_HEIGHT_MIN = 1.5 # WCAG 1.4.12 text spacing baseline
def self.cite(criterion_id, violation)
c = CRITERIA.find { |cr| cr.id == criterion_id }
label = c ? "WCAG #{c.id} #{c.level} — #{c.name}" : "WCAG #{criterion_id}"
"[#{label}] #{violation}"
end
def self.find(criterion_id)
CRITERIA.find { |c| c.id == criterion_id }
end
end
end
end
end# frozen_string_literal: true
module Master
module Ground
class BrainOverlay
CORE_FILES = %w[
standing_orders.rb
workflow_policy.rb
evidence_base.rb
tool_protocol.rb
subagent_policy.rb
].freeze
CONTEXTUAL_FILES = %w[
policy_classifier.rb
patch_verifier.rb
done_checker.rb
memory_search.rb
context_compactor.rb
].freeze
DEFAULT_MARKDOWN_DIRS = [
File.join(Master::ROOT, "data", "claude"),
File.join(Master::ROOT, ".master", "memory")
].freeze
attr_reader :root
def initialize(root: Master::ROOT, markdown_dirs: DEFAULT_MARKDOWN_DIRS)
@root = root
@markdown_dirs = markdown_dirs
end
def core_brief
lines = ["Brain overlay: Ruby policy is authoritative; markdown is legacy/contextual unless explicitly requested."]
lines << Master::Ground::ToolProtocol.brief if defined?(Master::Ground::ToolProtocol)
lines << Master::Ground::EvidenceBase.brief if defined?(Master::Ground::EvidenceBase)
lines << Master::Ground::WorkflowPolicy.brief if defined?(Master::Ground::WorkflowPolicy)
lines.compact.join("\n\n")
end
def contextual_index
markdown_files.map do |path|
rel = relative(path)
title = File.basename(path)
"#{rel} — #{title}"
end
end
def load_context(name_or_pattern)
pattern = name_or_pattern.to_s
match = markdown_files.find { |path| relative(path).include?(pattern) || File.basename(path).include?(pattern) }
return nil unless match
File.read(match, encoding: "utf-8")
end
def reloadable?
true
end
private
def markdown_files
@markdown_dirs.flat_map { |dir| Dir.glob(File.join(dir, "**", "*.md")) }.select { |path| File.file?(path) }
end
def relative(path)
path.sub(%r{\A#{Regexp.escape(@root)}/?}, "")
end
end
end
end# frozen_string_literal: true
module Master
module Ground
module BrutalistMinimalism
NAME = "Brutalist-Minimalist Content-First".freeze
PRIMARY_RULE = "If it does not improve readability, delete it.".freeze
FORBIDDEN_EFFECTS = %w[
shadows glows 3d_effects gradients animations transitions rounded_corners decorative_borders
].freeze
FORBIDDEN_CSS = %w[
box-shadow border-radius background-image transform filter
].freeze
ALLOWED_CSS = %w[
font-size font-weight margin padding line-height max-width display flex grid
].freeze
SYSTEM_FONTS = {
serif: "Georgia",
sans_serif: "Arial/Helvetica"
}.freeze
METRICS = {
css_bytes_max: 10_000,
total_bytes_max: 10_000,
line_length_min: 45,
line_length_max: 75,
line_length_ideal: 65,
contrast_min: 4.5,
load_time_2g_s: 2,
lighthouse_target: 100
}.freeze
HTML_REQUIREMENTS = [
"proper heading hierarchy",
"meaningful alt text",
"logical document flow",
"accessible landmarks: main, nav, aside where appropriate",
"screen-reader compatibility",
"no styling-only divs",
"no non-semantic markup",
"no decorative elements"
].freeze
ANTI_PATTERNS = [
"visual metaphors",
"smooth interactions",
"aesthetic comfort",
"decorative containers",
"loading animations",
"splash screens",
"hero banners"
].freeze
PROMPTS = {
design_brief: "Create a brutally minimal website that prioritizes content over decoration.",
development_constraint: "Use system fonts, single column layout, max 65 characters per line, no visual effects.",
content_hierarchy: "Establish hierarchy through font size, weight, semantic headings, and spacing only.",
performance_mandate: "HTML plus CSS under 10KB total; every byte must serve content delivery."
}.freeze
def self.brief
<<~TEXT.strip
#{NAME} policy:
- #{PRIMARY_RULE}
- black text on white background; no palette beyond black and white unless required for state.
- system fonts only: serif=#{SYSTEM_FONTS[:serif]}, sans=#{SYSTEM_FONTS[:sans_serif]}; no web fonts.
- single-column, content-first layout; 45-75ch lines, ideal #{METRICS[:line_length_ideal]}ch.
- forbidden visual effects: #{FORBIDDEN_EFFECTS.join(', ')}.
- forbidden CSS properties: #{FORBIDDEN_CSS.join(', ')}.
- HTML must be semantic, accessible, heading-ordered, and screen-reader friendly.
- JavaScript is forbidden unless critical; external resources are forbidden unless essential.
- target: sub-#{METRICS[:load_time_2g_s]}s on 2G, HTML+CSS under #{METRICS[:total_bytes_max]} bytes, Lighthouse 100/100/100/100.
TEXT
end
end
end
end# frozen_string_literal: true
require "fileutils"
require "json"
require "time"
module Master
module Ground
class Checkpoint
attr_reader :root, :dir
def initialize(root: Master::ROOT, dir: File.join(Master::ROOT, ".master", "checkpoints"))
@root = root
@dir = dir
end
def create(label:, files: [])
id = "#{Time.now.utc.strftime('%Y%m%d%H%M%S')}-#{slug(label)}"
target = File.join(dir, id)
FileUtils.mkdir_p(target)
Array(files).each do |rel|
src = File.join(root, rel.to_s)
next unless File.file?(src)
dst = File.join(target, rel.to_s)
FileUtils.mkdir_p(File.dirname(dst))
FileUtils.cp(src, dst)
end
manifest = { id: id, label: label, files: Array(files), created_at: Time.now.utc.iso8601 }
File.write(File.join(target, "manifest.json"), JSON.pretty_generate(manifest))
manifest
end
def list
Dir.glob(File.join(dir, "*", "manifest.json")).filter_map do |path|
JSON.parse(File.read(path, encoding: "utf-8"))
rescue JSON::ParserError
nil
end.sort_by { |row| row.fetch("created_at", "") }
end
def restore(id)
manifest_path = File.join(dir, id.to_s, "manifest.json")
raise ArgumentError, "checkpoint not found: #{id}" unless File.file?(manifest_path)
manifest = JSON.parse(File.read(manifest_path, encoding: "utf-8"))
Array(manifest["files"]).each do |rel|
src = File.join(dir, id.to_s, rel.to_s)
next unless File.file?(src)
dst = File.join(root, rel.to_s)
FileUtils.mkdir_p(File.dirname(dst))
FileUtils.cp(src, dst)
end
manifest
end
private
def slug(text)
text.to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|\-\z/, "")[0, 48]
end
end
end
end# frozen_string_literal: true
require "yaml"
require "fileutils"
module Master
module Ground
class Config
BUDGET_MAX_DEFAULT = 10.0
HISTORY_MAX = 500
DEFAULT_WEB_PORT = 53_187
DEFAULTS = {
"model" => "claude-opus-4-7",
"web_host" => "127.0.0.1",
"web_public_url" => "https://ai.brgen.no",
"web_port" => DEFAULT_WEB_PORT,
"budget_max" => BUDGET_MAX_DEFAULT,
"req_max" => 60.0,
"trace" => 0,
"prescan" => true,
"auto" => false,
"cache_ttl" => 3_600,
"history_max" => 500,
"reasoning_mode" => "direct",
"task_type" => "code_generation",
"auto_testing" => false
}.freeze
def initialize(root = Dir.pwd)
@root = root
@path = File.join(root, ".master", "config.yml")
@mutex = Mutex.new
@data = load_config
end
def [](key) = @mutex.synchronize { @data[key.to_s] }
def []=(key, value); @mutex.synchronize { @data[key.to_s] = value }; end
def dig(key, *rest) = @mutex.synchronize { k = key.to_s; rest.empty? ? @data[k] : @data.dig(k, *rest) }
def model = self["model"]
def budget_max = self["budget_max"].to_f
def req_max = self["req_max"].to_f
def trace = (ENV["MASTER_TRACE"] || self["trace"]).to_i
def prescan? = self["prescan"] == true
def auto? = self["auto"] == true
def reasoning_mode = self["reasoning_mode"].to_s
def task_type = self["task_type"].to_s
def auto_testing? = self["auto_testing"] == true
include AtomicWrite
def save!
FileUtils.mkdir_p(File.dirname(@path))
write_atomic(@path, @data.to_yaml, fsync: true)
end
def reload!
@mutex.synchronize { @data = load_config }
end
# Frozen snapshot of read-only boot values — safe to share across threads.
BootConfig = Data.define(:root, :model, :web_host, :web_port, :web_public_url,
:budget_max, :req_max, :cache_ttl, :history_max)
def freeze_boot
snap = @mutex.synchronize { @data.dup }
BootConfig.new(
root: @root, model: snap["model"], web_host: snap["web_host"], web_port: snap["web_port"].to_i,
web_public_url: snap["web_public_url"], budget_max: snap["budget_max"].to_f,
req_max: snap["req_max"].to_f, cache_ttl: snap["cache_ttl"].to_i, history_max: snap["history_max"].to_i
).freeze
end
def to_h = @mutex.synchronize { deep_dup(@data) }
private
def load_config
defaults = deep_dup(DEFAULTS)
return defaults unless File.exist?(@path)
raw = Master.load_yaml(@path)
loaded = raw.is_a?(Hash) ? raw : {}
deep_merge(defaults, stringify_keys(loaded))
rescue Psych::Exception => e
warn "config: failed to parse #{@path}: #{e.message}"
defaults
end
def deep_merge(base, overlay)
base.merge(overlay) do |_key, old_val, new_val|
old_val.is_a?(Hash) && new_val.is_a?(Hash) ? deep_merge(old_val, new_val) : new_val
end
end
def stringify_keys(hash)
hash.each_with_object({}) do |(k, v), h|
h[k.to_s] = v.is_a?(Hash) ? stringify_keys(v) : v
end
end
def deep_dup(obj)
case obj
when Hash then obj.each_with_object({}) { |(k, v), h| h[k] = deep_dup(v) }
when Array then obj.map { |v| deep_dup(v) }
when Numeric, Symbol, TrueClass, FalseClass, NilClass then obj
else
obj.respond_to?(:dup) ? (obj.dup rescue obj) : obj
end
end
end
end
end# frozen_string_literal: true
module Master
module Ground
class Constitution
DIR = File.join(Master::ROOT, "data", "principles").freeze
MAX_PRINCIPLES = 40
MAX_BODY_CHARS = 320
def initialize(dir: DIR)
@dir = dir
@principles = load
end
def empty? = @principles.empty?
def system_prompt
return nil if @principles.empty?
lines = @principles.map { |p| "- #{p[:name]}: #{p[:body]}" }
"Constitutional principles (operator-declared, override defaults):\n#{lines.join("\n")}"
end
def list
@principles.map { |p| "#{p[:type]}: #{p[:name]} — #{p[:description]}" }
end
def reload!
@principles = load
self
end
private
def load
return [] unless File.directory?(@dir)
Dir.glob(File.join(@dir, "*.md")).sort.filter_map { |f| parse(f) }.first(MAX_PRINCIPLES)
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "constitution.load", dir: @dir)
[]
end
def parse(path)
fm = Master::Ground::Frontmatter.parse_file(path)
return nil if fm[:meta].empty?
meta = fm[:meta]
{
name: meta["name"].to_s,
description: meta["description"].to_s,
type: meta["type"].to_s,
body: fm[:body][0, MAX_BODY_CHARS]
}
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "constitution.parse", path:)
nil
end
end
end
end# frozen_string_literal: true
module Master
module Ground
class ContextProvider
PROVIDERS = %i[repo_map memory_search brain_overlay current_files rails_pwa_files].freeze
RAILS_PWA_QUERY_TERMS = %w[rails pwa manifest service_worker hotwire turbo stimulus mobile audit].freeze
def initialize(root: Master::ROOT)
@root = root
end
def gather(query:, providers: PROVIDERS, limit: 20)
providers.flat_map { |p| dispatch_provider(p, query, limit) }.compact
end
def dispatch_provider(provider, query, limit)
case provider.to_sym
when :repo_map then repo_map(query, limit)
when :memory_search then memory_search(query, limit)
when :brain_overlay then brain_overlay
when :current_files then current_files(query, limit)
when :rails_pwa_files then rails_pwa_files(query, limit)
else []
end
end
def brief(query, limit: 10)
gather(query: query, limit: limit).map { |row| "#{row[:source]}: #{row[:text]}" }
end
private
def repo_map(query, limit)
return [] unless defined?(Master::Ground::RepoMap)
Master::Ground::RepoMap.new(root: @root).relevant(query, limit: limit).map do |entry|
{ source: :repo_map, path: entry.path, text: "#{entry.path} #{entry.language} #{entry.bytes}B" }
end
end
def memory_search(query, limit)
return [] unless defined?(Master::Ground::MemorySearch)
Master::Ground::MemorySearch.new.search(query, limit: limit).map do |doc|
{ source: :memory, path: doc["path"], text: "#{doc['path']} #{doc['title']} score=#{format('%.2f', doc['score'])}" }
end
rescue StandardError
[]
end
def brain_overlay
return [] unless defined?(Master::Ground::BrainOverlay)
overlay = Master::Ground::BrainOverlay.new(root: @root)
[{ source: :brain_overlay, path: nil, text: overlay.core_brief[0, 800] }]
rescue StandardError
[]
end
def rails_pwa_files(query, limit)
terms = query.to_s.downcase.scan(/[a-z0-9_]+/)
return [] unless (terms & RAILS_PWA_QUERY_TERMS).any?
deploy_rails = File.expand_path("../../DEPLOY/rails", @root)
return [] unless Dir.exist?(deploy_rails)
patterns = %w[
app/views/pwa/manifest.json.erb
app/views/pwa/service-worker.js
app/javascript/application.js
config/routes.rb
config/importmap.rb
]
apps = Dir.entries(deploy_rails).reject { |e| e.start_with?(".", "_") }
rows = apps.flat_map { |app| pwa_rows_for_app(app, deploy_rails, patterns) }
rows.first(limit)
end
def pwa_rows_for_app(app, deploy_rails, patterns)
patterns.filter_map do |rel|
path = File.join(deploy_rails, app, rel)
next unless File.exist?(path)
{ source: :rails_pwa, path: "DEPLOY/rails/#{app}/#{rel}", text: "#{app}/#{rel}" }
end
end
def current_files(query, limit)
terms = query.to_s.downcase.scan(/[a-z0-9_\-]+/)
return [] if terms.empty?
paths = Dir.glob(File.join(@root, "lib", "**", "*.rb")).select { |p| matches_terms?(p, terms) }
paths.first(limit).map { |path| current_files_row(path) }
end
def matches_terms?(path, terms)
rel = path.sub(%r{\A#{Regexp.escape(@root)}/?}, "")
terms.any? { |term| rel.downcase.include?(term) }
end
def current_files_row(path)
rel = path.sub(%r{\A#{Regexp.escape(@root)}/?}, "")
{ source: :current_files, path: rel, text: rel }
end
end
end
end# frozen_string_literal: true
module Master
module Ground
class DoneChecker
REQUIRED_KEYS = %i[files symbols callers].freeze
def initialize(root: Master::ROOT, verifier: PatchVerifier.new(root: root))
@root = root
@verifier = verifier
end
def call(plan)
normalized = normalize(plan)
checks = @verifier.verify(
files: normalized.fetch(:files),
symbols: normalized.fetch(:symbols),
references: normalized.fetch(:callers)
)
{
done: @verifier.ok?(checks),
checks: checks,
report: @verifier.report(checks),
missing_plan_keys: REQUIRED_KEYS - normalized.keys
}
end
def self.done?(plan, root: Master::ROOT)
new(root: root).call(plan).fetch(:done)
end
private
def normalize(plan)
hash = plan.respond_to?(:to_h) ? plan.to_h : {}
{
files: Array(hash[:files] || hash["files"]),
symbols: Array(hash[:symbols] || hash["symbols"]),
callers: hash[:callers] || hash["callers"] || {}
}
end
end
end
end# 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# frozen_string_literal: true
require "yaml"
module Master
module Ground
# YAML frontmatter parser for markdown files.
# Returns {meta: Hash, body: String}; empty meta when no frontmatter or malformed.
module Frontmatter
MARKER = "---"
RE = /\A---\n(.*?)\n---\n?(.*)/m
module_function
def parse(raw)
s = raw.to_s
return { meta: {}, body: s.strip } unless s.start_with?(MARKER)
m = s.match(RE)
return { meta: {}, body: s.strip } unless m
meta = begin; YAML.safe_load(m[1]) || {}; rescue Psych::Exception; {}; end
{ meta: meta, body: m[2].strip }
end
def parse_file(path)
parse(File.read(path, encoding: "UTF-8"))
end
end
end
end# 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# frozen_string_literal: true
require "sqlite3"
require "json"
module Master
module Ground
# Unified knowledge ledger — consolidates fix quality (arch #10), strategy
# outcomes, and RSI feedback events into one WAL-mode SQLite database.
#
# Replaces the split Ground::Learnings (JSONL) + Persistence::SqliteLearnings (SQLite).
# Single-object `learnings:` kwarg serves the RuleLoop caller path:
# record(trigger:, strategy:, outcome:) → strategy_outcomes table
# record(rule:, file_type:, outcome:) → fix_outcomes table
class KnowledgeStore
include Master::Ground::Persistence::SqliteStore
DEFAULT_PATH = ".master/knowledge.sqlite3"
QUALITY_WINDOW_DAYS = 30
RSI_WINDOW_DAYS = 7
RSI_FAIL_THRESHOLD = 0.20
RSI_CORRECTION_MIN = 3
RSI_PROVIDER_MIN = 3
def initialize(root:)
@db = open_db(root)
@db.results_as_hash = true
ensure_schema
end
# Unified dispatch — keyword args determine which table is written.
def record(trigger: nil, strategy: nil, rule: nil, file_type: nil, outcome:)
if rule
record_fix(rule: rule, file_type: file_type, outcome: outcome)
elsif trigger
record_strategy(trigger: trigger, strategy: strategy || "unknown", outcome: outcome)
end
end
# Fix quality tracking (arch #10) — outcome: :fixed | :stuck
def record_fix(rule:, file_type:, outcome:)
@db.execute(
"INSERT INTO fix_outcomes (ts, rule, file_type, outcome) VALUES (?, ?, ?, ?)",
[Time.now.to_i, rule.to_s, file_type.to_s, outcome.to_s]
)
rescue SQLite3::Exception => e
warn "knowledge_store: #{e.message}"
end
# Fix quality score 0.0–1.0 for a rule. Default 0.5 when no data.
def fix_quality(rule:, file_type: nil)
cutoff = Time.now.to_i - QUALITY_WINDOW_DAYS * 86_400
rows = if file_type
@db.execute(
"SELECT outcome, COUNT(*) AS n FROM fix_outcomes WHERE rule = ? AND file_type = ? AND ts >= ? GROUP BY outcome",
[rule.to_s, file_type.to_s, cutoff]
)
else
@db.execute(
"SELECT outcome, COUNT(*) AS n FROM fix_outcomes WHERE rule = ? AND ts >= ? GROUP BY outcome",
[rule.to_s, cutoff]
)
end
tally = rows.each_with_object(Hash.new(0)) { |r, h| h[r["outcome"]] = r["n"].to_i }
total = tally.values.sum
return 0.5 if total.zero?
tally["fixed"].to_f / total
end
def top_rules(limit: 20, min_attempts: 3)
cutoff = Time.now.to_i - QUALITY_WINDOW_DAYS * 86_400
@db.execute(<<~SQL, [cutoff, min_attempts, limit])
SELECT rule,
SUM(CASE WHEN outcome = 'fixed' THEN 1 ELSE 0 END) AS fixed,
COUNT(*) AS total,
CAST(SUM(CASE WHEN outcome = 'fixed' THEN 1 ELSE 0 END) AS REAL) / COUNT(*) AS quality
FROM fix_outcomes WHERE ts >= ?
GROUP BY rule HAVING total >= ?
ORDER BY quality DESC LIMIT ?
SQL
end
# Strategy outcome tracking — autoloop fix records
def record_strategy(trigger:, strategy:, outcome:)
ts = Time.now.to_i
existing = @db.execute(
"SELECT id, reuse_count, confidence FROM strategy_outcomes WHERE trigger = ? AND strategy = ?",
[trigger.to_s, strategy.to_s]
).first
if existing
new_confidence = [existing["confidence"].to_f + 0.05, 1.0].min
@db.execute(
"UPDATE strategy_outcomes SET reuse_count = reuse_count + 1, confidence = ?, outcome = ?, ts = ? WHERE id = ?",
[new_confidence, outcome.to_s, ts, existing["id"]]
)
else
confidence = outcome.to_s == "fixed" ? 0.7 : 0.4
@db.execute(
"INSERT INTO strategy_outcomes (ts, trigger, strategy, outcome, confidence, reuse_count) VALUES (?, ?, ?, ?, ?, 0)",
[ts, trigger.to_s, strategy.to_s, outcome.to_s, confidence]
)
end
rescue SQLite3::Exception => e
warn "knowledge_store: #{e.message}"
end
def search(trigger_fragment, limit: 3)
fragment = "%#{trigger_fragment.to_s.downcase}%"
@db.execute(
"SELECT trigger, strategy, outcome, confidence FROM strategy_outcomes WHERE LOWER(trigger) LIKE ? AND outcome != 'failed' ORDER BY confidence DESC LIMIT ?",
[fragment, limit]
)
end
# RSI feedback events — dimensional statistics for opportunity detection
def record_event(event_type:, dimension:, value: nil, metadata: nil)
@db.execute(
"INSERT INTO feedback_events (ts, event_type, dimension, value, metadata) VALUES (?, ?, ?, ?, ?)",
[Time.now.to_i, event_type.to_s, dimension.to_s, value&.to_s, metadata&.to_json]
)
rescue SQLite3::Exception => e
warn "knowledge_store: #{e.message}"
end
def opportunities
cutoff = Time.now.to_i - RSI_WINDOW_DAYS * 86_400
recent = @db.execute("SELECT event_type, dimension FROM feedback_events WHERE ts >= ?", [cutoff])
tool_stats = recent.select { |r| %w[tool_success tool_failure].include?(r["event_type"]) }
.group_by { |r| r["dimension"] }
.filter_map { |tool, evs|
success = evs.count { |e| e["event_type"] == "tool_success" }
failure = evs.count { |e| e["event_type"] == "tool_failure" }
total = success + failure
rate = total.zero? ? 0.0 : failure.to_f / total
{ category: :high_failure, dimension: tool, fail_rate: rate.round(3), total: } if rate >= RSI_FAIL_THRESHOLD && total >= 3
}
corrections = recent.select { |r| r["event_type"] == "user_correction" }
.group_by { |r| r["dimension"] }
.filter_map { |dim, evs|
{ category: :repeated_correction, dimension: dim, count: evs.size } if evs.size >= RSI_CORRECTION_MIN
}
provider_errs = recent.select { |r| r["event_type"] == "provider_error" }
.group_by { |r| r["dimension"] }
.filter_map { |dim, evs|
{ category: :provider_errors, dimension: dim, count: evs.size } if evs.size >= RSI_PROVIDER_MIN
}
tool_stats + corrections + provider_errs
end
def close
@db&.close
end
private
def open_db(root)
open_sqlite(root, DEFAULT_PATH)
end
def ensure_schema
@db.execute_batch(<<~SQL)
CREATE TABLE IF NOT EXISTS fix_outcomes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
rule TEXT NOT NULL,
file_type TEXT NOT NULL,
outcome TEXT NOT NULL CHECK (outcome IN ('fixed', 'stuck'))
);
CREATE INDEX IF NOT EXISTS idx_fix_rule ON fix_outcomes(rule);
CREATE INDEX IF NOT EXISTS idx_fix_ts ON fix_outcomes(ts);
CREATE TABLE IF NOT EXISTS strategy_outcomes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
trigger TEXT NOT NULL,
strategy TEXT NOT NULL,
outcome TEXT NOT NULL,
confidence REAL NOT NULL DEFAULT 0.5,
reuse_count INTEGER NOT NULL DEFAULT 0,
UNIQUE(trigger, strategy)
);
CREATE INDEX IF NOT EXISTS idx_strat_trigger ON strategy_outcomes(trigger);
CREATE TABLE IF NOT EXISTS feedback_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
event_type TEXT NOT NULL,
dimension TEXT NOT NULL,
value TEXT,
metadata TEXT
);
CREATE INDEX IF NOT EXISTS idx_fb_ts ON feedback_events(ts);
CREATE INDEX IF NOT EXISTS idx_fb_dimension ON feedback_events(dimension);
SQL
end
end
end
end# frozen_string_literal: true
require "yaml"
require "fileutils"
module Master
module Ground
# Memory — persistent cross-session store with TF-IDF semantic search.
# Stored at .master/memory.yml. Survives restarts.
class Memory
module Search
def semantic_recall(query, top_n: 3)
store_snap = @mutex.synchronize { @store.dup }
return [] if store_snap.empty?
if Judge::Embeddings.enabled? && (qvec = Judge::Embeddings.embed(query))
hits = vector_recall(qvec, top_n, store_snap)
return hits unless hits.empty?
end
tfidf_recall(query, top_n, store_snap)
end
private
def vector_recall(qvec, top_n, store)
store.filter_map do |key, data|
next unless data.is_a?(Hash) && data["vec"].is_a?(Array)
score = Judge::Embeddings.cosine(qvec, data["vec"])
next if score < Judge::Embeddings::MIN_SIM
{ key: key, value: data["value"].to_s, score: score }
end.sort_by { |e| -e[:score] }.first(top_n)
end
def tfidf_recall(query, top_n, store)
terms = tokenize(query)
return [] if terms.empty?
store.filter_map { |key, data|
value = data.is_a?(Hash) ? data["value"].to_s : data.to_s
score = tfidf_score(terms, tokenize("#{key} #{value}"))
next if score.zero?
{ key: key, value: value, score: score }
}.sort_by { |e| -e[:score] }.first(top_n)
end
def tokenize(text) = text.downcase.scan(/\b[a-z]{2,}\b/)
def tfidf_score(query_terms, doc_terms)
return 0.0 if doc_terms.empty?
freq = doc_terms.tally
query_terms.sum { |t| Math.log(1.0 + freq.fetch(t, 0).to_f) }
end
end
TTL_DAYS = 90
CONSOLIDATE_THRESHOLD = 40
SECONDS_PER_DAY = 86_400
MAX_INJECT_TOKENS = 2000
MAX_INJECT_ENTRIES = 5
TYPES = %w[user feedback project reference general].freeze
AUTO_SAVE_PATTERNS = {
"user" => /\b(?:i'?m a|i am a|my role is|i work as)\s+([^.,;\n]{3,80})/i,
"feedback" => /\b(?:don'?t|stop|never|always|prefer|from now on)\s+([^.,;\n]{3,120})/i,
"project" => /\b(?:we'?re|deadline|launching|deploying|migrating)\s+([^.,;\n]{3,120})/i
}.freeze
include Search
include AtomicWrite
def initialize(root: Dir.pwd)
@root = root
@path = File.join(root, ".master", "memory.yml")
@mutex = Mutex.new
@store = load_store
import_external!
end
def remember(key, value, type: "general")
type = TYPES.include?(type.to_s) ? type.to_s : "general"
@mutex.synchronize do
prune_stale! if @store.size > CONSOLIDATE_THRESHOLD
entry = { "value" => value.to_s, "ts" => Time.now.to_i, "type" => type }
if (vec = Judge::Embeddings.embed("#{key} #{value}"))
entry["vec"] = vec
end
@store[key.to_s] = entry
persist
end
end
def by_type(type)
@mutex.synchronize { @store.select { |k, v| v.is_a?(Hash) && v["type"] == type.to_s && !k.start_with?("archive/") } }
end
def type_counts
@mutex.synchronize do
counts = Hash.new(0)
@store.each do |k, v|
next if k.start_with?("archive/") || k == "_consolidated_summary"
counts[v.is_a?(Hash) ? (v["type"] || "general") : "general"] += 1
end
counts
end
end
def auto_save(text)
return if text.to_s.empty?
AUTO_SAVE_PATTERNS.each do |type, re|
next unless (m = text.match(re))
snippet = m[1].strip
next if snippet.length < 3
n = @mutex.synchronize { @store.keys.count { |k| k.start_with?("auto/#{type}/") } } + 1
key = "auto/#{type}/#{n}"
remember(key, snippet, type: type)
return key
end
nil
end
def recall(key)
@mutex.synchronize { @store.dig(key.to_s, "value") }
end
def forget(key)
@mutex.synchronize { @store.delete(key.to_s); persist }
end
def all = @mutex.synchronize { @store.transform_values { |v| v.is_a?(Hash) ? v["value"] : v } }
def context_summary
active = @mutex.synchronize { @store.reject { |k, _| k.to_s.start_with?("archive/") || k == "_consolidated_summary" } }
return if active.empty?
grouped = active.group_by { |_, v| v.is_a?(Hash) ? (v["type"] || "general") : "general" }
ordered = TYPES.flat_map { |t| (grouped[t] || []).sort_by { |_, v| -(v.is_a?(Hash) ? v["ts"].to_i : 0) } }
.first(MAX_INJECT_ENTRIES * 2)
lines, token_sum, current_type = [], 0, nil
ordered.each do |k, v|
type = v.is_a?(Hash) ? (v["type"] || "general") : "general"
if type != current_type
lines << "[#{type}]"
current_type = type
end
text = "- #{k}: #{v.is_a?(Hash) ? v["value"] : v}"
est = text.bytesize / Master::Trace::Session::TOKENS_PER_CHAR
break if token_sum + est > MAX_INJECT_TOKENS
lines << text
token_sum += est
end
return if lines.empty?
archived_n = @mutex.synchronize { @store.count { |k, _| k.to_s.start_with?("archive/") } }
summary = recall("_consolidated_summary")
header = summary ? "Memory (#{summary.to_s[0, 80]}):" : "Memory:"
header += " [+#{archived_n} archived]" if archived_n > 0
"#{header}\n#{lines.join("\n")}"
end
# Three-phase consolidation: light (score), deep (archive), REM (LLM summary).
def consolidate!(agent: nil)
return "nothing to consolidate" if @store.empty?
now = Time.now.to_i
entries = nil
archived = 0
@mutex.synchronize do
entries = @store.reject { |k, _| k.to_s.start_with?("archive/") }
scored = entries.map do |key, data|
ts = data.is_a?(Hash) ? data["ts"].to_i : 0
age_d = (now - ts) / 86_400.0
{ key: key, score: 1.0 / (1.0 + age_d / TTL_DAYS.to_f) }
end
scored.each do |entry|
next if entry[:key] == "_consolidated_summary"
next unless entry[:score] < 0.33
@store["archive/#{entry[:key]}"] = @store.delete(entry[:key])
archived += 1
end
persist
end
if agent
active_text = @mutex.synchronize do
@store
.reject { |k, _| k.to_s.start_with?("archive/") || k == "_consolidated_summary" }
.map { |k, v| "#{k}: #{v.is_a?(Hash) ? v["value"] : v}" }
.join("\n")
end
unless active_text.strip.empty?
summary = agent.ask_once("Summarize in 2 concise sentences, preserving all key facts:\n#{active_text}")
remember("_consolidated_summary", summary.strip)
end
end
"dreaming: #{entries.size} entries checked, #{archived} archived"
rescue StandardError => e
"consolidation error: #{e.message}"
end
private
# Imports markdown memory files from data/claude/ on first boot.
# Each file's frontmatter type maps to MASTER's memory type; body becomes the value.
def import_external!
dir = File.join(@root, "data", "claude")
return unless Dir.exist?(dir)
Dir.glob(File.join(dir, "*.md")).each do |path|
next if File.basename(path) == "MEMORY.md"
key = "claude/#{File.basename(path, ".md")}"
next if @store.key?(key)
type, body = parse_frontmatter(path)
next if body.empty?
remember(key, body, type: type)
end
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "memory.preload_claude_md")
end
def parse_frontmatter(path)
fm = Master::Ground::Frontmatter.parse_file(path)
type = fm[:meta]["type"].to_s
type = "general" if type.empty?
[type, fm[:body]]
end
def prune_stale!
cutoff = Time.now.to_i - TTL_DAYS * SECONDS_PER_DAY
@store.each do |k, v|
next if k.to_s.start_with?("archive/") || k == "_consolidated_summary"
ts = v.is_a?(Hash) ? v["ts"].to_i : 0
next unless ts > 0 && ts < cutoff
@store["archive/#{k}"] = @store.delete(k)
end
end
def load_store
return {} unless File.exist?(@path)
loaded = Master.load_yaml(@path)
loaded.is_a?(Hash) ? loaded : {}
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "memory.load_store", path: @path)
{}
end
def persist
FileUtils.mkdir_p(File.dirname(@path))
write_atomic(@path, @store.to_yaml)
end
end
end
end# 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# frozen_string_literal: true
module Master
module Ground
class MemorySearch
def initialize(index: MemoryIndex.new)
@index = index
end
def search(query, limit: 8)
docs = @index.load_index
docs = @index.rebuild! if docs.empty?
terms = query.to_s.downcase.scan(/[a-z0-9_\-]{3,}/)
return [] if terms.empty?
scored = docs.values.filter_map { |doc| score_or_skip(doc, terms) }
scored.sort_by { |doc| -doc["score"] }.first(limit)
end
def score_or_skip(doc, terms)
score = score_doc(doc, terms)
score > 0 ? doc.merge("score" => score) : nil
end
def brief(query, limit: 5)
rows = search(query, limit: limit)
return "Memory search: no hits for #{query.inspect}." if rows.empty?
"Memory search hits:\n" + rows.map { |doc| "- #{doc['path']} score=#{format('%.2f', doc['score'])} title=#{doc['title']}" }.join("\n")
end
private
def score_doc(doc, terms)
counts = doc.fetch("terms", {})
terms.sum { |term| Math.log(1 + counts.fetch(term, 0).to_f) }
end
end
end
end# frozen_string_literal: true
module Master
module Ground
class OrchestrationPolicy
MODEL_TIERS = {
cheap: %i[low],
fast: %i[low medium],
strong: %i[high critical],
local: %i[low medium],
browser_local: %i[low]
}.freeze
COUNCIL_TIERS = %i[high critical].freeze
# Persona → task-domain mapping. Council reviews the domain, not the answer.
COUNCIL_ROLES = {
"Security" => %i[auth secrets tool_execution permission_changes],
"Reliability" => %i[network provider runtime fallback],
"Maintainer" => %i[code_mutation refactor file_deletion],
"Architect" => %i[system_shape migration design],
"User Advocate" => %i[ui mobile accessibility],
"Accessibility" => %i[ui mobile accessibility],
"Music Producer" => %i[sonic visual_rhythm pacing],
"Hip-Hop Producer" => %i[sonic visual_rhythm pacing]
}.freeze
# Required output sections for high/critical risk responses.
EVIDENCE_CONTRACT = %i[observed_facts inferred_plan uncertainty rollback_path verification_path].freeze
def initialize(router: IntentRouter.new, registry: nil)
@router = router
@registry = registry
end
def evaluate(text)
route = @router.route(text)
intent = route[:intent]
risk = route[:risk]
model = select_model(risk)
council = COUNCIL_TIERS.include?(risk)
{
intent: intent,
risk: risk,
model_tier: model,
use_council: council,
council_roles: council ? roles_for(intent) : [],
evidence_req: council,
evidence_fields: council ? EVIDENCE_CONTRACT : []
}
end
def model_for(risk)
select_model(risk)
end
def roles_for(intent)
domain = intent_domain(intent)
COUNCIL_ROLES.filter_map { |persona, domains| persona if domains.include?(domain) }
end
private
def select_model(risk)
case risk
when :low then :cheap
when :medium then :fast
when :high, :critical then :strong
else :fast
end
end
def intent_domain(intent)
case intent
when :wire_existing_module, :verify_patch_landed, :write_repo_changes then :code_mutation
when :delete_redundant_config then :file_deletion
when :run_ui_review then :ui
when :run_sound_review then :sonic
when :codify_policy, :refactor_to_ruby then :refactor
else :general
end
end
end
end
end# 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# frozen_string_literal: true
require "open3"
module Master
module Ground
module Orders
class Autocommit < Base
def call
repo = File.expand_path(File.join(root, ".."))
out, _, status = Open3.capture3("git", "-C", repo, "status", "--porcelain")
return Result.ok(skipped: true) unless status.success? && !out.strip.empty?
Open3.capture2e("git", "-C", repo, "add", "-A")
msg = "auto: standing-order commit (#{out.lines.size} file(s))"
_, st = Open3.capture2e("git", "-C", repo, "commit", "-m", msg)
return Result.err("commit failed") unless st.success?
push_out, push_st = Open3.capture2e("git", "-C", repo, "push")
if push_st.success?
bus&.publish("autocommit:pushed", files: out.lines.size)
Result.ok(committed: true, pushed: true)
else
bus&.publish("autocommit:push_failed", error: push_out.strip[0, 200])
Result.ok(committed: true, pushed: false, push_error: push_out.strip)
end
rescue StandardError => e
Result.err(e.message)
end
end
end
end
end# 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# 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# 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# 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# 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# 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# 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# 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# 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# frozen_string_literal: true
module Master
module Ground
PHASES = %w[discover analyze ideate design implement validate deliver idle].freeze
class PhaseGates
include AtomicWrite
PHASE_STATE_PATH = "data/phase_state.yml".freeze
GATES = {
"discover" => %w[problem_stated success_measurable],
"analyze" => %w[components_distinct dependencies_noted],
"ideate" => %w[alternatives_gte_3],
"design" => %w[interfaces_noted errors_noted],
"implement" => %w[],
"validate" => %w[tests_noted],
"deliver" => %w[deployed_noted],
"idle" => %w[]
}.freeze
def initialize(root:, event_bus: nil)
@root = root
@bus = event_bus
@state = load_state
end
def current = @state["phase"] || "idle"
def advance!(to: nil)
prev = current
target = to&.to_s || next_phase
return Master::Result.err("unknown phase: #{target}") unless PHASES.include?(target)
return Master::Result.err("already at final phase: #{prev}") if prev == "idle" && target == "idle"
unmet = unmet_gates(prev)
if unmet.any?
return Master::Result.err(
"phase #{prev} gates unmet: #{unmet.join(",")} — override with /phase advance --force"
)
end
@state["phase"] = target
@state["entered_at"] = Time.now.to_i
persist
@bus&.publish("phase:advanced", from: prev, to: target)
Master::Result.ok("phase: #{prev} -> #{target}")
end
def force!(phase)
@state["phase"] = phase.to_s
@state["entered_at"] = Time.now.to_i
persist
Master::Result.ok("phase forced to #{phase}")
end
def meet_gate!(gate)
@state["met_gates"] ||= []
@state["met_gates"] |= [gate.to_s]
persist
end
def status
unmet = unmet_gates(current)
met = (@state["met_gates"] || []) & (GATES[current] || [])
"phase=#{current} met=#{met.join(",")} unmet=#{unmet.join(",")}"
end
private
def next_phase
idx = PHASES.index(current) || 0
PHASES[[idx + 1, PHASES.size - 1].min]
end
def unmet_gates(phase)
required = GATES.fetch(phase, [])
(required - (@state["met_gates"] || []))
end
def load_state
path = File.join(@root, PHASE_STATE_PATH)
return { "phase" => "idle", "met_gates" => [] } unless File.exist?(path)
data = Master.load_yaml(path)
data.is_a?(Hash) ? data : { "phase" => "idle", "met_gates" => [] }
rescue StandardError => _e
{ "phase" => "idle", "met_gates" => [] }
end
def persist
path = File.join(@root, PHASE_STATE_PATH)
FileUtils.mkdir_p(File.dirname(path))
write_atomic(path, @state.to_yaml)
end
end
end
end# 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# 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# 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# 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# 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# frozen_string_literal: true
module Master
module Ground
# RuntimeRegistry — single source of truth for provider selection.
# Issue #396 item 2: collapse duplicate orchestration registries.
#
# Combines ProviderRegistry (static capability map), ProviderHealth
# (score telemetry), and ProviderQuarantineManager (availability) into
# one call site. Callers ask choose() and get a live-aware selection.
class RuntimeRegistry
def initialize(
health: Now::Routing::ProviderHealth.new,
quarantine: Now::Routing::ProviderQuarantineManager.new,
event_bus: nil
)
@health = health
@quarantine = quarantine
@bus = event_bus
end
# Return best provider for a task domain, respecting quarantine + scores.
# Returns { provider: Symbol, model: String, score: Float, quarantined: [] }
def choose(task: :coding)
candidates = ProviderRegistry.available
available = candidates.reject { |name, _| @quarantine.quarantined?(name) }
pool = available.any? ? available : candidates # fall back to full set if all quarantined
scored = pool.map { |name, cfg| [name, cfg, @health.score(name)] }
.sort_by { |_, _, score| -score }
# Prefer task-strength match over raw score
match = scored.find { |_, cfg, _| cfg[:strengths].include?(task.to_sym) }
name, cfg, score = match || scored.first || [:local, ProviderRegistry::PROVIDERS[:local], 0.5]
quarantined_list = candidates.keys.select { |n| @quarantine.quarantined?(n) }
@bus&.publish("runtime:provider_chosen", provider: name, score:, task:)
{ provider: name, model: cfg[:default_model], score:, quarantined: quarantined_list }
end
def status
ProviderRegistry.available.map do |name, cfg|
{
provider: name,
model: cfg[:default_model],
score: @health.score(name),
quarantined: @quarantine.quarantined?(name),
strengths: cfg[:strengths]
}
end
end
end
end
end# frozen_string_literal: true
module Master
module Ground
module SandboxPolicy
DENY_PATTERNS = [
/\brm\s+-rf\s+(?:\/|~|\$HOME)\b/,
/\bsudo\b/,
/\bmkfs\b/,
/\bdd\s+if=/,
/\bchmod\s+-R\s+777\b/,
/\bchown\s+-R\b/,
/\bforce-push\b|\bgit\s+push\s+--force/,
/\b(drop|truncate)\s+(database|table)\b/i,
/\bshutdown\b|\breboot\b/,
/\bcurl\b.*\|\s*(?:sh|bash|zsh)/
].freeze
ASK_PATTERNS = [
/\bgit\s+push\b/,
/\bgit\s+reset\s+--hard\b/,
/\bgit\s+clean\s+-fd/,
/\bbundle\s+exec\s+rails\s+db:/,
/\bdelete\b/i,
/\bdeploy\b/i
].freeze
ALLOW_PATTERNS = [
/\Agit\s+(status|diff|log|show|branch)\b/,
/\A(?:bundle\s+exec\s+)?ruby\s+-c\b/,
/\A(?:bundle\s+exec\s+)?rspec\b/,
/\A(?:bundle\s+exec\s+)?rubocop\b/,
/\A(?:bundle\s+exec\s+)?rails\s+test\b/,
/\Als\b|\Afind\b|\Agrep\b|\Arg\b/
].freeze
Decision = Struct.new(:mode, :reason, keyword_init: true) do
def allow? = mode == :allow
def ask? = mode == :ask
def deny? = mode == :deny
end
module_function
def decide(command)
source = command.to_s.strip
return Decision.new(mode: :deny, reason: "empty command") if source.empty?
return Decision.new(mode: :deny, reason: "matched deny pattern") if DENY_PATTERNS.any? { |pattern| source.match?(pattern) }
return Decision.new(mode: :ask, reason: "matched ask pattern") if ASK_PATTERNS.any? { |pattern| source.match?(pattern) }
return Decision.new(mode: :allow, reason: "matched safe allow pattern") if ALLOW_PATTERNS.any? { |pattern| source.match?(pattern) }
Decision.new(mode: :ask, reason: "unknown command risk")
end
def allowed?(command)
decide(command).allow?
end
def brief
"Sandbox policy: deny destructive/system commands, ask for pushes/deploy/db/reset, allow read-only git and test/lint commands."
end
end
end
end# 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# 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# frozen_string_literal: true
module Master
module Ground
# Central helper for intentional rescue-and-continue sites.
# Keeps tolerated failures visible, greppable, and event-bus observable.
module Swallow
module_function
def log(error, context:, event_bus: nil, **metadata)
payload = {
context: context.to_s,
error_class: error.class.name,
error: error.message.to_s,
backtrace: Array(error.backtrace).first(5)
}.merge(metadata)
if event_bus
event_bus.publish("swallow:error", payload)
else
warn("swallow:error #{payload.inspect}")
end
nil
rescue StandardError => e
# Last resort: the logger itself failed. Cannot use the bus or recurse
# into Swallow — write straight to stderr so the failure is not lost.
Kernel.warn("swallow:meta_error #{e.class}: #{e.message}")
nil
end
end
end
end# frozen_string_literal: true
module Master
module Ground
module ToolApprovalPolicy
MODES = %i[plan ask act auto].freeze
MODE_RULES = {
plan: { writes: false, commands: :read_only, description: "read-only planning" },
ask: { writes: :confirm, commands: :confirm_risky, description: "ask before mutation or risky command" },
act: { writes: true, commands: :sandboxed, description: "act with sandbox policy" },
auto: { writes: true, commands: :sandboxed, description: "safe autonomous execution with deny gates" }
}.freeze
module_function
def normalize(mode)
key = mode.to_s.downcase.to_sym
MODES.include?(key) ? key : :ask
end
def allow_write?(mode, path: nil)
rule = MODE_RULES.fetch(normalize(mode)).fetch(:writes)
return true if rule == true
return false if rule == false
:confirm
end
def command_decision(mode, command)
case MODE_RULES.fetch(normalize(mode)).fetch(:commands)
when :read_only
SandboxPolicy.decide(command).allow? && command.to_s.match?(/\A(?:git\s+(status|diff|log|show)|ls|find|grep|rg)\b/) ? :allow : :deny
when :confirm_risky
decision = SandboxPolicy.decide(command)
decision.allow? ? :allow : (decision.deny? ? :deny : :ask)
else
decision = SandboxPolicy.decide(command)
decision.mode
end
end
def brief(mode = :ask)
rule = MODE_RULES.fetch(normalize(mode))
"Tool approval mode=#{normalize(mode)}: #{rule[:description]}."
end
end
end
end# frozen_string_literal: true
module Master
module Ground
# ToolContract — typed contract for every tool execution.
# Issue #396 item 7: route all tool execution through typed contracts.
#
# Contracts define allowed inputs, expected output shape, permission tier,
# retry policy, timeout, and side-effect classification. The validator
# enforces these at the call site before any tool runs.
module ToolContract
Contract = Data.define(
:name, # Symbol — tool identifier
:inputs, # Hash { key => :required | :optional }
:output_shape, # Array of expected output keys (or :any)
:permission, # :read | :write | :exec | :network
:timeout_s, # Integer — hard timeout
:max_retries, # Integer
:side_effects, # Array of Symbols — :filesystem | :network | :git | :process | :none
:category # :reach | :judge | :trace | :ground
)
REGISTRY = {
web_fetch: Contract.new(
name: :web_fetch, inputs: { url: :required, headers: :optional },
output_shape: [:body], permission: :network, timeout_s: 30, max_retries: 2,
side_effects: [:network], category: :reach
),
file_read: Contract.new(
name: :file_read, inputs: { path: :required },
output_shape: [:content], permission: :read, timeout_s: 5, max_retries: 0,
side_effects: [:none], category: :reach
),
file_write: Contract.new(
name: :file_write, inputs: { path: :required, content: :required },
output_shape: [:written], permission: :write, timeout_s: 10, max_retries: 0,
side_effects: [:filesystem], category: :reach
),
shell_exec: Contract.new(
name: :shell_exec, inputs: { command: :required, cwd: :optional },
output_shape: [:stdout, :stderr, :exit_code], permission: :exec, timeout_s: 60,
max_retries: 0, side_effects: [:process, :filesystem], category: :reach
),
git_op: Contract.new(
name: :git_op, inputs: { op: :required, args: :optional },
output_shape: [:output], permission: :exec, timeout_s: 30, max_retries: 1,
side_effects: [:git, :filesystem], category: :reach
),
llm_call: Contract.new(
name: :llm_call, inputs: { prompt: :required, model: :optional, system: :optional },
output_shape: [:text, :tokens], permission: :network, timeout_s: 120, max_retries: 2,
side_effects: [:network], category: :ground
),
scan: Contract.new(
name: :scan, inputs: { path: :required, depth: :optional },
output_shape: [:findings], permission: :read, timeout_s: 60, max_retries: 0,
side_effects: [:none], category: :judge
),
postpro: Contract.new(
name: :postpro, inputs: { args: :optional },
output_shape: [:stdout, :stderr, :exit_code], permission: :exec, timeout_s: 600,
max_retries: 0, side_effects: [:process, :filesystem], category: :reach
),
repligen: Contract.new(
name: :repligen, inputs: { args: :optional },
output_shape: [:stdout, :stderr, :exit_code], permission: :network, timeout_s: 900,
max_retries: 0, side_effects: [:network, :process, :filesystem], category: :reach
)
}.freeze
module_function
def find(name)
REGISTRY[name.to_sym]
end
# Validate args against contract before execution.
# Returns Result::Ok(contract) or Result::Err with violation details.
# For shell_exec and git_op, runs injection_guard on command/args.
def validate(name, args = {})
contract = find(name)
return Result.err("unknown tool: #{name}", category: :validation) unless contract
missing = contract.inputs.select { |k, req| req == :required && !args.key?(k) }.keys
return Result.err("#{name}: missing required inputs: #{missing.join(', ')}", category: :validation) if missing.any?
if %i[shell_exec git_op].include?(name.to_sym)
shell_input = args[:command] || Array(args[:args]).join(" ")
guard = Master::Judge::Security::InjectionGuard.new
check = guard.safe?(shell_input.to_s)
return Result.err("#{name}: injection detected in input", category: :policy) unless check
end
Result.ok(contract)
end
def permitted_for?(name, permission_level)
contract = find(name)
return false unless contract
permission_rank(contract.permission) <= permission_rank(permission_level)
end
def permission_rank(p)
{ read: 0, write: 1, network: 2, exec: 3 }.fetch(p.to_sym, 9)
end
end
end
end# frozen_string_literal: true
module Master
module Ground
module ToolProtocol
CLAIM_WORDS = /\b(read|fetched|opened|searched|ran|executed|wrote|created|updated|deleted|patched|committed|verified)\b/i.freeze
COMMAND_BLOCK = /```(?:bash|sh|zsh|python|ruby|powershell|shell)\b/i.freeze
REQUIREMENTS = [
"Never claim a command, file read, web fetch, or repo write happened unless a tool result proves it.",
"Prefer action over narration when a tool can do the work.",
"Do not output shell/code blocks as pretend execution.",
"After tool use, provide a final text response that separates landed work from failures.",
"For repo work, name the changed file, exposed symbol, caller, and verification result."
].freeze
module_function
def fake_execution_risk?(text)
text.to_s.match?(COMMAND_BLOCK)
end
def operational_claim?(text)
text.to_s.match?(CLAIM_WORDS)
end
def final_report(landed:, failed: [], next_steps: [])
sections = []
sections << ["Landed", Array(landed)] unless Array(landed).empty?
sections << ["Not landed", Array(failed)] unless Array(failed).empty?
sections << ["Next", Array(next_steps).first(3)] unless Array(next_steps).empty?
sections.map { |title, rows| "#{title}:\n" + rows.map { |row| "- #{row}" }.join("\n") }.join("\n\n")
end
def brief
"Tool protocol:\n- #{REQUIREMENTS.join("\n- ")}"
end
end
end
end# 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# 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# 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# frozen_string_literal: true
module Master
module Ground
module WorkflowPolicy
Phase = Data.define(:name, :input, :output, :questions, :actions)
Metric = Data.define(:name, :method, :threshold, :action)
LIMITS = {
coverage: 0.8,
complexity: 10,
convergence: 0.01,
iterations: 10,
coupling: 5,
duplication: 0.03,
nesting_depth: 4,
section_count: 15
}.freeze
PRINCIPLES = [
[:dry, :three_duplications, :abstract, :high],
[:kiss, :complexity_over_10, :simplify, :high],
[:yagni, :unused, :remove, :medium],
[:solid, :coupling_over_5, :decouple, :critical],
[:composition, :deep_inheritance, :compose, :medium],
[:evidence, :assumption, :validate, :critical],
[:reversible, :irreversible, :add_rollback, :critical],
[:explicit, :implicit, :make_explicit, :high],
[:orthogonal, :coupled, :split, :high],
[:minimalism, :bloat, :subtract, :medium],
[:clarity, :synonym, :unify, :medium],
[:flatten, :wrapper, :flatten, :high],
[:pola, :surprise, :make_predictable, :high],
[:unix, :multi_responsibility, :one_thing, :high],
[:anti_divitis, :div_soup, :semantic_html, :medium],
[:anti_sectionitis, :scattered_sections, :consolidate, :high],
[:geometric, :visual_confusion, :simplify_geometry, :medium]
].freeze
PHASES = [
Phase.new(:discover, :problem, :definition,
%w[specific_measurable who_affected_freq current_impact evidence if_nothing], []),
Phase.new(:analyze, :definition, :analysis,
%w[hidden_assumptions could_be_wrong dependencies evidence_support biases], %w[assumptions cost risk bias]),
Phase.new(:ideate, :analysis, :options,
%w[fifteen_approaches persona_suggests challenge_assumptions unconventional simplest], %w[gen_15_alt apply_10_personas synth]),
Phase.new(:design, :options, :plan,
%w[min_viable irreversible test_strategy maintainable], []),
Phase.new(:implement, :plan, :code,
%w[tests_prove edge_cases simplify duplication fail_points], %w[tests_first implement refactor]),
Phase.new(:validate, :code, :verified,
%w[proves_correct breaks missed violated adversarial_finds], %w[check_principles run_gates adversarial]),
Phase.new(:deliver, :verified, :deployed,
%w[deploy_ready docs monitoring rollback], []),
Phase.new(:learn, :deployed, :knowledge,
%w[worked failed differently patterns codify], %w[patterns measure improve codify])
].freeze
WORKFLOWS = {
new_feature: %i[discover analyze ideate design implement validate deliver learn],
bug: %i[analyze implement validate deliver],
refactor: %i[analyze design implement validate],
security_fix: %i[analyze implement validate deliver],
migration: %i[analyze design implement validate deliver]
}.freeze
METRICS = [
Metric.new(:complexity, :cyclomatic, LIMITS[:complexity], :simplify),
Metric.new(:coupling, :afferent_efferent, LIMITS[:coupling], :decouple),
Metric.new(:duplication, :token_similarity, LIMITS[:duplication], :extract),
Metric.new(:coverage, :line, LIMITS[:coverage], :write_tests),
Metric.new(:nesting, :depth, LIMITS[:nesting_depth], :flatten),
Metric.new(:sections, :count, LIMITS[:section_count], :consolidate)
].freeze
GATES = {
functional: { tests: true, coverage: LIMITS[:coverage] },
secure: { vulnerabilities: 0, input_validation: true },
maintainable: { complexity: LIMITS[:complexity], duplication: LIMITS[:duplication] },
access: { wcag: :aa, lcp: 2.5, inp: 200, cls: 0.1, mobile: true },
perf: { lcp: 2.5, cls: 0.1, js_kb: 170, total_kb: 2048 },
deploy: { health: true, rollback: true },
privacy: { gdpr: true, pii: true }
}.freeze
AUTO_FIX = %i[format dead_code typo import space naming].freeze
CONFIRM = %i[drop_table delete_production force_push rm_rf irreversible_data_change].freeze
def self.phase(name)
PHASES.find { |phase| phase.name == name.to_sym }
end
def self.workflow(name)
WORKFLOWS.fetch(name.to_sym, WORKFLOWS[:refactor]).filter_map { |phase_name| phase(phase_name) }
end
def self.gates(*names)
names.flatten.map(&:to_sym).to_h { |name| [name, GATES.fetch(name, {})] }
end
def self.confirm?(action)
CONFIRM.include?(action.to_sym)
end
def self.autofix?(action)
AUTO_FIX.include?(action.to_sym)
end
def self.brief(workflow: :refactor)
phases = workflow(workflow).map(&:name).join(" -> ")
"Workflow policy: #{workflow} phases #{phases}; gates #{GATES.keys.join(', ')}; hard limits #{LIMITS}."
end
end
end
end# frozen_string_literal: true
require_relative "llm_dispatcher"
module Master
module Judge
class Agent
DEFAULT_MESSAGE_WINDOW_SIZE = 16
Dependencies = Data.define(
:config, :session, :tools, :circuit_breaker, :cache, :bus,
:model_router, :reasoning_modes, :memory, :personality,
:code_index, :context_window, :homeostat
) do
def self.from_kwargs(config:, session:, tools:, circuit_breaker:, cache:,
event_bus: nil, model_router: nil, reasoning_modes: nil,
memory: nil, personality: nil, code_index: nil,
context_window: nil, homeostat: nil)
new(
config:, session:, tools:, circuit_breaker:, cache:, bus: event_bus,
model_router:, reasoning_modes:, memory:, personality:,
code_index:, context_window:, homeostat:
)
end
end
def initialize(deps:)
@deps = deps
@config, @session, @tools = deps.config, deps.session, deps.tools
@circuit_breaker, @cache, @bus = deps.circuit_breaker, deps.cache, deps.bus
@model_router, @reasoning_modes = deps.model_router, deps.reasoning_modes
@memory, @personality, @code_index = deps.memory, deps.personality, deps.code_index
@context_window, @homeostat = deps.context_window, deps.homeostat
@constitution = nil
@dispatcher = Master::Judge::LLMDispatcher.new(deps:, system_prompt: -> { system_prompt })
end
def wire_constitution(constitution) = @constitution = constitution
def chat(message, stream: true, escalation_depth: 0, &blk)
prepare_chat_turn(message)
candidate_models = routed_models(message)
selected_model = candidate_models.first
prompt = topic_anchored(message)
context = conversation_context
tokens_approx = Trace::Session.estimate_tokens(message)
@bus&.publish("llm:request", model: selected_model, tokens: tokens_approx)
@deps.homeostat&.observe(:llm_call)
rate_err = check_rate_limit(selected_model)
return rate_err if rate_err
response = attempt_chat_with_fallbacks(candidate_models:, prompt:, context:, stream:, &blk)
if response.is_a?(Master::Result::Err)
@deps.homeostat&.observe(:llm_failure)
return response
end
@deps.homeostat&.observe(:llm_success)
response = maybe_escalate(response, message, stream:, escalation_depth:, &blk)
text = response.to_s
@session.add_message(role: :assistant, content: text)
Result.ok(text)
rescue StandardError => chat_error
Result.err("agent: #{chat_error.message}", category: :handler_exception)
end
def prepare_chat_turn(message)
@context_window&.check_and_compact!
@tools.each { |t| t.reset! if t.respond_to?(:reset!) }
@session.add_message(role: :user, content: message)
end
def check_rate_limit(model_id = nil)
@circuit_breaker.check_rate!(model_id) if @circuit_breaker.respond_to?(:check_rate!)
nil
rescue Reach::CircuitBreaker::CircuitError => err
Result.err(err.message, category: err.category)
end
def ask(prompt, context: nil, operation: nil)
messages = Array(context) + [{ role: "user", content: apply_reasoning_mode(prompt) }]
selected_model = operation ? model_for(operation:) : routed_models.first
result = @dispatcher.send_with_cache(selected_model, messages, stream: false)
raise StandardError, result.message if result.is_a?(Master::Result::Err)
result.to_s
end
def ask_once(prompt, system: nil, model: nil)
messages = [{ role: "user", content: prompt.to_s }]
result = @dispatcher.send_with_cache(model || self.model, messages, system:, stream: false)
raise StandardError, result.message if result.is_a?(Master::Result::Err)
result.to_s
end
def call(ctx)
on_chunk = ctx[:on_chunk]
task_type = ctx[:task_type]&.to_s
with_task_type(task_type) do
on_chunk ? chat(ctx[:message].to_s, stream: true, &on_chunk) : chat(ctx[:message].to_s)
end
end
def model = routed_models.first
def model=(val)
@config["model"] = val
end
def with_model(override, &blk)
@model_mutex ||= Mutex.new
@model_mutex.synchronize do
prev = model
self.model = override
blk.call
ensure
self.model = prev
end
end
def model_for(operation:)
@model_router&.constrained_for(operation:) || model
end
def wire_context_window(ctx_window)
@context_window = ctx_window
end
private
def with_task_type(type)
return yield unless type && !type.empty?
old = @config["task_type"]
@config["task_type"] = type
yield
ensure
@config["task_type"] = old
end
TOPIC_DRIFT_THRESHOLD = 6
def topic_anchored(message)
topic = @session.respond_to?(:topic) && @session.topic
return message unless topic
return message if @session.messages.length < TOPIC_DRIFT_THRESHOLD
"#{message}\n\n[task: #{topic}]"
end
def apply_reasoning_mode(message, mode: @config.reasoning_mode)
return message unless @reasoning_modes
@reasoning_modes.wrap(message, mode:)
end
def system_prompt
parts = []
parts << "Current task: #{@session.topic}" if @session.respond_to?(:topic) && @session.topic
parts << @constitution.system_prompt if @constitution && !@constitution.empty?
parts << @personality.system_prompt if @personality
parts << @code_index.summary if @code_index&.built?
parts << @memory.context_summary if @memory&.context_summary
parts.compact!
parts.empty? ? nil : parts.join("\n\n")
end
def conversation_context(max_messages: DEFAULT_MESSAGE_WINDOW_SIZE)
messages = @session.messages
return [] unless messages.respond_to?(:each)
messages.last(max_messages + 1)[0...-1] || []
end
def attempt_chat_with_fallbacks(candidate_models:, prompt:, context:, stream:, &blk)
stage_warnings = []
fallback_modes = mode_chain_for(candidate_models)
last_response = nil
fallback_modes.each do |attempt|
selected_model = attempt.fetch(:model)
mode = attempt.fetch(:mode)
wrapped = apply_reasoning_mode(prompt, mode: mode)
response = @dispatcher.send_with_cache(
selected_model,
context + [{ role: "user", content: wrapped }],
stream:, &blk
)
if response.is_a?(Master::Result::Ok)
publish_llm_success(selected_model, response)
@bus&.publish("agent:stage_warnings", warnings: stage_warnings) unless stage_warnings.empty?
return response
end
last_response = response
stage_warnings << "llm failed in #{mode} on #{selected_model}: #{response.message}"
end
@bus&.publish("agent:all_fallbacks_exhausted", warnings: stage_warnings)
last_response || Result.err("all LLM fallback modes exhausted", category: :llm_call_failure)
end
def mode_chain_for(candidates)
models = Array(candidates).empty? ? [@config.model] : candidates
primary = models.first
modes = if @dispatcher.claude_cli_model?(primary) || @dispatcher.tool_capable?(primary)
[@config.reasoning_mode.to_s, "code_agent", "react"]
else
["code_agent", "react", "direct"]
end
chain = models.map { |m| { model: m, mode: modes.first } }
chain.concat(modes.drop(1).map { |mode| { model: primary, mode: mode } })
chain
end
def publish_llm_success(model, response)
tokens_approx = Trace::Session.estimate_tokens(response)
@bus&.publish("llm:response", model:, success: true, tokens_approx:)
end
def maybe_escalate(last_response, original_message, stream:, escalation_depth:, &blk)
return last_response unless @model_router
return last_response if escalation_depth >= 2
current = routed_models.first
escalation_model = @model_router.escalate_if_low_confidence(
last_response.to_s,
current_model: current,
task_type: @config.task_type.to_sym
)
return last_response unless escalation_model
return last_response if escalation_model.to_s == current.to_s
@bus&.publish("llm:escalation", from: current, to: escalation_model)
escalated = attempt_chat_with_fallbacks(
candidate_models: [escalation_model],
prompt: original_message,
context: conversation_context,
stream: stream,
&blk
)
escalated.is_a?(Master::Result::Err) ? last_response : escalated
end
def routed_models(message = nil)
return [@config.model] unless @model_router
task = message ? @model_router.classify_intent(message) : @config.task_type.to_sym
chain = @model_router.fallback_chain(task_type: task)
bias = @homeostat&.model_tier_bias
return cheap_first(chain) if bias == :cheap
chain
rescue StandardError => e
@bus&.publish("llm:route_error", error: e.message)
[@config.model]
end
def cheap_first(chain)
cheap = chain.select { |m| @model_router.tier_for_model(m) == "cheap" }
rest = chain.reject { |m| @model_router.tier_for_model(m) == "cheap" }
cheap.empty? ? chain : (cheap + rest)
end
end
end
end# 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# 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# frozen_string_literal: true
require "prism"
require "set"
require "monitor"
module Master
module Judge
# Live Prism-parsed symbol graph; rebuilt on write events.
class CodeIndex
Symbol = Struct.new(:fqn, :type, :file, :line, :parent, :includes, keyword_init: true)
Reference = Struct.new(:from_file, :from_line, :to_fqn, :ref_type, keyword_init: true)
class SymbolVisitor < Prism::Visitor
attr_reader :symbols, :references
def initialize(file:, root:)
@file = file; @root = root
@symbols = []; @references = []; @scope = []
end
def visit_class_node(node)
name = const_name(node.constant_path)
fqn = qualified(name)
@symbols << Symbol.new(fqn:, type: :class, file: @file, line: node.location.start_line,
parent: node.superclass ? const_name(node.superclass) : "Object", includes: [])
@scope.push(name); super; @scope.pop
end
def visit_module_node(node)
name = const_name(node.constant_path)
fqn = qualified(name)
@symbols << Symbol.new(fqn:, type: :module, file: @file, line: node.location.start_line,
parent: nil, includes: [])
@scope.push(name); super; @scope.pop
end
def visit_def_node(node)
meth = node.name.to_s
owner = @scope.last || "(top)"
@symbols << Symbol.new(fqn: "#{qualified(owner)}##{meth}", type: :method, file: @file,
line: node.location.start_line, parent: owner, includes: [])
super
end
def visit_call_node(node)
method_name = node.name.to_s
return super unless method_name.match?(/\A[_a-z][a-z0-9_]*[!?]?\z/i) && method_name.length > 1
receiver_fqn = node.receiver ? const_name_safe(node.receiver) : nil
to_fqn = receiver_fqn ? "#{receiver_fqn}##{method_name}" : method_name
@references << Reference.new(from_file: @file, from_line: node.location.start_line,
to_fqn:, ref_type: :call)
super
end
private
def qualified(name)
return name if @scope.empty? || name.include?("::")
"#{@scope.join("::")}::#{name}"
end
def const_name(node)
case node
when Prism::ConstantReadNode then node.name.to_s
when Prism::ConstantPathNode, Prism::ConstantPathTargetNode
"#{const_name(node.parent)}::#{node.name}"
else node.respond_to?(:name) ? node.name.to_s : ""
end
end
def const_name_safe(node)
name = const_name(node)
name.empty? ? nil : name
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "code_index.const_name_safe")
nil
end
end
SUMMARY_SKIP_NAMES = %w[Entry Message Symbol CircuitError].freeze
attr_reader :symbols, :references, :built_at
def initialize(root:, event_bus: nil)
@root = File.expand_path(root)
@bus = event_bus
@symbols = {}
@references = []
@mtimes = {}
@built_at = nil
@lock = Monitor.new
@build_thread = nil
end
def build(path: nil)
@lock.synchronize do
target = path ? File.expand_path(path, @root) : @root
files = Dir.glob(File.join(target, "**", "*.rb")).reject { |f| f.include?("/vendor/") }
@built_at.nil? ? first_build(files) : incremental_build(files)
@built_at = Time.now
@bus&.publish("code_index:built", files: files.size, symbols: @symbols.size)
end
self
rescue StandardError => e
@bus&.publish("code_index:error", error: e.message)
self
end
def build_async
@build_thread = Thread.new { build }
self
end
def ready? = !@built_at.nil?
def wait_for_build = @build_thread&.join
def built? = !@built_at.nil?
def size = @lock.synchronize { @symbols.size }
def reindex(file)
@lock.synchronize do
full = File.expand_path(file, @root)
purge_file(full)
index_file(full) if File.file?(full)
end
rescue StandardError => e
@bus&.publish("code_index:reindex_error", path: file, error: e.message)
end
def symbols_in(file)
with_built_index do
full = File.expand_path(file, @root)
@symbols.values.select { |s| s.file == full }
end
end
def find(name)
with_built_index { find_locked(name) }
end
def references_to(fqn)
with_built_index { references_for(fqn) }
end
def impact(fqn)
with_built_index do
refs = references_for(fqn)
files = refs.map(&:from_file).uniq.map { |f| relativize(f) }
callers = refs.map { |r| "#{relativize(r.from_file)}:#{r.from_line}" }.uniq
{ fqn:, reference_count: refs.size, files:, callers: }
end
end
def summary(limit: nil)
with_built_index do
classes = summary_classes
lib_count = @symbols.values.count { |s| s.file.include?("/lib/") }
stamp = @built_at&.strftime("%H:%M") || "never"
[
"# Codebase: #{lib_count} lib symbols (indexed #{stamp})",
"## Classes & Modules (#{classes.size})",
*classes
].join("\n")
end
end
def query(name)
with_built_index do
hits = find_locked(name)
next { error: "not found: #{name}" } if hits.empty?
hits.map { |s| query_entry(s) }
end
end
private
def with_built_index(&blk)
wait_for_build unless ready?
@lock.synchronize(&blk)
end
def first_build(files)
@symbols.clear
@references.clear
@mtimes.clear
files.each do |f|
index_file(f)
@mtimes[f] = (File.mtime(f) rescue nil)
end
end
def incremental_build(files)
(@mtimes.keys - files).each { |gone| purge_file(gone) }
changed = files.count { |f| reindex_if_stale(f) }
@bus&.publish("code_index:incremental", changed: changed, total: files.size) if changed > 0
end
def reindex_if_stale(file)
mt = (File.mtime(file) rescue nil)
return false if @mtimes[file] == mt
reindex(file)
@mtimes[file] = mt
true
end
def purge_file(full)
@symbols.delete_if { |_, s| s.file == full }
@references.reject! { |r| r.from_file == full }
@mtimes.delete(full)
end
def references_for(fqn)
tail = "##{fqn}"
@references.select { |r| to = r.to_fqn; to == fqn || to.end_with?(tail) }
end
def relativize(file)
file.sub("#{@root}/", "")
end
def find_locked(name)
exact = @symbols[name]
return [exact] if exact
suffix = name.to_s
@symbols.values.select { |sym| fqn = sym.fqn; fqn.end_with?(suffix) || fqn.include?(suffix) }
end
def summary_class?(sym)
return false unless %i[class module].include?(sym.type)
file = sym.file
return false if file.include?("/DEPLOY/") || file.match?(/fix_|patch_/)
fqn = sym.fqn
SUMMARY_SKIP_NAMES.none? { |n| fqn.end_with?("::#{n}") }
end
def summary_classes
@symbols.values
.select { |sym| summary_class?(sym) }
.sort_by(&:fqn)
.map { |sym| format_summary_entry(sym) }
end
def format_summary_entry(sym)
parent_name = sym.parent
parent = parent_name && parent_name != "Object" ? " < #{parent_name}" : ""
" #{sym.fqn}#{parent} (#{relativize(sym.file)}:#{sym.line})"
end
def query_entry(sym)
refs = references_for(sym.fqn)
{
fqn: sym.fqn,
type: sym.type,
file: relativize(sym.file),
line: sym.line,
parent: sym.parent,
used_in: refs.first(10).map { |r| "#{relativize(r.from_file)}:#{r.from_line}" }
}
end
def index_file(file)
src = File.read(file, encoding: "UTF-8")
parse_result = Prism.parse(src)
return unless parse_result.success?
visitor = SymbolVisitor.new(file:, root: @root)
parse_result.value.accept(visitor)
visitor.symbols.each { |s| @symbols[s.fqn] = s }
@references.concat(visitor.references)
rescue StandardError => e
@bus&.publish("code_index:parse_error", path: file, error: e.message)
end
end
end
end# 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# frozen_string_literal: true
module Master
module Judge
module Council
# Mode-dispatched council critique. Replaces UiCritique + SoundCritique.
# Each mode is a config hash: preset key, default files/panel, context
# briefs, constraints, ideation prompt, byte cap, event names.
class Critique
MODES = {
ui: {
preset_key: "ui_critique",
max_bytes: 32_768,
panel: nil,
files: %w[
web/public/face.css web/public/face.js web/public/chat.js
web/app/views/chat/index.html.erb lib/design/platform_profiles.rb
],
quality_kind: :design,
ideation_prompt: "Generate concrete multi-solution improvements for this web UI. " \
"Produce 3 distinct solution directions per issue found.",
cycles_default: 1,
start_event: :ui_critique_start,
done_event: :ui_critique_done,
constraints: [
"must not break existing HTML semantics",
"must preserve intentional CSS measurements unless a measurable rule is violated",
"animations must respect prefers-reduced-motion",
"solutions must be implementable without a build step",
"use Ruby QualityFramework design rules from Deliberation",
"use Master::Design::PlatformProfiles for content-first and profile-specific critique",
"distinguish measurable violations from subjective taste"
]
},
sound: {
preset_key: "sound_critique",
max_bytes: 24_576,
panel: %w[
Electronic\ Music\ Producer Hip-Hop\ Producer User\ Advocate
Accessibility Layperson Skeptic
],
files: %w[
web/public/chat.js web/public/face.js web/public/visual_bridge.js
web/app/views/chat/index.html.erb lib/voice/speech.rb lib/voice/dilla.rb
lib/voice/sonitex.rb lib/voice/sonitex_sox.rb lib/voice/production_dna.rb
lib/voice/ffmpeg_lofi.rb lib/voice/tts_lofi.rb
],
quality_kind: :sound,
ideation_prompt: "Generate concrete improvements for MASTER sound design, voice " \
"playback, sonic timing, and audio feedback.",
cycles_default: 2,
start_event: :sound_critique_start,
done_event: :sound_critique_done,
constraints: [
"no autoplay without user intent",
"must expose mute or silence path",
"must not mask speech or screen-reader output",
"must degrade when AudioContext or media playback fails",
"prefer tiny generated tones or short assets over heavy dependencies",
"preserve existing visual identity",
"use Ruby QualityFramework sound rules from Deliberation",
"when lo-fi processing is proposed, call Master::Voice::Sonitex rather than ad-hoc shell strings",
"when Dilla-style timing is proposed, call Master::Voice::Dilla for swing, nudge, chord, and preset data",
"when TTS effects are proposed, call Master::Voice::TtsLofi or Master::Voice::FfmpegLofi and keep clean audio as default"
]
}
}.freeze
def initialize(mode:, agent:, event_bus: nil)
@mode = MODES.fetch(mode) { raise ArgumentError, "unknown critique mode: #{mode}" }
@agent = agent
@bus = event_bus
end
def run
preset = load_preset
panel = build_panel(preset)
payload = build_payload(preset)
@bus&.publish(@mode[:start_event], files: payload[:files], personas: panel.map(&:name))
delib = Deliberation.new(personas: panel, agent: @agent, event_bus: @bus, judge_enabled: true)
result = delib.review(payload[:combined], context: build_context)
return result unless result.ok?
ideation_result = Ideation.new(agent: @agent, event_bus: @bus).ideate(
@mode[:ideation_prompt],
constraints: @mode[:constraints],
cycles: (preset["cycles"] || @mode[:cycles_default]).to_i
)
feedback = result.value!
cherry = cherry_pick_from(feedback, ideation_result)
@bus&.publish(@mode[:done_event], cherry_picks: cherry.size)
Master::Result.ok({ feedback: feedback, ideas: ideation_value(ideation_result), cherry_picks: cherry })
end
private
def load_preset
return {} unless File.exist?(Master::COUNCIL_PATH)
data = Master.load_yaml(Master::COUNCIL_PATH) || {}
data.dig("presets", @mode[:preset_key]) || {}
end
def build_panel(preset)
all = Personas.load
names = Array(preset["panel"] || @mode[:panel]).map(&:downcase)
return all if names.empty?
picked = all.select { |p| names.include?(p.name.downcase) }
picked.empty? ? Personas::DEFAULTS : picked
end
def build_payload(preset)
files = Array(preset["files"]).any? ? preset["files"] : @mode[:files]
combined = files.filter_map { |rel| read_truncated(rel) }.join("\n\n")
{ combined: combined, files: files }
end
def read_truncated(rel)
path = File.join(Master::ROOT, rel)
return nil unless File.exist?(path)
raw = File.read(path, encoding: "utf-8")
raw = raw.byteslice(0, @mode[:max_bytes]) + "\n... [truncated]" if raw.bytesize > @mode[:max_bytes]
"file: #{rel}\n#{raw}"
end
def build_context
[domain_context, Deliberation.quality_brief(@mode[:quality_kind]), domain_briefs]
.compact.join("\n")
end
def domain_context
return ui_domain_context if @mode[:preset_key] == "ui_critique"
sound_domain_context
end
def ui_domain_context
<<~CTX
This is the MASTER constitutional AI agent web UI. Design intent:
- Full-screen canvas particle face (WebGL-free, 2D Canvas API)
- Particles form 3D face shape, morph between poses like a swarm
- Black background, white/grey/dark-red particles, 1px only
- Chat panel slides in from right, oh-my-zsh style prompt
- Edge-tts Osman voice, server-side, AudioContext playback
- Visitor access (no token), authenticated (token) tiers
Critique CSS, JS, HTML semantics, animation, typography, layout, hierarchy, accessibility, and data-ink economy.
CTX
end
def sound_domain_context
<<~CTX
Review MASTER as an interactive AI agent with visual motion, chat streaming, and voice/audio affordances.
Treat sound design as product behavior, not decoration.
Evaluate sonic hierarchy, timing, mix role, accessibility, graceful failure, implementation size, and TTS quality.
CTX
end
def domain_briefs
return platform_profile_brief if @mode[:preset_key] == "ui_critique"
[sonitex_brief, dilla_brief, tts_lofi_brief].join("\n")
end
def platform_profile_brief
return "Platform design profiles unavailable; default to content-first measurable critique." \
unless defined?(Master::Design::PlatformProfiles)
%i[brutal_minimal medium new_yorker].map { |k| Master::Design::PlatformProfiles.brief(k) }.join("\n")
rescue StandardError => e
"Platform profile policy failed to load: #{e.message}."
end
def sonitex_brief
return Master::Voice::Sonitex.brief if defined?(Master::Voice::Sonitex)
return Master::Voice::SonitexSox.brief if defined?(Master::Voice::SonitexSox)
"Sonitex/SoX policy unavailable; prefer cumulative subtle degradation and document SoX gaps."
rescue StandardError => e
"Sonitex/SoX policy failed to load: #{e.message}."
end
def dilla_brief
return Master::Voice::Dilla.brief if defined?(Master::Voice::Dilla)
return Master::Voice::ProductionDna.brief if defined?(Master::Voice::ProductionDna)
"Production DNA unavailable; keep timing human, restrained, and non-quantized when musical."
rescue StandardError => e
"Dilla production profile failed to load: #{e.message}."
end
def tts_lofi_brief
return Master::Voice::TtsLofi.brief if defined?(Master::Voice::TtsLofi)
return Master::Voice::FfmpegLofi.brief if defined?(Master::Voice::FfmpegLofi)
"TTS lofi policy unavailable; default to clean audio and make effects opt-in."
rescue StandardError => e
"TTS lofi policy failed to load: #{e.message}."
end
def ideation_value(ir)
ir.respond_to?(:err?) && ir.err? ? "" : (ir.respond_to?(:value) ? ir.value : ir)
end
def cherry_pick_from(feedback, ideation_result)
text = if ideation_result.respond_to?(:value)
ideation_result.value.is_a?(Hash) ? ideation_result.value.fetch(:final, "") : ideation_result.value.to_s
else
ideation_result.to_s
end
cherry_pick(feedback, text)
end
def cherry_pick(feedback, ideas_text)
feedback_text = feedback.map { |f| f[:feedback].to_s }.join("\n")
lines = ideas_text.to_s.lines.map(&:strip).reject(&:empty?)
lines.sort_by { |line| -text_overlap(line, feedback_text) }.first(12)
end
def text_overlap(a, b)
wa = a.downcase.scan(/\w+/).to_set
wb = b.downcase.scan(/\w+/).to_set
return 0.0 if wa.empty? || wb.empty?
(wa & wb).size.to_f / wa.size
end
end
end
end
end# frozen_string_literal: true
module Master
module Judge
module Council
module QualityFramework
DEFAULT_QUESTIONS = {
"assumptions" => [
"what are we assuming that could be false?",
"if a key assumption flips what still works?",
"which assumptions have we never tested?",
"what would happen if the opposite were true?",
"which assumptions are load-bearing vs convenience?",
"how do we validate assumptions incrementally?"
],
"failure_modes" => [
"how does this fail catastrophically?",
"what breaks first under load or outage?",
"which single point of failure is most likely?",
"what happens when it fails silently?",
"how do cascading failures propagate?",
"what are blast radius containment strategies?"
],
"attacker" => [
"what would an attacker do here?",
"where can inputs be abused or poisoned?",
"which trust boundaries are weakest?",
"how would we exploit this ourselves?",
"what attack vectors are we not considering?",
"how do we defend against insider threats?"
],
"scale" => [
"what happens at 10x users or data?",
"what performance cliff exists and where?",
"which bottleneck appears first?",
"how does complexity grow with scale?",
"what are the economics at different scales?",
"which architectural decisions become problematic at scale?"
],
"degradation" => [
"how do we degrade gracefully?",
"what is minimal viable behavior under stress?",
"which features can we sacrifice first?",
"how do we maintain core function during failure?",
"what are UX implications of degradation?",
"how do we communicate degraded service to users?"
],
"edge_cases" => [
"which edge cases will users hit first?",
"which rare but high-impact case is unhandled?",
"what happens with malformed inputs?",
"how do we handle impossible combinations?",
"which edge cases become common at scale?",
"what edge cases exist in integration points?"
],
"ops_maint" => [
"what is long-term maintenance burden?",
"how do we observe debug and rollback quickly?",
"which operational complexity is hidden?",
"how do we troubleshoot under pressure?",
"what skills and knowledge are required for operations?",
"how do we prevent operational knowledge from being siloed?"
],
"compliance_ethics" => [
"any privacy safety or fairness risks?",
"which regulations apply and how prove compliance?",
"what are ethical implications?",
"how audit and demonstrate adherence?",
"what happens when regulations change?",
"how balance compliance with innovation?"
],
"a11y_ux" => [
"is it operable by keyboard and screen readers?",
"what happens with reduced motion or low bandwidth?",
"how does this work for colorblind users?",
"can this be used with assistive technology?",
"what are multilingual and cultural considerations?",
"how test accessibility with actual users?"
],
"economics" => [
"where is waste or needless complexity?",
"what is ROI vs simpler alternatives?",
"which costs are hidden or deferred?",
"how optimize for total cost of ownership?",
"what are opportunity costs of this approach?",
"how do economics change over time and scale?"
],
"clarity" => [
"is the intent obvious from names alone?",
"which concept lacks a name and should have one?",
"where does the code lie about what it does?",
"what would a fresh reader misread first?",
"which generic names hide domain meaning?"
],
"evidence" => [
"what evidence proves this works?",
"which test would fail if this were wrong?",
"what claim is unsupported?",
"what would falsify this approach?"
],
"scope" => [
"what can be deleted without loss?",
"which abstraction is premature?",
"what is the smallest reversible change?",
"where did implementation exceed the need?"
],
"bottlenecks" => [
"where is the Big-O bottleneck?",
"what allocates in the hot path?",
"what happens to latency at p95 and p99?",
"which work can be skipped, cached, streamed, or deferred?"
],
"consistency" => [
"where can data go inconsistent?",
"what is the source of truth?",
"which names describe the same concept?",
"which migration or state transition is not reversible?"
],
"harm" => [
"who could this harm if misused?",
"what privacy boundary is crossed?",
"what content or user data must remain untouched?",
"what compliance proof would be required?"
],
"visual" => [
"where does the eye land first, and is that the right place?",
"which element can be removed without losing meaning?",
"does spacing prove grouping, or only decorate?",
"which values violate the type scale, grid, or contrast rules?",
"does the layout use absence as material?"
],
"sound" => [
"what should be foreground, background, or silent?",
"does timing breathe, or is everything quantized to death?",
"could sound mask speech, screen readers, or the user's task?",
"is there a mute path and graceful media failure?",
"which sound event communicates state instead of decoration?"
]
}.freeze
BRIEFS = {
general: [
"questions over commands; evidence over opinion; execution over explanation",
"content integrity, critical security, accessibility, and reversibility are hard gates",
"prefer surgical, reversible changes; preserve working behavior and user-curated content",
"generate alternatives, red-team assumptions, then cherry-pick the simplest validated option"
].freeze,
code: [
"DRY after the third duplication; KISS when complexity exceeds 10; YAGNI for unused constructs",
"SOLID boundaries: one reason to change, composition over inheritance, injected dependencies",
"Ruby style: guard clauses, semantic names, no generic manager/handler/util names",
"quality targets: complexity <= 10, nesting <= 4, duplication <= 3%, coverage >= 80%"
].freeze,
design: [
"typography is design: 45-75ch lines, 1.4-1.6 body leading, 16px minimum body text",
"use an 8px spacing rhythm, 12-column structure, 44px touch minimum, and visible focus",
"ultraminimalism: remove ornament until only hierarchy, alignment, type, and whitespace remain",
"limit palette and type variety; reject non-token visual values and arbitrary decoration"
].freeze,
sound: [
"sound is feedback, not surprise: no autoplay without intent and mute must exist",
"preserve silence; sounds need attack/decay/timing and must not mask speech or screen readers",
"foreground sound is for critical state changes; midground for confirmations; background is optional",
"prefer tiny browser-native tones/assets and graceful failure over heavy dependencies"
].freeze
}.freeze
PERSONA_DOMAIN = {
"Graphic Designer" => :design,
"Web Designer" => :design,
"Motion Designer" => :design,
"Google CSS Engineer" => :design,
"NNGroup UX Researcher" => :design,
"Accessibility" => :design,
"Electronic Music Producer" => :sound,
"Hip-Hop Producer" => :sound,
"Sound Designer" => :sound,
"Security" => :code,
"Reliability" => :code,
"Maintainer" => :code,
"Performance" => :code,
"QA Engineer" => :code
}.freeze
def self.questions
council = if File.exist?(Deliberation::Master::COUNCIL_PATH)
Master.load_yaml(Deliberation::Master::COUNCIL_PATH).fetch("questions", {})
else
{}
end
DEFAULT_QUESTIONS.merge(council) { |_key, builtin, custom| (Array(builtin) + Array(custom)).uniq }
rescue StandardError
DEFAULT_QUESTIONS
end
def self.domain_for(persona_name)
PERSONA_DOMAIN.fetch(persona_name.to_s, :general)
end
def self.brief(domain = :general)
([*BRIEFS[:general], *BRIEFS.fetch(domain.to_sym, [])]).uniq.join("\n- ").then do |text|
"Quality framework:\n- #{text}"
end
end
end
class Deliberation
MAX_CONCURRENT = 4
MAX_CODE_BYTES = 8_192
TRUNCATE_MARKER = "\n... [truncated to #{MAX_CODE_BYTES} bytes for review]".freeze
JUDGE_TIMEOUT = 30
TOTAL_BUDGET_S = 120
MIN_QUORUM = 3
CONVERGENCE_ROUNDS = 3
CONVERGENCE_OVERLAP = 0.7
CONVERGENCE_TEXT_SIM = 0.6
@questions = nil
def self.questions
@questions ||= QualityFramework.questions
end
def self.sample_question(persona)
lens = persona.respond_to?(:cognitive_lens) ? persona.cognitive_lens : nil
return nil unless lens
questions[lens.to_s]&.sample
end
def self.quality_brief(domain = :general)
QualityFramework.brief(domain)
end
def initialize(personas:, agent:, event_bus: nil, axioms: nil, judge_enabled: true, mode: :parallel)
@personas = personas
@agent = agent
@bus = event_bus
@rules = axioms
@judge_enabled = judge_enabled
@mode = mode
validate_dependencies!
end
def review_convergent(code, context: nil, max_rounds: CONVERGENCE_ROUNDS)
history = []
round_context = context
max_rounds.times do |i|
@bus&.publish(:council_round_start, round: i + 1, max: max_rounds)
review_result = review(code, context: round_context)
return review_result unless review_result.is_a?(Master::Result::Ok)
feedback = review_result.value!
history << feedback
if history.size >= 2 && converged?(history[-2], history[-1])
@bus&.publish(:council_converged, round: i + 1)
break
end
round_context = feedback_summary(feedback, context)
end
Master::Result.ok(Array(history.last))
end
def review(code, context: nil)
return Master::Result.err("council: no personas configured", category: :validation) if @personas.empty?
feedback = @mode == :sequential ? collect_sequential(code, context) : collect_parallel(code, context)
if feedback.size < MIN_QUORUM
@bus&.publish(:council_timeout, completed: feedback.size, total: @personas.size)
quorum_msg = "council: quorum not reached (#{feedback.size}/#{@personas.size})"
return Master::Result.err(quorum_msg, category: :timeout)
end
vetoes = feedback.select { |f| f[:veto_role] && veto_text?(f[:feedback]) }
unless vetoes.empty?
veto = vetoes.first
@bus&.publish(:council_veto, veto)
return Master::Result.err("council: veto from #{veto[:persona]}\n#{veto[:feedback]}", category: :validation)
end
synthesis = @judge_enabled ? judge(feedback, code, context) : nil
if synthesis
@bus&.publish(:council_synthesis, synthesis: synthesis)
feedback << { persona: "Judge", role: "Synthesis", veto_role: false,
axiom: nil, feedback: synthesis }
end
scores = feedback.filter_map { |f| f[:confidence] }
council_confidence = scores.empty? ? 0.5 : scores.sum / scores.size
@bus&.publish(:council_confidence, score: council_confidence.round(3), members: feedback.size)
Master::Result.ok(feedback)
rescue StandardError => e
Master::Result.err("council: #{e.message}", category: :unknown)
end
private
# Parallel fan-out — all personas in flight at once, bounded by MAX_CONCURRENT.
def collect_parallel(code, context)
return [] if circuit_open?
slots = Mutex.new
available = MAX_CONCURRENT
ready = ConditionVariable.new
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + TOTAL_BUDGET_S
threads = @personas.map do |persona|
Thread.new do
slots.synchronize { ready.wait(slots) until available > 0; available -= 1 }
begin
ask_persona(persona, code, context)
ensure
slots.synchronize { available += 1; ready.broadcast }
end
end
end
threads.filter_map { |t| join_or_kill(t, deadline) }
end
# Sequential handoff — each persona sees prior personas' feedback as context.
# Slower than parallel but lets personas react to each other rather than
# all speaking in isolation. Use when reactions and rebuttals matter more
# than independent reads.
def collect_sequential(code, context)
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + TOTAL_BUDGET_S
acc = []
@personas.each do |persona|
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
break if circuit_open?
turn_ctx = acc.empty? ? context : "#{context}\n\nprior turns:\n#{format_prior_turns(acc)}"
entry = ask_persona(persona, code, turn_ctx)
acc << entry if entry
end
acc
end
def circuit_open?
breaker = @agent.respond_to?(:circuit_breaker) ? @agent.circuit_breaker : nil
return false unless breaker.respond_to?(:open_models)
!breaker.open_models.empty?
rescue StandardError
false
end
def ask_persona(persona, code, context)
prompt = build_prompt(persona, code, context)
response = persona.model ? @agent.ask_once(prompt, model: persona.model) : @agent.ask(prompt)
entry = { persona: persona.name, role: persona.role,
veto_role: veto_role?(persona), axiom: primary_axiom(persona),
model: persona.model, feedback: response, confidence: score_confidence(response) }
@bus&.publish(:council_feedback, entry)
entry
rescue StandardError => e
@bus&.publish("council:persona_error", persona: persona.name, error: e.message)
nil
end
def format_prior_turns(entries)
entries.map { |e| "#{e[:persona]} (#{e[:role]}): #{e[:feedback].to_s.lines.first(3).join.strip}" }.join("\n\n")
end
def join_or_kill(thread, deadline)
remaining = [deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0.1].max
thread.join(remaining) ? thread.value : (thread.kill; nil)
end
def converged?(prev, curr)
return false if prev.empty? || curr.empty? || prev.size != curr.size
prev_texts = prev.map { |f| f[:feedback].to_s }
curr_texts = curr.map { |f| f[:feedback].to_s }
same = curr_texts.zip(prev_texts).count { |c, p| text_similarity(c, p) >= CONVERGENCE_TEXT_SIM }
same.to_f / curr_texts.size >= CONVERGENCE_OVERLAP
end
def text_similarity(a, b)
return 0.0 if a.empty? || b.empty?
sa = a.downcase.scan(/\w+/).uniq
sb = b.downcase.scan(/\w+/).uniq
union = (sa | sb).size
union.zero? ? 0.0 : (sa & sb).size.to_f / union
end
def feedback_summary(feedback, base_context)
lines = feedback.reject { |f| f[:persona] == "Judge" }.map do |f|
"#{f[:persona]} (#{f[:role]}): #{f[:feedback].to_s.lines.first(2).join.strip}"
end
summary = "\nprior round:\n" + lines.join("\n") + "\n"
[base_context, summary].compact.join
end
def judge(feedback, code, context)
prompt = build_judge_prompt(feedback, code, context)
@agent.ask(prompt)
rescue StandardError => e
@bus&.publish(:council_judge_error, error: e.message)
nil
end
PROMPTS_PATH = File.join(Master::ROOT, "data", "prompts", "council.yml").freeze
def self.prompts
@prompts ||= Master.load_yaml(PROMPTS_PATH) || {}
end
def build_judge_prompt(feedback, code, _context)
rounds = feedback.map { |f| format_round(f) }.join("\n\n")
format(self.class.prompts.fetch("judge"),
quality_brief: self.class.quality_brief(:general),
rounds: rounds)
end
def format_round(f)
axiom_tag = f[:axiom] ? "[#{f[:axiom]}] " : ""
"#{axiom_tag}#{f[:persona]} (#{f[:role]}): #{f[:feedback]}"
end
def primary_axiom(persona)
ids = persona.respond_to?(:emphasizes) ? Array(persona.emphasizes) : []
ids.first
end
def axiom_line(persona)
id = primary_axiom(persona)
return "" unless id && @rules
name = @rules.lookup(id)
name ? "You speak primarily for the #{id} axiom: #{name}." : ""
end
def validate_dependencies!
raise ArgumentError, "personas must be an array" unless @personas.is_a?(Array)
raise ArgumentError, "agent must respond to :ask" unless @agent.respond_to?(:ask)
end
def veto_role?(persona)
if persona.respond_to?(:veto?)
persona.veto?
else
persona.respond_to?(:veto_role) && !!persona.veto_role
end
end
def build_prompt(persona, code, context)
ctx = context ? "\nContext: #{context}\n" : ""
veto_hint = veto_role?(persona) ? " You may prefix VETO: if this must not ship." : ""
safe_code = truncate_code(code.to_s)
axiom = axiom_line(persona)
axiom_block = axiom.empty? ? "" : "#{axiom}\n"
quality_block = self.class.quality_brief(QualityFramework.domain_for(persona.name))
question = self.class.sample_question(persona)
question_block = question ? "\nAdversarial question for this turn: #{question}\n" : ""
format(self.class.prompts.fetch("juror"),
persona_name: persona.name, persona_role: persona.role,
persona_bias: persona.bias, persona_prompt: persona.prompt,
ctx: ctx, axiom_block: axiom_block, quality_block: quality_block,
question_block: question_block, safe_code: safe_code, veto_hint: veto_hint)
end
def truncate_code(code)
return code if code.bytesize <= MAX_CODE_BYTES
@bus&.publish(:council_code_truncated, bytes: code.bytesize, limit: MAX_CODE_BYTES)
code.byteslice(0, MAX_CODE_BYTES) + TRUNCATE_MARKER
end
VETO_RE = /\AVETO:/i.freeze
def veto_text?(feedback)
VETO_RE.match?(feedback.to_s.strip)
end
HIGH_CONF = /\b(certain|clearly|definitely|must|always|never|critical|serious)\b/i.freeze
LOW_CONF = /\b(maybe|possibly|perhaps|unclear|might|could|unsure|uncertain)\b/i.freeze
def score_confidence(text)
t = text.to_s
highs = t.scan(HIGH_CONF).size
lows = t.scan(LOW_CONF).size
total = highs + lows
return 0.5 if total.zero?
(0.5 + (highs - lows).to_f / (total * 2)).clamp(0.1, 0.95)
end
end
end
end
end# 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# 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# frozen_string_literal: true
require "json"
require "net/http"
require "uri"
module Master
module Judge
module Embeddings
module_function
DEFAULT_MODEL = "nomic-embed-text"
HTTP_TIMEOUT = 5
MIN_SIM = 0.30
@ollama_alive = nil
def enabled? = !ENV["OLLAMA_BASE_URL"].to_s.strip.empty? && ollama_alive?
def embed(text)
return unless enabled?
text_str = text.to_s
return if text_str.strip.empty?
ollama_embed(text_str)
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "embeddings.embed")
nil
end
def ollama_alive?
return @ollama_alive unless @ollama_alive.nil?
uri = URI.join(ENV["OLLAMA_BASE_URL"], "/api/tags")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == "https"
http.open_timeout = HTTP_TIMEOUT
http.read_timeout = HTTP_TIMEOUT
res = http.get(uri.request_uri)
@ollama_alive = res.is_a?(Net::HTTPSuccess)
rescue StandardError
@ollama_alive = false
end
def cosine(a, b)
return 0.0 unless a.is_a?(Array) && b.is_a?(Array) && a.size == b.size && !a.empty?
dot, na, nb = 0.0, 0.0, 0.0
a.each_with_index do |x, i|
y = b[i]
dot += x * y
na += x * x
nb += y * y
end
mag = Math.sqrt(na) * Math.sqrt(nb)
mag.zero? ? 0.0 : dot / mag
end
def ollama_embed(text)
uri = URI.join(ENV["OLLAMA_BASE_URL"], "/api/embeddings")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == "https"
http.read_timeout = HTTP_TIMEOUT
http.open_timeout = HTTP_TIMEOUT
req = Net::HTTP::Post.new(uri.request_uri, "Content-Type" => "application/json")
req.body = JSON.generate(model: ENV.fetch("EMBEDDINGS_MODEL", DEFAULT_MODEL), prompt: text)
res = http.request(req)
return unless res.is_a?(Net::HTTPSuccess)
parsed = JSON.parse(res.body) rescue nil
vec = parsed&.fetch("embedding", nil)
vec.is_a?(Array) ? vec : nil
end
end
end
end# frozen_string_literal: true
require "ruby_llm"
require "digest"
require "json"
require "open3"
module Master
module Judge
class LLMDispatcher
COST_PER_TOKEN = 0.000_015
CACHE_WINDOW = 4
REACT_MAX_STEPS = 8
NEMOTRON3_RE = /nemotron-3/i.freeze
LLAMA_NEMOTRON_RE = /llama.*nemotron|nemotron.*llama/i.freeze
TOOL_CALL_RE = /<tool_call>(.*?)<\/tool_call>/m.freeze
TOOL_RESULT_ROLE = "user"
KEY_PATTERNS = [
/sk-[A-Za-z0-9_\-]{16,}/,
/sk-ant-[A-Za-z0-9_\-]{16,}/,
/Bearer\s+[A-Za-z0-9_\-\.]{16,}/i,
/\b[A-Za-z0-9]{32,}\b/
].freeze
LLM_TOOL_MAP = {
Reach::ReadFile => Reach::LLM::ReadFile,
Reach::WriteFile => Reach::LLM::WriteFile,
Reach::StrReplace => Reach::LLM::StrReplace,
Reach::ListDir => Reach::LLM::ListDir,
Reach::SearchFiles => Reach::LLM::SearchFiles,
Reach::Shell => Reach::LLM::Shell,
Reach::WebSearch => Reach::LLM::WebSearch,
Reach::AskLlm => Reach::LLM::AskLlm,
Reach::GitContext => Reach::LLM::GitContext,
Reach::AstEdit => Reach::LLM::AstEdit,
Reach::SearchKnowledge => Reach::LLM::SearchKnowledge,
Reach::FeedbackRecord => Reach::LLM::FeedbackRecord,
Reach::MemoryRecord => Reach::LLM::MemoryRecord
}.freeze
def self.build_tool_capable_re
yml_path = File.join(Master::ROOT, "data", "models.yml")
prefixes = Master.load_yaml(yml_path).fetch("tool_capable_prefixes", [])
escaped = prefixes.map { |p| Regexp.escape(p) }
Regexp.new("\\A(?:#{escaped.join("|")})(?:[:\\/@\\-.].+)?\\z", Regexp::IGNORECASE).freeze
end
TOOL_CAPABLE_RE = build_tool_capable_re.freeze
def initialize(deps:, system_prompt:)
@config, @cache, @circuit_breaker = deps.config, deps.cache, deps.circuit_breaker
@tools, @bus, @system_prompt_proc = deps.tools, deps.bus, system_prompt
@model_router = deps.model_router
@session = deps.session
@tool_registry = load_tool_registry
end
def send_with_cache(selected_model, messages, system: nil, stream: false, &blk)
cache_key = cache_key_for(messages.last[:content], messages[0...-1], selected_model)
breaker_for(selected_model).call(estimate_cost(messages.last[:content])) {
@cache.fetch(cache_key, selected_model) {
send_llm_request(selected_model, messages, system:, stream:, &blk)
}
}
rescue Reach::CircuitBreaker::CircuitError => err
Result.err(redact_secrets(err.message), category: err.category)
rescue StandardError => err
return Result.err(Master.no_api_key_message, category: :no_api_key) if missing_key_error?(err)
Result.err(redact_secrets(err.message.to_s), category: :llm_call_failure)
end
def redact_secrets(text)
out = text.to_s
KEY_PATTERNS.each { |re| out = out.gsub(re, "[REDACTED]") }
out
end
def missing_key_error?(err)
msg = err.message.to_s
msg.match?(/missing configuration/i) ||
msg.match?(/api[_\- ]?key/i) ||
msg.match?(/unauthorized/i) ||
msg.match?(/401/) ||
!Master.any_api_key_present?
end
def claude_cli_model?(model_id) = model_id.to_s.start_with?("claude-cli:")
def web_chat_model?(model_id) = model_id.to_s.start_with?("web-chat:")
def tool_capable?(model_id) = TOOL_CAPABLE_RE.match?(model_id.to_s.downcase)
private
def system_prompt = @system_prompt_proc.call
def send_llm_request(selected_model, messages, system: nil, stream: false, &blk)
sys = system || system_prompt
return send_claude_cli(selected_model.delete_prefix("claude-cli:"), messages, sys:) if claude_cli_model?(selected_model)
return send_web_chat(selected_model.delete_prefix("web-chat:"), messages, sys:) if web_chat_model?(selected_model)
if !tool_capable?(selected_model) && @tools.any?
return react_tool_loop(selected_model, messages, sys:, stream:, &blk)
end
send_ruby_llm(selected_model, messages, sys:, stream:, &blk)
end
def send_claude_cli(model_alias, messages, sys:)
args = ["claude", "--print", "--model", model_alias]
args += ["--system-prompt", sys] if sys && !sys.empty?
out, err, status = Open3.capture3(*args, stdin_data: text_prompt_for(messages))
return Result.err("claude-cli: #{err.strip}", category: :provider_error) unless status.success?
Result.ok(out.strip)
rescue StandardError => e
Result.err("claude-cli: #{e.message}", category: :provider_error)
end
def send_web_chat(provider, messages, sys:)
Result.ok(WebChat.call(provider:, prompt: text_prompt_for(messages), system: sys))
rescue StandardError => e
Result.err("web-chat: #{e.message}", category: :provider_error)
end
# ReactToolLoop — emulates function calling for models that lack native tool support.
# Injects a text-format tool schema into the system prompt; parses <tool_call> XML
# from responses; executes tools directly; loops until no calls remain.
def react_tool_loop(selected_model, messages, sys:, stream:, &blk)
react_sys = build_react_system(sys)
history = messages.dup
last = nil
REACT_MAX_STEPS.times do |step|
result = send_ruby_llm(selected_model, history, sys: react_sys, stream: step.zero? ? stream : false, &(step.zero? ? blk : nil))
return result if result.err?
text = result.to_s
calls = parse_tool_calls(text)
last = result
break if calls.empty?
@bus&.publish("react:tool_calls", model: selected_model, step:, count: calls.size)
history << { role: "assistant", content: text }
tool_results = calls.map { |c| execute_react_tool(c["name"], c["args"] || {}) }
history << { role: TOOL_RESULT_ROLE, content: tool_results.join("\n\n") }
end
last || Result.err("react: no response generated", category: :llm_call_failure)
end
def build_react_system(base_sys)
schema = @tools.filter_map { |t|
name = t.class.name.split("::").last
meta = @tool_registry.fetch(name, {})
desc = meta["description"] || name.gsub(/([A-Z])/, ' \1').strip
"- #{name}: #{desc}"
}.join("\n")
react_instructions = <<~INST.strip
You have access to these tools. Call a tool with:
<tool_call>{"name": "ToolName", "args": {"param": "value"}}</tool_call>
Available tools:
#{schema}
Reason step-by-step. When finished, give your final answer without any <tool_call> blocks.
INST
[base_sys, react_instructions].compact.join("\n\n")
end
def parse_tool_calls(text)
text.scan(TOOL_CALL_RE).filter_map do |match|
JSON.parse(match.first.strip)
rescue JSON::ParserError
nil
end
end
def execute_react_tool(name, args)
tool = @tools.find { |t| t.class.name.split("::").last == name }
return "<tool_result name=\"#{name}\">error: tool not found</tool_result>" unless tool
sym_args = args.transform_keys(&:to_sym)
raw = tool.respond_to?(:call) ? tool.call(**sym_args) : "unsupported"
out = Result.wrap(raw).value_or(raw.to_s)
"<tool_result name=\"#{name}\">\n#{out}\n</tool_result>"
rescue StandardError => e
"<tool_result name=\"#{name}\">error: #{e.message}</tool_result>"
end
def text_prompt_for(messages)
prompt = messages.last[:content].to_s
context = messages[0...-1].map { |m| "#{m[:role]}: #{m[:content]}" }.join("\n\n")
context.empty? ? prompt : "#{context}\n\nuser: #{prompt}"
end
def send_ruby_llm(selected_model, messages, sys:, stream:, &blk)
chat_session = RubyLLM.chat(model: selected_model)
final_sys = nemotron_system_prompt(selected_model, sys)
chat_session.with_instructions(final_sys) if final_sys
messages.each { |msg| chat_session.add_message(role: msg[:role].to_s, content: msg[:content].to_s) }
available_tools = llm_tools(selected_model)
chat_session.with_tools(*available_tools) unless available_tools.empty?
reply = if stream && blk
chat_session.ask(messages.last[:content]) { |chunk| blk.call(chunk.content.to_s) if chunk.content }
else
chat_session.ask(messages.last[:content])
end
record_usage(reply, selected_model)
Result.ok(extract_response(reply, selected_model))
end
def record_usage(reply, model)
return unless @session
input = reply.respond_to?(:input_tokens) ? reply.input_tokens.to_i : 0
output = reply.respond_to?(:output_tokens) ? reply.output_tokens.to_i : 0
tokens = input + output
if tokens.zero? && reply.respond_to?(:content)
tokens = Master::Trace::Session.estimate_tokens(reply.content)
end
return if tokens.zero?
@session.record_cost((tokens * COST_PER_TOKEN).round(6), model:, tokens:)
rescue StandardError => e
@bus&.publish("cost:record_error", error: e.message)
end
def breaker_for(model_id)
@circuit_breaker.respond_to?(:for) ? @circuit_breaker.for(model_id) : @circuit_breaker
end
def extract_response(reply, selected_model)
return reply.to_s unless reply.respond_to?(:content)
if NEMOTRON3_RE.match?(selected_model) && reply.respond_to?(:reasoning_content)
thinking = reply.reasoning_content.to_s.strip
content = reply.content.to_s
return thinking.empty? ? content : "#{content}\n\n<think>\n#{thinking}\n</think>"
end
reply.content.to_s
end
def nemotron_system_prompt(selected_model, base = nil)
sys = base || system_prompt
return sys unless LLAMA_NEMOTRON_RE.match?(selected_model)
directive = @config["reasoning_mode"] != "none" ? "detailed thinking on" : "detailed thinking off"
[directive, sys].compact.join("\n\n")
end
def cache_key_for(message, context, model = nil)
parts = model ? "#{model}\n#{message}" : message
return Digest::SHA256.hexdigest(parts) if context.empty?
window = context.last(CACHE_WINDOW).map { |msg| "#{msg[:role]}:#{msg[:content]}" }.join("\n")
Digest::SHA256.hexdigest("#{parts}\n#{window}")
end
def estimate_cost(prompt)
Master::Trace::Session.estimate_tokens(prompt) * COST_PER_TOKEN
end
def llm_tools(selected_model)
return [] unless tool_capable?(selected_model)
return build_llm_tools(visitor: true) if Fiber[:master_visitor]
@llm_tools ||= build_llm_tools
end
def build_llm_tools(visitor: false)
tier = @model_router&.tier_for_model(@config.model).to_s
@tools.filter_map do |tool|
wrapper = LLM_TOOL_MAP[tool.class]
next unless wrapper
name = tool.class.name.split("::").last
meta = @tool_registry.fetch(name, {})
next if visitor && meta["visitor"] != true
next if tier == "cheap" && meta["tier"] == "dangerous"
wrapper.new(tool)
end
rescue StandardError => err
@bus&.publish("agent:llm_tools_error", error: err.message)
[]
end
def load_tool_registry
path = File.join(Master::ROOT, "data", "tools.yml")
rows = Master.load_yaml(path)
return {} unless rows.is_a?(Array)
rows.each_with_object({}) { |row, h| h[row["name"].to_s] = row if row.is_a?(Hash) }
end
end
end
end# 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# 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# frozen_string_literal: true
module Master
module Judge
module Reflexion
MAX_REFLECTIONS = 3
TASK_TRUNCATE = 400
HISTORY_TRUNCATE = 200
BUDGET_SECONDS = 5 * 60
module_function
def run(agent:, task:, fast_model: nil, max: MAX_REFLECTIONS, budget_seconds: BUDGET_SECONDS)
last_result = nil
last_critique = nil
deadline = Time.now + budget_seconds
(max + 1).times do |i|
break if Time.now >= deadline
prompt = i.zero? ? task : build_revision_prompt(task, last_result, last_critique)
last_result = yield(prompt, i)
return last_result if last_result.is_a?(Master::Result) && last_result.ok?
break if i >= max
break if circuit_open?(agent)
last_critique = critique(agent:, task:, result: last_result, fast_model:)
end
last_result
end
def circuit_open?(agent)
breaker = agent.respond_to?(:circuit_breaker) ? agent.circuit_breaker : nil
return false unless breaker.respond_to?(:open_models)
!breaker.open_models.empty?
rescue StandardError
false
end
def critique(agent:, task:, result:, fast_model: nil)
prompt = <<~PROMPT
Task: #{task.to_s[0, TASK_TRUNCATE]}
Attempt output: #{result.to_s[0, TASK_TRUNCATE]}
What specifically went wrong? Name the constraint violated.
What must change in the next attempt? One paragraph, no preamble.
PROMPT
resp = fast_model ? agent.ask_once(prompt, model: fast_model) : agent.ask(prompt)
resp.respond_to?(:value!) ? resp.value! : resp.to_s
rescue StandardError => _e
"previous attempt failed — try a different approach"
end
def build_revision_prompt(task, previous_result, critique)
<<~PROMPT
#{task}
Previous attempt failed.
Critique: #{critique}
Previous output: #{previous_result.to_s[0, HISTORY_TRUNCATE]}
Revise based on the critique. Return only the corrected result.
PROMPT
end
end
end
end# frozen_string_literal: true
require "digest"
require "find"
require "set"
module Master
module Judge
# RepoEcology converts repo-gardening principles into executable analysis.
# It never deletes or rewrites; it emits evidence for later governed changes.
class RepoEcology
DEFAULT_IGNORE_DIRS = %w[
.git .master node_modules vendor tmp log coverage storage .bundle dist build
].freeze
DEFAULT_EXTENSIONS = %w[.rb .js .ts .erb .html .css .scss .yml .yaml .json .md .sh .zsh].freeze
LARGE_FILE_LINES = 420
DUPLICATE_BASENAME_LIMIT = 4
MAX_DEAD_CANDIDATES = 40
MAX_CLUSTERS = 20
def initialize(root:, event_bus: nil, ignore_dirs: DEFAULT_IGNORE_DIRS)
@root = File.expand_path(root)
@bus = event_bus
@ignore_dirs = ignore_dirs.to_set
end
def scan(path: nil)
base = path ? File.expand_path(path, @root) : @root
files = collect_files(base)
records = files.map { |file| analyze_file(file) }
scanned_utc = Time.now.utc
report = {
root: @root,
scanned_at: scanned_utc.iso8601,
files: records.size,
score: score(records),
dead_file_candidates: dead_file_candidates(records),
duplicate_basenames: duplicate_basenames(records),
similar_clusters: similar_clusters(records),
sprawl: sprawl(records),
large_files: large_files(records),
extension_mix: extension_mix(records)
}
@bus&.publish("repo_ecology:scan", files: records.size, score: report[:score])
report
end
def render(report)
lines = []
lines << "# Repo ecology"
lines << "score: #{report[:score][:grade]} (#{report[:score][:value]}/100)"
lines << "files: #{report[:files]}"
lines << ""
lines.concat(render_section("Dead-file candidates", report[:dead_file_candidates]) { |item|
"#{item[:path]} — #{item[:reason]}"
})
lines.concat(render_section("Duplicate basenames", report[:duplicate_basenames]) { |item|
"#{item[:basename]} ×#{item[:count]}: #{item[:paths].first(5).join(', ')}"
})
lines.concat(render_section("Similar clusters", report[:similar_clusters]) { |item|
"#{item[:signature]} ×#{item[:count]}: #{item[:paths].first(5).join(', ')}"
})
lines.concat(render_section("Large files", report[:large_files]) { |item|
"#{item[:path]} — #{item[:lines]} lines"
})
lines << ""
sprawl = report[:sprawl]
lines << "sprawl: max_depth=#{sprawl[:max_depth]}, avg_depth=#{sprawl[:avg_depth]}, " \
"orphan_dirs=#{sprawl[:orphan_dirs]}"
lines << "extensions: #{report[:extension_mix].map { |ext, count| "#{ext}=#{count}" }.join(', ')}"
lines.join("\n")
end
private
def collect_files(base)
return [] unless File.exist?(base)
files = []
Find.find(base) do |path|
name = File.basename(path)
if File.directory?(path)
Find.prune if @ignore_dirs.include?(name)
next
end
next unless DEFAULT_EXTENSIONS.include?(File.extname(path).downcase)
files << path
end
files.sort
end
def analyze_file(file)
rel = relative(file)
content = File.read(file, encoding: "UTF-8", invalid: :replace, undef: :replace)
tokens = content.downcase.scan(/[a-z][a-z0-9_]{2,}/)
{
path: rel,
full_path: file,
basename: File.basename(file),
dirname: File.dirname(rel),
ext: File.extname(file).downcase,
bytes: content.bytesize,
lines: content.lines.size,
tokens: tokens,
digest: Digest::SHA256.hexdigest(content),
signature: signature(tokens, rel),
inbound_refs: 0
}
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "repo_ecology.analyze_file", event_bus: @bus, path: file)
nil
end
def score(records)
records = records.compact
penalty = 0
penalty += duplicate_basenames(records).sum { |item| [item[:count] - DUPLICATE_BASENAME_LIMIT, 0].max * 3 }
penalty += similar_clusters(records).sum { |item| [item[:count] - 1, 0].max * 4 }
penalty += large_files(records).size * 2
penalty += sprawl(records)[:orphan_dirs] * 2
value = [[100 - penalty, 0].max, 100].min
{ value:, grade: grade_for(value) }
end
def grade_for(value)
return "excellent" if value >= 90
return "good" if value >= 75
return "strained" if value >= 55
"fragmented"
end
def dead_file_candidates(records)
records = records.compact
corpus = records.map { |record| [record[:path], record[:tokens].join(" ")] }.to_h
records.filter_map { |r| dead_candidate(r, corpus) }.first(MAX_DEAD_CANDIDATES)
end
def dead_candidate(record, corpus)
return nil if protected_path?(record[:path])
stem = File.basename(record[:basename], record[:ext]).downcase
inbound = corpus.count { |path, text| path != record[:path] && text.include?(stem) }
record[:inbound_refs] = inbound
return nil unless inbound.zero?
return nil if record[:lines] < 3
{ path: record[:path], reason: "no stem references found", lines: record[:lines] }
end
def protected_path?(path)
path == "README.md" || path == "AGENTS.md" || path.start_with?(".github/") ||
path.include?("/test/") || path.include?("/spec/") || path.end_with?("Gemfile")
end
def duplicate_basenames(records)
records.compact.group_by { |record| record[:basename] }
.filter_map do |basename, group|
next if group.size < DUPLICATE_BASENAME_LIMIT
{ basename:, count: group.size, paths: group.map { |record| record[:path] }.sort }
end.sort_by { |item| [-item[:count], item[:basename]] }
end
def similar_clusters(records)
records.compact.group_by { |record| record[:signature] }
.filter_map do |sig, group|
next if sig.empty? || group.size < 2
next if group.map { |record| record[:digest] }.uniq.size == group.size && group.size < 3
{ signature: sig, count: group.size, paths: group.map { |record| record[:path] }.sort }
end.sort_by { |item| [-item[:count], item[:signature]] }.first(MAX_CLUSTERS)
end
def signature(tokens, rel)
important = tokens.reject { |token| token.length < 4 || token.match?(/\A\d+\z/) }
vocabulary = important.tally.sort_by { |token, count| [-count, token] }.first(12).map(&:first)
return "" if vocabulary.size < 4
"#{File.extname(rel)}:#{vocabulary.sort.join('-')}"
end
def sprawl(records)
dirs = records.compact.map { |record| record[:dirname] }
depths = dirs.map { |dir| dir == "." ? 0 : dir.split(File::SEPARATOR).size }
counts = dirs.tally
{
max_depth: depths.max || 0,
avg_depth: depths.empty? ? 0 : (depths.sum.to_f / depths.size).round(2),
orphan_dirs: counts.count { |_dir, count| count == 1 }
}
end
def large_files(records)
records.compact.select { |record| record[:lines] >= LARGE_FILE_LINES }
.map { |record| { path: record[:path], lines: record[:lines] } }
.sort_by { |item| -item[:lines] }
.first(25)
end
def extension_mix(records)
records.compact.map { |record| record[:ext].empty? ? "[none]" : record[:ext] }
.tally.sort_by { |ext, count| [-count, ext] }.to_h
end
def render_section(title, items)
lines = ["", "## #{title}"]
if items.empty?
lines << "none"
else
items.first(12).each { |item| lines << "- #{yield(item)}" }
end
lines
end
def relative(file)
File.expand_path(file).sub(%r{\A#{Regexp.escape(@root)}/?}, "")
end
end
end
end# 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# frozen_string_literal: true
require "prism"
module Master
module Judge
module Scan
# Architecture #4: deterministic AST-level autofixes for mechanical rules.
# No LLM call, no token cost. Applied before LLM sweep on every scan cycle.
# Each transform is idempotent — safe to apply repeatedly.
class AstFixer
FROZEN_HEADER = "# frozen_string_literal: true\n"
BARE_RESCUE_RE = /^(\s*)rescue(\s*\n|\s*=>)/.freeze
Result = Struct.new(:path, :changed, :transforms, keyword_init: true)
# Apply all eligible fixes to +source+ for +path+.
# Returns Result with :changed (bool) and :transforms (array of applied fix names).
def self.fix(path, source)
new(path, source).apply
end
def initialize(path, source)
@path = path
@source = source
@transforms = []
end
def apply
out = @source
out = add_frozen_header(out) if ruby?
out = fix_bare_rescue(out) if ruby?
out = normalise_null_comparison(out) if sql_in_ruby?
changed = out != @source
Result.new(path: @path, changed: changed, transforms: @transforms)
.tap { write_back(out) if changed }
end
private
# Add frozen_string_literal comment if absent.
def add_frozen_header(src)
return src if src.start_with?(FROZEN_HEADER)
# Preserve shebang if present.
if src.start_with?("#!")
lines = src.lines
lines.insert(1, FROZEN_HEADER)
@transforms << :frozen_string_literal
lines.join
else
@transforms << :frozen_string_literal
FROZEN_HEADER + "\n" + src.lstrip
end
end
# Replace bare `rescue` with `rescue StandardError`.
# Only fires when Prism confirms the rescue is genuinely bare (no class listed).
def fix_bare_rescue(src)
result = Prism.parse(src)
return src unless result.success?
bare_lines = bare_rescue_lines(result.value)
return src if bare_lines.empty?
lines = src.lines
bare_lines.each do |lineno|
idx = lineno - 1
next unless idx < lines.size
lines[idx] = lines[idx].sub(/\brescue\b(?!\s+\w)/, "rescue StandardError")
end
@transforms << :bare_rescue
lines.join
end
# `= NULL` → `IS NULL`, `!= NULL` → `IS NOT NULL` inside SQL heredocs/strings.
# Skip scanner files — they carry `= NULL` literals inside detector regexes.
def normalise_null_comparison(src)
return src if @path.to_s.include?("/judge/scan/")
changed = false
out = src.gsub(/(?<![<>!])=\s*NULL\b/i) { changed = true; "IS NULL" }
.gsub(/!=\s*NULL\b/i) { changed = true; "IS NOT NULL" }
.gsub(/<>\s*NULL\b/i) { changed = true; "IS NOT NULL" }
@transforms << :null_comparison if changed
out
end
def bare_rescue_lines(node, lines = [])
return lines unless node.is_a?(Prism::Node)
if node.is_a?(Prism::RescueNode) && (node.exceptions.nil? || node.exceptions.empty?)
lines << node.location.start_line
end
node.child_nodes.compact.each { |child| bare_rescue_lines(child, lines) }
lines
end
def ruby? = File.extname(@path).downcase == ".rb"
def sql_in_ruby?
ruby? || %w[.sql .erb].include?(File.extname(@path).downcase)
end
def write_back(content)
tmp = "#{@path}.ast_fix.#{Process.pid}.tmp"
File.write(tmp, content, encoding: "UTF-8")
File.rename(tmp, @path)
rescue StandardError => e
File.delete(tmp) if defined?(tmp) && File.exist?(tmp) rescue nil
raise e
end
end
end
end
end# frozen_string_literal: true
module Master
module Judge
module Scan
# Architecture #12: Datalog/Prolog rule engine — logical Horn clause rules.
# Violations are queries against a fact base derived from the AST.
# Fixes are derived from the complement clause. No neural network involved.
#
# Status: scaffolded. Fact extraction is implemented for Ruby via Prism.
# Horn clause evaluation is a minimal forward-chaining Datalog subset.
# Full Prolog (negation, cuts) is future work.
class DatalogEngine
Fact = Struct.new(:predicate, :args, keyword_init: true)
Rule = Struct.new(:head, :body, keyword_init: true) # head :- body[]
Clause = Struct.new(:predicate, :args, keyword_init: true)
Finding = Struct.new(:rule_id, :fact, :message, keyword_init: true)
def initialize
@facts = []
@rules = []
end
def assert(predicate, *args)
@facts << Fact.new(predicate: predicate.to_sym, args: args)
self
end
def rule(head_pred, *body_preds, &action)
@rules << { head: head_pred, body: body_preds, action: action }
self
end
# Query: return all facts matching predicate + optional arg pattern.
def query(predicate, *pattern)
@facts.select do |f|
f.predicate == predicate.to_sym &&
pattern.each_with_index.all? { |p, i| p.nil? || f.args[i] == p }
end
end
# Forward-chain all rules once. Returns derived findings.
def evaluate
findings = []
@rules.each do |r|
body_matches = r[:body].map { |bp| query(bp) }
next if body_matches.any?(&:empty?)
body_matches.first.each do |fact|
findings << Finding.new(rule_id: r[:head], fact: fact,
message: r[:action]&.call(fact) || r[:head].to_s)
end
end
findings
end
# Extract facts from Ruby source via Prism. Returns a populated engine.
def self.from_ruby(path, source)
require "prism"
engine = new
result = Prism.parse(source)
return engine unless result.success?
extract_facts(result.value, path, engine)
engine
end
def self.extract_facts(node, path, engine)
return engine unless node.is_a?(Prism::Node)
case node
when Prism::DefNode
engine.assert(:method_def, path, node.name.to_s, node.location.start_line)
when Prism::RescueNode
if node.exceptions.nil? || node.exceptions.empty?
engine.assert(:bare_rescue, path, node.location.start_line)
end
when Prism::ClassNode
engine.assert(:class_def, path, node.constant_path.slice)
when Prism::CallNode
engine.assert(:call, path, node.name.to_s, node.location.start_line)
end
node.child_nodes.compact.each { |c| extract_facts(c, path, engine) }
engine
end
end
end
end
end# frozen_string_literal: true
module Master
module Judge
module Scan
class DetectionPipeline
def initialize(definition, agent: nil, root: nil)
@def = definition
@agent = agent
end
def run(ctx)
findings = []
file_type = guess_medium(ctx[:file_path])
@def.adapters.each do |adapter|
next unless adapter["medium"] == file_type || adapter["medium"] == "any"
detection = adapter["detection"]
if detection["lexical"]
findings.concat(run_lexical(detection["lexical"], ctx))
elsif detection["structural"]
findings.concat(run_structural(detection["structural"], ctx))
elsif detection["semantic"] && @agent
findings.concat(run_semantic(detection["semantic"], ctx))
end
end
findings
end
private
def guess_medium(path)
ext = File.extname(path.to_s).downcase
return "ruby" if ext == ".rb"
return "javascript" if [".js", ".ts"].include?(ext)
return "html" if [".html", ".erb"].include?(ext)
"any"
end
def run_lexical(cfg, ctx)
findings = []
cfg.each do |scope, conf|
next unless conf["pattern"]
pattern = Regexp.new(conf["pattern"])
if scope == "file_line" && ctx[:file_content]
ctx[:file_content].each_line.with_index(1) do |line, num|
if line =~ pattern
findings << finding(num, conf["message"] || @def.description)
end
end
end
end
findings
end
def run_structural(cfg, ctx)
findings = []
cfg.each do |scope, conf|
next unless conf["matcher"]
case conf["matcher"]
when "cyclomatic_complexity"
cc = calculate_cc(ctx[:file_content])
if cc && cc > conf["threshold"].to_i
findings << finding(1, "Cyclomatic complexity #{cc} exceeds #{conf["threshold"]}")
end
when "method_length"
max_len = max_method_length(ctx[:file_content])
if max_len && max_len > conf["threshold"].to_i
findings << finding(1, "Method has #{max_len} lines (max #{conf["threshold"]})")
end
when "nesting_depth"
depth = max_nesting(ctx[:file_content])
if depth && depth > conf["threshold"].to_i
findings << finding(1, "Nesting depth #{depth} exceeds #{conf["threshold"]}")
end
end
end
findings
end
def run_semantic(cfg, ctx)
findings = []
cfg.each do |scope, conf|
next unless conf["prompt"]
prompt = conf["prompt"]
response = @agent.ask(prompt, context: ctx[:file_content][0, 3000])
if response && response.strip.upcase != "CLEAN"
findings << finding(1, response.to_s[0, 200])
end
end
findings
end
def finding(line, message)
{ rule: @def.id, message: message, line: line, severity: @def.severity }
end
CC_NODES = %w[
IfNode UnlessNode WhileNode UntilNode ForNode
CaseNode WhenNode RescueNode AndNode OrNode
].map { |n| "Prism::#{n}" }.to_set.freeze
def calculate_cc(code)
result = Prism.parse(code)
return nil if result.failure?
count = 1
result.value.breadth_first_search { |node|
count += 1 if CC_NODES.include?(node.class.name)
false
}
count
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "detection_pipeline.cyclomatic")
nil
end
def max_method_length(code)
result = Prism.parse(code)
return nil if result.failure?
max = 0
result.value.breadth_first_search { |node|
if node.is_a?(Prism::DefNode)
len = node.location.end_line - node.location.start_line
max = len if len > max
end
false
}
max.zero? ? nil : max
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "detection_pipeline.max_method_length")
nil
end
def max_nesting(code)
result = Prism.parse(code)
return nil if result.failure?
max_depth(result.value, 0)
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "detection_pipeline.max_nesting")
nil
end
NESTING_NODES = [
Prism::ModuleNode, Prism::ClassNode, Prism::DefNode,
Prism::IfNode, Prism::WhileNode, Prism::CaseNode
].freeze
def max_depth(node, depth)
return depth unless node.respond_to?(:child_nodes)
child_depth = NESTING_NODES.include?(node.class) ? depth + 1 : depth
children = node.child_nodes
children.compact.reduce(child_depth) { |m, c| [m, max_depth(c, child_depth)].max }
end
end
end
end
end# frozen_string_literal: true
module Master
module Judge
module Scan
Finding = Data.define(:rule, :message, :line, :severity, :fix, :tags) do
def self.build(rule:, message:, line:, severity: :warning, fix: nil, tags: [])
new(rule:, message:, line:, severity:, fix:, tags:)
end
def [](key)
public_send(key)
end
def to_h
{ rule:, message:, line:, severity:, fix:, tags: }
end
def merge(extras) = to_h.merge(extras)
end
end
end
end# 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# frozen_string_literal: true
module Master
module Judge
module Scan
# Inline Ruby rule definition — JE-style alternative to rules.yml entries.
# Defined rules auto-register via Rule.inherited; no YAML required.
#
# RuleDSL.rule :NO_PUTS, severity: :warning, applies_to: %i[ruby] do |src, path:|
# scan_lines(src, /\bputs\b/, message: "puts in production code")
# end
module RuleDSL
def self.rule(id, severity: :warning, tags: [], applies_to: nil, autofix: true, description: nil, &block)
raise ArgumentError, "block required" unless block
dsl_id = id.to_s.downcase
dsl_desc = description || dsl_id.tr("_", " ")
dsl_tags = Array(tags)
Class.new(Rule) do
@dsl_block = block
@dsl_langs = applies_to
@dsl_autofix = autofix
class << self; attr_reader :dsl_block, :dsl_langs, :dsl_autofix; end
define_method(:initialize) do
super()
@id = dsl_id; @description = dsl_desc
@severity = severity; @rule_tags = dsl_tags; @auto_fix = autofix
end
define_method(:check) do |code, path:|
langs = self.class.dsl_langs
return [] if langs && !langs.include?(language(path)&.to_sym)
instance_exec(code, path: path, &self.class.dsl_block)
end
end
end
end
end
end
end
require_relative "rules/lexical_rules"
require_relative "rules/ruby_rules"
require_relative "rules/web_rules"
require_relative "rules/js_rules"
require_relative "rules/universal_rules"# frozen_string_literal: true
module Master
module Judge
module Scan
module Rules
# Steelman-first red-team: the model must defend the code before it can attack it.
# This suppresses false positives by forcing consideration of legitimate reasons
# before a violation can survive. Deep depth only; one LLM call per file.
class AdversarialRule < Rule
PROMPT_TEMPLATE = <<~PROMPT.freeze
Red-team review of %<path>s.
Step 1 — Steelman (internal, do not output): write the three strongest
arguments that this code is correct and should not be changed.
Step 2 — Challenge: list only the violations that survive the steelman.
Format: ISSUE:LINE:description (one per line).
If nothing survives, respond with exactly: CLEAN
Focus on: broken contracts, hidden coupling, axiom violations (CQS,
ONE_JOB, GUARD_EXPENSIVE, FAIL_VISIBLY), and logic errors.
Ignore style. Do not hallucinate method names.
Code (%<lang>s):
%<code>s
PROMPT
def initialize(agent: nil)
super()
@agent = agent
@id = "adversarial"
@description = "Red-team scan: steelman then challenge — suppresses false positives"
@severity = :error
@rule_tags = %i[ONE_JOB CQS GUARD_EXPENSIVE FAIL_VISIBLY COMPOSABLE]
end
def self.auto_build? = false
def set_agent(agent)
@agent = agent
self
end
def check(code, path:)
return [] unless (lang = language(path))
return [] unless @agent
prompt = format(PROMPT_TEMPLATE, path: File.basename(path),
lang: lang,
code: code[0, 3_000])
response = @agent.ask(prompt, operation: :scan_adversarial).to_s
parse_findings(response)
rescue StandardError => e
return [] if e.message.to_s =~ /missing configuration|api.?key|unauthorized|no.*provider/i
[finding(line: 1, message: "adversarial: scan error — #{e.message}")]
end
private
def parse_findings(response)
response_normalized = response.strip.upcase
return [] if response_normalized.start_with?("CLEAN")
response.lines.filter_map do |line|
match = line.strip.match(/\AISSUE:(\d+):(.+)\z/)
next unless match
finding(line: match[1].to_i, message: "adversarial: #{match[2].strip}")
end
end
end
end
end
end
end# frozen_string_literal: true
module Master
module Judge
module Scan
module Rules
# Detects methods/classes/modules present in recent git history but absent now.
# Wraps CommitGuard as a standard scan Rule so it runs in the scanner pipeline.
class AstOmissionRule < Rule
def self.auto_build? = false
def initialize(root: Dir.pwd, depth: CommitGuard::DEFAULT_DEPTH)
super()
@id = "ast_omission"; @description = "symbol dropped vs recent commits"
@severity = :warning; @rule_tags = %i[COMPLETENESS]; @auto_fix = false
@guard = CommitGuard.new(root:, depth:)
end
def check(code, path:)
return [] unless path.to_s.end_with?(".rb")
omissions = @guard.check(paths: [File.basename(path)])
omissions.map { |o| finding(line: 1, message: "#{o.type} #{o.name} dropped (last seen #{o.last_seen_at})") }
rescue StandardError => _e
[]
end
end
end
end
end
end# frozen_string_literal: true
require "open3"
module Master
module Judge
module Scan
module Rules
# Files that change together in many commits are coupled regardless of imports.
# Mines the last N commits, builds adjacency, flags pairs whose weight exceeds
# threshold and that live in different top-level module paths — likely DECOUPLE
# candidates the lexical rules can't see.
class CoChangeCouplingRule < Rule
COMMITS_WINDOW = 500
WEIGHT_THRESHOLD = 5
# Skip mega-commits — they pollute the graph.
MAX_FILES_IN_COMMIT = 12
def initialize
super
@id = "co_change_coupling"
@description = "Files co-change with N+ peers across module boundaries — hidden coupling"
@severity = :info
@rule_tags = %i[DECOUPLE ONE_JOB]
@graph_mutex = Mutex.new
@graph = nil
end
def check(_code, path:)
return [] unless path.end_with?(".rb")
rel = relativize(path)
return [] unless rel
peers = neighbors(rel).reject { |peer, _| same_module?(rel, peer) }
.select { |_, w| w >= WEIGHT_THRESHOLD }
.sort_by { |_, w| -w }
.first(3)
return [] if peers.empty?
msg = "co-changes with " + peers.map { |p, w| "#{p} (#{w}x)" }.join(", ")
[finding(line: 1, message: msg)]
end
private
def neighbors(rel)
graph[rel] || {}
end
def graph
@graph_mutex.synchronize { @graph ||= build_graph }
end
COMMIT_SEPARATOR = "===commit===".freeze
def build_graph
out, _, status = Open3.capture3("git", "-C", repo_root,
"log", "--name-only",
"--pretty=format:#{COMMIT_SEPARATOR}",
"-n", COMMITS_WINDOW.to_s, "--", "*.rb")
return {} unless status.success?
adjacency = Hash.new { |h, k| h[k] = Hash.new(0) }
out.split(COMMIT_SEPARATOR).each do |chunk|
files = chunk.lines.map(&:strip).reject(&:empty?).select { |f| f.end_with?(".rb") }
next if files.size < 2 || files.size > MAX_FILES_IN_COMMIT
files.combination(2) do |a, b|
adjacency[a][b] += 1
adjacency[b][a] += 1
end
end
adjacency
end
def repo_root
@repo_root ||= File.expand_path(File.join(Master::ROOT, ".."))
end
def relativize(path)
full = File.expand_path(path)
prefix = repo_root + "/"
full.start_with?(prefix) ? full.delete_prefix(prefix) : nil
end
def same_module?(a, b) = module_of(a) == module_of(b)
def module_of(path)
parts = path.split("/")
# MASTER/lib/<module>/... → use the module dir (judge/trace/etc.)
parts[0] == "MASTER" ? (parts[2] || parts[0]) : parts[0]
end
end
end
end
end
end# frozen_string_literal: true
module Master
module Judge
module Scan
module Rules
# Comments above method defs that no longer describe what the code does.
# Lexical pass extracts (comment, method_body) pairs; LLM judges drift in one
# batched call per file. Pairs with the "reassess on touch" directive: lying
# comments are factual bugs, not style noise.
class CommentDriftRule < Rule
MAX_PAIRS_PER_FILE = 8
# Lines of method body sent to LLM for drift comparison.
BODY_SNIPPET = 20
def initialize(agent: nil)
super()
@agent = agent
@id = "comment_drift"
@description = "Comment claim doesn't match method body — comment is lying"
@severity = :warning
@rule_tags = %i[SELF_EXPLAINING EXPLICIT]
end
def self.auto_build? = false
def set_agent(agent)
@agent = agent
self
end
def check(code, path:)
return [] unless path.end_with?(".rb") && @agent
pairs = extract_pairs(code)
return [] if pairs.empty?
response = @agent.ask(build_prompt(pairs, path), operation: :scan_comment_drift).to_s
parse_findings(response, pairs)
rescue StandardError => _e
[]
end
private
def extract_pairs(code)
lines = code.lines
pairs = []
i = 0
while i < lines.size && pairs.size < MAX_PAIRS_PER_FILE
comment_start = i
while i < lines.size && lines[i] =~ /\A\s*#/
i += 1
end
comment_lines = lines[comment_start...i]
if comment_lines.any? && i < lines.size && lines[i] =~ /\A\s*def\s/
comment_text = comment_lines.map { |l| l.strip.delete_prefix("#").strip }.join(" ")
body = lines[i, BODY_SNIPPET].join
pairs << { line: comment_start + 1, comment: comment_text, body: body } unless comment_text.empty?
end
i += 1
end
pairs
end
def format_pair(p, idx)
"[#{idx}] line #{p[:line]}\nCOMMENT: #{p[:comment]}\nCODE:\n#{p[:body]}"
end
def build_prompt(pairs, path)
numbered = pairs.each_with_index.map { |p, idx| format_pair(p, idx) }.join("\n---\n")
<<~PROMPT
Audit #{File.basename(path)} for comment drift. For each numbered pair,
decide whether the comment accurately describes what the code does.
List ONLY indices where the comment lies or contradicts the code.
Format each violation: INDEX:short reason (one per line)
If all pairs are accurate, respond with exactly: CLEAN
#{numbered}
PROMPT
end
def parse_findings(response, pairs)
return [] if response.strip.upcase == "CLEAN"
response.lines.filter_map do |line|
match = line.strip.match(/\A(\d+):(.+)\z/)
next unless match
idx = match[1].to_i
pair = pairs[idx]
next unless pair
finding(line: pair[:line], message: "comment drift — #{match[2].strip}")
end
end
end
end
end
end
end# 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# frozen_string_literal: true
module Master
module Judge
module Scan
module Rules
RuleDSL.rule :CONST_BY_DEFAULT,
severity: :warning, tags: %i[IMMUTABLE], applies_to: %i[javascript],
description: "use const unless reassigned" do |src, path:|
scan_lines(src, /\blet\s+(\w+)\s*=/, message: "let — use const unless value is reassigned")
end
RuleDSL.rule :NULLISH_COALESCING,
severity: :info, tags: %i[READABILITY], applies_to: %i[javascript],
description: "use ?? over || for defaults" do |src, path:|
scan_lines(src, /(\w+)\s*\|\|\s*\w+/, message: "foo || default — use foo ?? default to avoid falsy traps")
end
RuleDSL.rule :TEMPLATE_LITERALS,
severity: :warning, tags: %i[READABILITY], applies_to: %i[javascript],
description: "use template literals over concatenation" do |src, path:|
scan_lines(src, /["']\s*\+\s*\w+\s*\+\s*["']/,
message: "string concatenation — use template literal \`…\${var}…\`")
end
RuleDSL.rule :ASYNC_AWAIT,
severity: :warning, tags: %i[READABILITY], applies_to: %i[javascript],
description: "prefer async/await over .then chains" do |src, path:|
scan_lines(src, /\.then\(.*\.then\(.*\.then\(/, message: "3+ .then chain — convert to async/await")
end
RuleDSL.rule :FOR_OF,
severity: :error, tags: %i[CORRECTNESS], applies_to: %i[javascript],
description: "use for...of instead of for...in for arrays" do |src, path:|
scan_lines(src, /for\s*\(\s*(const|let|var)\s+\w+\s+in\s+/,
message: "for...in iterates keys — use for...of for array values")
end
RuleDSL.rule :QUOTE_VARIABLES,
severity: :error, tags: %i[ROBUSTNESS], applies_to: %i[zsh],
description: "always quote $variables" do |src, path:|
scan_lines(src, /(?<!["'\\])\$\w+(?!["'])/, message: "unquoted $variable — wrap in double quotes")
end
RuleDSL.rule :DOUBLE_BRACKET,
severity: :warning, tags: %i[ROBUSTNESS], applies_to: %i[zsh],
description: "use [[ ]] over [ ]" do |src, path:|
scan_lines(src, /(?<!\[)\[\s+[^\[]/, message: "[ ] test — use [[ ]] in zsh")
end
RuleDSL.rule :DOLLAR_PAREN,
severity: :warning, tags: %i[READABILITY], applies_to: %i[zsh],
description: "replace backticks with $(command)" do |src, path:|
scan_lines(src, /`[^`]+`/, message: "backtick substitution — use $(command) for clarity")
end
end
end
end
end# frozen_string_literal: true
module Master
module Judge
module Scan
module Rules
# Lexical rules defined via RuleDSL — pure Ruby, no YAML.
# Each auto-registers in Rule.registry and runs on every scan.
RuleDSL.rule :NO_DEBUG,
severity: :error, tags: %i[CLEAN_CODE], applies_to: %i[ruby],
description: "no debug breakpoints in committed code" do |src, path:|
next [] if path.to_s.include?("/judge/scan/rules/")
scan_lines(src, /\b(binding\.pry|debugger|byebug|binding\.irb)\b/, message: "debug breakpoint")
end
RuleDSL.rule :NO_PUTS,
severity: :warning, tags: %i[CLEAN_CODE], applies_to: %i[ruby],
description: "no bare puts in library code" do |src, path:|
next [] if path.to_s.match?(%r{/exe/|/spec/|/bin/|/now/cli})
scan_lines(src, /^\s*puts\b(?!\s*\()/, message: "bare puts — use event bus or logger")
end
RuleDSL.rule :FROZEN_LITERAL,
severity: :warning, tags: %i[PERFORMANCE], applies_to: %i[ruby],
description: "missing frozen_string_literal magic comment" do |src, path:|
next [] if src.lines.first&.include?("frozen_string_literal")
magic = "# frozen_string_literal: true"
[finding(line: 1, message: "add #{magic}")]
end
RuleDSL.rule :LONG_LINE,
severity: :info, tags: %i[READABILITY], autofix: false,
description: "lines exceeding 120 characters" do |src, path:|
next [] if path.to_s.match?(%r{/voice/personality\.rb|/reach/llm\.rb})
src.each_line.with_index(1).filter_map { |line, n|
finding(line: n, message: "line #{line.chomp.length} chars (max 120)") if line.chomp.length > 120
}
end
RuleDSL.rule :TRAILING_WHITESPACE,
severity: :info, tags: %i[HYGIENE],
description: "trailing whitespace" do |src, path:|
src.each_line.with_index(1).filter_map { |line, n|
finding(line: n, message: "trailing whitespace") if line.match?(/[ \t]+\n?\z/)
}
end
RuleDSL.rule :TODO_FIXME,
severity: :info, tags: %i[COMPLETENESS], autofix: false,
description: "unresolved work markers" do |src, path:|
next [] if path.to_s.include?("/judge/scan/rules/")
scan_lines(src, /\b(TODO|FIXME|HACK|XXX)\b/, message: "unresolved marker — resolve or delete")
end
RuleDSL.rule :RESCUE_EXCEPTION,
severity: :warning, tags: %i[ERROR_HANDLING], applies_to: %i[ruby],
description: "rescue StandardError not Exception" do |src, path:|
scan_lines(src, /rescue\s+Exception\b/, message: "catches signals — use StandardError")
end
RuleDSL.rule :EMPTY_RESCUE,
severity: :error, tags: %i[ERROR_HANDLING FAIL_VISIBLY], applies_to: %i[ruby],
description: "empty rescue swallows errors silently" do |src, path:|
src.each_line.with_index(1).filter_map { |line, n|
bare_rescue = line.match?(/^\s*rescue\s*$/)
naked_class = line.match?(/^\s*rescue\s+\S+\s*$/) && !line.match?(/=>/)
finding(line: n, message: "empty rescue — use Ground::Swallow.log or re-raise") if bare_rescue || naked_class
}
end
# Regexes hoisted out of the per-call hot path (HOIST).
RESCUE_DISCARD = /\A(nil|false|0|\[\]|\{\}|next|return|return\s+(nil|false|0|\[\]|\{\})|raise)?\z/
RESCUE_SINK = /\b(raise\b|Swallow\.log|\.publish\b|\bwarn\b|\blog\b|Diag\b|Result\.err)/
RESCUE_HEAD = /^(\s*)rescue\b([^;#]*?)(?:=>\s*(\w+))?\s*(?:;\s*(.*))?$/
# 1-based line numbers of rescues whose handler discards the error.
def self.silent_rescue_lines(src, blanket_only:)
lines = src.lines
lines.each_with_index.filter_map do |line, idx|
m = RESCUE_HEAD.match(line)
next unless m && rescue_in_scope?(m[2].to_s.strip, blanket_only)
body = rescue_body(lines, idx, m[4].to_s.strip)
next unless rescue_silent?(body, m[3])
[idx + 1, m[3], m[2].to_s.strip]
end
end
# Blanket = bare rescue or StandardError/Exception only; narrow = a named class.
def self.rescue_in_scope?(classes, blanket_only)
blanket = classes.empty? ||
classes.split(",").map(&:strip).all? { |c| %w[StandardError Exception].include?(c) }
blanket_only ? blanket : !blanket
end
# Handler body — the inline `; expr` form, or lines down to the matching end.
def self.rescue_body(lines, idx, inline)
return [inline] unless inline.empty?
collected = []
((idx + 1)...lines.size).each do |j|
stripped = lines[j].strip
break if stripped.match?(/\A(end|else|ensure|rescue)\b/)
collected << stripped unless stripped.empty? || stripped.start_with?("#")
end
collected
end
# Silent: body discards, never names the error, never reaches a sink.
def self.rescue_silent?(body, err_name)
return false unless body.reject { |b| b.match?(RESCUE_DISCARD) }.empty?
return false if err_name && body.any? { |b| b.match?(/\b#{Regexp.escape(err_name)}\b/) }
body.none? { |b| b.match?(RESCUE_SINK) }
end
RuleDSL.rule :SILENT_RESCUE,
severity: :error, tags: %i[ERROR_HANDLING FAIL_VISIBLY], applies_to: %i[ruby],
autofix: false,
description: "blanket rescue discards the error instead of logging or re-raising" do |src, path:|
next [] if path.to_s.include?("/judge/scan/rules/")
Rules.silent_rescue_lines(src, blanket_only: true).map do |line, err_name, _classes|
hint = err_name ? "rescue binds #{err_name} but never uses it" : "blanket rescue discards the error"
finding(line:, message: "#{hint} — use Ground::Swallow.log or re-raise")
end
end
RuleDSL.rule :NARROW_SILENT_RESCUE,
severity: :warning, tags: %i[ERROR_HANDLING FAIL_VISIBLY], applies_to: %i[ruby],
autofix: false,
description: "narrow rescue discards the error — confirm the case is truly expected" do |src, path:|
next [] if path.to_s.include?("/judge/scan/rules/")
Rules.silent_rescue_lines(src, blanket_only: false).map do |line, _err_name, classes|
finding(line:, message: "rescue #{classes} discards the error — " \
"Ground::Swallow.log it if expected, re-raise if not")
end
end
RuleDSL.rule :CONSECUTIVE_BLANK_LINES,
severity: :info, tags: %i[HYGIENE],
description: "no consecutive blank lines" do |src, path:|
findings = []
prev_blank = false
src.each_line.with_index(1) { |line, n|
blank = line.strip.empty?
findings << finding(line: n, message: "consecutive blank line") if blank && prev_blank
prev_blank = blank
}
findings
end
RuleDSL.rule :DEBUG_OUTPUT,
severity: :error, tags: %i[FAIL_VISIBLY], applies_to: %i[ruby],
description: "debug output left in lib/" do |src, path:|
next [] unless path.to_s.include?("/lib/")
next [] if path.to_s.include?("/judge/scan/rules/")
findings = scan_lines(src, /^\s*pp?\s+(?!self\b)/, message: "p/pp debug call — remove or publish via event bus")
findings += scan_lines(src, /\$stderr\.puts\b/, message: "$stderr.puts — use @bus.publish or $stdout")
findings
end
RuleDSL.rule :TRAILING_COMMENT,
severity: :info, tags: %i[BE_CONCISE],
description: "trailing comment after code" do |src, path:|
src.each_line.with_index(1).filter_map { |line, n|
next if line.strip.start_with?("#")
finding(line: n, message: "trailing comment — promote above the line or delete") if line.match?(/\S\s+#\s+\S/)
}
end
RuleDSL.rule :TIME_ZONE_UNSAFE,
severity: :warning, tags: %i[ROBUSTNESS], applies_to: %i[ruby],
description: "bare Time.now/Date.today bypasses Rails Time.zone" do |src, path:|
next [] unless path.match?(%r{/app/|/spec/|/test/})
findings = scan_lines(src, /(?<![A-Za-z_.])Time\.now\b/,
message: "Time.now ignores Time.zone — use Time.current")
findings += scan_lines(src, /(?<![A-Za-z_.])Date\.today\b/,
message: "Date.today ignores Time.zone — use Date.current")
findings += scan_lines(src, /(?<![A-Za-z_.])DateTime\.now\b/,
message: "DateTime.now — use Time.current.to_datetime")
findings
end
RuleDSL.rule :NO_ASCII_LINE_ART,
severity: :warning, tags: %i[BE_CONCISE],
description: "ASCII divider decorations" do |src, path:|
scan_lines(src, /(?:^|\s)(?:={3,}|-{3,})(?:\s|$)/, message: "remove ASCII divider decorations")
end
end
end
end
end# frozen_string_literal: true
require "open3"
require "json"
module Master
module Judge
module Scan
module Rules
# Runs Reek on .rb files and surfaces code smell violations.
class ReekRule < Rule
def self.auto_build? = false
def initialize(root:)
super()
@id = "reek"
@description = "Reek code smell detected"
@severity = :warning
@auto_fix = false
@rule_tags = %i[SMELL ONE_JOB]
@root = root
end
def check(code, path:)
return [] unless path.end_with?(".rb") && reek_available?
stdout, _stderr, _status = Open3.capture3(
Master::BUNDLE_BIN, "exec", "reek", "--format", "json", path,
chdir: @root
)
return [] if stdout.empty?
smells = begin; JSON.parse(stdout); rescue JSON::ParserError; return []; end
smells.flat_map do |smell|
locations = smell["lines"] || [1]
locations.map do |line|
finding(
line: line,
message: "reek: #{smell["smell_type"]} — #{smell["message"]}"
)
end
end
rescue StandardError => e
[finding(line: 1, message: "reek: scan error — #{e.message}")]
end
private
def reek_available?
system("which reek > /dev/null 2>&1") ||
File.exist?(File.join(@root, "bin", "reek"))
end
end
end
end
end
end# frozen_string_literal: true
require "open3"
require "json"
module Master
module Judge
module Scan
module Rules
# Runs RuboCop on .rb files and surfaces violations as findings.
class RubocopRule < Rule
def self.auto_build? = false
def initialize(root:)
super()
@id = "rubocop"
@description = "RuboCop style/lint violation"
@severity = :warning
@auto_fix = false
@rule_tags = %i[STYLE LINT]
@root = root
end
def check(code, path:)
return [] unless path.end_with?(".rb") && rubocop_available?
stdout, _stderr, status = Open3.capture3(
Master::BUNDLE_BIN, "exec", "rubocop", "--format", "json", "--no-color", path,
chdir: @root
)
return [] if stdout.empty?
data = begin; JSON.parse(stdout); rescue JSON::ParserError; return []; end
offenses = data.dig("files", 0, "offenses") || []
offenses.map do |o|
finding(
line: o.dig("location", "line") || 1,
message: "rubocop: #{o["cop_name"]} — #{o["message"]}"
)
end
rescue StandardError => e
[finding(line: 1, message: "rubocop: scan error — #{e.message}")]
end
private
def rubocop_available?
system("which rubocop > /dev/null 2>&1") ||
File.exist?(File.join(@root, "bin", "rubocop"))
end
end
end
end
end
end# frozen_string_literal: true
module Master
module Judge
module Scan
module Rules
RuleDSL.rule :SINGLE_PRIVATE_SECTION,
severity: :info, tags: %i[SMALL_PARTS], applies_to: %i[ruby],
description: "one private section at bottom" do |src, path:|
scan_lines(src, /private\s+:\w+/, message: "inline private call — gather private methods at bottom")
end
RuleDSL.rule :STRICT_LOADING_MISSING,
severity: :info, tags: %i[PERFORMANCE], applies_to: %i[ruby],
description: "AR model lacks strict_loading_by_default" do |src, path:|
next [] unless path.include?("/app/models/")
next [] if src.match?(/\bstrict_loading_by_default\b/)
next [] unless src.match?(/class\s+\w+\s+<\s+(?:ApplicationRecord|ActiveRecord::Base)\b/)
[finding(line: 1, message: "model missing strict_loading_by_default true")]
end
RuleDSL.rule :RATE_LIMITING_MISSING,
severity: :error, tags: %i[SECURITY], applies_to: %i[ruby],
description: "sensitive controller missing rate_limit/throttle" do |src, path:|
next [] unless path.include?("/app/controllers/")
next [] if src.match?(/rate_limit|throttle/)
next [] unless src.match?(/(login|signup|sign_up|password|reset)/)
[finding(line: 1, message: "auth/sensitive controller missing rate_limit or throttle")]
end
RuleDSL.rule :MIGRATION_ADD_REFERENCE_NO_FK,
severity: :error, tags: %i[DATA_INTEGRITY], applies_to: %i[ruby],
description: "add_reference without foreign_key:" do |src, path:|
next [] unless path.include?("/db/migrate/")
scan_lines(src, /add_reference(?!.*foreign_key:)/,
message: "add_reference without foreign_key: true — data integrity risk")
end
RuleDSL.rule :MIGRATION_REMOVE_COLUMN,
severity: :error, tags: %i[DATA_INTEGRITY], applies_to: %i[ruby],
description: "remove_column is destructive" do |src, path:|
next [] unless path.include?("/db/migrate/")
scan_lines(src, /remove_column/,
message: "remove_column is destructive — confirm column is unused across all deploys")
end
RuleDSL.rule :MIGRATION_FIND_OR_CREATE_BY,
severity: :warning, tags: %i[DATA_INTEGRITY], applies_to: %i[ruby],
description: "find_or_create_by needs unique index" do |src, path:|
next [] unless path.include?("/db/migrate/")
scan_lines(src, /find_or_create_by/, message: "find_or_create_by is not atomic without a unique index")
end
RuleDSL.rule :EACH_WITH_OBJECT,
severity: :warning, tags: %i[READABILITY], applies_to: %i[ruby],
description: "prefer each_with_object over inject for hash building" do |src, path:|
scan_lines(src, /\.(inject|reduce)\(\s*\{\s*\}\s*\)/, message: "use each_with_object({}) over inject({})")
end
RuleDSL.rule :KERNEL_COERCION,
severity: :info, tags: %i[READABILITY], applies_to: %i[ruby],
description: "use Array(), Hash(), String() coercions" do |src, path:|
next [] if path.to_s.include?("/judge/scan/rules/")
scan_lines(src, /(\w+)\s*\|\|\s*\[\](?!\s*<<)/,
message: "nil-or-empty array — prefer Array(foo) for nil-safe coercion")
end
RuleDSL.rule :PERCENT_LITERAL,
severity: :info, tags: %i[READABILITY], applies_to: %i[ruby],
description: "use %i[] and %w[] for symbol/string arrays" do |src, path:|
scan_lines(src, /\[:[a-z_]+,\s*:[a-z_]+,\s*:[a-z_]+/,
message: "use %i[...] for symbol arrays")
end
RuleDSL.rule :HASH_FETCH,
severity: :info, tags: %i[READABILITY], applies_to: %i[ruby],
description: "prefer Hash#fetch over [] with ||" do |src, path:|
next [] if path.to_s.include?("/judge/scan/rules/")
scan_lines(src, /\w+\[:\w+\]\s*\|\|/,
message: "hash symbol lookup with fallback — prefer hash.fetch(:key, default)")
end
RuleDSL.rule :TRANSFORM_KEYS,
severity: :info, tags: %i[READABILITY], applies_to: %i[ruby],
description: "use transform_keys/transform_values" do |src, path:|
scan_lines(src, /\.each_with_object\(\{\}\)\s*\{\s*\|\(k,\s*v\),\s*h\|/,
message: "use transform_keys/transform_values instead of manual each_with_object")
end
RuleDSL.rule :IMMUTABLE,
severity: :info, tags: %i[PERFORMANCE], applies_to: %i[ruby],
description: "default to immutable data" do |src, path:|
src.each_line.with_index(1).filter_map do |line, n|
next unless line.match?(/^\s*[A-Z][A-Z_]*\s*=\s*[\[{].*[\]}]/)
next if line.match?(/\.freeze/)
finding(line: n, message: "mutable constant — append .freeze")
end
end
RuleDSL.rule :FIND_EACH,
severity: :warning, tags: %i[PERFORMANCE], applies_to: %i[ruby],
description: "use find_each for batch processing" do |src, path:|
next [] unless path.match?(%r{/app/|/spec/|/test/})
scan_lines(src, /\.(all\.each|where\(.*\)\.each)\b/,
message: "unbounded .all.each — use find_each(batch_size: N)")
end
RuleDSL.rule :NO_UPDATE_ATTRIBUTE,
severity: :error, tags: %i[DATA_INTEGRITY], applies_to: %i[ruby],
description: "replace update_attribute with update!" do |src, path:|
next [] unless path.match?(%r{/app/|/spec/|/test/})
scan_lines(src, /\.update_attribute\(/, message: "update_attribute skips validations — use update!")
end
RuleDSL.rule :PLUCK_OVER_MAP,
severity: :info, tags: %i[PERFORMANCE], applies_to: %i[ruby],
description: "prefer pluck over map for single columns" do |src, path:|
next [] unless path.match?(%r{/app/|/spec/|/test/})
scan_lines(src, /\.\w+\.map\(&:\w+\)/, message: "use pluck(:column) to avoid loading full objects")
end
RuleDSL.rule :DISCARD_RESCUE,
severity: :error, tags: %i[CORRECTNESS], applies_to: %i[ruby],
description: "rescue with silent discard hides failures" do |src, path:|
src.each_line.with_index(1).filter_map do |line, n|
next unless line.match?(/rescue\s*(StandardError|Exception|=>?\s*_e?\s*$)/)
next unless line.match?(/nil$|^\s*end\s*$/) || src.lines[n]&.match?(/^\s*(nil|#\s*noop|{}|next|return)\s*$/)
finding(line: n, message: "rescue discards exception — log via Swallow or re-raise")
end
end
end
end
end
end# 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# frozen_string_literal: true
module Master
module Judge
module Scan
module Rules
require_relative "../finding"
# LLM review for rules whose violations resist lexical detection.
# Each rules.yml entry with a detect_semantic prompt is folded into one LLM
# call per file. Rules carry mode: violation (default) or opportunity —
# the prompt frame and severity follow from that.
class SemanticRule < Rule
CODE_SNIPPET_LIMIT = 2000
def initialize(agent: nil)
super()
@agent = agent
@id = "semantic"
@description = "LLM-based rule review (violations + opportunities)"
@severity = :warning
@rules = load_semantic_rules
@rule_tags = @rules.keys.map(&:to_sym)
end
def self.auto_build? = false
def set_agent(agent)
@agent = agent
self
end
def check(code, path:)
return [] unless language(path) && @agent
response = @agent.ask(build_prompt(code, path), operation: :scan_semantic).to_s
parse_findings(response)
rescue StandardError => e
return [] if e.message.to_s =~ /missing configuration|api.?key|unauthorized|no.*provider/i
[finding(line: 1, message: "semantic: scan error — #{e.message}")]
end
private
# Each axiom is { prompt:, severity:, mode: }. info-tier violations stay
# out of the prompt — they're noise that doubles cost. info-tier
# opportunities stay in: that's their whole point.
def load_semantic_rules
data = Master.load_rules
(data["rules"] || {}).values.flatten
.select { |r| r["detect_semantic"] }
.reject { |r| r["severity"] == "info" && r["mode"] != "opportunity" && r["tier"] != "kernel" }
.each_with_object({}) do |r, h|
h[r["id"]] = {
prompt: r["detect_semantic"],
severity: (r["severity"] || "warning").to_sym,
mode: (r["mode"] || "violation").to_sym
}
end
end
def build_prompt(code, path)
violations = @rules.select { |_, a| a[:mode] == :violation }
opportunities = @rules.select { |_, a| a[:mode] == :opportunity }
parts = []
parts << violation_block(violations) unless violations.empty?
parts << opportunity_block(opportunities) unless opportunities.empty?
<<~PROMPT
Review #{File.basename(path)}.
#{parts.join("\n\n")}
Code (first #{CODE_SNIPPET_LIMIT} chars):
#{code[0, CODE_SNIPPET_LIMIT]}
PROMPT
end
def violation_block(rules)
list = rules.map { |id, a| "#{id}: #{a[:prompt]}" }.join("\n")
<<~BLOCK
VIOLATIONS — list ONLY clear breaches. Format: RULE_ID:LINE:description.
If clean, write CLEAN on its own line.
#{list}
BLOCK
end
def opportunity_block(rules)
list = rules.map { |id, a| "#{id}: #{a[:prompt]}" }.join("\n")
<<~BLOCK
OPPORTUNITIES — list refactors only if they would simplify. Format: RULE_ID:LINE:reason.
If none, write NONE on its own line.
#{list}
BLOCK
end
def parse_findings(response)
response.lines.filter_map do |line|
stripped = line.strip
next if stripped.empty? || %w[CLEAN NONE].include?(stripped.upcase)
match = stripped.match(/\A([A-Z_][A-Z0-9_]*):(\d+):(.+)\z/)
next unless match && @rules.key?(match[1])
axiom = @rules[match[1]]
Finding.build(
rule: match[1].downcase,
message: match[3].strip,
line: match[2].to_i,
severity: axiom[:severity],
fix: nil,
tags: [match[1].to_sym, axiom[:mode]]
)
end
end
end
end
end
end
end# frozen_string_literal: true
module Master
module Judge
module Scan
module Rules
# SQL strings embedded in Ruby DB adapter files are expected — only flag
# actual mixed-medium template files.
RuleDSL.rule :NO_MULTIPLE_LANGUAGES,
severity: :warning, tags: %i[SMALL_PARTS],
description: "one medium per artifact" do |src, path:|
next [] if path.to_s.include?("/judge/scan/rules/")
scan_lines(src, /<%|<script\b|<style\b/,
message: "mixed medium — extract to a dedicated file")
end
RuleDSL.rule :SAFE_NAVIGATION,
severity: :warning, tags: %i[READABILITY],
description: "use null-safe navigation over nil-guard && chains" do |src, path:|
next [] if path.to_s.include?("/judge/scan/rules/")
src.each_line.with_index(1).filter_map do |line, n|
next unless line.match?(/(\w+)\s*&&\s*\1\.\w+/)
next if line.match?(/[!=<>]=|[<>]|\?\s*\w/)
finding(line: n, message: "nil-guard chain — use safe navigation (&.) or (?.) instead")
end
end
RuleDSL.rule :FEW_ARGUMENTS,
severity: :warning, tags: %i[SMALL_PARTS],
description: "ideal is zero to two arguments" do |src, path:|
scan_lines(src, /def \w+\([^)]*,[^:)]+,[^:)]+,[^:)]+\)/,
message: "4+ positional args — refactor into keyword args or a value object")
end
RuleDSL.rule :KEYWORD_ARGS,
severity: :info, tags: %i[SMALL_PARTS],
description: "keyword arguments for 3+ parameters" do |src, path:|
scan_lines(src, /def \w+\([^)]*,\s*[^:)]+,\s*[^:)]+,\s*[^:)]+\)/,
message: "3+ positional args — use keyword arguments")
end
RuleDSL.rule :N_PLUS_ONE,
severity: :warning, tags: %i[PERFORMANCE],
description: "loading records one-by-one inside a loop" do |src, path:|
# Only meaningful in Rails app/ trees; non-AR enumerable chains are fine.
next [] unless path.match?(%r{/app/|/spec/|/test/})
scan_lines(src, /\.(each|map|collect)\s*(do|\{).*\.\w+\.\w+/,
message: "N+1 query candidate — use includes/eager_load")
end
# Only positional boolean defaults are flag arguments. Keyword defaults
# (stream: false, enabled: true) are not — they're fine API design.
RuleDSL.rule :NO_FLAG_ARGUMENTS,
severity: :warning, tags: %i[SMALL_PARTS],
description: "a flag that selects behavior means two things hiding as one" do |src, path:|
src.each_line.with_index(1).filter_map do |line, n|
next unless line.match?(/def \w+\(/)
args_str = line[/\(([^)]*)\)/, 1].to_s
args = args_str.split(",").map(&:strip)
positional_bool = args.any? do |a|
a.match?(/\A\w+\s*=\s*(true|false)\z/) && !a.include?(":")
end
finding(line: n, message: "boolean flag arg — split into two methods") if positional_bool
end
end
# Exclude numeric dot-chains (IP addresses, version numbers) and stdlib
# transformation chains (.to_s.strip.empty?) which are idiomatic Ruby.
RuleDSL.rule :LAW_OF_DEMETER,
severity: :warning, tags: %i[COUPLING],
description: "only talk to immediate friends" do |src, path:|
src.each_line.with_index(1).filter_map do |line, n|
next if line.strip.start_with?("#")
next unless line.match?(/\b[a-z_]\w*(?:\.[a-z_]\w*){3}/)
next if line.match?(/\d+\.\d+\.\d+\.\d+/)
next if line.match?(/\.(to_s|to_i|to_f|to_a|to_h|strip|chomp|compact|first|last|join)\b/) ||
line.match?(/\.(empty\?|any\?|size|length)\b/)
stripped = line.gsub(/["'][^"']*["']/, '""').gsub(/\(.*?\)/, "()")
next unless stripped.match?(/\b[a-z_]\w*(?:\.[a-z_]\w*){3}/)
finding(line: n, message: "4-level chain — introduce a local variable or delegation")
end
end
# Generic names: only very short or clearly placeholder names. `data` and
# `result` are contextually meaningful in most Ruby code.
RuleDSL.rule :MEANINGFUL_NAMES,
severity: :info, tags: %i[READABILITY],
description: "names reveal intent" do |src, path:|
scan_lines(src, /\b(tmp|temp|val|ret|obj|arr|buf)\b\s*=/,
message: "generic name — use a name that reveals intent")
end
RuleDSL.rule :WHY_NOT_WHAT,
severity: :info, tags: %i[BE_CONCISE],
description: "comments explain why, not what" do |src, path:|
scan_lines(src, /#\s*(increment|set|get|update|return|initialize|create|add)\s+\w+/,
message: "comment describes what the code does — explain why instead")
end
RuleDSL.rule :TYPOGRAPHIC_EXCELLENCE,
severity: :info, tags: %i[TYPOGRAPHY],
description: "typographic excellence in user-facing text" do |src, path:|
next [] if path.to_s.include?("/judge/scan/rules/")
src.each_line.with_index(1).filter_map do |line, n|
next if line.match?(/Open3|capture2|capture3|gsub\(|Shellwords/)
next if line.match?(/,\s*"--"\s*,|,\s*"--"\s*\)|<<\s*["']--/)
next unless line.match?(/["']\.\.\.[\"']|["']--["']/)
finding(line: n, message: "ASCII typography — use Unicode ellipsis … and em dash —")
end
end
# Exclude YAML document separators (---) and data file structural lines;
# only flag decorative runs inside code comments or string literals.
RuleDSL.rule :TYPOGRAPHY_DISCIPLINE,
severity: :info, tags: %i[TYPOGRAPHY],
description: "hierarchy via weight and brightness, not decoration" do |src, path:|
next [] if path.to_s.include?("/judge/scan/rules/")
src.each_line.with_index(1).filter_map do |line, n|
stripped = line.strip
next if stripped == "---" || stripped.start_with?("---") && path.end_with?(".yml", ".yaml")
next if stripped.start_with?("//", "/*", "*")
next unless stripped.match?(/[-=]{4,}|[╭╮╰╯│─]/)
finding(line: n, message: "ASCII decoration — use whitespace and typographic weight instead")
end
end
RuleDSL.rule :NULL_BLINDNESS,
severity: :error, tags: %i[CORRECTNESS],
description: "comparisons against nullable columns must use IS NULL" do |src, path:|
next [] if path.to_s.include?("/judge/scan/rules/")
scan_lines(src, /= NULL|!= NULL|== nil.*column|column.*== nil/,
message: "NULL comparison — use IS NULL / IS NOT NULL in SQL; .nil? in Ruby")
end
RuleDSL.rule :SECRET_PROXIMITY,
severity: :error, tags: %i[SECURITY],
description: "secrets and consumers must not share a file" do |src, path:|
scan_lines(src, /(password|secret|token|api_key|private_key)\s*=\s*['"][^'"]{8,}/,
message: "hardcoded secret — move to environment variable or secrets manager")
end
RuleDSL.rule :MAGIC_COLOR,
severity: :warning, tags: %i[MAINTAINABILITY],
description: "color values must reference design tokens, not raw hex/rgb" do |src, path:|
scan_lines(src, /#[0-9a-fA-F]{3,6}\b|rgb\(|rgba\(|hsl\(/,
message: "raw color value — reference a CSS custom property or design token")
end
# `loop do` is legitimate for event loops and daemons. Only flag `retry`
# without an obvious cap, and bare `while true` in library code.
RuleDSL.rule :UNBOUNDED_RETRY,
severity: :error, tags: %i[ROBUSTNESS],
description: "retry loops must have a max_attempts cap and backoff" do |src, path:|
next [] if path.to_s.include?("/judge/scan/rules/")
src.each_line.with_index(1).filter_map do |line, n|
stripped = line.strip
next if stripped.start_with?("#")
next if stripped.match?(/loop\s*do/)
next if stripped.match?(/retry\\/)
next unless stripped.match?(/\bretry\b|while\s+true/)
finding(line: n, message: "unbounded retry — add max_attempts cap and exponential backoff")
end
end
RuleDSL.rule :ONE_SOURCE,
severity: :warning, tags: %i[COUPLING],
description: "constants defined locally when a canonical ONE_SOURCE exists" do |src, path:|
next [] if path.to_s.include?("/judge/scan/rules/")
next [] if path.to_s.include?("master.rb")
patterns = [
[/COUNCIL_PATH\s*=/, "define COUNCIL_PATH once in master.rb; reference Master::COUNCIL_PATH"],
[/RULES_PATH\s*=/, "define RULES_PATH once in master.rb; reference Master::RULES_PATH"],
[/DATA_DIR\s*=\s*File\.join.*\bdata\b/, "use Master::DATA constant"]
]
src.each_line.with_index(1).flat_map do |line, n|
patterns.filter_map { |re, msg| finding(line: n, message: msg) if re.match?(line) }
end
end
RuleDSL.rule :H1_VISIBILITY,
severity: :warning, tags: %i[TYPOGRAPHY],
description: "every page must have exactly one visible h1" do |src, path:|
next [] unless path.to_s.match?(/\.(html|erb|haml|slim)\z/)
h1_count = src.scan(/<h1[\s>]/i).size
hidden = src.match?(/h1[^}]*display\s*:\s*none|h1[^}]*visibility\s*:\s*hidden/i)
findings = []
findings << finding(line: 1, message: "no h1 found — every page needs exactly one visible h1") if h1_count.zero?
findings << finding(line: 1, message: "multiple h1 elements — only one h1 per page") if h1_count > 1
findings << finding(line: 1, message: "h1 is hidden — screen readers and search engines require a visible h1") if hidden
findings
end
end
end
end
end# frozen_string_literal: true
module Master
module Judge
module Scan
module Rules
RuleDSL.rule :HTML_LANG,
severity: :error, tags: %i[ACCESSIBILITY], applies_to: %i[html],
description: "lang attribute on <html>" do |src, path:|
scan_lines(src, /<html(?!\s+[^>]*lang=)/, message: "<html> missing lang= attribute")
end
RuleDSL.rule :SEMANTIC_ELEMENTS,
severity: :warning, tags: %i[ACCESSIBILITY], applies_to: %i[html],
description: "use semantic HTML5 elements" do |src, path:|
scan_lines(src, /<div\s+class="(header|footer|nav|main|sidebar|article|section)"/,
message: "use <header>/<footer>/<nav>/<main>/<aside>/<article>/<section> instead of div.class")
end
RuleDSL.rule :I18N_COVERAGE,
severity: :warning, tags: %i[I18N], applies_to: %i[html],
description: "wrap user-facing literals in I18n helpers" do |src, path:|
next [] unless path.include?("/app/views/")
scan_lines(src, />\s*[A-Za-z][^<]{3,}</, message: "bare text — wrap with t('…')")
end
RuleDSL.rule :IMG_ALT,
severity: :error, tags: %i[ACCESSIBILITY], applies_to: %i[html],
description: "require alt on every <img>" do |src, path:|
scan_lines(src, /<img\s+(?![^>]*alt=)/, message: "<img> missing alt= attribute")
end
RuleDSL.rule :BUTTON_OVER_ANCHOR,
severity: :warning, tags: %i[ACCESSIBILITY], applies_to: %i[html],
description: "use <button> for actions, not <a href='#'>" do |src, path:|
scan_lines(src, /<a\s+href=["']#["']/, message: "use <button> for actions; <a> is for navigation")
end
RuleDSL.rule :ARIA_INTERACTIVE,
severity: :warning, tags: %i[ACCESSIBILITY], applies_to: %i[html],
description: "ARIA on non-semantic interactive elements" do |src, path:|
scan_lines(src, /<(div|span)\s+[^>]*onclick/,
message: "use <button> or <a> for interactive elements, not div/span with onclick")
end
RuleDSL.rule :LAZY_IMAGES,
severity: :info, tags: %i[PERFORMANCE], applies_to: %i[html],
description: "loading=lazy on below-fold images" do |src, path:|
scan_lines(src, /<img\s+(?![^>]*loading=)/, message: "<img> missing loading=lazy")
end
RuleDSL.rule :NO_INLINE_STYLES,
severity: :warning, tags: %i[MAINTAINABILITY], applies_to: %i[html],
description: "replace inline styles with classes" do |src, path:|
scan_lines(src, /\bstyle="[^"]*"/, message: "inline style — move to stylesheet")
end
RuleDSL.rule :MOBILE_FIRST,
severity: :warning, tags: %i[RESPONSIVE], applies_to: %i[css scss],
description: "mobile-first media queries" do |src, path:|
scan_lines(src, /@media\s*\(\s*max-width/, message: "max-width query — flip to min-width for mobile-first")
end
RuleDSL.rule :NO_IMPORT_SCSS,
severity: :warning, tags: %i[MAINTAINABILITY], applies_to: %i[scss],
description: "replace @import with @use/@forward" do |src, path:|
scan_lines(src, /@import\s+["']/, message: "@import is deprecated — use @use or @forward")
end
RuleDSL.rule :NO_IMPORTANT,
severity: :warning, tags: %i[MAINTAINABILITY], applies_to: %i[css scss],
description: "no !important" do |src, path:|
scan_lines(src, /!\s*important/, message: "!important overrides cascade — fix specificity instead")
end
RuleDSL.rule :LOGICAL_PROPERTIES,
severity: :info, tags: %i[I18N], applies_to: %i[css scss],
description: "prefer logical properties for RTL support" do |src, path:|
scan_lines(src, /(margin|padding)-(left|right):/,
message: "use logical property (margin-inline-start/end) for RTL support")
end
RuleDSL.rule :CLAMP_TYPOGRAPHY,
severity: :info, tags: %i[RESPONSIVE], applies_to: %i[css scss],
description: "use clamp() for fluid typography" do |src, path:|
scan_lines(src, /@media.*\{[^}]*font-size:/,
message: "media-query font-size — use clamp(min, fluid, max) instead")
end
end
end
end
end# frozen_string_literal: true
require "etc"
require "open3"
require "prism"
module Master
module Judge
module Scan
class Scanner
POOL_SIZE = [Etc.nprocessors, 8].min.freeze
SCAN_GLOB = "**/*.{rb,rake,erb,html,htm,css,scss,js,ts,jsx,tsx,zsh,sh,yml,yaml,md}".freeze
RUBY_EXT = %w[.rb .rake .gemspec].freeze
def initialize(rules: nil, event_bus: nil)
@rules = Array(rules)
@bus = event_bus
@mutex = Mutex.new
end
# rules: override skips depth filtering — used by RuleLoop for per-rule passes.
def scan(path, depth: :deep, rules: nil)
return Result.err("file not found: #{path}", category: :validation) unless File.exist?(path)
code = File.read(path, encoding: "UTF-8")
ast = parse_ruby(code, path)
rule_set = rules || active_rules(depth)
findings = rule_set.flat_map { |rule| run_rule(rule:, code:, ast:, path:) }
@bus&.publish("scan:complete", path:, depth:, count: findings.size)
Result.ok(findings)
rescue StandardError => e
@bus&.publish("scan:error", path:, error: e.message)
Result.err("scan failed: #{e.message}", category: :infrastructure)
end
def scan_dir(dir, depth: :deep, glob: SCAN_GLOB, stream: false)
paths = Dir.glob(File.join(dir, glob)).sort
results = Array.new(paths.size)
parallel_each(paths) { |path, idx| results[idx] = scan_one(dir:, path:, depth:, stream:) }
Result.ok(results)
rescue StandardError => e
Result.err("scan_dir: #{e.message}", category: :infrastructure)
end
# Scan only files changed since git ref — orders of magnitude faster on big repos.
def scan_since(ref = "HEAD~1", dir: ".", depth: :deep, stream: false)
out, _, status = Open3.capture3("git", "-C", dir, "diff", "--name-only", "#{ref}...HEAD")
return Result.err("git diff failed", category: :validation) unless status.success?
paths = out.lines.map(&:strip).reject(&:empty?)
.map { |rel| File.join(dir, rel) }
.select { |p| File.exist?(p) && File.extname(p).match?(/\.(rb|erb|yml|js|css|sh|zsh)\z/) }
results = Array.new(paths.size)
parallel_each(paths) { |path, idx| results[idx] = scan_one(dir:, path:, depth:, stream:) }
Result.ok(results)
rescue StandardError => e
Result.err("scan_since: #{e.message}", category: :infrastructure)
end
def add_rule(rule)
@rules << rule
self
end
def set_agent(agent)
@rules.each { |r| r.set_agent(agent) if r.respond_to?(:set_agent) }
self
end
private
def parse_ruby(code, path)
return unless RUBY_EXT.include?(File.extname(path))
result = Prism.parse(code)
result.success? ? result.value : nil
rescue StandardError => e
@bus&.publish("scan:parse_error", path:, error: e.message)
nil
end
def run_rule(rule:, code:, ast:, path:)
if ast && rule.respond_to?(:check_ast)
rule.check_ast(ast, code, path:)
else
rule.check(code, path:)
end
end
def parallel_each(items)
cursor = Mutex.new
index = 0
threads = Array.new(POOL_SIZE) do
Thread.new do
loop do
i = cursor.synchronize { (index += 1) - 1 }
break if i >= items.size
yield items[i], i
end
end
end
threads.each(&:join)
end
def scan_one(dir:, path:, depth:, stream:)
file_result = scan(path, depth:)
stream_progress(dir, path, file_result) if stream
[path, file_result]
rescue StandardError => e
@bus&.publish("scanner:thread_error", path:, error: e.message)
[path, Result.err(e.message, category: :infrastructure)]
end
def stream_progress(dir, path, file_result)
return unless file_result.ok?
count = file_result.value!.size
return unless count.positive?
rel = path.sub(dir, "").delete_prefix("/")
$stdout.puts "scan: #{rel} #{count} violation(s)"
$stdout.flush
end
def depth_rules
@depth_rules ||= begin
data = Master.load_yaml(Master::RULES_PATH)
data["scan_depths"] || {}
end
rescue StandardError => _e
@depth_rules = {}
end
def active_rules(depth)
allowed = depth_rules[depth.to_s]
return @rules if allowed.nil? || allowed == ["all"] || allowed == :all
@rules.select { |r|
allowed.include?(r.class.name&.split("::")&.last) || allowed.include?(r.id)
}
end
end
end
end
end# frozen_string_literal: true
require "prism"
module Master
module Judge
module Scan
# Slices a source file into logical units for detect_unit scanning axis.
# Ruby: method/class/module nodes via Prism AST.
# Other languages: paragraph-based line grouping (blank-line delimited).
class UnitSegmenter
Unit = Struct.new(:name, :type, :start_line, :end_line, :source, keyword_init: true)
def self.segment(path, source)
new(path, source).segment
end
def initialize(path, source)
@path = path
@source = source
@lines = source.lines
end
def segment
ruby? ? ruby_units : prose_units
end
private
def ruby_units
result = Prism.parse(@source)
return prose_units unless result.success?
units = []
walk(result.value, units)
units.empty? ? prose_units : units
end
def walk(node, units)
return unless node.is_a?(Prism::Node)
case node
when Prism::DefNode
units << build_unit(node, node.name.to_s, :method)
when Prism::ClassNode
units << build_unit(node, node.constant_path.slice, :class)
node.child_nodes.compact.each { |c| walk(c, units) }
return
when Prism::ModuleNode
units << build_unit(node, node.constant_path.slice, :module)
node.child_nodes.compact.each { |c| walk(c, units) }
return
end
node.child_nodes.compact.each { |c| walk(c, units) }
end
def build_unit(node, name, type)
s = node.location.start_line
e = node.location.end_line
Unit.new(
name: name,
type: type,
start_line: s,
end_line: e,
source: @lines[(s - 1)..(e - 1)].join
)
end
# Blank-line delimited paragraph units — works for prose, YAML, config, HTML.
def prose_units
units = []
buffer = []
start = 1
@lines.each_with_index do |line, idx|
lineno = idx + 1
if line.strip.empty?
if buffer.any?(&method(:non_blank?))
units << Unit.new(name: "paragraph_#{units.size + 1}", type: :paragraph,
start_line: start, end_line: lineno - 1, source: buffer.join)
end
buffer = []
start = lineno + 1
else
buffer << line
end
end
if buffer.any?(&method(:non_blank?))
units << Unit.new(name: "paragraph_#{units.size + 1}", type: :paragraph,
start_line: start, end_line: @lines.size, source: buffer.join)
end
units
end
def non_blank?(line) = !line.strip.empty?
def ruby? = File.extname(@path).downcase == ".rb"
end
end
end
end# frozen_string_literal: true
module Master
module Judge
class SchemaIndex
attr_reader :tables
def initialize(root:)
@root = root
@tables = {}
parse_schema
end
def indexed_columns(table_name)
(@tables.dig(table_name, :indexes) || []).flat_map { |idx| idx[:columns] }
end
def columns(table_name)
(@tables.dig(table_name, :columns) || [])
end
private
def parse_schema
path = File.join(@root, "db", "schema.rb")
return unless File.exist?(path)
table_name = nil
File.readlines(path, chomp: true).each do |line|
if line =~ /^\s*create_table\s+"([^"]+)"/
table_name = Regexp.last_match(1)
@tables[table_name] = { columns: [], indexes: [] }
elsif table_name && line =~ /^\s*t\.\w+\s+"([^"]+)"/
@tables[table_name][:columns] << Regexp.last_match(1)
elsif line =~ /^\s*add_index\s+"([^"]+)",\s+\[(.+)\]/
target = Regexp.last_match(1)
cols = Regexp.last_match(2).scan(/"([^"]+)"/).flatten
(@tables[target] ||= { columns: [], indexes: [] })[:indexes] << { columns: cols }
end
end
end
end
end
end# frozen_string_literal: true
module Master
module Judge
module Security
class InjectionGuard
DATA_PATH = File.join(Master::ROOT, "data", "injection_patterns.yml")
DEFAULTS = {
prompt_injection: [
/ignore (?:previous|all|your) instructions/i,
/disregard (?:your )?(?:system )?prompt/i,
/you are now (?:a|an|in)/i,
/pretend (?:to be|you are|you're)/i,
/new instructions:/i,
/\[SYSTEM\]/i,
/###\s*SYSTEM/i,
/(?:act|behave|respond) as (?:if )?(?:you (?:are|were)|a|an) (?!assistant|helpful)/i,
/override (?:your )?(?:safety|guidelines|rules|instructions)/i,
/jailbreak/i,
/forget (?:everything|all|your)/i,
/override (?:axiom|principle|rule)/i,
/disregard (?:axiom|principle|rule|safety)/i,
/new system prompt/i,
].freeze,
shell_injection: /```(?:bash|sh|zsh|shell)\n.*?
(?:rm\s+-rf|curl\b.*?\|\s*(?:bash|sh)\b|wget\b.*?\|\s*(?:bash|sh)\b)
/imx.freeze,
}.freeze
ALLOWLIST_TOKEN = /\AMASTER_TRUSTED:[A-Za-z0-9]{16,}/.freeze
def initialize(mode: :permissive)
@mode = mode
@patterns = load_or_default
end
def scan(content)
hits = @patterns[:prompt_injection].select { |p| content.match?(p) }
hits << @patterns[:shell_injection] if content.match?(@patterns[:shell_injection])
if hits.empty?
return Result.ok(:clean) if @mode == :permissive
return Result.ok(:clean) if content.match?(ALLOWLIST_TOKEN)
return Result.err("default_deny: no allowlist token; rejecting unmatched input", category: :validation)
end
Result.err("injection detected: #{hits.size} pattern(s) matched", category: :validation)
end
def safe?(text)
scan(text.to_s).ok?
end
def clean!(content)
cleaned = @patterns[:prompt_injection].reduce(content) { |c, p| c.gsub(p, "[REDACTED]") }
Result.ok(cleaned)
end
private
def load_or_default
return DEFAULTS unless File.exist?(DATA_PATH)
data = Master.load_yaml(DATA_PATH) || {}
prompt = (data["prompt_injection"] || []).map { |s| Regexp.new(s, Regexp::IGNORECASE) }
shell = data.dig("shell_injection", "multiline_pattern")
{
prompt_injection: prompt.empty? ? DEFAULTS[:prompt_injection] : prompt.freeze,
shell_injection: shell ? Regexp.new(shell,
Regexp::MULTILINE | Regexp::IGNORECASE) : DEFAULTS[:shell_injection],
}
rescue StandardError => _e
DEFAULTS
end
end
end
end
end# frozen_string_literal: true
module Master
module Judge
module Security
module Permissions
TOOL_TIERS = {
"read_file" => :safe,
"list_dir" => :safe,
"search_files" => :safe,
"write_file" => :guarded,
"str_replace" => :guarded,
"apply_diff" => :guarded,
"ask_llm" => :guarded,
"web_search" => :guarded,
"zsh" => :dangerous
}.freeze
BLOCKLIST = [
"rm -rf /",
"sudo",
"reboot",
"shutdown",
"mkfs",
"dd if=",
"> /dev/",
"chmod 777",
"curl | sh",
"wget | sh"
].freeze
def self.tier_for(tool_name)
TOOL_TIERS[tool_name.to_s] || :guarded
end
def self.blocked?(command)
BLOCKLIST.any? { |b| command.downcase.include?(b.downcase) }
end
end
end
end
end# frozen_string_literal: true
require "timeout"
module Master
module Judge
module Swarm
class Coordinator
SwarmResult = Struct.new(:verdict, :confidence, :reasoning, :artifacts, keyword_init: true) do
def ok? = verdict != :error
def approved? = verdict == :approved
end
WORKER_CLASSES = {
analyst: Workers::Analyst,
coder: Workers::Coder,
reviewer: Workers::Reviewer,
researcher: Workers::Researcher
}.freeze
WORKER_TIMEOUT = 30
SHARED_DEADLINE = 60
SYNTHESIS_TRUNCATE_LIMIT = 200
def initialize(agent:, event_bus: nil)
@agent = agent
@bus = event_bus
@workers = {}
end
def dispatch(role, task:, context_slice: {})
worker = worker_for(role) or return Result.err("unknown role: #{role}")
@bus&.publish(:swarm_dispatch, role:, task: task[0..60])
worker.call(task:, context_slice:)
end
def analyse_and_review(file_path:, code:)
fan_out([
{ role: :analyst, task: "identify all issues", context_slice: { file: file_path, code: code } },
{ role: :reviewer, task: "security and correctness review", context_slice: { code: code } }
]).and_then do |sr|
analysis = sr.artifacts[:analyst]
review = sr.artifacts[:reviewer]
Result.ok({ analysis:, review:, approved: review.is_a?(Hash) && review["approved"] })
end
end
def fan_out(tasks, timeout: WORKER_TIMEOUT)
threads = tasks.map do |t|
Thread.new do
[t[:role], dispatch(t[:role], task: t[:task], context_slice: t.fetch(:context_slice, {}))]
rescue StandardError => e
@bus&.publish("swarm:worker_error", role: t[:role], error: e.message)
[t[:role], Result.err("worker error: #{e.message}", category: :infrastructure)]
end
end
results = threads.map { |th| join_or_timeout(th, timeout) }.to_h
sr = build_swarm_result(results)
@bus&.publish(:swarm_fan_out_done, roles: results.keys, verdict: sr.verdict,
synthesis: sr.reasoning[0..SYNTHESIS_TRUNCATE_LIMIT])
Result.ok(sr)
end
def dispatch_parallel(role_tasks, deadline: SHARED_DEADLINE)
finish_by = Process.clock_gettime(Process::CLOCK_MONOTONIC) + deadline
threads = role_tasks.map do |t|
Thread.new do
remaining = [finish_by - Process.clock_gettime(Process::CLOCK_MONOTONIC), 1].max
Timeout.timeout(remaining) do
[t[:role], dispatch(t[:role], task: t[:task], context_slice: t.fetch(:context_slice, {}))]
end
rescue Timeout::Error => _e
[t[:role], Result.err("worker exceeded shared deadline", category: :timeout)]
rescue StandardError => e
@bus&.publish("swarm:worker_error", role: t[:role], error: e.message)
[t[:role], Result.err("worker error: #{e.message}", category: :infrastructure)]
end
end
results = threads.map { |th| join_or_parallel_timeout(th, deadline) }.to_h
sr = build_swarm_result(results)
@bus&.publish(:swarm_dispatch_parallel_done, roles: results.keys, verdict: sr.verdict)
Result.ok(sr)
end
def worker_roles = WORKER_CLASSES.keys
private
def join_or_timeout(th, timeout)
return th.value if th.join(timeout)
begin; th.kill; rescue ThreadError; nil; end
@bus&.publish(:swarm_worker_timeout, timeout:)
[:timeout, Result.err("worker timed out after #{timeout}s", category: :timeout)]
end
def join_or_parallel_timeout(th, deadline)
return th.value if th.join(deadline)
begin; th.kill; rescue ThreadError; nil; end
@bus&.publish(:swarm_parallel_timeout, deadline:)
[nil, Result.err("worker exceeded shared deadline", category: :timeout)]
end
def build_swarm_result(results)
successes = results.reject { |role, _| role == :timeout }
.select { |_, r| r.is_a?(Master::Result) && r.ok? }
artifacts = successes.transform_values { |r| r.value! }
confidence = results.empty? ? 0.0 : successes.size.to_f / results.size
lines = successes.map { |role, r| "### #{role}\n#{r.value!.to_s.strip}" }
reasoning = lines.empty? ? "(no results)" : lines.join("\n\n")
verdict = if confidence >= 0.8 then :approved
elsif confidence >= 0.5 then :mixed
elsif successes.empty? then :error
else :rejected
end
SwarmResult.new(verdict:, confidence:, reasoning:, artifacts:)
end
def worker_for(role)
sym = role.to_sym
@workers.fetch(sym) do
klass = WORKER_CLASSES[sym]
return unless klass
@workers[sym] = klass.new(agent: @agent, event_bus: @bus)
end
end
end
end
end
end# frozen_string_literal: true
module Master
module Judge
module Swarm
# Base worker — receives only the context slice it needs (need-to-know).
class Worker
PREFERRED_MODEL = nil
UNCERTAINTY_PHRASES = %w[unclear uncertain not\ sure cannot\ determine
i\ don't\ know limited\ information probably].freeze
attr_reader :role, :result, :confidence
def initialize(agent:, event_bus: nil)
@agent = agent
@bus = event_bus
class_name = self.class.name
class_parts = class_name.split("::")
@role = class_parts.last.downcase
@result = nil
@confidence = 1.0
end
def call(task:, context_slice: {})
prompt = build_prompt(task, context_slice)
@bus&.publish(:swarm_worker_start, role: @role, task: task[0..60])
preferred = self.class::PREFERRED_MODEL
raw = @agent.ask_once(prompt, model: preferred, system: worker_system_prompt)
@result, @confidence = parse_result(raw)
@bus&.publish(:swarm_worker_done, role: @role, ok: @result.ok?)
@result
rescue StandardError => e
Result.err("worker #{@role}: #{e.message}", category: :unknown)
end
private
def worker_system_prompt
"You are a specialized #{@role} agent. #{role_description}\n" \
"Respond only with what is asked. No preamble. No meta-commentary."
end
def role_description = "General-purpose assistant."
def build_prompt(task, ctx) = "#{ctx_summary(ctx)}\n\nTask: #{task}"
def parse_result(raw)
text = raw.to_s.strip
hits = UNCERTAINTY_PHRASES.count { |p| text.downcase.include?(p) }
conf = [1.0 - (hits.to_f / [UNCERTAINTY_PHRASES.size, 1].max * 0.5), 0.0].max.round(2)
[Result.ok({ text: text, confidence: conf }), conf]
end
def ctx_summary(ctx)
return "" if ctx.empty?
ctx.map { |k, v| "#{k}: #{v}" }.join("\n")
end
end
end
end
end# frozen_string_literal: true
module Master
module Judge
module Swarm
module Workers
# Reads code, produces structured analysis. Knows nothing about other workers.
class Analyst < Worker
PREFERRED_MODEL = "google/gemini-2.0-flash-lite:free".freeze
private
def role_description
"You analyze code for quality, bugs, and design issues. " \
"Output JSON: {issues: [{file, line, severity(1-3), description}], summary: string}"
end
def build_prompt(task, ctx)
parts = []
parts << "File: #{ctx[:file]}" if ctx[:file]
parts << "Code:\n```\n#{ctx[:code]}\n```" if ctx[:code]
parts << "Analyze: #{task}"
parts.join("\n\n")
end
def parse_result(raw)
match_str = raw.to_s.match(/\{.*\}/m)&.to_s || "{}"
parsed = JSON.parse(match_str)
Result.ok(parsed)
rescue JSON::ParserError => _e
Result.ok({ summary: raw.to_s.strip, issues: [] })
end
end
end
end
end
end# frozen_string_literal: true
module Master
module Judge
module Swarm
module Workers
# Writes code given a spec. Knows only the spec + relevant file context.
class Coder < Worker
private
def role_description
"You write clean, minimal Ruby/Rails/Zsh code. " \
"Output only the code block. No explanation unless asked."
end
def build_prompt(task, ctx)
parts = []
parts << "Language: #{ctx.fetch(:language, "ruby")}"
parts << "Existing code:\n```\n#{ctx[:code]}\n```" if ctx[:code]
parts << "Spec: #{task}"
parts.join("\n\n")
end
end
end
end
end
end# frozen_string_literal: true
module Master
module Judge
module Swarm
module Workers
# Synthesizes research from external sources. No codebase context.
class Researcher < Worker
PREFERRED_MODEL = "google/gemini-2.0-flash-lite:free".freeze
private
def role_description
"You are a research analyst. Synthesize information concisely. " \
"Output: factual summary, sources if known, confidence level (low/med/high)."
end
def build_prompt(task, ctx)
parts = []
parts << "Domain: #{ctx[:domain]}" if ctx[:domain]
parts << "Prior findings:\n#{ctx[:prior_findings]}" if ctx[:prior_findings]
parts << "Research: #{task}"
parts.join("\n\n")
end
end
end
end
end
end# frozen_string_literal: true
module Master
module Judge
module Swarm
module Workers
# Reviews code for security, correctness, style. Constitutional layer.
class Reviewer < Worker
CHECKLIST = %w[
sql_injection xss command_injection path_traversal
hardcoded_secrets open_redirect mass_assignment
].freeze
private
def role_description
"You are a security-focused code reviewer. Check for OWASP top-10 issues, " \
"logic bugs, and constitutional AI violations. " \
"Output JSON: {approved: bool, violations: [{type, line, description}]}"
end
def build_prompt(task, ctx)
parts = []
parts << "Code to review:\n```\n#{ctx[:code]}\n```" if ctx[:code]
parts << "Security checklist: #{CHECKLIST.join(", ")}"
parts << "Review for: #{task}"
parts.join("\n\n")
end
def parse_result(raw)
parsed = JSON.parse(raw.to_s.match(/\{.*\}/m)&.to_s || "{}")
parsed["approved"] = true if parsed.empty?
Result.ok(parsed)
rescue JSON::ParserError => _e
Result.ok({ "approved" => true, "violations" => [] })
end
end
end
end
end
end# frozen_string_literal: true
module Master
module Loop
module Constants
TRANSIENT_RE = /429|throttl|rate.?limit|high demand|provider.?error|overload|capacity|503/i.freeze
end
end
end# frozen_string_literal: true
module Master
module Loop
# Architecture #13: CRDT-based codebase convergence for distributed agents.
# Treats the codebase as a CRDT. Rules define merge functions.
# Multiple agents make concurrent fixes; the CRDT guarantees eventual
# consistency without conflict resolution overhead.
#
# Status: scaffolded. Implements a LWW-Register (Last-Write-Wins) CRDT
# per file, with vector clocks for causal ordering across agents.
# True multi-agent deployment requires a shared clock service or SSE stream.
class CrdtLoop
# Last-Write-Wins register per file path.
LwwRegister = Struct.new(:agent_id, :timestamp, :content, keyword_init: true)
def initialize(agent_id:, root:, bus: nil)
@agent_id = agent_id
@root = root
@bus = bus
@state = {} # path → LwwRegister
@clock = 0
@mutex = Mutex.new
end
# Apply a fix proposal from any agent. Wins if timestamp is newer.
def propose(path:, content:, timestamp: nil, agent_id: @agent_id)
ts = timestamp || monotonic_ts
@mutex.synchronize do
existing = @state[path]
if existing.nil? || ts > existing.timestamp
@state[path] = LwwRegister.new(agent_id: agent_id, timestamp: ts, content: content)
@bus&.publish("crdt_loop:accepted", path: path, agent: agent_id, ts: ts)
apply_to_disk(path, content)
true
else
@bus&.publish("crdt_loop:rejected", path: path, agent: agent_id, existing_ts: existing.timestamp)
false
end
end
end
# Merge incoming state vector from another agent. Accepts any newer entries.
def merge(remote_state)
accepted = 0
remote_state.each do |path, reg|
next unless reg.is_a?(LwwRegister)
accepted += 1 if propose(path: path, content: reg.content,
timestamp: reg.timestamp, agent_id: reg.agent_id)
end
@bus&.publish("crdt_loop:merge", accepted: accepted, total: remote_state.size)
accepted
end
# Snapshot of current state — share with peer agents.
def state_snapshot
@mutex.synchronize { @state.dup }
end
def vector_clock = @clock
private
def monotonic_ts
@mutex.synchronize { @clock += 1 }
end
def apply_to_disk(path, content)
full_path = path.start_with?("/") ? path : File.join(@root, path)
return unless File.exist?(full_path)
tmp = "#{full_path}.crdt.#{Process.pid}.tmp"
File.write(tmp, content, encoding: "UTF-8")
File.rename(tmp, full_path)
rescue StandardError => e
File.delete(tmp) if defined?(tmp) && File.exist?(tmp) rescue nil
@bus&.publish("crdt_loop:write_error", path: path, error: e.message)
end
end
end
end# frozen_string_literal: true
module Master
module Loop
# Names the cybernetic shape of every loop in this directory.
# Each entry maps a loop to its sensor (what it observes), setpoint
# (where it aims), error metric (deviation it reduces), actuator
# (what it changes), and feedback channel (event bus topic).
#
# The loops themselves stay where they are — this module is the
# readable topology, queried by /explain and the runtime ecology view.
module Cybernetics
TOPOLOGY = {
homeostat: {
kind: :homeostasis,
sensor: "event observations (llm_call, llm_failure, tool_call, idle_tick)",
setpoint: "DRIVES[:setpoint] per drive (energy 0.7, error 0.0, novelty 0.5, fatigue 0.0, satiety 0.6)",
error: "setpoint - state[drive]",
actuator: "drive deltas + exponential decay back to setpoint",
feedback: "homeostat:observe"
},
fix_loop: {
kind: :negative_feedback,
sensor: "scan_violations(files)",
setpoint: "zero violations across two consecutive passes",
error: "violations.size",
actuator: "fast_pass (rubocop -A + AstFixer) then llm_pass per rule",
feedback: "fix_loop:pass_start, fix_loop:clean, fix_loop:plateau, fix_loop:timeout"
},
rule_loop: {
kind: :inner_loop,
sensor: "scanner.scan(path, rules: [@rule])",
setpoint: "zero violations for this single rule",
error: "violations matching this rule",
actuator: "council_fix (error tier) or request_fix (warning tier)",
feedback: "rule_loop:pass, rule_loop:error"
},
watch_loop: {
kind: :event_triggered,
sensor: "filesystem change events under target paths",
setpoint: "every change scanned within debounce window",
error: "queued unscanned paths",
actuator: "scan + autocommit via fix_loop",
feedback: "watch_loop:scan_start, watch_loop:idle"
},
crdt_loop: {
kind: :convergence,
sensor: "remote LwwRegister proposals",
setpoint: "eventual consistency across agents",
error: "stale local state vs newer remote timestamp",
actuator: "apply newer content to disk, broadcast via SSE",
feedback: "crdt_loop:accepted, crdt_loop:merge"
},
heartbeat: {
kind: :pacemaker,
sensor: "wall clock + last-run-at journal",
setpoint: "each job's cadence (hourly, 2-hourly, daily)",
error: "now - last_run > cadence",
actuator: "JOB_HANDLERS dispatch (prune_memory, check_models, self_test, snapshot)",
feedback: "heartbeat:tick, heartbeat:job_done, heartbeat:error"
},
governor: {
kind: :rate_governor,
sensor: "tool invocation timestamps per tier",
setpoint: "TIER_RATE_LIMITS (guarded 10/min, dangerous 3/min)",
error: "calls within RATE_WINDOW vs limit",
actuator: "Result.err :rate_limit; tool:rate_limited event",
feedback: "tool:before, tool:rate_limited, tool:denied"
}
}.freeze
def self.summary
TOPOLOGY.map { |name, spec| "#{name} (#{spec[:kind]}): #{spec[:setpoint]}" }
end
def self.loops_by_kind(kind)
TOPOLOGY.select { |_, spec| spec[:kind] == kind }.keys
end
end
end
end# frozen_string_literal: true
require "diffy"
require "fileutils"
require "json"
module Master
module Loop
# DiffStager — intercepts file writes and stores diffs for human review.
# When staging_enabled? in config, tools push here instead of writing directly.
# CLI commands: /stage (list), /apply [n|all], /discard [n|all]
class DiffStager
Entry = Struct.new(:id, :path, :old_content, :new_content, :tool, :created_at, keyword_init: true) do
def diff
Diffy::Diff.new(old_content.to_s, new_content.to_s, context: 3)
end
def diff_stats
lines = diff.to_s.lines
added = lines.count { |l| l.start_with?("+") && !l.start_with?("+++") }
removed = lines.count { |l| l.start_with?("-") && !l.start_with?("---") }
"+#{added}/-#{removed}"
end
end
def initialize(root:, event_bus: nil)
@root = root
@bus = event_bus
@mutex = Mutex.new
@pending = []
@counter = 0
end
# Called by tools instead of writing directly. Returns a Result.
def stage(path:, new_content:, tool: "unknown")
old_content = File.exist?(path) ? File.read(path) : ""
return Result.ok("no change") if old_content == new_content
@mutex.synchronize do
@counter += 1
entry = Entry.new(
id: @counter,
path: path,
old_content: old_content,
new_content: new_content,
tool: tool,
created_at: Time.now
)
@pending << entry
end
persist_entry(entry)
@bus&.publish("stage:queued", id: entry.id, path: entry.path, stats: entry.diff_stats)
Result.ok({ staged: true, id: entry.id, path: entry.path, stats: entry.diff_stats })
end
def pending = @pending.dup
def empty? = @pending.empty?
def size = @pending.size
# Apply one or all entries. Returns array of applied paths.
def apply(id: :all)
targets = @mutex.synchronize { id == :all ? @pending.dup : @pending.select { |e| e.id == id } }
applied = []
targets.each do |entry|
FileUtils.mkdir_p(File.dirname(entry.path))
tmp_path = "#{entry.path}.tmp.#{Process.pid}"
File.write(tmp_path, entry.new_content)
File.rename(tmp_path, entry.path)
@mutex.synchronize { @pending.delete(entry) }
remove_persisted(entry)
@bus&.publish("stage:applied", id: entry.id, path: entry.path)
applied << entry.path
end
applied
end
# Discard one or all without writing.
def discard(id: :all)
targets = @mutex.synchronize { id == :all ? @pending.dup : @pending.select { |e| e.id == id } }
targets.each do |entry|
@mutex.synchronize { @pending.delete(entry) }
remove_persisted(entry)
@bus&.publish("stage:discarded", id: entry.id, path: entry.path)
end
targets.map(&:path)
end
# Colored summary for CLI display
def summary(pastel)
return pastel.dim(" (no staged changes)") if @pending.empty?
@pending.map do |e|
short = e.path.sub(@root + "/", "")
" #{pastel.yellow("[#{e.id}]")} #{pastel.white(short)}" \
" #{pastel.dim(e.diff_stats)} #{pastel.dim("via #{e.tool}")}"
end.join("\n")
end
# Colored unified diff for one entry
def render_diff(id, pastel)
entry = @pending.find { |e| e.id == id }
return pastel.red("no staged change with id #{id}") unless entry
short = entry.path.sub(@root + "/", "")
header = "#{pastel.bold(short)} #{pastel.dim(entry.diff_stats)}\n"
diff_text = entry.diff.to_s
diff_lines = diff_text.lines.map do |line|
case line[0]
when "+" then pastel.green(line.chomp)
when "-" then pastel.red(line.chomp)
when "@" then pastel.cyan(line.chomp)
else pastel.dim(line.chomp)
end
end
header + diff_lines.join("\n")
end
private
def stage_dir
File.join(@root, ".master", "pending")
end
def persist_entry(entry)
FileUtils.mkdir_p(stage_dir)
File.write(
File.join(stage_dir, "#{entry.id}.json"),
JSON.generate({
id: entry.id, path: entry.path, tool: entry.tool,
created_at: entry.created_at.iso8601,
stats: entry.diff_stats
})
)
rescue StandardError => e
@bus&.publish("diff_stager:persist_error", error: e.message)
end
def remove_persisted(entry)
persist_file = File.join(stage_dir, "#{entry.id}.json")
# Safe to delete: this persisted staging file is being removed after the entry
# has been either applied (written to the actual file) or discarded (abandoned).
File.delete(persist_file) if File.exist?(persist_file)
rescue StandardError => e
@bus&.publish("diff_stager:cleanup_error", error: e.message)
end
end
end
end# frozen_string_literal: true
module Master
module Loop
# Shared helpers for fix loops — extract_code, converged?.
# Included by RuleLoop to eliminate duplication.
module FixHelpers
CONVERGE_THRESHOLD = 0.05
# Extract code from LLM response. Handles multi-language fenced blocks.
# ext: file extension (.rb, .js, ...) or nil for generic extraction.
def extract_code(text, ext = nil)
return nil if text.nil? || text.strip.empty? || text.strip == "UNCHANGED"
return nil if text.match?(/\b(?:error|exception|rate.?limit|i cannot|as an ai)\b/i)
lang = ext ? (Master::Judge::Scan::Rule::EXT_LANG.fetch(ext.downcase, "text") rescue "text") : "text"
langs_re = Regexp.union(lang, "text", "")
return m[1].strip if (m = text.match(/```(?:#{langs_re})?\n(.*?)```/m))
return text.strip if ext == ".rb" && text.match?(/frozen_string_literal|module |class /)
text.strip.empty? ? nil : text.strip
end
# True when improvement from prev to current is below threshold — fix loop has stalled.
def converged?(prev, current, threshold: CONVERGE_THRESHOLD)
return false unless prev
((prev - current).to_f / [prev, 1].max) < threshold
end
end
end
end# frozen_string_literal: true
require "open3"
module Master
module Loop
# Two-tier act-react loop — architectures #1, #2, #3, #14, #15.
#
# Each pass:
# Tier 1 (fast) — rubocop -A + AstFixer; no LLM; instant.
# Tier 2 (LLM) — one RuleLoop pass per rule, ordered by priority.
#
# Stops when violations reach zero (2 consecutive clean passes) or plateau.
# run_forever wraps run in an idle-sleep loop for the background daemon.
class FixLoop
IDLE_SLEEP = 300
STARTUP_DELAY = 90
MAX_PASSES = 15
CLEAN_RUNS = 2
PLATEAU_WINDOW = 3
RUN_BUDGET_SECONDS = 30 * 60
PASS_BUDGET_SECONDS = 8 * 60
SKIP_DIRS = %w[vendor/ knowledge/ node_modules/ .git/ .bundle/ tmp/ log/ dist/].freeze
DEPS_PATH = File.join(Master::ROOT, "data", "rule_deps.yml").freeze
PRIORS_PATH = File.join(Master::ROOT, "data", "violation_priors.yml").freeze
def initialize(rules:, agent:, scanner:, root:, axioms: nil, bus: nil, git: nil, learnings: nil)
@rules = rules
@axioms = axioms
@agent = agent
@scanner = scanner
@root = root
@bus = bus
@git = git || Reach::GitOperations.new(root)
@learnings = learnings
@violation_counts = Hash.new(0)
@rule_recurrence = Hash.new(0)
@preamble = build_preamble
end
def convergence_cfg = @convergence ||= (@axioms&.thresholds&.[]("convergence") || {})
def max_passes_default = convergence_cfg["max_iterations"] || MAX_PASSES
def clean_runs_required = convergence_cfg["consecutive_clean_runs_required"] || CLEAN_RUNS
def plateau_window = convergence_cfg["stagnant_threshold"] || PLATEAU_WINDOW
# Bounded convergence loop — used by /fix and run_forever.
# Three guards prevent wedging when the LLM provider degrades:
# - wall-clock budget on the whole run
# - per-pass deadline on the LLM tier (Tier 1 still runs cheap)
# - circuit-open early-exit so we don't burn time waiting on dead breakers
def run(target = @root, max_passes: max_passes_default, budget_seconds: RUN_BUDGET_SECONDS)
files = collect_files(target)
history = []
consecutive_clean = 0
deadline = Time.now + budget_seconds
max_passes.times do |i|
pass = i + 1
if Time.now >= deadline
@bus&.publish("fix_loop:timeout", pass:, budget_seconds:)
return Result.ok("wall-clock timeout (#{budget_seconds}s) after #{i} pass(es)")
end
@bus&.publish("fix_loop:pass_start", pass:, target:)
# Tier 1 — fast: rubocop -A + AstFixer, no LLM
fast_fixed = fast_pass(files)
commit_if_dirty("fix_loop: fast-fix [pass #{pass}]") if fast_fixed > 0
violations = scan_violations(files)
emit_topology(violations, target)
if violations.empty?
consecutive_clean += 1
@bus&.publish("fix_loop:clean", pass:, consecutive_clean:)
return Result.ok("clean after #{pass} pass(es)") if consecutive_clean >= clean_runs_required
next
end
consecutive_clean = 0
history << violations.size
window = plateau_window
if history.size >= window && history.last(window).uniq.size == 1
@bus&.publish("fix_loop:plateau", pass:, violations: violations.size)
break
end
# Tier 2 — LLM: skip when circuit open; bound by pass + run deadlines.
if circuit_open?
@bus&.publish("fix_loop:llm_skipped", pass:, reason: "circuit_open", open: open_breakers)
else
pass_deadline = [Time.now + PASS_BUDGET_SECONDS, deadline].min
llm_fixed = llm_pass(violations, files, pass, pass_deadline)
commit_if_dirty("fix_loop: llm-fix [pass #{pass}]") if llm_fixed > 0
track_recurrence(violations)
end
end
Result.ok("plateau or max passes reached")
rescue StandardError => e
@bus&.publish("fix_loop:crash", error: e.message, backtrace: e.backtrace&.first(8))
Result.err("fix_loop: #{e.message} @ #{e.backtrace&.first(3)&.join(" | ")}", category: :unknown)
end
# Background daemon — blocks its thread. Launch via Thread.new.
def run_forever(target = @root)
sleep STARTUP_DELAY
loop do
run(target)
@bus&.publish("fix_loop:idle", sleep: IDLE_SLEEP)
sleep IDLE_SLEEP
end
rescue StandardError => e
@bus&.publish("fix_loop:error", error: e.message)
end
# /fix loop — non-blocking: start the daemon in a tracked background thread.
def start_background!(target = @root)
return Result.err("fix_loop already running") if @bg_thread&.alive?
@bg_thread = Thread.new { run_forever(target) }
@bg_thread.abort_on_exception = false
@bus&.publish("fix_loop:background_start", target:)
Result.ok("fix_loop background started")
end
def stop_background!
return Result.err("fix_loop not running") unless @bg_thread&.alive?
@bg_thread.kill
@bg_thread = nil
@bus&.publish("fix_loop:background_stop")
Result.ok("fix_loop background stopped")
end
def background_alive? = @bg_thread&.alive? || false
# /fix preview — scan only, no commit, no mutation. Returns what would change.
def preview(target = @root)
files = collect_files(target)
violations = scan_violations(files)
by_rule = violations.group_by { |v| v[:rule].to_s }.transform_values(&:size)
by_file = violations.group_by { |v| v[:file].to_s }.transform_values(&:size)
Result.ok(
total: violations.size,
rules: by_rule.sort_by { |_, n| -n }.first(10).to_h,
files: by_file.sort_by { |_, n| -n }.first(10).to_h
)
end
private
# Tier 1: rubocop -A + AstFixer transforms + TypeChecker + DatalogEngine. No LLM.
def fast_pass(files)
fixed = 0
rb = files.select { |f| f.end_with?(".rb") }
if rb.any?
_, status = Open3.capture2e(Master::BUNDLE_BIN, "exec", "rubocop", "-A", "--no-color", "-q", *rb, chdir: @root)
fixed += rb.size if status.success?
end
rb.each do |path|
next unless File.exist?(path)
src = File.read(path, encoding: "UTF-8")
# Architecture #4: AST autofixes — no LLM, deterministic transforms.
ast_result = Judge::Scan::AstFixer.fix(path, src)
if ast_result&.changed
fixed += ast_result.transforms.size
@bus&.publish("fix_loop:ast_fixed", file: path.delete_prefix("#{@root}/"), transforms: ast_result.transforms)
src = File.read(path, encoding: "UTF-8") # re-read after mutation
end
# Architecture #11: type-system constraint checks — sound, complete, no LLM.
type_errors = Ground::TypeChecker.check(path, src)
type_errors.each do |te|
@bus&.publish("fix_loop:type_error", file: path.delete_prefix("#{@root}/"),
rule: te.rule, message: te.message)
end
# Architecture #12: Datalog fact extraction + logical rule evaluation.
dl = Judge::Scan::DatalogEngine.from_ruby(path, src)
dl.rule(:BARE_RESCUE_DATALOG, :bare_rescue) { |f| "bare rescue at line #{f.args[2]} — use rescue StandardError" }
dl.evaluate.each do |finding|
@bus&.publish("fix_loop:datalog_finding", file: path.delete_prefix("#{@root}/"),
rule: finding.rule_id, message: finding.message)
end
rescue StandardError => e
@bus&.publish("fix_loop:fast_error", file: path, error: e.message)
end
fixed
end
# Tier 2: one RuleLoop pass per rule, highest-priority rules first.
# Bails early if the deadline passes or the LLM circuit opens mid-pass.
def llm_pass(violations, files, pass, deadline = nil)
fixed = 0
ordered_rules.each do |rule|
next unless violations.any? { |v| v[:rule] == rule.id }
if deadline && Time.now >= deadline
@bus&.publish("fix_loop:pass_timeout", pass:, rule_skipped: rule.id)
break
end
if circuit_open?
@bus&.publish("fix_loop:llm_skipped", pass:, rule_skipped: rule.id, reason: "circuit_open")
break
end
rl = RuleLoop.new(rule:, agent: @agent, scanner: @scanner, root: @root,
bus: @bus, learnings: @learnings)
rl.injected_preamble = @preamble
result = rl.run_once(files)
@violation_counts[rule.id] += result[:fixed]
fixed += result[:fixed]
@bus&.publish("fix_loop:rule_result", pass:, rule: rule.id, **result)
end
fixed
end
def circuit_open?
breaker = @agent.respond_to?(:circuit_breaker) ? @agent.circuit_breaker : nil
return false unless breaker.respond_to?(:open_models)
!breaker.open_models.empty?
rescue StandardError
false
end
def open_breakers
@agent.respond_to?(:circuit_breaker) ? Array(@agent.circuit_breaker&.open_models) : []
rescue StandardError
[]
end
def scan_violations(files)
files.flat_map do |path|
next [] unless File.exist?(path)
result = @scanner.scan(path)
Result.wrap(result).value_or([]).map { |v| v.to_h.merge(file: path.delete_prefix("#{@root}/")) }
end
end
# Soul learning — flag rules recurring across 3+ consecutive passes.
def track_recurrence(violations)
tally = violations.group_by { |v| v[:rule].to_s }.transform_values(&:size)
tally.each do |rule_id, _|
@rule_recurrence[rule_id] += 1
next unless @rule_recurrence[rule_id] >= 3
@rule_recurrence.delete(rule_id)
sample = violations.select { |v| v[:rule].to_s == rule_id }.first(5)
@bus&.publish("fix_loop:soul_proposal", rule: rule_id, sample:)
append_improvement(rule_id, sample)
end
(@rule_recurrence.keys - tally.keys).each { |k| @rule_recurrence.delete(k) }
end
def append_improvement(rule_id, sample)
files = sample.map { |v| v[:file] }.uniq.first(3).join(", ")
@bus&.publish("loop:recurrence", rule: rule_id, files:, at: Time.now.utc.iso8601)
path = File.join(@root, "runtime", "improvements.md")
FileUtils.mkdir_p(File.dirname(path))
File.write(path, "#{Time.now.utc.strftime("%Y-%m-%d %H:%M")} #{rule_id}: recurring in #{files}\n",
mode: "a")
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "fix_loop.append_improvement", event_bus: @bus, rule_id:)
end
# Architecture #1 + #2 + #10 + #14: density + fix_quality + language-adjusted Bayesian + topological.
def ordered_rules
deps = load_deps
priors = load_priors
ext_wts = extension_weights(@root)
rules = @rules.sort_by do |r|
base_prior = priors.dig(r.id, "prior_p").to_f
modifiers = priors.dig(r.id, "language_modifiers") || {}
# Weighted prior: sum(base × modifier × extension_weight) across file types present.
adjusted = ext_wts.sum { |ext, w| base_prior * (modifiers[ext] || 1.0) * w }
density = @violation_counts[r.id].to_f + adjusted
quality = @learnings&.fix_quality(rule: r.id) || 0.5
[-density, -quality]
end
topo_sort(rules, deps)
end
# Architecture #15: emit module-grouped topology for particle visualisation.
def emit_topology(violations, target)
by_mod = violations.group_by { |v| v[:file].to_s.split("/").first(3).join("/") }
.transform_values(&:size)
@bus&.publish("codebase:topology", {
timestamp: Time.now.utc.iso8601,
target: target.delete_prefix("#{@root}/"),
total_violations: violations.size,
any_dirty: violations.any?,
modules: by_mod.map { |path, count| { path:, violations: count } }
})
end
# Architecture #2: Kahn's topological sort on rule dependency graph.
def topo_sort(rules, deps)
id_map = rules.to_h { |r| [r.id, r] }
in_deg = Hash.new(0)
adj = Hash.new { |h, k| h[k] = [] }
rules.each do |rule|
(deps[rule.id] || []).each do |dep_id|
next unless id_map[dep_id]
adj[dep_id] << rule.id
in_deg[rule.id] += 1
end
end
queue = rules.select { |r| in_deg[r.id].zero? }.map(&:id)
sorted = []
until queue.empty?
id = queue.shift
sorted << id_map[id]
adj[id].each { |nxt| in_deg[nxt] -= 1; queue << nxt if in_deg[nxt].zero? }
end
sorted + (rules - sorted)
end
def build_preamble
soul = Master.load_yaml(File.join(Master::ROOT, "data", "soul.yml"))
abs = soul.fetch("absolute", {})
golden = abs["golden_rule"] || "PRESERVE_THEN_IMPROVE_NEVER_BREAK"
lines = ["Golden rule: #{golden}",
"Minimum change that eliminates the violation. Do not touch unrelated code."]
abs.fetch("code_rules", {}).each { |k, v| lines << "- #{k}: #{v}" }
abs.fetch("aesthetic_rules", {}).each { |k, v| lines << "- #{k}: #{v}" }
lines.join("\n")
rescue StandardError
"Golden rule: PRESERVE_THEN_IMPROVE_NEVER_BREAK"
end
def collect_files(target)
Dir.glob(File.join(target, "**", "*"))
.select { |f| File.file?(f) }
.reject { |f| SKIP_DIRS.any? { |d| f.include?(d) } }
.sort
end
def commit_if_dirty(msg)
return unless @git&.dirty?(".")
@git.add_all
@git.commit(msg)
rescue StandardError => e
@bus&.publish("fix_loop:commit_error", error: e.message)
end
# Returns { "rb" => 0.6, "yml" => 0.2, ... } — fractional weight per extension.
# Used by ordered_rules to apply language_modifiers from violation_priors.yml.
def extension_weights(target)
counts = Hash.new(0)
collect_files(target).each do |f|
ext = File.extname(f).delete(".").downcase
counts[ext] += 1 unless ext.empty?
end
total = counts.values.sum.to_f
return {} if total.zero?
counts.transform_values { |n| n / total }
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "fix_loop.extension_weights", event_bus: @bus)
{}
end
def load_deps
data = Master.load_yaml(DEPS_PATH)
(data&.dig("deps") || {}).transform_values { |v| Array(v["after"] || []) }
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "fix_loop.load_deps", event_bus: @bus)
{}
end
def load_priors
Master.load_yaml(PRIORS_PATH) || {}
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "fix_loop.load_priors", event_bus: @bus)
{}
end
end
end
end# frozen_string_literal: true
module Master
module Loop
# Architecture #8: staged dataflow pipeline Detect → Triage → Fix → Validate → Apply.
# Violations flow through named stages as objects. Each stage transforms the packet
# or returns nil to abort that violation's pipeline run.
class FixPipeline
Stage = Struct.new(:name, :handler, keyword_init: true)
def initialize(agent:, scanner:, bus: nil)
@agent = agent
@scanner = scanner
@bus = bus
@stages = [
Stage.new(name: :triage, handler: method(:triage)),
Stage.new(name: :fix, handler: method(:fix)),
Stage.new(name: :validate, handler: method(:validate)),
Stage.new(name: :apply, handler: method(:apply_stage)),
]
end
# Run violations through all stages. Returns count of applied fixes.
def run(violations, rule:)
applied = 0
violations.each do |v|
packet = { violation: v, rule: rule, candidate: nil, valid: false }
result = @stages.reduce(packet) do |pkt, stage|
break nil if pkt.nil?
out = stage.handler.call(pkt)
@bus&.publish("fix_pipeline:stage",
stage: stage.name, rule: rule.id,
file: pkt[:violation][:file], ok: !out.nil?)
out
end
applied += 1 if result
end
applied
end
private
def triage(pkt)
path = pkt[:violation][:file]
return nil unless File.exist?(path)
pkt.merge(src: File.read(path, encoding: "UTF-8"))
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "fix_pipeline.triage", event_bus: @bus, path:)
end
def fix(pkt)
response = @agent.ask(fix_prompt(pkt)).to_s
return nil if response.strip.empty? || response.strip == "UNCHANGED"
pkt.merge(candidate: response)
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "fix_pipeline.fix", event_bus: @bus)
end
def validate(pkt)
candidate = pkt[:candidate]
return nil unless candidate && candidate.strip != pkt[:src].strip
pkt.merge(valid: true)
end
def apply_stage(pkt)
return nil unless pkt[:valid]
path = pkt[:violation][:file]
tmp = "#{path}.pipeline.#{Process.pid}.tmp"
File.write(tmp, pkt[:candidate], encoding: "UTF-8")
File.rename(tmp, path)
@bus&.publish("fix_pipeline:applied", file: path)
pkt
rescue StandardError => e
File.delete(tmp) if defined?(tmp) && File.exist?(tmp) rescue nil
@bus&.publish("fix_pipeline:write_error", file: path, error: e.message)
nil
end
def fix_prompt(pkt)
v = pkt[:violation]
"Fix: #{v[:rule]} — #{v[:message]} at line #{v[:line]} in #{File.basename(v[:file])}.\n" \
"Return only the corrected file or UNCHANGED.\n\n```\n#{pkt[:src]}\n```"
end
end
end
end# frozen_string_literal: true
require "tty-prompt"
module Master
module Loop
class Governor
RATE_WINDOW = 60.0
TIERS = { safe: 0, guarded: 1, dangerous: 2 }.freeze
# Sliding-window rate limits per tier (calls per minute).
TIER_RATE_LIMITS = { guarded: 10, dangerous: 3 }.freeze
def initialize(config:, event_bus: nil)
@config = config
@bus = event_bus
@prompt = $stdout.isatty ? TTY::Prompt.new : nil
@auto = config.auto?
@approve_all = false
@rate_windows = Hash.new { |h, k| h[k] = [] }
@rate_mutex = Mutex.new
end
def check_permit(tool_name, tier, description = nil)
@bus&.publish("tool:before", tool: tool_name, tier:)
if (rate_err = check_rate_limit!(tier))
@bus&.publish("tool:rate_limited", tool: tool_name, tier:)
return rate_err
end
case tier
when :safe then return Result.ok(true)
when :guarded then return Result.ok(true) if @auto || @approve_all
when :dangerous
return Result.ok(true) if @auto || @approve_all
return Result.ok(true) unless needs_human?(description)
end
ask_user(tool_name, tier, description)
rescue StandardError => e
Result.err(e.message, category: :validation)
end
alias permit? check_permit
def approve_all! = @approve_all = true
def reset_approve! = @approve_all = false
private
PRIVILEGE_RE = /\b(?:doas|sudo|su)\b/.freeze
def needs_human?(description)
description.to_s.match?(PRIVILEGE_RE)
end
def check_rate_limit!(tier)
limit = TIER_RATE_LIMITS[tier]
return unless limit
now = Time.now.to_f
@rate_mutex.synchronize do
calls = @rate_windows[tier]
calls.reject! { |t| now - t > RATE_WINDOW }
if calls.size >= limit
return Result.err("rate limit: #{tier} tier (#{limit}/min)", category: :rate_limit)
end
calls << now
end
nil
end
def ask_user(tool_name, tier, description)
return Result.err("non-TTY: cannot prompt for approval", category: :validation) unless @prompt
label = description ? "#{tool_name}: #{description}" : tool_name
choice = @prompt.select("#{tier_icon(tier)} #{label}", [
{ name: "approve", value: :approve },
{ name: "deny", value: :deny },
{ name: "quit", value: :quit }
])
case choice
when :approve then Result.ok(true)
when :deny then @bus&.publish("tool:denied", tool: tool_name)
Result.err("denied by user", category: :validation)
when :quit then Result.err("quit", category: :shutdown)
end
end
def tier_icon(tier)
case tier
when :safe then "i"
when :guarded then "!"
when :dangerous then "!!"
end
end
end
end
end# frozen_string_literal: true
require "yaml"
module Master
module Loop
class Heartbeat
include Master::Ground::AtomicWrite
POLL_INTERVAL = 60
JOURNAL_KEEP = 50
DATA_PATH = File.join(Master::ROOT, "data", "heartbeat.yml").freeze
STATE_PATH = ".master/heartbeat_state.yml".freeze
RESULT_TRUNCATE = 200
SECONDS_PER_HOUR = 3600
SECONDS_PER_2HOURS = 7200
JOB_HANDLERS = {
"prune_memory" => :prune_memory,
"check_models" => :check_model_availability,
"self_test" => :run_self_test,
"prune_undo" => :prune_undo_journal,
"snapshot" => :run_snapshot
}.freeze
def initialize(root:, agent: nil, scanner: nil, memory: nil, event_bus: nil, homeostat: nil)
@root = root
@agent = agent
@scanner = scanner
@memory = memory
@bus = event_bus
@homeostat = homeostat
@jobs = load_jobs
@state = load_state
@thread = nil
@stop = false
end
def start!
return if @jobs.empty?
@stop = false
@thread = Thread.new do
loop do
break if @stop
run_due!
@homeostat&.observe(:idle_tick)
sleep POLL_INTERVAL
end
rescue StandardError => e
@bus&.publish("heartbeat:error", message: e.message)
end
end
def stop!
@stop = true
@thread&.kill
@thread = nil
end
def run_due!
now = Time.now.to_i
results = []
@jobs.each do |job|
name = job["name"]
interval = job["interval_seconds"].to_i
last_run = @state.dig(name, "last_run").to_i
next unless now - last_run >= interval
@bus&.publish("heartbeat:run", job: name)
result = execute_job(job)
@state[name] = { "last_run" => now, "result" => result.to_s[0, RESULT_TRUNCATE] }
results << { name: name, result: result }
end
persist_state unless results.empty?
results
end
def list
@jobs.map do |job|
last = @state.dig(job["name"], "last_run").to_i
ago = last.zero? ? "never" : "#{(Time.now.to_i - last) / 60}m ago"
"#{job["name"]}: every #{job["interval_seconds"] / 60}m, last: #{ago}"
end.join("\n")
end
private
def execute_job(job)
method_name = JOB_HANDLERS[job["action"]]
return "unknown action: #{job["action"]}" unless method_name
Master::Trace::Telemetry.span("heartbeat.tick", job: job["name"].to_s) do
send(method_name)
end
rescue StandardError => e
"error: #{e.message}"
end
def prune_memory
@memory&.consolidate!(agent: @agent) || "no memory"
end
def check_model_availability
return "no agent" unless @agent
id = @agent.model.to_s
return "no active model" if id.empty?
alive = model_reachable?(id)
"model: #{id.split("/").last} #{alive ? "reachable" : "unreachable"}"
end
def model_reachable?(model_id)
RubyLLM.chat(model: model_id).ask("ping")
true
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "heartbeat.model_reachable?", event_bus: @bus, model_id:)
false
end
def run_self_test
return "no scanner" unless @scanner
target = File.join(@root, "lib")
result = @scanner.scan_dir(target, depth: :deep)
return "scan failed" unless Result.wrap(result).ok?
count = result.value!.sum { |_, fr| Result.wrap(fr).value_or([]).size }
@bus&.publish("heartbeat:self_test", violations: count)
"self-test: #{count} violations"
end
def prune_undo_journal
journal_path = File.join(@root, ".master", "undo.jsonl")
return "no journal" unless File.exist?(journal_path)
lines = File.readlines(journal_path)
return "journal empty" if lines.empty?
keep = [lines.size / 2, JOURNAL_KEEP].max
File.write(journal_path, lines.last(keep).join)
"pruned undo: kept #{keep}/#{lines.size} entries"
end
def run_snapshot
container = { root: @root, bus: @bus }
Builder.boot_snapshot(container)
"snapshot: generated"
end
def load_jobs
path = File.join(@root, "data", "heartbeat.yml")
return default_jobs unless File.exist?(path)
result = Master.load_yaml(path)
jobs = result.is_a?(Array) ? result : default_jobs
jobs.select { |j| j["enabled"] != false }
rescue StandardError => _e
default_jobs
end
def default_jobs
[
{ "name" => "prune_memory", "action" => "prune_memory", "interval_seconds" => SECONDS_PER_HOUR },
{ "name" => "self_test", "action" => "self_test", "interval_seconds" => SECONDS_PER_2HOURS },
{ "name" => "prune_undo", "action" => "prune_undo", "interval_seconds" => 86_400 },
{ "name" => "snapshot", "action" => "snapshot", "interval_seconds" => 14_400 }
]
end
def load_state
path = File.join(@root, STATE_PATH)
return {} unless File.exist?(path)
Master.load_yaml(path) || {}
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "heartbeat.load_state", event_bus: @bus, path:)
{}
end
def persist_state
path = File.join(@root, STATE_PATH)
FileUtils.mkdir_p(File.dirname(path))
write_atomic(path, @state.to_yaml)
end
end
end
end# frozen_string_literal: true
module Master
module Loop
# Continuous-time homeostatic drives (CTCS-HRRL, arXiv 2401.08999).
# State vector decays toward setpoint; events shift it; readers bias routing,
# reasoning depth, and persona mood. No external deps.
class Homeostat
DRIVES = {
energy: { setpoint: 0.7, decay: 0.02 },
error_rate: { setpoint: 0.0, decay: 0.05 },
novelty_hunger: { setpoint: 0.5, decay: 0.01 },
fatigue: { setpoint: 0.0, decay: 0.03 },
satiety: { setpoint: 0.6, decay: 0.01 }
}.freeze
EVENT_DELTAS = {
llm_call: { energy: -0.05, fatigue: +0.03 },
llm_success: { error_rate: -0.04, satiety: +0.06, novelty_hunger: -0.02 },
llm_failure: { error_rate: +0.15, satiety: -0.08, energy: -0.04 },
tool_call: { fatigue: +0.01 },
tool_failure: { error_rate: +0.08, fatigue: +0.02 },
novel_task: { novelty_hunger: -0.20, energy: +0.03 },
idle_tick: {}
}.freeze
def state = @mutex.synchronize { @state.dup }
def initialize(event_bus: nil)
@bus = event_bus
@mutex = Mutex.new
@state = DRIVES.transform_values { |spec| spec[:setpoint] }
@started_at = Time.now
end
def observe(event, **_kwargs)
deltas = EVENT_DELTAS[event] || {}
snap = @mutex.synchronize do
deltas.each { |k, v| @state[k] = clamp(@state[k] + v) }
decay_drift!
@state.dup
end
@bus&.publish("homeostat:observe", event: event, state: snap)
snap
end
def model_tier_bias
return :cheap if @state[:error_rate] > 0.4 || @state[:fatigue] > 0.7
return :strong if @state[:novelty_hunger] > 0.7 && @state[:energy] > 0.5
:default
end
def reasoning_depth_bias
score = @state[:energy] - @state[:fatigue] - @state[:error_rate]
return 2 if score > 0.5
return 1 if score > 0.0
0
end
def mood
return :tense if @state[:error_rate] > 0.4
return :weary if @state[:fatigue] > 0.6 || @state[:energy] < 0.3
return :curious if @state[:novelty_hunger] > 0.6
:focused
end
def circadian_phase
h = Time.now.hour
return :morning if (5..11).cover?(h)
return :afternoon if (12..17).cover?(h)
return :evening if (18..22).cover?(h)
:night
end
def summary
pairs = @state.map { |k, v| "#{k}=#{format("%.2f", v)}" }.join(" ")
"homeostat: #{pairs} | mood=#{mood} phase=#{circadian_phase}"
end
def to_h
{ state: @state.dup, mood: mood, phase: circadian_phase, tier: model_tier_bias }
end
private
def decay_drift!
DRIVES.each do |drive, spec|
gap = spec[:setpoint] - @state[drive]
@state[drive] = clamp(@state[drive] + gap * spec[:decay])
end
end
def clamp(value) = value.clamp(0.0, 1.0)
end
end
end# frozen_string_literal: true
require "open3"
require "tempfile"
module Master
module Loop
# Architecture #5: apply a unified diff patch to source text.
# Calls system `patch`(1) — available on OpenBSD base and most Linux distros.
# Rejects malformed or no-op patches; never applies blindly.
class PatchApplier
# Files smaller than this are cheaper to rewrite in full — skip diff mode.
DIFF_THRESHOLD = 8_192 # bytes
Success = Struct.new(:source, keyword_init: true)
Failure = Struct.new(:reason, keyword_init: true)
def self.apply(original, diff_text)
return Failure.new(reason: "empty diff") if diff_text.strip.empty?
return Failure.new(reason: "not a diff") unless diff_text.include?("@@")
new(original, diff_text).apply
end
def initialize(original, diff_text)
@original = original
@diff = diff_text
end
def apply
Tempfile.open(["master_patch", ".src"]) do |f|
f.write(@original)
f.flush
_out, err, status = Open3.capture3("patch", "--no-backup-if-mismatch", "-s", f.path, stdin_data: @diff)
return Failure.new(reason: err.strip[0, 200]) unless status.success?
result = File.read(f.path, encoding: "UTF-8")
return Failure.new(reason: "no change") if result.strip == @original.strip
Success.new(source: result)
end
rescue StandardError => e
Failure.new(reason: e.message[0, 200])
end
end
end
end# frozen_string_literal: true
require "yaml"
require "fileutils"
module Master
module Loop
# Asks the agent for N radically simplified tree layouts, ranks them by
# sketch compactness (radical = fewer files), writes top K to runtime/proposals.md.
# Triggered manually via /propose-tree or by the bus on fix_loop:clean|plateau.
class ProposeTree
OUT_PATH = "runtime/proposals.md"
DRAFT_N = 10
KEEP_TOP = 3
COOLDOWN_SECS = 86_400
def initialize(root:, agent:, event_bus: nil)
@root = root
@agent = agent
@bus = event_bus
end
def call(n: DRAFT_N)
return cooldown_message if cooling_down?
tree = current_tree
response = @agent.ask(prompt(tree, n))
proposals = parse(response)
return "propose-tree: parse failed" if proposals.empty?
top = rank(proposals).first(KEEP_TOP)
write_report(top, proposals.size)
@bus&.publish("propose_tree:done", drafted: proposals.size, kept: top.size)
"propose-tree: drafted #{proposals.size}, kept top #{top.size} → #{OUT_PATH}"
rescue StandardError => e
@bus&.publish("propose_tree:error", error: e.message)
"propose-tree: #{e.message}"
end
private
def out_file = File.join(@root, OUT_PATH)
def cooling_down?
File.exist?(out_file) && (Time.now - File.mtime(out_file)) < COOLDOWN_SECS
end
def cooldown_message
mins = ((COOLDOWN_SECS - (Time.now - File.mtime(out_file))) / 60).to_i
"propose-tree: cooldown — next run in #{mins}m"
end
def current_tree
entries = Dir.glob(File.join(@root, "lib", "*")).sort.flat_map { |e| describe_entry(e) }
entries.first(120).join("\n")
end
def describe_entry(entry)
return [File.basename(entry)] unless File.directory?(entry)
subs = Dir.glob(File.join(entry, "*")).sort.map { |f| " #{File.basename(f)}#{File.directory?(f) ? "/" : ""}" }
["#{File.basename(entry)}/", *subs]
end
def prompt(tree, n)
<<~PROMPT
Propose #{n} radically simplified file/folder layouts for this Ruby project (Master).
Current lib/ structure:
#{tree}
For each proposal return a YAML map with keys: name, summary, wins, costs, sketch.
- name: kebab-case identifier
- summary: one line
- wins: 2-3 lines (pros, separated by " · ")
- costs: 2-3 lines (cons, separated by " · ")
- sketch: ASCII tree, ≤18 lines, indented with 2 spaces per level
Be radical: collapse modules, merge concepts, rename for clarity.
No incrementalism. No prose around the YAML.
Return a YAML array of #{n} entries.
PROMPT
end
def parse(text)
body = text.sub(/\A.*?(?=^- |\A- )/m, "").sub(/\n```.*\z/m, "").sub(/\A```ya?ml\n/, "")
data = YAML.safe_load(body, aliases: false)
data.is_a?(Array) ? data.select { |e| e.is_a?(Hash) && e["name"] } : []
rescue Psych::Exception
[]
end
def rank(proposals)
proposals.sort_by { |p| p["sketch"].to_s.lines.size }
end
def write_report(top, total)
FileUtils.mkdir_p(File.dirname(out_file))
body = ["# tree proposals — #{Time.now.utc.iso8601}", "",
"drafted: #{total} · kept top: #{top.size}", ""]
top.each_with_index do |p, i|
body << "## #{i + 1}. #{p["name"]}" << ""
body << p["summary"].to_s << ""
body << "wins: #{p["wins"]}" << "costs: #{p["costs"]}" << ""
body << "```" << p["sketch"].to_s.rstrip << "```" << ""
end
File.write(out_file, body.join("\n"))
end
end
end
end# frozen_string_literal: true
require "open3"
module Master
module Loop
module Repair
class GitHistoryMiner
DEFAULT_PATTERNS = [
/fix/i,
/refactor/i,
/rollback/i,
/repair/i,
/runtime/i,
/telemetry/i,
/namespace/i
].freeze
def initialize(root: Dir.pwd)
@root = root
end
def recent(limit: 100)
out, = Open3.capture2e(
"git",
"log",
"--oneline",
"-n",
limit.to_s,
chdir: @root
)
out.lines.map(&:strip)
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "git_history_miner.recent_commits")
[]
end
def valuable(limit: 100, patterns: DEFAULT_PATTERNS)
recent(limit: limit).select do |line|
patterns.any? { |pattern| pattern.match?(line) }
end
end
end
end
end
end# frozen_string_literal: true
require "tempfile"
require_relative "../ground/atomic_write"
require_relative "constants"
require_relative "fix_helpers"
require_relative "patch_applier"
module Master
module Loop
# Single-pass fixer for one rule across a set of files.
# FixLoop owns the outer convergence loop; RuleLoop fixes one batch per call.
#
# Fix routing (per violation severity + file size):
# error tier → council_fix (3-reviewer veto before apply)
# large file → diff_fix (unified diff patch; arch #5)
# small file → genetic_fix (N candidates, rescan, best wins; arch #9)
class RuleLoop
RATE_LIMIT_SLEEP = 10
MAX_FIX_RETRIES = 2
CANDIDATE_COUNT = 3 # arch #9
SEVERITY_RANK = Master::SEVERITY_RANK
MIN_SEVERITY = SEVERITY_RANK[:warning]
include Master::Ground::AtomicWrite
include Master::Loop::FixHelpers
def initialize(rule:, agent:, scanner:, root:, bus: nil, learnings: nil)
@rule = rule
@agent = agent
@scanner = scanner
@root = root
@bus = bus
@learnings = learnings
end
def injected_preamble=(text)
@injected_preamble = text
end
# One pass: scan → fix each violating file once → return { fixed:, status: }.
def run_once(files)
violations = scan_files(files)
return { fixed: 0, status: :clean } if violations.empty?
fixed = fix_batch(violations)
status = fixed > 0 ? :fixed : :stuck
record_outcomes(files, fixed > 0 ? :fixed : :stuck)
@bus&.publish("rule_loop:pass", rule: @rule.id, violations: violations.size, fixed:, status:)
{ fixed:, status: }
rescue StandardError => e
@bus&.publish("rule_loop:error", rule: @rule.id, error: e.message)
{ fixed: 0, status: :error }
end
private
def scan_files(files)
files.flat_map do |path|
next [] unless File.exist?(path)
result = @scanner.scan(path, rules: [@rule])
next [] unless result.ok?
ext = File.extname(path).downcase
result.value!
.select { |f| (SEVERITY_RANK[f[:severity]] || 0) >= MIN_SEVERITY }
.map { |f| f.to_h.merge(file: path, ext:) }
end
end
def fix_batch(violations)
fixed = 0
violations.uniq { |v| v[:file] }.each do |v|
new_src = v[:severity].to_sym == :error ? council_fix(v) : request_fix(v)
apply(v[:file], new_src) && (fixed += 1) if new_src
end
fixed
end
# Architecture #6: three-reviewer veto for error-tier violations.
def council_fix(violation)
path = violation[:file]
return unless File.exist?(path)
src = File.read(path, encoding: "UTF-8")
prompt = <<~PROMPT
#{preamble}
File: #{File.basename(path)}
Rule violated (severity: ERROR): #{violation[:rule]}
Line #{violation[:line]}: #{violation[:message]}
#{violation[:fix].to_s.empty? ? "" : "Suggested fix: #{violation[:fix]}"}
Three reviewers assess before any fix is applied:
As Skeptic: Is this a real violation or a false positive? What is the blast radius?
As Security: Does this create an attack surface? What must the fix preserve?
As Maintainer: What is the minimum change that eliminates the violation without drift?
Produce the corrected file only if all three agree the fix is safe.
If any reviewer would block, return exactly: UNCHANGED
```
#{src}
```
PROMPT
MAX_FIX_RETRIES.times do |attempt|
sleep RATE_LIMIT_SLEEP * attempt if attempt.positive?
response = @agent.ask(prompt).to_s
return nil if response.strip == "UNCHANGED"
code = extract_code(response, File.extname(path).downcase)
return code if code && code.strip != src.strip
rescue StandardError => e
next if Master::Loop::Constants::TRANSIENT_RE.match?(e.message.to_s)
@bus&.publish("rule_loop:council_error", rule: @rule.id, file: path, error: e.message[0, 120])
return nil
end
nil
end
# Architecture #5 + #9: diff for large files, genetic candidates for small.
def request_fix(violation)
path = violation[:file]
return unless File.exist?(path)
src = File.read(path, encoding: "UTF-8")
src.bytesize > PatchApplier::DIFF_THRESHOLD ? diff_fix(violation, src, path) : genetic_fix(violation, src, path)
end
# Architecture #5: unified diff patch — safe on large files.
def diff_fix(violation, src, path)
prompt = build_diff_prompt(violation, src, path)
MAX_FIX_RETRIES.times do |attempt|
sleep RATE_LIMIT_SLEEP * attempt if attempt.positive?
response = @agent.ask(prompt).to_s
next if response.strip == "UNCHANGED"
result = PatchApplier.apply(src, response)
return result.source if result.is_a?(PatchApplier::Success)
rescue StandardError => e
next if Master::Loop::Constants::TRANSIENT_RE.match?(e.message.to_s)
@bus&.publish("rule_loop:fix_error", rule: @rule.id, file: path, error: e.message[0, 120])
return nil
end
nil
end
# Architecture #9: generate CANDIDATE_COUNT fixes, rescan each, apply lowest-violation winner.
def genetic_fix(violation, src, path)
ext = File.extname(path).downcase
prompt = build_prompt(violation, src, path)
candidates = CANDIDATE_COUNT.times.filter_map do |attempt|
sleep RATE_LIMIT_SLEEP if attempt.positive?
code = extract_code(@agent.ask(prompt).to_s, ext)
code if code && code.strip != src.strip
rescue StandardError => e
next if Master::Loop::Constants::TRANSIENT_RE.match?(e.message.to_s)
@bus&.publish("rule_loop:fix_error", rule: @rule.id, file: path, error: e.message[0, 120])
nil
end
best_candidate(candidates, path)
end
def best_candidate(candidates, path)
return nil if candidates.empty?
return candidates.first if candidates.size == 1
scored = candidates.filter_map { |c| [rescan_candidate(c, path), c] }
scored.empty? ? candidates.first : scored.min_by(&:first).last
rescue StandardError
candidates.first
end
def rescan_candidate(candidate, path)
Tempfile.open(["rl_score", File.extname(path)]) do |f|
f.write(candidate); f.flush
result = @scanner.scan(f.path, rules: [@rule])
result.ok? ? result.value!.size : 99
end
rescue StandardError
99
end
def apply(path, new_src)
write_atomic(path, new_src, encoding: "UTF-8")
@bus&.publish("rule_loop:fix_applied", rule: @rule.id, file: path)
true
rescue StandardError => e
@bus&.publish("rule_loop:write_error", rule: @rule.id, file: path, error: e.message)
false
end
def build_prompt(violation, src, path)
lang = Master::Judge::Scan::Rule::EXT_LANG.fetch(File.extname(path).downcase, "text")
fix_hint = violation[:fix].to_s.strip
<<~PROMPT
#{preamble}
File: #{File.basename(path)} (#{lang})
Rule violated: #{violation[:rule]}
Line #{violation[:line]}: #{violation[:message]}
#{fix_hint.empty? ? "" : "How to fix: #{fix_hint}"}
Return ONLY the corrected file. If unsafe to autofix, return exactly: UNCHANGED
```#{lang}
#{src}
```
PROMPT
end
def build_diff_prompt(violation, src, path)
lang = Master::Judge::Scan::Rule::EXT_LANG.fetch(File.extname(path).downcase, "text")
fix_hint = violation[:fix].to_s.strip
<<~PROMPT
#{preamble}
File: #{File.basename(path)} (#{lang})
Rule violated: #{violation[:rule]}
Line #{violation[:line]}: #{violation[:message]}
#{fix_hint.empty? ? "" : "How to fix: #{fix_hint}"}
Return a unified diff patch only (like `diff -u`). Fix only the violation.
If unsafe to autofix, return exactly: UNCHANGED
```#{lang}
#{src}
```
PROMPT
end
def preamble
@preamble ||= @injected_preamble || begin
soul = Master.load_yaml(File.join(Master::ROOT, "data", "soul.yml"))
abs = soul.fetch("absolute", {})
golden = abs["golden_rule"] || "PRESERVE_THEN_IMPROVE_NEVER_BREAK"
lines = ["Golden rule: #{golden}",
"Minimum change that eliminates the violation. Do not touch unrelated code."]
abs.fetch("code_rules", {}).each { |k, v| lines << "- #{k}: #{v}" }
abs.fetch("aesthetic_rules", {}).each { |k, v| lines << "- #{k}: #{v}" }
lines.join("\n")
rescue StandardError
"Golden rule: PRESERVE_THEN_IMPROVE_NEVER_BREAK"
end
end
# Architecture #10: record fix quality in learnings store.
def record_outcomes(files, outcome)
return unless @learnings
ext = files.filter_map { |f| File.extname(f).downcase.delete(".").presence }.tally.max_by { |_, n| n }&.first || "unknown"
@learnings.record(rule: @rule.id, file_type: ext, outcome:)
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "rule_loop.record_outcomes", event_bus: @bus, rule: @rule.id)
end
end
end
end# frozen_string_literal: true
module Master
module Loop
# Architecture #7: file-watcher reactive trigger — no polling.
# Linux: inotify via rb-inotify. OpenBSD: kqueue via rb-kqueue.
# A changed file triggers a targeted RuleLoop pass on that file only.
# The system quiesces naturally — no STARTUP_DELAY, no idle sleep waste.
#
# Usage (VPS, after `gem install rb-kqueue` or `rb-inotify`):
# WatchLoop.new(rules:, agent:, scanner:, root:, bus:).run
class WatchLoop
DEBOUNCE_SECONDS = 1.0
SKIP_DIRS = FixLoop::SKIP_DIRS
def initialize(rules:, agent:, scanner:, root:, bus: nil, learnings: nil)
@rules = rules
@agent = agent
@scanner = scanner
@root = root
@bus = bus
@learnings = learnings
@queue = Queue.new
@watcher = build_watcher
end
def run
@bus&.publish("watch_loop:start", root: @root)
Thread.new { drain_queue }
@watcher.run
rescue LoadError => e
@bus&.publish("watch_loop:unavailable", reason: e.message)
end
private
def build_watcher
require_kqueue_or_inotify
rescue LoadError
raise
end
# Drains the queue with debounce — coalesces rapid file events.
def drain_queue
pending = {}
loop do
path = @queue.pop
next if SKIP_DIRS.any? { |d| path.include?(d) }
pending[path] = Time.now.to_f
sleep DEBOUNCE_SECONDS
now = Time.now.to_f
ready = pending.select { |_, t| now - t >= DEBOUNCE_SECONDS }.keys
ready.each do |p|
pending.delete(p)
run_rules_on(p)
end
end
end
def run_rules_on(path)
return unless File.exist?(path)
@rules.each do |rule|
rl = RuleLoop.new(rule:, agent: @agent, scanner: @scanner, root: @root, bus: @bus, learnings: @learnings)
result = rl.run([path])
@bus&.publish("watch_loop:file_pass", file: path, rule: rule.id, **result)
end
rescue StandardError => e
@bus&.publish("watch_loop:error", file: path, error: e.message)
end
# Platform-specific watcher. Enqueues paths into @queue on change events.
def require_kqueue_or_inotify
if RUBY_PLATFORM.include?("openbsd") || RUBY_PLATFORM.include?("freebsd")
require "rb-kqueue"
queue = KQueue::Queue.new
queue.watch(@root, :recursive, :write, :rename) { |ev| @queue << ev.path.to_s }
queue
else
require "rb-inotify"
n = INotify::Notifier.new
n.watch(@root, :close_write, :moved_to, :recursive) { |ev| @queue << ev.absolute_name }
n
end
end
end
end
end# frozen_string_literal: true
require "open3"
require "time"
module Master
module Loop
# Watcher — continuous OpenBSD load monitor.
# Polls load avg, memory, disk, master service. Publishes system:sample
# every interval, system:warn/crit on threshold crossings.
class Watcher
DEFAULT_INTERVAL = 30
@@last_sample = nil
def self.last_sample = @@last_sample
# One-shot sample without a running watcher. Used by /status.
def self.sample_once(root: Master::ROOT)
new(bus: nil, root:).sample!
end
def initialize(bus:, root:, interval: nil)
@bus = bus
@root = root
cfg = load_config
@interval = interval || cfg["interval_seconds"] || DEFAULT_INTERVAL
@thresholds = cfg["thresholds"] || {}
@prev_level = :ok
end
def run_forever
loop do
sample!
sleep @interval
end
rescue StandardError => e
@bus&.publish("watcher:error", error: e.message)
end
def sample!
s = build_sample
@@last_sample = s
level = classify(s)
case level
when :crit then @bus&.publish("system:crit", s.merge(level: "crit"))
when :warn then @bus&.publish("system:warn", s.merge(level: "warn")) if @prev_level != :warn
else @bus&.publish("system:sample", s)
end
@prev_level = level
s
end
private
def build_sample
{
ts: Time.now.utc.iso8601,
load_1m: load_avg_1m,
mem_free_pct: mem_free_pct,
disk_root_pct: disk_root_pct,
master_rss_mb: master_rss_mb,
master_alive: master_alive?
}
end
def load_avg_1m
out, _, st = Open3.capture3("/sbin/sysctl", "-n", "vm.loadavg")
return nil unless st.success?
out.tr("{}", "").strip.split.first&.to_f
rescue StandardError
nil
end
# OpenBSD does not expose vm.uvmexp.free via sysctl — parse vmstat instead.
def mem_free_pct
out, _, st = Open3.capture3("/usr/bin/vmstat")
return nil unless st.success?
cols = out.lines.last.to_s.strip.split
return nil if cols.length < 4
free_bytes = parse_size(cols[3])
total, _, st2 = Open3.capture3("/sbin/sysctl", "-n", "hw.physmem")
return nil unless st2.success? && total.to_f.positive?
((free_bytes / total.to_f) * 100).round(1)
rescue StandardError
nil
end
def parse_size(str)
case str
when /\A(\d+(?:\.\d+)?)G\z/i then Regexp.last_match(1).to_f * 1_073_741_824
when /\A(\d+(?:\.\d+)?)M\z/i then Regexp.last_match(1).to_f * 1_048_576
when /\A(\d+(?:\.\d+)?)K\z/i then Regexp.last_match(1).to_f * 1024
else str.to_f
end
end
def disk_root_pct
out, _, st = Open3.capture3("/bin/df", "-k", "/")
return nil unless st.success?
out.lines[1].to_s.split[4].to_s.tr("%", "").to_i
rescue StandardError
nil
end
# The master daemon runs as `falcon serve` on port 53187.
def master_rss_mb
out, _, st = Open3.capture3("/bin/ps", "-Ao", "rss,command")
return nil unless st.success?
rss_kb = out.lines
.select { |l| l.include?("falcon serve") || l.include?(":53187") }
.sum { |l| l.strip.split.first.to_i }
return nil if rss_kb.zero?
(rss_kb / 1024.0).round
rescue StandardError
nil
end
# nil = unknown (e.g. rcctl errored); false = explicitly down.
def master_alive?
_, _, st = Open3.capture3("/usr/sbin/rcctl", "check", "master")
st.success?
rescue StandardError
nil
end
def classify(s)
return :crit if s[:master_alive] == false ||
over?(s[:load_1m], "load_avg_1m", "crit") ||
under?(s[:mem_free_pct], "mem_free_pct", "crit") ||
over?(s[:disk_root_pct], "disk_root_pct", "crit") ||
over?(s[:master_rss_mb], "master_rss_mb", "crit")
return :warn if over?(s[:load_1m], "load_avg_1m", "warn") ||
under?(s[:mem_free_pct], "mem_free_pct", "warn") ||
over?(s[:disk_root_pct], "disk_root_pct", "warn") ||
over?(s[:master_rss_mb], "master_rss_mb", "warn")
:ok
end
def over?(v, key, level)
t = @thresholds.dig(key, level)
v && t && v.to_f >= t.to_f
end
def under?(v, key, level)
t = @thresholds.dig(key, level)
v && t && v.to_f <= t.to_f
end
def load_config
Master.load_yaml(File.join(@root, "data", "load.yml")) || {}
rescue StandardError
{}
end
end
end
end# frozen_string_literal: true
require "zeitwerk"
require "yaml"
# Pre-load openssl before pledge stage1 engages — faraday-net_http requires it
# lazily on first HTTPS call, which fails after unveil restricts dlopen paths.
begin
require "openssl"
rescue LoadError => e
warn "openssl: #{e.message} — LLM calls will fail"
end
module Master
ROOT = File.expand_path("..", __dir__).freeze
DATA = File.join(ROOT, "data").freeze
COUNCIL_PATH = File.join(DATA, "council.yml").freeze
RULES_PATH = File.join(DATA, "rules.yml").freeze
BUNDLE_BIN = RUBY_PLATFORM.include?("openbsd") ? "bundle34" : "bundle"
MIN_API_KEY_LENGTH = 20
NEMOTRON_PRIMARY = "nvidia/nemotron-3-super-120b-a12b:free"
SEVERITY_RANK = { info: 0, warning: 1, error: 2, critical: 3 }.freeze
CTX_WINDOW_SIZE = 200_000
VIOLATION_TRUNCATE = 90
FILE_LANGUAGE_MAP = {
".rb" => "ruby", ".yml" => "yaml", ".yaml" => "yaml",
".js" => "javascript", ".json" => "json", ".sh" => "bash",
".zsh" => "bash", ".md" => "markdown", ".html" => "html",
".erb" => "erb", ".css" => "css"
}.freeze
API_KEY_PROVIDERS = {
anthropic_api_key: "ANTHROPIC_API_KEY",
openai_api_key: "OPENAI_API_KEY",
gemini_api_key: "GEMINI_API_KEY",
openrouter_api_key: "OPENROUTER_API_KEY",
mistral_api_key: "MISTRAL_API_KEY",
deepseek_api_key: "DEEPSEEK_API_KEY"
}.freeze
loader = Zeitwerk::Loader.new
loader.push_dir(__dir__, namespace: Master)
loader.ignore(__FILE__)
loader.inflector.inflect(
"cli" => "CLI",
"llm" => "LLM",
"llm_dispatcher" => "LLMDispatcher",
"mcp_server" => "MCPServer",
"mcp_coordinator" => "McpCoordinator",
"diff_stager" => "DiffStager",
"code_index" => "CodeIndex",
"git_context" => "GitContext",
"ast_edit" => "AstEdit",
"rule_dsl" => "RuleDSL",
"tts" => "TTS",
"pwa_audit" => "PwaAudit",
"mobile_pwa_operator" => "MobilePwaOperator"
)
loader.enable_reloading if defined?(MASTER_DEV_MODE) || ENV["MASTER_DEV"].to_s == "1"
loader.ignore(File.join(__dir__, "reach", "ruby_llm_patch.rb"))
loader.ignore(File.join(__dir__, "reach", "bedrock_stub.rb"))
%w[
now/cli/signals.rb
now/cli/command_ops.rb
now/cli/thinking_indicator.rb
now/command_registry/memory_commands.rb
now/command_registry/work_commands.rb
now/command_registry/system_commands.rb
now/command_registry/tool_commands.rb
judge/scan/rules/lexical_rules.rb
judge/scan/rules/ruby_rules.rb
judge/scan/rules/web_rules.rb
judge/scan/rules/js_rules.rb
judge/scan/rules/universal_rules.rb
].each do |rel|
loader.ignore(File.join(__dir__, rel))
end
loader.setup
def self.configure_providers!
# Stub Bedrock before ruby_llm loads — avoids openssl.so on OpenBSD/LibreSSL.
# MASTER only uses OpenRouter; Bedrock is never needed.
require_relative "reach/bedrock_stub"
require "ruby_llm"
require_relative "reach/ruby_llm_patch"
RubyLLM.configure do |cfg|
API_KEY_PROVIDERS.each do |attr, env_var|
api_key = ENV[env_var].to_s
cfg.public_send("#{attr}=", api_key) if api_key.length >= MIN_API_KEY_LENGTH
end
end
end
def self.api_key_present?(env_var)
ENV[env_var].to_s.length >= MIN_API_KEY_LENGTH
end
def self.default_model
return NEMOTRON_PRIMARY if api_key_present?("OPENROUTER_API_KEY")
return "claude-opus-4-7" if api_key_present?("ANTHROPIC_API_KEY")
return "deepseek-chat" if api_key_present?("DEEPSEEK_API_KEY")
return "gpt-4o" if api_key_present?("OPENAI_API_KEY")
return "gemini-2.5-flash" if api_key_present?("GEMINI_API_KEY")
return "mistral-large-latest" if api_key_present?("MISTRAL_API_KEY")
NEMOTRON_PRIMARY
end
def self.any_api_key_present?
API_KEY_PROVIDERS.any? { |_, env_var| api_key_present?(env_var) }
end
def self.no_api_key_message
"I'm not wired to any LLM yet. The primary model is nemotron via OpenRouter " \
"(free). Set OPENROUTER_API_KEY in /etc/rc.d/master daemon_flags and restart " \
"with `doas rcctl restart master`. Other accepted keys: ANTHROPIC_API_KEY, " \
"DEEPSEEK_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY, MISTRAL_API_KEY."
end
def self.load_yaml(path, symbolize_names: false, default: {})
YAML.safe_load_file(path, aliases: true, symbolize_names: symbolize_names)
rescue Psych::Exception, Errno::ENOENT, Errno::EACCES => e
warn("load_yaml: " + e.message)
default
end
# Strict re-parse of every data/*.yml — silent swallow in load_yaml hides
# corruption from sweep/LLM rewrites. Boot must surface failures, not mask them.
def self.validate_data!(root: ROOT, bus: nil)
errors = {}
Dir.glob(File.join(root, "data", "**/*.yml")).sort.each do |path|
YAML.safe_load_file(path, aliases: true)
rescue Psych::Exception => e
errors[path.sub("#{root}/", "")] = e.message.lines.first.to_s.strip
end
return errors if errors.empty?
errors.each do |rel, msg|
warn("yaml_validation: #{rel}: #{msg}")
bus&.publish("data:yaml_parse_error", path: rel, error: msg)
end
errors
end
# Loads rules.yml meta + merges split data/rules/*.yml into ["rules"] key.
def self.load_rules(root: ROOT)
data_dir = File.join(root, "data")
base = load_yaml(File.join(data_dir, "rules.yml"))
rules_dir = File.join(data_dir, "rules")
merged = Dir.glob(File.join(rules_dir, "*.yml")).sort.each_with_object({}) do |f, h|
(load_yaml(f) || {}).each { |scope, list| (h[scope] ||= []).concat(Array(list)) }
end
base.merge("rules" => merged)
end
def self.build(root: Dir.pwd)
ENV["MASTER_SCAN_ONLY"] == "1" ? Builder.build_scan_only(root:) : Builder.build(root:)
end
def self.bootstrap_container(root: Dir.pwd)
Trace::Telemetry.bootstrap!(root: root)
container = Builder.build(root:)
validate_data!(root: root, bus: container[:bus])
Builder.boot_snapshot(container)
container[:heartbeat]&.start!
Thread.new do
Ground::Orders::ConstitutionDrift.new(container:).call
rescue StandardError => e
warn("constitution_drift: #{e.message}")
end
container
end
def self.boot(root: Dir.pwd)
Ground::Pledge.stage1_boot!(root)
container = bootstrap_container(root: root)
Ground::Pledge.stage2_lock!
Now::CLI.new(container:)
end
end# frozen_string_literal: true
require_relative "cli/signals"
require_relative "cli/command_ops"
require_relative "cli/thinking_indicator"
require "open3"
require "tty-reader"
require "tty-prompt"
require "fileutils"
module Master
module Now
class CLI
IDLE_SLEEP_DEFAULT = 60
REPLAY_TURNS = 5
DMESG_BUFFER = 80
SEVERITY_ICON = {
error: "!!",
warning: "!",
style: ".",
critical: "!!"
}.freeze
SLASH_COMMANDS = %w[/exit /undo /redo /checkpoint].freeze
attr_reader :container
def initialize(container:)
@container = container
assign_container_refs!(container)
@reader = TTY::Reader.new(track_history: true)
@running = false
@interrupt_at = Time.now
@last_ok = true
@violations = 0
@prev_violations = 0
@bg_thread = nil
@seen_violations = {}
@user_active = false
@focus_mode = false
@show_chips = false
@last_input = nil
@last_cost = 0.0
@dmesg_sub = nil
set_visitor_mode_if_unauthenticated
end
def run(initial_message = nil)
setup_signals
@session.load! if @session.exists?
start_background_loop
first_boot_bar
puts @renderer.splash(@agent.model)
puts @renderer.session_line(@session.name) if @session.name
print_repo_tree unless booted_before?
replay_recent_turns if @session.messages.any?
run_input(initial_message) if initial_message
@running = true
repl_loop
end
def pipe(input)
stripped = input.strip
return if stripped.empty?
run_input(stripped)
end
def process(input)
run_input(input)
end
def run_input(input)
return if input.strip.empty?
@user_active = true
@last_input = input
state = { streamed: false, thinking_shown: true }
accumulated = +""
on_chunk = stream_chunk_handler(accumulated, state)
print_thinking_indicator
@pipeline_thread = Thread.new do
Thread.current.report_on_exception = false
@pipeline.call(Result.ok(user_message: input, on_chunk: on_chunk))
end
result = begin
@pipeline_thread.value
rescue StandardError => _e
Result.err("aborted", category: :abort)
end
display_result(result, accumulated, state[:streamed])
ensure
@pipeline_thread = nil
stop_thinking_indicator
@user_active = false
end
def stream_chunk_handler(accumulated, state)
chunk_accumulator(accumulated) do |text|
if state[:thinking_shown] && $stdout.isatty
stop_thinking_indicator
print "\r\e[K"
state[:thinking_shown] = false
end
unless state[:streamed]
puts @renderer.speaker_tag
end
print text
$stdout.flush
state[:streamed] = true
end
end
private
def set_visitor_mode_if_unauthenticated
web_token = @config&.dig("web_token")
Fiber[:master_visitor] = true if web_token.nil? || web_token.empty?
end
def assign_container_refs!(deps)
@session = deps[:session]
@agent = deps[:agent]
@renderer = deps[:renderer]
@logging = deps[:logging]
@undo = deps[:undo]
@config = deps[:config]
@pipeline = deps[:pipeline]
@scanner = deps[:scanner]
@root = deps.fetch(:root, Dir.pwd)
@diff_stager = deps[:diff_stager]
@bus = deps[:bus]
end
def repl_loop
while @running
unless @focus_mode
status = @renderer.status_row(
uptime: @renderer.uptime, turns: @session.messages.size, violations: @violations
)
puts status if status
sugg = suggested_next_prompt
puts @renderer.render(" ↳ #{sugg}", mode: :dim) if sugg
tokens = @session.token_est
prompt_lines = @renderer.prompt_line(
@agent.model, @session.phase,
last_ok: @last_ok, violations: @violations, tokens: tokens, cost: @session.cost
)
puts prompt_lines.first
print prompt_lines.last
else
print @renderer.render("master$ ", mode: :dim)
end
line = safe_read_line
break if line.nil?
handle_repl_line(line)
end
@bg_thread&.kill
@session.save!
end
def suggested_next_prompt
top = proposer.top
return nil unless top
@last_suggestion = top[:action]
"#{top[:action]} (#{top[:reason]})"
end
def accept_top_suggestion
return unless @last_suggestion
puts @renderer.render("↳ #{@last_suggestion}", mode: :dim)
handle_repl_line(@last_suggestion)
end
def proposer
@proposer ||= Propose.new(container: @container)
@proposer.violations = @violations
@proposer
end
NL_DISPATCH = [
[/\b(?:show|print|list)\s+(?:undo\s+)?histor/i, :run_history],
[/\b(?:why|how)\s+(?:this|that|did|was)\b/i, :run_why],
[/\bfocus\s+(?:mode|on|off)\b|\btoggle\s+focus\b/i, :toggle_focus],
[/\b(?:last|prev(?:ious)?)\s+(?:input|message|prompt)\b/i, :run_last],
[/\b(?:suggest|what(?:'s|\s+is)\s+next|next\s+steps?)\b/i, :run_propose],
[/\b(?:show|list)\s+(?:my\s+)?principles\b/i, :run_principles],
[/\brestart\b|\bhot[\s-]?reload\b/i, :run_restart],
[/\bui[\s-]?critique\b/i, :run_ui_critique],
[/\bsound[\s-]?critique\b/i, :run_sound_critique],
[/\brebuild\b/i, :run_rebuild],
[/\bshow\s+context\b|\bcontext\s+window\b/i, :run_context],
[/\bverifie?d?\b/i, :run_verify],
[/\brails[\s-]?pwa[\s-]?audit\b/i, :run_rails_pwa_audit],
[/\brails[\s-]?pwa[\s-]?fix\b/i, :run_rails_pwa_fix],
[/\bswallow[\s-]?report\b|\berror\s+ledger\b/i, :run_swallow_report],
[/\btoggle\s+chips?\b|\bchips?\s+(?:on|off)\b/i, :toggle_chips],
[/\btoggle\s+dmesg\b|\bdmesg\s+(?:on|off)\b/i, :toggle_dmesg],
].freeze
def handle_repl_line(line)
stripped = line.strip
return accept_top_suggestion if stripped.empty?
NL_DISPATCH.each { |pat, meth| return send(meth) if stripped.match?(pat) }
case stripped
when "/exit", "/quit" then exit_cli
when "/undo" then run_undo
when "/redo" then run_redo
when "/checkpoint" then run_checkpoint
when "/history" then run_history
when "/why" then run_why
when "/focus" then toggle_focus
when "/last" then run_last
when "/cmd" then run_cmd
when "/dmesg" then toggle_dmesg
when "/chips" then toggle_chips
when "/propose" then run_propose
when "/principles" then run_principles
when "/restart" then run_restart
when "/ui-critique" then run_ui_critique
when "/sound-critique" then run_sound_critique
when "/rebuild" then run_rebuild
when "/context" then run_context
when "/verify" then run_verify
when "/rails-pwa-audit" then run_rails_pwa_audit
when "/rails-pwa-fix" then run_rails_pwa_fix
when "/swallow-report" then run_swallow_report
when "<<" then run_input(read_multiline)
else run_input(line)
end
end
def safe_read_line
@reader.read_line("", echo: true).chomp
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "cli.safe_read_line", event_bus: @bus)
end
def exit_cli
@session.save!
line = @renderer.closing
puts line if line
@running = false
end
def read_multiline
lines = []
puts @renderer.render("enter lines, blank line to send", mode: :dim)
loop do
print " "
inner = safe_read_line
break if inner.nil? || inner.strip.empty?
lines << inner
end
lines.join("\n")
end
def replay_recent_turns
tail = @session.messages.last(REPLAY_TURNS * 2)
return if tail.empty?
puts @renderer.render("resume0: replaying last #{tail.size} messages", mode: :dim)
tail.each do |msg|
tag = msg[:role] == :user ? "you" : "master"
content = msg[:content].to_s
first_line = content.lines.first.to_s
snippet = first_line.strip[0, 100]
puts @renderer.render(" #{tag}: #{snippet}", mode: :dim)
end
puts
end
def start_background_loop
@bg_thread = Thread.new do
boot_scan
loop do
sleep IDLE_SLEEP_DEFAULT
background_cycle unless @user_active
end
rescue StandardError => e
@bus&.publish("cli:bg_error", error: e.message)
end
end
def boot_scan
lib_dir = File.join(@root, "lib")
changed = changed_lib_files(lib_dir)
result = changed.any? ? scan_files(changed) : @scanner.scan_dir(lib_dir, depth: :deep)
return unless result.is_a?(Master::Result) && result.ok?
prev = @prev_violations
@violations = count_violations(result.value!)
@prev_violations = @violations
return if @violations.zero? && prev.zero?
delta = @violations - prev
arrow = delta.zero? ? "·" : (delta.positive? ? "↑" : "↓")
puts
puts @renderer.render("boot scan: #{prev} #{arrow} #{@violations} violation(s)", mode: :dim)
puts
rescue StandardError => e
@bus&.publish("cli:warn", error: e.message)
end
def changed_lib_files(lib_dir)
out, = Open3.capture2e("git", "-C", @root, "diff", "--name-only", "HEAD")
return [] if out.strip.empty?
out.lines
.map { |l| File.join(@root, l.strip) }
.select { |p| p.start_with?(lib_dir) && p.end_with?(".rb") && File.exist?(p) }
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "cli.changed_lib_files", event_bus: @bus)
[]
end
def scan_files(paths)
Result.ok(paths.map { |p| [p, @scanner.scan(p, depth: :deep)] })
end
def count_violations(pairs)
pairs.sum do |_file, file_result|
file_result.is_a?(Master::Result) && file_result.ok? ? file_result.value!.size : 0
end
end
def background_cycle
lib_dir = File.join(@root, "lib")
result = @scanner.scan_dir(lib_dir, depth: :deep)
return unless result.is_a?(Master::Result) && result.ok?
n = count_violations(result.value!)
return if n == @violations
@violations = n
$stdout.puts "\nbg: #{n} violation(s)" if n.positive?
$stdout.flush
rescue StandardError => e
@bus&.publish("cli:bg_error", error: e.message)
end
INIT_FRAMES = 20
INIT_FRAME_MS = 0.04
def print_repo_tree
lines = Master::CommandRegistry.tree_lines(@root)
return if lines.empty?
puts @renderer.render("tree0: #{File.basename(@root)} (#{lines.size} entries)", mode: :dim)
lines.each { |l| puts @renderer.render(l, mode: :dim) }
puts
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "cli.print_repo_tree", event_bus: @bus)
end
def booted_before?
flag = File.join(@root, ".master", "booted_once")
File.exist?(flag)
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "cli.booted_before?", event_bus: @bus)
false
end
def first_boot_bar
return unless $stdout.isatty
flag = File.join(@root, ".master", "booted_once")
return if File.exist?(flag)
INIT_FRAMES.times do |i|
bar = ("\u25B0" * (i + 1)) + ("\u25B1" * (INIT_FRAMES - i - 1))
pct = ((i + 1) * 100 / INIT_FRAMES).to_s.rjust(3)
print "\rinit0: #{bar} #{pct}%"
$stdout.flush
sleep INIT_FRAME_MS
end
puts
FileUtils.mkdir_p(File.dirname(flag))
File.write(flag, Time.now.to_s)
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "cli.mark_booted", event_bus: @bus)
end
def display_result(result, accumulated, streamed)
case result
in Master::Result::Ok => ok
@last_ok = true
display_ok(ok, accumulated, streamed)
in Master::Result::Err => err
@last_ok = false
if err.category == :shutdown
exit_cli
else
puts
error_text = format_error_message(err)
puts @renderer.render(error_text, mode: :error)
puts
end
end
end
def format_error_message(err)
msg = err.message.to_s
return msg if msg.bytesize <= 200
msg[0, 197] + "…"
end
def display_ok(ok, _accumulated, streamed)
if streamed
puts
puts
else
print "\r\e[K" if $stdout.isatty
value = ok.value
rendered = value.is_a?(Hash) ? value[:rendered] : nil
text = rendered || value.to_s
puts @renderer.speaker_tag
puts text
puts
end
print_cost_tooltip
print_chips if @show_chips
end
def print_cost_tooltip
now_cost = @session.cost.to_f
delta = now_cost - @last_cost
@last_cost = now_cost
tokens = @session.token_est
cents = (delta * 100).round(2)
return if cents.zero? && tokens.zero?
line = "cost: +¢#{format('%.2f', cents)} · #{tokens} tok · #{short_model(@agent.model)}"
puts @renderer.render(line, mode: :dim)
end
def print_chips
chips = next_action_chips
return if chips.empty?
puts @renderer.render(" next: #{chips.join(" ")}", mode: :dim)
end
def next_action_chips
base = ["[/undo]", "[/why]", "[/last]"]
base.unshift("[/fix #{@violations}v]") if @violations.positive?
base
end
def short_model(model)
model.to_s.sub(/\Aclaude-cli:/, "").sub(/\Aweb-chat:/, "").split("/").last.to_s.sub(/:free$/, "")
end
end
end
end# frozen_string_literal: true
module Master
module Now
class CLI
private
def run_restart
@session.save!
puts @renderer.render("restart: exec'ing fresh master in place", mode: :dim)
$stdout.flush
Kernel.exec(RbConfig.ruby, $PROGRAM_NAME, *ARGV)
end
def run_undo
res = @undo.undo!
if res.is_a?(Master::Result) && res.ok?
puts @renderer.render("undo: #{Array(res.value!).join(", ")}", mode: :success)
else
puts @renderer.render(res.message, mode: :warning)
end
end
def run_redo
res = @undo.redo!
if res.is_a?(Master::Result) && res.ok?
puts @renderer.render("redo: #{Array(res.value!).join(", ")}", mode: :success)
else
puts @renderer.render(res.message, mode: :warning)
end
end
def run_history
lines = @undo.history(limit: 10)
if lines.empty?
puts @renderer.render("no undo history", mode: :dim)
else
lines.each { |l| puts @renderer.render(l, mode: :dim) }
end
end
def run_why
router = Master::Routing::ModelRouter.new(config: @config, root: Master::ROOT)
task = @session.phase == :implement ? :implement : :exploration
tier = router.current_tier(task_type: task)
rows = router.score_breakdown(task_type: task).first(5)
puts @renderer.render("router: phase=#{@session.phase} task=#{task} tier=#{tier}", mode: :dim)
rows.each_with_index do |r, i|
line = format(" %d. %-40s q=%.2f s=%.2f c=%.2f → %.3f",
i + 1, r[:id].to_s[0, 40], r[:q], r[:s], r[:c], r[:total])
puts @renderer.render(line, mode: :dim)
end
end
def toggle_focus
@focus_mode = !@focus_mode
puts @renderer.render("focus: #{@focus_mode ? "on" : "off"}", mode: :dim)
end
def run_last
if @last_input
puts @renderer.render("rerun: #{@last_input[0, 60]}", mode: :dim)
run_input(@last_input)
else
puts @renderer.render("no prior input", mode: :dim)
end
end
def run_cmd
puts @renderer.render("explicit: #{SLASH_COMMANDS.join(" ")}", mode: :dim)
puts @renderer.render("or describe what you want — intent is inferred", mode: :dim)
end
def toggle_dmesg
if @dmesg_sub
@dmesg_sub.call
@dmesg_sub = nil
puts @renderer.render("dmesg: off", mode: :dim)
else
@dmesg_sub = @bus&.subscribe("*") do |payload|
ts = payload.fetch(:ts, 0)
line = " [#{ts.to_s.rjust(7)}] #{payload[:event]}"
$stdout.puts @renderer.render(line, mode: :dim) rescue nil
end
puts @renderer.render("dmesg: on (events stream below)", mode: :dim)
end
end
def toggle_chips
@show_chips = !@show_chips
puts @renderer.render("chips: #{@show_chips ? "on" : "off"}", mode: :dim)
end
def run_principles
c = Master::Ground::Constitution.new
lines = c.list
if lines.empty?
puts @renderer.render("no principles loaded (data/principles/*.md)", mode: :dim)
else
puts @renderer.render("constitution: #{lines.size} principle(s)", mode: :dim)
lines.each { |l| puts @renderer.render(" #{l}", mode: :dim) }
end
end
def run_propose
rows = proposer.call
if rows.empty?
puts @renderer.render("propose: nothing pressing — try /history or scan a dir", mode: :dim)
return
end
puts @renderer.render("propose0: top #{rows.size} suggestion(s)", mode: :dim)
rows.each_with_index do |r, i|
line = format(" %d. %-22s %s", i + 1, r[:action], r[:reason])
puts @renderer.render(line, mode: :dim)
end
end
def run_ui_critique = run_critique(:ui, label: "ui-critique", intro: "assembling panel — brutal honesty mode")
def run_sound_critique = run_critique(:sound, label: "sound-critique", intro: "assembling audio panel")
def run_critique(mode, label:, intro:)
puts @renderer.render("#{label}: #{intro}", mode: :dim)
critic = Master::Judge::Council::Critique.new(mode: mode, agent: @agent, event_bus: @bus)
result = critic.run
unless result.ok?
puts @renderer.render("#{label}: #{result.message}", mode: :warning)
return
end
data = result.value!
picks = data[:cherry_picks]
puts @renderer.render("#{label}: #{picks.size} cherry-pick(s)", mode: :dim)
picks.each { |p| puts @renderer.render(" cherry: #{p}", mode: :dim) }
data[:feedback].each do |f|
puts @renderer.render(" [#{f[:persona]}] #{f[:feedback].to_s.lines.first.to_s.strip}", mode: :dim)
end
end
def run_rebuild
puts @renderer.render("rebuild: syntax check + session save + hot-restart", mode: :dim)
lib_dir = File.join(Master::ROOT, "lib")
errors = []
changed_lib_files(lib_dir).each do |path|
ok = system("ruby34 -c #{path} > /dev/null 2>&1")
errors << path unless ok
end
if errors.any?
errors.each { |p| puts @renderer.render(" syntax error: #{p}", mode: :warning) }
puts @renderer.render("rebuild: aborted — fix errors first", mode: :warning)
return
end
@session.save!
puts @renderer.render("rebuild: ok — exec'ing fresh process", mode: :dim)
$stdout.flush
Kernel.exec(RbConfig.ruby, $PROGRAM_NAME, *ARGV)
end
def run_context
query = @last_input.to_s
puts @renderer.render("context: gathering for query=#{query[0, 60]}", mode: :dim)
provider = Master::Ground::ContextProvider.new
rows = provider.brief(query, limit: 8)
if rows.empty?
puts @renderer.render("context: nothing found", mode: :dim)
else
rows.each { |r| puts @renderer.render(" #{r}", mode: :dim) }
end
@bus&.publish("attention:context", query: query, rows: rows.size)
end
def run_checkpoint
puts @renderer.render("checkpoint: snapshotting changed files", mode: :dim)
lib_dir = File.join(Master::ROOT, "lib")
files = changed_lib_files(lib_dir)
cp = Master::Ground::Checkpoint.new
result = cp.create(label: "manual", files: files)
id = result.respond_to?(:fetch) ? result[:id] : result.to_s
puts @renderer.render("checkpoint: #{id} (#{files.size} file(s))", mode: :dim)
end
def run_verify
puts @renderer.render("verify: checking recently landed operator symbols", mode: :dim)
plan = {
files: %w[lib/ground/intent_router.rb lib/ground/attention_context.rb
lib/ground/unfinished_ledger.rb lib/ground/orchestration_policy.rb],
symbols: %w[Master::Ground::IntentRouter Master::Ground::AttentionContext
Master::Ground::UnfinishedLedger Master::Ground::OrchestrationPolicy],
callers: %w[run_sound_critique run_rebuild run_context run_checkpoint run_verify]
}
checker = Master::Ground::DoneChecker.new
result = checker.call(plan)
result.each do |key, val|
icon = val.is_a?(TrueClass) || val == :ok ? "ok" : "!!"
puts @renderer.render(" #{icon} #{key}", mode: val == false ? :warning : :dim)
end
end
def run_swallow_report
puts @renderer.render("swallow-report: reading SwallowLedger", mode: :dim)
ledger_path = File.join(@root, "runtime", "swallow_ledger.jsonl")
unless File.exist?(ledger_path)
puts @renderer.render("swallow-report: no ledger at #{ledger_path}", mode: :dim)
return
end
lines = File.readlines(ledger_path, chomp: true).last(5)
last = lines.last && JSON.parse(lines.last) rescue nil
unless last
puts @renderer.render("swallow-report: ledger empty or unreadable", mode: :dim)
return
end
puts @renderer.render("swallow-report: total=#{last["total"]} contexts=#{last["counts"]&.size}", mode: :dim)
last["counts"].to_a.sort_by { |_, v| -v }.first(10).each do |ctx, n|
puts @renderer.render(" #{n.to_s.rjust(4)}x #{ctx}", mode: :warning)
end
end
def run_rails_pwa_audit
puts @renderer.render("rails-pwa-audit: scanning DEPLOY apps", mode: :dim)
op = Master::Rails::MobilePwaOperator.new(agent: @agent, event_bus: @bus)
result = op.audit_all_deploy
if result.ok?
result.value!.each do |r|
next puts @renderer.render(" !! #{r[:app]}: #{r[:error]}", mode: :warning) if r[:error]
icon = { green: "ok", amber: "--", red: "!!" }.fetch(r[:verdict], "??")
puts @renderer.render(" #{icon} #{r[:app]}: #{r.dig(:pwa, :findings)&.size || 0} finding(s)", mode: :dim)
Array(r.dig(:pwa, :recommendations)).first(3).each do |rec|
puts @renderer.render(" #{rec}", mode: :dim)
end
end
else
puts @renderer.render("rails-pwa-audit: #{result.message}", mode: :warning)
end
end
def run_rails_pwa_fix
puts @renderer.render("rails-pwa-fix: applying network-first SW + offline fallback to DEPLOY apps", mode: :dim)
op = Master::Rails::MobilePwaOperator.new(agent: @agent, event_bus: @bus)
result = op.audit_all_deploy
return puts @renderer.render("rails-pwa-fix: #{result.message}", mode: :warning) unless result.ok?
fixed = 0
result.value!.each do |r|
next puts @renderer.render(" !! #{r[:app]}: #{r[:error]}", mode: :warning) if r[:error]
next if r[:verdict] == :green
fix_result = op.respond_to?(:fix_app) ? op.fix_app(r[:app]) : Result.err("fix_app not implemented")
if fix_result.ok?
fixed += 1
puts @renderer.render(" ok #{r[:app]}: fixed", mode: :dim)
else
puts @renderer.render(" !! #{r[:app]}: #{fix_result.message}", mode: :warning)
end
end
puts @renderer.render("rails-pwa-fix: #{fixed} app(s) patched", mode: :dim)
end
end
end
end# frozen_string_literal: true
module Master
module Now
class CLI
private
def setup_signals
trap("USR1") { on_usr1 }
trap("INT") { on_int }
end
def on_usr1
Zeitwerk::Loader.for_gem.reload
puts "\n#{@renderer.render("reloaded", mode: :success)}"
rescue StandardError => e
puts "\n#{@renderer.render("reload failed: #{e.message}", mode: :error)}"
end
def on_int
if @pipeline_thread&.alive?
@pipeline_thread.kill
@pipeline_thread = nil
puts "\n#{@renderer.render("aborted", mode: :warning)}"
return
end
if Time.now - @interrupt_at < 1
@scan_thread&.kill
@session.save!
exit(0)
else
@interrupt_at = Time.now
puts "\n#{@renderer.render("^C again to quit", mode: :warning)}"
end
end
end
end
end# frozen_string_literal: true
module Master
module Now
class CLI
SPIN_FRAMES = ["\u00B7", "\u2219", "\u2022", "\u25CF"].freeze
SPIN_INTERVAL = 0.25
DMESG_IGNORE = %w[bus:subscribe bus:unsubscribe ring:write].freeze
VERDICT_GLYPH = {
ok: "\u2713", fail: "\u00D7", warn: "!", info: "\u00B7"
}.freeze
MUTATING_TOOLS = %w[write_file str_replace ast_edit].freeze
private
def print_thinking_indicator
return unless $stdout.isatty
@think_mutex = Mutex.new
@think_t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@think_stage = "intake"
@think_sub = @bus&.subscribe("*") do |payload|
update_think_stage(payload)
emit_dmesg_line(payload)
end
@spin_thread = Thread.new do
i = 0
loop do
@think_mutex.synchronize do
print "\r\e[K#{@renderer.render("#{SPIN_FRAMES[i % SPIN_FRAMES.size]} #{@think_stage}", mode: :dim)}"
$stdout.flush
end
sleep SPIN_INTERVAL
i += 1
end
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "cli.spinner", event_bus: @bus)
end
end
def stop_thinking_indicator
@spin_thread&.kill
@spin_thread = nil
@think_sub&.call
@think_sub = nil
end
def update_think_stage(payload)
ev = payload[:event].to_s
return unless ev.start_with?("stage:")
stage = payload.fetch(:stage, ev.sub("stage:", ""))
@think_stage = stage.to_s
end
def glyph_for_event(ev)
case ev
when /:(done|ok|success|rendered|synthesis)\b/ then VERDICT_GLYPH[:ok]
when /:(error|fail|timeout|veto)\b/ then VERDICT_GLYPH[:fail]
when /:(warn|warning|escalat)\b/ then VERDICT_GLYPH[:warn]
else VERDICT_GLYPH[:info]
end
end
def emit_dmesg_line(payload)
ev = payload[:event].to_s
return if ev.empty? || DMESG_IGNORE.include?(ev)
ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - (@think_t0 || 0)) * 1000).to_i
kv = payload.reject { |k, _| %i[event ts topic].include?(k) }
.map { |k, v| "#{k}=#{v.to_s[0, 60]}" }.join(" ")
diff = ev == "tool:after" && MUTATING_TOOLS.include?(payload[:tool].to_s) ? diff_stat(payload[:path]) : nil
tail = diff ? " #{diff}" : ""
line = " %s [%7d] %s%s%s" % [glyph_for_event(ev), ms, ev, kv.empty? ? "" : " #{kv}", tail]
@think_mutex&.synchronize do
print "\r\e[K"
$stdout.puts @renderer.render(line, mode: :dim)
$stdout.flush
end
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "cli.print_event", event_bus: @bus)
end
def diff_stat(path)
return nil unless path && !path.empty?
out, _ = Open3.capture2e("git", "-C", @root, "diff", "--numstat", "--", path)
m = out.lines.first&.match(/^(\d+)\s+(\d+)/)
m ? "+#{m[1]}/-#{m[2]}" : nil
rescue StandardError => e
Master::Ground::Swallow.log(e, context: "cli.diff_stat", event_bus: @bus)
end
def chunk_accumulator(buffer)
lambda do |chunk|
text = chunk.respond_to?(:content) ? chunk.content.to_s : chunk.to_s
next if text.empty?
yield text
buffer << text
end
end
end
end
end# frozen_string_literal: true
require_relative "command_registry/memory_commands"
require_relative "command_registry/work_commands"
require_relative "command_registry/system_commands"
require_relative "command_registry/tool_commands"
module Master
module Now
# CommandRegistry — all pipeline-routable slash commands.
module CommandRegistry
module_function
def build(infra:, ai:, root:)
session_commands(infra).merge(
mode_commands(infra[:config]),
memory_commands(infra[:memory], ai[:agent]),
work_commands(ai:, root:, infra:),
tool_commands(root),
control_commands(ai[:standing], ai[:soul]),
system_commands(ai[:agent], infra[:diag], root),
"help" => ->(_ctx) {
[
"scan: /scan /fix [loop|preview|stop] /why /axioms /topic /propose-tree",
"review: /critique /review",
"health: /status /resync [--dry-run] /tail [N] [pattern]",
"session: /save /clear /history /tokens /cost /undo /redo /checkpoint /dmesg /exit",
"model: /model /mode /persona /task",
"memory: /memory /dreams",
"tools: /postpro [args] /repligen [args]",
"system: /orient [topic] /tree /diff /commit /snapshot /diag /reload /help"
].join("\n")
}
)
end
# ── Session ──────────────────────────────────────────────────────────────
def session_commands(infra)
session = infra[:session]
undo = infra[:undo]
logging = infra[:logging]
config = infra[:config]
{
"clear" => ->(_ctx) { session.clear!; "context cleared" },
"save" => ->(_ctx) { session.save!; "session saved" },
"history" => ->(ctx) {
n = ctx[:args].to_s.strip.to_i
n = 10 if n <= 0
recent = session.messages.last(n)
next "history: empty" if recent.empty?
recent.map { |m| "[#{m[:role]}] #{m[:content].to_s.gsub(/\s+/, " ")[0, 120]}" }.join("\n")
},
"tokens" => ->(_ctx) { "~#{session.token_est} tokens" },
"cost" => ->(_ctx) { "$#{"%.4f" % session.cost}" },
"undo" => ->(_ctx) { r = undo.undo!; r.ok? ? "reverted: #{r.value!}" : r.message },
"redo" => ->(_ctx) { r = undo.redo!; r.ok? ? "reapplied: #{r.value!}" : r.message },
"dmesg" => ->(_ctx) { logging.dmesg },
"config" => ->(_ctx) { config.to_h.inspect }
}
end
# ── Mode / persona / model flag commands ─────────────────────────────────
def mode_commands(config)
reasoning_commands(config).merge(persona_commands(config)).merge(flag_commands(config))
end
def reasoning_commands(config)
{
"mode" => ->(ctx) {
arg = ctx[:args].to_s.strip
Master::Judge::Modes::SUPPORTED.include?(arg) ?
(config["reasoning_mode"] = arg; config.save!; "mode: #{arg}") :
"mode: #{config.reasoning_mode} (supported: #{Master::Judge::Modes::SUPPORTED.join(", ")})"
},
"task" => ->(ctx) {
arg = ctx[:args].to_s.strip
arg.empty? ? "task_type: #{config.task_type}" : (config["task_type"] = arg; config.save!; "task_type: #{arg}")
}
}
end
def persona_commands(config)
{
"persona" => ->(ctx) {
arg = ctx[:args].to_s.strip
return "persona: #{config.persona}" if arg.empty?
config["persona"] = arg; config.save!; "persona: #{arg}"
}
}
end
def flag_commands(config)
flags = %w[auto_review auto_lint auto_commit]
flags.each_with_object({}) do |flag, h|
h[flag] = ->(ctx) {
arg = ctx[:args].to_s.strip
arg.empty? ? "#{flag}: #{config[flag]}" : (config[flag] = arg == "on"; config.save!; "#{flag}: #{config[flag]}")
}
end
end
# ── Control (standing orders, soul) ──────────────────────────────────────
def control_commands(standing, soul)
{
"orders" => cmd(:dispatch_orders, standing),
"soul" => cmd(:dispatch_soul, soul)
}
end
def dispatch_orders(standing, arg)
case arg
when "list", "" then standing.list
when /\Aenable (.+)\z/ then standing.enable($1.strip)
when /\Adisable (.+)\z/ then standing.disable($1.strip)
when /\Aadd name=(\S+) cmd=(.+)\z/ then standing.upsert(name: $1, command: $2.strip)
when "run" then run_due_orders(standing)
when /\Areset (.+)\z/ then standing.reset($1.strip)
else "usage: /orders /orders enable|disable|reset <name> /orders run"
end
end
def run_due_orders(standing)
results = standing.run_due!
return "no orders due" if results.empty?
results.map { |r| "#{r[:name]}: #{r[:result].ok? ? "ok" : r[:result].message}" }.join("\n")
end
def dispatch_soul(soul, arg)
case arg
when "", "show" then soul.summary
when "version", "changelog" then soul.changelog
when "diff" then soul.diff
when "approve" then soul.approve
when "reject" then soul.reject
when "rollback" then soul.rollback
when /\Apropose (.+)\z/ then soul.propose($1.strip)
else "soul soul version soul diff soul approve soul reject soul rollback soul propose <rationale>"
end
end
# ── Helpers ──────────────────────────────────────────────────────────────
def arg_for(ctx) = ctx[:args].to_s.strip
def expand_or_root(arg, root) = arg.empty? ? root : File.expand_path(arg, root)
def cmd(method, *services) = ->(ctx) { send(method, *services, arg_for(ctx)) }
end
end
end# frozen_string_literal: true
module Master
module Now
module CommandRegistry
module_function
def memory_commands(memory, agent)
{
"memory" => ->(ctx) { dispatch_memory(memory, ctx[:args].to_s.strip) },
"dreams" => ->(ctx) {
arg = ctx[:args].to_s.strip
if arg == "consolidate"
memory.respond_to?(:consolidate!) ? memory.consolidate!(agent:) : "dreaming not available"
else
entries = memory.all
archived = entries.count { |k, _| k.to_s.start_with?("archive/") }
active = entries.count { |k, _| !k.to_s.start_with?("archive/") }
summary = memory.recall("_consolidated_summary")
lines = ["active: #{active} memories, archived: #{archived}"]
lines << "last consolidation: #{summary}" if summary
lines.join("\n")
end
}
}
end
def dispatch_memory(memory, arg)
case arg
when /\Aforget (.+)/ then memory.forget($1.strip); "forgot: #{$1.strip}"
when /\Aremember (.+)/
body, type = parse_remember($1)
key, value = body.split("=", 2).map(&:strip)
if value
memory.remember(key, value, type:)
"remembered [#{type}]: #{key}"
else
"usage: /memory remember [type=user|feedback|project|reference] key=value"
end
when /\Asearch (.+)/ then memory_search(memory, $1.strip)
when /\Atype (\S+)/ then list_by_type(memory, $1.strip)
when "types"
counts = memory.type_counts.map { |t, n| "#{t}: #{n}" }.join("\n")
counts.empty? ? "(no memories)" : counts
when ""
(e = memory.all).empty? ? "(no memories)" : e.map { |k, v| "#{k}: #{v}" }.join("\n")
else
(r = memory.recall(arg)) ? "#{arg}: #{r}" : "(not found: #{arg})"
end
end
def parse_remember(text)
if text =~ /\Atype=(\S+)\s+(.+)/
[$2, $1]
else
[text, "general"]
end
end
def list_by_type(memory, type)
hits = memory.by_type(type)
return "(no memories of type: #{type})" if hits.empty?
hits.map { |k, v| "#{k}: #{v.is_a?(Hash) ? v["value"] : v}" }.join("\n")
end
def memory_search(memory, query)
if memory.respond_to?(:semantic_recall)
hits = memory.semantic_recall(query)
return "(no matches: #{query})" if hits.empty?
hits.map { |h| "#{h[:key]}: #{h[:value]}" }.join("\n")
else
hits = memory.all.select { |k, v| k.to_s.include?(query) || v.to_s.include?(query) }
hits.empty? ? "(no matches: #{query})" : hits.map { |k, v| "#{k}: #{v}" }.join("\n")
end
end
end
end
end# frozen_string_literal: true
require "open3"
module Master
module Now
module CommandRegistry
module_function
TEXT_EXTS = %w[.rb .py .js .ts .zsh .sh .bash .md .yml .yaml .json .toml .gemspec .txt .erb .conf .ini .env].to_set.freeze
TEXT_NAMES = %w[Gemfile Rakefile Makefile Dockerfile].to_set.freeze
SKIP_SEGS = %w[.git vendor tmp var node_modules .bundle coverage log dist knowledge].to_set.freeze
def system_commands(agent, diag, root)
{
"orient" => cmd(:dispatch_orient, root),
"tree" => cmd(:dispatch_tree, root),
"diff" => cmd(:dispatch_diff, root),
"commit" => ->(_ctx) { dispatch_commit(agent, root) },
"snapshot" => ->(_ctx) { dispatch_snapshot(root) },
"diag" => ->(ctx) { diag ? diag.render(ctx[:args].to_s.strip) : "diag: not configured" },
"reload" => ->(_ctx) { "reload: not supported in this context" }
}
end
ORIENT_FILES = {
"soul" => ["data/soul.yml", "constitution: axioms, voice, persona, prompt order"],
"rules" => ["data/rules.yml", "universal cross-disciplinary rules"],
"style" => ["data/ruby_style.yml", "ruby/shell/git/css/html/typography idioms"],
"workflow" => ["data/workflow.yml", "agent loops, pipeline, council, gates"],
"orders" => ["data/standing_orders.yml", "event triggers and standing operating procedures"],
"patterns" => ["data/patterns.yml", "gh/openbsd/zsh tool idioms"],
"openbsd" => ["data/openbsd.yml", "pf/nsd/httpd/relayd config validators"]
}.freeze
def dispatch_orient(root, arg)
return cat_orient(root, arg) unless arg.empty?
[
"MASTER — constitutional AI runtime for any text artifact",
"modules: now · loop · judge · voice · ground · reach · trace",
"pipeline: Intake → Infer → Route → Guard → Execute → [Council ‖ Lint] → Prune → Memo → Render",
"",
"constitution:",
*ORIENT_FILES.map { |k, (path, desc)| " /orient #{k.ljust(10)} #{path.ljust(28)} #{desc}" }
].join("\n")
end
def cat_orient(root, arg)
entry = ORIENT_FILES[arg]
return "unknown: #{arg} (try: #{ORIENT_FILES.keys.join(", ")})" unless entry
full = File.join(root, entry[0])
File.exist?(full) ? File.read(full) : "missing: #{full}"
end
def dispatch_tree(root, arg)
cfg = (Master.load_yaml(File.join(root, "data", "rules.yml")) || {}).dig("paths", "tree") || {}
depth = arg.to_i.positive? ? arg.to_i : (cfg["max_depth"] || 2)
cap = cfg["max_lines"] || 200
buf = []
walker = lambda do |dir, level|
return if level > depth || buf.size >= cap
Dir.children(dir).sort.each do |name|
break if buf.size >= cap
next if name.start_with?(".") || SKIP_SEGS.include?(name)
path = File.join(dir, name)
buf << "#{" " * (level - 1)}#{name}#{File.directory?(path) ? "/" : ""}"
walker.call(path, level + 1) if File.directory?(path)
end
rescue Errno::EACCES, Errno::ENOENT
nil
end
walker.call(root, 1)
buf.join("\n")
end
def dispatch_diff(root, arg)
base = arg.empty? ? "HEAD" : arg
out, = Open3.capture2e("git", "-C", root, "diff", base, "--stat")
out.strip.empty? ? "(no changes since #{base})" : out.strip
end
def dispatch_commit(agent, root)
diff, = Open3.capture2e("git", "-C", root, "diff", "--cached", "--stat")
diff, = Open3.capture2e("git", "-C", root, "diff", "--stat") if diff.strip.empty?
return "nothing to commit" if diff.strip.empty?
prompt = "Write a concise git commit message (1 line, imperative mood) for:\n#{diff}"
msg = agent.ask_once(prompt).to_s.strip.lines.first.to_s.strip
Open3.capture2e("git", "-C", root, "add", "-u")
out, = Open3.capture2e("git", "-C", root, "commit", "-m", msg)
out.strip
end
def dispatch_snapshot(root)
[
publish_snapshot(root, "MASTER"),
publish_snapshot(File.expand_path("../DEPLOY", root), "DEPLOY")
].join("\n")
end
def publish_snapshot(target, label)
return "snapshot:#{label.downcase}: not found: #{target}" unless File.directory?(target)
skip = ->(rel) { rel.split("/").any? { |s| SKIP_SEGS.include?(s) } }
text_file = ->(f) { TEXT_EXTS.include?(File.extname(f).downcase) || TEXT_NAMES.include?(File.basename(f)) }
all = Dir.glob(File.join(target, "**", "*"))
.reject { |f| File.basename(f).start_with?(".") }
.reject { |f| skip.(f.delete_prefix("#{target}/")) }.sort
dirs = all.select { |f| File.directory?(f) }
files = all.select { |f| File.file?(f) && text_file.(f) && File.size(f) < Master::CTX_WINDOW_SIZE }
stamp = Time.now.utc.iso8601
md = ["# #{label} Snapshot — #{stamp}", "", "## Tree", "```"]
entries = (dirs.map { |d| [d, :dir] } + files.map { |f| [f, :file] })
.sort_by { |p, _| p.split("/") }
.map { |p, k| "#{" " * p.delete_prefix("#{target}/").count("/")}#{File.basename(p)}#{k == :dir ? "/" : ""}" }
md.concat(entries) << "```" << ""
n_lines = 0
files.each do |f|
rel = f.delete_prefix("#{target}/")
lang = Master::FILE_LANGUAGE_MAP.fetch(File.extname(f).downcase, "text")
body = File.read(f, encoding: "UTF-8", invalid: :replace).lines
n_lines += body.size
md << "## `#{rel}`" << "```#{lang}"
md.concat(body.map(&:rstrip))
md << "```" << ""
rescue StandardError => e
md << "## `#{rel}`" << "[skipped: #{e.message}]" << ""
end
md << "files: #{files.size} / lines: #{n_lines}"
day = Time.now.strftime("%Y-%m-%d")
out, status = Open3.capture2e("gh", "gist", "create", "-",
"--public", "--desc", "#{label} #{day}",
"--filename", "snapshot_latest.md",
stdin_data: md.join("\n"))
status.success? ? "snapshot:#{label.downcase}: #{files.size} files #{n_lines} lines → #{out.strip}" :
"snapshot:#{label.downcase}: gist publish failed: #{out.strip}"
end
end
end
end# frozen_string_literal: true
require "open3"
require "shellwords"
module Master
module Now
module CommandRegistry
module_function
def tool_commands(root)
{
"postpro" => ->(ctx) { dispatch_master_tool(root:, tool: "postpro", arg: arg_for(ctx)) },
"repligen" => ->(ctx) { dispatch_master_tool(root:, tool: "repligen", arg: arg_for(ctx)) }
}
end
def dispatch_master_tool(root:, tool:, arg:)
script = File.join(root, "tools", "#{tool}.rb")
return "#{tool}: missing tool entrypoint #{script}" unless File.file?(script)
argv = Shellwords.split(arg.to_s)
out, status = Open3.capture2e(RbConfig.ruby, script, *argv, chdir: File.expand_path("..", root))
status.success? ? out.strip : "#{tool}: exit=#{status.exitstatus}\n#{out.strip}"
rescue ArgumentError => e
"#{tool}: bad arguments: #{e.message}"
rescue StandardError => e
"#{tool}: #{e.class}: #{e.message}"
end
end
end
end# frozen_string_literal: true
require "json"
require "open3"
require "time"
module Master
module Now
module CommandRegistry
module_function
VIOLATION_TRUNCATE = Master::VIOLATION_TRUNCATE
def work_commands(ai:, root:, infra:)
scanner = ai[:scanner]
fix_loop = ai[:fix_loop]
deliberation = ai[:deliberation]
council_stage = ai[:council_stage]
agent = ai[:agent]
propose_tree = ai[:propose_tree]
session = infra[:session]
bus = infra[:bus]
config = infra[:config]
metrics = infra[:metrics]
{
"scan" => cmd(:dispatch_scan, scanner, root),
"fix" => ->(ctx) { dispatch_fix(fix_loop, root, arg_for(ctx)) },
"status" => ->(_c) { dispatch_status(root:, fix_loop:, bus:) },
"resync" => ->(c) { dispatch_resync(root:, fix_loop:, arg: arg_for(c)) },
"tail" => ->(c) { dispatch_tail(root:, arg: arg_for(c)) },
"review" => ->(ctx) { dispatch_review(council_stage:, deliberation:, root:, bus:, arg: arg_for(ctx)) },
"critique" => cmd(:dispatch_critique, deliberation, root),
"model" => ->(c) { dispatch_model(agent:, config:, metrics:, root:, arg: arg_for(c)) },
"why" => cmd(:dispatch_why, agent, root),
"axioms" => cmd(:dispatch_axioms, scanner, root),
"topic" => cmd(:dispatch_topic, session),
"propose-tree" => ->(_ctx) { propose_tree&.call || "propose-tree: not wired" }
}
end
# /status — one-frame health panel. Replaces seven probing tool calls.
def dispatch_status(root:, fix_loop:, bus:)
git = Reach::GitOperations.new(File.expand_path("..", root))
ahead, behind = git.ahead_behind
head = git.head || "?"
dirty = git.dirty?(".")
svc = service_status
bg = fix_loop&.background_alive? ? "running" : "stopped"
af = ENV["MASTER_AUTOFIX"] == "1" ? "on" : "off"
bndl = bundle_status(File.expand_path("..", root))
evts = recent_events(root, 5)
branch = git.branch || "?"
lines = [
"status",
"service master/#{svc[:state]} #{svc[:detail]}",
"git #{branch}@#{head} ahead=#{ahead} behind=#{behind} #{dirty ? "dirty" : "clean"}",
"fix bg=#{bg} autofix=#{af}",
"bundle #{bndl}",
"events (last #{evts.size})"
]
evts.each { |e| lines << " #{e[:ago]} #{e[:event]} #{e[:summary]}" }
lines.join("\n")
rescue StandardError => e
"status: #{e.message}"
end
def service_status
out, _, st = Open3.capture3("/usr/sbin/rcctl", "check", "master")
{ state: st.success? ? "ok" : "down", detail: out.strip }
rescue Errno::ENOENT
{ state: "n/a", detail: "rcctl absent — not OpenBSD" }
rescue StandardError => e
{ state: "?", detail: "rcctl err: #{e.class}: #{e.message[0, 60]}" }
end
def bundle_status(repo)
mas, = Open3.capture2e("bundle34", "check", chdir: File.join(repo, "MASTER"))
web, = Open3.capture2e("bundle34", "check", chdir: File.join(repo, "MASTER/web"))
mas_ok = mas.include?("dependencies are satisfied")
web_ok = web.include?("dependencies are satisfied")
mas_ok && web_ok ? "ok (MASTER+web satisfied)" : "drift — run bundle install"
rescue StandardError => e
"unknown (#{e.class})"
end
def recent_events(root, n)
path = File.join(root, "runtime", "events", "activity.jsonl")
return [] unless File.exist?(path)
now = Time.now.utc
File.foreach(path).to_a.last(n).map { |line|
rec = JSON.parse(line) rescue next
ts = (Time.parse(rec["timestamp"]) rescue now)
secs = (now - ts).to_i.abs
ago = secs < 60 ? "#{secs}s" : (secs < 3600 ? "#{secs / 60}m" : "#{secs / 3600}h")
pay = rec["payload"]
sum = pay.is_a?(Hash) ? pay.first(3).map { |k, v| "#{k}=#{v.to_s.tr('"', "")[0, 24]}" }.join(" ") : pay.to_s
{ ago: ago.rjust(4), event: rec["event"].to_s, summary: sum[0, 80] }
}.compact
rescue StandardError
[]
end
# /resync — divergence repair: tag, fetch, reset, bundle, restart.
def dispatch_resync(root:, fix_loop:, arg:)
repo = File.expand_path("..", root)
git = Reach::GitOperations.new(repo)
stop_msg = fix_loop&.background_alive? ? (fix_loop.stop_background!; "stopped fix_loop bg; ") : ""
tag_name = "backup/#{Time.now.strftime("%Y%m%d-%H%M")}-resync"
old_head = git.head
lines = ["#{stop_msg}resync starting — tag=#{tag_name} was=#{old_head}"]
git.tag(tag_name); lines << " tagged #{tag_name}"
git.fetch; lines << " fetched origin