enola-guides 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/README.md +20 -3
  4. data/examples/knowledge-base/README.md +20 -0
  5. data/examples/knowledge-base/enola-cluster.yaml +7 -0
  6. data/examples/knowledge-base/wiki/_state/enola/findings.json +10 -0
  7. data/examples/knowledge-base/wiki/_state/enola/receipts.json +16 -0
  8. data/examples/knowledge-base/wiki/shop/permanent/architecture.md +19 -0
  9. data/examples/knowledge-base/wiki/shop/permanent/public-surface.md +14 -0
  10. data/examples/laws/cluster-laws/README.md +1 -1
  11. data/examples/laws/cluster-laws/enola/constraints/a-mutating-action-is-authorized.rb +10 -0
  12. data/examples/laws/cluster-laws/enola/constraints/every-api-route-has-a-consumer.rb +9 -0
  13. data/examples/laws/cluster-laws/enola/constraints/one-owner-per-table.rb +9 -0
  14. data/examples/laws/maintenance-tasks/README.md +1 -1
  15. data/examples/laws/maintenance-tasks/enola/constraints/a-maintenance-task-never-invokes-controller-code.rb +9 -0
  16. data/examples/laws/maintenance-tasks/enola/constraints/{maintenance-tasks.rb → every-maintenance-task-lives-in-the-maintenance-namespace.rb} +0 -7
  17. data/examples/rails-shop/enola/constraints/a-concern-never-reaches-the-class-that-includes-it.rb +9 -0
  18. data/examples/rails-shop/enola/constraints/a-mutating-action-is-authorized.rb +10 -0
  19. data/examples/rails-shop/enola/constraints/a-service-answers-to-call.rb +9 -0
  20. data/examples/rails-shop/enola/constraints/background-jobs-never-invoke-controller-code.rb +9 -0
  21. data/examples/rails-shop/enola/constraints/billing-is-documented.rb +10 -0
  22. data/examples/rails-shop/enola/constraints/billing-keeps-to-its-own-tables.rb +9 -0
  23. data/examples/rails-shop/enola/constraints/frames-keep-their-query-budget.rb +9 -0
  24. data/examples/rails-shop/enola/constraints/jobs-models-and-mailers-never-depend-on-each-other-in-a-circle.rb +9 -0
  25. data/examples/rails-shop/enola/constraints/orders-are-reached-through-their-public-surface.rb +9 -0
  26. data/examples/rails-shop/enola/constraints/reach-for-an-existing-service.rb +8 -0
  27. data/examples/rails-shop/enola/constraints/scopes-come-in-pairs.rb +9 -0
  28. data/examples/rails-shop/enola/constraints/the-public-api-stays-small.rb +10 -0
  29. data/examples/rails-shop/enola/constraints/the-request-api-stays-in-controllers.rb +9 -0
  30. data/guides/09-a-knowledge-base-with-enola.md +148 -0
  31. data/guides/10-hooks.md +101 -0
  32. data/guides/11-the-skills-as-they-run.md +81 -0
  33. data/guides/12-the-wrapper.md +69 -0
  34. data/lib/enola-guides.rb +1 -1
  35. data/templates/constraints-starter/background-jobs-never-invoke-controller-code.rb +9 -0
  36. data/templates/constraints-starter/models-do-not-reach-controllers.rb +9 -0
  37. data/templates/constraints-starter/policies-only-answer.rb +9 -0
  38. data/templates/constraints-starter/the-request-api-stays-in-controllers.rb +9 -0
  39. data/templates/hooks/intent-first-gate.sh +49 -0
  40. data/templates/hooks/record.sh +37 -0
  41. data/templates/hooks/session-refresh.sh +31 -0
  42. data/templates/hooks/stop-debt.sh +39 -0
  43. data/templates/skills/enola-citation-hygiene/SKILL.md +22 -0
  44. data/templates/skills/enola-deepdive/SKILL.md +21 -0
  45. data/templates/skills/enola-drift/SKILL.md +21 -0
  46. data/templates/skills/enola-ingest-check/SKILL.md +23 -0
  47. data/templates/skills/enola-zoom-out/SKILL.md +16 -0
  48. metadata +42 -5
  49. data/examples/laws/cluster-laws/enola/constraints/cluster.rb +0 -24
  50. data/examples/rails-shop/enola/constraints/laws.rb +0 -93
  51. data/templates/constraints-starter.rb +0 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 103217d2be760b0b582027a9119e6acc6fae874ffc1171f8e59df35ac5ffb09b
