textus 0.52.0 → 0.53.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 (254) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +62 -54
  4. data/SPEC.md +62 -187
  5. data/docs/architecture/README.md +88 -77
  6. data/exe/textus +1 -1
  7. data/lib/textus/action/accept.rb +53 -0
  8. data/lib/textus/action/audit.rb +133 -0
  9. data/lib/textus/action/base.rb +42 -0
  10. data/lib/textus/{read → action}/blame.rb +30 -22
  11. data/lib/textus/action/boot.rb +26 -0
  12. data/lib/textus/action/data_mv.rb +71 -0
  13. data/lib/textus/action/deps.rb +48 -0
  14. data/lib/textus/action/doctor.rb +26 -0
  15. data/lib/textus/action/drain.rb +41 -0
  16. data/lib/textus/action/enqueue.rb +55 -0
  17. data/lib/textus/action/get.rb +80 -0
  18. data/lib/textus/action/jobs.rb +38 -0
  19. data/lib/textus/action/key_delete.rb +46 -0
  20. data/lib/textus/action/key_delete_prefix.rb +46 -0
  21. data/lib/textus/action/key_mv.rb +143 -0
  22. data/lib/textus/action/key_mv_prefix.rb +59 -0
  23. data/lib/textus/action/list.rb +44 -0
  24. data/lib/textus/action/propose.rb +54 -0
  25. data/lib/textus/action/published.rb +26 -0
  26. data/lib/textus/action/pulse/scanner.rb +118 -0
  27. data/lib/textus/action/pulse.rb +87 -0
  28. data/lib/textus/action/put.rb +63 -0
  29. data/lib/textus/action/rdeps.rb +49 -0
  30. data/lib/textus/action/reject.rb +49 -0
  31. data/lib/textus/action/rule_explain.rb +95 -0
  32. data/lib/textus/action/rule_lint.rb +70 -0
  33. data/lib/textus/action/rule_list.rb +46 -0
  34. data/lib/textus/action/schema_envelope.rb +31 -0
  35. data/lib/textus/action/uid.rb +35 -0
  36. data/lib/textus/action/where.rb +38 -0
  37. data/lib/textus/action/write_verb.rb +58 -0
  38. data/lib/textus/background/job/base.rb +27 -0
  39. data/lib/textus/background/job/materialize.rb +31 -0
  40. data/lib/textus/background/job/refresh.rb +22 -0
  41. data/lib/textus/background/job/sweep.rb +31 -0
  42. data/lib/textus/background/job.rb +19 -0
  43. data/lib/textus/background/plan.rb +9 -0
  44. data/lib/textus/background/planner/plan.rb +113 -0
  45. data/lib/textus/{maintenance → background}/retention/apply.rb +7 -9
  46. data/lib/textus/background/worker.rb +67 -0
  47. data/lib/textus/boot.rb +53 -45
  48. data/lib/textus/command.rb +36 -0
  49. data/lib/textus/container.rb +1 -1
  50. data/lib/textus/{domain → core}/duration.rb +1 -1
  51. data/lib/textus/{domain → core}/freshness/evaluator.rb +11 -11
  52. data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
  53. data/lib/textus/{domain → core}/freshness.rb +2 -2
  54. data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
  55. data/lib/textus/{domain → core}/retention.rb +2 -2
  56. data/lib/textus/{domain → core}/sentinel.rb +1 -1
  57. data/lib/textus/doctor/check/generator_drift.rb +1 -1
  58. data/lib/textus/doctor/check/handler_permit.rb +34 -0
  59. data/lib/textus/doctor/check/hooks.rb +11 -18
  60. data/lib/textus/doctor/check/illegal_keys.rb +1 -1
  61. data/lib/textus/doctor/check/intake_registration.rb +5 -5
  62. data/lib/textus/doctor/check/proposal_targets.rb +3 -3
  63. data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
  64. data/lib/textus/doctor/check/schema_violations.rb +8 -2
  65. data/lib/textus/doctor/check.rb +12 -9
  66. data/lib/textus/{read → doctor}/validator.rb +22 -13
  67. data/lib/textus/doctor.rb +6 -6
  68. data/lib/textus/envelope/io/writer.rb +65 -36
  69. data/lib/textus/envelope.rb +5 -3
  70. data/lib/textus/errors.rb +17 -9
  71. data/lib/textus/events.rb +21 -0
  72. data/lib/textus/gate/auth.rb +181 -0
  73. data/lib/textus/gate.rb +114 -0
  74. data/lib/textus/init/templates/machine_intake.rb +39 -35
  75. data/lib/textus/init/templates/orientation_reducer.rb +15 -11
  76. data/lib/textus/init.rb +90 -73
  77. data/lib/textus/key/path.rb +9 -2
  78. data/lib/textus/layout.rb +13 -0
  79. data/lib/textus/manifest/data.rb +14 -14
  80. data/lib/textus/manifest/entry/base.rb +15 -11
  81. data/lib/textus/manifest/entry/parser.rb +6 -6
  82. data/lib/textus/manifest/entry/produced.rb +3 -2
  83. data/lib/textus/manifest/entry/publish/mode.rb +1 -1
  84. data/lib/textus/manifest/entry/publish/to_paths.rb +2 -1
  85. data/lib/textus/manifest/entry/validators/events.rb +1 -1
  86. data/lib/textus/{domain/policy/handler_allowlist.rb → manifest/policy/handler_permit.rb} +4 -4
  87. data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
  88. data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
  89. data/lib/textus/manifest/policy/react.rb +30 -0
  90. data/lib/textus/{domain → manifest}/policy/retention.rb +3 -3
  91. data/lib/textus/{domain → manifest}/policy/source.rb +24 -19
  92. data/lib/textus/manifest/policy.rb +36 -48
  93. data/lib/textus/manifest/resolver.rb +3 -2
  94. data/lib/textus/manifest/rules.rb +4 -4
  95. data/lib/textus/manifest/schema/keys.rb +17 -11
  96. data/lib/textus/manifest/schema/validator.rb +24 -22
  97. data/lib/textus/manifest/schema/vocabulary.rb +1 -1
  98. data/lib/textus/manifest/schema.rb +2 -2
  99. data/lib/textus/manifest.rb +2 -2
  100. data/lib/textus/{produce → pipeline}/acquire/handler.rb +2 -2
  101. data/lib/textus/{produce → pipeline}/acquire/intake.rb +22 -20
  102. data/lib/textus/{produce → pipeline}/acquire/projection.rb +13 -11
  103. data/lib/textus/{produce → pipeline}/acquire/serializer/json.rb +2 -2
  104. data/lib/textus/{produce → pipeline}/acquire/serializer/text.rb +1 -1
  105. data/lib/textus/{produce → pipeline}/acquire/serializer/yaml.rb +2 -2
  106. data/lib/textus/{produce → pipeline}/acquire/serializer.rb +1 -1
  107. data/lib/textus/{produce → pipeline}/engine.rb +7 -5
  108. data/lib/textus/{produce → pipeline}/render.rb +3 -1
  109. data/lib/textus/ports/audit_log.rb +31 -5
  110. data/lib/textus/ports/audit_subscriber.rb +4 -4
  111. data/lib/textus/{domain/jobs → ports/queue}/job.rb +19 -12
  112. data/lib/textus/ports/queue.rb +1 -1
  113. data/lib/textus/ports/sentinel_store.rb +2 -2
  114. data/lib/textus/ports/watcher_lock.rb +48 -0
  115. data/lib/textus/projection.rb +8 -8
  116. data/lib/textus/schema/tools.rb +4 -3
  117. data/lib/textus/session.rb +6 -3
  118. data/lib/textus/step/base.rb +35 -0
  119. data/lib/textus/step/builtin/csv_fetch.rb +19 -0
  120. data/lib/textus/step/builtin/ical_events_fetch.rb +30 -0
  121. data/lib/textus/step/builtin/json_fetch.rb +18 -0
  122. data/lib/textus/step/builtin/markdown_links_fetch.rb +20 -0
  123. data/lib/textus/step/builtin/rss_fetch.rb +26 -0
  124. data/lib/textus/step/builtin.rb +22 -0
  125. data/lib/textus/{hooks → step}/catalog.rb +3 -3
  126. data/lib/textus/{hooks → step}/context.rb +15 -13
  127. data/lib/textus/step/discovery.rb +24 -0
  128. data/lib/textus/{hooks → step}/error_log.rb +1 -1
  129. data/lib/textus/{hooks → step}/event_bus.rb +15 -16
  130. data/lib/textus/step/fetch.rb +13 -0
  131. data/lib/textus/{hooks → step}/fire_report.rb +1 -1
  132. data/lib/textus/step/loader.rb +108 -0
  133. data/lib/textus/step/observe.rb +31 -0
  134. data/lib/textus/step/registry_store.rb +66 -0
  135. data/lib/textus/{hooks → step}/signature.rb +1 -1
  136. data/lib/textus/step/transform.rb +12 -0
  137. data/lib/textus/step/validate.rb +11 -0
  138. data/lib/textus/step.rb +10 -0
  139. data/lib/textus/store.rb +17 -15
  140. data/lib/textus/surfaces/cli/group/data.rb +11 -0
  141. data/lib/textus/surfaces/cli/group/key.rb +11 -0
  142. data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
  143. data/lib/textus/surfaces/cli/group/rule.rb +11 -0
  144. data/lib/textus/surfaces/cli/group/schema.rb +11 -0
  145. data/lib/textus/surfaces/cli/group.rb +50 -0
  146. data/lib/textus/surfaces/cli/runner.rb +236 -0
  147. data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
  148. data/lib/textus/surfaces/cli/verb/get.rb +21 -0
  149. data/lib/textus/surfaces/cli/verb/init.rb +20 -0
  150. data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
  151. data/lib/textus/surfaces/cli/verb/put.rb +30 -0
  152. data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
  153. data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
  154. data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
  155. data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
  156. data/lib/textus/surfaces/cli/verb.rb +111 -0
  157. data/lib/textus/surfaces/cli.rb +148 -0
  158. data/lib/textus/surfaces/mcp/catalog.rb +99 -0
  159. data/lib/textus/surfaces/mcp/errors.rb +34 -0
  160. data/lib/textus/surfaces/mcp/server.rb +145 -0
  161. data/lib/textus/surfaces/mcp/session.rb +9 -0
  162. data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
  163. data/lib/textus/surfaces/mcp.rb +8 -0
  164. data/lib/textus/surfaces/role_scope.rb +38 -0
  165. data/lib/textus/surfaces/watcher.rb +38 -0
  166. data/lib/textus/version.rb +1 -1
  167. data/lib/textus.rb +64 -22
  168. metadata +132 -118
  169. data/lib/textus/cli/group/hook.rb +0 -9
  170. data/lib/textus/cli/group/key.rb +0 -9
  171. data/lib/textus/cli/group/mcp.rb +0 -9
  172. data/lib/textus/cli/group/rule.rb +0 -9
  173. data/lib/textus/cli/group/schema.rb +0 -9
  174. data/lib/textus/cli/group/zone.rb +0 -9
  175. data/lib/textus/cli/group.rb +0 -48
  176. data/lib/textus/cli/runner.rb +0 -193
  177. data/lib/textus/cli/verb/doctor.rb +0 -17
  178. data/lib/textus/cli/verb/get.rb +0 -18
  179. data/lib/textus/cli/verb/hook_run.rb +0 -48
  180. data/lib/textus/cli/verb/hooks.rb +0 -50
  181. data/lib/textus/cli/verb/init.rb +0 -18
  182. data/lib/textus/cli/verb/mcp_serve.rb +0 -22
  183. data/lib/textus/cli/verb/put.rb +0 -30
  184. data/lib/textus/cli/verb/schema_diff.rb +0 -15
  185. data/lib/textus/cli/verb/schema_init.rb +0 -19
  186. data/lib/textus/cli/verb/schema_migrate.rb +0 -19
  187. data/lib/textus/cli/verb/serve.rb +0 -19
  188. data/lib/textus/cli/verb.rb +0 -116
  189. data/lib/textus/cli.rb +0 -138
  190. data/lib/textus/dispatcher.rb +0 -54
  191. data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
  192. data/lib/textus/domain/action.rb +0 -9
  193. data/lib/textus/domain/jobs/registry.rb +0 -37
  194. data/lib/textus/domain/permission.rb +0 -7
  195. data/lib/textus/domain/policy/base_guards.rb +0 -25
  196. data/lib/textus/domain/policy/evaluation.rb +0 -15
  197. data/lib/textus/domain/policy/guard.rb +0 -35
  198. data/lib/textus/domain/policy/guard_factory.rb +0 -40
  199. data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
  200. data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
  201. data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
  202. data/lib/textus/domain/policy/predicates/registry.rb +0 -39
  203. data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
  204. data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
  205. data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
  206. data/lib/textus/hooks/builtin.rb +0 -70
  207. data/lib/textus/hooks/loader.rb +0 -54
  208. data/lib/textus/hooks/rpc_registry.rb +0 -43
  209. data/lib/textus/jobs/handlers.rb +0 -62
  210. data/lib/textus/jobs/scheduler.rb +0 -36
  211. data/lib/textus/jobs/seeder.rb +0 -57
  212. data/lib/textus/maintenance/drain.rb +0 -42
  213. data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
  214. data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
  215. data/lib/textus/maintenance/rule_lint.rb +0 -66
  216. data/lib/textus/maintenance/serve.rb +0 -30
  217. data/lib/textus/maintenance/worker.rb +0 -74
  218. data/lib/textus/maintenance/zone_mv.rb +0 -64
  219. data/lib/textus/maintenance.rb +0 -15
  220. data/lib/textus/mcp/catalog.rb +0 -70
  221. data/lib/textus/mcp/errors.rb +0 -32
  222. data/lib/textus/mcp/server.rb +0 -138
  223. data/lib/textus/mcp/session.rb +0 -7
  224. data/lib/textus/mcp/tool_schemas.rb +0 -15
  225. data/lib/textus/mcp.rb +0 -6
  226. data/lib/textus/mustache.rb +0 -117
  227. data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
  228. data/lib/textus/produce/events.rb +0 -36
  229. data/lib/textus/read/audit.rb +0 -130
  230. data/lib/textus/read/boot.rb +0 -26
  231. data/lib/textus/read/capabilities.rb +0 -70
  232. data/lib/textus/read/deps.rb +0 -38
  233. data/lib/textus/read/doctor.rb +0 -27
  234. data/lib/textus/read/freshness.rb +0 -152
  235. data/lib/textus/read/get.rb +0 -73
  236. data/lib/textus/read/jobs.rb +0 -31
  237. data/lib/textus/read/list.rb +0 -24
  238. data/lib/textus/read/published.rb +0 -22
  239. data/lib/textus/read/pulse.rb +0 -98
  240. data/lib/textus/read/rdeps.rb +0 -39
  241. data/lib/textus/read/rule_explain.rb +0 -96
  242. data/lib/textus/read/rule_list.rb +0 -54
  243. data/lib/textus/read/schema_envelope.rb +0 -25
  244. data/lib/textus/read/uid.rb +0 -29
  245. data/lib/textus/read/validate_all.rb +0 -36
  246. data/lib/textus/read/where.rb +0 -24
  247. data/lib/textus/role_scope.rb +0 -78
  248. data/lib/textus/write/accept.rb +0 -58
  249. data/lib/textus/write/enqueue.rb +0 -50
  250. data/lib/textus/write/key_delete.rb +0 -65
  251. data/lib/textus/write/key_mv.rb +0 -141
  252. data/lib/textus/write/propose.rb +0 -54
  253. data/lib/textus/write/put.rb +0 -74
  254. data/lib/textus/write/reject.rb +0 -68
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2095b69b4e135e71a1ae786759d299c8df4e78a0164a142411d0df72ce63aa4e
4
- data.tar.gz: fb01669c8458fbfa0db50cff6314d30c9033abc02632998e431b2d11cb7e893c
3
+ metadata.gz: c7df129e69143765baa5302608a7813c0666d3c9d91266e0a47a4ece61a4f9f6
4
+ data.tar.gz: 2362c085078165a6da46e55dac29b75346d0f14527de514898f1f5136b094b21
5
5
  SHA512:
