textus 0.55.1 → 0.56.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 (329) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -2768
  3. data/README.md +17 -18
  4. data/lib/textus/artifact.rb +8 -0
  5. data/lib/textus/errors.rb +2 -39
  6. data/lib/textus/human.rb +7 -0
  7. data/lib/textus/infra/adapters/concurrency_adapter.rb +15 -0
  8. data/lib/textus/infra/clock.rb +7 -0
  9. data/lib/textus/infra/database.rb +127 -0
  10. data/lib/textus/infra/file_system.rb +54 -0
  11. data/lib/textus/infra/locks.rb +50 -0
  12. data/lib/textus/infra/port/clock.rb +18 -0
  13. data/lib/textus/infra/port/publisher.rb +71 -0
  14. data/lib/textus/infra/port/sentinel.rb +25 -0
  15. data/lib/textus/infra/port/sentinel_store.rb +93 -0
  16. data/lib/textus/infra/store/audit_log.rb +42 -0
  17. data/lib/textus/infra/store/base.rb +25 -0
  18. data/lib/textus/infra/store/cursor_store.rb +27 -0
  19. data/lib/textus/infra/store/entry_index.rb +31 -0
  20. data/lib/textus/infra/store/envelope/meta.rb +63 -0
  21. data/lib/textus/infra/store/file_store.rb +23 -0
  22. data/lib/textus/infra/store/index/builder.rb +28 -0
  23. data/lib/textus/infra/store/index/lookup.rb +23 -0
  24. data/lib/textus/infra/store/link_store.rb +43 -0
  25. data/lib/textus/infra/store/workflow_queue.rb +54 -0
  26. data/lib/textus/infra/store.rb +29 -0
  27. data/lib/textus/ingest.rb +8 -0
  28. data/lib/textus/lanes/artifact/cache.rb +35 -0
  29. data/lib/textus/lanes/artifact/handlers.rb +29 -0
  30. data/lib/textus/lanes/artifact/lifecycle.rb +23 -0
  31. data/lib/textus/lanes/artifact/verbs.rb +18 -0
  32. data/lib/textus/lanes/ingest/dedup.rb +17 -0
  33. data/lib/textus/lanes/ingest/entry_types/asset.rb +45 -0
  34. data/lib/textus/lanes/ingest/entry_types/link.rb +16 -0
  35. data/lib/textus/lanes/ingest/entry_types/text.rb +17 -0
  36. data/lib/textus/lanes/ingest/entry_types.rb +25 -0
  37. data/lib/textus/lanes/ingest/handlers.rb +54 -0
  38. data/lib/textus/lanes/ingest/index_rebuilder.rb +13 -0
  39. data/lib/textus/lanes/ingest/key_builder.rb +33 -0
  40. data/lib/textus/lanes/ingest/resolver.rb +21 -0
  41. data/lib/textus/lanes/knowledge/freeform.rb +15 -0
  42. data/lib/textus/lanes/knowledge/handlers.rb +53 -0
  43. data/lib/textus/lanes/knowledge/runner.rb +45 -0
  44. data/lib/textus/lanes/knowledge/verbs.rb +16 -0
  45. data/lib/textus/lanes/scratchpad/handlers.rb +164 -0
  46. data/lib/textus/lanes/scratchpad/verbs.rb +73 -0
  47. data/lib/textus/{gate → protocol}/binder.rb +2 -10
  48. data/lib/textus/protocol/boot/catalog.rb +34 -0
  49. data/lib/textus/protocol/boot/context.rb +124 -0
  50. data/lib/textus/protocol/boot/protocol.rb +33 -0
  51. data/lib/textus/protocol/boot.rb +42 -0
  52. data/lib/textus/protocol/builder/domain.rb +17 -0
  53. data/lib/textus/protocol/builder/infrastructure.rb +17 -0
  54. data/lib/textus/protocol/builder/runtime.rb +46 -0
  55. data/lib/textus/protocol/builder.rb +11 -0
  56. data/lib/textus/protocol/container.rb +22 -0
  57. data/lib/textus/protocol/diff.rb +170 -0
  58. data/lib/textus/protocol/entry_constraint.rb +54 -0
  59. data/lib/textus/protocol/envelope.rb +70 -0
  60. data/lib/textus/protocol/format/base.rb +80 -0
  61. data/lib/textus/protocol/format/bash.rb +10 -0
  62. data/lib/textus/protocol/format/javascript.rb +10 -0
  63. data/lib/textus/protocol/format/json.rb +82 -0
  64. data/lib/textus/protocol/format/markdown.rb +53 -0
  65. data/lib/textus/protocol/format/python.rb +10 -0
  66. data/lib/textus/protocol/format/ruby.rb +10 -0
  67. data/lib/textus/protocol/format/script.rb +55 -0
  68. data/lib/textus/protocol/format/text.rb +48 -0
  69. data/lib/textus/protocol/format/yaml.rb +105 -0
  70. data/lib/textus/protocol/format.rb +73 -0
  71. data/lib/textus/protocol/freshness/ttl_evaluator.rb +61 -0
  72. data/lib/textus/protocol/gate.rb +61 -0
  73. data/lib/textus/protocol/handlers/command_context.rb +19 -0
  74. data/lib/textus/protocol/handlers/concern.rb +15 -0
  75. data/lib/textus/protocol/handlers/ops/audit_filter.rb +37 -0
  76. data/lib/textus/protocol/handlers/ops/bulk_planner.rb +38 -0
  77. data/lib/textus/protocol/handlers/ops/data_mv.rb +49 -0
  78. data/lib/textus/protocol/handlers/ops/git_blame.rb +65 -0
  79. data/lib/textus/protocol/handlers/ops/pulse_coordinator.rb +37 -0
  80. data/lib/textus/protocol/handlers/ops/source_expander.rb +41 -0
  81. data/lib/textus/protocol/handlers/query_context.rb +41 -0
  82. data/lib/textus/protocol/handlers/read.rb +67 -0
  83. data/lib/textus/protocol/handlers/system.rb +93 -0
  84. data/lib/textus/protocol/handlers/write.rb +57 -0
  85. data/lib/textus/protocol/init.rb +143 -0
  86. data/lib/textus/protocol/key/grammar.rb +35 -0
  87. data/lib/textus/protocol/key/matching.rb +23 -0
  88. data/lib/textus/protocol/key/path.rb +23 -0
  89. data/lib/textus/protocol/key.rb +25 -0
  90. data/lib/textus/protocol/lane.rb +12 -0
  91. data/lib/textus/{store/geometry.rb → protocol/layout.rb} +32 -22
  92. data/lib/textus/protocol/links/resolver.rb +33 -0
  93. data/lib/textus/protocol/links/uri_rewriter.rb +30 -0
  94. data/lib/textus/protocol/links.rb +10 -0
  95. data/lib/textus/protocol/manifest/data.rb +90 -0
  96. data/lib/textus/protocol/manifest/entry/base.rb +95 -0
  97. data/lib/textus/protocol/manifest/entry/leaf.rb +19 -0
  98. data/lib/textus/protocol/manifest/entry/nested.rb +19 -0
  99. data/lib/textus/protocol/manifest/entry/parser.rb +71 -0
  100. data/lib/textus/protocol/manifest/entry/publish.rb +209 -0
  101. data/lib/textus/protocol/manifest/entry.rb +10 -0
  102. data/lib/textus/protocol/manifest/policy/publish_target.rb +36 -0
  103. data/lib/textus/protocol/manifest/policy.rb +69 -0
  104. data/lib/textus/protocol/manifest/resolver.rb +156 -0
  105. data/lib/textus/protocol/manifest/schema/contract.rb +53 -0
  106. data/lib/textus/protocol/manifest/schema/validator.rb +52 -0
  107. data/lib/textus/protocol/manifest/schema.rb +40 -0
  108. data/lib/textus/protocol/manifest.rb +62 -0
  109. data/lib/textus/protocol/produce/render/context.rb +43 -0
  110. data/lib/textus/protocol/produce/render.rb +44 -0
  111. data/lib/textus/protocol/schema/registry.rb +44 -0
  112. data/lib/textus/protocol/schema/tools.rb +104 -0
  113. data/lib/textus/protocol/schema.rb +117 -0
  114. data/lib/textus/protocol/session/bridge.rb +11 -0
  115. data/lib/textus/protocol/session/context.rb +5 -0
  116. data/lib/textus/protocol/session.rb +87 -0
  117. data/lib/textus/protocol/store_engine/delete.rb +37 -0
  118. data/lib/textus/protocol/store_engine/move.rb +109 -0
  119. data/lib/textus/protocol/store_engine/pipeline.rb +61 -0
  120. data/lib/textus/protocol/store_engine/put.rb +104 -0
  121. data/lib/textus/protocol/store_engine.rb +86 -0
  122. data/lib/textus/protocol/v4.rb +25 -0
  123. data/lib/textus/protocol/verb_registry/arg_spec.rb +19 -0
  124. data/lib/textus/protocol/verb_registry/core_verbs.rb +164 -0
  125. data/lib/textus/protocol/verb_registry/verb_spec.rb +42 -0
  126. data/lib/textus/protocol/verb_registry.rb +42 -0
  127. data/lib/textus/surface/cli/group/{rule.rb → web.rb} +2 -2
  128. data/lib/textus/surface/cli/runner.rb +25 -25
  129. data/lib/textus/surface/cli/sources.rb +1 -3
  130. data/lib/textus/surface/cli/verb/get.rb +3 -6
  131. data/lib/textus/surface/cli/verb/init.rb +1 -1
  132. data/lib/textus/surface/cli/verb/put.rb +8 -6
  133. data/lib/textus/surface/cli/verb/web_serve.rb +22 -0
  134. data/lib/textus/surface/cli.rb +25 -6
  135. data/lib/textus/surface/dispatch.rb +17 -0
  136. data/lib/textus/surface/mcp/adapters/mcp_adapter.rb +28 -0
  137. data/lib/textus/surface/mcp/catalog.rb +26 -62
  138. data/lib/textus/surface/mcp/cursor_manager.rb +22 -0
  139. data/lib/textus/surface/mcp/drift_annotation.rb +19 -0
  140. data/lib/textus/surface/mcp/errors.rb +0 -10
  141. data/lib/textus/surface/mcp/server.rb +46 -46
  142. data/lib/textus/surface/watcher.rb +18 -12
  143. data/lib/textus/surface/web.rb +50 -0
  144. data/lib/textus/value/call.rb +1 -1
  145. data/lib/textus/{core → value}/duration.rb +1 -4
  146. data/lib/textus/value/etag.rb +7 -7
  147. data/lib/textus/value/payload.rb +7 -0
  148. data/lib/textus/version.rb +1 -1
  149. data/lib/textus/workflow/consumer.rb +39 -0
  150. data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
  151. data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
  152. data/lib/textus/workflow/dsl/step_builder.rb +56 -0
  153. data/lib/textus/workflow/dsl.rb +111 -21
  154. data/lib/textus/workflow/event_emitter.rb +21 -0
  155. data/lib/textus/workflow/publisher.rb +59 -0
  156. data/lib/textus/workflow/registry.rb +31 -8
  157. data/lib/textus/workflow/runner.rb +25 -61
  158. data/lib/textus/workflow/scheduler.rb +45 -0
  159. data/lib/textus/workflow/step_executor.rb +78 -0
  160. data/lib/textus/workflow/step_helpers.rb +102 -0
  161. data/lib/textus/workflow.rb +2 -5
  162. data/lib/textus.rb +16 -80
  163. metadata +167 -183
  164. data/SPEC.md +0 -908
  165. data/docs/architecture/README.md +0 -273
  166. data/docs/reference/conventions.md +0 -138
  167. data/lib/textus/action/accept.rb +0 -46
  168. data/lib/textus/action/audit.rb +0 -94
  169. data/lib/textus/action/base.rb +0 -42
  170. data/lib/textus/action/blame.rb +0 -79
  171. data/lib/textus/action/boot.rb +0 -15
  172. data/lib/textus/action/data_mv.rb +0 -58
  173. data/lib/textus/action/deps.rb +0 -19
  174. data/lib/textus/action/doctor.rb +0 -17
  175. data/lib/textus/action/drain.rb +0 -31
  176. data/lib/textus/action/enqueue.rb +0 -37
  177. data/lib/textus/action/get.rb +0 -34
  178. data/lib/textus/action/ingest.rb +0 -199
  179. data/lib/textus/action/jobs.rb +0 -27
  180. data/lib/textus/action/key_delete.rb +0 -26
  181. data/lib/textus/action/key_delete_prefix.rb +0 -35
  182. data/lib/textus/action/key_mv.rb +0 -122
  183. data/lib/textus/action/key_mv_prefix.rb +0 -48
  184. data/lib/textus/action/list.rb +0 -28
  185. data/lib/textus/action/propose.rb +0 -42
  186. data/lib/textus/action/published.rb +0 -22
  187. data/lib/textus/action/pulse.rb +0 -49
  188. data/lib/textus/action/put.rb +0 -38
  189. data/lib/textus/action/rdeps.rb +0 -24
  190. data/lib/textus/action/reject.rb +0 -28
  191. data/lib/textus/action/rule_explain.rb +0 -81
  192. data/lib/textus/action/rule_lint.rb +0 -62
  193. data/lib/textus/action/rule_list.rb +0 -38
  194. data/lib/textus/action/schema_envelope.rb +0 -22
  195. data/lib/textus/action/uid.rb +0 -19
  196. data/lib/textus/action/where.rb +0 -21
  197. data/lib/textus/boot.rb +0 -202
  198. data/lib/textus/contract/arg.rb +0 -10
  199. data/lib/textus/contract/dsl.rb +0 -88
  200. data/lib/textus/contract/spec.rb +0 -25
  201. data/lib/textus/contract.rb +0 -12
  202. data/lib/textus/core/freshness/evaluator.rb +0 -150
  203. data/lib/textus/core/freshness/verdict.rb +0 -34
  204. data/lib/textus/core/freshness.rb +0 -11
  205. data/lib/textus/core/retention/sweep.rb +0 -57
  206. data/lib/textus/core/retention.rb +0 -11
  207. data/lib/textus/core/sentinel.rb +0 -30
  208. data/lib/textus/doctor/check/audit_log.rb +0 -34
  209. data/lib/textus/doctor/check/generator_drift.rb +0 -29
  210. data/lib/textus/doctor/check/illegal_keys.rb +0 -60
  211. data/lib/textus/doctor/check/manifest_files.rb +0 -25
  212. data/lib/textus/doctor/check/notebook_sources.rb +0 -50
  213. data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
  214. data/lib/textus/doctor/check/proposal_targets.rb +0 -45
  215. data/lib/textus/doctor/check/protocol_version.rb +0 -47
  216. data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
  217. data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
  218. data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
  219. data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
  220. data/lib/textus/doctor/check/schema_violations.rb +0 -28
  221. data/lib/textus/doctor/check/schemas.rb +0 -26
  222. data/lib/textus/doctor/check/sentinels.rb +0 -60
  223. data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
  224. data/lib/textus/doctor/check/templates.rb +0 -29
  225. data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
  226. data/lib/textus/doctor/check.rb +0 -41
  227. data/lib/textus/doctor/validator.rb +0 -93
  228. data/lib/textus/doctor.rb +0 -54
  229. data/lib/textus/format/base.rb +0 -49
  230. data/lib/textus/format/json.rb +0 -102
  231. data/lib/textus/format/markdown.rb +0 -80
  232. data/lib/textus/format/shared.rb +0 -17
  233. data/lib/textus/format/text.rb +0 -55
  234. data/lib/textus/format/yaml.rb +0 -125
  235. data/lib/textus/format.rb +0 -46
  236. data/lib/textus/gate/auth.rb +0 -212
  237. data/lib/textus/gate.rb +0 -92
  238. data/lib/textus/init.rb +0 -157
  239. data/lib/textus/jobs.rb +0 -9
  240. data/lib/textus/key/distance.rb +0 -55
  241. data/lib/textus/key/grammar.rb +0 -33
  242. data/lib/textus/key/matching.rb +0 -24
  243. data/lib/textus/key/path.rb +0 -28
  244. data/lib/textus/manifest/capabilities.rb +0 -29
  245. data/lib/textus/manifest/data.rb +0 -102
  246. data/lib/textus/manifest/entry/base.rb +0 -105
  247. data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
  248. data/lib/textus/manifest/entry/leaf.rb +0 -17
  249. data/lib/textus/manifest/entry/nested.rb +0 -37
  250. data/lib/textus/manifest/entry/parser.rb +0 -99
  251. data/lib/textus/manifest/entry/produced.rb +0 -36
  252. data/lib/textus/manifest/entry/publish/mode.rb +0 -45
  253. data/lib/textus/manifest/entry/publish/none.rb +0 -14
  254. data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
  255. data/lib/textus/manifest/entry/publish/template.rb +0 -16
  256. data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
  257. data/lib/textus/manifest/entry/publish/tree.rb +0 -50
  258. data/lib/textus/manifest/entry/publish.rb +0 -45
  259. data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
  260. data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
  261. data/lib/textus/manifest/entry/validators/publish.rb +0 -30
  262. data/lib/textus/manifest/entry/validators.rb +0 -18
  263. data/lib/textus/manifest/entry.rb +0 -8
  264. data/lib/textus/manifest/policy/matcher.rb +0 -51
  265. data/lib/textus/manifest/policy/publish_target.rb +0 -34
  266. data/lib/textus/manifest/policy/react.rb +0 -30
  267. data/lib/textus/manifest/policy/retention.rb +0 -29
  268. data/lib/textus/manifest/policy/source.rb +0 -30
  269. data/lib/textus/manifest/policy.rb +0 -95
  270. data/lib/textus/manifest/resolver.rb +0 -118
  271. data/lib/textus/manifest/rules.rb +0 -86
  272. data/lib/textus/manifest/schema/contract.rb +0 -61
  273. data/lib/textus/manifest/schema/keys.rb +0 -90
  274. data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
  275. data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
  276. data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
  277. data/lib/textus/manifest/schema/semantics.rb +0 -27
  278. data/lib/textus/manifest/schema/validator.rb +0 -48
  279. data/lib/textus/manifest/schema/vocabulary.rb +0 -25
  280. data/lib/textus/manifest/schema.rb +0 -30
  281. data/lib/textus/manifest.rb +0 -65
  282. data/lib/textus/meta.rb +0 -54
  283. data/lib/textus/port/audit_log.rb +0 -268
  284. data/lib/textus/port/build_lock.rb +0 -64
  285. data/lib/textus/port/clock.rb +0 -10
  286. data/lib/textus/port/publisher.rb +0 -60
  287. data/lib/textus/port/sentinel_store.rb +0 -83
  288. data/lib/textus/port/storage/file_stat.rb +0 -19
  289. data/lib/textus/port/storage/file_store.rb +0 -26
  290. data/lib/textus/port/store.rb +0 -93
  291. data/lib/textus/port/watcher_lock.rb +0 -48
  292. data/lib/textus/produce/engine.rb +0 -50
  293. data/lib/textus/produce/render.rb +0 -20
  294. data/lib/textus/schema/tools.rb +0 -115
  295. data/lib/textus/schema.rb +0 -100
  296. data/lib/textus/schemas.rb +0 -54
  297. data/lib/textus/store/compositor.rb +0 -34
  298. data/lib/textus/store/container.rb +0 -43
  299. data/lib/textus/store/cursor.rb +0 -26
  300. data/lib/textus/store/envelope/reader.rb +0 -43
  301. data/lib/textus/store/envelope/writer.rb +0 -195
  302. data/lib/textus/store/index/builder.rb +0 -74
  303. data/lib/textus/store/index/lookup.rb +0 -60
  304. data/lib/textus/store/jobs/base.rb +0 -13
  305. data/lib/textus/store/jobs/index.rb +0 -15
  306. data/lib/textus/store/jobs/materialize.rb +0 -15
  307. data/lib/textus/store/jobs/plan.rb +0 -11
  308. data/lib/textus/store/jobs/planner.rb +0 -104
  309. data/lib/textus/store/jobs/queue.rb +0 -154
  310. data/lib/textus/store/jobs/registry.rb +0 -19
  311. data/lib/textus/store/jobs/retention.rb +0 -50
  312. data/lib/textus/store/jobs/sweep.rb +0 -21
  313. data/lib/textus/store/jobs/worker.rb +0 -64
  314. data/lib/textus/store/session.rb +0 -37
  315. data/lib/textus/store.rb +0 -99
  316. data/lib/textus/surface/cli/verb/doctor.rb +0 -19
  317. data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
  318. data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
  319. data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
  320. data/lib/textus/surface/mcp.rb +0 -8
  321. data/lib/textus/surface/projector.rb +0 -27
  322. data/lib/textus/surface/role_scope.rb +0 -34
  323. data/lib/textus/value/command.rb +0 -16
  324. data/lib/textus/value/envelope.rb +0 -89
  325. data/lib/textus/value/result.rb +0 -26
  326. data/lib/textus/workflow/collector.rb +0 -27
  327. data/lib/textus/workflow/context.rb +0 -5
  328. data/lib/textus/workflow/loader.rb +0 -17
  329. data/lib/textus/workflow/pattern.rb +0 -18
data/CHANGELOG.md CHANGED
@@ -1,2830 +1,237 @@
1
+ <!-- Generated by textus from git log. Do not edit by hand — run `textus drain` to refresh. -->
2
+
1
3
  # Changelog
2
4
 
3
5
  All notable changes to this project are documented in this file.
4
6
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
8
 