4
- data.tar.gz: 65f13f8cd1c54cafec16aca3f50a80710dcc77df8f6e66077c54fb346afeaf3a
3
+ metadata.gz: a8d331c92a1d9c3343d76d079967a3d6c96ec41227c35ca99c2977c8980764b6
4
+ data.tar.gz: 578aebe657940e45dcab775af903b1e8f6ddd4eb47ab4880d77b81a8ae67fcf1
5
5
  SHA512:
6
- metadata.gz: 9cac7aed45a1041aeaeb66c46040f165dfa4156b05d5c2c088890dd18912930b6924059b5f0b1bcc00c4379b7574e524ab8794a43c936a5e5915a4afccf7572c
7
- data.tar.gz: fe230a7e9daac6c8c88662bdcce76f1be1ff8019992c48f57a95afb469fc4bad58bd02e6171d62603e796b80cd31938826d59a5c8010adada9fa116d5a16e13d
6
+ metadata.gz: 57c7b7963c5d75e2750338d4f287c8be1115a808d28004f3cefae76b2823de297b3b44e94a2150638b499bb32994f4df32449494b0f2a5daa7cee5e80d3072d6
7
+ data.tar.gz: 7571fc1b51b77801f6d8f0c032b59f1c550a442ac1e73fb686ba5aded8efc000b2ba4c6c83aa1579c587b8b073abb095fc035c89bb88fc367d7eedaf8e4e8b65
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 (2026-08-22)
4
+
5
+ What a knowledge base gets. Four guides (a knowledge base with enola,
6
+ hooks, the skills as they run, the wrapper), four hook templates
7
+ (intent-first gate, turn record, stop-time debt, session refresh), five
8
+ skills as a knowledge base runs them (deepdive, ingest check, drift,
9
+ citation hygiene, zoom out) and a worked example knowledge base with two
10
+ anchored pages, receipts and a finding ledger.
11
+
12
+ ## 0.1.2 (2026-08-22)
13
+
14
+ One law per file: the Rails shop, the cluster laws, the maintenance-task laws
15
+ and the starter each put every law in a file of its own that declares the parts it
16
+ speaks about, so a reader finds a law by its name and a reviewer sees one
17
+ law per diff.
18
+
3
19
  ## 0.1.1 (2026-08-22)
4
20
 
5
21
  The laws catalogue: `examples/laws/`, twenty-six complete declarations one
data/README.md CHANGED
@@ -18,11 +18,13 @@ gem contents enola-guides
18
18
  ## Start with an example
19
19
 
20
20
  [`examples/rails-shop`](examples/rails-shop): a Rails application declaring
21
- thirteen laws in the Ruby DSL, from "jobs never invoke controller code" to
21
+ thirteen laws in the Ruby DSL, one file each, from "jobs never invoke controller code" to
22
22
  "billing keeps to its own tables" and "a mutating action is authorized",
23
23
  with Prism and Rubydex configured as providers beside the extractors.
24
24
  [`examples/ember-app`](examples/ember-app): an Ember application bound to
25
- the conventions recipe in one block. [`examples/laws`](examples/laws/README.md):
25
+ the conventions recipe in one block. [`examples/knowledge-base`](examples/knowledge-base/README.md):
26
+ two pages anchored to code, a cluster config, receipts and a finding ledger,
27
+ the smallest knowledge base the skills and hooks run against. [`examples/laws`](examples/laws/README.md):
26
28
  twenty-six laws, one directory each, from foreign keys on a tenant column
27
29
  to parts that may not cycle, each a complete declaration you copy and run.
28
30
 
@@ -43,6 +45,16 @@ to parts that may not cycle, each a complete declaration you copy and run.
43
45
  census, the runtime capture.
44
46
  8. [Beyond one repository](guides/08-beyond-one-repo.md): the cluster, the
45
47
  seams, the laws across them, the history.
