Status: Proposed. Date: 2026-09-28. Amends ADR 0066: the gem ships the development tooling too, and the dev_tooling filter in hecks.gemspec goes (section 3). Builds on ADR 0053 (the journal every command writes). Nothing below is built yet.
bin/ holds 92 hand-written Ruby scripts. Each one parses its own arguments, performs its own checks, shells out to git, gh, cargo, RubyGems or the filesystem inline, and reports by printing and exiting. None of what they do reaches a journal, so a check that starts failing leaves no history, and the rules they enforce are invisible to model_check.
The pieces for doing this the domain way already exist:
bin/project_cli (lib/hecks/cli/project_cli.rb) generates a launcher beside each domain. The launcher boots the domain and hands ARGV to Hecks::Facade::CliRunner, which projects verbs, arguments and refusals from the bluebook each time it runs.adapters/ directory binds them. qa/bluebook/quality_control.hecksagon declares the CI port and qa/adapters/github_checks binds it.lib/hecks/deploy/bluebook/ shows a bluebook that lives inside lib/.lib/hecks/language/bluebook/ shows one bluebook spread over several files: bluebook.bluebook, aggregate.bluebook and the rest each open Hecks.bluebook "Bluebook" and add their own aggregates. The boot loads every *.hecksagon in the directory.lib/hecks.rb requires none of the development tooling (ADR 0066), so code that sits in the gem costs nothing until something asks for it.rust/host images, which do not carry the Ruby gem at all.Every bin script becomes a command or query on a bluebook, with its outside-world work in adapters behind ports. All 92 move in one pass.
given on the command, with a message that states the rule.git, gh, cargo, gem, npm, Postgres, AWS and the filesystem sit behind ports the hecksagon declares, bound in the domain’s adapters/ directory, in the same shape as qa/adapters/github_checks.project_cli writes the launcher beside each domain. Nobody writes a launcher by hand, and bin/<name> is deleted.The framework gets its own domain, Hecks.bluebook "Hecks", in lib/hecks/hecks/. Following the language domain, it is spread over files that each open Hecks.bluebook "Hecks":
lib/hecks/hecks/
hecks.bluebook the root: vision, and the Hecks aggregate itself
custodian.bluebook Custodian: operating any domain
codebase.bluebook Codebase: working on this repository
hecks.hecksagon wiring for the whole domain
adapters/ driven adapters; Codebase's under adapters/codebase/
hecks the launcher project_cli generates
| Part | Files | For | Runs | Holds |
|---|---|---|---|---|
| Root | hecks.bluebook |
Everyone | Anywhere | The framework itself: its vision and version |
| Custodian | custodian.bluebook |
Clients running their own domains | Anywhere | Looking at a domain, running it, building it for the Rust host, fuzzing it, caring for its stored data, vendoring bluebooks, entry points |
| Codebase | codebase.bluebook, adapters/codebase/ |
Maintainers of Hecks | In a hecks checkout; refuses elsewhere | Evolving the language, Rust/Ruby conformance, comment style and codemods, test suite tooling, corpus-wide regeneration, releasing the gem |
The split is by audience, not by packaging: all of it ships. The test for where a command goes is whether a client would run it against their own domain. If so it is Custodian’s, even when Hecks maintainers also use it; if it only makes sense on this repository, it is Codebase’s. Because project_cli names a launcher after its bluebook, the generated launcher is hecks, and exe/hecks becomes that launcher instead of a hand-written router. Verbs are the Hecks commands directly, for example hecks verify_engine_agreement or hecks era.merge_tail.
A script whose concern already has a bluebook joins that bluebook instead: the qa_* scripts go to QualityControl and the deploy scripts go to Deploy.
Everything in this ADR ships in the gem: the Hecks domain with Codebase and its adapters, every attached chapter including QualityControl, and the development tooling ADR 0066 kept out (fuzzing/, bench/, corpus.rb, codemod.rb, query_ir.rb, grammar/evolve.rb, doc/). The dev_tooling filter in hecks.gemspec goes, and spec/gemspec_packaging_spec.rb changes from asserting that those files are absent to asserting that lib/hecks.rb never loads them.
The gem also packages the Rust workspace, so a client can build their own domain for rust/host from an installed gem:
rust/Cargo.toml, Cargo.lock and the crates a domain build uses: the kernel (rust/src/ without generated/), codegen/, parser/, host/, project/, build/, web/ and lsp/. About 4 MB of source.target/ build output, rust/tests/, and the generated corpus domains in rust/src/generated/ (about 10 MB) with their Cargo features. The packaged workspace is a clean kernel that knows no domains.project_rust works by writing a domain’s code into rust/src/generated/ and adding a feature for it to Cargo.toml. Custodian’s Build therefore copies the packaged workspace into the client project, keyed by gem version (for example .hecks/rust/<version>/), generates the domain there, and points CARGO_TARGET_DIR there. The RustToolchain adapter does the copying. In a hecks checkout, Codebase’s Regeneration keeps writing corpus domains into the checkout’s own rust/.Keeping a deployment small does not depend on the package:
exe/hecks runs, never from lib/hecks.rb. A Ruby deployment that requires hecks and boots its own domain loads none of Custodian, Codebase, QualityControl or the tooling; they are files on disk.rust/host images do not carry the gem.given that the working tree is a hecks checkout, so an installed gem answers “needs a hecks checkout” instead of globbing an absent rust/ or rewriting its own lib/. The SourceTree and Workspace adapters answer that question.The Hecks domain also takes in every chapter that describes the framework itself: the language declared in itself, Tenancy, Deploy and QualityControl. Each stays a chapter of its own: it keeps its name, its namespace (QualityControl::Patch, Deploy::Tenant, Bluebook::Aggregate), its store and its directory. The Hecks hecksagon attaches them the way uses_framework attaches Governance, and cross-chapter reactions go through translates.
| Group | Chapter | Lives in | For | Runs |
|---|---|---|---|---|
| Language | Bluebook | lib/hecks/language/bluebook/ |
Both | Anywhere |
| Language | Paging (extends Bluebook through attaches_to, comes with it) |
lib/hecks/language/bluebook/attaches/ |
Both | Anywhere |
| Language | Hecksagon | lib/hecks/language/hecksagon/ |
Both | Anywhere |
| Language | World | lib/hecks/language/world/ |
Both | Anywhere |
| Language | Adapter | lib/hecks/language/adapter.bluebook |
Both | Anywhere |
| Language | Port | lib/hecks/language/port.bluebook |
Both | Anywhere |
| Language | Translation | lib/hecks/language/translation/ |
Both | Anywhere |
| Language | Expression | lib/hecks/grammar/expression.bluebook |
Both | Anywhere |
| Runtime | Tenancy | lib/hecks/tenancy/bluebook/ |
Clients | Anywhere |
| Operations | Deploy | lib/hecks/deploy/bluebook/ |
Clients | Anywhere |
| Operations | QualityControl | lib/hecks/quality_control/, moved from qa/ |
Maintainers | In a hecks checkout |
The language chapters serve both audiences: clients’ domains run on them, and maintainers change them.
QualityControl’s chapter, hecksagon and adapters move from qa/bluebook/ and qa/adapters/ into lib/hecks/quality_control/ so they ship with the rest. What belongs to this repository’s QA practice stays in qa/: the .world file naming the ledger database, settings.yml, the stress domains and the specs. The QA ledger keeps its PostgresEra era and tables, because they are keyed by the chapter name, which does not change.
Two chapters are named Translation today; Hecks attaches only the language’s (section 6).
Some chapters stay out on purpose:
qa/stress_domains/ are subjects QualityControl rotates through, not part of the framework.rust/parser/tests/fixtures/, the Fixture* chapters) are inputs to specs.The one hecks launcher reaches every chapter’s verbs, for example hecks quality_control log_bug or hecks deploy lint. No chapter gets a launcher of its own.
# lib/hecks/hecks/hecks.hecksagon
Hecks.hecksagon "Hecks" do
uses_framework "Governance"
attaches "Bluebook", "Hecksagon", "World", "Adapter", "Port", "Translation", "Expression"
attaches "Tenancy"
attaches "Deploy", "QualityControl"
end
attaches stands for the attaching word; see Open items.
The Bluebook chapter’s own vision names what stays hand-written: “the interpreter — evaluating a predicate held as data — and IO”. The floor is only that. The parser’s output, the IR and the meta-validator’s judgments are already modelled: “Loading a domain becomes dispatching commands into this meta-domain; the IR it stores must equal the IR the DSL builder produces.” They are the attached Bluebook chapter’s aggregates.
Around the floor, every part that has identity and changing state becomes an aggregate, so its changes are journaled like any other domain’s:
| Aggregate | Held by | Identified by | State and lifecycle | Takes over |
|---|---|---|---|---|
| Release | Root | gem version | tagged → published → verified; carries the IR version (Bluebook::Chapter::IR_VERSION) it shipped |
Hecks::VERSION as the record of what is running and what was shipped |
| SyntaxBootCache | Root | digest of the grammar chapters | fresh → stale → rebuilt; Invalidate, Rebuild |
the on-disk cache under Hecks::CacheDir, whose invalidations are silent today |
| Host | Custodian | stack or URL | Observe records the version and era a running rust/host reports; emits EraChanged |
check_era, which becomes a command on it |
| FrameworkMember | Custodian | member name | the capabilities it provides | Hecks::Framework.members and providers_of, read as records; Hecks catalogs members without attaching them |
| Kernel | Codebase | kernel version | the capability tables and a coverage result per run | project_kernel_capabilities, rust_kernel_coverage |
| Adapter | Adapter chapter (exists) | adapter name | gains the optional port operations it implements and the guarantees it gives (section 10), which a domain today only learns from a warning at boot |
the .adapter declarations lib/hecks/adapters/driven/ already holds |
The Hecks domain’s own adapters (InProcessBoot, JournalStore, RustToolchain and the rest in this ADR) get .adapter declarations too, so they appear beside the built-in ones.
Release moves to the root because every install has a version, and “what am I running” and “what did we ship” become one record. The steps that publish a release (tagging, pushing to RubyGems and npm) stay in Codebase as Publishing, and advance the root’s Release through its lifecycle.
What stays code is the interpreter and the IO inside each adapter.
Three rules keep the Hecks domain beside a client domain, not in front of it:
rust/host call the runtime directly. A client’s command takes no extra hop, writes no second journal entry, and depends on neither the Hecks domain’s store nor its version.exe/hecks or any of its own verbs. The committed exe/hecks stays the bootstrap, so regenerating the launcher never needs a working launcher.Two chapters are named Translation: the grammar’s register of rule kinds (lib/hecks/grammar/translation.bluebook) and the language’s meta-model of an edge file (lib/hecks/language/translation/). They are two halves of one concept, and ADR 0082 merges them.
3.0 does not wait for that. Hecks attaches only the language’s Translation chapter. The grammar chapter stays unattached, as it is today, since it only ever loads alone in Hecks::Grammar.grammar_chapters. The names never meet, so nothing is renamed.
Every script becomes one or more commands or queries, named for the domain action (section 12). The launcher column shows the 3.0 form; the Hecks domain’s own verbs are bare, and an attached chapter’s verbs follow its name (hecks quality_control …, hecks deploy …). <x> is the positional identifying argument, --flag a boolean, name=value everything else. Destructive commands dry-run without --confirm.
Custodian, for clients
| Script | Aggregate · command | Launcher |
|---|---|---|
ir |
Introspection · IR (query) | hecks ir [domain] [--translations] [--meta] |
shape |
Introspection · Shape (query) | hecks introspection.shape <domain> |
stores |
Introspection · Stores (query) | hecks stores <domain> |
history |
Introspection · History (query) | hecks introspection.history <domain> |
statements |
Introspection · Statements (query) | hecks introspection.statements <domain> chapter=Name |
narrate |
Introspection · Narrative (query) | hecks narrate [domain] [aggregate=Name] |
docs |
Introspection · Document (query) | hecks docs [domain] [aggregate=Name] |
project_diagrams |
Introspection · Diagrams (query) | hecks project_diagrams <domain> chapter=Name |
project_glossary |
Introspection · Glossary (query) | hecks introspection.glossary <domain> chapter=Name |
model_check |
Introspection · ModelCheck | hecks model_check [domains=a,b] [--strict] [profile=client] |
run |
Operation · Run | hecks run [domain] script=steps.json, or hecks run [domain] <verb> name=value … |
project |
Operation · RefreshProjections | hecks operation.refresh_projections <domain> |
behaviors |
Operation · RunBehaviors | hecks operation.run_behaviors <path> |
console |
Operation · OpenConsole | hecks console [domain] |
follow |
Operation · Follow (query, streams) | hecks operation.follow <domain> [aggregate=Name] [interval=0.5] [--from-now] |
smoke_test |
Operation · SmokeTest | hecks smoke_test [domain] |
smoke_http |
Operation · SmokeHttp | hecks operation.smoke_http path=/p secret=… [url=] [header=] [scheme=timestamped] [payload=] [payload_file=] [health_path=] [state_path=] |
check_era |
Host · CheckEra | hecks host.check_era <url> expected=era-file [timeout=10] |
merge_tail |
Era · MergeTail | hecks era.merge_tail <domain> winners=id:old,id:new --confirm |
reattest_era |
Era · Reattest | hecks era.reattest <domain> era=N --confirm |
backfill_era_projections |
Era · BackfillProjections | hecks era.backfill_projections <domain> |
scaffold_translation |
Era · ScaffoldTranslation (query) | hecks era.scaffold_translation <domain> |
translation_audit |
Era · AuditTranslation (query) and Era · ApproveTranslation | hecks era.audit_translation <domain>; hecks era.approve_translation <domain> --confirm |
compact |
Era · Compact | hecks era.compact <domain> [aggregates=A,B] --confirm |
heki_compact |
Era · CompactHeki | hecks era.compact_heki <domain> [aggregates=A,B] --confirm |
| (new) | Era · HoldFirst | hecks era.hold_first <domain> --confirm |
vendor_bluebook |
Package · Vendor | hecks package.vendor <package[@version]> [from=path] [root=path] |
project_cli |
Door · ProjectCli | hecks project_cli [domains=a,b] |
hecks_mcp_door |
Door · ServeMcp | hecks mcp (stdio) |
project_rust |
Build · ProjectRust | hecks build.project_rust <domain> |
project_wasm |
Build · BuildWasm | hecks build.build_wasm <domain> |
project_wasm_browser |
Build · BuildBrowserWasm | hecks build.build_browser_wasm <domain> |
rust_coverage |
Build · RustCoverage (query) and Build · CheckCoverageAllowlist | hecks build.rust_coverage <module> [codegen=ruby]; hecks build.check_coverage_allowlist |
rust_conformance |
Build · CheckConformance | hecks build.check_conformance <domain> script=steps.json [artifact=native] |
rust_conformance_fuzz |
Build · FuzzConformance | hecks build.fuzz_conformance <domain> artifact=native [seeds=10] [steps=25] |
fuzz |
Fuzzing · Fuzz | hecks fuzz_run.fuzz [domain] [seeds=20] [steps=30] [workers=] [adapter=memory] |
generate |
Fuzzing · GenerateSequence (query) | hecks fuzz_run.generate_sequence <domain> [seed=1] [steps=30] [adversarial=0.0] |
bench |
Fuzzing · Bench | hecks fuzz_run.bench [domains=pizzas,banking] [targets=] [iterations=1000] [warmup=200] [runs=3] [rust_binary=] [format=markdown] [output=] |
Deploy, for clients (attached chapter)
| Script | Aggregate · command | Launcher |
|---|---|---|
project_deploy |
Deploy · Project | hecks deploy project <domain> [tenant=] [schema=] [out=] [environment=] |
lint_deploy_recipes |
Deploy · Lint | hecks deploy lint [makefiles=a,b] |
deploy_template_diff |
Deploy · Diff (query) | hecks deploy diff before=a.yaml after=b.yaml [--json] [--strict] |
project_oidc |
Deploy · ProjectOidc | hecks deploy project_oidc [domains=a,b] |
project_tenant |
Tenant · Provision | hecks deploy provision <domain_dir> slug=s domain= realm= schema= database= [adapter=PostgresEra] |
Codebase, for maintaining Hecks
| Script | Aggregate · command | Launcher |
|---|---|---|
project_model |
Language · ProjectModel | hecks language_run.project_model |
project_vocabulary |
Language · ProjectVocabulary | hecks language_run.project_vocabulary |
project_rust_vocabulary |
Language · ProjectRustVocabulary | hecks language_run.project_rust_vocabulary |
project_refusal_wording |
the same command, a second name | hecks language_run.project_refusal_wording |
project_reserved_names |
Language · ProjectReservedNames | hecks language_run.project_reserved_names |
project_parser_table |
Language · ProjectParserTable | hecks language_run.project_parser_table |
project_bootstrap_table |
Language · ProjectBootstrapTable | hecks language_run.project_bootstrap_table |
project_field_hints |
Language · ProjectFieldHints | hecks language_run.project_field_hints |
expression_projection |
Language · ProjectExpressionTables | hecks language_run.project_expression_tables [--stdout] |
reference |
Language · ProjectReference | hecks language_run.project_reference |
evolve |
Language · WordStatus (query); Propose, Admit, Deprecate, Retire, Rename; ProposeArgument, AdmitArgument, DeprecateArgument, RetireArgument | hecks language_run.word_status; hecks language_run.propose <word> context=X [body=none] [inner=] [opens=] [fills=]; hecks language_run.rename <word> context=X to=Y; hecks language_run.propose_argument <word> context=X kind=K [required=false] [at=N] [named=] [pairs_shape=]; the rest take <word> context=X |
project_kernel_capabilities |
Kernel · ProjectCapabilities | hecks kernel_run.project_kernel_capabilities |
rust_kernel_coverage |
Kernel · MeasureCoverage | hecks kernel_run.measure_kernel_coverage |
check_engine_agreement |
Conformance · CheckEngineAgreement | hecks conformance_run.check_engine_agreement |
doc_coverage |
Conformance · MeasureDocCoverage | hecks conformance_run.measure_doc_coverage |
argument_gate_matrix |
Conformance · ArgumentGateMatrix | hecks conformance_run.argument_gate_matrix [--confirm] (writes only with --confirm) |
regen_codegen_domains |
Regeneration · RegenerateCorpus | hecks regeneration_run.regenerate_corpus [--check] |
standardize_comments |
Style · ReportComments (query), CheckComments, FixComments, WriteCommentBaseline, CheckCommentsUnchanged | hecks style_run.report_comments paths=a,b [only=] [--json] [top=20]; hecks style_run.check_comments paths=…; hecks style_run.fix_comments paths=… --confirm; hecks style_run.write_comment_baseline --confirm; hecks style_run.check_comments_unchanged ref=REF |
standardize_comments_rust |
Style · ReportRustComments (query), CheckRustComments, FixRustComments | hecks style_run.report_rust_comments paths=…; hecks style_run.check_rust_comments paths=…; hecks style_run.fix_rust_comments paths=… --confirm |
canonicalise |
Style · Canonicalise | hecks style_run.canonicalise <file.json> |
codemod_hoist_local_givens |
Codemod · HoistLocalGivens | hecks codemod_run.hoist_local_givens --confirm |
codemod_implicit_append_fields |
Codemod · DropImplicitAppendFields | hecks codemod_run.drop_implicit_append_fields --confirm |
rspec_shard_files |
TestSuite · ShardSpecs (query) | hecks test_suite_run.shard_specs group=1 groups=N [runtime_log=] |
rspec_io_parallel_files |
TestSuite · ListIoParallelSpecs (query) | hecks test_suite_run.list_io_parallel_specs exclude=REGEX [tags=] [check=file] [write=file] |
refresh_rspec_runtime_baseline |
TestSuite · RefreshRuntimeBaseline | hecks test_suite_run.refresh_runtime_baseline [workers=6] [from_run=ID] |
spec_example |
TestSuite · RunSpecExample | hecks test_suite_run.run_spec_example file=path example=text |
stress_concurrency_specs |
TestSuite · StressConcurrency | hecks test_suite_run.stress_concurrency [runs=30] [parallel=] [seed_start=1] |
regenerate_persistence_legacy_fixtures |
TestSuite · RegenerateLegacyFixtures | hecks test_suite_run.regenerate_legacy_fixtures --confirm |
seed_semantics_corpus |
TestSuite · SeedSemanticsCorpus | hecks test_suite_run.seed_semantics_corpus |
pattern-cases |
TestSuite · RecordPatternCases | hecks test_suite_run.record_pattern_cases |
corpus |
Corpus · RustDomains, RegenOrder, RustCoverage (queries) | hecks corpus_run.rust_domains; hecks corpus_run.regen_order; hecks corpus_run.corpus_rust_coverage |
query_ir |
Corpus · IrConstructs, IrDuplicates, IrImpact (queries) | hecks corpus_run.ir_constructs [names=a,b]; hecks corpus_run.ir_duplicates [domains=a,b] [--meta]; hecks corpus_run.ir_impact name=N field=F |
hecks_query_ir_mcp |
Corpus · ServeQueryIrMcp | hecks corpus_run.serve_query_ir_mcp (stdio) |
present |
Corpus · Present | hecks corpus_run.present [port=4567] |
release |
Publishing · Publish | hecks publishing_run.publish [--gem-only] [--npm-only] [--npm-local] [--no-wait] --confirm (without --confirm, the old --dry-run) |
release_gem |
Publishing · PublishGem | hecks publishing_run.publish_gem --confirm |
QualityControl, for maintainers (attached chapter)
| Script | Aggregate · command | Launcher |
|---|---|---|
qa_tick |
Sweep · Tick | hecks quality_control tick |
qa_sweep |
Sweep · Run, and Target · Release | hecks quality_control sweep [target] [--all] [seeds=] [steps=] [adversarial=] [role_draw=] [dry_run_share=] [self_consistency=] [modes=a,b] [--persistence-parity] [--no-parity]; hecks quality_control release_target <target> notes=text |
qa_pr_check |
Clearance · CheckPullRequests | hecks quality_control check_pull_requests |
qa_open_pr |
Patch · Open, and Improvement · Open | hecks quality_control open_patch bug=BUG#n title=… [body=]; hecks quality_control open_improvement angle=ANGLE-n title=… [body=] |
qa_log_bug |
Bug · Log | hecks quality_control log_bug sweep= title= demonstration= symptom= expectation= submitter= triage=self_contained [tags=a,b] [reproduced=yes] |
qa_seed_angles |
Angle · Seed | hecks quality_control seed_angles |
qa_seed_targets |
Target · Seed | hecks quality_control seed_targets |
qa_generated_domains |
Target · CheckGeneratedDomains | hecks quality_control check_generated_domains [domains=3] [start=] [forms=a,b] [seeds=5] [steps=25] [adversarial=0.3] [--rust] [shrink_budget=200] [domain_shrink_budget=40] [promote=dir name=N] [--from-dials] [blueprint=] [sources=a,b] |
qa_mine_combinations |
Target · MineCombinations | hecks quality_control mine_combinations [candidates=3] [--rust] [seeds=5] [steps=25] [adversarial=0.3] [repair_rounds=1] [agent=cmd] [from=dir] [against=a,b] [--brief] |
qa_domain_novelty |
Target · JudgeNovelty (query) | hecks quality_control judge_novelty <domain> against=a,b |
qa_discover_external_domains |
Target · DiscoverExternalDomains (query) | hecks quality_control discover_external_domains [projects_dir=~/Projects] [max_depth=3] [known_paths=a,b] |
qa_postgres_migrate |
Sweep · MigrateLedgerFromHeki | hecks quality_control migrate_ledger domain_dir= heki_dir= [aggregates=A,B] --confirm |
qa_postgres_role |
Sweep · CreateLedgerRole | hecks quality_control create_ledger_role <database> [role=hecks_qa] |
qa_concurrency_racer |
Sweep · Race (run only as a child process) | not user-facing; ProcessPool starts it with domain= database= schema= verb= args= |
Internal flags that exist only for one tool to call itself (qa_generated_domains --check/--binary/--match) become a child command the ProcessPool adapter starts, not launcher arguments. Where a script’s usage header disagreed with its code, the table follows the code.
Publishing goes to Codebase rather than Custodian because it publishes this repository’s gem, so it only runs in a checkout. The Release it advances lives in the root (section 5).
Building for the Rust host is split by the same test. Custodian’s Build generates and compiles one domain, the way a client deploying to rust/host does, and checks that build against the Ruby engine: rust_conformance replays a script through both, rust_conformance_fuzz does the same with generated sequences, and rust_coverage reports which of the domain’s constructs are routed. Codebase’s Regeneration rebuilds every corpus domain’s committed output, which only this repository has, and Codebase’s Conformance keeps the checks on the language itself (the kernel, the query engines, the reference docs).
project_refusal_wording is an alias for project_rust_vocabulary; it becomes a second name for the same Language command, not a command of its own.
The rules hand-coded in qa_open_pr and qa_pr_check move onto Patch.Open and Improvement.Open. Three are givens today: the branch prefix (a pattern: on the branch), the bug being fixed and the angle being under investigation (givens that read through the reference, as customer.status == "active" does in the banking example). The other two follow ADR 0081, and stay in the GitPr adapter until it lands in a 3.x minor after 3.0:
DailyQuota aggregate, identified by date, from which each Patch.Open takes a slot.HEAD“ becomes a fact Patch.Open declares and the GitPr adapter answers at dispatch. Their git and gh calls move into a GitPr adapter. The declared IssueTracker port gets a bound adapter.Under ADR 0068 a major version means a breaking DSL or runtime change with a CHANGELOG Breaking: entry, and this ADR makes several:
exe/hecks stops being a hand-written router and becomes the generated launcher. The command names stay, but arguments are projected from the bluebook by CliRunner, so flags and argument order change.bin/ goes. Makefiles that project_deploy generated in client repositories call bin/<name> and stop working until they are regenerated.Hecks becomes a reserved chapter name. A client chapter named Hecks is refused at boot with a message to rename it, through the reserved-names check project_reserved_names already maintains.ADR 0068’s rule 4 gives a break that reaches an installed client site one release of warning where a warning is possible. The two parts that reach client sites get one:
hecks era.compact pizzas --confirm”), and project_deploy notes in each Makefile it generates which bin/ calls change. Each warning names 3.0.0 as the removal version.bin/ path go.Inside this repository the move is one pull request, so there is never a second way for a maintainer to run a tool. It is built as ordered commits, each keeping the whole suite green, so it can be reviewed and bisected one step at a time:
lib/hecks/quality_control/.exe/hecks.bin/.Client pins move to 3.0.0 explicitly, because deploys pin exactly (ADR 0068, rule 3). 3.0.0 is the first release the root’s Release aggregate records.
rust/host images build from the packaged gem. An image installs the exact gem version (deploys pin exactly, ADR 0068 rule 3) and runs Custodian’s Build, instead of building from a tag of this repository. Builds from a checkout still work the same way, for testing unreleased changes.attaches, resolved by chapter name. Hecks::Framework.members generalizes into an index of every chapter the gem carries, so a hecksagon names a chapter and never a path. uses_framework keeps its narrower meaning (a bounded context with governance). The launcher reads a first argument that names an attached chapter as that chapter (hecks quality_control log_bug); the Hecks domain’s own verbs stay bare (hecks era.merge_tail).hecks.gemspec beside lib/. The package carries lib/, exe/hecks and the Rust workspace but never hecks.gemspec, so the check cannot mistake an install for a checkout. rust/ no longer tells them apart, since it ships.givens on Era commands.
EraStore#verify_integrity!)compacted_through floor--accept is today--force is todayEraCheck.lineage_capable? exists; it becomes one given, “the store keeps eras”.bin/compact deletes up to the aggregate’s own checkpoint only, not ADR 0079’s per-projection floor (the minimum entry_count across every bound projection). Era’s Compact command lands with or after ADR 0079 and carries its floor as a given (“every bound projection has caught up past the target”), with JournalStore supplying the projection positions as facts. Until then, Compact refuses for any aggregate with a bound projection, as heki_compact already does.translation_audit and scaffold_translation hold era 1 (hold_first!) when they find an empty lineage, so running an audit changes the store. The move splits that apart:
HoldFirst, which an operator runs on purpose or the first boot runs as it does today..hecks/ directory by default, so a check’s history lives beside the domain it checks..hecks/, as a client’s do..adapter declares only its port, fields and secrets, and what an adapter can do is found at runtime by asking the class (a missing save_saga produces a boot warning that saga state is lost on restart). Four pieces replace that:
SaveSaga, LoadSaga and AppendOutbox on the persistence port, and an adapter lists the optional ones it implements. A capability is an operation the port already defines, so no new vocabulary is needed.guarantees :atomic_save, :durable.SaveSaga and LoadSaga, and a policy with an effect needs AppendOutbox. Where the IR cannot tell, a hecksagon states a need on the binding: persisted_by "Heki", requires: :durable. Boot matches needs against declarations and refuses a binding that falls short, the way a chapter’s provides is matched to its users. Today’s saga warning becomes that refusal.The Adapter aggregate records implements and guarantees, so “which bound adapters cannot do what this domain needs” is a query.
.behaviors files beside its bluebook, such as lib/hecks/hecks/custodian.behaviors, as examples/pizzas/bluebook/pizzas.behaviors does today.spec/adapters/conformance/<capability>_spec.rb, and every adapter that declares the capability runs it.spec/adapters/driven/<adapter>_spec.rb, where the existing adapters’ specs already are. That includes the io: true specs for anything that needs a real git, cargo or Postgres.qa/, into .behaviors beside lib/hecks/quality_control/.spec/hecks_domain_boundary_spec.rb lands as the first commit of the implementation, before lib/hecks/hecks/ exists, so the boundary is guarded from the start. It asserts three things:
require "hecks", no file under lib/hecks/hecks/ is in $LOADED_FEATURES.Hecks::Facade::CliRunner, as a generated launcher does, loads nothing under lib/hecks/hecks/ either.Hecks chapter, and holds one only when the client’s hecksagon says attaches "Hecks".main equal to origin/main on a clean tree, the client package version matching the gem’s, a CHANGELOG heading for the version, and an existing tag pointing at the release commit. On Deploy: --schema requiring --tenant, and the adapter being AwsLambda or AwsFargate.This list comes from a static scan, not an estimate. The scan starts from each of the 92 scripts and follows require/require_relative into lib/ and qa/, recording every subprocess, network, database, file, environment, clock, randomness, terminal and process-control call with its file and line. It then resolves by hand every call whose command is built at runtime, and every subprocess helper (git(...), gh(...), Release::Runner::Commands, Vendoring::GitSource). The core runtime that require "hecks" loads is scanned once, separately, since every script shares it.
Driven adapters the scripts need:
| Port | Adapter wraps | Scripts that reach it |
|---|---|---|
| Git | git |
qa_open_pr, qa_tick, qa_mine_combinations, regen_codegen_domains, standardize_comments, bench (commit id), release (preflight, tagging), vendor_bluebook (git archive, through Vendoring::GitSource) |
| GitHub | gh |
qa_open_pr, qa_pr_check, refresh_rspec_runtime_baseline |
| GemRegistry | gem build, gem push, the RubyGems versions API over curl |
release, release_gem |
| NpmRegistry | npm ci, npm publish, npm view |
release |
| SecretVault | 1Password op run |
release, release_gem |
| RustToolchain | cargo, rustup, wasm-bindgen, wasmtime, and the binaries they build (hecks-build, the parser, each domain’s binary) |
project_rust, project_wasm, project_wasm_browser, rust_coverage, rust_conformance, rust_conformance_fuzz, fuzz, bench, qa_generated_domains |
| RubyChild | bundle exec ruby / bundle exec rspec, ruby |
corpus, evolve, lint_deploy_recipes, rust_coverage, rspec_io_parallel_files, stress_concurrency_specs, refresh_rspec_runtime_baseline, qa_concurrency_racer, qa_discover_external_domains, qa_domain_novelty, qa_generated_domains, qa_mine_combinations, qa_sweep, qa_tick |
| TestRunner | RSpec::Core::Runner in process |
spec_example |
| Agent | claude -p (QA_MINER_AGENT overrides) |
qa_mine_combinations |
| Shell | sh -c, rsync, tar |
qa_log_bug (a bug’s demonstration), qa_generated_domains, vendor_bluebook |
| PgAdmin | PG.connect outside the persistence adapters: roles, scratch databases |
qa_postgres_role, fuzz, bench, qa_sweep, regenerate_persistence_legacy_fixtures |
| SqliteFixture | sqlite3 and SQLite3::Database |
regenerate_persistence_legacy_fixtures |
| HostHttp | Net::HTTP |
check_era, smoke_http |
| ProcessPool | fork, Process.wait, signal traps |
fuzz, follow, qa_mine_combinations, qa_sweep, qa_tick, regen_codegen_domains |
| Terminal | IRB, stdin, y/N prompts | console, run, release, standardize_comments, bench |
| Workspace | file writes (52 scripts) and reads (68) | most scripts; SourceTree is the part that rewrites tracked source |
clock (exists) |
Time.now, sleep |
fuzz, bench, release, smoke_http, qa_open_pr, qa_sweep, qa_tick, qa_seed_angles, qa_generated_domains, qa_mine_combinations |
Environment variables are read by 25 scripts; they become world configuration, not an adapter. The fuzzing generators’ Random is seeded and deterministic, so it is logic, not IO.
RubyChild mostly disappears. Most bundle exec ruby bin/<name> calls are one hecks tool running another. Once both are commands on the same domain, the call becomes a dispatch. It stays a subprocess only where isolation is the point: the concurrency racer, the stress specs, and checking a generated domain in a clean process.
What the scan rules out:
project_deploy renders a Makefile and scripts, and those run aws, docker and sam when an operator runs them. The runtime’s one AWS SDK call is inside the Lambda persistence adapter.lint_deploy_recipes runs ruby bin/project_deploy and reads the generated Makefile as text; nothing runs make.gh; the command appears only in hint text.qa_discover_external_domains. It runs bundle, not gem.Already adapters in the runtime (reached through Hecks.boot, used by Custodian’s DomainRuntime and JournalStore):
SystemClock, SecureRandomIdentity, InProcessKeyVault, GoogleAuthenticationTenantProvisioner file writeHecks::CacheDirPorts::Persistence::PostgresDump runs pg_dump and pg_restore, but no script reaches it.
Driving adapters:
exe/heckshecks_mcp_door, hecks_query_ir_mcp)present serves (Forms::App)Era.Compact, Era.MergeTail and Build.BuildWasm. The launcher shows the snake form (hecks era.compact). The old script names survive only in the 2.x warnings, which point at the 3.0 form.exe/hecks already ships keep their launcher form: run, docs, narrate, ir, stores, model_check, smoke_test, project_diagrams, project_cli and mcp (ADR 0066). Clients already type them, so only their argument forms change.hecks era.compact pizzas)--name (--confirm, --strict)name=value, as today; a list is comma-separated--confirm is an argument and the command’s given requires it before anything changes. It replaces --force, --accept, --approve, --write and --yes. qa_sweep’s fractional --dry-run becomes dry_run_share.standardize_comments --fix becomes FixComments, corpus --rust-domains becomes RustDomains, and evolve’s ten subcommands become ten commands. Each gets its own givens and events, so FixComments can require --confirm while CheckComments does not.ask only when a command shares its name. Otherwise the bare name answers (hecks ir, hecks introspection.history pizzas).Today the host mints eras and checks approvals itself, at every boot (ADR 0030). The Ruby era tools reach a production database only from a laptop, through a temporary bastion and an SSM tunnel that make mint-era sets up. For a domain on a shared database, not even that is generated. merge_tail, compact, heki_compact, reattest_era and backfill_era_projections have no production path at all, although the host’s own tamper refusal tells an operator to run reattest_era. A shared-database domain also cannot approve an edge in production, so an edge with compute or rekey rules would stop it booting.
Production follows the database-migrations model, the direction ADR 0030 already took for minting. Nothing reaches into a production database from outside.
make mint-era’s bastion and tunnel remain for incidents, documented as such, and are never part of a normal deploy.Locally and against rehearsal databases, Custodian’s Era commands still act directly on the store, as the scripts do today.
translations/<edge>.approval, holding the edge’s digest, who approved it and when. A reviewer sees the edge and its approval in the same diff.approval.rs), which a committed file cannot know, since production keeps writing between the commit and the deploy. The rehearsal takes over what the tip check did, confirming the edge applies cleanly to real data. Until the 3.x rehearsal gate exists, a committed approval for an edge with compute or rekey rules also requires a rehearsal the approver records by hand in the approval file.mint_harness does today), and the deploy requires a green run for any deploy that carries era work. No standing copy of production is kept.What lands when:
hecks era.approve_translation <domain> --confirm writes an approval file, and the host reads committed approvals at boot as well as journal rows. That closes the one gap that can stop a domain booting, for shared-database domains included.model_check can see the rules that were hidden inside scripts, including QualityControl’s PR-opening rules.hecks launcher replaces 92 separate front doors, and its help text comes from the bluebook.bin/<name> changes in the same pass: .github/workflows/, Makefiles generated by project_deploy, pre-commit and pre-push hooks, specs that shell out, docs/, the README, and the exe/hecks routes from ADR 0066, which become Hecks commands with the same names.console and follow are interactive, qa_concurrency_racer and stress_concurrency_specs must run as separate OS processes, and qa_tick forks other steps. Their commands exist, but the adapter behind them still starts the process or session, and the journal records that it ran, not what the session did.project_cli generates every launcher, including hecks itself, and project_cli is itself a Hecks command. The generated exe/hecks is committed, so the cycle only matters when regenerating it: the previous exe/hecks produces the next one.exe/hecks runs, never from lib/hecks.rb, so boot cost for an ordinary domain does not change.exe/hecks runs.spec/gemspec_packaging_spec.rb asserts that the Rust workspace ships without target/, rust/tests/ or any generated corpus domain, and that lib/hecks.rb loads none of the tooling.hecks --help lists the Codebase and QualityControl verbs too, each refusing with “needs a hecks checkout” when run outside one.given refuses first.bin/<name> as a short script that dispatches one verb. Rejected: project_cli already generates a launcher from the bluebook, and hand-written ones drift from it.exe/hecks, and no domain for the framework itself.codebase.bluebook, a second codebase.hecksagon and adapters/codebase/). Rejected: keeping deployments small is already handled by not loading the tooling and by Rust images, and the split cost a second hecksagon for one domain, file-level packaging rules, and two kinds of install that boot different domains.QualityControl::Patch becoming Hecks::Patch, and so on). Rejected: the QA ledger’s era and tables are keyed by the chapter name and would need a data migration, and renaming the self-hosted Bluebook chapter reaches into the meta-validator.hecks.bluebook file. Rejected: fourteen aggregates in one file is hard to read; the root, Custodian and Codebase files each hold one concern.qa/. Rejected in favour of lib/hecks/, so everything ships and sits beside the runtime it operates on.Where the build settled a detail this ADR left open or spelled differently. The design above is unchanged; the code is on hecks-3-0, under lib/hecks/hecks/.
Hecks::Domain. A new chapter word, namespace "Hecks::Domain", installs them there, because aggregates such as Release, Codemod, Corpus and Fuzzing share names with the gem’s own modules. A chapter whose constants would land in ::Hecks itself is refused at build.answered_by "Port"); that word was removed before release. The bluebook declares the question and the shape of its answer (returns Name, or returns list_of(Name), a value object of the owning aggregate), the hecksagon binds it to a port (answers_query "Name"), and the world picks the adapter. Every row an adapter answers is built as that value object before it enters the domain, so a wrong type, a missing field or a broken invariant is refused. At boot a query has exactly one answer path: it filters the aggregate’s records, or it returns a value object and one port binds it. The adapter method is the query’s snake-cased name. Every Introspection query, the Era readings and Follow use it (DomainRuntime, JournalStore, SourceTree).run=<key>. Each run aggregate is identified by a run argument. Where the domain’s hecks.world opts in through its launcher setting, a creating command given no key gets a UUID and the answer carries it as run:; an explicit key always wins. A chapter without the setting keeps the plain launcher.launcher setting’s names table maps mcp to ServeMcp and console to OpenConsole: help lists the short name, and serve_mcp and open_console keep working. The Introspection queries are named for their verbs (Narrate, Docs, ProjectDiagrams, Glossary, not the Narrative, Document and Diagrams of section 7). narrate and docs take aggregate=Name, and rename takes new_name=Y, not to=Y.Run: LanguageRun, KernelRun, ConformanceRun, RegenerationRun, StyleRun, CodemodRun, TestSuiteRun, CorpusRun, PublishingRun, FuzzRun, ModelCheckRun. Custodian also holds Introspection, Operation, Host, Era, Package, Door and Build.Admit is Permit (EraPermitted), since Language already has an Admit. Release’s Publish is MarkPublished, since PublishingRun has a Publish. Release.Verify runs from published or verified, so repeating it is harmless and a release finished elsewhere still reaches verified. Host reports EraObserved, EraDrifted and HostLost, not the EraChanged of section 5; EraChanged belongs to Era.hecks.world names PostgresEra with one default_adapter line, which binds every aggregate of the chapter and of the attached Governance chapter (governance.world). The database is HECKS_DATABASE, else a local hecks database, and it connects as the ordinary role hecks. environments/memory.world swaps both chapters to Memory when HECKS_ENVIRONMENT=memory (or Hecks.boot(path, environment: "memory")); spec/spec_helper.rb sets the variable, so specs never touch a database.taken_at and its adapter fills it. The Rust host binds no adapter, so it refuses such a query and model_check reports :external_query for each one on a domain with a Rust target.Facade became Doors. Hecks::Facade is Hecks::Doors (Surface is Doors::RubyDoor; the MCP door lives beside it). The old names warn for one release and are removed in 3.1.0.lib/hecks/gate/stages.yml lists each check of a stage (an id, a title, the command, what a red check means), and hecks gate_run.gate <stage> runs them together and reports every red one. GateRun is a Codebase aggregate, so a run is journaled and --wait exits 1 when it faults. .githooks/pre-push calls the same tool.Glossary and ProjectDiagrams answer file names mapped to text, and ScaffoldTranslation answers the edge file’s text; the client saves it.--wait and exit status. A refused command or a bad argument exits 1. Without --wait, a command that was accepted exits 0 even if its lifecycle later fails, and a query exits 0. With --wait the record is re-read after every reaction has run and the launcher exits 1 when it sits in a state its lifecycle marks :failure (ADR 0097; first built as a world failure_states list: flagged failed drifted unreachable refused faulted halted). --wait and the other launcher features work only for a chapter whose hecks.world opts in.translations/<from>-<to>.approval, in the domain’s bluebook directory. It is JSON holding the edge, its digest, who approved it and when, plus a rehearsal block (snapshot, host_version, result, at; result must be pass) that an edge with a compute or rekey rule requires. hecks era.approve_translation <domain> --confirm writes it. This settles the file-name part of the first open item below; the format of committed attestations and tail-merge winners is still open.