6
- metadata.gz: 728c7e305b878cb68bf10af0bd93d9ca93b8b1a2be07a7ec7333de73f127415d4aeae112863bc9fa4d1cec67f51a0da25e2ad984b7b4283f4724c878241ff0c0
7
- data.tar.gz: 7019ce011bf7297d430c2de9f8da591bc951d863e6834ba40c928eeca1bb0082626e2a0814c8bb6412288fb60659faedc5807e77258fbbcf3b3099672ddb19f4
6
+ metadata.gz: a120d5d78f679fb9019817bf8491e00abe1145416c95faf6f80d5f62d078dbe8a054d15e5ec080ff5544eb3b2e02466130617db2d7d4fbf32dab05d3d8295fe1
7
+ data.tar.gz: a4dd565844225f88ae27683e37b6f508a3e64a6828a4b09ee472b815576b57eb9fd1ccf86d4cea91ecd92dde7ca59f0288ac01abf786afb9cce4833c28bb088c
data/CHANGELOG.md CHANGED
@@ -9,6 +9,31 @@ The **gem version** (`0.x.y`) is distinct from the **protocol version**
9
9
  bump is a breaking change that requires a store migration; the gem version
10
10
  tracks both additive improvements and breaking protocol bumps independently.
11
11
 
12
+ ## 0.53.0 — 2026-06-15 — Auth consolidation and code quality
13
+
14
+ ### Added
15
+
16
+ - **`bin/dev`**: local script for syncing skills from GitHub during development.
17
+
18
+ ### Changed
19
+
20
+ - **Gate dispatch consolidated** (PR #215): removed redundant `container:` param from `Gate#dispatch` (always `@container`); removed dead `Textus::Dispatch` module; removed double auth call in `Accept`; removed dead `else` branch and `role_filter` from `RoleScope` verb dispatch.
21
+ - **`Background::Planner::Planner` renamed** to `Planner::Plan` to resolve the double-name antipattern.
22
+ - **`Writer#put` decomposed** (38→12 lines): extracted into 8 single-purpose sub-methods (resolve, uid, serialize, schema-validate, etag-check, persist, envelope, audit).
23
+ - **`Gate::Auth#check!` / `check_action!` deduplicated**: shared predicate-evaluation logic extracted into `evaluate_predicates`.
24
+ - **`Init.run` split** (30→10 lines): 8 focused class methods (`check_target!`, `create_directories`, `write_steps_readme`, `write_manifest`, `scaffold_agent`, `setup_state_dirs`, `write_gitignore`, `build_result`).
25
+ - **`auth`/`writer`/`reader` helpers pulled into `WriteVerb`**: removed 4-file duplication from `Put`, `KeyMv`, `KeyDelete`, `Reject`.
26
+ - **`Action::Base#args` auto-generated** from `initialize` params: ~115 lines of boilerplate removed across 25 action classes.
27
+ - **`AuditLog#verify_integrity`**: per-line checking extracted into `check_line_integrity` + `iterate_with_prev_seq`.
28
+ - **`Error#initialize`**: 5-param constructor replaced with `ErrorInfo` parameter object (backward-compatible).
29
+ - **`EventBus#invoke`**: replaced `Thread.new`/`kill` with `Timeout.timeout`.
30
+ - **Hand-rolled Mustache replaced** with `mustache` gem: 117 lines of template engine deleted.
31
+ - **RuboCop formatting** applied across the codebase.
32
+
33
+ ### Changed (breaking)
34
+
35
+ - **The proposal payload key `frontmatter` → `_meta`** ([ADR 0113](docs/architecture/decisions/0113-proposal-block-meta-key.md)). A proposal entry's proposed metadata moved from the top-level `frontmatter:` key to `_meta:`, so a proposal now carries the exact `{ _meta, body }` envelope shape `accept` replays — retiring the only place the on-disk word "frontmatter" appeared as a runtime data key. `accept` reads `env.meta["_meta"]` and the `schema_valid` special case shrinks to a dig-with-fallback. No shim (house style): a proposal authored with the old key accepts with empty metadata and fails schema validation loudly. **Migration:** re-author any in-flight proposal with `_meta:` instead of `frontmatter:` — proposals are transient `queue` entries, not durable canon.
36
+
12
37
  ## 0.52.0 — 2026-06-09 — The authority model is a produced reference doc (ADR 0112)
13
38
 
14
39
  The "who may write what" tables stop being hand-copied across the canon docs and become a fourth generated reference doc, projected from the source of truth on every `drain`.
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- <!-- Generated from .textus/zones/knowledge/readme.md — edit there, then run `textus drain`. Do not hand-edit README.md (it is clobbered on drain and flagged by doctor). ADR 0103. -->
1
+ <!-- Generated from .textus/data/knowledge/readme.md — edit there, then run `textus drain`. Do not hand-edit README.md (it is clobbered on drain and flagged by doctor). ADR 0103. -->
2
2
  <p align="center">
3
3
  <picture>
4
4
  <source media="(prefers-color-scheme: dark)" srcset="assets/branding/wordmark-dark.png">
@@ -73,7 +73,7 @@ TRANSIENT │ artifacts.feeds.* │ proposals (queue) │
73
73
 
74
74
  *(The `machine` lane's other half, `artifacts.derived.*`, isn't on this grid — it's a computed **output** projected from the lanes, not an input climbing toward trust.)*
75
75
 
76
- Without coordination, they overwrite each other and nothing remembers why. textus gives each actor a **lane** — called a **zone** in the manifest and CLI, the term used everywhere technical from here on — routes everything they can't write directly through a **proposals queue**, and writes every successful change to an **append-only audit log**. The lanes are enforced at the protocol level, not by convention.
76
+ Without coordination, they overwrite each other and nothing remembers why. textus gives each actor a **lane** — enforced at the protocol level, not by convention — routes everything they can't write directly through a **proposals queue**, and writes every successful change to an **append-only audit log**.
77
77
 
78
78
  ```
79
79
  knowledge/ author only — who you are, what you decide, how you sound (knowledge.identity.* for identity facts)
@@ -90,7 +90,7 @@ That's the load-bearing claim: **coordination is a protocol invariant, not a lib
90
90
 
91
91
  ```sh
92
92
  gem install textus
93
- textus init # creates .textus/ with zones + schemas
93
+ textus init # creates .textus/ with lanes + schemas
94
94
 
95
95
  # an agent proposes a change — it targets a knowledge entry, but lands in proposals/
96
96
  textus propose notes.oncall --as=agent --stdin <<'JSON'
@@ -109,12 +109,12 @@ Try the gate the other way (`textus put knowledge.notes.X --as=agent`) and you g
109
109
 
110
110
  ## Try it
111
111
 
112
- - **Worked end-to-end store** — the role gate (propose → accept), drain/publish (`CLAUDE.md` / `AGENTS.md` generated from knowledge entries), schemas, templates, and a hook: [`examples/project/`](examples/project/)
112
+ - **Worked end-to-end store** — the role gate (propose → accept), drain/publish (`CLAUDE.md` / `AGENTS.md` generated from knowledge entries), schemas, templates, and a hook: [`.textus/`](.textus/)
113
113
  - **Wire textus into Claude Code via MCP** — 4 steps, ~5 minutes: [`docs/how-to/agents-mcp.md`](docs/how-to/agents-mcp.md)
114
114
 
115
115
  ## Protocol, not just a gem
116
116
 
117
- This Ruby gem is the reference implementation of **`textus/3`** — a wire format and storage convention any language can speak. The protocol owns the envelope shape, the role/zone gate, the audit log format, and the key grammar. The gem version (semver, see badge) and the protocol version (`textus/3`) move independently; envelopes carry the `protocol` field so consumers can pin to the contract, not the implementation.
117
+ This Ruby gem is the reference implementation of **`textus/3`** — a wire format and storage convention any language can speak. The protocol owns the envelope shape, the role/lane gate, the audit log format, and the key grammar. The gem version (semver, see badge) and the protocol version (`textus/3`) move independently; envelopes carry the `protocol` field so consumers can pin to the contract, not the implementation.
118
118
 
119
119
  - Specification: [`SPEC.md`](SPEC.md)
120
120
  - Architecture: [`docs/architecture/README.md`](docs/architecture/README.md)
@@ -137,7 +137,7 @@ bundle exec exe/textus --help
137
137
 
138
138
  ## What `textus init` gives you
139
139
 
140
- You get `.textus/` with all four zone directories, baseline schemas, a starter manifest, and a gitignored `.run/` for disposable runtime state (the audit log, per-role cursors, produce locks). Roles declare capabilities; each zone declares a `kind:`, and write authority is derived from the role's capabilities crossed with the zone's kind:
140
+ You get `.textus/` with all four lane directories under `data/`, baseline schemas, a starter manifest, and a gitignored `.run/` for disposable runtime state (the audit log, per-role cursors, produce locks). Roles declare capabilities; each lane declares a `kind:`, and write authority is derived from the role's capabilities crossed with the lane's kind:
141
141
 
142
142
  ```yaml
143
143
  roles:
@@ -145,7 +145,7 @@ roles:
145
145
  - { name: agent, can: [propose, keep] }
146
146
  - { name: automation, can: [converge] }
147
147
 
148
- zones:
148
+ lanes:
149
149
  - { name: knowledge, kind: canon } # author — canonical truth
150
150
  - { name: notebook, kind: workspace } # keep — agent's own durable lane
151
151
  - { name: proposals, kind: queue } # propose — proposals awaiting accept
@@ -154,12 +154,12 @@ zones:
154
154
 
155
155
  ```
156
156
  .textus/
157
- manifest.yaml # role capabilities + zone kinds + key-to-path mapping
157
+ manifest.yaml # role capabilities + lane kinds + key-to-path mapping
158
158
  schemas/ # YAML field shapes per entry family
159
159
  templates/ # mustache templates for derived entries
160
- hooks/ # one .rb per hook
160
+ steps/ # step subclasses: fetch/, transform/, validate/, observe/
161
161
  .gitignore # generated — ignores .run/ and any tracked:false entries
162
- zones/ # one dir per zone; kinds + capabilities are in the manifest above
162
+ data/ # one dir per lane; kinds + capabilities are in the manifest above
163
163
  knowledge/ # e.g. identity (knowledge.identity.*), voice, decisions, notes
164
164
  notebook/
165
165
  proposals/
@@ -171,13 +171,13 @@ zones:
171
171
  sentinels/ # publish bookkeeping (target sha) — regenerated on drain (ADR 0070)
172
172
  ```
173
173
 
174
- Manifest `path:` fields are relative to `.textus/zones/`. So `knowledge.notes.org.jane` lives at `.textus/zones/knowledge/notes/org/jane.md`.
174
+ Manifest `path:` fields are relative to `.textus/data/`. So `knowledge.notes.org.jane` lives at `.textus/data/knowledge/notes/org/jane.md`.
175
175
 
176
176
  Read and write:
177
177
 
178
178
  ```sh
179
179
  textus get knowledge.notes.org.jane
180
- textus list --zone=knowledge
180
+ textus list --lane=knowledge
181
181
  printf '%s' '{"_meta":{"name":"bob","org":"acme"},"body":"hi\n"}' \
182
182
  | textus put knowledge.notes.bob --as=human --stdin
183
183
  textus drain --as=automation # re-pull stale inputs + recompute derived outputs
@@ -187,80 +187,90 @@ textus audit --limit=20 # query the audit log
187
187
 
188
188
  (All verbs return JSON envelopes; `--output=json` is the default and the only format in v1.)
189
189
 
190
- For a worked store — knowledge entries, a staged proposal, schemas, a template, and a `drain` that publishes `CLAUDE.md` / `AGENTS.md` — see [`examples/project/`](examples/project/).
190
+ For a worked store — knowledge entries, a staged proposal, schemas, a template, and a `drain` that publishes `CLAUDE.md` / `AGENTS.md` — see [`.textus/`](.textus/).
191
191
 
192
192
  ## What's shipped
193
193
 
194
194
  - **Per-entry formats & publish.** `format: markdown|json|yaml|text` per entry; a typed `publish:` block (`to:` for file fan-out, `tree:` for a whole-subtree mirror) byte-copies derived files to their consumer paths. ([SPEC §5.2–5.3](SPEC.md))
195
195
  - **Stable identity.** Auto-minted `uid:` survives writes and `textus key mv`; reorganising never breaks references.
196
- - **Capability × zone-kind gate.** Writes carry `--as=<role>`; a role may write a zone iff it holds the capability the zone's `kind:` requires (`canon`→`author`, `workspace`→`keep`, `machine`→`converge`, `queue`→`propose`). The wrong role gets `write_forbidden` naming the capability needed and the roles that hold it. ([SPEC §5](SPEC.md))
196
+ - **Capability × lane-kind gate.** Writes carry `--as=<role>`; a role may write a lane iff it holds the capability the lane's `kind:` requires (`canon`→`author`, `workspace`→`keep`, `machine`→`converge`, `queue`→`propose`). The wrong role gets `write_forbidden` naming the capability needed and the roles that hold it. ([SPEC §5](SPEC.md))
197
197
  - **Agent loop.** `textus boot` orients a fresh session; `textus pulse --since=N` is the per-turn heartbeat (changed entries, stale keys, pending proposals). ([docs/how-to/agents-mcp.md](docs/how-to/agents-mcp.md))
198
- - **`textus doctor`.** Health checks across schemas, hooks, keys, sentinels, and the audit log.
198
+ - **`textus doctor`.** Health checks across schemas, step registrations, keys, sentinels, and the audit log.
199
199
 
200
- ## CLI and zones
200
+ ## CLI and lanes
201
201
 
202
202
  Every command operates on one store, located in this order: `--root <path>` flag → **`TEXTUS_ROOT`** env → walk up from the working directory for a `.textus/` ([SPEC §3.1](SPEC.md)). Write verbs require `--as=<role>`, resolved as: `--as` flag → **`TEXTUS_ROLE`** env → `.textus/role` file → default `human` ([SPEC §5.1](SPEC.md)). Default roles: `human`, `agent`, `automation` (rename or add your own in the manifest's `roles:` block). All verbs accept `--output=json` and return the envelope defined in [SPEC §8](SPEC.md).
203
203
 
204
204
  - Full verb table — read, write, health, scaffolding — is in [SPEC §9](SPEC.md).
205
- - Zone semantics and the capability × zone-kind mapping live in [SPEC §5](SPEC.md), with the reference in [`docs/reference/zones.md`](docs/reference/zones.md).
205
+ - Lane semantics and the capability × lane-kind mapping live in [SPEC §5](SPEC.md), with the reference in [`docs/reference/zones.md`](docs/reference/zones.md).
206
206
 
207
- `textus boot` prints the same information for the current store: zones, entry families with schemas, registered hooks, write flows, and the verb catalog. Run it inside a store and you get the live picture; reach for the SPEC when you want the contract.
207
+ `textus boot` prints the same information for the current store: lanes, entry families with schemas, registered steps, write flows, and the verb catalog. Run it inside a store and you get the live picture; reach for the SPEC when you want the contract.
208
208
 
209
209
  ## Produce and publish
210
210
 
211
211
  Produced entries (`kind: produced`) declare how they're acquired in one `source:` block (ADR 0093/0094); `drain` materialises them:
212
212
 
213
- - **`source: { from: project, select: [...], pluck:, sort_by:, limit:, transform: name }`** — a *projection*: textus computes the entry's data from other entries, then renders it through a template under `.textus/templates/` (markdown/text) or a templateless path that lets a transform hook shape the output directly (json/yaml). Projections cap at 1000 rows; the vendored Mustache subset caps at depth 8. No partials, no lambdas, no HTML escaping.
214
- - **`source: { from: handler, handler: name, ttl: 1h, config: {...} }`** — *intake*: an RPC handler pulls external bytes on a `ttl` cadence; `drain` re-pulls when the entry goes stale.
215
- - **`source: { from: command, sources: [...] }`** — *externally generated*: an out-of-band command writes the file; textus tracks the declared `sources` for staleness.
213
+ - **`source: { from: derive, select: [...], pluck:, sort_by:, limit:, transform: name }`** — a *derived* entry: textus computes its data from other entries, then renders it through a template under `.textus/templates/` (markdown/text) or a templateless path that lets a transform hook shape the output directly (json/yaml). Projections cap at 1000 rows; the vendored Mustache subset caps at depth 8. No partials, no lambdas, no HTML escaping.
214
+ - **`source: { from: fetch, handler: name, ttl: 1h, config: {...} }`** — *intake*: a Step::Fetch handler pulls external bytes on a `ttl` cadence; `drain` re-pulls when the entry goes stale.
215
+ - **`source: { from: external, sources: [...] }`** — *externally managed*: an out-of-band command writes the file; textus tracks the declared `sources` for staleness.
216
216
 
217
217
  Publishing is one typed `publish:` block (ADR 0052). `publish: { to: [path, ...] }` byte-copies a single produced file to one or more targets. `publish: { tree: "dir" }` on a nested entry mirrors its whole stored subtree to one target directory, preserving layout (path-driven — no keys or template variables). Sentinels for every published file live under `.textus/.run/sentinels/` (git-ignored runtime state, regenerated on drain — ADR 0070). See SPEC §5.2, §5.3, §5.12.
218
218
 
219
219
  ## Extension points
220
220
 
221
- textus exposes a hook DSL. Drop `.rb` files into `.textus/hooks/` (subdirectories are fine; files load alphabetically by full path). There are two kinds:
221
+ textus extends through **steps** subclass the right base, place the file in `.textus/steps/<kind>/`, and `drain` discovers it. Three kinds:
222
222
 
223
- **RPC hooks** one handler, the framework uses what you return:
223
+ **`Step::Fetch`**acquires bytes for an intake entry (`from: fetch`). One class per named handler; `drain` invokes it when the entry is stale.
224
224
 
225
- | Event | Fires when | You return |
226
- |---|---|---|
227
- | `:resolve_handler` | an intake needs bytes | `{_meta:, body:}` |
228
- | `:transform_rows` | a projection builds | the reshaped rows |
229
- | `:validate` | `textus doctor` runs | doctor issues (or none) |
230
-
231
- **Pub-sub hooks** 0..N handlers, fire-and-react (no return value):
225
+ ```ruby
226
+ # .textus/steps/fetch/local_file.rb
227
+ module Textus
228
+ module Step
229
+ class LocalFileFetch < Fetch
230
+ def call(config:, args:, **)
231
+ path = config["path"] or raise "local-file requires source.config.path"
232
+ { "_meta" => { "last_fetched_at" => Time.now.utc.iso8601 },
233
+ "body" => File.read(File.expand_path(path)) }
234
+ end
235
+ end
236
+ end
237
+ end
238
+ ```
232
239
 
233
- | Event(s) | Fires when |
234
- |---|---|
235
- | `:entry_written` · `:entry_deleted` · `:entry_renamed` | a write lands |
236
- | `:entry_fetched` | an intake-driven write lands |
237
- | `:entry_produced` | a produced entry materializes |
238
- | `:entry_published` | a produced file is copied to its target |
239
- | `:proposal_accepted` · `:proposal_rejected` | a proposal is resolved |
240
- | `:entry_fetch_started` · `:entry_fetch_failed` · `:produce_failed` | produce lifecycle |
241
- | `:store_loaded` · `:session_opened` | the store loads · a role connects |
240
+ **`Step::Transform`** reshapes projected rows for a `from: derive` entry.
242
241
 
243
242
  ```ruby
244
- # Inside .textus/hooks/local_file.rb
245
- Textus.hook do |reg|
246
- reg.on(:resolve_handler, :local_file) do |config:, args:, **|
247
- path = config["path"] or raise "local-file requires source.config.path"
248
- {
249
- _meta: { "last_fetched_at" => Time.now.utc.iso8601, "source_path" => path },
250
- body: File.read(File.expand_path(path)),
251
- }
243
+ # .textus/steps/transform/rank_by_recency.rb
244
+ module Textus
245
+ module Step
246
+ class RankByRecencyTransform < Transform
247
+ def call(rows:, config:, **)
248
+ rows.sort_by { |r| r["updated_at"].to_s }.reverse
249
+ end
250
+ end
252
251
  end
253
252
  end
254
253
  ```
255
254
 
255
+ **`Step::Observe`** — reacts to lifecycle events (fire-and-forget, 0..N per event):
256
+
256
257
  ```ruby
257
- Textus.hook do |reg|
258
- reg.on(:transform_rows, :rank_by_recency) do |rows:, **|
259
- rows.sort_by { |r| r["updated_at"].to_s }.reverse
258
+ # .textus/steps/observe/log_writes.rb
259
+ module Textus
260
+ module Step
261
+ class LogWritesObserve < Observe
262
+ on :entry_written
263
+
264
+ def call(key:, envelope:, **)
265
+ $textus_event_log << [key, envelope.etag]
266
+ end
267
+ end
260
268
  end
261
269
  end
262
270
  ```
263
271
 
272
+ Observable events: `:entry_written`, `:entry_deleted`, `:entry_fetched`, `:entry_renamed`, `:entry_produced`, `:entry_published`, `:produce_failed`, `:proposal_accepted`, `:proposal_rejected`, `:store_loaded`, `:session_opened`, `:entry_fetch_started`, `:entry_fetch_failed`.
273
+
264
274
  Stale intake entries are re-pulled by `drain`, not by reads — `get` is a pure
265
275
  read that annotates the returned envelope with a freshness verdict (ADR 0089).
266
276
  `drain` re-pulls anything past its `source.ttl` and recomputes derived outputs:
@@ -268,18 +278,16 @@ read that annotates the returned envelope with a freshness verdict (ADR 0089).
268
278
  ```sh
269
279
  textus drain --as=automation # re-pull every stale intake + recompute derived
270
280
  textus drain artifacts.feeds --as=automation # scope to one prefix
271
- textus get artifacts.feeds.calendar.events # a pure read; carries a freshness verdict
281
+ textus get artifacts.feeds.calendar.events # a pure read; carries a freshness verdict
272
282
  ```
273
283
 
274
- See SPEC.md §5.10 for the full hook contract.
275
-
276
284
  Schemas (`.textus/schemas/<name>.yaml`) declare field shapes, per-field `maintained_by:` ownership, and an `evolution:` block (`added_in`, `deprecated_at`, `migrate_from`). Full contract in SPEC §5.8.
277
285
 
278
286
  See [`docs/how-to/agents-mcp.md`](docs/how-to/agents-mcp.md) for the agent boot → pulse loop.
279
287
 
280
288
  ## Examples
281
289
 
282
- [`examples/project/`](examples/project/) — textus as a project's own context store (a fictional Rails service, `ledger`). Human-authored `knowledge/` (project facts, runbooks), a staged ADR in `proposals/` showing the agent-propose / human-accept loop, schemas validating each family, a mustache template plus a `:transform_rows` hook, and a `drain` that publishes the `artifacts.derived.orientation` projection to `CLAUDE.md` and `AGENTS.md`. Includes a copy-paste adoption recipe for your own repo.
290
+ [`.textus/`](.textus/) — textus as a project's own context store (a fictional Rails service, `ledger`). Human-authored `knowledge/` (project facts, runbooks), a staged ADR in `proposals/` showing the agent-propose / human-accept loop, schemas validating each family, a mustache template plus a `Step::Transform` step, and a `drain` that publishes the `artifacts.derived.orientation` projection to `CLAUDE.md` and `AGENTS.md`. Includes a copy-paste adoption recipe for your own repo.
283
291
 
284
292
  ## Tests
285
293