7
- The **gem version** (`0.x.y`) is distinct from the **protocol version**
8
- (currently `textus/3`, embedded in every envelope as `protocol`). A protocol
9
- bump is a breaking change that requires a store migration; the gem version
10
- tracks both additive improvements and breaking protocol bumps independently.
11
-
12
- ## 0.55.1 2026-06-22 CI fix: converge_now purges done jobs
13
-
14
- ### Fixed
15
-
16
- - **publish_tree prune spec** — `converge_now` now purges "done" jobs before reseeding. The SQLite-backed job queue (`INSERT OR IGNORE`) was silently skipping re-enqueues for already-done jobs, so the second materialize never ran and the source-removed file was never pruned from the published tree.
17
-
18
- ## 0.55.0 2026-06-22 Architecture Deepening Phase 2
19
-
20
- **ADR-0119**: dry-monad actions, container split, geometry authority.
21
-
22
- ### Added
23
-
24
- - **dry-monad Success/Failure actions** (PR #234): all 30 actions now return `Success(result)` or `Failure(code:, message:)` via `Dry::Monads`. `Value::Result.unwrap` converts Failure → `ActionError`. `be_success`/`be_failure` RSpec matchers replace `spec/unit/` tests.
25
- - **`Container::Infrastructure` / `Container::Coordination`** (PR #234): single `LazyContainer` replaced by two `Data.define` records — Infrastructure owns manifest, file_store, geometry; Coordination owns gate, compositor, workflows, audit_log. Wired at boot via `wire_gate!`.
26
- - **`Geometry` as sole path authority** (PR #234): `root`, `schemas_dir`, `hooks_dir`, `store_db_path` delegated to `Geometry`. Removed `root` from Infrastructure. High-value `File.join` callers converted (data_mv, ingest, base, loader, etag, schema/tools, 6 doctor checks).
27
- - **`bin/db` helper**: sqlite3 wrapper for `.textus/.state/store.db` `bin/db "SELECT * FROM jobs"` or just `bin/db` for interactive shell.
28
- - **Centralized SQLite index** (PR #228, #233): `Store::Index::Builder` manages entries FTS index in `store.db`.
29
-
30
- ### Changed
31
-
32
- - **Gate reads `Action::VERBS` directly** (PR #234): `Gate::VERB_ACTIONS` deleted. Gate dispatches via `Action::VERBS[cmd.verb]`.
33
- - **Composite actions** (PR #232): `Accept`, `Propose`, `Reject` separated as composite verbs with declarative `chain` steps, never instantiating sibling action classes directly.
34
- - **`knowledge.decisions` keyless nested mode**: decision files no longer enumerate in `list`; accessible via `get` with full key.
35
-
36
- ### Fixed
37
-
38
- - **CLI `--args` JSON parsing** (PR #233): `Runner.coerce` now `JSON.parse(raw)` for `Hash`-typed args — fixes `textus enqueue --args='{"key":"..."}'` which previously passed the raw JSON string to `Queue::Job`.
39
- - **adr-log workflow convention drift** (PR #233): `Get.new(key:).call(...)` → `Get.call(key: k, ...)` the old pattern raised silently since actions became class-method-only.
40
- - **RuboCop `Lint/NoReturnInBeginEndBlocks`**: enqueue action restructured with method-level `rescue`.
41
-
42
- ### Removed
43
-
44
- - `Gate::VERB_ACTIONS` constant (replaced by `Action::VERBS`)
45
- - `LazyContainer` (replaced by `Container::Infrastructure`/`Coordination`)
46
- - `spec/unit/` directory + `spec/support/spec_layout.rb` (378 examples removed)
47
- - Orphaned `.textus/.state/jobs.sqlite3` database file
48
-
49
- ## 0.54.2 2026-06-18 ingest dedup, .run .state rename
50
-
51
- ### Added
52
-
53
- - **Content-hash dedup in `Action::Ingest`**: re-ingesting the same URL or file bytes silently creates a supersede chain — new entry with full content, old stripped to tombstone with a `superseded_by` pointer.
54
- - **`Ports::RawIndex`**: lightweight YAML-based O(1) index for raw lane content-hash/URL lookups, stored at `.textus/.state/indexes/raw.yaml`.
55
- - **`content_hash` field**: every new raw entry stores a `sha256:` content hash for fast dedup lookups.
56
- - **Asset file move on re-ingest**: asset files are moved (not copied) to the new date path on supersede.
57
-
58
- ### Changed
59
-
60
- - **`.run` `.state` rename**: the runtime artifact directory is renamed from `.textus/.run/` to `.textus/.state/`. The `state/` subdirectory becomes `cursors/`. Layout path methods follow automatically. Manual migration step: `mv .textus/.run .textus/.state`.
61
-
62
- ## 0.54.1 — 2026-06-17 — MCP SDK, dry ecosystem, ERB templates
63
-
64
- ### Added
65
-
66
- - **`Textus::Types` module**: shared `dry-types` constrained types (`RoleName`, `Cursor`, `FormatName`) used across all dry-struct structs.
67
-
68
- ### Changed
69
-
70
- - **MCP surface → official `mcp` SDK** (PR #223): the hand-rolled JSON-RPC 2.0 stdio loop is replaced by the official `mcp` gem (`~> 0.20`). Protocol version auto-negotiates up to `2025-11-25` (was hardcoded `2024-11-05`). `Catalog.build_tools` generates `MCP::Tool` instances from verb contracts; `Server` manages session lifecycle and delegates dispatch. `routing.rb`, the `MCP::Session` alias, and `ToolSchemas` are deleted.
71
- - **`Session`, `Envelope`, `Container` → `Dry::Struct`** (PR #223): `Data.define` replaced with `Dry::Struct` for typed, loudly-failing construction at object creation time. `Session` enforces `RoleName` and non-negative `Cursor`; `Envelope` enforces known `FormatName`; `Container` has typed port references.
72
- - **Manifest validator → dry-schema + Semantics** (PR #223): the 318-line programmatic validator is split into `contract.rb` (dry-schema structural contract) and `semantics.rb` (cross-field rules, `walk`-based unknown-key detection, ADR migration hints). Public interface `Validator.validate!` unchanged.
73
- - **Mustache → ERB** (PR #223): `mustache` gem removed; all 8 `.mustache` templates converted to `.erb` using `ERB#result_with_hash`. `Produce::Render` now uses `ERB.new(template, trim_mode: "-").result_with_hash(ctx)`.
74
-
75
- ### Removed
76
-
77
- - `mustache` runtime dependency (replaced by Ruby stdlib ERB).
78
- - `MCP::Routing` module, `MCP::Session` alias, `MCP::ToolSchemas` module — superseded by the SDK.
79
-
80
- ## 0.54.0 — 2026-06-16 — Workflow system, raw ingest pipeline, MCP resources
81
-
82
- ### Added
83
-
84
- - **Workflow system** (PR #218): Ruby DSL files in `.textus/workflows/` replace the `step/` extension model. `Workflow::Registry` (loaded from `.textus/workflows/**/*.rb`) is resolved by `Produce::Engine` at produce time; `Workflow::Runner` threads data through each named step and persists. Core components: `Workflow::Context`, `Workflow::Pattern` (glob matching), `Workflow::DSL`, `Workflow::Collector`, `Workflow::Registry`, `Workflow::Loader`, `Workflow::Runner`.
85
- - **`artifacts.index` produced workflow**: pre-computed full-store catalog (`artifacts.index`) served as a produced artifact — enumerates every manifest entry with its key, lane, format, etag, and `generated_at` timestamp.
86
- - **MCP `resources/list` and `resources/read`** (PR #221): machine-lane `produced` entries are now exposed as MCP resources at `textus://<key>` URIs with correct MIME types. Clients discover them via the `resources` capability advertised in the `initialize` handshake.
87
- - **Raw ingest pipeline** (PR #219, [ADR 0114](docs/architecture/decisions/0114-foundation-refactor.md), [ADR 0116](docs/architecture/decisions/0116-raw-lane-and-ingest-verb.md)): new `raw` zone-kind with a `write-once` gate and an `ingest` capability/verb (`Action::Ingest`). Doctor checks and an asset sentinel guard the lane. The `raw` bijection entry maps `raw → ingest`.
88
-
89
- ### Changed
90
-
91
- - **`artifacts.derived.*` → `artifacts.*`**: the `derived` key segment is dropped — all machine-lane entries flatten to `artifacts.<name>` in both the manifest and the on-disk store. Workflows, boot, and all other callers updated.
92
- - **`boot` reads from `artifacts.index`**: inline computation removed from the boot action; the pre-computed catalog drives the response. `--lean` flag removed (PR #220).
93
- - **`pulse` slimmed**: `stale`, `doctor`, and `next_due_at` removed from the pulse envelope; `index_etag` added so clients can cheaply detect catalog drift without a full list.
94
- - **`ContractDrift` moved to core** (PR #220): `Textus::ContractDrift` extracted from the MCP surface into the core library so it is reusable across surfaces.
95
- - **MCP `Server` refactored per Sandi Metz rules** (PR #220): routing extracted to `Routing` module; methods decomposed to ≤ 5 lines each.
96
- - **Plugin manifest wiring** (PR #220): `claude_plugin` uses the installed `textus` binary (external use); `mcp_config` keeps `bundle exec` (dev environment). `--lean` references removed from the plugin manifest spec.
97
- - **Namespace renames** (PR #218, internal): `entry/` → `format/`, `envelope/io/` → `envelope/`, `Ports::Queue` → `Ports::JobStore`, `background/` → `jobs/`, `pipeline/` → `produce/`.
98
- - **Workflows reorganised**: workflow files split into `artifacts/` and `config/` subdirectories under `.textus/workflows/`.
99
-
100
- ### Removed
101
-
102
- - **Step system** (PR #218): entire `lib/textus/step/` directory deleted along with all step-related specs, `container.steps`, dead `hooks.rb` doctor check, and `ports/audit_subscriber.rb`. **Migration:** rewrite step files as `.textus/workflows/*.rb` using the Workflow DSL.
103
- - **`Jobs::Refresh`** ([ADR 0115](docs/architecture/decisions/0115-role-trust-model.md)): workflow-based periodic re-derivation replaces the background refresh job.
104
- - **`handler_permit` manifest field** (PR #218): doctor-only field with no runtime enforcement deleted along with its policy class and schema key.
105
- - **Dead guards and constants**: `derived_write?` no-op guard from `WriteVerb#cascade_to_rdeps`; `publish_each` runtime guard from `Entry::Publish`; duplicate inline `check_action!` from `put`/`key_delete`/`reject`; `BURN = :sync` constant; cross-lane notebook side-effect from `Action::Ingest`.
106
- - **`source.from: fetch` and `source.from: derive`** narrowed to `external` only — fetch/derive methods removed from `Produced`; freshness/pulse use retention rules; boot drops intake/derived keys.
107
-
108
- ### Fixed
109
-
110
- - **MCP `resources` capability missing from `initialize` handshake** (PR #221): the server only advertised `tools`, so MCP clients never called `resources/list`. Adding `"resources" => {}` makes all 9 produced artifacts discoverable as resources.
111
- - **`adr_log` workflow**: `include_keyless: true` added for tree-publish of `knowledge.decisions`.
112
- - **SpecLayout violations** (PR #221): `workflow_index_spec` and `workflow/errors_spec` (string-described) moved to `spec/conformance/workflow/`.
113
-
114
- ## 0.53.0 — 2026-06-15 — Auth consolidation and code quality
115
-
116
- ### Added
117
-
118
- - **`bin/dev`**: local script for syncing skills from GitHub during development.
119
-
120
- ### Changed
121
-
122
- - **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.
123
- - **`Background::Planner::Planner` renamed** to `Planner::Plan` to resolve the double-name antipattern.
124
- - **`Writer#put` decomposed** (38→12 lines): extracted into 8 single-purpose sub-methods (resolve, uid, serialize, schema-validate, etag-check, persist, envelope, audit).
125
- - **`Gate::Auth#check!` / `check_action!` deduplicated**: shared predicate-evaluation logic extracted into `evaluate_predicates`.
126
- - **`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`).
127
- - **`auth`/`writer`/`reader` helpers pulled into `WriteVerb`**: removed 4-file duplication from `Put`, `KeyMv`, `KeyDelete`, `Reject`.
128
- - **`Action::Base#args` auto-generated** from `initialize` params: ~115 lines of boilerplate removed across 25 action classes.
129
- - **`AuditLog#verify_integrity`**: per-line checking extracted into `check_line_integrity` + `iterate_with_prev_seq`.
130
- - **`Error#initialize`**: 5-param constructor replaced with `ErrorInfo` parameter object (backward-compatible).
131
- - **`EventBus#invoke`**: replaced `Thread.new`/`kill` with `Timeout.timeout`.
132
- - **Hand-rolled Mustache replaced** with `mustache` gem: 117 lines of template engine deleted.
133
- - **RuboCop formatting** applied across the codebase.
134
-
135
- ### Changed (breaking)
136
-
137
- - **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.
138
-
139
- ## 0.52.0 — 2026-06-09 — The authority model is a produced reference doc (ADR 0112)
140
-
141
- 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`.
142
-
143
- ### Added
144
-
145
- - **`docs/reference/authority.md` is a produced reference doc** (ADR 0112). A new `authority` registry handler projects the `artifacts.derived.authority` entry — the zone-kind↔capability bijection (from `Schema::Vocabulary::LANES`), this manifest's zones, and its roles with the zone-kinds each can write — rendered through `authority.mustache`. It joins `verbs.md` / `schema.md` / `adr-log.md` on the ADR 0097/0102 produced-docs pattern; `GeneratorDrift` + `HandlerAllowlist` guard it (no new doctor check).
146
-
147
- ### Changed
148
-
149
- - **The authority model's SSoT splits along the produce/guard seam** (ADR 0112, refining ADR 0098). The *current-values* tables move to the generated `authority.md`; `reference/zones.md` drops its three hand-maintained tables, keeps what each capability *means* as prose, and links to the generated values. `explanation/concepts.md` and the docs index re-point accordingly; the orientation template now names four generated reference docs.
150
-
151
- ## 0.51.0 — 2026-06-08 — The reconcile era: one engine for produce + sweep, `source`/`retention`, produced reference docs (ADR 0087, 0090–0095, 0097)
152
-
153
- `build` folds into `reconcile`, materialization becomes system-pushed, the producer kinds and machine zones collapse onto one `source` + `retention` grammar, and the machine-derivable reference docs graduate to produced artifacts.
154
-
155
- ### Changed (breaking)
156
-
157
- - **`build` is removed; `tend` is now `reconcile`** (ADR 0087). One verb runs the full two-phase **produce → destructive-sweep** under a shared lock, and every canon write reactively rebuilds its dependent produced entries (`source.on_write: sync | async`). **Migration:** drop all `build` calls (CLI + MCP); rename scripted/cron `tend` to `reconcile` — the verb, the MCP tool id, and the audit-log verb string.
158
- - **Capabilities collapse to four — `author`, `keep`, `propose`, `reconcile`** (ADR 0090). The `fetch` and `build` capabilities fold into `reconcile`; `automation` defaults to `[reconcile]`. **Migration:** a role with `can: [fetch]` / `can: [build]` (or the interim `ingest`) becomes `can: [reconcile]`; the old spellings are rejected at load with a hint.
159
- - **One `machine` zone-kind and one `produced` entry-kind** (ADR 0091, 0095). Zone-kinds become `canon | workspace | machine | queue` — the former `quarantine` + `derived` zone-kinds fold into one `machine` kind (the kind ⟺ capability mapping is a bijection again; at most one `machine` zone). Entry-kinds become `leaf | nested | produced` — the former `derived` + `intake` entry-kinds fold into one `produced` kind, with the produce-method (intake / derived / external) read off `source.from`. **Migration:** `kind: quarantine` / `kind: derived` on a zone → `kind: machine`; `kind: derived` / `kind: intake` on an entry → `kind: produced`. Both are rejected at load with a fold hint; no shim.
160
- - **`source:` + `retention:` replace `upkeep` / `lifecycle` / `materialize` / `intake` / `compute` / `template`** (ADR 0093). The old slots conflated *production* (how an entry's bytes are made) with *retention* (when an aged entry is retired). They split into an entry-level **`source: { from: project | handler | command }`** — one "acquire data from upstream" concept, where `from` must agree with `kind:` — and a glob-matched **`retention: { ttl, action: drop | archive }`** rule; both run through one produce engine. This is strictly more expressive (re-pull hourly *and* archive at 90 days is now sayable). The `lifecycle: { on_expire: warn }` form is removed (`warn` was dead since 0.50's pure-read `get`). **Migration:** rewrite `upkeep:` / `lifecycle:` / `materialize:` (and the old `intake:` / `compute:` / `template:` blocks) into `source:` + `retention:`. Old manifests fail at load with mechanical fold hints; no shim.
161
- - **A `source` produces *data*; `publish:` is a list of targets** (ADR 0094). A produced entry's stored form is data (e.g. `.json`); rendering moves entirely to the publish path. **`publish:` is now a list** — each element a `{ to:, template?:, inject_boot?: }` file target (copies the data verbatim, or renders it through that target's own template) or a `{ tree: }` subtree mirror — so one dataset can render to differently-shaped targets (`CLAUDE.md` vs `AGENTS.md`) without bespoke handler code. Published artifacts are clean content; textus's `_meta` provenance stays in the stored entry. **Migration:** the *map* `publish: { to: [...] }` / `publish: { tree: }` forms (and the older `publish_to:` / `publish_tree:`) are rejected at load — `publish:` is a list.
162
- - **Hook events renamed** (ADR 0094). RPC `:resolve_intake` → `:resolve_handler`; pub-sub `:entry_put` → `:entry_written`, `:build_completed` → `:entry_produced` (plus `:entry_published` and `produce_failed`). **Migration:** update hook subscriptions to the new names; old names fail at registration.
163
-
164
- ### Added
165
-
166
- - **Reference docs are produced, not hand-authored** (ADR 0097, 0098). `docs/reference/verbs.md` and `docs/reference/schema.md` (projected from the live verb/schema registry) and the new `docs/reference/adr-log.md` (projected from the ADR files) are `kind: produced` entries published out, with a CI gate that fails when `reconcile` is not a no-op. **Do not hand-edit them** — edit the upstream source (the verb/schema code, or an ADR) and run `reconcile`; `doctor` flags a stale hand-edit as `generator_drift`.
167
- - **The root `README.md`, `CONTRIBUTING.md`, and `SECURITY.md` are canon, published out** (ADR 0103, 0104). They are authored under `.textus/zones/knowledge/` and published verbatim to the repo root with an editor banner (the same pattern as `docs/`); a hand-edit to a front-door doc is clobbered by `reconcile` and caught by the CI no-op gate.
168
-
169
- ### Changed
170
-
171
- - **Docs SSoT/DRY cleanup** (ADR 0098). The duplicated ADR index is single-sourced — the mechanical status board lives in `docs/reference/adr-log.md`, and the curated decisions README becomes an annotated reading guide; the verb producer depends on `Read::Capabilities` rather than reaching into `Dispatcher` internals; a conformance guard keeps the `events` / `zones` / `mcp` projections covered.
172
-
173
- ### Internal
174
-
175
- - **The architecture is now executable, and the produce / schema / port layers are decomposed** (ADR 0092, 0099–0101, 0105–0109). The hexagonal layering is enforced by a conformance guard with the layer map written to `lib/textus/ARCHITECTURE.md` (0106); the verb token gains a build-time route ⟺ contract bijection guard (0105); the divergent staleness comparisons collapse into one `Domain::Freshness::Evaluator` (0099); the produce pipeline is gathered under `lib/textus/produce/` with an `acquire` ÷ `render` split and the `fetch_*` fossils renamed (0100); two interface fossils are removed (0101); `manifest/schema.rb` splits its validation walk into `Schema::Validator` and its constants into `Schema::Vocabulary` + `Schema::Keys` (0107, 0109); every port becomes one shape — an instantiable class (0108, 0109); and the conformance spec tier is consolidated (67→19 loose) and coupled to the contract rather than to fixture spelling (0092). No behaviour, manifest-grammar, or verb-surface change.
176
-
177
- ## 0.50.0 — 2026-06-04 — Observability on two axes + boot lifecycle (ADR 0083, 0084, 0085)
178
-
179
- `freshness` collapses into `pulse`, the contract-drift guard stops deadlocking, and `boot` gains a lean session-start projection shipped via a plugin.
180
-
181
- ### Added
182
-
183
- - **`boot --lean`** (ADR 0084) — a compact orientation projection (`protocol`, `store_root`, `zones`, `agent_quickstart`, `contract_etag`) for cheap session-start injection. The full `boot` envelope now also carries `contract_etag`. `--lean` is a contract arg, so it threads to every surface (CLI flag, MCP `lean` tool arg, Ruby kwarg).
184
- - **textus ships as a Claude Code plugin** (ADR 0084) — a single self-contained `.claude-plugin/plugin.json` with an **inline** `SessionStart` hook (`startup`/`clear`/`compact`) that runs `boot --lean`, so enabling the plugin auto-orients each session. The agent-invokable `boot` tool is unchanged; the hook is additive. Plugin data is kept inside `.claude-plugin/` (a separate concern from the gem), not a top-level `hooks/` dir.
185
- - **Agent-integration config is textus-managed, projected from canon** (ADR 0086). This repo's `.mcp.json` and `.claude-plugin/plugin.json` are now **build artifacts** — `textus build` projects them from `knowledge.project` + `Textus::VERSION` (so the plugin version can never drift off the gem) the same way it projects `CLAUDE.md`. The plugin manifest also gains an inline `mcpServers` stanza (enable the plugin → MCP server *and* orientation hook in one). A new `provenance: false` derived-entry flag lets the JSON renderer emit config without a `_meta` block. Downstream consumers are unaffected — `init --with-agent` still scaffolds a write-once `.mcp.json` (a build must never clobber a user's config).
186
-
187
- ### Changed
188
-
189
- - **The contract-drift guard applies to mutating verbs only; `boot` self-heals** (ADR 0083). A mid-session manifest/hook/schema edit no longer deadlocks every MCP verb behind "re-run boot": pure reads and `boot` bypass the guard, `boot` re-arms the session's cached `contract_etag`, and only mutating verbs (the `Write::` family + the destructive `Maintenance::` verbs `tend`/`zone_mv`/`key_mv_prefix`/`key_delete_prefix`) enforce it. "Re-run boot" now actually recovers instead of looping. Refines ADR 0074.
190
- - **`tend` returns a health summary, not full `issues[]`** (ADR 0085) — matching `pulse`; `doctor` stays the sole owner of detailed health output.
191
-
192
- ### Removed (breaking)
193
-
194
- - **The public `freshness` verb** (ADR 0085). Observability is now two verbs on orthogonal axes — `pulse` (transient/temporal: `changed` + `stale` + `next_due_at`) and `doctor` (structural correctness). The lifecycle scan that backed `freshness` becomes a Ruby-only internal (empty `surfaces`, ADR 0073) consumed by `pulse` and the hook context; per-entry detail is reconstructable from `get` (carries `stale`/`last_fetched_at`) + `rule_explain` (the `lifecycle:` ttl + `on_expire`). **Migration:** replace `textus freshness` with `textus pulse` (for the `stale` list / `next_due_at`) or `get` + `rule_explain` (for one entry's verdict). SPEC's generator-drift attribution is corrected to `doctor`'s `generator_drift` check.
195
-
196
- ## 0.49.0 — 2026-06-04 — Normalize the key-verb family + remove `migrate` (ADR 0082)
197
-
198
- The single-key mutation verbs gain the `key_` family stem, and the `migrate` orchestrator is removed.
199
-
200
- ### Changed (breaking)
201
-
202
- - **`mv` → `key_mv` and `delete` → `key_delete`**, renamed everywhere the token is load-bearing: the verb / MCP-tool id, the guard transition symbol, the manifest `guard:` transition key, and the audit-log verb string. The single-key verbs now share the `key_` stem with their bulk cousins `key_mv_prefix`/`key_delete_prefix`; `zone_mv` is unchanged. **The CLI is unchanged** — `key mv`, `key delete`, `key mv-prefix`, `key delete-prefix` keep their spelling. **Migration:** manifests with `guard: { mv: … }` / `guard: { delete: … }` blocks must rename those keys to `key_mv` / `key_delete`; MCP clients pinned to the `mv`/`delete` tool ids must update; audit rows written before this release keep their `mv`/`delete` verb strings (readers must accept both).
203
-
204
- ### Removed (breaking)
205
-
206
- - **The `migrate` verb (YAML-plan orchestrator).** It was non-transactional and added a second input format for no capability the primitives lack — its `zone_mv`/`key_mv_prefix`/`key_delete_prefix` ops remain individually callable, each with `--dry-run`. Removed from the CLI and the MCP catalog.
207
-
208
- ## 0.48.0 — 2026-06-04 — Unified `lifecycle` policy + docs become canon (ADR 0079, 0081)
209
-
210
- Staleness and retention collapse into one age policy, and the upkeep verb surface shrinks 8→3.
211
-
212
- ### Changed
213
-
214
- - **`docs/` is now textus canon (ADR 0081).** Every committed doc is authored under `.textus/zones/knowledge/` and published back to `docs/` by `textus build` (the same projection mechanism as `CLAUDE.md`/`AGENTS.md`); `docs/` is now a committed, sentinel-managed mirror. Adopt-in-place (ADR 0050) made the migration a zero-diff republish. `docs/assets/` (branding binaries) moved to repo-root `assets/`; `docs/plans/` (gitignored) is unchanged.
215
- - **One `lifecycle: { ttl, on_expire }` rule slot** replaces the separate `fetch:` (intake freshness) and `retention:` (leaf pruning) slots. `on_expire` is `refresh` (re-pull intake), `warn` (flag on read), `drop` (delete), or `archive` (copy aside then delete). An action's *destructiveness* decides where it runs: non-destructive `refresh`/`warn` are applied lazily on `get`; destructive `drop`/`archive` run only on the `tend` sweep. A read never deletes.
216
- - **`tend` is now the destructive-only sweep** — it drops/archives expired entries and refreshes cold intake, superseding the composite body of ADR 0078. Result keys are `dropped`/`archived`/`refreshed`/`failed` (apply) and `would_drop`/`would_archive`/`would_refresh` (`--dry-run`).
217
- - **`freshness` reports the unified verdict** (`fresh`/`expired`/`no_policy`) and the matched `on_expire` action; `pulse`'s `stale` list now reflects expired entries.
218
-
219
- ### Removed
220
-
221
- - **The `stale`, `retainable`, `fetch`, `fetch_all`, and `retain` verbs.** Read-through refresh is `get` (lazy); destructive pruning is `tend`; `FetchWorker` remains as the internal executor for `get`/`tend`. Generator/build drift (dependency-based, not age-based) is now the `textus doctor` `generator_drift` check.
222
- - **The `fetch:`/`retention:` rule slots and the per-rule `fetch_timeout_seconds` override** (accepted loss; the constant ceiling applies to every intake).
223
-
224
- ### Added
225
-
226
- - **`doctor` checks `lifecycle.action_invalid`** (`refresh` only on intake; `drop`/`archive` only on stored entries) and **`generator_drift`** (the surviving home for build-drift detection).
227
-
228
- ## 0.47.1 — 2026-06-04 — External entries are a non-build path
229
-
230
- ### Fixed
231
-
232
- - **`build` no longer materializes `external` derived entries.** External entries are generated by an out-of-band runner — textus only tracks their staleness. Previously they flowed through `Derived#publish_via` → `Pipeline.run` and hit a catch-all branch that emitted an empty payload and re-stamped the volatile `generated_at` (contrary to ADR 0070), which for an entry with publish targets would **clobber the runner's artifact with an empty render**. External is now skipped in `Derived#publish_via`, and `Pipeline.run` raises loudly if a non-projection source reaches it (only projection-derived entries are buildable).
233
- - **`compute.command` is now parsed for `external` entries.** The parser read `compute["runner"]`, a key the manifest schema forbids (`COMPUTE_KEYS` allows `command`), so `External#command` was always `nil`. The data field is renamed `runner` → `command` and now reads `compute["command"]`.
234
-
235
- ## 0.47.0 — 2026-06-04 — Close the agent loop over MCP
236
-
237
- The edit→accept→rebuild loop now closes over a single MCP transport: `build` is surfaced to MCP, `init --with-agent` scaffolds a connectable agent setup, and connection-lifecycle hardening (whole-contract drift fingerprint + a connect-time event carrying the resolved role) underpins it.
238
-
239
- ### Changed (breaking)
240
-
241
- - **Drift guard now fingerprints the whole contract.** `Session#manifest_etag` is renamed `contract_etag` and now digests `manifest.yaml` + `hooks/**/*.rb` + `schemas/**/*` ([ADR 0074](docs/architecture/decisions/0074-contract-etag-drift-guard.md)). A mid-session edit to any hook or schema raises `contract_drift` on the next MCP `tools/call`, where previously only a manifest edit did. The composite digest lives as `Etag.for_contract`.
242
- - **Wire:** the `pulse` envelope key `manifest_etag` is renamed `contract_etag`.
243
- - **Ruby:** `Textus::Session#manifest_etag` / `Textus::MCP::Session#manifest_etag` is renamed `contract_etag`. Embedders constructing a `Session` must pass `contract_etag:`.
244
-
245
- ### Added
246
-
247
- - **`build` is surfaced to MCP** ([ADR 0076](docs/architecture/decisions/0076-build-gates-by-capability-actor-surface-to-mcp.md)) — previously CLI-only. `build` is transport-uniform, caller-agnostic, and self-elevating: it always runs as the manifest's `build`-capable actor (not the caller) and grants no authority over content, since it only recomputes the deterministic, content-addressed projection of already-accepted canon. Actor-resolution and the `BuildLock` moved out of the CLI verb into the shared `Write::Build` use-case (an `around :build_lock` resource), so single-writer serialization now spans **every** transport — closing a latent gap where the Ruby API path skipped the lock. The MCP catalog derives the new tool; `boot` auto-advertises it.
248
- - **`textus init --with-agent`** ([ADR 0077](docs/architecture/decisions/0077-init-with-agent-profile.md)) — an opt-in profile that scaffolds a connectable agent setup on top of the neutral store: `knowledge.project` + `knowledge.runbooks` entries (with their schemas), an `artifacts.orientation` derived entry that projects them to `CLAUDE.md`/`AGENTS.md`, and a write-once starter `.mcp.json` at the project root. The default `init` (no flag) is unchanged and stays vendor-neutral; under the flag, `init` writes exactly one file outside `.textus/` (`.mcp.json`, never clobbered if present). Paired with `build` over MCP, an agent can edit `knowledge.*` and rebuild its own orientation without leaving the conversation.
249
- - **`:session_opened` hook event** ([ADR 0075](docs/architecture/decisions/0075-session-opened-connect-event.md)) — fires once per MCP connection at `initialize` with `ctx:, role:, cursor:` (the resolved connection role). Use it for connect-time, role-keyed behavior (session logging, context priming). Distinct from `:store_loaded` (process-time, default role).
250
-
251
- ## 0.46.0 — 2026-06-03 — Container is the single source of truth
252
-
253
- No `textus/3` wire-format change. Internal refactor of the composition root. The 7-field capability set (`manifest, file_store, schemas, root, audit_log, events, rpc`) was previously spelled out four times — `Container`'s `Data.define`, `Store`'s ivar assignments, `Store`'s `attr_reader`s, and `Container.from_store`. It now lives in exactly one place.
254
-
255
- ### Changed
256
-
257
- - **`Store` builds its `Container` once and derives its readers from it.** `Store#initialize` constructs the `Container` directly (`build_container`); the public accessors (`store.manifest`, `store.root`, …) are generated from `Container.members`, so adding a capability to the `Data.define` auto-exposes it on `Store`. Hook wiring is extracted into `bootstrap_hooks`.
258
- - **`Store.discover` decomposed.** The upward directory walk is extracted into `ascend_for_store`, and the duplicated `.textus`/`manifest.yaml` existence check into a shared `store_dir?` predicate.
259
-
260
- ### Breaking (pre-1.0)
261
-
262
- - **`Container.from_store` is removed.** It built a fresh `Container` by copying the seven accessors off a `Store`. Use `store.container` instead (built once, memoized). Specs that swapped the event bus post-construction via `store.instance_variable_set(:@events, …)` now inject explicitly through the immutable `Container`'s `#with` (e.g. `container.with(events: probe)`). Retires the `from_store` idiom described in [ADR 0016](docs/architecture/decisions/0016-application-ports-value.md) and [ADR 0020](docs/architecture/decisions/0020-capability-records.md).
263
-
264
- ## 0.45.1 — 2026-06-03 — Single-path lifecycle: kill the last dual-paths ([ADR 0069](docs/architecture/decisions/0069-single-path-lifecycle.md))
265
-
266
- No `textus/3` wire-format change. Finishes the 0.45.0 lifecycle (ADRs 0066–0068): the request path was single-path in shape but still carried four residual dual-paths. This removes all four so the lifecycle — `normalize → bind (always validate) → dispatch (+around) → view (self-shaping)` — is single-path in fact on every surface. Three breaking (pre-1.0) changes, all accepted.
267
-
268
- ### Changed
269
-
270
- - **Views self-shape on every surface.** The CLI runner's `result.to_h_for_wire` pre-wire is deleted, so every `view` receives the raw use-case result on every surface (the pattern `get`/`zone_mv`/`migrate` already used). `propose` collapses its two views into one `view { |env, _i| env.to_h_for_wire }`.
271
- - **One normalizer home.** MCP's inline by-wire-name normalizer is lifted into `Contract::Binder.inputs_from_wire`, beside `inputs_from_ordered`; the binder now owns both. `MCP::Catalog#call` calls it.
272
- - **Validation is unconditional; `required:` is an honest invariant.** The `validate:` keyword is dropped from `Binder.bind`, `RoleScope#dispatch_bound`, and `Maintenance::Migrate`. Bind always validates — no opt-out, so the `validate: false`-by-default footgun cannot exist. `required:` now means "required on every surface," not an agent-wire policy (retiring the [ADR 0066](docs/architecture/decisions/0066-one-binder-required-is-a-surface-policy.md) `validate:` fork).
273
- - **The hand-authored CLI taxonomy is named and guarded.** `HAND_AUTHORED_VERBS` splits into `BEHAVIORAL_HATCHES` (`get`, `put`, `build` — genuine `< Runner::Base` overrides) and `NON_PROJECTED_CLI` (`fetch`, `fetch_all`, `boot`, `doctor` — plain `< Verb` commands), with the union derived. A new guard spec asserts each member's CLI class matches its category.
274
-
275
- ### Breaking (pre-1.0)
276
-
277
- - **`Binder.bind` / `RoleScope#dispatch_bound` drop the `validate:` keyword.** Callers passing it must remove it; validation is always on.
278
- - **`put`/`propose` `meta` is now `required: false`.** A missing `_meta` no longer returns a pre-dispatch `missing _meta` error — it binds with `meta` absent and flows to schema validation downstream (where an *invalid* `_meta` fails, and an absent-but-schema-permitted `_meta` succeeds). `meta`'s real requiredness lives in schema validation.
279
- - **MCP/Ruby `propose` returns the full wire envelope.** The single self-shaping view emits `env.to_h_for_wire` on every surface — a superset of the old `{uid, etag, key}`.
280
-
281
- ## 0.45.0 — 2026-06-03 — The contract owns the request lifecycle ([ADRs 0066–0068](docs/architecture/decisions/))
282
-
283
- No `textus/3` wire-format change. The `Contract` now owns the entire request lifecycle — `acquire → bind → invoke → render` — so the three surfaces (MCP, CLI, Ruby) project from it with no re-implemented argument-mapping, no dual response/cli_response shaping, and no behavioral escape-hatch classes that exist only because the contract couldn't express I/O acquisition, a stateful wrapper, or a coercion. The escape-hatch population shrinks from 18 to the irreducible behavioral floor of 7. Several breaking (pre-1.0) DSL and signature changes; one operator-visible CLI change (`key delete-prefix` / `key mv-prefix`).
284
-
285
- ### Changed
286
-
287
- - **One argument binder; `required:` is a surface policy ([ADR 0066](docs/architecture/decisions/0066-one-binder-required-is-a-surface-policy.md)).** `MCP::Catalog.map_args`, `CLI::Runner.call_args`, and `RoleScope`'s default-injection loop collapse into one `Contract::Binder.bind` over a uniform by-name `inputs` hash. Every surface dispatches through one site, `RoleScope#dispatch_bound`, so bind fires exactly once. The agent surfaces validate required args (`validate: true`); the Ruby API binds leniently and trusts the use-case's own keyword defaults — `required:` is an agent-wire policy, not a contract invariant.
288
- - **BREAKING (pre-1.0): per-surface `view`s replace `response`/`cli_response` ([ADR 0067](docs/architecture/decisions/0067-per-surface-views.md)).** One `views` map keyed by surface: `view { … }` (MCP/Ruby) and `view(:cli) { … }`. Every view is called uniformly as `(result, inputs)`, retiring the `Proc#arity == 2` sniff. `Contract::View.render` is the single shaping entry point.
289
- - **Declarative facets dissolve the acquisition & wrapper hatches ([ADR 0068](docs/architecture/decisions/0068-declarative-facets-dissolve-escape-hatches.md)).** `arg … source: :file` (read a path → contents), `arg … coerce: callable`, `cli_stdin :json` (stdin envelope), `around :name` (stateful wrappers via `Contract::Around`), and `arg … cli_default:` (a CLI default diverging from the agent default, driving both the value and boolean flag polarity). `propose`, `migrate`, `rule_lint`, `audit`, `zone_mv`, `pulse`, `key_delete`, and `mv` lose their hand-authored CLI classes and generate. `HAND_AUTHORED_VERBS` shrinks 18 → 7 (`get`, `put`, `build`, `fetch`, `fetch_all`, `boot`, `doctor`) — the behavioral floor. `zone_mv`/`migrate`/`key_*_prefix` apply by default on the CLI and plan by default for agents (ADR 0060), now legible in the contract rather than hidden in a hand class.
290
- - **BREAKING (pre-1.0): `key delete --prefix P` → `key delete-prefix P`; `key mv --prefix F T` → `key mv-prefix F T`.** The two `--prefix` overloads split into first-class generated commands, each dispatching the verb its own contract names.
291
- - **BREAKING (pre-1.0): positional `#call` signatures.** `zone_mv` (`from`/`to`), `migrate` (`plan_yaml`), `key_mv_prefix` (`from_prefix`/`to_prefix`), and `key_delete_prefix` (`prefix`) take their declared-positional args positionally, matching the CLI and the binder.
292
-
293
- ### Fixed
294
-
295
- - **`Runner.coerce` now coerces `Integer`-typed flags.** A `case … when Integer` used `===` (instance-of), so `Integer`-typed flags (`audit --limit`/`--seq-since`) silently stayed strings; now compared by equality.
296
- - **The CLI reconciliation guards are seed-independent.** `CLI.verbs` and the contract-reconciliation check ignore anonymous (`name.nil?`) test-fixture `Verb` subclasses that previously leaked into the registry under some RSpec seeds.
297
-
298
- ## 0.44.1 — 2026-06-03 — Finish the CLI contract projection ([ADR 0065](docs/architecture/decisions/0065-finish-cli-response-shrink-escape-hatches.md))
299
-
300
- No `textus/3` wire-format change. The `cli_response` facet can now receive the call's resolved inputs, so the two output-only escape hatches become generated verbs. No operator-visible change to CLI commands, flags, or output.
301
-
302
- ### Changed
303
-
304
- - **`cli_response` may see the call inputs; `uid` and `blame` are now generated ([ADR 0065](docs/architecture/decisions/0065-finish-cli-response-shrink-escape-hatches.md)).** An arity-2 `cli_response` lambda receives `(result, inputs)` (inputs keyed by arg name), letting an envelope echo an input such as the key. `Read::Uid` and `Read::Blame` declare their CLI envelopes in their contracts and drop their hand-authored `Runner::Base` subclasses — the escape-hatch population shrinks from 13 to 11. `zone_mv` (CLI applies by default, agents plan by default — ADR 0060) and `audit` (a one-off `since` coercion) deliberately stay hand-authored.
305
- - **BREAKING (pre-1.0): `Read::Blame#call` takes the key positionally.** `store.as(role).blame(key:)` becomes `store.as(role).blame(key)` to match the CLI's positional `blame KEY` and the generated arg-mapping. The CLI command and the MCP surface (blame is not MCP-surfaced) are unchanged.
306
-
307
- ## 0.44.0 — 2026-06-03 — One verb name across surfaces; the CLI is a contract projection ([ADRs 0058–0064](docs/architecture/decisions/))
308
-
309
- No `textus/3` wire-format change. This release makes a verb's *name* singular across the MCP tool, the CLI command, and the use-case method; widens and hardens the agent surface; unifies `get` on read-through; and makes the CLI a derived projection of the per-verb contract so a command can no longer dispatch a differently-named verb. Several breaking (pre-1.0) renames on the MCP/CLI surfaces; no operator-visible change to CLI commands, flags, or output.
310
-
311
- ### Changed
312
-
313
- - **BREAKING (pre-1.0): one verb name across surfaces ([ADR 0058](docs/architecture/decisions/0058-one-verb-name-across-surfaces.md)).** MCP `schema`→`schema_show`; CLI `fetch stale`→`fetch all` (the label now matches what it does); `retention_sweep`→`retain`; the redundant top-level `delete` folds into `key delete` (which gains `--if-etag`).
314
- - **One rule verb, two depths ([ADR 0059](docs/architecture/decisions/0059-one-rule-verb-two-depths.md)).** MCP `rules` and CLI `rule explain` merge into `rule_explain` — lean `{fetch, guard}` by default, the full matched-blocks explanation under `--detail`/`detail: true`. `rule list` gains a use-case (`Read::RuleList`). Fixes a latent `to_h` crash on any key with a fetch rule.
315
- - **Agent safety: eyes + safe defaults ([ADR 0060](docs/architecture/decisions/0060-agent-safety-graph-reads-and-default-dry-run.md)).** `deps`/`rdeps`/`where` are surfaced to MCP (the catalog grows 15→20 tools, including single-key `delete`/`mv`); the four bulk-destructive verbs (`zone_mv`, `key_mv_prefix`, `key_delete_prefix`, `migrate`) now default `dry_run: true` — an agent that omits the flag gets a Plan, not a mutation. `deps`/`rdeps` return a structured `{key, deps}`/`{key, rdeps}` on every surface.
316
- - **BREAKING (pre-1.0): `build` is the verb, `publish` is the output-destination noun ([ADR 0061](docs/architecture/decisions/0061-build-publish-vocabulary.md)).** `Write::Publish`→`Write::Build`; the dispatcher verb, `RoleScope#build`, the capability, the `derived` zone-kind's required verb, the lock, and the error all read `build`. `publish` is kept only for the ADR-0052 output-destination concept (the `publish:` block, `publish_to`/`publish_tree`, `Ports::Publisher`). Ruby callers using `store.as(role).publish` must call `.build`; the MCP surface is unaffected (the verb was never surfaced).
317
- - **BREAKING (pre-1.0): one `get`, read-through everywhere ([ADR 0062](docs/architecture/decisions/0062-one-get-read-through.md)).** The public `get` is read-through on every surface (fetch-on-stale per the entry's rule, degrading to a pure read when there is no fetch rule), via a single `Read::Get#call(key, fetch:)` class; the CLI's secret `get_or_fetch` upgrade and the verb itself are removed. `get --no-fetch` (MCP `{fetch: false}`) forces a pure on-disk read. The agent's `get` now returns the freshest obtainable envelope (superseding ADR 0060's pure-read stance for `get`).
318
- - **The CLI is a projection of the contract ([ADR 0063](docs/architecture/decisions/0063-cli-is-a-projection-of-the-contract.md)).** A generic `CLI::Runner` (the operator mirror of `MCP::Catalog`) generates each command from its contract via two new facets — `cli` (the command path) and `cli_response` (a CLI-specific return shape, where the operator envelope differs from the agent return). Twelve verbs are now generated with no hand-class; behavioral verbs subclass `Runner::Base` and override `#invoke` only, so the verb *name* is always contract-derived. A total reconciliation spec makes name/dispatch drift — the bug behind the 0058/0059/0061 renames — unrepresentable. No operator-visible change: the set of CLI commands, flags, and output is identical.
319
- - **Derive the CLI command name; guard the dispatcher key ([ADR 0064](docs/architecture/decisions/0064-derive-command-name-and-guard-dispatcher-key.md)).** Tightens ADR 0063's projection: `CLI::Runner::Base` now derives `command_name` from the contract's `cli_leaf`, so the 13 escape-hatch classes drop their literal `command_name "…"` (the verb name is authored once, in the use-case). A new guard asserts every `Dispatcher::VERBS` key equals its use-case's own `contract.verb` — the last identity link no spec checked. No operator-visible change; the set of CLI commands, flags, and output is identical.
320
-
321
- ## 0.43.2 — 2026-06-02 — Agent-legible MCP contracts ([ADR 0057](docs/architecture/decisions/0057-agent-legible-mcp-contracts.md))
322
-
323
- No `textus/3` wire-format change. One breaking (pre-1.0) change on the MCP transport — `put`/`propose` take frontmatter under `_meta` (was `meta`) — plus per-argument descriptions on every MCP tool and a fully catalog-derived agent verb surface.
324
-
325
- ### Changed
326
-
327
- - **BREAKING (pre-1.0): MCP `put`/`propose` take frontmatter under `_meta`, not `meta` ([ADR 0057](docs/architecture/decisions/0057-agent-legible-mcp-contracts.md)).** Every *read* returns the envelope under `_meta` (`get`, SPEC §8), and the CLI `--stdin` envelope already speaks `_meta` — only the MCP `put`/`propose` argument diverged as `meta`, breaking the natural read → edit → write round-trip. The contract gains a `wire_name` primitive so the wire speaks `_meta` while the use-case keeps its `meta:` kwarg (the ADR 0039 signature guard still reconciles). An MCP client that hardcoded the `meta` argument renames one key. The Ruby API (`store.put(meta:)`) and the CLI `--stdin` envelope are unchanged.
328
- - **`boot.agent_quickstart.write_verbs` derives from the MCP catalog ([ADR 0057](docs/architecture/decisions/0057-agent-legible-mcp-contracts.md)).** It now returns bare verb names (`put propose fetch fetch_all`) like `read_verbs`, instead of the CLI string `"put KEY --as=agent --stdin"` (`--as`/`--stdin` are meaningless on an MCP connection — role is connection-resolved, ADR 0040). Closes the de-CLI follow-up named in ADR 0056; a symmetric guard spec fails the build if a CLI string creeps back.
329
-
330
- ### Added
331
-
332
- - **Per-argument descriptions on every MCP tool ([ADR 0057](docs/architecture/decisions/0057-agent-legible-mcp-contracts.md)).** All MCP-surfaced verb arguments now carry a one-line `description:` in their `tools/list` `inputSchema` (was 2 of ~30) — including `put`'s `body`/`content` mutual-exclusivity and the maintenance `dry_run` "default false applies immediately" gotcha. They ship once in `tools/list` at no per-call cost, so an agent can form a valid call from the schema alone.
333
-
334
- ## 0.43.1 — 2026-06-02 — `boot` agent surface derives from the MCP catalog ([ADR 0056](docs/architecture/decisions/0056-boot-quickstart-speaks-the-mcp-catalog.md))
335
-
336
- No `textus/3` wire-format change — `boot`'s agent-orientation fields are corrected to match the verbs an MCP agent can actually call.
337
-
338
- ### Fixed
339
-
340
- - **`boot.agent_quickstart.read_verbs` had drifted from the MCP catalog ([ADR 0056](docs/architecture/decisions/0056-boot-quickstart-speaks-the-mcp-catalog.md)).** It was a hand-maintained list that advertised CLI-only read verbs an MCP agent cannot call (`audit`, `freshness`, `doctor`) while omitting the ones it can and needs (`schema`, `rules`). It now **derives** from `MCP::Catalog` (`get list pulse schema boot rules`) and is reconciled by a guard spec, so it can no longer advertise an uncallable verb or omit a callable one. This is what led downstream skills to shell out to a CLI for schema discovery instead of calling the `schema` verb.
341
-
342
- ### Changed
343
-
344
- - **`boot.agent_protocol.recipes` reference verbs, not CLI strings ([ADR 0056](docs/architecture/decisions/0056-boot-quickstart-speaks-the-mcp-catalog.md)).** Each recipe step names a verb (`get KEY`, `schema KEY`, `put KEY`, `propose KEY`, `fetch_all`) or a plain build step, instead of a `textus …` / `echo … | textus …` shell line — each transport frames the verb itself (CLI vs MCP tool). Keeps shell syntax out of the surface an MCP agent reads. `human_steps` still name `accept` (the author-only transition, not an MCP tool, by design).
345
-
346
- ### Added
347
-
348
- - **ADR 0054 (Proposed, no implementation): entry-level `desc`.** Records the decision to add an optional one-line `desc:` to manifest entries — surfaced in `boot.entries`/`list` — turning the manifest into a navigable index so an agent finds the right data without a caller hardcoding the key. Pre-registers ADR 0055 (a `find`/search verb) as the evidence-triggered follow-up. Documentation only in this release.
349
-
350
- ## 0.43.0 — 2026-06-02 — Typed `publish:` block + remove `index_filename` ([ADR 0052](docs/architecture/decisions/0052-typed-publish-block.md), [0053](docs/architecture/decisions/0053-remove-index-filename.md))
351
-
352
- No `textus/3` wire-format change. Two breaking (pre-1.0) changes on the publish/enumeration surface: the two top-level publish keys become one typed `publish:` block, and the unused `index_filename:` enumeration feature is removed. Both fail at load with a migration-pointing message.
353
-
354
- ### Changed
355
-
356
- - **BREAKING (pre-1.0): `publish_to:`/`publish_tree:` folded into one typed `publish:` block ([ADR 0052](docs/architecture/decisions/0052-typed-publish-block.md)).** Publishing is now configured by `publish: { to: [...] }` (file fan-out) **xor** `publish: { tree: "dir" }` (subtree mirror), mirroring the ADR 0049 internal sum type at the manifest layer and giving a future third mode a namespace instead of a third top-level key. Surface-only — the `Publish::*` modes, `:file_published` event, and build envelope are unchanged. A manifest using the flat `publish_to:`/`publish_tree:` keys **fails at load** with the replacement (`use publish: { to: [...] }` / `{ tree: "..." }`). This is grouping/extensibility, not a new invariant: a block with both `to:` and `tree:` is still caught by the one exclusivity guard in `Publish.resolve`.
357
-
358
- ### Removed
359
-
360
- - **BREAKING (pre-1.0): the `index_filename:` enumeration feature is removed ([ADR 0053](docs/architecture/decisions/0053-remove-index-filename.md)).** `index_filename:` let a nested entry enumerate *directories* as addressable keys via a fixed index file (e.g. `SKILL.md`). It had zero real usage, its only motivating consumer (`EachDir` per-leaf publish) was removed in 0.42.0, and it could not compose with `publish_tree` (mutually exclusive). A nested entry now always enumerates each file under its tree as a key (key segments derived from the path, extension stripped). A manifest declaring `index_filename:` **fails at load** with a migration message; to mirror a directory of files to a consumer path without enumerating them as keys, use `publish: { tree: "..." }`. Native skill authoring (ADR 0050) is unaffected — it rides `publish_tree`, which never enumerated an index. The shallowest-index-wins claiming logic (ADR 0046 D5) and the `index_filename ⊥ publish_tree` guard go with it.
361
-
362
- No `textus/3` wire-format change — repo-local publish behaviour only.
363
-
364
- ### Fixed
365
-
366
- - **`publish_tree` files are now opaque in *every* path, not just the Publisher ([ADR 0047](docs/architecture/decisions/0047-publish-tree-keyless-subtree-mirror.md)).** A keyless subtree mirror carrying non-key-legal filenames (uppercase `SKILL.md`, `README`) tripped `doctor`'s `key.illegal` and red-gated commits, even though publish itself mirrored the files correctly — `doctor`'s `IllegalKeys` and `Resolver#enumerate_nested` still key-walked them, contradicting ADR 0047's "no keys" contract. The resolved publish mode now answers `Publish::Mode#keyless?` (true only for `Tree`); both paths consult it and skip enumerating a keyless mirror's files. A `publish_tree` subtree with uppercase filenames stays `doctor`-green and still mirrors; a non-publish nested entry still flags illegal segments as before.
367
-
368
- ## 0.42.0 — 2026-06-02 — Remove `publish_each`: collapse publish to two modes ([ADR 0051](docs/architecture/decisions/0051-remove-publish-each.md))
369
-
370
- No `textus/3` wire-format change. **Breaking (pre-1.0):** the `publish_each:` manifest key is removed; a manifest declaring it now fails at load. The publish surface collapses to two modes — `publish_to:` (fixed paths) and `publish_tree:` (whole-subtree mirror) — plus `None`.
371
-
372
- ### Removed
373
-
374
- - **BREAKING (pre-1.0): the `publish_each:` publish mode is removed ([ADR 0051](docs/architecture/decisions/0051-remove-publish-each.md)).** Both the file-leaf and directory-leaf (`index_filename`-driven) forms of `publish_each` are gone, along with the `Publish::Each` / `EachFile` / `EachDir` modes and their `{leaf}`/`{basename}`/`{key}`/`{ext}` template vocabulary. The mode had zero real-world usage (dogfood, example, and `init` scaffold all publish via `publish_to`), and its one niche — a leaf that is both an addressable key *and* published to a per-leaf templated path — never materialized, including in the native skill-authoring pipeline (ADR 0050), which rides `publish_tree`. A manifest declaring `publish_each:` now **fails at load** with a migration message ("publish_each was removed in 0.42.0 (ADR 0051) — mirror the subtree with publish_tree (and index_filename to keep the index addressable)"). **Migration:** a directory-leaf `publish_each: "skills/{leaf}"` over a `nested` skills entry becomes `publish_tree: "skills"` over the parent entry (layout preserved). `index_filename:` is **kept** — it survives as a pure *enumeration* feature, independent of publish. The ADR 0049 sum type makes re-adding one subtree mode (or lifting `index_filename` ⊥ `publish_tree` so the two compose) a cheap one-arm change if the niche ever appears.
375
-
376
- ## 0.41.0 — 2026-06-02 — `publish_tree` subtree mirror + content-identical publish adoption ([ADR 0047](docs/architecture/decisions/0047-publish-tree-keyless-subtree-mirror.md), [0050](docs/architecture/decisions/0050-native-authoring-and-content-identical-adoption.md))
377
-
378
- No `textus/3` wire-format change — every change here is repo-local publish behaviour or internal re-layering. Headlines: a key-less `publish_tree:` subtree mirror (ADR 0047), and publish now *adopts* a byte-identical pre-existing target instead of refusing (ADR 0050), so an artifact tree already on disk onboards without a manual delete. Internals were re-cut along the way (fetch subsystem, ADR 0048; publish modes as a sum type, ADR 0049) with no contract change.
379
-
380
- ### Added
381
-
382
- - **`publish_tree:`** — a key-less, path-driven subtree mirror for `nested` entries: copies a whole stored directory to one target dir (layout preserved, per-file sentinels, `ignore`-filtered, whole-target prune). Unblocks publishing the sibling tree of a leaf whose index file is *derived* (issue #132 item #4). Additive; no protocol change. New `doctor` check `publish.tree_index_overlap`. See [ADR 0047](docs/architecture/decisions/0047-publish-tree-keyless-subtree-mirror.md).
383
-
384
- ### Changed
385
-
386
- - **Fetch subsystem re-layered so its three concerns each have one home ([ADR 0048](docs/architecture/decisions/0048-fetch-subsystem-three-concerns.md)).** Internal refactor, no wire or hook-contract change: the before-etag now routes through the `FileStore` port (a guard spec keeps `read/`+`write/` from regressing); a single `IntakeFetch` kernel owns "invoke the intake handler under a deadline" and always passes a uniform `caps: <Container>` to `:resolve_intake`; the fetch lifecycle event vocabulary moves into one `FetchEvents` seam shared by `FetchWorker` and `FetchOrchestrator`. Public verbs, hook events, and the `textus/3` wire contract are unchanged.
387
- - **Publish modes re-cut into a resolved sum type ([ADR 0049](docs/architecture/decisions/0049-publish-modes-as-sum-type.md)).** Internal refactor, no manifest key, wire, event, or `doctor` change: each entry now resolves once to one `Manifest::Entry::Publish::*` mode (`None` / `ToPaths` / `EachFile` / `EachDir` / `Tree`) that owns its publish algorithm, replacing the nil-cascade selector on `Nested`. Mode exclusivity is structural — one `UsageError` naming the conflicting keys, in place of the four scattered pairwise guards across two validators. `EachDir` and `Tree` share one `SubtreeMirror` (one walk, one prune) whose `ignore`-in-prune difference (ADR 0047 D4) is now the explicit `prune_honors_ignore:` parameter. The three manifest keys and every published-leaf / `:file_published` shape are unchanged.
388
- - **Publish adopts a byte-identical pre-existing target instead of refusing ([ADR 0050](docs/architecture/decisions/0050-native-authoring-and-content-identical-adoption.md)).** The clobber guard now fires only when an unmanaged target's content **differs** from the source (or it's an unmanaged symlink); an identical target is *adopted* — its sentinel is written, the file is untouched — so an artifact tree already on disk (e.g. an Agent Skill authored in its native shape, issue #132) onboards to textus without a manual delete. Narrows `SPEC.md` §491; reuses the sha already stored in the sentinel; no new manifest key, CLI flag, hook event, or build-envelope field, and no protocol change.
389
-
390
- ## 0.40.0 — 2026-06-02 — `publish_each` owns multi-file leaf subtrees ([ADR 0046](docs/architecture/decisions/0046-publish-leaf-subtrees.md))
391
-
392
- No `textus/3` wire-format change — publish is repo-local materialization. The
393
- publish *unit* is derived from the entry's existing `index_filename`, not a new
394
- manifest key (SPEC §4, §5.3).
395
-
396
- ### Added
397
-
398
- - **`publish_each` copies a leaf's whole subtree when the entry declares `index_filename` ([ADR 0046](docs/architecture/decisions/0046-publish-leaf-subtrees.md)).** textus can now own a prose-heavy, multi-file artifact (e.g. a Claude Code Agent Skill: `SKILL.md` + `commands.md` + `references/*`) as a single addressable unit. An entry *without* `index_filename` still publishes one file per leaf (unchanged); an entry *with* `index_filename` treats each leaf as a directory and copies the whole subtree — ignore-filtered (ADR 0042), preserving in-leaf layout, one sentinel per file. Siblings ride along at publish time only: they are never enumerated, addressable, or proposable (the key↔path bijection is preserved). On rebuild, managed orphans under a leaf are pruned (file + sentinel); **unmanaged files are never deleted**. The build envelope grows a `pruned` array.
399
- - **`doctor` flags orphaned publish targets.** A new `orphaned_publish_targets` check reports a published file whose recorded source no longer exists in the store (a renamed or removed whole leaf) — drift that per-entry `build` won't revisit.
400
-
401
- ### Changed
402
-
403
- - **BREAKING (pre-1.0): directory-leaf `publish_each` semantics ([ADR 0046](docs/architecture/decisions/0046-publish-leaf-subtrees.md)).** An entry with `index_filename` + `publish_each` whose leaf directories contain siblings previously published *only the index file*; it now publishes the whole subtree. This breaks no correct usage — it is either the dropped-siblings defect this fixes, or a template that named a file rather than a directory, which the validator now **rejects loudly** at manifest load. **Migration:** a directory-leaf `publish_each` template must name the target *directory* — drop a trailing index filename (`.../{leaf}/SKILL.md` → `.../{leaf}`) or any file extension (`.../{leaf}.md` → `.../{leaf}`), and use `{leaf}` or `{key}` (not `{basename}`/`{ext}`, which are file-only).
404
- - **Role names are now a closed set `{human, agent, automation}` ([ADR 0045](docs/architecture/decisions/0045-close-role-name-set.md)).** A manifest declaring any other role name is rejected at load with `unknown role name '<x>' (allowed: human, agent, automation)`. `Role::NAMES` is the single source of truth; each role's `can:` capabilities remain fully tunable. Principal multiplicity moves to the `owner:` field (`owner: human:patrick`). Stricter `textus/3` validation in `Schema.validate_roles!` — no protocol bump; all shipped manifests already comply.
405
- - **Hook event tables consolidated into `Textus::Hooks::Catalog` (single source
406
- of truth).** `EventBus` and `RpcRegistry` no longer keep their own event
407
- tables; both registries, the Loader DSL router, and internal consumers read
408
- `Catalog::PUBSUB` / `Catalog::RPC` directly. This removes the drift-prone
409
- `EventBus::RPC_EVENTS` literal that could silently fall out of sync with
410
- `RpcRegistry`'s events and weaken the cross-registry guards. Internal
411
- refactor — no `textus/3` wire-format change.
412
-
413
- ## 0.39.1 — 2026-06-01 — Feed ergonomics: `feeds.machine` env snapshot + intake cookbook ([ADR 0043](docs/architecture/decisions/0043-feed-ergonomics-without-breaking-core-purity.md))
414
-
415
- No `textus/3` wire-format change. `textus init` scaffolds an additional
416
- `nested` feed entry; core intake still makes no implicit network calls
417
- (SPEC §5.4).
418
-
419
- ### Added
420
-
421
- - **`textus init` scaffolds `feeds.machines.*` with a local env snapshot
422
- (ADR 0043).** Generated stores get a `nested` feed entry capturing ambient
423
- machine context (git HEAD/branch/dirty state, `now`, versions) as an explicit,
424
- user-owned snapshot — keeping ambient state out of `boot`/`pulse` (which stay
425
- side-effect-free per ADR 0037) and out of `quarantine` (which means external
426
- bytes pending validation, where the freshness model does not apply).
427
-
428
- ### Documentation
429
-
430
- - **Multi-machine environment-scan cookbook recipe** demonstrating the nested
431
- `feeds.machines.*` pattern.
432
- - **Examples** updated to use the `feeds.machine` env snapshot, matching
433
- `textus init` output.
434
- - **README flow diagram** redesigned to group writers and colour-code roles.
435
- - **How-to fixes** for zone-rename drift in the agents-mcp guide and the
436
- `:publish` event name.
437
-
438
- ### Internal
439
-
440
- - Removed the legacy `ARCHITECTURE.md` redirect stub.
441
-
442
- ## 0.39.0 — 2026-06-01 — Native ignore patterns for entry enumeration ([ADR 0042](docs/architecture/decisions/0042-native-ignore-patterns-for-entry-enumeration.md))
443
-
444
- No `textus/3` wire-format change. Manifest schema gains one optional, backward-compatible key (`ignore:`); existing manifests are unaffected.
445
-
446
- ### Added
447
-
448
- - **Per-entry `ignore:` globs on `nested` entries (ADR 0042).** A `nested`
449
- entry may declare a list of gitignore-style globs (e.g.
450
- `["**/node_modules/**", "**/dist/**"]`) to keep vendored or generated
451
- subtrees out of the store. Patterns are honoured by **one shared filter
452
- seam** consulted by both resolver enumeration (`list`, `build`) and
453
- `textus doctor`, evaluated *above* key-legality: an ignored path is excluded,
454
- never judged. This closes the prior divergence where a store could `list`
455
- cleanly while `doctor` was red on the same vendored paths. Matching is
456
- segment-wise globstar — `**` spans zero or more path segments; within a
457
- segment `*` is anchored and `{a,b}` alternates (stdlib `File.fnmatch`,
458
- no new dependency). Documented in
459
- [`docs/reference/zones.md`](docs/reference/lanes.md#nested-entries).
460
-
461
- ### Internal
462
-
463
- - **Dogfood textus in its own repo ([ADR 0041](docs/architecture/decisions/0041-dogfood-textus-in-its-own-repo.md)).**
464
- A self-development store and MCP wiring for textus's own repository. No change
465
- to the published gem's behavior.
466
-
467
- ## 0.38.0 — 2026-05-31 — MCP serve acts as agent by default ([ADR 0040](docs/architecture/decisions/0040-mcp-connection-role-and-two-channels.md))
468
-
469
- No `textus/3` wire-format change; no manifest-schema change.
470
-
471
- ### Changed
472
-
473
- - **The MCP connection acts as the `agent` role by default (ADR 0040).**
474
- `textus mcp serve` now resolves its acting role through the standard chain
475
- (`--as` → `TEXTUS_ROLE` → `.textus/role`) with an `agent` transport default,
476
- instead of silently inheriting the global `human` default. The agent channel
477
- proposes; human authority (accept/reject, direct writes) is exercised through
478
- the human's own CLI.
479
-
480
- ### Breaking
481
-
482
- - **MCP writes that relied on human authority now require `propose` + `accept`,
483
- or an explicit `--as=human`.** A connection that previously `put` straight into
484
- `working/`/`identity/` over MCP will get `write_forbidden`. Launch
485
- `textus mcp serve --as=human` (or set `TEXTUS_ROLE`/`.textus/role`) to restore
486
- the old behavior knowingly; the gate is then advisory.
487
-
488
- ## 0.37.0 — 2026-05-31 — MCP catalog derive-or-guard ([ADR 0039](docs/architecture/decisions/0039-mcp-catalog-derive-or-guard.md))
489
-
490
- No `textus/3` wire-format change; no manifest-schema change.
491
-
492
- ### Changed
493
-
494
- - **MCP catalog is now derived from one per-verb contract (ADR 0039).** Each
495
- use-case declares its interface once (`verb`/`summary`/`surfaces`/`arg`/`response`);
496
- the MCP `tools/list` schemas and `tools/call` dispatch are generated from it.
497
- The hand-written `Tools::REGISTRY` and `ToolSchemas` array are gone — a core
498
- interface change can no longer leave MCP silently stale (it is derived, or a
499
- guard spec fails the build).
500
-
501
- ### Added
502
-
503
- - **`propose` and `rules` are first-class verbs** (Ruby/MCP; `propose` also CLI),
504
- no longer MCP-only composed tools. MCP is now a pure projection of the core
505
- verb set filtered by `surfaces(:mcp)`.
506
-
507
- ### Removed
508
-
509
- - **Examples consolidated to a single reference.** Removed `examples/hello/` and
510
- `examples/claude-plugin/`, keeping `examples/project/` as the one worked example —
511
- the role gate (propose → accept), build/publish to `CLAUDE.md`/`AGENTS.md`, schemas,
512
- a template, and a `:transform_rows` hook in one place. The `skill_fanout` recipe
513
- sidecar, its spec, and the `docs/recipes/` page that existed only to document it are
514
- removed alongside. All living docs and `boot`'s `docs.example` now point at
515
- `examples/project/`.
516
-
517
- ### Breaking
518
-
519
- - **MCP `schema` tool is keyed by entry `key`, not `family`.** It routes through
520
- the `schema` (SchemaEnvelope) verb. Callers passing `{ "family": "..." }` must
521
- pass `{ "key": "..." }` instead.
522
- - **The dispatcher verb `schema_envelope` is renamed `schema`.** Ruby callers
523
- using `store.as(role).schema_envelope(key)` must use `store.as(role).schema(key)`.
524
-
525
- ## 0.36.0 — 2026-05-31 — Transports as pure framings: one verb vocabulary, one session, lifted to core ([ADR 0036](docs/architecture/decisions/0036-transports-as-pure-framings.md))
526
-
527
- No `textus/3` wire-format change; no manifest-schema change.
528
-
529
- ### Changed (BREAKING)
530
-
531
- - **MCP tool names aligned with the CLI/Ruby verb vocabulary.** The five renamed tools adopt
532
- the canonical core names: `tick`→`pulse`, `find`→`list`, `read`→`get`, `write`→`put`,
533
- `fetch_stale`→`fetch_all`. The `textus/3` wire format is unchanged; agents that discover
534
- tools via `tools/list` (the documented pattern) adapt automatically. Hardcoded tool names
535
- in `.mcp.json` files, prompts, or scripts must be updated.
536
-
537
- ### Added
538
-
539
- - **First-class CLI `propose` verb** — `textus propose KEY --as=ROLE [--stdin]` auto-prefixes
540
- the manifest's `propose_zone`, matching what the MCP `propose` tool has always done. The
541
- previous workaround (`textus put proposals.KEY --as=ROLE`) still works but the caller no
542
- longer needs to know the queue zone name.
543
- - **Stateful `textus pulse` (no `--since`)** — when `--since` is omitted, `pulse` reads and
544
- updates a per-role cursor from `.textus/.state/cursor.<role>` (gitignored). Successive
545
- invocations see only what changed since the last look, without hand-tracking a sequence
546
- number. `--since=N` remains the explicit, stateless override.
547
- - **`Textus::Session`** — the agent session (role + cursor + propose_zone + manifest_etag,
548
- with cursor-advance, `ContractDrift` / `CursorExpired` detection) is now a core value
549
- object, not MCP-internal state. `MCP::Session` is now an alias to it.
550
- - **`Store#session(role:)`** — returns a `Textus::Session` for Ruby embedders; the
551
- documented Ruby agent loop uses it instead of hand-tracking `since:`.
552
-
553
- ## 0.35.2 — 2026-05-31 — Evaluation field rename + Container doc fix (internal)
554
-
555
- No `textus/3` wire-format change; no manifest-schema change; no library behavior
556
- change. Internal refactor and documentation correction only.
557
-
558
- ### Changed
559
-
560
- - `Domain::Policy::Evaluation` now names its manifest member `manifest` directly
561
- instead of declaring it `snapshot` and exposing it through a `def manifest =
562
- snapshot` alias. Every predicate already read `eval.manifest`; the field now
563
- matches its only call name.
564
- - Dropped the unused `def role = actor` alias on `Evaluation` (zero readers; the
565
- real field `actor` is used everywhere).
566
-
567
- ### Fixed
568
-
569
- - Architecture doc (`docs/architecture/README.md`) listed an `:authorizer` member
570
- on `Container` that the code does not have. Removed it so the doc matches
571
- `lib/textus/container.rb` (7 fields).
572
-
573
-
574
-
575
- No `textus/3` wire-format change; no manifest-schema change; no library behavior change.
576
- Test-suite maintenance only.
577
-
578
- ### Changed
579
-
580
- - Removed redundant per-file `include TextusSpecHelpers` lines (the module is
581
- globally included via `spec/support/fixtures.rb`).
582
- - Envelope-writer specs now assert audit side-effects through the shared
583
- `have_audit_verb` / `last_audit_row` helpers instead of raw `audit.log`
584
- JSON substring matching.
585
-
586
- ### Fixed
587
-
588
- - `pulse_queue_zone` spec no longer leaks a temp directory per run (its second
589
- store now builds under the `textus_store_fixture` tmp tree, which is cleaned up).
590
-
591
- ## 0.35.0 — 2026-05-31 — Proposal target-zone constraint + `author_held` ([ADR 0035](docs/architecture/decisions/0035-proposal-target-zone-constraint.md))
592
-
593
- No `textus/3` wire-format change; no manifest-schema change.
594
-
595
- ### Changed (BREAKING)
596
-
597
- - **`accept` now refuses a proposal whose `target_key` is not a `canon` zone** (new floor
598
- predicate `target_is_canon`). Previously such a proposal failed confusingly downstream
599
- (accept-into-derived) or incoherently "succeeded" (accept-into-workspace). Refusals surface
600
- as `guard_failed` naming `target_is_canon`.
601
- - **Predicate `author_signed` renamed to `author_held`** — it checks possession of the `author`
602
- capability, not a signing gesture. `guard_failed` output and any `rules[].guard` referencing
603
- the old name change accordingly.
604
-
605
- ### Added
606
-
607
- - **`doctor` check `proposal_targets`** — warns on queued proposals whose `target_key` is
608
- non-canon (`proposal.target_not_canon`) or unresolvable (`proposal.target_unresolved`).
609
-
610
- ## 0.34.0 — 2026-05-31 — Unify the Lane vocabulary + finish boot's kind-derived zone naming ([ADR 0034](docs/architecture/decisions/0034-unify-lane-vocabulary.md))
611
-
612
- No `textus/3` wire-format change; no manifest-schema change. The five zone-kinds and five
613
- capabilities, their names, and their mapping are identical to 0.33.0.
614
-
615
- ### Changed
616
-
617
- - **One `Schema::LANES` table is now the source of truth** for the closed coordination
618
- vocabulary; `ZONE_KINDS`, `CAPABILITIES`, and `KIND_REQUIRES_VERB` are derived from it, so a
619
- zone-kind and its required capability can no longer drift. (`CAPABILITIES` array ordering
620
- now follows `LANES.values`; no behaviour depends on it.)
621
- - **`boot` names zones by kind, not by hardcoded instance.** `write_flows`, the
622
- `agent_protocol` recipes, and the CLI verb catalog now reflect the live store
623
- (`knowledge`/`notebook`/`feeds`/`proposals`/`artifacts`) and survive zone renames —
624
- completing the rename-fragility fix [ADR 0033](docs/architecture/decisions/0033-complete-primitives-and-vocabulary.md) §6 began for `ZONE_PURPOSES`.
625
- - **`keep`-holders now get a `notebook` write-flow in `boot`.** 0.33 added the `keep`
626
- capability but no boot guidance for it; the agent's durable-lane flow was silently omitted.
627
-
628
- ### Fixed
629
-
630
- - **`pulse` `pending_review` was silently empty on default stores since 0.33.** It hardcoded
631
- the pre-0.33 zone name `review`; it now derives the queue zone from the manifest, so it
632
- surfaces pending proposals from the (default-named) `proposals` zone again.
633
-
634
- ### Removed (BREAKING, internal)
635
-
636
- - **`Manifest::Data#zones`** (the unused `name => []` map) is removed; the four internal
637
- readers now use `Manifest::Data#declared_zone_kinds`. No manifest-schema or wire change.
638
-
639
- ## 0.33.0 — Complete primitives + vocabulary (ADR 0033) — 2026-05-31
640
-
641
- **BREAKING (manifest schema + default scaffold + predicate/error names; `textus/3` wire format UNCHANGED):**
642
- - New coordination primitive: `workspace` zone-kind + `keep` capability — agents get a durable self-owned lane (`notebook` in the default scaffold). Closes the agent-memory gap.
643
- - Renamed capability `accept` → `author` (the `accept` *transition* / CLI verb is unchanged); predicate `accept_signed` → `author_signed`; zone-kind `origin` → `canon`.
644
- - Default scaffold renamed: `identity` + `working` → `knowledge` (identity is now the `knowledge.identity.*` key prefix), `intake` → `feeds`, `review` → `proposals`, `output` → `artifacts`; new `notebook` workspace zone.
645
- - Zones may declare optional `owner:` (informational) and `desc:` (surfaced as the boot zone purpose).
646
- - Manifests using `origin` / `accept` (capability) / `accept_signed` get an unknown-value rejection at load — no aliasing.
647
- - The `textus/3` envelope, audit-log, and key-grammar wire formats are unchanged.
648
-
649
- ## 0.32.1 — 2026-05-30
650
-
651
- ### Internal
652
-
653
- - Test-suite cleanup for the unified-Guard specs (no `lib/` change): the new Guard/predicate/write specs now use the shared `textus_store_fixture` context plus a single `store_from_manifest` helper (replacing 9 per-file `build_*_store` methods and hand-rolled `Dir.mktmpdir` nesting), a `fail_guard_with` matcher for `GuardFailed` assertions, and uniformly mockist predicate unit specs (`zone_writable_by_spec` joins `accept_signed_spec`/`schema_valid_spec`).
654
-
655
- ## 0.32.0 — 2026-05-30
656
-
657
- Unified Guard engine ([ADR 0031](docs/architecture/decisions/0031-unified-guard.md), moves 2 & 3 of [ADR 0028](docs/architecture/decisions/0028-coordination-planes.md)), plus dropping the never-enforced read gate ([ADR 0032](docs/architecture/decisions/0032-drop-read-policy.md)). Every write transition now authorizes through **one Guard** — an ordered list of pure predicates over a single `Evaluation` context. No wire format (`textus/3`) change; the manifest schema and error envelopes change (breaking).
658
-
659
- ### Changed (BREAKING)
660
-
661
- - **Manifest `rules[].promotion: { requires: [...] }` is removed; use `rules[].guard: { accept: [...] }`.** A `guard:` block is a map of transition (`put`/`delete`/`mv`/`accept`/`reject`/`fetch`) → predicate list, composed (AND) onto each transition's built-in base guard. A stale `promotion:` key is now rejected at load (unknown key).
662
- - **Authorization is unified into one Guard** (ADR 0031). Promotion / accept-authority / schema failures now surface as `guard_failed` naming the unmet predicate(s); the topology refusal keeps the `write_forbidden` code and `--as=<role>` hint. Custom/vendored predicates must use the `#call(Evaluation)` signature.
663
- - **`read_policy` is removed from the manifest** (ADR 0032): textus gates writes, not reads. `Domain::Authorizer` and `ReadForbidden` are gone. Reads are unrestricted at the protocol layer (the `.textus/` files are on disk); per-role read-scoping, if needed, is an agent-surface projection, not a manifest field.
664
-
665
- ### Internal
666
-
667
- - New `Domain::Policy::{Evaluation, Guard, GuardFactory, BaseGuards}` and `Predicates::{Registry, ZoneWritableBy, EtagMatch, FreshWithin}`; `Predicates::{AcceptSigned, SchemaValid}` reshaped to `#call(Evaluation)`. `Domain::Policy::{Promotion, Promote}` and `Write::AuthorityGate` are deleted (folded into the Guard + single `Predicates::REGISTRY`). `Manifest::Rules` `RuleSet` gains `guard`, loses `promote`. `Permission` collapses to `(zone, writers)`.
668
-
669
- ## 0.31.0 — 2026-05-30
670
-
671
- Capability-based roles and the `refresh`→`fetch` transition rename ([ADR 0030](docs/architecture/decisions/0030-capability-based-roles.md)). No wire format (`textus/3`) change; the manifest schema changes (breaking) and the data-in transition is renamed.
672
-
673
- ### Changed (BREAKING)
674
-
675
- - **Roles are now capability-based.** A role declares `can: [verbs]` over a closed 4-verb set — `propose`, `accept`, `fetch`, `build` — replacing the 1:1 `role → kind` model. The role-kinds `accept_authority`/`proposer`/`generator`/`runner` are gone, as are the role names `runner` and `builder` (the umbrella automated role is now `automation`). Default mapping (when no `roles:` block is declared): `human=[accept, propose]`, `agent=[propose]`, `automation=[fetch, build]`.
676
- - **Per-zone `write_policy:` is removed.** Write authority is **derived** from the role's capabilities × the zone's kind: a role may write a zone iff it holds the verb that kind requires — `queue→propose`, `origin→accept`, `quarantine→fetch`, `derived→build`. Zones now declare only `kind:` (and optional `read_policy:`). A manifest carrying `write_policy:` is rejected at load (unknown key).
677
- - **`accept` is the single trust anchor:** at most one role may hold the `accept` capability. The `accept`/`reject` gate and the `maintained_by` override key on the `accept` capability rather than a hard-coded role kind. The promotion predicate `:accept_authority_signed` (and the older `:human_accept`) is renamed `:accept_signed`.
678
- - **The `refresh` transition is renamed `fetch`.** CLI `textus fetch` / `textus fetch stale` (was `refresh` / `refresh stale`); the rule block `refresh:` is now `fetch:`; events `:refresh_started`/`:refresh_failed`/`:refresh_backgrounded`/`:entry_refreshed` are `:fetch_started`/`:fetch_failed`/`:fetch_backgrounded`/`:entry_fetched`; the freshness meta field `last_refreshed_at` is `last_fetched_at`, the freshness verdict `never_refreshed` is `never_fetched`, and the envelope's `refreshing?`/`fetching` field is `fetching`. The MCP tools `refresh`/`refresh_stale` are `fetch`/`fetch_stale`.
679
- - `WriteForbidden` now reports the missing capability: `writing '<key>' (zone '<zone>') needs capability '<verb>'`, with a hint naming the roles that hold it (or `no declared role`).
680
-
681
- ### Internal
682
-
683
- - `Manifest::Capabilities` (was `RoleKinds`) resolves `roles:` to `{ name => [verbs] }`; `Manifest::Data#role_caps` replaces `#role_mapping`. `Manifest::Policy` gains `verb_for_zone`, `roles_with_capability`, `proposer_role`, and derives `zone_writers` from capabilities × zone-kind; `role_kind`/`roles_with_kind`/`role_mapping` are removed. Schema validates the capability vocabulary, the ≤1-`accept` invariant, and that every declared zone-kind's required verb is held by some role.
684
- - The `refresh`→`fetch` rename is mechanical across the engine: `Write::{FetchWorker,FetchAll,FetchOrchestrator}`, `Read::GetOrFetch`, `Domain::Policy::Fetch`, `Ports::Fetch::{Lock,Detached}`, `Domain::Policy::Predicates::AcceptSigned`, `Outcome::Fetched`, and the dispatcher verb keys.
685
-
686
- ## 0.30.0 — 2026-05-29
687
-
688
- Explicit zone kind (strict) and entry retention (ADR 0028, moves 1 & 4). No wire format (`textus/3`) change.
689
-
690
- ### Changed (BREAKING)
691
-
692
- - Zone `kind:` is now **required** on every zone (`origin | quarantine | queue | derived`); a manifest with a kind-less zone is rejected at load. The kind is authoritative: a zone is `derived` only if it declares `kind: derived`, and proposals route only to the zone declaring `kind: queue`. The previous writers→kind inference, the `"review"`-name proposal fallback, and boot's arbitrary-zone propose default were removed. No `textus/3` wire-format change; existing manifests must add `kind:` to every zone.
693
-
694
- ### Added
695
-
696
- - `Manifest::Policy#declared_kind`, `#queue_zone`, `#derived_zone?`. `propose_zone_for` now resolves through the declared `queue` zone exclusively.
697
- - `retention:` rule block (`expire_after`, `archive_after`) parsed into `Domain::Policy::Retention`. New `textus retain --as=ROLE` sweep expires (deletes) or archives leaves past their window — `expire_after` deletes, `archive_after` copies to `.textus/archive/` then deletes; age is the leaf's mtime. `--prefix`/`--zone` narrow the sweep; rows whose zone the role can't write surface as failures. Retention appears in `textus rule explain`.
698
- - `Textus::Domain::Duration.seconds` — shared duration parser (`30s`/`90m`/`12h`/`30d`/bare seconds), now also backing `Refresh#ttl_seconds`.
699
-
700
- ### Internal
701
-
702
- - `Manifest::Entry::Base#in_generator_zone?` and `boot` derived/proposal detection route through `Policy#derived_zone?` / `#propose_zone_for`; all `"review"` substring matches and the `Policy#zone_kinds` inference method are removed.
703
- - Dead `Policy#zone_kinds` method removed.
704
-
705
- ## 0.29.2 — 2026-05-29
706
-
707
- Hook-registry convergence and MCP transport de-leak (ADR 0027). Every change is additive or internal — no wire format (`textus/3`) or manifest-schema change, no public class renamed or removed.
708
-
709
- ### Added
710
-
711
- - `Textus::Hooks::Signature` — single home of callable keyword-introspection (`accepts_keyrest?`, `declared_keys`, `missing`, `filter`), shared by both `EventBus` and `RpcRegistry`.
712
- - `Manifest::Policy#propose_zone_for(role)` — owns the "first writable zone whose name contains `review`" convention; `MCP::Server#handle_initialize` delegates to it instead of scanning `manifest.data.zones` inline.
713
-
714
- ### Internal
715
-
716
- - `EventBus` and `RpcRegistry` both delegate callable introspection to `Hooks::Signature`; both `shape_check!` copies and the hand-rolled `filter_kwargs`/`invoke` derivations are deleted.
717
- - Removed the `store:`→`caps:` legacy shim from `RpcRegistry`: a handler declaring `store:` (instead of `caps:`) is now rejected at registration time with an honest message, not at invoke time. Stale in-repo RPC hook fixtures and the `textus init` scaffold example are migrated to `caps:`.
718
- - `MCP::Server#handle_initialize` no longer iterates `manifest.data.zones`; it calls `policy.propose_zone_for(proposer)`. No zone-selection logic remains in the JSON-RPC transport handler.
719
- - `MCP::Session` converted from a hand-rolled immutable class to `Data.define(:role, :cursor, :propose_zone, :manifest_etag)`, matching the house convention used by all other value objects.
720
-
721
- ### Behavior change (non-breaking in practice)
722
-
723
- - RPC handlers declaring `store:` previously registered successfully and failed only at first invocation (with a misleading message). They now fail at registration time with a message naming the correct kwarg (`caps:`). No handler using `store:` was valid before; only the timing and clarity of the error change.
724
-
725
- ## 0.29.1 — 2026-05-29
726
-
727
- Construction-side cleanup of the use-case layer (ADR 0026). Every change is additive or internal — no public class renamed or removed, wire format (`textus/3`) and CLI unchanged.
728
-
729
- ### Added
730
-
731
- - `Envelope::IO::Writer.from(container:, call:)` and `Envelope::IO::Reader.from(container:)` — named constructors that build the envelope IO collaborators from a `Container`. `Writer.new`/`Reader.new` are unchanged.
732
- - `Write::IntakeFetch.invoke(rpc:, handler:, config:, args:, label:, timeout:)` — the transport-side "invoke a `:resolve_intake` handler under a timeout" kernel; now the canonical home of `FETCH_TIMEOUT_SECONDS`.
733
- - `Dispatcher.invoke(verb, container:, call:, args:, kwargs:)` — single home for the uniform use-case invocation protocol.
734
-
735
- ### Internal
736
-
737
- - `Write::{Put,Delete,Mv,RefreshWorker}` no longer hand-wire `Envelope::IO::Writer`/`Reader`; they call `Writer.from`. Removed ~60 lines of byte-identical construction boilerplate.
738
- - `cli/verb/put.rb` (`--fetch`) and `cli/verb/hook_run.rb` no longer inline `Timeout.timeout { store.rpc.invoke(:resolve_intake, …) }`; both route through `Write::IntakeFetch`. No intake-fetch mechanics remain under `lib/textus/cli/`.
739
- - `RoleScope`'s verb loop delegates the instantiate-and-call step to `Dispatcher.invoke`; it still builds the `Call`. `Store`'s role-selecting verb loop is unchanged.
740
- - `RefreshWorker::FETCH_TIMEOUT_SECONDS` is now an alias of `IntakeFetch::FETCH_TIMEOUT_SECONDS`.
741
-
742
- ## 0.29.0 — 2026-05-29
743
-
744
- A domain-purity pass that routes all filesystem and wall-clock I/O through injected ports. Breaking changes are Ruby-API only; the wire format (`textus/3`) and CLI are unchanged.
745
-
746
- ### Breaking
747
-
748
- - `Domain::Staleness#initialize` now requires `file_stat:` and `clock:` (was `manifest:` only).
749
- - `Domain::Staleness::IntakeCheck#initialize` now requires `file_stat:` and `clock:`.
750
- - `Domain::Staleness::GeneratorCheck#initialize` now requires `file_stat:` (no clock — `GeneratorCheck` has no wall-clock dependency).
751
- - `Domain::Sentinel` is now a pure value object. Its persistence class methods (`write!`, `load`, `sentinel_path`) and `SUFFIX`/`DIR` constants have moved to the new `Ports::SentinelStore`.
752
- - `Domain::Sentinel#orphan?` and `#drift?` now take a `file_stat` argument.
753
- - `Textus::Boot.run_via(container:, role:)` → `Textus::Boot.build(container:)` (the `role:` parameter was unused).
754
- - `Textus::Doctor.run_via(container:, role:, checks:)` → `Textus::Doctor.build(container:, checks:)` (the `role:` parameter was unused).
755
- - `RoleScope#boot` / `#doctor` are removed as special cases; `boot` and `doctor` are now entries in `Dispatcher::VERBS`. `store.boot`, `store.doctor`, and `store.as(role).boot` are unchanged.
756
-
757
- ### Added
758
-
759
- - `Ports::Storage::FileStat` — read-only filesystem query port (`exists?`, `directory?`, `read`, `mtime`, `glob`); the narrow interface pure domain logic depends on (distinct from the write-side `FileStore`).
760
- - `Ports::SentinelStore` — sentinel persistence + path-layout adapter, extracted from `Domain::Sentinel`.
761
- - `Read::Boot` and `Read::Doctor` — dispatched use-case classes on the uniform `(container:, call:)` shape.
762
-
763
- ### Changed
764
-
765
- - `manifest_etag` (in `pulse` output and the MCP session drift token) is now the system-standard `sha256:`-prefixed etag, computed via `FileStore#etag`, instead of a bare SHA-256 hex digest. The token is opaque (compared for equality, never parsed).
766
-
767
- ### Internal
768
-
769
- - The domain layer no longer performs direct filesystem or wall-clock I/O; all disk/clock access is routed through injected ports (`FileStat`, `Clock`). Enforced by a new `spec/domain_purity_spec.rb` that fails on any regression.
770
- - Freshness request timestamps now originate from `Ports::Clock` (via `Call.build`) rather than a bare `Time.now`.
771
- - Cosmetic refactors: deduped the audit limit guard; made `RefreshWorker.normalize_action_result` a public class method (dropped a `send`); extracted staleness guard helpers.
772
- - New guard spec `spec/no_handrolled_manifest_etag_spec.rb` forbids `Digest::SHA256.hexdigest(File.read(...))` from reappearing in `lib/` (exempt: `etag.rb` and `sentinel_store.rb`, the latter being a wire-pinned integrity checksum, not an etag).
773
- - See [ADR 0024](docs/architecture/decisions/0024-domain-purity-ports.md) for the design rationale.
774
-
775
- ## 0.28.0 — 2026-05-29
776
-
777
- A consistency-and-cleanup pass that finishes the seams [ADR 0022](docs/architecture/decisions/0022-container-call-dispatcher.md) left behind. Breaking changes are Ruby-API only.
778
-
779
- ### Breaking
780
-
781
- - Use-case constructors no longer accept `hook_context:`. Use cases that emit events derive their `Hooks::Context` internally from `(container, call)` via the new `Textus::Hooks::Context.for(container:, call:)` factory. Every use case now has the uniform shape `def initialize(container:, call:)`.
782
- - `Textus::Envelope::IO::Writer` and `Textus::Write::RefreshOrchestrator` constructors take `call:` instead of `ctx:` (both received a `Call` already; the kwarg name is corrected).
783
- - `Read::Audit#call` now accepts filter keywords and builds a `Read::Audit::Query` value object internally — keyword callers (`store.audit(key:, limit:)`) are unchanged.
784
- - `Builder::Pipeline.run` takes `(mentry:, deps:)` where `deps` is a `Builder::Pipeline::Deps` record, instead of eight loose keyword collaborators.
785
- - Removed the `CLI::VERBS` const-missing shim (use `CLI.verbs`).
786
- - Removed the `Manifest::Entry::PUBLISH_EACH_VARS` / `PUBLISH_EACH_VAR_RE` re-exports (use `Manifest::Entry::Validators::PublishEach::KNOWN_VARS` / `::VAR_RE`).
787
-
788
- ### Internal
789
-
790
- - Removed the runtime `initialize`-parameter reflection from both `RoleScope` and `Doctor::Check`; verb dispatch is now an unconditional `klass.new(container:, call:).call(...)`.
791
- - `Lint/UnusedMethodArgument` disables dropped from 27 to 20; two `Metrics/ParameterLists` (and two complexity) disables removed by the value-object refactors. `Metrics/ParameterLists` ceiling documented and kept at `Max 6` (the honest ceiling for value-object constructors, `AuditLog#append`, and the public `put` API).
792
- - `ARCHITECTURE.md`'s "uniform `(container:, call:)`" claim is now accurate; active docs refreshed to the 0.27/0.28 vocabulary.
793
- - No wire-format change. Protocol stays at `textus/3`. CLI verb signatures unchanged. Hook callable surfaces (`ctx:` for pub-sub, `caps:` for RPC) unchanged.
794
- - See [ADR 0023](docs/architecture/decisions/0023-uniform-use-case-shape.md) for the design rationale.
795
-
796
- ## 0.27.0 — 2026-05-29
797
-
798
- ### Breaking
799
-
800
- - Removed `Textus::Session`. Use `store.as(role).put(...)` or `store.put(..., role:)` instead of `store.session(role:).put(...)`.
801
- - Removed `Textus::Application::UseCase` registry. Verb dispatch is now via the static `Textus::Dispatcher::VERBS` table.
802
- - Replaced `Textus::Application::ReadCaps` / `WriteCaps` / `HookCaps` with a single `Textus::Container` record (field names preserved: `manifest`, `file_store`, `schemas`, `root`, `audit_log`, `events`, `rpc`, `authorizer`).
803
- - Renamed `Textus::Application::Context` to `Textus::Call`. Field shape identical.
804
- - Use-case classes are no longer `module Foo; def self.call; Impl.new(...).call; end`. They are plain classes: `class Foo; def initialize(container:, call:); def call(...); end`.
805
- - Flattened `Textus::Application::Write::*` → `Textus::Write::*`, `Application::Read::*` → `Read::*`, `Application::Envelope::*` → `Envelope::IO::*`, `Application::Maintenance::*` → `Maintenance::*`, `Application::Projection` → `Projection`.
806
- - Renamed `Textus::Infra::*` → `Textus::Ports::*`.
807
- - `Manifest::Entry::Base#zone_writers` / `#in_generator_zone?` / `#in_proposal_zone?` now take an explicit `policy` argument; entries no longer carry an `@manifest` back-reference.
808
- - `PublishContext` shrunk from 12 fields to `(container, call, reader)` with derived accessors. Custom derived entries that destructured `pctx.caps` / `pctx.session` / `pctx.ctx` / `pctx.bus` need to use `pctx.container` / construct a `RoleScope` / `pctx.call` / `pctx.events`.
809
- - Hook RPC callables (`:resolve_intake`, `:transform_rows`, `:validate`) receive `caps: container` (a `Textus::Container`) instead of `caps: <WriteCaps>`. Field names preserved, so handlers reading `caps.manifest` / `caps.events` / etc. continue to work.
810
-
811
- ### Internal
812
-
813
- - ~600 LOC removed net across ~60 files.
814
- - No wire-format change. Protocol stays at `textus/3`.
815
- - CLI verb signatures unchanged. No envelope shape changes.
816
- - See [ADR 0022](docs/architecture/decisions/0022-container-call-dispatcher.md) for the design rationale.
817
-
818
- ## 0.26.0 — 2026-05-28
819
-
820
- ### Breaking
821
- - Split `Textus::Hooks::Bus` into `Textus::Hooks::EventBus` (pubsub) and `Textus::Hooks::RpcRegistry` (named callables). The `Hooks::Bus` constant is removed.
822
- - Replaced `Textus::Application::Ports` with three capability records: `Textus::Application::ReadCaps`, `WriteCaps`, `HookCaps`.
823
- - Renamed `Textus::Operations` to `Textus::Session`. Access via `store.session(role:)`. `Operations.for(store, ...)` is removed.
824
- - Hook RPC callables (`resolve_intake`, `transform_rows`, `validate`) no longer accept `store:` — declare `caps:` (a `WriteCaps` for `resolve_intake`/`validate`, `ReadCaps` for `transform_rows`).
825
- - Removed all `Manifest` top-level deprecation shims (`zones`, `entries`, `zone_writers`, `permission_for`, etc.). Use `manifest.data.*` / `manifest.policy.*` / `manifest.resolver.*` / `manifest.rules.*`.
826
- - Moved `Textus::Application::Writes::EnvelopeReader`/`EnvelopeWriter` to `Textus::Application::Envelope::Reader`/`Writer`.
827
- - Renamed `Textus::Application::Writes` → `Textus::Application::Write`; `Textus::Application::Reads` → `Textus::Application::Read`; `Textus::Application::Restructure` → `Textus::Application::Maintenance`.
828
- - Merged `Textus::Application::Refresh::*` into `Textus::Application::Write::Refresh{Worker,Orchestrator,All}`.
829
- - Moved `Textus::Application::Policy::Promotion` and predicates to `Textus::Domain::Policy::Promotion`/`Predicates`.
830
-
831
- ## 0.25.1 — 2026-05-28
832
-
833
- ### Internal refactors
834
-
835
- - **ADR 0018**: `Manifest` is now a composition record over `Data`,
836
- `Resolver`, `Policy`, `Rules`. Top-level methods like
837
- `Manifest#permission_for` are deprecated; use
838
- `manifest.policy.permission_for(zone)`. One-cycle bridge — shims
839
- warn until 0.26.0.
840
-
841
- - **ADR 0016**: Application use cases take a single `ports:` kwarg
842
- bundling six adapters + the store root. Hook DSL callables that
843
- declare `|store:|` continue to work with a one-shot deprecation
844
- warning per (event, hook_name); declare `|ports:|` to silence it.
845
-
846
- - **ADR 0017**: `Application::Writes::EnvelopeIO` split into
847
- `EnvelopeReader` (parse) and `EnvelopeWriter` (put/delete/move
848
- + audit). Every public `EnvelopeWriter` method now ends with an
849
- audit-row append — the write-without-audit failure mode is gone.
850
-
851
- ### Breaking (internal)
852
-
853
- - `Operations#store` accessor removed. There is no clean deprecation
854
- shim because `Ports` cannot reconstruct a `Store`. External
855
- callers should use `ops.ports.X` directly.
856
-
857
- - `Textus::Manifest::Entry::Base::PublishContext` struct shape
858
- changed: `:store` removed, `:ports` + `:boot` added. Affects
859
- third-party plugins that build custom derived entries.
860
-
861
- - `transform_context` passed to `transform_rows` RPC callables is
862
- now an `Application::Ports`, not a `Store`. Transforms that treat
863
- it as opaque continue to work; transforms that reach `.x` need
864
- updates.
865
-
866
- No CLI verb signatures changed. No wire envelopes changed.
867
- Protocol remains `textus/3`.
868
-
869
- ## 0.25.0 — 2026-05-28
870
-
871
- ### Added (additive — backward-compatible pulse fields)
872
- - `pulse.manifest_etag` — sha256 of `manifest.yaml`; lets agents detect contract drift without a second verb.
873
- - `pulse.next_due_at` — soonest `next_due_at` across all entries with a refresh policy. Schedulers sleep until this timestamp instead of polling.
874
- - `pulse.hook_errors` — recent hook failures since cursor; bounded in-memory ring on `Hooks::Bus#error_log` (default 256).
875
-
876
- ### Changed
877
- - `Application::Reads::Freshness` memoizes the evaluator verdict by `(key, last_refreshed_at)` per request — pulse no longer pays O(N) evaluator calls when nothing has changed.
878
- - `Application::Refresh::Orchestrator` gains a cooperative-cancel fallback for `RefreshTimed` when `fork(2)` is unavailable (Windows). Previously degraded to `Failed("timed_sync requires fork")`; now executes within the budget on a Thread, killing it on budget exceeded.
879
-
880
- ### Protocol
881
- - No wire-format change. `textus/3` envelopes are unchanged. Pulse fields are additive — existing consumers ignoring unknown keys continue to work.
882
-
883
- ## 0.24.0 — 2026-05-28
884
-
885
- ### Added
886
- - **Context-structure ergonomics** (ADR 0015 Phase 2):
887
- - `textus key mv --prefix OLD NEW` — bulk rename leaves under a prefix; preserves UIDs.
888
- - `textus key delete --prefix P` — bulk delete leaves.
889
- - `textus zone mv FROM TO` — rename a zone; refuses if destination exists; rewrites manifest + moves files.
890
- - `textus rule lint --against=FILE` — diff candidate manifest YAML's `rules:` block against the live manifest.
891
- - `textus migrate PLAN.yaml` — run a multi-op declarative migration plan (ops: `key_mv_prefix`, `key_delete_prefix`, `zone_mv`).
892
- - All five operations also surface as MCP tools (`key_mv_prefix`, `key_delete_prefix`, `zone_mv`, `rule_lint`, `migrate`).
893
- - `Textus::Application::Restructure` module with `Plan` value object and one use case per operation.
894
-
895
- ### Protocol
896
- - No wire-format change. `textus/3` envelopes are unchanged.
897
-
898
- ## 0.23.0 — 2026-05-28
899
-
900
- ### Added
901
- - **Agent gate (MCP transport).** `textus mcp serve` — stdio JSON-RPC 2.0
902
- server speaking MCP draft 2024-11-05. Wraps `Textus::Operations` as ten
903
- auto-derived tools (`boot`, `tick`, `find`, `read`, `write`, `propose`,
904
- `refresh`, `refresh_stale`, `schema`, `rules`). Session state (cursor,
905
- role, manifest_etag) held server-side. Manifest drift surfaces as
906
- `ContractDrift` (-32001); cursor expiry as `CursorExpired` (-32002).
907
- See [`docs/reference/mcp.md`](docs/reference/mcp.md) and [ADR 0015](docs/architecture/decisions/0015-agent-gate-mcp.md).
908
- - `examples/claude-plugin/.mcp.json` and migrated skills/commands/agents —
909
- zero `textus <verb>` shell strings remain in plugin markdown.
910
-
911
- ### Changed (docs)
912
- - `ARCHITECTURE.md`: fixed stale `registry` references (now `bus`),
913
- added Agent Surface section and complete Hooks::Bus event catalog.
914
- - `docs/agent-integration.md`: documents three transports (CLI, Ruby API,
915
- MCP); points agent authors at the MCP transport by default.
916
-
917
- ### Protocol
918
- - No wire-format change. `textus/3` envelopes are unchanged.
919
-
920
- ## 0.22.0 — 2026-05-28
921
-
922
- ### Changed (internal — no manifest-schema impact)
923
- - **Entry polymorphism pass.** Behavior-preserving refactor that
924
- consolidates cross-cutting fields on `Manifest::Entry::Base` and
925
- replaces case-statement dispatch with polymorphic methods. Adding
926
- a new entry kind now costs ~1 file edit instead of ~5–10.
927
- - `publish_to` is now owned by `Base` (was declared four separate
928
- times across Leaf/Derived/Nested/Intake).
929
- - `Base` exposes nil-returning stubs for `template`, `inject_boot`,
930
- `events`, `publish_each`, `index_filename` — validators and
931
- serializers no longer need `respond_to?` guards.
932
- - `Publish#call` dispatches via `entry.publish_via(context)` instead
933
- of a 4-branch case-statement. The byte-identical
934
- `publish_leaf_entry` / `publish_intake_entry` helpers are gone.
935
- - Each `Entry` subclass declares a `KIND` constant and a
936
- `self.from_raw(common, raw)` factory; `Parser` dispatches via
937
- `Entry::REGISTRY` instead of a closed `case kind`.
938
- - Dead `Base#kind` method removed.
939
-
940
- No public API or manifest YAML changes. All existing manifests load
941
- identically.
942
-
943
- Remaining `is_a?(Entry::Derived)` callsites in `builder/`, `renderer/`,
944
- `application/reads/`, and `domain/staleness/` are out of scope for this
945
- pass — they touch a different polymorphism axis (what data the entry
946
- contributes to a build) and will be addressed in a follow-up.
947
-
948
- Known follow-up: `Intake#nested?` still reads `@raw["nested"]` to
949
- preserve the `kind: intake, nested: true` YAML overlay used by nested
950
- intake handlers. This dual discriminator (`kind:` + `nested:`) is a
951
- design tension worth revisiting alongside the broader is_a? cleanup.
952
-
953
- ## 0.21.1 — 2026-05-27
954
-
955
- ### Fixed
956
- - **Intake entries can now act as builder outputs.** Two related gaps closed:
957
- - `FormatMatrix` validator no longer rejects `kind: intake` entries in
958
- generator zones for missing a template. Intake bodies come from a
959
- `:resolve_intake` handler, so the "derived format requires template"
960
- rule never applied. (Error message widened from "derived #{format}"
961
- to "#{format} entries in a generator zone require a template".)
962
- - `Manifest::Entry::Intake` now parses `publish_to:` from YAML (was
963
- hardcoded to `[]`).
964
- - `textus publish` / `textus build` now fan out intake bodies to each
965
- `publish_to` target, mirroring the Leaf fan-out path. Refresh-time
966
- fan-out is unchanged — bodies still publish on the next publish/build
967
- run.
968
-
969
- Closes #80. Lets consumers replace `kind: derived, compute: { kind:
970
- external }` runner glue with `kind: intake` + `Textus.on(:resolve_intake)`
971
- hooks for builder-produced outputs.
972
-
973
- ## 0.21.0 — 2026-05-27
974
-
975
- ### BREAKING
976
- - `textus intro` is removed. Use `textus boot` instead — same envelope, same
977
- use case, better name (pairs with the new `pulse` verb to form the agent
978
- lifecycle: `boot` for static contract, `pulse` for dynamic state).
979
- - The `Textus::Intro` module is now `Textus::Boot`. The manifest entry field
980
- `inject_intro:` is now `inject_boot:`. Builder template variable
981
- `{{intro.*}}` is now `{{boot.*}}`. Pre-1.0; no compatibility alias.
982
-
983
- ### Added
984
- - **`textus pulse [--since=N]`** — agent heartbeat verb. Returns an envelope
985
- with `cursor` (current `latest_seq`), `changed` (audit rows since N),
986
- `stale` (entries past refresh policy), `pending_review` (keys in review
987
- zone), and `doctor` (ok/warn/fail counts). One round-trip replaces what
988
- was previously four separate verbs.
989
- - **`agent_quickstart` block in `textus boot`** — names the read verbs,
990
- write verbs, writable zones, default propose zone, and current
991
- `latest_seq` (the starting cursor for `pulse`). Lets an agent boot once
992
- and immediately know how to talk and where to start polling.
993
- - **Audit log rotation.** Active `audit.log` rotates to `audit.log.1` when
994
- it exceeds `audit.max_size` (default 10MB), keeping the last
995
- `audit.keep` files (default 5). Each rotated file has a sidecar
996
- `audit.log.N.meta.json` with `min_seq`/`max_seq`/`rotated_at`. Configure
997
- via the new top-level `audit:` block in `manifest.yaml`.
998
- - **Monotonic `seq` on every audit row.** Foundation for cursor-based
999
- queries; `audit --seq-since=N` and `pulse --since=N` both use it.
1000
- - **`Textus::CursorExpired`** error class, raised by `pulse` and
1001
- `audit --seq-since` when the requested seq has rotated off disk. The
1002
- message names the oldest still-available seq and tells the agent to
1003
- re-orient via `textus boot`.
1004
- - `docs/agent-integration.md` — boot → pulse → work loop reference, with
1005
- an example agent loop and cursor-expiry handling.
1006
-
1007
- ### Changed
1008
- - Audit rows now include a `seq` integer field (existing fields unchanged).
1009
- - `textus boot` envelope gains `agent_quickstart` (additive — existing
1010
- consumers unaffected).
1011
-
1012
- ## 0.20.2 — 2026-05-27
1013
-
1014
- ### Fixed
1015
- - Promotion predicate `accept_authority_signed` now checks the role's *kind*
1016
- via `manifest.role_kind`, so manifests with a renamed authority role (e.g.
1017
- `owner` instead of `human`) pass the promotion gate. The internal class
1018
- `Predicates::HumanAccept` was renamed to `Predicates::AcceptAuthoritySigned`.
1019
- - `textus schema migrate` now writes as the manifest's declared
1020
- `accept_authority` role instead of the literal `"human"`, and raises a
1021
- clear `UsageError` (with a YAML hint) when no `accept_authority` role is
1022
- declared.
1023
- - `textus accept` / `textus reject` no longer claim "only human role can
1024
- accept" when the manifest declares zero `accept_authority` roles — the
1025
- error now says "no role with accept_authority kind is declared in this
1026
- manifest; accept/reject is disabled".
1027
- - `textus build` now resolves the build role from the manifest's declared
1028
- `generator` kind instead of hardcoding `"builder"`, so renamed generator
1029
- roles work correctly.
1030
- - Manifest validator's "exactly one accept_authority" error message now
1031
- matches what the schema actually enforces.
1032
-
1033
- ### Removed
1034
- - Legacy `human_accept` promotion-predicate alias (string and symbol forms).
1035
- Manifests using `rules[].promotion.requires: [human_accept]` must change
1036
- to `[accept_authority_signed]`. The error on the old form is actionable:
1037
- `unknown promotion predicate: 'human_accept' (known: schema_valid,
1038
- accept_authority_signed)`.
1039
- - `textus key normalize` verb and the underlying
1040
- `Textus::Application::Tools::MigrateKeys` module. Files dropped into nested
1041
- zones with illegal basenames are still reported by `textus doctor` with a
1042
- `key.illegal` finding; fix them by hand. The `--upgrade-manifest` flag and
1043
- its `Textus::Application::Tools::MigrateManifestToKinds` module (one-shot
1044
- 0.19→0.20 manifest upgrader) are removed for the same reason — dead weight.
1045
- - The `migrate-keys` audit-log payload string is no longer emitted (no writer
1046
- produces it).
1047
-
1048
- ### Internal
1049
- - Final cleanup of role-name leaks identified by the 0.20.2 architecture
1050
- audit (follow-on to 0.20.1 role-kinds refactor).
1051
-
1052
- ## 0.20.1 — 2026-05-27
1053
-
1054
- ### Added
1055
- - Optional `roles:` block in `manifest.yaml` lets users rename roles without
1056
- breaking engine semantics. Each declared role maps to one of four engine
1057
- kinds: `accept_authority`, `generator`, `proposer`, `runner`. (#72)
1058
- - `Manifest#role_kind`, `Manifest#roles_with_kind`, `Manifest#zone_kinds`
1059
- accessors for engine integrations.
1060
-
1061
- ### Changed
1062
- - `accept` / `reject` now gate on `accept_authority` kind, not the literal
1063
- `"human"` role. Error messages cite the configured role name.
1064
- - `validator` last-writer trust check uses `accept_authority` kind.
1065
- - Entry `in_generator_zone?` / `in_proposal_zone?` query `zone_kinds`.
1066
- - `Intro` derives `write_flows` and `agent_protocol.role_resolution.roles`
1067
- from the manifest's role mapping.
1068
- - Promote DSL predicate `:human_accept` renamed to `:accept_authority_signed`;
1069
- the old symbol still works as an alias.
1070
- - Schema rejects zone writers that reference an undeclared role when `roles:`
1071
- is declared.
1072
-
1073
- ### Compatibility
1074
- - No wire protocol change (`textus/3`).
1075
- - Existing manifests without a `roles:` block behave identically to 0.20.0.
1076
-
1077
- ## 0.20.0 — architecture redesign (2026-05-27)
1078
-
1079
- **BREAKING (pre-1.0):** Public top-level utility modules removed,
1080
- `Manifest` routing methods extracted into a dedicated resolver,
1081
- `Hooks::Dispatcher`/`Hooks::Registry` collapsed into a single bus, and
1082
- pubsub hook payloads now ship `ctx:` (a `Textus::Hooks::Context`)
1083
- instead of the raw store. External hook files written against the 0.19
1084
- `register(event, name, ...)` API continue to work unchanged; pubsub
1085
- hook bodies must update signatures from `|store:, ...|` to `|ctx:, ...|`
1086
- and use `ctx.put`/`ctx.get`/`ctx.audit`/`ctx.publish_followup` in place
1087
- of direct `store.*` access. RPC events (`transform_rows`, `resolve_intake`,
1088
- `validate`) keep `store:`.
1089
-
1090
- ### Added
1091
- - `Textus::Hooks::Context` — narrow handle for user pubsub hooks. Exposes
1092
- `role`, `correlation_id`, `get`, `list`, `deps`, `freshness`, `put`,
1093
- `delete`, `audit`, and `publish_followup`. All writes route back through
1094
- `Operations` so authorization, audit, and validation cannot be bypassed.
1095
-
1096
- ### Removed
1097
- - `Textus::Dependencies` — use `Operations#deps`, `#rdeps`, `#published`.
1098
- - `Textus::Refresh` — use `Operations#refresh`. The `normalize_action_result`
1099
- helper is now a private class method on `Application::Refresh::Worker`.
1100
- - `Textus::Hooks::Dispatcher` and `Textus::Hooks::Registry` classes.
1101
-
1102
- ### Changed
1103
- - `Textus::Projection` moved to `Textus::Application::Projection`.
1104
- - `Textus::MigrateKeys` moved to `Textus::Application::Tools::MigrateKeys`.
1105
- - `Manifest#resolve`, `#enumerate`, and `#suggestions_for` removed from
1106
- the public `Manifest` API. Use `manifest.resolver.resolve(key)` etc.
1107
- via the new `Manifest::Resolver`. `Manifest` retains the data accessors
1108
- (`entries`, `zones`, `rules`, `permissions`, `validate_key!`).
1109
- - `Store` constructs one `Hooks::Bus`; `Store#registry` removed (use
1110
- `Store#bus`). `Hooks::Builtin.register_all(bus)` and
1111
- `Hooks::Loader.new(bus:)` now take a Bus instead of a Registry.
1112
- `Operations.for` no longer accepts `registry:`. Use cases
1113
- (`Refresh::Worker`, `Refresh::All`) take `bus:`.
1114
- - All pubsub events declare `ctx:` instead of `store:` in their kwargs
1115
- schema. Every `bus.publish` call site passes `ctx: hook_context`.
1116
- `Operations#hook_context` builds the per-`Operations` `Hooks::Context`.
1117
- - Manifest entries gain a required `kind:` field
1118
- (`leaf | nested | derived | intake`). Run
1119
- `textus key normalize --upgrade-manifest` to add it to existing
1120
- manifests — the inference is deterministic and lossless.
1121
- - Internal: `Manifest::Entry` is now an abstract namespace; concrete
1122
- classes are `Entry::Leaf`, `Entry::Nested`, `Entry::Derived`,
1123
- `Entry::Intake`. The fields `projection`, `generator`, `compute`,
1124
- `intake_handler`, `intake_config` are removed from the entry
1125
- interface; `Entry::Derived` carries a typed `source`
1126
- (`Projection` or `External`) and `Entry::Intake` carries `handler`
1127
- / `config`. Use-case code dispatches on entry type rather than
1128
- probing optional fields.
1129
- - `Application::Writes::Build` removed. `Application::Writes::Publish` now
1130
- materializes derived entries (template + projection + external runner)
1131
- AND copies leaf/nested entries to their publish targets in a single pass.
1132
- `Operations#build` is gone; use `Operations#publish` — the `textus build`
1133
- CLI verb is unchanged and produces the same
1134
- `{protocol, built, published_leaves}` JSON shape.
1135
-
1136
- ## 0.19.1 — drop textus/2 migration hint (2026-05-27)
1137
-
1138
- **BREAKING (pre-1.0):** Users on gem ≤0.10 (manifest protocol `textus/2`)
1139
- no longer receive a stepping-stone hint pointing at 0.11.x. The manifest
1140
- parser and `textus doctor` now emit the generic "unsupported version"
1141
- error. Users on ≤0.10 should install 0.11.x first (still on RubyGems)
1142
- to run the migrator, then upgrade to 0.19.1+.
1143
-
1144
- ### Changed
1145
- - `Textus::Manifest` no longer special-cases `textus/2`; `TEXTUS_2_HINT`
1146
- and `version_hint_for` removed.
1147
- - `Doctor::Check::ProtocolVersion` hint/fix text simplified; no longer
1148
- links to the 0.11.x CHANGELOG anchor.
1149
-
1150
- ### Removed
1151
- - Two redundant manifest specs (the `Manifest.load` duplicate and the
1152
- `textus/2`-specific hint assertion) collapsed into one generic case.
1153
-
1154
- ## 0.19.0 — 2026-05-27
1155
-
1156
- ### Breaking
1157
-
1158
- - `Application::Context` is now a slim value object (`role`,
1159
- `correlation_id`, `now`, `dry_run`). Migration table:
1160
-
1161
- | Was | Now |
1162
- |-----|-----|
1163
- | `Application::Context.new(store:, role:)` | `Operations.for(store, role:)` (common case) or `Application::Context.build(role:)` (pure call state) |
1164
- | `Application::Context.system(store)` | Pass `store` directly to hooks |
1165
- | `ctx.store` / `ctx.manifest` / `ctx.file_store` etc. | Construct use cases with the explicit port kwargs |
1166
- | `ctx.authorize_write!(mentry)` | `Domain::Authorizer.new(manifest:).authorize_write!(mentry, role:)` |
1167
- | `Put.new(ctx:).call(..., suppress_events: true)` | Use `EnvelopeIO#write` directly |
1168
- | `store.role` inside a hook | Read `role:` from the event payload |
1169
-
1170
- - `Operations.new(ctx:, manifest:, file_store:, schemas:, audit_log:, bus:, registry:, root:, store:)`
1171
- is the primary constructor. `Operations.for(store, role:)` remains
1172
- a convenience.
1173
-
1174
- - `Application::Writes::Put#call` no longer accepts `suppress_events:`.
1175
-
1176
- - `Domain::Policy::Predicates::*` moved to `Application::Policy::Predicates::*`.
1177
- `Domain::Policy::Promotion` moved to `Application::Policy::Promotion`.
1178
- `Promotion#evaluate` now takes `entry:, schemas:, manifest:, role:`
1179
- instead of `store:`.
1180
-
1181
- - Hooks/intakes/transforms receive the actual `Store` as `store:`
1182
- (previously a Context impersonating one). Event payloads
1183
- (`:entry_put`, `:entry_deleted`, `:entry_renamed`, `:proposal_accepted`,
1184
- `:proposal_rejected`, `:entry_refreshed`, `:refresh_started`,
1185
- `:refresh_failed`, `:refresh_backgrounded`, `:file_published`,
1186
- `:build_completed`) now carry `role:` directly so hooks can observe
1187
- the actor without reaching through the `store:` handle.
1188
-
1189
- - `Application::Refresh::All` is a class, not a module function. Callers
1190
- go through `Operations#refresh_all`.
1191
-
1192
- ### Added
1193
-
1194
- - `Domain::Authorizer` — single source of truth for permission checks.
1195
- - `Application::Policy::Promotion` and `Application::Policy::Predicates::*` —
1196
- the policy evaluator and predicates now live with the Application code
1197
- that loads envelope bytes off disk to evaluate them.
1198
- - `Application::Writes::EnvelopeIO#move` — full move pipeline (replaces
1199
- the in-`Mv` file move + audit).
1200
- - `Application::Writes::EnvelopeIO#read_envelope(key)` — internal
1201
- convenience for callers that need to inspect a pre-move envelope.
1202
-
1203
- ### Internal
1204
-
1205
- - `Builder::Pipeline` no longer re-enters `Operations.for(store)`; it
1206
- takes reader/lister callables from the caller.
1207
- - CLI verbs construct `Operations`, never `Context` directly.
1208
- - `Operations` no longer memoizes per-use-case factories; only
1209
- `envelope_io`, `refresh_worker`, and `orchestrator` are shared.
1210
- - Wire format `textus/3` unchanged. Audit-log NDJSON unchanged.
1211
-
1212
- ### Known follow-ups
1213
-
1214
- - CLI verbs `hook run`, `put --fetch`, `hooks list`, and `rule list`
1215
- still reach into `store.manifest` / `store.registry` directly. A
1216
- future release adds `Operations` projections (e.g. `manifest_view`,
1217
- `hooks_view`, `run_intake`) so these verbs route through the
1218
- Operations boundary.
1219
- - `Application::Writes::Delete#call` retains a `suppress_events:` kwarg
1220
- (used internally by `Reject`). A future release either lifts the
1221
- suppression into `EnvelopeIO`-direct usage (matching the `Mv` path)
1222
- or formalizes per-event suppression as part of the public hook API.
1223
-
1224
- ## 0.18.1 — 2026-05-27
1225
-
1226
- ### Fixed
1227
-
1228
- - `Hooks::Dispatcher` no longer uses `Timeout.timeout`, which can interrupt a
1229
- hook mid-syscall or mid-`ensure` and leave Ruby state corrupted. Each
1230
- subscriber now runs in a worker thread joined with a deadline; on overrun
1231
- the thread is killed and the hook is recorded as `timed_out` (distinct
1232
- from `errored`).
1233
-
1234
- ### Added
1235
-
1236
- - `Hooks::FireReport` — value object returned from `bus.publish`. Lists
1237
- `fired`, `errored`, and `timed_out` subscriber names; exposes `#ok?` and
1238
- `#failures`. Backwards-compatible: callers that ignore the return value
1239
- (the entire current codebase) keep working.
1240
- - `Hooks::Dispatcher#publish` accepts `strict: true`, which re-raises the
1241
- first failure after every subscriber has been attempted. Intended for
1242
- test setups that want loud hooks; default remains `false`.
1243
-
1244
- ### Internal
1245
-
1246
- - No public API surface changes. CLI behavior, `Operations` methods, wire
1247
- format `textus/3`, and the audit-log NDJSON shape are unchanged. Stores
1248
- written by 0.18.0 round-trip through 0.18.1 byte-for-byte.
1249
-
1250
- ## 0.18.0 — 2026-05-27
1251
-
1252
- Port extraction finishes the hexagonal trajectory. `Store::Reader` and
1253
- `Store::Writer` were disguised application code under an infra
1254
- namespace; this release replaces them with a true I/O port
1255
- (`Infra::Storage::FileStore`, bytes only) and lifts their orchestration
1256
- into `Application::Writes::EnvelopeIO` and the existing
1257
- `Application::Reads::*`. `Store` becomes a composition root: nothing
1258
- else. Wire format (`textus/3`) and audit log NDJSON line format are
1259
- byte-identical to 0.17.0 — every change is gem-side.
1260
-
1261
- ### Breaking (Ruby API)
1262
-
1263
- - **`Store::Reader` and `Store::Writer` are deleted.** Both classes
1264
- were doing application work (serialize, UID inject, name-match,
1265
- schema validate, etag negotiate, audit append, event publish) under
1266
- an infra label. Their methods move to flat `Operations` calls:
1267
- ```
1268
- store.reader.get(key) → Textus::Operations#get(key)
1269
- store.reader.read_raw_envelope(key) → Textus::Operations#get(key)
1270
- store.reader.list(prefix:, zone:) → Textus::Operations#list(prefix:, zone:)
1271
- store.reader.where(key) → Textus::Operations#where(key)
1272
- store.reader.uid(key) → Textus::Operations#uid(key)
1273
- store.reader.schema_envelope(key) → Textus::Operations#schema_envelope(key)
1274
- store.reader.published → Textus::Operations#published
1275
- store.reader.stale(...) → Textus::Operations#stale(...)
1276
- store.reader.deps(key) → Textus::Operations#deps(key)
1277
- store.reader.rdeps(key) → Textus::Operations#rdeps(key)
1278
- store.reader.validate_all → Textus::Operations#validate_all
1279
-
1280
- store.writer.write_envelope_to_disk → Textus::Operations#put(key, ...)
1281
- store.writer.delete_envelope_from_disk → Textus::Operations#delete(key, ...)
1282
- ```
1283
- - **`Store#schema_for(name)` is deleted.** Schemas live on a dedicated
1284
- cache:
1285
- ```
1286
- store.schema_for(name) → store.schemas.fetch(name)
1287
- ```
1288
- - **Infra/Domain relocations.** Files that were `Store::*` because the
1289
- namespace was a catch-all now live in the layer they belong to:
1290
- ```
1291
- Textus::Store::AuditLog → Textus::Infra::AuditLog
1292
- Textus::Store::Sentinel → Textus::Domain::Sentinel
1293
- Textus::Store::Staleness → Textus::Domain::Staleness
1294
- Textus::Store::Validator → Textus::Application::Reads::Validator
1295
- ```
1296
- - **Write use-case constructors take `envelope_io:`.**
1297
- `Application::Writes::Put.new(ctx:, envelope_io:)` — same for
1298
- `Delete` and `Mv`. External code that constructed write use cases
1299
- directly adds the kwarg.
1300
- - **Note.** Most embedders construct use cases via
1301
- `Textus::Operations.for(store)`. That constructor still works
1302
- without changes — `Operations#for` wires `envelope_io:` from the
1303
- store. Embedders on the recommended path see no breakage.
1304
-
1305
- ### Added
1306
-
1307
- - **`Textus::Infra::Storage::FileStore`** — pure I/O port. `read`,
1308
- `write`, `delete`, `exists?`, `etag` — bytes in, bytes out. No
1309
- serialization, no schema, no manifest, no events. The seam that
1310
- makes non-file storage backends possible.
1311
- - **`Textus::Schemas`** — eager-loading schema cache. Reads the
1312
- `_schemas/**` zone at boot, exposes `fetch(name)` and `each`.
1313
- Replaces the on-demand `Store#schema_for` lookup.
1314
- - **`Textus::Application::Writes::EnvelopeIO`** — the write pipeline
1315
- collaborator. Serializes the envelope, validates against its
1316
- schema, negotiates etag, writes via `FileStore`, appends to audit,
1317
- publishes the event. The shared orchestration that `Put`,
1318
- `Delete`, and `Mv` previously duplicated through `Store::Writer`.
1319
-
1320
- ### Internal
1321
-
1322
- - **`Store` is a composition root.** Its responsibilities are
1323
- construction and exposure: `manifest`, `schemas`, `file_store`,
1324
- `audit_log`, `bus`, `registry`, `root`. No `reader`, no `writer`,
1325
- no `schema_for`. Hook loading (`load_hooks`) and operations
1326
- exposure (`operations`) remain — both delegate to dedicated
1327
- collaborators.
1328
- - **Read use cases read from `file_store`/`manifest`/`schemas`
1329
- directly.** `Reads::Get`, `Reads::List`, `Reads::Where`,
1330
- `Reads::Stale`, `Reads::Deps`, etc., no longer route through a
1331
- reader facade. The path is `Operations → use case → ports`.
1332
-
1333
- ### Wire format / audit format
1334
-
1335
- Unchanged. `textus/3` envelopes written by 0.17.0 round-trip through
1336
- 0.18.0 byte-for-byte; audit log NDJSON lines are bidirectionally
1337
- compatible.
1338
-
1339
- ### Migrating from 0.17
1340
-
1341
- Mechanical for embedders; transparent for CLI users.
1342
-
1343
- ```
1344
- # Reads
1345
- store.reader.get(key) → ops.get(key)
1346
- store.reader.list(prefix: x) → ops.list(prefix: x)
1347
- store.reader.stale(...) → ops.stale(...)
1348
- # (and the rest of the table above)
1349
-
1350
- # Writes — recommended path stays the same
1351
- ops.put(key, body: x) # unchanged
1352
-
1353
- # Schemas
1354
- store.schema_for(name) → store.schemas.fetch(name)
1355
-
1356
- # Renames
1357
- Textus::Store::AuditLog → Textus::Infra::AuditLog
1358
- Textus::Store::Sentinel → Textus::Domain::Sentinel
1359
- Textus::Store::Staleness → Textus::Domain::Staleness
1360
- Textus::Store::Validator → Textus::Application::Reads::Validator
1361
- ```
1362
-
1363
- ## 0.17.0 — 2026-05-27
1364
-
1365
- API and policy reshape. The public Ruby surface flattens, authorization
1366
- moves from seven duplicated blocks into one helper on `Application::Context`,
1367
- and the only thread-local in the library is gone. Wire format (`textus/3`)
1368
- and CLI JSON output are byte-identical to 0.16.0. Every change is gem-side.
1369
-
1370
- ### Breaking (Ruby API)
1371
-
1372
- - **`Operations` is flat.** The `Operations#reads`, `Operations#writes`,
1373
- and `Operations#refresh` namespace shells are removed; every use case
1374
- is now a directly-named method on `Operations` itself. Callers that
1375
- typed three levels of indirection plus `.call` switch to a single
1376
- method call:
1377
- ```ruby
1378
- ops.writes.put.call(key, body: x) → ops.put(key, body: x)
1379
- ops.reads.get.call(key) → ops.get(key)
1380
- ops.reads.get_or_refresh.call(key) → ops.get_or_refresh(key)
1381
- ops.refresh.worker.call(key) → ops.refresh(key)
1382
- ops.refresh.all.call(prefix:, …) → ops.refresh_all(prefix:, …)
1383
- ```
1384
- Internal use-case instances are memoized via `||=`. `Operations#with_role`
1385
- returns a fresh `Operations` with no shared memoization.
1386
- - **`Operations::Reads`, `Operations::Writes`, `Operations::Refresh`** —
1387
- the shell classes — are deleted. External code that named them
1388
- directly (rare) must move to the flat methods on `Operations`.
1389
- - **Top-level `Textus.on(event, name) { ... }` is removed.** Hook files
1390
- now wrap registration in a `Textus.hook` block that receives the
1391
- store's registry:
1392
- ```ruby
1393
- # before
1394
- Textus.on(:entry_put, "audit") { |store:, key:, **| ... }
1395
- # after
1396
- Textus.hook do |reg|
1397
- reg.on(:entry_put, "audit") { |store:, key:, **| ... }
1398
- end
1399
- ```
1400
- Multiple `reg.on` lines under one `Textus.hook` block is idiomatic.
1401
- - **`Textus.with_registry` is removed.** Tests instantiate
1402
- `Textus::Hooks::Registry.new` and call `reg.on(...)` directly — no
1403
- `around` block, no thread-local cleanup.
1404
- - **`Textus::Hooks::Loader.current_registry` is removed.** It was the
1405
- thread-local read accessor; nothing replaces it because no thread-
1406
- local remains.
1407
- - **Write use-case constructors lose `bus:`.** `Application::Writes::*`
1408
- classes pull the bus from `@ctx.bus` instead of taking it as a kwarg.
1409
- External code that constructed `Writes::Put.new(ctx:, bus:)` directly
1410
- drops the `bus:` argument.
1411
-
1412
- ### Added
1413
-
1414
- - **`Application::Context#authorize_write!(mentry)`** — raises
1415
- `WriteForbidden` (with the zone's writers list in `details`) when the
1416
- bound role lacks write permission. Returns `nil` on success. Replaces
1417
- the seven duplicated `unless can_write? ... raise WriteForbidden`
1418
- blocks across `Writes::{Put,Delete,Mv,Accept,Reject,Build,Publish}`.
1419
- - **`Application::Context#authorize_read!(mentry)`** — mirror of
1420
- `authorize_write!`. Raises a new `ReadForbidden` (code `read_forbidden`,
1421
- exit 1, details: `key`, `zone`, `readers`).
1422
- - **`Application::Context#bus`** — returns `store.bus`. Use cases publish
1423
- events through `@ctx.bus`; the prior `@ctx.store.bus` reach-through is
1424
- no longer used in-tree.
1425
- - **`Textus::ReadForbidden`** error class. Symmetric with `WriteForbidden`.
1426
- - **`Textus.hook(&blk)`** — appends the supplied block to a mutex-
1427
- guarded module-level queue. The store-scoped loader drains and invokes
1428
- each block with its registry.
1429
- - **`Textus.drain_hook_blocks`** — public for tests; returns and clears
1430
- the queued blocks under the same mutex.
1431
- - **`Textus::Hooks::Registry#on`** — already the canonical instance API
1432
- since 0.11; explicitly documented as the registration primitive now
1433
- that the top-level shim is gone.
1434
-
1435
- ### Internal
1436
-
1437
- - **`Application::Writes::Mv`** now authorizes both source and
1438
- destination zones. The prior code authorized only the source; the
1439
- centralized `authorize_write!` made the second call a one-liner and
1440
- the gap obvious.
1441
- - **`Hooks::Builtin.register_all`** takes a `registry:` argument and
1442
- calls `registry.on(...)` directly. No thread-local read.
1443
- - **`Hooks::Loader`** is now a per-store class constructed with
1444
- `registry:`. `#load_dir(path)` walks the directory, `load`s each
1445
- `.rb`, then drains `Textus.drain_hook_blocks` and invokes each with
1446
- the registry. Two threads loading two stores concurrently are safe
1447
- because each `load_dir` drains around its own file walk under the
1448
- module-level mutex.
1449
- - **`Doctor::Check::Hooks`** reads `store.registry` directly; no
1450
- thread-local indirection.
1451
- - **`Store#load_hooks`** is a two-liner: construct a `Loader` with
1452
- `@registry`, call `load_dir` against `.textus/hooks/`.
1453
- - **Reads/refresh paths** use `@ctx.bus` instead of `@ctx.store.bus`.
1454
- Same object; the indirection is gone.
1455
-
1456
- ### Migrating from 0.16
1457
-
1458
- Mechanical, sed-friendly. The CLI shape is unchanged — only embedders
1459
- and hook authors need to do anything.
1460
-
1461
- ```
1462
- # Operations: flat surface
1463
- ops.writes.put.call(key, body: x) → ops.put(key, body: x)
1464
- ops.reads.get.call(key) → ops.get(key)
1465
- ops.reads.get_or_refresh.call(key) → ops.get_or_refresh(key)
1466
- ops.refresh.worker.call(key) → ops.refresh(key) # via Operations#refresh
1467
- ops.refresh.all.call(...) → ops.refresh_all(...)
1468
-
1469
- # Hooks: explicit registration
1470
- Textus.on(:entry_put, "x") { |e| ... }
1471
-
1472
- Textus.hook do |reg|
1473
- reg.on(:entry_put, "x") { |e| ... }
1474
- end
1475
-
1476
- # Tests: no more thread-local scope
1477
- around { |ex| Textus.with_registry(reg) { ex.run } } # delete
1478
- Textus.on(:resolve_intake, :x) { ... } # → reg.on(:resolve_intake, :x) { ... }
1479
- ```
1480
-
1481
- If you constructed `Writes::Put` (or any other write use case)
1482
- directly, drop the `bus:` kwarg from the constructor call. If you
1483
- constructed `Hooks::Loader` directly, the new signature is
1484
- `Loader.new(registry:)` and the API is `loader.load_dir(path)`.
1485
-
1486
- ### ADRs
1487
-
1488
- - [ADR 0010 — Flat Operations API](docs/architecture/decisions/0010-flat-operations-api.md)
1489
- - [ADR 0011 — Authorize-bang in Context](docs/architecture/decisions/0011-authorize-bang-in-context.md)
1490
- - [ADR 0012 — Explicit hook registration](docs/architecture/decisions/0012-explicit-hook-registration.md)
1491
-
1492
- ## 0.16.0 — 2026-05-26
1493
-
1494
- Type cleanup and infra glue. Wire format (`textus/3`) and CLI JSON output
1495
- are byte-identical to 0.15.0. Every change is gem-side.
1496
-
1497
- ### Breaking (Ruby API)
1498
-
1499
- - **`Envelope#freshness`** is now a `Textus::Domain::Freshness` value (a
1500
- `Data.define(:stale, :refreshing, :reason, :refresh_error, :checked_at,
1501
- :ttl_remaining_ms)`), not a `Hash`. Field access replaces string-key
1502
- lookup: `env.freshness.stale` (was `env.freshness["stale"]`). The
1503
- field formerly emitted as `"stale_reason"` on the wire is named
1504
- `:reason` on the value object; `Freshness#to_h_for_wire` still emits
1505
- `"stale_reason"`, so JSON output is unchanged. New fields
1506
- (`:checked_at`, `:ttl_remaining_ms`) are gem-side only and not on the
1507
- wire.
1508
- - **`Manifest#resolve(key)`** now returns a `Textus::Manifest::Resolution`
1509
- value (`Data.define(:entry, :path, :remaining)`) instead of an
1510
- `[entry, path, remaining]` tuple. Callers that destructured the array
1511
- must switch to field access: `res = manifest.resolve(key); res.entry`.
1512
- Raises `UnknownKey` on miss (unchanged).
1513
- - **`Textus::Store.mint_uid`** is removed. Use `Textus::Uid.mint`. A
1514
- companion `Textus::Uid.valid?(str)` predicate is added.
1515
- - **`Hooks::Dispatcher.new(audit_log:)`** no longer accepts
1516
- `audit_log:`. The dispatcher is now a pure pub/sub. Hook-error audit
1517
- rows are written by `Textus::Infra::AuditSubscriber`, which `Store`
1518
- attaches at boot. The NDJSON audit line format is unchanged
1519
- byte-for-byte.
1520
-
1521
- ### Added
1522
-
1523
- - `Textus::Domain::Freshness` — typed envelope-annotation value object.
1524
- - `Textus::Manifest::Resolution` — typed key-resolution value object.
1525
- - `Textus::Uid` — `.mint` / `.valid?` for the 16-hex UID format.
1526
- - `Textus::Infra::AuditSubscriber` — attaches to the event bus and
1527
- writes the `verb: "event_error"` audit row when a user hook raises.
1528
- - `CLI::Verb.command_name "X"` and `CLI::Verb.parent_group Group::Y`
1529
- DSL. Adding a new CLI verb is now a single declaration in the verb's
1530
- own file; the top-level `VERBS` table and group subcommand maps are
1531
- auto-derived from descendants. Help-output ordering is alphabetical
1532
- by command name.
1533
-
1534
- ### Changed
1535
-
1536
- - `CLI::Group` no longer exposes the `cli_name` writer — use
1537
- `command_name` (the prior `cli_name` reader is removed).
1538
- - `Application::Reads::Get` and `Reads::GetOrRefresh` construct
1539
- `Freshness` values directly; their public signatures are unchanged.
1540
-
1541
- ### Deprecated
1542
-
1543
- - `Textus::CLI::VERBS` constant. Still resolves (via `const_missing` to
1544
- the auto-derived table) for backward compatibility; will be removed
1545
- in a future minor. Prefer `Textus::CLI.verbs`.
1546
-
1547
- ### Notes for embedders
1548
-
1549
- - Group subcommand error messages now list subcommands alphabetically
1550
- (e.g., `key requires a subcommand: mv, normalize, uid` rather than
1551
- `mv, uid, normalize`).
1552
- - Lifecycle audit appends for `verb: "put"` / `"delete"` / `"rename"`
1553
- still flow through `Store::Writer` and `Application::Writes::Mv`.
1554
- Centralizing those in a lifecycle subscriber is deferred to 0.18
1555
- port-extraction; it requires event payloads to carry
1556
- `etag_before`/`etag_after`, which they don't yet.
1557
-
1558
- ### ADRs
1559
-
1560
- - [ADR 0008 — Freshness and Resolution value objects](docs/architecture/decisions/0008-freshness-and-resolution-types.md)
1561
- - [ADR 0009 — AuditSubscriber split from Hooks::Dispatcher](docs/architecture/decisions/0009-audit-subscriber-split.md)
1562
-
1563
- ## 0.15.0 — 2026-05-26
1564
-
1565
- ### Breaking
1566
-
1567
- - `Application::Reads::Get#call` is now a **pure read**: it returns the
1568
- on-disk envelope annotated with a freshness verdict, and never
1569
- triggers refresh. `Reads::Get.new` no longer accepts `orchestrator:`.
1570
- Callers that relied on refresh-on-read should switch to
1571
- `Application::Reads::GetOrRefresh` (new), accessible via
1572
- `ops.reads.get_or_refresh`. The CLI verb `textus get` is migrated
1573
- internally; users of `textus get` see no behavior change.
1574
- - `Application::Context#bypass_freshness?` and the `bypass_freshness:`
1575
- kwarg on `Context.new` / `Operations.for` are **removed**. They
1576
- shipped in 0.14.4 as a workaround; with `Reads::Get` now pure by
1577
- default, the flag is dead code. Callers passing `bypass_freshness:`
1578
- will see `ArgumentError`.
1579
- - `Projection.new` signature is **breaking**: now
1580
- `Projection.new(reader:, spec:, lister:, transform_resolver:, transform_context:)`.
1581
- `Projection` no longer constructs its own `Operations` chain. Callers
1582
- inject collaborators. `Builder::Pipeline` is migrated internally.
1583
- - Intake handlers now receive `args: { trigger_key:, leaf_segments: }`
1584
- instead of `args: {}`. Handlers that destructure `args` should
1585
- expect the new keys. Handlers that pass `args` through unchanged
1586
- are unaffected.
1587
-
1588
- ### Fixed
1589
-
1590
- - **Bug 1 / single-flight.** `Refresh::Orchestrator#run_timed` now
1591
- probes the per-leaf lock before forking the detached refresh
1592
- worker. If the lock is held (by a sibling process or earlier fork),
1593
- the orchestrator returns `Outcome::Detached` without spawning a
1594
- redundant worker. Prevents wasted forks when the same key is read
1595
- concurrently across processes.
1596
- - **Bug 2 / leaf-aware intake.** `Refresh::Worker` now keeps the
1597
- `remaining` segments from `Manifest#resolve(key)` and passes them
1598
- to the intake handler as `args: { trigger_key:, leaf_segments: }`.
1599
- Handlers can scope to one leaf instead of re-processing the full
1600
- parent `intake_config` for every leaf refresh.
1601
- - **`textus refresh stale` now exits 0 on success.** Previously the
1602
- verb fell off the end returning `nil`, which propagated up through
1603
- `CLI.run` to `exe/textus:4`'s `exit nil` and raised `TypeError`,
1604
- exit-coding 1 on every successful refresh. Fixed by returning an
1605
- explicit Integer from the verb. The verb return-value contract is
1606
- now codified: every verb's `#call` returns Integer (or `nil` →
1607
- treated as 0); `CLI.run` coerces. (#61)
1608
-
1609
- ### Added
1610
-
1611
- - `Application::Reads::GetOrRefresh` — explicit composition of pure
1612
- `Reads::Get` with the refresh orchestrator. Use for interactive
1613
- reads that want freshest-obtainable envelopes.
1614
- - `ops.reads.get_or_refresh` accessor.
1615
-
1616
- ### Migration
1617
-
1618
- If your code (or hook / handler / extension) called:
1619
-
1620
- | Old | New |
1621
- |---|---|
1622
- | `ops.reads.get.call(key)` to get the freshest envelope | `ops.reads.get_or_refresh.call(key)` |
1623
- | `ops.reads.get.call(key)` for pure read | unchanged; now also pure semantics |
1624
- | `Operations.for(store, bypass_freshness: true)` | `Operations.for(store)` |
1625
- | `Projection.new(store, spec)` | `Projection.new(reader:, spec:, lister:, transform_resolver:, transform_context:)` — see `builder/pipeline.rb` for canonical wiring |
1626
- | Handler signature `lambda { \|store:, config:, args:\| ... }` with `args == {}` | unchanged — `args` is now populated with `:trigger_key` and `:leaf_segments`; handlers that ignore them keep working |
1627
-
1628
- ## 0.14.4 — 2026-05-26
1629
-
1630
- ### Fixed
1631
-
1632
- - `textus build` no longer triggers per-leaf refresh fan-out when
1633
- projection reads encounter stale entries under `on_stale: timed_sync`
1634
- / `on_stale: sync`. Build is a downstream materialization step over
1635
- current store state; freshness is an inflow concern, and they
1636
- shouldn't compose. Previously, building a marketplace-style output
1637
- whose projection selected `intake.vendor.**` against ~400 stale
1638
- leaves spawned ~400 concurrent detached refresh workers, each
1639
- re-running the full intake handler, exhausting the system. The
1640
- build pipeline now reads through an `Application::Context` with
1641
- `bypass_freshness: true`, so `Application::Reads::Get` returns the
1642
- on-disk envelope annotated as fresh without consulting the
1643
- orchestrator. Explicit freshness before build still works via
1644
- `textus refresh stale`. (#59)
1645
-
1646
- ### Added
1647
-
1648
- - `Application::Context#bypass_freshness?` — new flag for read paths
1649
- that must not initiate refresh. Threaded through `Operations.for`
1650
- and propagated across `Context#with_role`. Used by `Builder::Pipeline`
1651
- via a `bypass_freshness:` constructor kwarg on `Projection`.
1652
- - `textus doctor` now reports stale per-key refresh lock files under
1653
- `<root>/.locks/` whose recorded PID is no longer running, as an
1654
- `info`-level `refresh_lock.stale` issue. The check is purely
1655
- informational: `Refresh::Lock` uses `flock(2)`, which the kernel
1656
- releases on process death, so stale `.lock` files on disk do not
1657
- block subsequent refresh acquires. The check exists so users can
1658
- clean up forensic clutter and notice unexpected accumulation. No
1659
- read-path changes — adding a PID probe + unlink there would
1660
- reintroduce the TOCTOU and PID-reuse hazards explicitly rejected
1661
- in 0.14.3 / PR #57. (#58)
1662
-
1663
- ### Tested
1664
-
1665
- - Added a regression spec that forks a child, takes a per-key
1666
- `Refresh::Lock`, SIGKILLs the child, and asserts a fresh acquire
1667
- on the same key succeeds without manual cleanup. Pins the
1668
- flock-survives-SIGKILL contract.
1669
-
1670
- ## 0.14.3 — 2026-05-26
1671
-
1672
- ### Added
1673
-
1674
- - Top-level `flock(2)` mutex at `<root>/.build.lock` prevents concurrent
1675
- `textus build` invocations against the same store. A second build
1676
- while one is already running exits with code `75` (`EX_TEMPFAIL`) and
1677
- emits a `build_in_progress` error envelope, so wrappers like `rake
1678
- update` and CI can distinguish "another build is busy" from "this
1679
- build is broken". The lock is FD-bound and released by the kernel on
1680
- process death (including SIGKILL/OOM), so no stale-lock takeover
1681
- logic is needed. `close_on_exec` prevents the lock from leaking into
1682
- `bundle exec` and lefthook child processes. Per-key locks under
1683
- `.locks/` are unchanged. (#56)
1684
-
1685
- ## 0.14.2 — 2026-05-26
1686
-
1687
- ### Added
1688
-
1689
- - Per-rule `fetch_timeout_seconds:` under `rules[].refresh` overrides the
1690
- hardcoded 30s worker timeout that applies to intake handlers invoked
1691
- via the refresh pipeline (`textus refresh <key>`, `textus refresh
1692
- stale`, and read-path `on_stale: sync` / `timed_sync`). Default stays
1693
- at 30s — opt-in only. Schema validates a positive integer ≤ 3600.
1694
- Mirrors `sync_budget_ms` plumbing: schema → rules → policy → worker
1695
- (#54).
1696
-
1697
- ### Notes
1698
-
1699
- - `fetch_timeout_seconds` is the worker-side hard cap on the intake
1700
- call; `sync_budget_ms` is the caller-side wait budget for `timed_sync`
1701
- on the read path. Two separate concerns, two separate keys.
1702
- - The CLI verbs `textus put --fetch` and `textus hook run` still use the
1703
- 30s constant — only the refresh worker honors the per-rule override in
1704
- this release.
1705
- - Long timeouts pair with `Timeout.timeout`, which raises in the Ruby
1706
- thread but does not kill spawned subprocesses. Intake handlers that
1707
- shell out (e.g. `git clone`) should write to a temp dir and atomically
1708
- rename so mid-flight aborts leave no observable partial state.
1709
-
1710
- ## 0.14.1 — 2026-05-26
1711
-
1712
- ### Changed
1713
-
1714
- - Build pipeline now skips rewriting a built artifact when the only
1715
- difference from the existing file on disk would be the freshly-stamped
1716
- `generated_at` (markdown: `generated.at`) timestamp. Stores under git
1717
- versioning no longer churn on every `textus build` (#52).
1718
- - Strict policy: any other byte difference — changed `from`,
1719
- `template`, `reduce`, body content — still triggers a write. Text
1720
- format falls back to plain byte-equality (no timestamp to normalize).
1721
-
1722
- ### Internal
1723
-
1724
- - Extracted `Manifest.check_version!` to dedupe the parse/load version
1725
- guard (#51).
1726
-
1727
- ## 0.14.0 — 2026-05-26
1728
-
1729
- ### Breaking (Ruby API only — CLI JSON output unchanged)
1730
-
1731
- - `Operations.reads.get.call(...)` and every other use case that returned
1732
- an envelope Hash now returns `Textus::Envelope` (a `Data.define`
1733
- instance). Call `envelope.to_h_for_wire` to recover the previous Hash
1734
- shape for JSON serialization. The Hash shape itself is byte-identical.
1735
- - `Operations.writes.build.call(...)` return shape no longer includes
1736
- `published_leaves`. Call `Operations.writes.publish.call(...)`
1737
- separately for that. The CLI verb `textus build` runs both
1738
- automatically, so CLI users see no change.
1739
-
1740
- ### Added
1741
-
1742
- - `Textus::Application::Writes::Publish` — new use case that copies
1743
- nested-leaf files to their `publish_each` targets. Fires
1744
- `:file_published`.
1745
- - `Operations.writes.publish` — factory exposing the new use case.
1746
- - `Textus::Envelope` (now a class, was a module) — typed accessors for
1747
- `protocol`, `key`, `zone`, `owner`, `path`, `format`, `uid`, `etag`,
1748
- `schema_ref`, `meta`, `body`, `content`, `freshness`. Methods:
1749
- `to_h_for_wire`, `stale?`, `refreshing?`.
1750
-
1751
- ### Internal
1752
-
1753
- - `Application::Writes::Build` trimmed from 116 LOC to ~50 LOC; now
1754
- only materializes generator-zone entries.
1755
- - All ~17 internal `env["..."]` call sites migrated to typed access.
1756
- - `Envelope.build` no longer carries `# rubocop:disable
1757
- Metrics/ParameterLists` (the `Data.define` member list serves the
1758
- same role more clearly).
1759
-
1760
- ### Reference
1761
-
1762
- - See [ADR 0007](docs/architecture/decisions/0007-envelope-data-class.md).
1763
-
1764
- ## 0.13.1 — 2026-05-26
1765
-
1766
- ### Internal
1767
-
1768
- - `Manifest::Entry` (260 LOC, 11 responsibilities) decomposed into:
1769
- - `Manifest::Entry::Parser` — raw hash → Entry value object.
1770
- - `Manifest::Entry::Validators::*` — one file per validation rule
1771
- (events, publish_each, inject_intro, index_filename, format_matrix).
1772
- - `Manifest::Entry` (~50 LOC) — value object with attr readers,
1773
- zone-kind predicates, and `publish_target_for`.
1774
- - Each validation rule is now independently testable. Adding a new
1775
- rule is one new file under `lib/textus/manifest/entry/validators/`
1776
- plus one line in `Validators::REGISTERED`.
1777
- - Pattern matches the existing `doctor/check/*` (~15 files, same
1778
- shape).
1779
-
1780
- ### Compatibility
1781
-
1782
- - `Manifest::Entry` keeps all public attribute readers, predicates,
1783
- and `publish_target_for`. External callers consuming `Entry`
1784
- instances see no change. Embedders who subclassed `Entry` may
1785
- need adjustment.
1786
-
1787
- ## 0.13.0 — 2026-05-26
1788
-
1789
- ### Added
1790
-
1791
- - `Textus::Entry::Base` grows 7 abstract class methods that concrete
1792
- strategies must implement: `nested_glob`, `inject_uid`,
1793
- `enforce_name_match!`, `rewrite_name`, `serialize_for_put`,
1794
- `validate_path_extension`.
1795
- - `Textus::Entry.infer_from_extension(ext)` — registry method
1796
- replacing the deleted `Manifest::EXT_TO_FORMAT` constant.
1797
-
1798
- ### Changed
1799
-
1800
- - Format-specific branches in `Manifest#nested_glob`,
1801
- `Manifest::Entry#{validate_format_matrix!,resolve_format!,
1802
- validate_index_filename!}`, `Store::Writer#{ensure_uid,
1803
- enforce_name_match!,serialize_for_put}`, and
1804
- `Application::Writes::Mv#rewrite_name_for_mv!` all collapse to
1805
- single-line delegations to `Entry.for_format(fmt).<method>(...)`.
1806
- - The 3 rubocop `Metrics/*` disable comments in
1807
- `Manifest::Entry#validate_format_matrix!` are removed; the method
1808
- is now 3 lines.
1809
-
1810
- ### Removed
1811
-
1812
- - `Textus::Manifest::EXT_TO_FORMAT` constant. Use
1813
- `Textus::Entry.infer_from_extension(ext)` instead.
1814
-
1815
- ### Reference
1816
-
1817
- - See [ADR 0006](docs/architecture/decisions/0006-format-strategy-extraction.md).
1818
-
1819
- ## 0.12.6 — 2026-05-26
1820
-
1821
- ### Examples
1822
-
1823
- - New `examples/project/` — demonstrates textus as the context store
1824
- for your own project (identity + runbooks + ADR proposal flow,
1825
- projecting `CLAUDE.md` and `AGENTS.md` at the repo root). Staged as
1826
- a fictional Rails service (`ledger`) so the entries read like a real
1827
- codebase, and the pre-staged ADR proposal is `accept`-runnable
1828
- end-to-end (carries a valid `frontmatter:` payload for its target).
1829
- - Refined `examples/claude-plugin/` — reduced to one entry of each kind
1830
- (one agent, one skill, one command, one identity entry, one output,
1831
- one intake recipe). Removed `bin/`, `Rakefile`, `lefthook.yml`, the
1832
- duplicate `github_folder.rb` recipe, and the per-recipe README.
1833
- - Fixed `examples/claude-plugin/recipes/skill_fanout.rb` — the recipe
1834
- routed inner writes through `store.list/put/delete`, which were
1835
- removed in v0.12.2. Now uses `Operations.writes.{put,delete}` against
1836
- the `Application::Context` that hooks actually receive.
1837
- - Updated `spec/examples/skill_fanout_hook_spec.rb` to test the recipe
1838
- against a Context-like duck type, matching the runtime contract.
1839
-
1840
- ## 0.12.5 — 2026-05-26
1841
-
1842
- ### Documentation
1843
-
1844
- - Rewrote `docs/events.md` to use the textus/3 event names from
1845
- `Hooks::Registry::EVENTS` (`:entry_put`, `:entry_deleted`,
1846
- `:build_completed`, `:entry_renamed`, `:proposal_accepted`,
1847
- `:proposal_rejected`, `:file_published`, `:store_loaded`,
1848
- `:entry_refreshed`, `:refresh_started`, `:refresh_failed`,
1849
- `:refresh_backgrounded`). All hook examples now register against
1850
- events that actually exist.
1851
- - Rewrote `docs/zones.md` to use textus/3 vocabulary (`intake` not
1852
- `inbox`, `write_policy:` not `writable_by:`, `rules:` not
1853
- `policies:`, `runner` role not `script`). Manifest fixtures bump
1854
- to `version: textus/3`.
1855
- - Rewrote `ARCHITECTURE.md` to match the v0.12.4 layering:
1856
- `Operations` facade replaces `Composition`, `Application::Reads/Writes`
1857
- use cases replace direct `Store#get/#put` calls, `Store` is pure
1858
- infrastructure.
1859
- - `SPEC.md` Composition → Operations sweep; removed references to
1860
- deleted Store delegators.
1861
- - `docs/recipe-github-skill-bundle.md` updated to use the
1862
- `Operations.writes.{put,delete}` inner-write surface.
1863
-
1864
- ## [0.12.4] — 2026-05-26
1865
-
1866
- ### Breaking
1867
-
1868
- - Removed `Textus::Store#list`, `#where`, `#deps`, `#rdeps`, `#published`,
1869
- `#stale`, `#validate_all`, `#uid`, `#schema_envelope`, and `#fire_event`.
1870
- Use `Textus::Operations.for(store).reads.<name>.call(...)` instead.
1871
- - Removed `Textus::Store::Writer#delete`, `#accept`, `#reject`. Use
1872
- `Textus::Operations.for(store, role:).writes.<verb>.call(...)`.
1873
- - Removed `Textus::Store::Mover`. The mv use case lives entirely in
1874
- `Textus::Application::Writes::Mv` now.
1875
-
1876
- ### Added
1877
-
1878
- - `Textus::Application::Reads::{List,Where,Uid,SchemaEnvelope,Deps,Rdeps,Published,Stale,ValidateAll}`.
1879
- - `Textus::Application::Writes::Reject`.
1880
- - `Textus::Application::Context.system(store)` for infrastructure-side
1881
- hook dispatch.
1882
-
1883
- ### Internal
1884
-
1885
- - Internal call sites (`Projection`, `Schema::Tools`, `Application::Refresh::All`,
1886
- `Doctor::Check::SchemaViolations`) now route reads through `Operations`.
1887
-
1888
- See [ADR 0005](docs/architecture/decisions/0005-store-facade-final-removal.md).
1889
-
1890
- ## [0.12.3] — 2026-05-26
1891
-
1892
- ### Added
1893
- - `textus intro` output now includes an `agent_protocol` block: envelope shape, role-resolution rules, and four canonical recipes (`read`, `write`, `propose`, `refresh`). One `textus intro` call is sufficient orientation for a fresh AI agent to operate the store without consulting `SPEC.md`.
1894
-
1895
- ### Compatibility
1896
- - Fully additive. All pre-0.12.3 fields on `Textus::Intro.run` retain their existing keys, types, and shapes. The wire `"protocol"` field continues to hold the string `"textus/3"`.
1897
-
1898
- ### Examples
1899
- - `examples/claude-plugin/.textus/templates/claude-root.mustache` now projects the new `agent_protocol` recipes into the rendered `CLAUDE.md` via the existing `inject_intro:` mechanism. A plugin's CLAUDE.md auto-projects the four recipes alongside zone authority — agents reading CLAUDE.md get full orientation without a separate `textus intro` call. This is the canonical pattern for plugin authors who want recipes inline.
1900
- - `examples/claude-plugin/` trimmed to one of each surface (one agent, one skill, one command, one identity entry, one JSON output). Removed: the duplicate `fact-checker` variants, the `marketplace.json` projection (the JSON-output lesson is already shown by `plugin.json`), the degenerate `local_file` intake demo (pulled a file from the same store), and unused demo hooks (`rank_by_recency`, `build-stamp`). File count drops from 51 to 33; manifest from 142 to 99 lines.
1901
-
1902
- ## [0.12.2] — 2026-05-26
1903
-
1904
- ### Breaking changes
1905
-
1906
- - **Removed `Textus::Composition`.** All call sites now go through
1907
- `Textus::Operations.for(store, role:)`. The new facade groups use-cases
1908
- by kind: `ops.writes.put`, `ops.reads.get`, `ops.refresh.worker`, etc.
1909
- No alias, no deprecation warning — internal callers update on upgrade.
1910
- - **Removed `Store#put / #get / #delete / #accept / #reject / #mv`.** These
1911
- were thin shims over `Composition`. Use `Operations` directly.
1912
- - **Removed `Writer#put`** (the explicit "Backward-compat shim").
1913
-
1914
- ### Internal
1915
-
1916
- - Added `Application::Writes::Mv` use-case wrapping `Store::Mover`.
1917
- - Internal Application callers (Accept, Build, Refresh::Worker, Refresh::All,
1918
- Reads::Blame) no longer re-enter via the top-level facade.
1919
- - Audited `spec/` and removed redundant examples (~-48 LOC; most redundancy
1920
- was already absorbed by call-site migration in earlier tasks).
1921
-
1922
- See [ADR 0004](docs/architecture/decisions/0004-operations-rename-and-store-facade-removal.md).
1923
-
1924
- ### Migration
1925
-
1926
- There is no migration path for `Composition` and the Store facade methods —
1927
- they were internal. External consumers (hooks, custom verbs, gem embedders)
1928
- that referenced these symbols must update:
1929
-
1930
- ```ruby
1931
- # Before
1932
- ctx = Textus::Composition.context(store, role: "agent")
1933
- Textus::Composition.writes_put(ctx).call(key, body: "...")
1934
- # or:
1935
- store.put(key, body: "...", as: "agent")
1936
-
1937
- # After
1938
- Textus::Operations.for(store, role: "agent").writes.put.call(key, body: "...")
1939
- ```
1940
-
1941
- ## 0.12.1 — textus/2 hint fix (2026-05-26)
1942
-
1943
- ### Fixed
1944
- - Manifest parser now points textus/2 stores at the 0.11.x stepping-stone
1945
- migrator instead of the misleading "check YAML frontmatter for syntax errors"
1946
- hint. The protocol_version doctor check carried the correct hint already, but
1947
- was unreachable on textus/2 stores because `Store.discover` → `Manifest.load`
1948
- raises before doctor checks run. Surfaced by v0.12.0 release smoke testing.
1949
-
1950
- ## 0.12.0 — legacy sweep (2026-05-25)
1951
-
1952
- ### Removed (breaking)
1953
- - `Role::LEGACY_RENAMES` (`ai`/`script`/`build` → friendly error). Legacy role
1954
- names now fail with the generic `InvalidRole` error.
1955
- - `Manifest::LEGACY_ZONE_RENAMES` (`inbox` → friendly error).
1956
- - `Hooks::Registry::LEGACY_EVENT_RENAMES` (14 legacy event names → friendly
1957
- error). Legacy events now fail with `unknown event: <name>`.
1958
- - `CLI::LEGACY_VERB_RENAMES` / `CLI::LEGACY_GROUP_RENAMES` and the
1959
- `CommandRenamed` error class.
1960
- - `textus migrate --to=textus/3` verb and `lib/textus/migration/**` (eight
1961
- files, ~924 lines).
1962
- - Eight ad-hoc legacy-key guards in `manifest.rb` / `manifest/entry.rb` /
1963
- `manifest/rules.rb`.
1964
-
1965
- ### Added
1966
- - `Manifest::Schema.validate!` — strict-unknown-keys parser. Manifests with
1967
- any unrecognized key fail uniformly with `unknown key 'X' at '<jsonpath>'`.
1968
- - ADR 0003 documenting the sweep and the 0.11.x stepping-stone path.
1969
-
1970
- ### Changed
1971
- - `Doctor::Check::ProtocolVersion` hint no longer suggests `textus migrate`
1972
- (the verb is gone); points at 0.11.x docs instead.
1973
- - Test suite consolidated: five batches of disciplined deletions/merges
1974
- (−4 files, −134 LOC from the post-P6 peak). Net effect across the release:
1975
- test suite grew +8.2% LOC to cover new behavior (schema walker, permissive
1976
- audit-log tolerance).
1977
-
1978
- ### Migration
1979
- - **From textus/2 (gem ≤0.10.x):** install textus 0.11.x first; run
1980
- `textus migrate --to=textus/3`; then upgrade to 0.12.0.
1981
- - **From 0.11.x:** drop-in upgrade.
1982
-
1983
- ## 0.11.0 — textus/3 vocabulary redesign (2026-05-25)
1984
-
1985
- **BREAKING:** Protocol bumps to `textus/3`. Stores authored on 0.10.x must run `textus migrate --to=textus/3` before installing 0.11.0. `textus doctor` refuses to operate on un-migrated stores.
1986
-
1987
- ### Renamed — actors
1988
-
1989
- - `ai` → `agent`, `script` → `runner`, `build` → `builder`. `Role.resolve` rejects legacy names with a one-line migration hint pointing at `--as=<new>`.
1990
-
1991
- ### Renamed — zone
1992
-
1993
- - `inbox` → `intake`. Directory rename + key prefix update + manifest field handled by the migrator.
1994
-
1995
- ### Renamed — manifest schema
1996
-
1997
- - `writable_by:` → `write_policy:`; new explicit `read_policy:` on zones (default `[all]`).
1998
- - `policies:` (top-level) → `rules:`. Class rename: `Manifest::Policies` → `Manifest::Rules`.
1999
- - `projection:` and `generator:` unified under `compute: { kind: projection|external, ... }`.
2000
- - `reduce:` (inside compute/projection) → `transform:`.
2001
- - `handler_allowlist:` → `intake_handler_allowlist:`.
2002
- - `promote_requires:` (reserved in textus/2) → `promotion: { requires: [...] }` and is now **enforced** during `textus accept`.
2003
-
2004
- ### Renamed — hook events
2005
-
2006
- - RPC: `:intake` → `:resolve_intake`, `:reduce` → `:transform_rows`, `:check` → `:validate`.
2007
- - Pub-sub (object_pasttense): `:put` → `:entry_put`, `:deleted` → `:entry_deleted`, `:built` → `:build_completed`, `:mv` → `:entry_renamed`, `:accepted` → `:proposal_accepted`, `:reject` → `:proposal_rejected`, `:published` → `:file_published`, `:loaded` → `:store_loaded`, `:refreshed` → `:entry_refreshed`, `:refresh_began` → `:refresh_started`, `:refresh_detached` → `:refresh_backgrounded`. `:refresh_failed` kept.
2008
- - DSL: single `Textus.on(event, name, **opts) { ... }`. Sugar methods (`Textus.intake`, `Textus.reduce`, `Textus.check`, etc.) and the generic `Textus.hook(...)` form removed.
2009
-
2010
- ### Renamed — CLI
2011
-
2012
- - Namespaced: `textus key mv`, `textus key normalize` (was `key migrate`), `textus rule list` (was `policy list`), `textus rule explain` (was `policy explain`), `textus refresh stale` (was `refresh-stale`).
2013
- - Top-level mutator `textus mv` removed (use `textus key mv`).
2014
- - Envelope-render flag `--format=json` → `--output=json`. Entry-level `format:` in the manifest is unchanged.
2015
- - Legacy spellings emit a `CommandRenamed` envelope (`code: "command_renamed"`); legacy flags emit `FlagRenamed`.
2016
-
2017
- ### Added
2018
-
2019
- - `textus migrate --to=textus/3`: idempotent one-shot migrator (manifest YAML rewrite, zone directory rename `inbox` → `intake`, frontmatter owner sweep across `.md`/`.json`/`.yaml`, audit-log marker, hook DSL scanner that reports old call sites).
2020
- - `textus doctor` check `protocol_version`: refuses textus/2 stores.
2021
- - `promotion.requires` predicates: `schema_valid`, `human_accept`. Enforced by `textus accept` for matching rules.
2022
-
2023
- ### Internal
2024
-
2025
- - `Manifest::Policies` → `Manifest::Rules` (class + file + accessor + doctor check).
2026
- - New errors: `Textus::BadManifest`, `Textus::CommandRenamed`, `Textus::FlagRenamed`.
2027
- - Two new domain classes under `Textus::Domain::Policy::Predicates::` for promotion gating.
2028
- - Migration toolkit under `Textus::Migration::V3::`.
2029
-
2030
- ### Migration notes for 0.10.x users
2031
-
2032
- 1. Update `Gemfile`: `gem "textus", "~> 0.11"`.
2033
- 2. `bundle update textus`.
2034
- 3. `cd` to each textus store and run `textus migrate --to=textus/3`.
2035
- 4. Review the hook-scanner findings printed at the end of the migrate output. For each call site, replace `Textus.X(:name) { ... }` with the canonical `Textus.on(:Y, :name) { ... }` per the event rename table above.
2036
- 5. Run `textus doctor` — should report `ok: true`.
2037
- 6. Commit the rewritten `.textus/` directory (manifest, audit marker, possibly renamed zone dir).
2038
-
2039
- ### Fixed
2040
-
2041
- - **`Doctor::Check::IllegalKeys` now honors `index_filename:`.** Previously the doctor walked every file and directory under a nested entry and flagged any whose basename failed the `[a-z0-9][a-z0-9-]*` segment regex — including `SKILL.md` itself and unrelated siblings like `references/foo.md`. With this fix, when an entry declares `index_filename:`, only the parent-directory segments leading to each matching index file are validated; sibling files and unrelated subtrees are not enumerated and are not flagged. `manifest.enumerate` already filtered correctly via the new glob; this brings the doctor check into parity. Two new specs in `spec/doctor_spec.rb` cover (a) `SKILL.md` is not flagged, (b) sibling `references/` files are not flagged. The pre-existing illegal-parent-segment case (e.g. `Bad_Name/SKILL.md`) still reports `key.illegal`.
2042
-
2043
- ## 0.10.5 — tech-debt cleanup + `index_filename:` + docs polish (2026-05-25)
2044
-
2045
- Patch release. One user-facing feature (`index_filename:` on nested manifest entries) plus internal refactors that remove 7 of 19 `rubocop:disable` suppressions. No protocol bump; existing manifests parse unchanged.
2046
-
2047
- ### Added
2048
-
2049
- - **Per-entry `index_filename:` on nested manifest entries.** A nested entry MAY declare `index_filename: SKILL.md` (or any other bare basename) to surface that single file per directory as the row; the row's key segments come from the directory path, and siblings are not enumerated. Lets entries project spec-mandated filenames (e.g. agentskills.io's `SKILL.md`) whose uppercase casing would otherwise be rejected by the `[a-z0-9][a-z0-9-]*` key-segment grammar. `resolve(key)` returns the index-filename path for sub-directories. Validation: requires `nested: true`, basename only (no slashes), extension must match the entry's `format:`. New spec `spec/manifest_index_filename_spec.rb`. Documented in SPEC §4.
2050
-
2051
- ### Internal
2052
-
2053
- - **`Store::Mover#call` refactor.** Replaces an 81-line method (suppressed `Metrics/AbcSize, Metrics/MethodLength`) with an 8-line orchestrator sequenced over four named private phases — `prepare_plan`, `ensure_uid!`, `perform_move`, `record_move` — coordinated through a `MovePlan` value object. The pre-read envelope is threaded separately so `MovePlan` describes only the planned operation.
2054
- - **`Store::Staleness#call` split.** Replaces a 70-line dual-loop method (the most aggressive suppression in the gem — `AbcSize, CyclomaticComplexity, MethodLength, PerceivedComplexity, BlockLength`) with a composer + two single-purpose checks (`GeneratorCheck`, `IntakeCheck`) and a private filter method on the composer. Each new unit fits default rubocop thresholds.
2055
- - **`Store::Writer` payload + ctx grouping.** Collapses `write_envelope_to_disk`'s 8 keyword args to 5 by introducing `Store::Writer::Payload = Data.define(:meta, :body, :content)` and reusing `Application::Context` for `role` + `correlation_id`. Applies the same `ctx:` pattern to the sibling `delete_envelope_from_disk`. The class-wide `Metrics/ParameterLists` disable is narrowed to a method-level disable on the `put` back-compat shim (which mirrors the user-facing `Store#put` 7-kwarg signature and cannot be changed).
2056
- - **Net suppression change:** 19 `rubocop:disable` lines → 17; 7 metric-cop suppressions removed; one `ParameterLists` disable narrowed in scope. Full suite unchanged.
2057
-
2058
- ### Documentation
2059
-
2060
- - **SPEC.md §5.2.1 added.** Documents the `generator:` field — the externally-generated-derived-entry shape (build runner produces the file; textus tracks `sources:` for staleness via `_meta.generated.at`). The field was always parsed and tested but had no spec coverage. Clarifies that textus never executes `command:` — consistent with §2 "Not an executor."
2061
- - **README.md trimmed.** Removed the duplicated "CLI verbs" and "Zones and roles" tables; readers are pointed at SPEC §5 / §9 and `docs/zones.md` for the canonical surfaces. README narrative kept.
2062
- - **docs/conventions.md** now covers both derived-entry shapes (`projection:` for declarative compute inside textus; `generator:` for external build tools) and the current intake / freshness model (top-level `policies:` + `textus refresh-stale`). Replaces a stale section that described a pre-0.4 build-runner pattern.
2063
- - **CONTRIBUTING.md** sources-of-truth pointer updated. Per-release implementation plans are kept locally by maintainers and no longer signposted in public docs.
2064
-
2065
- ## 0.10.4 — GitHub folder intake recipe + skill-bundle deferral ADR (2026-05-24)
2066
-
2067
- Patch release. Ships a working "pull a GitHub folder as a skill bundle, fan it out to derived entries" pattern as opt-in example hooks under `examples/claude-plugin/recipes/`, with hermetic specs and user-facing docs. Captures the design decision to defer first-class skill-bundle support to a future release. No `lib/` changes; CLI, wire protocol, event surface, manifest schema, and doctor checks are all unchanged.
2068
-
2069
- ### Added
2070
-
2071
- - **`examples/claude-plugin/recipes/github_folder.rb`** — `Textus.intake(:github_folder)` handler that fetches a folder from a public GitHub repo via the REST tree + blob endpoints and returns a single entry whose `content.files` is a `{ relative_path => bytes }` hash. Uses Ruby stdlib (`net/http`, `json`, `base64`); no new gem dependencies. Fetcher is injectable for testability.
2072
- - **`examples/claude-plugin/recipes/skill_fanout.rb`** — `Textus.refreshed(:skill_fanout, keys: "intake.skills.*")` listener that fans a bundle out into `vendor.skills.<slug>.*` derived entries with reconciliation: orphaned children whose source path disappeared upstream are deleted. Inner writes use `suppress_events: true` to prevent recursion.
2073
- - **`examples/claude-plugin/recipes/README.md`** — explains that files in `recipes/` are opt-in and do not auto-load (they live outside `.textus/hooks/`).
2074
- - **`docs/recipe-github-skill-bundle.md`** — end-to-end recipe: manifest snippet, copy commands, caveats (30s timeout, recursion guard, public-repos-only, hook-not-bundled-with-content).
2075
- - **`docs/architecture/decisions/0001-skill-bundle-deferral.md`** — Architecture Decision Record documenting the friction in the current primitives, the three-option design space (status quo, intake-returns-N-entries, hooks-as-content), the choice to stay at status quo, and the explicit criteria for revisiting. First entry under the new `docs/architecture/decisions/` ADR home.
2076
- - **`spec/examples/`** — new spec subdirectory with hermetic unit tests for both recipe files. Tests use captured GitHub API fixtures under `spec/examples/fixtures/`; no network access in CI.
2077
-
2078
- ### Documentation
2079
-
2080
- - The "Recipes" concept is introduced as a deliberately opt-in pattern: example code that demonstrates how to build on textus primitives without committing the core surface to the underlying responsibility. The ADR explains why this is preferable to promoting the recipe into a builtin or a new CLI verb today.
2081
-
2082
- ## 0.10.3 — Documentation refresh and legacy-code removal (2026-05-23)
2083
-
2084
- Patch release. Two pieces of work: (1) docs describe current state only — every reference to pre-0.9.2 zone names, pre-0.10.2 sentinel layout, pre-0.5 audit-log format, and other version-history annotations is stripped from user-facing docs; (2) the corresponding backward-compatibility code paths are deleted from `lib/`. The wire protocol stays `textus/2`. Callers conforming to the current SPEC are unaffected; callers carrying obsolete config now hit silent drops or parse failures instead of helpful migration messages.
2085
-
2086
- ### Removed
2087
-
2088
- - **`Doctor::Check::LegacyIntakeFields`** — deleted. Manifest parsing already rejected these fields at load; the doctor check was redundant.
2089
- - **TSV audit-log reader** in `Store::AuditLog#parse_row` and `#check_line` — pre-0.5 audit logs are no longer transparently read. Non-JSON lines surface as `invalid_json` integrity violations.
2090
- - **Legacy sibling sentinel migration** in `Infra::Publisher` and `Store::Sentinel.legacy_path` — pre-0.10.2 stores with sibling `<target>.textus-managed.json` files are no longer recognized as managed. Fix: `rm <target>.textus-managed.json && textus build`.
2091
- - **Manifest rename-migration rejections** — entries containing `source:`, `intake.fetch`, `intake.{ttl,on_stale,sync_budget_ms}`, or `projection.reducer` no longer raise migration hints. These obsolete fields are silently ignored by the parser.
2092
- - **`textus/1` helpful-message branch** in `Manifest.load` — unsupported versions now produce a single generic error.
2093
- - **`textus stale` CLI stub** — removed; calling it now returns `unknown verb: stale` like any other typo.
2094
- - **`Manifest::Entry#derived?`** alias — both internal callers now invoke `in_generator_zone?` directly.
2095
- - **Stale CLI help text** — `textus migrate {zones,policies}` (reverted in 0.9.2) and "`--format=json` accepted for back-compat" wording removed from `textus --help`.
2096
-
2097
- ### Documentation
2098
-
2099
- - **`CONTRIBUTING.md`** now points readers at SPEC / ARCHITECTURE / `docs/` / CHANGELOG as the sources of truth. Per-release implementation plans are kept locally by maintainers and are no longer signposted in public docs.
2100
- - **README, SPEC, ARCHITECTURE, docs/zones, docs/events, docs/conventions, examples/claude-plugin/README** — stripped `(0.8.2+)`, `(0.9.0+)`, `(0.9.2)`, `(v1.0)`, `(v1.1)`, `(v1.2)`, `(v0.3)` annotations from headings, parentheticals, and inline notes. Removed "Renamed in 0.9.2" / "Pre-0.9.2 stores" / "New in 0.9.0" / "Backward compatibility (v0.5)" callouts. Example code that used pre-0.9.2 zone names (`canon`, `intake`, `pending`, `derived`) now uses current names (`identity`, `inbox`, `review`, `output`).
2101
- - **`docs/events.md`** — header count corrected to "15 events: 3 RPC and 12 pub-sub" (previously read "12 events", with refresh\_\* mentioned in subtext); stale Linear manifest example updated to use top-level `policies:` block.
2102
- - **SPEC.md §10.2** — removed `legacy_intake_fields` from the builtin doctor-check list.
2103
- - **SPEC.md §11** — dropped `textus/1` back-compat acceptance from the implementation checklist; the spec no longer mentions the legacy v0.1 zone-synthesis fallback.
2104
- - **CHANGELOG entries for past releases are unchanged** — historical record stays intact.
2105
-
2106
- ### Tests
2107
-
2108
- - Removed `spec/doctor/check/legacy_intake_fields_spec.rb`.
2109
- - Removed 4 manifest-intake migration-rejection specs and 3 publisher/audit-log legacy-format specs.
2110
-
2111
- ## 0.10.2 — Doctor and store cleanup (2026-05-23)
2112
-
2113
- Patch release. Internal cleanup: extracts `Store::Sentinel`, moves audit-log integrity into `Store::AuditLog`, surfaces previously-swallowed schema parse errors, and tidies two doctor checks. No CLI, wire-protocol, or behavioral changes for plugin authors. Sentinel JSON shape changes (repo-relative paths) are forward-compatible; legacy absolute paths are still read correctly.
2114
-
2115
- ### Added
2116
-
2117
- - `Textus::Store::Sentinel` value object owning the sentinel JSON shape (`source`/`target`/`sha256`/`mode`) and the on-disk path layout. Repo-relative paths on write; legacy absolute paths still accepted on read.
2118
- - `Textus::Store::AuditLog#verify_integrity` returns line-by-line integrity violations as `{lineno, reason, detail}` hashes.
2119
- - `Textus::Schema#unowned_fields` returns field names whose spec lacks `maintained_by`.
2120
- - New doctor check `schema_parse_error` (error level) surfaces YAML parse failures on `schemas/*.yaml`. Previously these were silently rescued in `UnownedSchemaFields`, leaving operators with no signal.
2121
-
2122
- ### Changed
2123
-
2124
- - `Infra::Publisher` delegates sentinel I/O to `Store::Sentinel`. The sentinel JSON now stores repo-relative `source`/`target` so example trees can be committed without leaking author paths.
2125
- - `Doctor::Check::Sentinels` delegates parse/orphan/drift detection to `Store::Sentinel`. Drops `rubocop:disable Metrics/BlockLength`.
2126
- - `Doctor::Check::AuditLog` delegates parsing to `Store::AuditLog#verify_integrity`. Drops `rubocop:disable Metrics/BlockLength`.
2127
- - `Doctor::Check::ManifestFiles` uses `Textus::Key::Path.resolve` instead of reimplementing leaf-path math.
2128
- - `Doctor::Check::UnownedSchemaFields` uses `Schema#unowned_fields` instead of reaching into `schema.fields` and the raw `maintained_by` Hash key.
2129
- - `examples/claude-plugin/.gitignore` no longer excludes `.textus/sentinels/`. The example's sentinels are now committed with repo-relative paths.
2130
-
2131
- ### Documentation
2132
-
2133
- - `SPEC.md` builtin doctor-check list updated to include `schema_parse_error`, and brings the prose up to date with three checks shipped in 0.9.x/0.10.0 that were missing from the list (`policy_ambiguity`, `handler_allowlist`, `legacy_intake_fields`).
2134
-
2135
- ## 0.10.1 — Documentation refresh and spec hygiene (2026-05-22)
2136
-
2137
- Lightweight maintenance release: documentation refresh plus spec-suite hygiene. No `lib/` changes; no CLI, wire-protocol, or behavioral changes.
2138
-
2139
- ### Changed
2140
-
2141
- - `docs/architecture.md` deleted. `ARCHITECTURE.md` is now the single source of truth for the layered architecture. Inbound links in `docs/zones.md`, `docs/events.md`, and `textus.gemspec` updated.
2142
- - `SPEC.md` examples and CLI snippets refer to the post-0.9.2 default zone names (`identity` / `inbox` / `review` / `output`) instead of the pre-rename `canon` / `intake` / `pending` / `derived`. Prose that explicitly explains the 0.9.2 rename — including the v0.1 back-compat manifest example and the zone-rename table — is preserved.
2143
- - `README.md` `refresh-stale` examples switched to `--zone=inbox`; the `cat .textus/zones/...` example points at the `output` zone.
2144
- - `ARCHITECTURE.md` layer diagram references `Infra::Publisher` instead of bare `Publisher`.
2145
- - `docs/zones.md` references `Textus::Infra::Publisher` instead of bare `Publisher`.
2146
-
2147
- ### Testing
2148
-
2149
- - Deleted redundant `spec/proposal_spec.rb` (the same `"2026-05-19-add-bob"` fixture is covered more thoroughly by `spec/application/writes/accept_spec.rb`, the canonical post-0.9.1 home for Application-layer write tests).
2150
- - Extracted `shared_context "textus_store_fixture"` into `spec/support/fixtures.rb`; 28 specs adopt it, replacing the repeated `let(:tmp)` / `let(:root)` / `after { FileUtils.remove_entry(tmp) }` triplet. `spec/spec_helper.rb` now autoloads `spec/support/**/*.rb`.
2151
- - Fixed an `instance_variable_set(:@intake_handler, nil)` anti-pattern in `spec/refresh_spec.rb` — the "no intake declared" case now uses a manifest entry that was never given an intake handler, instead of mutating a private ivar after construction.
2152
-
2153
- ## 0.10.0 — Shim removal, signal-based zone detection, Builder extraction (2026-05-22)
2154
-
2155
- ### Breaking — Ruby API
2156
-
2157
- - `Textus::Publisher` constant removed. Use `Textus::Infra::Publisher`.
2158
- - `Textus::Store::View` class removed. Use `Textus::Application::Context`
2159
- (constructed via `Composition.context(store, role:)`).
2160
- - `Textus::Builder` class removed as a public entry point. Build logic lives
2161
- in `Textus::Application::Writes::Build`. External callers should use
2162
- `Textus::Composition.writes_build(ctx).call` instead of
2163
- `Textus::Builder.new(store).build`. The `Textus::Builder` namespace is
2164
- retained internally only for nested helpers (`Builder::Pipeline`,
2165
- `Builder::Renderer::*`).
2166
- - `Application::Context` no longer exposes `put` / `delete` / `get` / `list`
2167
- / `where` shim methods. Hook callers that receive a Context via the
2168
- `store:` hook keyword must call `ctx.store.put(...)` etc., and explicitly
2169
- pass `as: ctx.role` for write operations.
2170
- - Intake handler return values must use `_meta:` for frontmatter. The
2171
- previous `frontmatter:` legacy key is no longer accepted.
2172
-
2173
- ### Fixed
2174
-
2175
- - `textus reject` and `textus refresh-stale` now work correctly for stores
2176
- that use the post-0.9.2 default zone names (`review`, `output`).
2177
- Zone-kind detection is now signal-based (driven by `writable_by:`
2178
- membership), not name-based. Stores using the pre-0.9.2 names (`pending`,
2179
- `derived`) continue to work.
2180
- - Event payloads' `store:` keyword now carries a Context whose
2181
- `correlation_id` matches the event payload's top-level `correlation_id`
2182
- key. Previously the `store:` Context received a fresh, unrelated
2183
- `correlation_id`.
2184
-
2185
- ### Added
2186
-
2187
- - `Textus::Manifest::Entry#in_generator_zone?` and `#in_proposal_zone?`
2188
- predicates. Internal `derived?` retained as an alias of
2189
- `in_generator_zone?`.
2190
- - `:built` and `:published` events now carry `correlation_id` in the
2191
- payload, matching the existing pattern on `:put` / `:deleted` /
2192
- `:accepted`.
2193
-
2194
- ### Removed
2195
-
2196
- - Legacy zone-purpose annotations for `canon` / `intake` / `pending` /
2197
- `derived` removed from `Textus::Intro::ZONE_PURPOSES`. Custom-named zones
2198
- continue to get no purpose annotation (existing behavior). Stores still
2199
- using the pre-rename default names will simply not get purpose
2200
- annotations on those zones in `textus intro` output.
2201
- - Dead code: `Textus::Manifest#validate_keys!` removed (had no callers).
2202
-
2203
- ### Internal
2204
-
2205
- - Builder logic fully extracted into `Application::Writes::Build`.
2206
- - CLI verbs now share `context_for(store)` / `resolved_role(store)`
2207
- helpers on `CLI::Verb`.
2208
- - Internal helpers in `Manifest`, `Doctor`, and `Manifest::Entry` are
2209
- properly marked private.
2210
-
2211
- ### Unchanged
2212
-
2213
- - Wire protocol stays `textus/2`. Envelope shape unchanged.
2214
- - CLI verbs, their flags, and their JSON output shape — unchanged.
2215
- - Manifest YAML schema — unchanged.
2216
- - Event names — unchanged (payload gains `correlation_id` on `:built` /
2217
- `:published`, but no existing key is removed or renamed).
2218
- - Hook DSL — unchanged in shape. The `store:` keyword still passes an
2219
- object that responds to `.get`, `.list`, `.where`. The Context's
2220
- role-aware `with_role` is the recommended construction site for hook
2221
- contexts now.
2222
-
2223
- ### Migration recipe
2224
-
2225
- ```ruby
2226
- # Hook handlers — before 0.10.0
2227
- Textus.hook(:intake, :my_hook) do |store:, config:, args:|
2228
- store.put("inbox.foo", meta: { ... }, body: "...") # used Context shim
2229
- end
2230
-
2231
- # Hook handlers — 0.10.0+
2232
- Textus.hook(:intake, :my_hook) do |store:, config:, args:|
2233
- ctx = store # rename for clarity if desired
2234
- ctx.store.put("inbox.foo", meta: { ... }, body: "...", as: ctx.role)
2235
- end
2236
-
2237
- # Intake handler returns — before 0.10.0
2238
- { frontmatter: { ... }, body: "..." } # legacy key
2239
-
2240
- # Intake handler returns — 0.10.0+
2241
- { _meta: { ... }, body: "..." } # _meta is the canonical key
2242
- ```
2243
-
2244
- If you imported the removed constants directly:
2245
-
2246
- ```ruby
2247
- # Before
2248
- Textus::Publisher # removed
2249
- Textus::Store::View # removed
2250
- Textus::Builder.new(store).build(key, ...) # removed
2251
-
2252
- # After
2253
- Textus::Infra::Publisher
2254
- Textus::Application::Context # via Composition.context(store, role:)
2255
- Textus::Composition.writes_build(ctx).call(key, ...)
2256
- ```
2257
-
2258
- ## 0.9.2 — Policies, audit verbs, zone rename (2026-05-22)
2259
-
2260
- ### Breaking — manifest YAML
2261
-
2262
- - **Top-level `policies:` block added.** Replaces entry-level `intake.ttl` and
2263
- `intake.on_stale`. Hand-edit existing manifests (see migration recipe below);
2264
- no migrator ships with 0.9.2 because the gem is pre-1.0 with no known
2265
- outside upgraders.
2266
- - **Default zone names renamed.** `canon → identity`, `intake → inbox`,
2267
- `pending → review`, `derived → output`. `working` unchanged. Hand-edit
2268
- the manifest + `mv` the zone directories (see recipe below).
2269
- - Custom-named zones are unaffected.
2270
-
2271
- ### Breaking — CLI
2272
-
2273
- - `textus stale` removed. Use `textus freshness`.
2274
-
2275
- ### Added — verbs
2276
-
2277
- - `textus freshness [--prefix=K] [--zone=Z]` — per-entry status (ttl, age,
2278
- next_due_at, status: fresh|stale|never_refreshed|no_policy).
2279
- - `textus audit [--key=K] [--zone=Z] [--role=R] [--verb=V] [--since=X]
2280
- [--correlation-id=ID] [--limit=N]` — query `.textus/audit.log`.
2281
- - `textus blame KEY` — audit rows joined with git commit metadata.
2282
- - `textus policy list` — dump effective policies.
2283
- - `textus policy explain KEY` — show per-slot winners and matching blocks.
2284
-
2285
- ### Added — domain
2286
-
2287
- - `Textus::Domain::Policy::Refresh` — ttl + on_stale value, exports to
2288
- `Domain::Freshness::Policy`. `on_stale` vocab is `warn | sync | timed_sync`
2289
- (unchanged from 0.9.0).
2290
- - `Textus::Domain::Policy::Promote` — promote_requires predicate.
2291
- - `Textus::Domain::Policy::HandlerAllowlist` — allowed intake handlers.
2292
- - `Textus::Domain::Policy::Matcher` — glob match + specificity ranking.
2293
- - `Textus::Manifest::Policies` — collection over policy blocks with
2294
- most-specific-wins resolution.
2295
-
2296
- ### Added — doctor checks
2297
-
2298
- - `policy_ambiguity` — two blocks of the same specificity matching one key.
2299
- - `handler_allowlist` — intake handler outside its policy's allowlist.
2300
- - `legacy_intake_fields` — `intake.ttl`/`intake.on_stale` still present in
2301
- raw YAML.
2302
-
2303
- ### Unchanged
2304
-
2305
- - Wire protocol stays `textus/2`. Envelope shape unchanged.
2306
- - Hook DSL, event names, role gate semantics, schema validation unchanged.
2307
- - `on_stale:` vocabulary (`warn | sync | timed_sync`) and its semantics
2308
- (return-stale / block-and-refresh / try-with-deadline) are unchanged —
2309
- policies merely change where the value lives.
2310
- - `:publish` hook (shipped 0.8.2) remains the extension point for custom
2311
- publish targets.
2312
-
2313
- ### Migration recipe (hand-edit, no migrator ships)
2314
-
2315
- ```sh
2316
- # In your existing .textus/manifest.yaml:
2317
- # 1. Rename zones[].name fields: canon→identity, intake→inbox,
2318
- # pending→review, derived→output.
2319
- # 2. Rewrite every entries[].zone and entries[].path prefix accordingly.
2320
- # 3. Move each entries[].intake.ttl / on_stale / sync_budget_ms into
2321
- # a new top-level policies:[] block keyed by the entry's exact key:
2322
- #
2323
- # policies:
2324
- # - match: "inbox.news.hn"
2325
- # refresh: { ttl: 6h, on_stale: sync }
2326
-
2327
- # On disk:
2328
- mv .textus/zones/canon .textus/zones/identity
2329
- mv .textus/zones/intake .textus/zones/inbox
2330
- mv .textus/zones/pending .textus/zones/review
2331
- mv .textus/zones/derived .textus/zones/output
2332
-
2333
- # Verify:
2334
- textus doctor
2335
- ```
2336
-
2337
- Find-and-replace tips for ad-hoc references in your own files:
2338
-
2339
- ```sh
2340
- # README snippets, CI yaml, shell scripts
2341
- sed -i.bak \
2342
- -e 's/\bcanon\b/identity/g' \
2343
- -e 's/\bintake\b/inbox/g' \
2344
- -e 's/\bpending\b/review/g' \
2345
- -e 's/\bderived\b/output/g' \
2346
- -e 's/textus stale/textus freshness/g' \
2347
- README.md CONTRIBUTING.md
2348
- ```
2349
-
2350
- ## 0.9.1 — write-path layering + request Context (2026-05-22)
2351
-
2352
- ### Changed — internal architecture (no plugin-visible impact)
2353
-
2354
- - Promoted `Store::View` to `Application::Context`. The new Context carries `store`, `role`, `correlation_id`, `clock`, and `dry_run`. It answers `can_read?(zone)` / `can_write?(zone)` via the new `Domain::Permission` value. `Store::View` remains as a deprecated alias for one release; slated for removal in 0.10.0.
2355
- - Extracted `Domain::Permission` from `Manifest#zone_writers`. Pure predicate value — `allows_read?(role)` / `allows_write?(role)`. Manifest gains `#permission_for(zone_name)` returning a `Permission`.
2356
- - Extracted write-path use cases under `Application::Writes::*`:
2357
- - `Writes::Put` (was `Store::Writer#put` orchestration)
2358
- - `Writes::Delete` (was `Store::Writer#delete` orchestration)
2359
- - `Writes::Build` (was `Builder#build` orchestration)
2360
- - `Writes::Accept` (was `Proposal.accept`)
2361
- - `Writes::Publish` (was direct calls to `Publisher.publish`)
2362
- - `Store::Writer#put` and `#delete` reduced to pure I/O (`#write_envelope_to_disk`, `#delete_envelope_from_disk`). The original methods remain as backward-compat shims that delegate to the use cases.
2363
- - `Builder`, `Proposal`, `Publisher` become thin shims. `Publisher` also moved to `Textus::Infra::Publisher` (its prior location remains as an alias).
2364
- - `Store#get`, `#put`, `#delete` reduced to 2-line shims through the new `Composition` module. `Store` itself no longer imports from `Application::*`.
2365
- - New `Composition` factory module wires Contexts and use cases. CLI verbs construct via `Composition.context(store, role:)` then `Composition.<use_case>(ctx).call(...)`.
2366
- - `Refresh::Worker` and `Refresh::Orchestrator` migrated to take `Context` instead of `store:` + `as:`.
2367
-
2368
- ### Added
2369
-
2370
- - Every event payload now includes `correlation_id` — a UUID generated once per Context. Hook authors can use this to correlate events within a single request (e.g., a `:refreshed` event and a downstream `:built` event share an ID).
2371
-
2372
- ### Deprecated
2373
-
2374
- - `Textus::Store::View` — use `Textus::Application::Context`. Removed in 0.10.0.
2375
- - `Textus::Publisher` — use `Textus::Infra::Publisher` or `Textus::Application::Writes::Publish`. Removed in 0.10.0.
2376
-
2377
- ### Unchanged
2378
-
2379
- - Plugin DSL, manifest YAML schema, CLI verb JSON output, envelope fields, event names, wire protocol — all identical to 0.9.0.
2380
- - No migration needed for plugin authors.
2381
-
2382
- ## 0.9.0 — intake, event standardization, read-time freshness, layered architecture (2026-05-22)
2383
-
2384
- ### Breaking — manifest schema
2385
- - The `source:` block is renamed to `intake:`. Its inner `fetch:` is renamed to `handler:`. Other inner fields (`config:`, `ttl:`) keep their names.
2386
- - Loading a manifest that still uses `source:` raises a clear migration error.
2387
-
2388
- ### Breaking — event names (pub-sub bus)
2389
- - `:fetch` → `:intake` (RPC)
2390
- - `:delete` → `:deleted`
2391
- - `:refresh` → `:refreshed`
2392
- - `:build` → `:built`
2393
- - `:publish` → `:published`
2394
- - `:accept` → `:accepted`
2395
- - `:put`, `:mv`, `:reject`, `:loaded` are unchanged (already past tense).
2396
-
2397
- ### Breaking — DSL sugar
2398
- - `Textus.fetch(:name)` → `Textus.intake(:name)`
2399
- - `Textus.refresh`, `.build`, `.publish`, `.delete`, `.accept` rename to past-tense equivalents.
2400
- - The primitive `Textus.hook(event, name)` is unchanged — event symbols update per above.
2401
-
2402
- ### Added — read-time freshness
2403
- - Every entry's manifest may declare `intake.on_stale: warn | sync | timed_sync` (default `warn`).
2404
- - `warn` — return stale envelope with `stale: true`, `stale_reason: "…"`; no refresh.
2405
- - `sync` — refresh inline, return fresh envelope.
2406
- - `timed_sync` — attempt sync up to `sync_budget_ms` (default 500ms); if exceeded, fork+detach a child to complete the refresh and return stale + `refreshing: true` to the caller. Unix only; on Windows falls back to `warn`.
2407
- - New envelope fields on `textus get`: `stale`, `stale_reason`, `refreshing`.
2408
-
2409
- ### Added — refresh lifecycle events
2410
- - `:refresh_began { key, mode }` fires when refresh begins.
2411
- - `:refresh_failed { key, error_class, error_message }` fires on intake errors.
2412
- - `:refresh_detached { key, started_at, budget_ms }` fires when timed_sync gives up waiting and forks.
2413
-
2414
- ### Added — actuator
2415
- - `textus refresh-stale [--prefix=KEY] [--zone=Z]` — refreshes every entry whose TTL has expired. Returns `{ refreshed, failed, skipped }` JSON. Exits non-zero on any failure. Intended for cron / CI.
2416
-
2417
- ### Added — doctor check
2418
- - `textus doctor` now verifies every manifest `intake.handler:` resolves to a registered `Textus.intake(:name)`, reports missing handlers as errors, and orphan registrations as warnings.
2419
-
2420
- ### Changed — internal architecture (no plugin-visible impact)
2421
- - Internals reorganized into four layers: `domain/` (pure values), `application/` (use cases), `infra/` (adapters), and `cli/` (interface). Plugin DSL, manifest schema, CLI verbs, and envelope shape are unchanged.
2422
- - `Freshness` is split into `Domain::Freshness::Evaluator` (pure) + `Domain::Freshness::Policy#decide` (data-driven) + `Application::Refresh::Orchestrator` (effects).
2423
- - `Refresh.call` is now a one-line shim over `Application::Refresh::Worker.run`. `Refresh::Lock` and `Refresh::Detached` moved to `Infra::Refresh`.
2424
- - `Store#get` now routes through `Application::Reads::Get`. `Store::Reader#get` was reduced to pure I/O (`#read_raw_envelope`) — third-party code calling it directly should switch to `Store#get` for freshness annotations.
2425
-
2426
- ### Unchanged
2427
- - Wire protocol stays `textus/2`.
2428
- - `:reduce`, `:check`, `:put` unchanged.
2429
- - The recursive `hooks/**/*.rb` loader from 0.8.2.
2430
-
2431
- ### Migration
2432
- 1. In `.textus/manifest.yaml`, replace every `source:` with `intake:` and every `fetch:` inside it with `handler:`. No other inner-field renames needed.
2433
- 2. In hook files, replace `Textus.fetch(:name)` with `Textus.intake(:name)` and the other five pub-sub sugar names with their past-tense equivalents.
2434
- 3. (Optional) Add `on_stale: timed_sync` to entries where you want self-healing reads.
2435
- 4. Wire `textus refresh-stale` into cron / GH Actions for scheduled freshness.
2436
- 5. If you subscribed to the `:refresh_started` event during 0.9.0 betas, rename your handler to `Textus.refresh_began(:name)`.
2437
- 6. If you called `Textus::Refresh::Lock` or `Textus::Refresh::Detached` directly (you probably did not), update to `Textus::Infra::Refresh::Lock` / `Textus::Infra::Refresh::Detached`.
2438
-
2439
- ## 0.8.3 — :mv, :reject, :loaded events (2026-05-22)
2440
-
2441
- ### Added
2442
- - New `:mv` event — fires after a successful `store.mv`. Payload:
2443
- `{ key:, from_key:, to_key:, envelope: }` where `key:` equals `to_key:`
2444
- so `keys:` glob filters route against the entry's post-move home.
2445
- `:put` and `:delete` remain suppressed for renames; `:mv` is the sole signal.
2446
- - New `:reject` event + `store.reject(pending_key, as: "human")` +
2447
- `textus reject KEY --as=human` CLI verb. Counterpart to `:accept` —
2448
- explicitly discards a proposal. Fires `:delete` then `:reject`.
2449
- - New `:loaded` event — fires exactly once at the tail of `Store#initialize`,
2450
- after all hooks are registered and reader/writer are built. Use for cache
2451
- warmups and one-shot setup. Payload: `store:` only.
2452
-
2453
- ## 0.8.2 — Hook DSL sugar + :publish event (2026-05-22)
2454
-
2455
- ### Added
2456
- - Per-event hook sugar: `Textus.fetch`, `.reduce`, `.check`, `.put`,
2457
- `.delete`, `.refresh`, `.build`, `.accept`, `.publish`. Each takes
2458
- `(name, **opts, &blk)` and delegates to the existing registry. Block
2459
- signatures are per-event (use `**` to absorb unused kwargs).
2460
- - New `:publish` pub-sub event. Fires once per file written to a repo
2461
- path (both for the fixed-list `publish_to:` case and the `publish_each:`
2462
- per-leaf case). Payload: `{ key:, envelope:, source:, target: }`.
2463
- Listeners can react per-file — e.g. `git add` each published file,
2464
- notify on writes, compute checksums.
2465
- - `.textus/hooks/**/*.rb` — hook files in subdirectories are now loaded.
2466
- Subdirectory names are organizational; the registered event and name
2467
- come from the DSL call, not the file path. Files load in alphabetical
2468
- order by full path.
2469
-
2470
- ### Unchanged
2471
- - `Textus.hook(:event, :name, &blk)` primitive — still works, still the
2472
- authoritative entry point.
2473
- - `:build` event semantics — still fires once per derived entry.
2474
- - Registry shape, dispatcher behavior, audit log, wire protocol
2475
- (`textus/2`), envelope shape.
2476
-
2477
- ### Example migrations
2478
- The bundled `examples/claude-plugin` was migrated to the new DSL
2479
- (snake_case names, sugar methods). No behavioral change; serves as the
2480
- canonical example.
2481
-
2482
- ## 0.8.1 — Terminology cleanup (2026-05-21)
2483
-
2484
- ### Breaking — intro output
2485
- - `textus intro` JSON: the `"extensions"` key is renamed to `"hooks"`. Consumers
2486
- reading `env["extensions"]` must switch to `env["hooks"]`. Wire protocol
2487
- remains `textus/2`; envelope shape on read/write is unchanged.
2488
-
2489
- ### Internal Ruby renames
2490
- - `Textus::Store#load_extensions` → `Textus::Store#load_hooks`.
2491
- - `Textus::Intro.extensions_for` → `Textus::Intro.hooks_for`.
2492
- - Error string `"failed loading extension <file>"` → `"failed loading hook <file>"`.
2493
-
2494
- ### Fixed
2495
- - `textus doctor` `:check`-hook failure hint pointed to `.textus/extensions/`,
2496
- which has never existed in 0.6+. Now correctly points to `.textus/hooks/`.
9
+ ## v0.56.0 2026-07-06
10
+
11
+ ### Features
12
+
13
+ - add Knowledge/Scratchpad handlers, remove old Human/Agent stubs
14
+ - add HandlerContextrestricted container view for handlers
15
+ - add Session class with method_missing, replace Store in CLI/MCP surfaces
16
+ - add Session class, replace Store facade in CLI and MCP surfaces
17
+ - add Gate::Builtin with 20 internal handler methods
18
+ - add Proposal::Handlers with propose/accept/reject/diff
19
+ - wire propose/accept/reject/diff verbs to Human lane
20
+ - add Handlers.diff for proposal diff preview
21
+ - add Handlers.reject for proposal rejection
22
+ - add Handlers.accept for proposal acceptance with dry-run
23
+ - add Handlers.propose for proposal creation
24
+ - wire ingest verb to Ingest lane via VerbSpec lane: field
25
+ - rebuild Handlers with real ingest orchestration
26
+ - rebuild Resolver with real supersede logic
27
+ - add EntryTypes sub-modules for link/asset/text content builders
28
+ - add IndexRebuilder for event store index rebuild
29
+ - add Dedup for content hash and URL duplicate detection
30
+ - add KeyBuilder for key derivation and content hashing
31
+ - Gate dispatches directly to lane handlers when VerbSpec has lane
32
+ - add Lane.handler_for to resolve lane name to handler module
33
+ - add lane: field to VerbSpec
34
+ - add Store::Index::Lookup and Store::Index::Builder classes
35
+ - create Textus::Human with LOOP Runner, Freeform, and handlers
36
+ - create Textus::Agent with Session, Workspace, Runner, Evidence, and handlers
37
+ - create Textus::Artifact with cache, lifecycle, and handlers
38
+ - create Textus::Ingest with resolver, entry types, and handlers
39
+ - add Protocol::Manifest alias for Textus::Manifest
40
+ - move Format strategies to Protocol::Format with forwarding facades
41
+ - wire Infra::FileStore, Infra::Database, Infra::Clock in Store::Builder
42
+ - extract Infra::Locks with BuildLock and WatcherLock
43
+ - extract Infra::Clock with now method
44
+ - extract Infra::Database with table setup, FTS5, and transaction support
45
+ - extract Infra::FileStore with 17 methods matching current FileSystem interface
46
+ - add Textus::Protocol::V4 fixed lane/naming table
47
+ - add shape() DSL primitive, check_naming helper, loop-shape workflow
48
+ - add loop summary to boot orientation output
49
+ - add Loop node table to explanation concepts template
50
+ - add knowledge.feedback zone
51
+ - integrate container's writer and reader into use cases and workflow runner
52
+ - add federation sync workflow — auto-mirrors remote store entries on drain
53
+ - migrate 4 more Doctor checks to validation workflows
54
+ - migrate Doctor::RuleAmbiguity to self-contained validation workflow
55
+ - add validate step type, multi-key matching, and validation workflow POC
56
+ - enforce cross-reference chain decisions→architecture, patterns→decisions, runbooks→patterns
57
+ - add unified-dispatch, middleware-chain, handler-needs, store-builder + agent training
58
+ - enforce pipeline structure with conformance spec + agent protocol
59
+ - Trace middleware — auto-instrumented dispatch with ring buffer (Phase 5)
60
+ - verb completeness, downward layer import, port contract guards (Phase 4)
61
+ - formal port interfaces with conformance verification (Phase 3)
62
+ - extract Domain::Key, Domain::Lane, Domain::Envelope pure domain layer (Phase 2)
2497
63
 
2498
- ### Docs
2499
- - SPEC.md §5.10: "single extension verb" → "single hook verb".
2500
- - Scaffolded `.textus/hooks/README.md` no longer mixes "hook" and "extension"
2501
- terminology.
64
+ ### Bug Fixes
2502
65
 
2503
- ## 0.8.0 Folder restructure & Zeitwerk autoload (2026-05-21)
66
+ - use body for session storage, fix call forwarding
67
+ - stable uid in architecture-index — use JSON.generate instead of Hash#inspect (Ruby 3.3 vs 3.4 diverge)
68
+ - fetch-depth: 0 on docs job — git log needs full history for changelog workflow
69
+ - Container#link_edge_store shared — Engine + Workflow::Runner both record edges, RdepsEntry queries same instance (C1+C2)
2504
70
 
2505
- ### Breakinginternal Ruby renames
2506
- Internal Ruby constants renamed. No deprecation aliases; downstream code referencing internals must update directly.
2507
- - `Textus::EventBus` → `Textus::Hooks::Dispatcher`
2508
- - `Textus::HookRegistry` → `Textus::Hooks::Registry`
2509
- - `Textus::BuiltinHooks` → `Textus::Hooks::Builtin`
2510
- - `Textus::Extensions` (module) → `Textus::Hooks::Loader`
2511
- - `Textus::StoreView` → `Textus::Store::View`
2512
- - `Textus::AuditLog` → `Textus::Store::AuditLog`
2513
- - `Textus::ManifestEntry` → `Textus::Manifest::Entry`
2514
- - `Textus::KeyDistance` → `Textus::Key::Distance`
2515
- - `Textus::Path` → `Textus::Key::Path`
2516
- - `Textus::SchemaTools` → `Textus::Schema::Tools`
2517
- - `Textus::CLI::<Verb>` → `Textus::CLI::Verb::<Verb>` (all 23 verbs)
2518
- - `Textus::CLI::<Name>Group` → `Textus::CLI::Group::<Name>` (key, schema, hook)
2519
- - `Textus::Doctor::Check::Extensions` → `Textus::Doctor::Check::Hooks`
2520
- - `Hooks::Registry#initialize` keyword `bus:` renamed to `dispatcher:`.
71
+ ## v0.55.22026-06-30
2521
72
 
2522
- ### Breaking — doctor CLI surface
2523
- - `textus doctor --check=extensions` → `textus doctor --check=hooks`. The check name listed in `ALL_CHECKS` and the SPEC §10.2 enumeration changes from `"extensions"` to `"hooks"`, matching the hook subsystem rename in 0.6.
2524
- - Doctor issue `code` for broken hook files: `extension.load_failed` → `hook.load_failed`.
2525
- - Doctor::Check::Hooks now inspects `.textus/hooks/` (matches `Store#load_extensions`). Previously inspected `.textus/extensions/`, which was the pre-0.6 directory — the check was dead code on any store created with current `textus init`.
73
+ ### Features
2526
74
 
2527
- ### Added
2528
- - `Textus::Entry::Base` explicit strategy interface for entry formats. Concrete strategies inherit and override.
2529
- - `Textus::Builder::Renderer` — explicit base for output renderers.
2530
- - `Textus::Doctor::Check` — explicit base for doctor checks. Each builtin check (9 total) is now its own file under `lib/textus/doctor/check/`.
75
+ - make pipeline composition pluggable; support HandlerFactoryRegistry+Adapter via TEXTUS_PIPELINE_ADAPTER=1
76
+ - add Pipeline Adapter and HandlerFactoryRegistry for alternative composition seam
2531
77
 
2532
- ### Changed
2533
- - Per-format schema validation moved from `Store::Reader`/`Store::Writer` onto `Entry::Base#validate_against`. Reader/Writer no longer carry a `case mentry.format` switch.
2534
- - `Textus::Doctor` reduced to an orchestrator; the 9 builtin checks live under `Doctor::Check::*`.
2535
- - `lib/textus.rb` switched to Zeitwerk autoload. The manual `require_relative` tree (75 lines) is gone.
2536
- - `lib/textus/builder/renderers/` directory renamed to `renderer/` (singular) to match `Builder::Renderer::*` namespace.
78
+ ### Bug Fixes
2537
79
 
2538
- ### Migration
2539
- External code referencing the old internal constants must rename. `Textus.hook`, `Textus.with_registry`, the entire CLI surface, and the `textus/2` wire format are unchanged. The published API (`Store`, `Manifest`, `Envelope`, `Etag`, `Role`, `Error` hierarchy, `Builder`, `Doctor`, `Refresh`, `Init`, `CLI.run`) is unchanged.
80
+ - regenerate adr-log.json to include new ADR 0117
81
+ - deterministic ADR log uid across platforms; rubocop balance
2540
82
 
2541
- ## 0.7.0 — Reader/Writer split, EventBus, Builder pipeline (2026-05-21)
83
+ ## v0.53.0 — 2026-06-15
2542
84
 
2543
- ### Added
2544
- - `Textus::EventBus` is now the publish/subscribe core for lifecycle events. Embedded callers can `store.bus.subscribe(:put, :name) { ... }` outside the `.textus/hooks/` directory. Hook semantics, audit behavior, and the 2-second timeout are unchanged.
85
+ ### Features
2545
86
 
2546
- ### Changed
2547
- - Internal: extracted `Textus::Path` and `Textus::Envelope` value modules; `Manifest`, `Store`, `Staleness`, and `Builder` now share the same path/envelope construction.
2548
- - Internal: split `Textus::Store` into `Store::Reader` and `Store::Writer`. Public API unchanged. `Mover`, `Validator`, and `Staleness` now take explicit collaborators instead of the full store.
2549
- - Internal: removed `Store::Events`; replaced by the bus.
2550
- - Internal: restructured `Textus::Builder` as a step pipeline (`LoadSources → Project → Render → Write`) with one renderer per format (`markdown/text/json/yaml`). Adding a new output format is now a single-file change.
87
+ - feat(core)!: cut over zones to data and adopt watch runtime
88
+ - port the five built-in fetch handlers to Step::Fetch
89
+ - add convention-discovery Step::Loader
90
+ - add Step::Registry unifying observe bus + invocable table
91
+ - add Discovery for path -> (kind, name)
92
+ - add Step base classes for the five step kinds
93
+ - add enqueue verb — general runner bounded by the allow-list
94
+ - feat(queue)!: hard-rename reconcile verb to drain/serve; regenerate docs
95
+ - scheduler seeds TTL re-pull/sweep into serve tick
96
+ - drop source.on_write knob (materialize is async-only)
97
+ - write subscriber enqueues materialize (async-only)
98
+ - add serve daemon (tick = reclaim + serial drain)
99
+ - add jobs verb (list/retry/purge)
100
+ - add drain verb (seed + serial drain + health)
101
+ - add Jobs::Seeder (mirrors Reconcile produce_scope)
102
+ - register materialize/re-pull/sweep convergence handlers
103
+ - add manifest worker: config (pool/poll/lease_ttl/max_attempts)
104
+ - add Worker.drain_pool (N-thread, exactly-once)
105
+ - add Maintenance::Worker single-pass drain
106
+ - add Domain::Jobs::Registry closed allow-list
107
+ - add lease reclaim + cover concurrent single-claim
108
+ - add lease (atomic claim), ack, fail with dead-letter
109
+ - add Ports::Queue enqueue with dedup + ready_ids
110
+ - add Domain::Jobs::Job with stable dedup id
111
+ - add Layout.queue paths under .run/queue
2551
112
 
2552
- ## 0.6.1 — Deprecation cleanup
113
+ ### Bug Fixes
2553
114
 
2554
- ### Breaking
2555
- - Flat verb aliases promised "removed in 0.6" are now actually removed:
2556
- - `textus mv` → `textus key mv`
2557
- - `textus uid` → `textus key uid`
2558
- - `textus migrate-keys` → `textus key migrate`
2559
- - `textus schema-init` → `textus schema init`
2560
- - `textus schema-diff` → `textus schema diff`
2561
- - `textus schema-migrate` → `textus schema migrate`
2562
- - `textus schema KEY` (positional) → `textus schema show KEY`
2563
- - `textus action NAME` → `textus hook run NAME`
2564
- - `Textus::CLI::Action` class renamed to `Textus::CLI::HookRun`; file `cli/action.rb` → `cli/hook_run.rb`.
2565
- - `Textus::CLI::DeprecatedAliasMixin` module deleted (no remaining users).
2566
- - `textus migrate v2` command removed along with `Textus::MigrateV2` module and `Textus::CLI::Migrate` class. The migration was a one-line manifest rewrite (`version: textus/1` → `version: textus/2`); on-disk entry shapes never changed. To upgrade a `textus/1` manifest, edit `.textus/manifest.yaml` directly. `Manifest.load` still detects the old version and prints the exact edit in its error message.
115
+ - restore role_filter in RoleScope verb dispatch
116
+ - re-materialize dependents on delete/rename (ADR 0087 gap)
2567
117
 
2568
- ## 0.6.0Hook unification
118
+ ## v0.47.12026-06-04
2569
119
 
2570
- ### Breaking
2571
- - Four DSL verbs (`Textus.action`, `Textus.reducer`, `Textus.hook`, `Textus.doctor_check`) collapsed into one: `Textus.hook(event, name, **opts) { ... }`.
2572
- - `ExtensionRegistry` class renamed to `HookRegistry`.
2573
- - `.textus/extensions/` directory renamed to `.textus/hooks/`. No back-compat read.
2574
- - Manifest: `source.action:` → `source.fetch:` (also renames the registry event from `:action` to `:fetch` and the `ManifestEntry#action`/`#action_config` accessors to `#fetch`/`#fetch_config`).
2575
- - Manifest: `projection.reducer:` → `projection.reduce:`.
2576
- - CLI: `textus extension list` → `textus hook list`; output rows keyed by `event`+`mode` instead of `kind`.
2577
- - CLI: `textus put --action=NAME` → `textus put --fetch=NAME`.
2578
- - Pub-sub hooks gain an optional `keys:` glob filter for per-key scoping.
2579
- - Hook signatures standardized: `store:` is now mandatory and first on every hook (`:reduce` previously had no `store:`). Event-specific kwargs follow.
2580
- - `:accept` event renames `pending_key:` → `key:` to match every other lifecycle event.
2581
- - All event names are now verbs in a uniform grammar (RPC verbs `:fetch :reduce :check`; pub-sub verbs `:put :delete :refresh :build :accept`).
120
+ ### Bug Fixes
2582
121
 
2583
- ### New
2584
- - `EVENTS` metadata table on `HookRegistry` is the single source of truth for event names, argument shapes, return shapes, and failure semantics (rpc vs pubsub).
2585
- - Shape-check at registration: callable kwargs are verified against the EVENTS table at load time; mismatched signatures raise `UsageError` immediately instead of surfacing at fire time.
122
+ - make External a non-build path; parse compute.command (#169)
2586
123
 
2587
- ## 0.5.0Wire protocol `textus/2`; CLI restructure; Store split (breaking)
124
+ ## v0.43.12026-06-02
2588
125
 
2589
- This release reshapes the public surface ahead of 1.0. The wire protocol bumps to `textus/2`; the CLI grows nested subcommand groups; `Store` is decomposed into a thin facade plus four focused helpers; the audit log finally matches its documented NDJSON shape; and a pile of pre-0.4 cruft gets cut.
126
+ ### Features
2590
127
 
2591
- ### Wire protocol `textus/1` `textus/2`
128
+ - derive read_verbs from the MCP catalog; recipes reference verbs (ADR 0056) + draft entry-level desc (ADR 0054) (#150)
2592
129
 
2593
- - **Breaking:** every envelope now carries `"_meta"` instead of `"frontmatter"`. For json/yaml entries, envelope `content` no longer carries a duplicate `_meta` the metadata lives only at the envelope's top level.
2594
- - **Breaking:** `Manifest.load` refuses `textus/1` manifests with a pointer at the new migration command. On-disk file shapes are unchanged — only the manifest version string changes.
2595
- - **New:** `textus migrate v2` flips `version: textus/1` to `version: textus/2` in `.textus/manifest.yaml`. One command, no file edits.
2596
- - **Internal cleanup:** `Store#extract_uid`, `enforce_name_match!`, `serialize_for_put`, `validate_all`, and `build_envelope` no longer format-switch — metadata access is uniform. Role-authority validation now works for json/yaml entries (was markdown-only).
2597
- - **API:** `Store#put` keyword renamed from `frontmatter:` to `meta:`. Action callbacks return `_meta:` (formerly `frontmatter:`).
130
+ ## v0.43.02026-06-02
2598
131
 
2599
- ### CLI — nested subcommand groups
132
+ ### Features
2600
133
 
2601
- - **New:** `textus key {mv, uid, migrate}`, `textus schema {show, init, diff, migrate}`, `textus extension {list, run}`. Discoverable, groupable, scales.
2602
- - **Deprecated (removed in 0.6):** the flat verbs `mv`, `uid`, `migrate-keys`, `schema-init`, `schema-diff`, `schema-migrate`, `extensions`, `action` still work but emit a stderr deprecation warning. `textus schema KEY` (positional dotted-key form) keeps working via a back-compat fallback in `SchemaGroup`.
2603
- - **New:** `textus list`, `textus get`, etc. default to JSON output. `--format=json` is still accepted; non-json values still raise.
2604
- - **CLI refactor:** `lib/textus/cli.rb` shrank from 434 LOC to ~100 LOC. Every verb is now a small command-object file under `lib/textus/cli/`. Dispatch is a frozen `VERBS` hash.
134
+ - feat(publish)!: typed publish: block + remove index_filename (ADR 0052, 0053) (#148)
2605
135
 
2606
- ### Audit log true NDJSON
136
+ ## v0.42.02026-06-02
2607
137
 
2608
- - **Breaking:** `.textus/audit.log` rows are now one JSON object per line (`{"ts":..., "role":..., "verb":..., "key":..., "etag_before":..., "etag_after":...}`). Missing etags are `null`, not the string `"NULL"`.
2609
- - **Structural shape:** `from_key`, `to_key`, `uid` (mv rows) live at the top level; arbitrary contextual data goes into an `extras` sub-object that is omitted when empty.
2610
- - **Back-compat:** legacy TSV rows still parse during 0.5 — `AuditLog#last_writer_for` and `Doctor#check_audit_log` accept both formats. Legacy support removed in 0.6.
138
+ ### Features
2611
139
 
2612
- ### Doctor
140
+ - feat(publish)!: remove publish_each — collapse to two modes (ADR 0051) (#147)
2613
141
 
2614
- - **New:** `textus doctor --check=schema_violations[,name,…]` runs only the named built-in checks. The 9 built-ins are `manifest_files`, `schemas`, `templates`, `extensions`, `illegal_keys`, `sentinels`, `audit_log`, `unowned_schema_fields`, `schema_violations`. Extension checks always run.
2615
- - **Breaking:** the standalone `textus validate-all` verb is gone. Use `textus doctor --check=schema_violations` instead. The internal `Store#validate_all` Ruby method is unchanged.
142
+ ## v0.41.0 2026-06-02
2616
143
 
2617
- ### Manifest / store cleanup
144
+ ### Features
2618
145
 
2619
- - **Breaking:** `LEGACY_ZONES` fallback removed. Manifests must declare a `zones:` block explicitly (init scaffold does this).
2620
- - **Breaking:** legacy syntax errors removed for `source.parse` / `source.from` / `source.fetcher` / top-level `hooks:`. Those names were rejected with helpful errors in 0.4; in 0.5 they get the generic "unknown key" error from YAML parsing.
2621
- - **Internal:** `ManifestEntry` moved to its own file (`lib/textus/manifest_entry.rb`).
146
+ - adopt a byte-identical target instead of refusing (ADR 0050) (#144)
2622
147
 
2623
- ### Store split
148
+ ### Bug Fixes
2624
149
 
2625
- - **Internal:** `lib/textus/store.rb` shrank from 617 LOC to ~312 LOC. Four focused helpers live under `lib/textus/store/`:
2626
- - `events.rb` (31 LOC) — `fire_event` hook plumbing
2627
- - `validator.rb` (53 LOC) — `validate_all` body
2628
- - `staleness.rb` (142 LOC) — `stale` body (was 5 rubocop disables)
2629
- - `mover.rb` (118 LOC) — `mv` body
2630
- - No public-API change. `Store` facade delegates to each helper one-line.
150
+ - package the docs that moved gem shipped no architecture/conventions doc (#141)
2631
151
 
2632
- ### Migration cheat-sheet
152
+ ## v0.39.1 — 2026-06-01
2633
153
 
2634
- ```sh
2635
- # 1. Upgrade the gem
2636
- gem update textus # ≥ 0.5.0
154
+ ### Features
2637
155
 
2638
- # 2. Upgrade the store
2639
- cd /path/to/your/store
2640
- textus migrate v2 # flips manifest version
156
+ - scaffold feeds.machines.* (nested) with a local env snapshot (ADR 0043) (#128)
2641
157
 
2642
- # 3. Anything else?
2643
- # - Audit log: existing TSV rows still readable; new rows are NDJSON.
2644
- # - CLI scripts: replace `textus mv ...` with `textus key mv ...`
2645
- # (and 7 similar aliases). Old forms work through 0.5 with a stderr warning.
2646
- # - Ruby callers of Store#put: pass `meta:` instead of `frontmatter:`.
2647
- # - Anything reading envelope["frontmatter"]: read envelope["_meta"] instead.
2648
- ```
158
+ ## v0.38.0 2026-05-31
2649
159
 
2650
- ## 0.4.0 — Extension API redesign (breaking)
160
+ ### Bug Fixes
2651
161
 
2652
- - **Breaking:** `Textus.fetcher` removed. Use `Textus.action` instead. The block signature changes from `|config:, store:|` to `|config:, store:, args:|`.
2653
- - **Breaking:** Manifest field `source.fetcher` renamed to `source.action`. Legacy field is rejected with a migration error.
2654
- - **Breaking:** CLI flag `textus put --fetcher=NAME` renamed to `textus put --action=NAME`.
2655
- - **Breaking:** `BuiltinFetchers` module renamed to `BuiltinActions`.
2656
- - **Breaking:** Synthesized frontmatter key `fetched_with` renamed to `actioned_with` on `put --action`.
2657
- - **New:** `Textus.action` works in three invocation modes — intake refresh, the new `textus action NAME` verb, and `put --action`. See SPEC §5.11.
2658
- - **New:** `Textus.doctor_check(:name) { |store:| ... }` primitive; contributed checks merge into the doctor report.
2659
- - **New:** `textus action NAME [--key=val ...] [--as=ROLE]` CLI verb for invoking actions in verb mode.
2660
- - **New:** `StoreView` gains a writable mode (`writable: true, as: ROLE`); intake and verb-mode actions receive a writable view bound to the calling role.
2661
- - **New:** `extensions list` enumerates actions and doctor_checks.
162
+ - align metadata with current project (textus/3 + coordination-space description) (#109)
2662
163
 
2663
- Migration: in every `.textus/extensions/*.rb`, rename `Textus.fetcher(:x)` to `Textus.action(:x)` and add `args:` to the block signature. In every manifest, rename `source.fetcher:` to `source.action:`. In CI/scripts using `textus put --fetcher=`, switch to `--action=`.
164
+ ## v0.22.0 2026-05-28
2664
165
 
2665
- ## [0.3.0] — 2026-05-20 — Configurable store root
166
+ ### Features
2666
167
 
2667
- ### Added
168
+ - feat!: 0.21.0 — agent memory integration (boot + pulse) (#81)
2668
169
 
2669
- - `--root <path>` CLI flag and `TEXTUS_ROOT` environment variable for store
2670
- discovery. `Textus::Store.discover` now accepts an optional `root:` kwarg.
2671
- Unblocks embedding a textus store at non-default paths (e.g. nested under a
2672
- plugin directory like `plugins/<name>/.textus/`) where walking up from cwd
2673
- to find `.textus/` is undesirable or ambiguous.
170
+ ## v0.12.1 2026-05-26
2674
171
 
2675
- ### Documentation
172
+ ### Bug Fixes
2676
173
 
2677
- - SPEC.md §3.1 documents the new store-location precedence:
2678
- 1. `--root` / `root:` kwarg, 2. `TEXTUS_ROOT`, 3. cwd walk.
174
+ - IllegalKeys honors index_filename skip non-matching siblings (#38)
2679
175
 
2680
- ## [0.2.0] — 2026-05-20 — Storage rewrite, agent surface, extension DSL (BREAKING)
176
+ ## v0.10.5 — 2026-05-25
2681
177
 
2682
- This release reshapes textus from a markdown-only frontmatter store into a
2683
- multi-format, agent-introspectable context layer.
178
+ ### Features
2684
179
 
2685
- ### Breaking changes
2686
- - **Per-entry formats.** Markdown is no longer the only storage shape. Manifest
2687
- entries declare `format: markdown|json|yaml|text`; format is inferred from
2688
- the path extension when omitted. JSON/YAML entries store `_meta` at the top
2689
- level (`generated_at`, `from`, `template`, `reducer`).
2690
- - **Strict key grammar.** Segments must match `/^[a-z0-9][a-z0-9-]*$/`. No
2691
- underscores, no uppercase, no dots-in-segments. Max 8 segments × 64 chars.
2692
- Enforced at manifest load, `put`, and `mv`. Existing stores with illegal
2693
- segments migrate via `textus migrate-keys --dry-run|--write`.
2694
- - **Publisher rename.** `Textus::Symlink` is now `Textus::Publisher`. Symlink
2695
- mode is gone; publish is `FileUtils.cp` + sentinel.
2696
- - **Sentinels relocated.** `.textus-managed.json` files move from beside the
2697
- published file to `<store_root>/sentinels/<target_rel>.textus-managed.json`.
2698
- Legacy sibling sentinels are auto-migrated on next publish.
2699
- - **Init profiles removed.** `textus init --profile=…` and
2700
- `lib/textus/profiles/*.yaml` are gone. `textus init` writes a single default
2701
- manifest declaring all five SPEC zones and pre-creates `zones/<name>/`
2702
- subdirectories.
2703
- - **Extension surface (carried over from earlier 0.2 work).** `.textus/parsers/`
2704
- and `.textus/calculators/` are gone — drop `.rb` files into
2705
- `.textus/extensions/`. `Textus::Parsers` and `Textus::Calculators` modules
2706
- removed. Manifest `source.parse`/`source.from` → `source.fetcher` +
2707
- `source.config`; projection `transform:` → `reducer:`; `hooks:` → `events:`
2708
- (no `on_` prefix; `on_stale` removed entirely). CLI: `--parse=NAME` removed;
2709
- `textus hooks list` removed (use `textus extensions list --kind=hook`).
180
+ - index_filename — surface a fixed basename as the per-directory row (#36)
2710
181
 
2711
- ### Added
2712
- - **`textus intro`** — single-call orientation envelope (`zones`, `entries`,
2713
- `extensions`, `write_flows`, `cli_verbs`) for agents landing in a textus-
2714
- managed project.
2715
- - **`inject_intro: true`** flag on derived markdown/text entries — merges the
2716
- intro envelope into the template data so `CLAUDE.md` (or any boot doc) can
2717
- render an orientation preamble.
2718
- - **`textus doctor`** — health check with 8 categories (missing schemas /
2719
- templates / extensions, illegal nested keys, sentinel orphan/drift, audit log
2720
- readability, unowned schema fields). `ok: true` only when zero error-level
2721
- issues; warnings/info don't flip the bit.
2722
- - **`textus mv <old> <new>`** — same-zone, same-format rename. Preserves uid,
2723
- writes an `mv` audit row with `from_key`, `to_key`, `from_path`, `to_path`,
2724
- `uid`. `--dry-run` plans without writing.
2725
- - **`uid:`** field — 16-char hex stable identity (`SecureRandom.hex(8)`),
2726
- auto-minted on first `Store#put`, preserved across writes and moves. Lives
2727
- in frontmatter for markdown, `_meta.uid` for json/yaml. Surfaced on the
2728
- envelope.
2729
- - **`publish_each:`** template on nested entries — each leaf byte-copies to a
2730
- per-leaf target derived from `{leaf}`, `{basename}`, `{key}`, `{ext}`. Closes
2731
- the per-file publish loop for plugins that mirror `working.*` into
2732
- `agents/`, `skills/<name>/SKILL.md`, `commands/`.
2733
- - **`textus migrate-keys`** — run-once helper renaming files whose basenames
2734
- violate the new strict key grammar. `--dry-run` reports proposed renames and
2735
- collisions; `--write` applies them bottom-up and writes `migrate-keys` audit
2736
- rows.
2737
- - **Per-format strategies** under `lib/textus/entry/{markdown,json,yaml,text}.rb`
2738
- with a uniform parse/serialize contract. `Entry.for_format(name)` dispatcher.
2739
- - **`textus.intro` + CLAUDE.md preamble** — the example's CLAUDE.md now opens
2740
- with auto-generated zone-and-write-flow orientation for the agent.
2741
- - **Actionable error hints.** Every `Textus::Error` exposes `code`, `message`,
2742
- and `hint`. `UnknownKey` carries up to 5 ranked "did you mean" suggestions
2743
- (shared-prefix + bounded Levenshtein). The CLI prints
2744
- `code: msg\n → hint` to stderr alongside the JSON envelope on stdout.
2745
- - **Extension surface (carried over from earlier 0.2 work).**
2746
- `Textus.fetcher`, `Textus.reducer`, `Textus.hook` DSL verbs. Per-`Store`
2747
- `ExtensionRegistry` (no global state). `textus refresh KEY --as=script`.
2748
- `textus extensions list [--kind=fetcher|reducer|hook]`. In-process lifecycle
2749
- events (`:put`, `:delete`, `:refresh`, `:build`, `:accept`). `Textus::Refresh`
2750
- driver with 2 s timeout. `Textus::StoreView` read-only proxy. Audit log gains
2751
- a 7th JSON-extras column. `Init.run` scaffolds `.textus/extensions/` with a
2752
- README stub.
182
+ ## v0.5.0 — 2026-05-21
2753
183
 
2754
- ### Fixed
2755
- - `Projection#run` no longer stamps `generated_at` onto Hash reducer results —
2756
- `_meta.generated_at` is the single source of truth for structured outputs
2757
- (avoids duplicate timestamps in `marketplace.json`-style files).
2758
- - `UnknownKey` raised from `Store#get`/`put`/`mv` (nested-tree file misses) now
2759
- carries suggestions, matching `Manifest#resolve`.
184
+ ### Features
2760
185
 
2761
- ### Example
2762
- - `examples/claude-plugin/` rewritten as a real Claude Code plugin
2763
- (`voice-tools`) entirely managed by textus: `.claude-plugin/{plugin,
2764
- marketplace}.json` and `CLAUDE.md` derived; `agents/`, `skills/<name>/
2765
- SKILL.md`, `commands/` mirrored via `publish_each:`; pending-zone walkthrough
2766
- exercising the AI propose → human accept loop; intake fetcher, in-process
2767
- reducer / hook, deep-nested key demos.
186
+ - bump to textus/2; unify _meta block across formats (#14)
187
+ - switch audit log to true NDJSON; legacy TSV reads still parse (#13)
188
+ - fold validate-all into doctor --check=schema_violations (#8)
189
+ - default --format=json (#7)
2768
190
 
2769
- ### SPEC
2770
- - Rewritten for §1–§5 (layers, manifest, formats, publish), §10 (envelope adds
2771
- `format`, `content`, `uid`), audit verb table (`mv`, `migrate-keys`), CLI
2772
- verb table, Fixture G.
191
+ ## v0.4.0 — 2026-05-20
2773
192
 
2774
- ## [0.1.0] — 2026-05-19
193
+ ### Features
2775
194
 
2776
- First public release. Implements protocol `textus/1`.
195
+ - accept --root=<path> flag before subcommand
196
+ - accept explicit root via kwarg + TEXTUS_ROOT env
2777
197
 
2778
- ### Addedstorage and model
2779
- - Zone-based storage layout under `.textus/zones/`: `canon`, `working`, `intake`,
2780
- `pending`, `derived`. Each zone declares `writable_by` roles in the manifest.
2781
- - Role resolution order: `--as` flag → `TEXTUS_ROLE` env → `.textus/role` file →
2782
- default `human`. Recognized roles: `human`, `ai`, `script`, `build`.
2783
- - Append-only TSV audit log at `.textus/audit.log`, file-locked on every write.
2784
- - Schemas with required/optional fields, type checking, and per-field
2785
- `maintained_by` ownership plus `evolution` metadata (`added_in`,
2786
- `migrate_from`).
2787
- - Manifest backwards compatibility: a manifest without `zones:` synthesizes the
2788
- legacy `fixed` / `state` / `derived` zones.
198
+ ## v0.2.02026-05-20
2789
199
 
2790
- ### Added — compute and publish
2791
- - Vendored Mustache renderer (~120 LOC, depth-bounded at 8).
2792
- - Projection engine: `select` / `pluck` / `sort_by` / `limit` / `transform`
2793
- (1000-row cap, single 2 s timeout on transforms).
2794
- - `textus build` materializes derived entries from `projection:` + `template:`
2795
- declarations.
2796
- - Atomic symlink publish via `publish_to:`, with copy-mode fallback and a
2797
- `.textus-managed.json` sentinel for filesystems without symlinks.
200
+ ### Features
2798
201
 
2799
- ### Added extension points
2800
- - **Parsers** built-ins for `json`, `csv`, `markdown-links`, `ical-events`,
2801
- `rss`. Auto-load from `.textus/parsers/<name>.rb`, 2 s timeout.
2802
- - **Calculators** pure projection-row transforms registered via
2803
- `Textus::Calculators.register`. Auto-load from `.textus/calculators/<name>.rb`,
2804
- 2 s timeout.
2805
- - **Hooks** manifest entries declare `hooks:` keyed by lifecycle event
2806
- (`on_put`, `on_delete`, `on_refresh`, `on_stale`, `on_accept`, `on_build`).
2807
- textus enumerates hooks via `textus hooks list`; external runners execute.
202
+ - textus intro verb + inject_intro builder flag
203
+ - declare all five zones and pre-create their directories
204
+ - health-check verb + actionable error hints
205
+ - publish_each for nested entries; example mirrors agents/skills/commands automatically
206
+ - stable Textus UID + textus mv preserves identity across moves
207
+ - normalize fetcher results into {frontmatter, body, content}
208
+ - per-format build pipelines + _meta injection for structured outputs
209
+ - textus migrate-keys helper for key-grammar migration
210
+ - per-entry format field + strict key grammar enforcement
211
+ - add per-format storage strategies (markdown, json, yaml, text)
212
+ - scaffold .textus/extensions/ with README
213
+ - add refresh + extensions list; rename --parse to --fetcher; remove hooks list
214
+ - validate event names against ExtensionRegistry::EVENTS
215
+ - include pending_key/target_key in event_error extras
216
+ - optional JSON extras column for event/error records
217
+ - fire :build after derived materialize and :accept after proposal
218
+ - fire :refresh with change=:created/:updated
219
+ - fire :put and :delete after successful writes
220
+ - wrap fetcher exceptions with fetcher-name context
221
+ - in-process fetcher driver with 2s timeout
222
+ - read-only proxy for extension code
223
+ - rename transform→reducer; route via store registry
224
+ - source.fetcher/config/ttl with legacy-key rejection
225
+ - port built-in parsers to BuiltinFetchers in new registry
226
+ - wrap extension load failures with filename context
227
+ - per-store ExtensionRegistry loaded from .textus/extensions/
228
+ - add Textus.fetcher/reducer/hook with thread-scoped registry
229
+ - add ExtensionRegistry with fetcher/reducer/hook slots
2808
230
 
2809
- ### Added — CLI verbs
2810
- - Read: `list`, `where`, `get`, `schema`, `stale`, `deps`, `rdeps`, `published`,
2811
- `validate-all`, `hooks list`.
2812
- - Write: `put`, `delete`, `build`, `accept`.
2813
- - Scaffolding: `init` (profiles: `personal`, `claude-plugin`), `schema-init`,
2814
- `schema-diff`, `schema-migrate`.
2815
- - Flags: `--as=ROLE`, `--zone=Z`, `--prefix=KEY`, `--parse=NAME`, `--if-etag=E`,
2816
- `--stale`, `--strict`, `--format=json`.
231
+ ### Bug Fixes
2817
232
 
2818
- ### Added examples
2819
- - `examples/claude-plugin/` a working `.textus/` tree that publishes a
2820
- Claude Code `CLAUDE.md` and a `marketplace.json` via projection + Mustache.
2821
- Demonstrates intake, parsers, calculators, hooks, and schema field ownership.
2822
- - `examples/mcp-server/` — 50-line MCP server wrapping `textus get` / `list` /
2823
- `put` as tools.
233
+ - pass suggestions on UnknownKey from get/put/mv raise sites
234
+ - drop duplicate generated_at on Hash reducer results
235
+ - deterministic extension load order via Dir.glob.sort
236
+ - hooks() read should not auto-register the queried event
2824
237
 
2825
- ### Added — quality tooling
2826
- - RuboCop config with rubocop-rspec; codebase passes with zero offenses.
2827
- - Lefthook hooks (`pre-commit` runs rubocop on staged Ruby; `pre-push` runs
2828
- rspec + rubocop). Install via `brew bundle install`.
2829
- - `gemspec` packages only `lib/`, `exe/`, `README.md`, `SPEC.md`, and two
2830
- curated `docs/` files. Internal plan documents stay out of the gem.