48
+ 9. [A knowledge base with enola](guides/09-a-knowledge-base-with-enola.md):
49
+ what a wiki of decisions gets when its pages anchor to code and every
50
+ number it states was read from a receipt.
51
+ 10. [Hooks](guides/10-hooks.md): the four agent hooks a knowledge base runs
52
+ (intent-first gate, turn record, stop-time debt, session refresh).
53
+ 11. [The skills, as they run](guides/11-the-skills-as-they-run.md): deepdive
54
+ before shaping, claim check while ingesting, drift on a schedule,
55
+ citation hygiene, the blast radius.
56
+ 12. [The wrapper](guides/12-the-wrapper.md): the thin command the skills
57
+ call, the named skip, the receipts and the ledger.
46
58
 
47
59
  ## Skills
48
60
 
@@ -52,7 +64,12 @@ before grepping, grade after changing), `enola-laws` (write a law as a
52
64
  sentence with a reason, lint it, read what it verdicts),
53
65
  `enola-grade-before-push` (the delta is the deliverable, a declined grade
54
66
  is not a pass), `enola-intent-first` (read the governing decision before
55
- the file moves). Copy a directory into your agent's skills folder.
67
+ the file moves), and the five a knowledge base runs: `enola-deepdive`,
68
+ `enola-ingest-check`, `enola-drift`, `enola-citation-hygiene`,
69
+ `enola-zoom-out`, each described as it runs in
70
+ [guide 11](guides/11-the-skills-as-they-run.md). Copy a directory into
71
+ your agent's skills folder; the hooks they pair with are under
72
+ [`templates/hooks/`](templates/hooks).
56
73
 
57
74
  ## Templates and recipes
58
75
 
@@ -0,0 +1,20 @@
1
+ # A knowledge base with enola, minimal
2
+
3
+ The smallest knowledge base that gets what [guide 09](../../guides/09-a-knowledge-base-with-enola.md) describes:
4
+ two pages anchored to code with `enola_intent`, a cluster config, the
5
+ committed receipts, and the finding ledger. The skills under
6
+ [`templates/skills`](../../templates/skills) and the hooks under
7
+ [`templates/hooks`](../../templates/hooks) run against exactly these
8
+ files.
9
+
10
+ - `enola-cluster.yaml`: the members, with `knowledge` pointing at the wiki.
11
+ - `wiki/shop/permanent/architecture.md`: a page whose `enola_intent`
12
+ anchors it to `app/jobs/**` and `app/models/**`, with one receipt
13
+ citation.
14
+ - `wiki/shop/permanent/public-surface.md`: a page anchored to the
15
+ controllers it describes.
16
+ - `wiki/_state/enola/receipts.json`: what `kb enola generate` recorded.
17
+ - `wiki/_state/enola/findings.json`: the ledger, one judged finding.
18
+
19
+ `governing_intent app/jobs/order_mailer_job.rb` returns the first page;
20
+ `kb enola citations` verifies its citation against the receipts file.
@@ -0,0 +1,7 @@
1
+ cluster: shop
2
+ knowledge: wiki
3
+ repos:
4
+ - path: ../rails-shop
5
+ label: shop
6
+ - path: ../ember-app
7
+ label: shop-ui
@@ -0,0 +1,10 @@
1
+ {
2
+ "findings": {
3
+ "cycles|app/jobs -> app/models -> app/jobs": {
4
+ "verdict": "accepted",
5
+ "at": "2026-08-22",
6
+ "reason": "real; the mailer path",
7
+ "landed": "wiki/shop/permanent/architecture.md"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "repos": {
3
+ "shop": {
4
+ "fingerprint": "8d1e5a7b93c2f0e4d6a18b7c3e9f0a2d4c6b8e1f3a5c7d9e0b2f4a6c8e0d2c40a",
5
+ "git_commit": "3f2a9c1d4e5b6a7c8d9e0f1a2b3c4d5e6f7a8b9c",
6
+ "enola_version": "0.1.22",
7
+ "providers": ["prism", "rubydex"]
8
+ },
9
+ "shop-ui": {
10
+ "fingerprint": "2c7e9b1d4f6a8c0e2b4d6f8a0c2e4b6d8f0a2c4e6b8d0f2a4c6e8b0d2f4a6c8e",
11
+ "git_commit": "9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c",
12
+ "enola_version": "0.1.22",
13
+ "providers": []
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,19 @@
1
+ ---
2
+ title: Shop architecture
3
+ enola_intent:
4
+ governs:
5
+ - app/jobs/**
6
+ - app/models/**
7
+ decisions:
8
+ - jobs enqueue, models decide; a job never calls back into a job
9
+ ---
10
+
11
+ # Shop architecture
12
+
13
+ Orders are written by `OrderService` only; the two other writers the
14
+ graph reported in 2026-08 were folded into it. `Order` has 41 dependents
15
+ (receipt shop@3f2a9c1, fingerprint 8d1e…c40a).
16
+
17
+ The cycle `app/jobs -> app/models -> app/jobs` through the mailer path is
18
+ real and accepted in the ledger; it is cut when the mailer moves under
19
+ `app/services`.
@@ -0,0 +1,14 @@
1
+ ---
2
+ title: Public surface
3
+ enola_intent:
4
+ governs:
5
+ - app/controllers/api/**
6
+ decisions:
7
+ - every route under /api has at least one consumer in shop-ui
8
+ ---
9
+
10
+ # Public surface
11
+
12
+ Fourteen routes under `/api`, all matched by a consumer in `shop-ui`
13
+ (coverage report, receipt shop@3f2a9c1). A route that stops matching is
14
+ a finding of `unused-routes`, judged in the ledger before any removal.
@@ -5,5 +5,5 @@ repositories, mutating actions selected through the routes they handle, and
5
5
  routes no loaded client calls. Each refuses by name in a single-repository
6
6
  snapshot rather than reporting silence as compliance.
7
7
 
8
- Declaration under `enola/constraints/`. Copy it, run `enola constraints lint .`,
8
+ One file per law under `enola/constraints/`, each file declaring the parts it speaks about. Copy it, run `enola constraints lint .`,
9
9
  then `enola --generate . && enola check .`.
@@ -0,0 +1,10 @@
1
+ Enola.architecture "platform" do
2
+ part :mutating_actions, service: :backend, files: "app/controllers/**", handles: [:post, :put, :patch, :delete]
3
+ part :policies, service: :backend, files: "app/policies/**"
4
+
5
+ law "a mutating action is authorized" do
6
+ mutating_actions.must_reach :policies
7
+ why "an action behind a POST, PUT, PATCH or DELETE route changes state, and the policy call is the one place the change is authorized"
8
+ mode :advisory
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ Enola.architecture "platform" do
2
+ part :api, service: :backend, files: "config/**", kind: :route
3
+
4
+ law "every api route has a consumer" do
5
+ api.must_have_consumer
6
+ why "a route nobody calls is a surface nobody maintains"
7
+ mode :advisory
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ Enola.architecture "platform" do
2
+ part :tables, files: "app/models/**", kind: :storage
3
+
4
+ law "one owner per table" do
5
+ tables.must_be_unique_across by: :table
6
+ why "a table two repositories both model is written by two codebases that never see each other's validations or callbacks"
7
+ mode :advisory
8
+ end
9
+ end
@@ -5,5 +5,5 @@ depends on, and a boundary against controller code that has no request to
5
5
  read. Both were mined from a codebase where they already held, which is the
6
6
  right moment to declare a law.
7
7
 
8
- Declaration under `enola/constraints/`. Copy it, run `enola constraints lint .`,
8
+ One file per law under `enola/constraints/`, each file declaring the parts it speaks about. Copy it, run `enola constraints lint .`,
9
9
  then `enola --generate . && enola check .`.
@@ -0,0 +1,9 @@
1
+ Enola.architecture "app" do
2
+ part :tasks, files: "app/tasks/**"
3
+ part :controllers, files: "app/controllers/**"
4
+
5
+ law "a maintenance task never invokes controller code" do
6
+ tasks.must_not_call controllers
7
+ why "a task runs from the runner or the console with no request around it, so controller code reached from one has nothing to read"
8
+ end
9
+ end
@@ -1,15 +1,8 @@
1
1
  Enola.architecture "app" do
2
2
  part :maintenance_tasks, files: "app/tasks/**", kind: :symbol, where: { symbol_kind: "class" }
3
- part :tasks, files: "app/tasks/**"
4
- part :controllers, files: "app/controllers/**"
5
3
 
6
4
  law "every maintenance task lives in the Maintenance namespace" do
7
5
  maintenance_tasks.names_must_match "Maintenance::*"
8
6
  why "the task runner resolves task constants from that namespace, so a task declared outside it never appears in the runner and cannot be run by anyone"
9
7
  end
10
-
11
- law "a maintenance task never invokes controller code" do
12
- tasks.must_not_call controllers
13
- why "a task runs from the runner or the console with no request around it, so controller code reached from one has nothing to read"
14
- end
15
8
  end
@@ -0,0 +1,9 @@
1
+ Enola.architecture "shop" do
2
+ rails
3
+
4
+ law "a concern never reaches the class that includes it" do
5
+ concerns.must_not_reach_includers
6
+ why "a mixin that knows its includer is half a class in hiding"
7
+ mode :ratchet
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ Enola.architecture "shop" do
2
+ rails
3
+ part :mutating_actions, files: "app/controllers/**", handles: [:post, :put, :patch, :delete]
4
+
5
+ law "a mutating action is authorized" do
6
+ mutating_actions.must_reach :policies
7
+ why "every write passes a policy before it touches a record"
8
+ mode :ratchet
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ Enola.architecture "shop" do
2
+ part :service_objects, files: "app/services/**", kind: :symbol, where: { symbol_kind: "class" }, owns: :methods
3
+
4
+ law "a service answers to call" do
5
+ service_objects.must_define :call
6
+ why "one door per service, so a caller never reads it to find the verb"
7
+ mode :ratchet
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ Enola.architecture "shop" do
2
+ rails
3
+
4
+ law "background jobs never invoke controller code" do
5
+ jobs.must_not_call controllers
6
+ why "rendering from a job goes through ApplicationController.renderer"
7
+ mode :ratchet
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ Enola.architecture "shop" do
2
+ part :billing, files: ["app/billing/**", "app/models/invoice.rb"]
3
+
4
+ law "billing is documented" do
5
+ billing.must_be_governed
6
+ since "2026-09-01"
7
+ why "code under a decision needs the page that decided it"
8
+ mode :ratchet
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ Enola.architecture "shop" do
2
+ part :billing, files: ["app/billing/**", "app/models/invoice.rb"]
3
+
4
+ law "billing keeps to its own tables" do
5
+ billing.storage_must_stay_home
6
+ why "a part that writes another part's table owns a bug it cannot see"
7
+ mode :ratchet
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ Enola.architecture "shop" do
2
+ rails
3
+
4
+ law "frames keep their query budget" do
5
+ controllers.must_keep_budget metric: :queries, max: 20
6
+ why "a frame past twenty queries is a page that will not scale; refused by name until a capture exists"
7
+ mode :advisory
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ Enola.architecture "shop" do
2
+ rails
3
+
4
+ law "jobs, models and mailers never depend on each other in a circle" do
5
+ jobs.must_not_cycle_with :models, :mailers
6
+ why "parts that reach each other in a circle cannot be taken apart"
7
+ mode :advisory
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ Enola.architecture "shop" do
2
+ part :orders, files: ["app/orders/**", "app/models/order.rb"], public: ["app/orders/public/**"]
3
+
4
+ law "orders are reached through their public surface" do
5
+ orders.stays_inside
6
+ why "the public directory is the contract; everything else may change"
7
+ mode :ratchet
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ Enola.architecture "shop" do
2
+ part :service_objects, files: "app/services/**", kind: :symbol, where: { symbol_kind: "class" }, owns: :methods
3
+
4
+ law "reach for an existing service" do
5
+ service_objects.advises "look in app/services before writing a new one"
6
+ why "duplication is a judgement no fact answers; advice where the edit happens is what can be offered honestly"
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ Enola.architecture "shop" do
2
+ part :records, ancestor: "ApplicationRecord"
3
+
4
+ law "scopes come in pairs" do
5
+ records.names_must_match "with_*", requires: "without_*"
6
+ why "every with_ scope has a without_ sibling, or the query reads half a thought"
7
+ mode :advisory
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ Enola.architecture "shop" do
2
+ part :api, files: "config/**", kind: :route
3
+
4
+ law "the public api stays small" do
5
+ api.at_most 120
6
+ growth 5
7
+ why "a route is a promise; the count may grow, but only on purpose"
8
+ mode :ratchet
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ Enola.architecture "shop" do
2
+ rails
3
+
4
+ law "the request api stays in controllers" do
5
+ models.must_not_call "params", receiver: :none
6
+ why "params without a receiver is the controller's; a model reading it only works inside a request"
7
+ mode :ratchet
8
+ end
9
+ end
@@ -0,0 +1,148 @@
1
+ # A knowledge base with enola
2
+
3
+ A team that keeps decisions beside its code (a wiki, ADRs, a design log)
4
+ can compile them into the graph. Then a file answers which decision
5
+ governs it, a decision answers which code it covers, a structural claim
6
+ in prose can be re-checked mechanically, and a finding judged once stays
7
+ judged. This guide is that practice, as it runs day to day on a
8
+ twenty-repository knowledge base.
9
+
10
+ ## Pages compile into the graph
11
+
12
+ A markdown page carrying an `enola_intent:` block is an intent fact when
13
+ the snapshot is taken, and every anchor on it is an edge to a file:
14
+
15
+ ```yaml
16
+ ---
17
+ title: Charges run through one service
18
+ enola_intent:
19
+ page:
20
+ type: decision
21
+ status: accepted
22
+ scope:
23
+ - shop
24
+ relations:
25
+ - rel: supersedes
26
+ to: wiki/adrs/charges-in-controllers.md
27
+ anchors:
28
+ - repo: shop
29
+ path: app/billing/charge.rb
30
+ - repo: shop
31
+ path: app/billing/refund.rb
32
+ ---
33
+ ```
34
+
35
+ Derive the block rather than hand-maintain it: if your pages already cite
36
+ the code they describe (`sources:` lines naming repository paths), the
37
+ anchors are those citations, and a small script stamps the block from
38
+ them and checks the derivation in CI. enola validates the block; keeping
39
+ it truthful to your conventions is yours.
40
+
41
+ Index the knowledge base as a member of the cluster, last, so its pages
42
+ compile over the code they anchor:
43
+
44
+ ```yaml
45
+ repos:
46
+ - ../shop
47
+ - ../mobile
48
+ - .
49
+ ```
50
+
51
+ ## Ask the graph what governs a file
52
+
53
+ ```bash
54
+ enola plan -paths app/billing/charge.rb .
55
+ ```
56
+
57
+ and over MCP, `governing_intent` for a file or a symbol: the pages whose
58
+ anchors cover it, with their relation trails (what supersedes what, what
59
+ depends on what), and for a page, which code its anchors cover. A file no
60
+ page governs is an answer too, and the difference between "no page
61
+ governs this" and "the graph was not asked" must stay visible: a missing
62
+ snapshot is a named skip, never silence.
63
+
64
+ This is the call behind the [intent-first gate](10-hooks.md): before an
65
+ agent's first edit into a repository, the governing pages are read.
66
+
67
+ ## Laws over the pages
68
+
69
+ Because anchors are facts, laws can read them (see [writing
70
+ laws](04-writing-laws.md)):
71
+
72
+ ```ruby
73
+ part :billing, files: "app/billing/**"
74
+ part :old_way, governed_by: "wiki/adrs/*.md status:superseded"
75
+ part :new_way, governed_by: "wiki/adrs/charges-in-one-service.md"
76
+
77
+ law "billing is documented" do
78
+ billing.must_be_governed
79
+ why "code under a decision needs the page that decided it"
80
+ end
81
+
82
+ law "the new way never reaches the old" do
83
+ new_way.must_not_call :old_way
84
+ why "a superseded decision's code is on its way out, not a dependency"
85
+ end
86
+ ```
87
+
88
+ ## Receipts are the committed state
89
+
90
+ Graph artifacts (`.enola/`, the cluster's output directory) are
91
+ machine-local and ignored. What enters version control is the receipt:
92
+ one entry per repository with the snapshot's fingerprint, the commit it
93
+ was taken at, the enola version and the providers that ran. Keep them in
94
+ one file the knowledge base owns:
95
+
96
+ ```json
97
+ {"repos": {"shop": {"git_commit": "…", "content_digest": "sha256:…", "enola_version": "0.5.0"}}}
98
+ ```
99
+
100
+ Two rules follow. **Regenerate the whole cluster, never one member**: a
101
+ snapshot scoped to one repository replaces the union rather than
102
+ updating its slice, and everything that reads the union then answers
103
+ from a narrower world without saying so. **Not comparable is not a
104
+ pass**: when a member's snapshot changed enola version or provider set,
105
+ the grade declines; re-pin and regenerate rather than reading silence as
106
+ clean.
107
+
108
+ ## Citations with a grammar
109
+
110
+ A structural claim in prose ("`internal/facts` has 157 dependents")
111
+ cites the receipt it was read from, in one fixed shape, so a script can
112
+ re-check it against the committed receipts and flag the ones whose
113
+ digest moved. The shape is yours to choose; what matters is that there
114
+ is one, that one extractor parses it, and that a periodic sweep lists
115
+ every citation with a verdict (verified, stale, malformed). A claim
116
+ nobody can re-check is a sentence, not a fact.
117
+
118
+ ## Findings are judged once
119
+
120
+ The explainers report candidates, not verdicts: a cycle at 0.7
121
+ confidence, a god class, an unused route. Before a finding grounds a
122
+ page, confirm it against the code and record the judgment in a ledger
123
+ beside the receipts, one entry per finding signature: accepted (and
124
+ where it landed), rejected, or permanent noise. The next session reads
125
+ the ledger instead of re-deciding, and the periodic sweep reports a
126
+ ledger entry whose finding no longer exists, which is the code moving
127
+ past a recorded judgment.
128
+
129
+ ## Pulled, with one push
130
+
131
+ Skills fetch the graph while already reasoning about something: a
132
+ deepdive asks `impact_analysis` and `governing_intent`, an ingest checks
133
+ a claim with `query_facts` while the source is open, a drift sweep
134
+ regenerates and diffs receipts. Nothing lands on a board because the
135
+ graph said so. The one exception is the session-end hook, which speaks
136
+ only when the session's change introduced a structural regression, or
137
+ when it could not grade at all; a gate nobody remembers to run is no gate.
138
+
139
+ ## The wrapper
140
+
141
+ Every skill calls a thin command the knowledge base owns rather than
142
+ the binary: `kb enola generate | receipt | diff | findings | judge | impact
143
+ | govern | citations | baseline | check | coverage | doctor`. It resolves
144
+ the binary, the cluster config and the receipts file, and when any is
145
+ absent it prints one line saying so and exits 0. That is what keeps a
146
+ skill from failing on a machine without enola, and what keeps "the graph
147
+ was not asked" distinguishable from "the graph agreed". The [wrapper
148
+ guide](12-the-wrapper.md) has its shape.
@@ -0,0 +1,101 @@
1
+ # Hooks: the graph at the moments that matter
2
+
3
+ A hook fires at a moment nobody has to remember: a session starting, the
4
+ first edit into a repository, a turn ending, a push. Each one here reads
5
+ the graph at that moment, blocks at most once, and forces a reading,
6
+ never a verdict. The scripts are in [`templates/hooks/`](../templates/hooks);
7
+ they are written for Claude Code's hook protocol (JSON on stdin, exit 2
8
+ to block with the message on stderr) and translate to any agent with
9
+ hooks.
10
+
11
+ ## enola's own pair
12
+
13
+ ```bash
14
+ enola install --hooks
15
+ ```
16
+
17
+ wires two: **session start** restores the snapshot and serves it over
18
+ MCP; **stop** grades the session's delta against the pinned baseline and
19
+ speaks only when the change introduced a regression or could not be
20
+ compared. `enola doctor` reports when each last fired and what it
21
+ concluded; a hook that never fires is the failure to look for.
22
+
23
+ ## The intent-first gate (PreToolUse on Edit and Write)
24
+
25
+ The first write into each repository per piece of work is blocked once.
26
+ The block carries two things: the pages that govern the file
27
+ (`governing_intent`, or `enola plan -paths <file>` when the MCP is not
28
+ in reach) and the pre-edit contract (the declared laws binding the path
29
+ and the blast radius over the current snapshot). The agent reads, then
30
+ retries, and the retry proceeds. The gate re-arms on every user message,
31
+ so each ask gets its own forced stop and no piece of work gets two.
32
+
33
+ What it refuses to do: decide. If the governing page contradicts the
34
+ intended change, the contract says amend the page first; the hook only
35
+ makes the page impossible to miss. When no page governs the file, it
36
+ says so and asks whether the work should be shaped. When the graph is
37
+ absent, it says so and lets the write through; a missing graph is a
38
+ named skip, never a wall.
39
+
40
+ `templates/hooks/intent-first-gate.sh` gates Edit and Write. The brain's
41
+ own gate also classifies shell commands as writes (a `sed -i`, a
42
+ redirect, a `cp` into the repository); the template marks where that
43
+ classifier goes and leaves it out, because the rules for what counts as a
44
+ write depend on your tree.
45
+
46
+ ## The stop-time debt (Stop)
47
+
48
+ When the turn ends, two debts can block it once each:
49
+
50
+ - **Code moved, no spec moved.** Files changed in a governed repository
51
+ this turn and no governing page did. The block asks for either the
52
+ amendment or an explicit sentence, in the reply, saying why the pages
53
+ as written already cover the change.
54
+ - **A push went out with no grade.** A `git push` to a repository
55
+ happened this turn and no `enola check` ran. The block asks for the
56
+ grade, read, before finishing.
57
+
58
+ Both read a small per-session record the PostToolUse hook writes
59
+ (`templates/hooks/record.sh`): which files moved, whether a push
60
+ happened, whether a check ran. The debt hook is the mechanical half of
61
+ two rules a team can otherwise only ask people to remember.
62
+
63
+ ## The union refresh (SessionStart)
64
+
65
+ When the session starts, compare the paired repository's HEAD with the
66
+ commit the committed receipt records. When they differ, regenerate that
67
+ member's slice of the union in the background and say so in one line;
68
+ when they agree, say that. The agent's first structural question then
69
+ reads a graph that matches the checkout it is about to edit.
70
+
71
+ ## Wiring
72
+
73
+ Claude Code, in `.claude/settings.json`:
74
+
75
+ ```json
76
+ {
77
+ "hooks": {
78
+ "SessionStart": [{"matcher": "startup|resume", "hooks": [
79
+ {"type": "command", "command": "enola hook session-start"},
80
+ {"type": "command", "command": "\"${CLAUDE_PROJECT_DIR:-.}/hooks/session-refresh.sh\""}]}],
81
+ "PreToolUse": [{"matcher": "Edit|Write|MultiEdit", "hooks": [
82
+ {"type": "command", "command": "\"${CLAUDE_PROJECT_DIR:-.}/hooks/intent-first-gate.sh\""}]}],
83
+ "PostToolUse": [{"matcher": "Edit|Write|MultiEdit|Bash", "hooks": [
84
+ {"type": "command", "command": "\"${CLAUDE_PROJECT_DIR:-.}/hooks/record.sh\""}]}],
85
+ "UserPromptSubmit": [{"hooks": [
86
+ {"type": "command", "command": "\"${CLAUDE_PROJECT_DIR:-.}/hooks/record.sh\" reset"}]}],
87
+ "Stop": [{"hooks": [
88
+ {"type": "command", "command": "enola hook stop"},
89
+ {"type": "command", "command": "\"${CLAUDE_PROJECT_DIR:-.}/hooks/stop-debt.sh\""}]}]
90
+ }
91
+ }
92
+ ```
93
+
94
+ ## What it feels like
95
+
96
+ An agent asked to change a billing service edits nothing until it has
97
+ read the decision that put charges through one service; it grades the
98
+ change before the push and writes the grade into the pull request; and
99
+ when it ends the turn having changed code under a decision without
100
+ touching the decision, it is asked to account for that once. None of
101
+ this needs the agent to know enola exists.