textus 0.49.0 → 0.51.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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +41 -43
  4. data/SPEC.md +176 -197
  5. data/docs/architecture/README.md +46 -42
  6. data/docs/reference/conventions.md +33 -28
  7. data/lib/textus/boot.rb +58 -47
  8. data/lib/textus/call.rb +1 -1
  9. data/lib/textus/cli/runner.rb +15 -10
  10. data/lib/textus/cli/verb/get.rb +1 -3
  11. data/lib/textus/cli/verb/hook_run.rb +1 -1
  12. data/lib/textus/cli/verb/put.rb +4 -20
  13. data/lib/textus/cli.rb +1 -4
  14. data/lib/textus/dispatcher.rb +1 -3
  15. data/lib/textus/doctor/check/generator_drift.rb +4 -3
  16. data/lib/textus/doctor/check/handler_allowlist.rb +1 -1
  17. data/lib/textus/doctor/check/intake_registration.rb +5 -5
  18. data/lib/textus/doctor/check/rule_ambiguity.rb +3 -3
  19. data/lib/textus/doctor/check/sentinels.rb +2 -2
  20. data/lib/textus/doctor/check/templates.rb +13 -11
  21. data/lib/textus/doctor.rb +0 -2
  22. data/lib/textus/domain/freshness/evaluator.rb +150 -14
  23. data/lib/textus/domain/freshness/verdict.rb +28 -6
  24. data/lib/textus/domain/freshness.rb +4 -33
  25. data/lib/textus/domain/policy/base_guards.rb +1 -1
  26. data/lib/textus/domain/policy/predicates/fresh_within.rb +1 -1
  27. data/lib/textus/domain/policy/publish_target.rb +34 -0
  28. data/lib/textus/domain/policy/retention.rb +29 -0
  29. data/lib/textus/domain/policy/source.rb +79 -0
  30. data/lib/textus/domain/retention/sweep.rb +57 -0
  31. data/lib/textus/domain/retention.rb +11 -0
  32. data/lib/textus/errors.rb +4 -4
  33. data/lib/textus/hooks/builtin.rb +5 -5
  34. data/lib/textus/hooks/catalog.rb +8 -7
  35. data/lib/textus/hooks/context.rb +5 -10
  36. data/lib/textus/init/templates/machine_intake.rb +4 -4
  37. data/lib/textus/init.rb +47 -47
  38. data/lib/textus/key/matching.rb +24 -0
  39. data/lib/textus/maintenance/reconcile.rb +160 -0
  40. data/lib/textus/manifest/capabilities.rb +1 -1
  41. data/lib/textus/manifest/data.rb +2 -2
  42. data/lib/textus/manifest/entry/base.rb +28 -8
  43. data/lib/textus/manifest/entry/nested.rb +3 -4
  44. data/lib/textus/manifest/entry/parser.rb +25 -21
  45. data/lib/textus/manifest/entry/produced.rb +56 -0
  46. data/lib/textus/manifest/entry/publish/subtree_mirror.rb +7 -6
  47. data/lib/textus/manifest/entry/publish/to_paths.rb +62 -11
  48. data/lib/textus/manifest/entry/validators/format_matrix.rb +3 -11
  49. data/lib/textus/manifest/entry/validators/publish.rb +3 -1
  50. data/lib/textus/manifest/entry/validators.rb +0 -1
  51. data/lib/textus/manifest/policy.rb +16 -4
  52. data/lib/textus/manifest/resolver.rb +10 -4
  53. data/lib/textus/manifest/rules.rb +37 -36
  54. data/lib/textus/manifest/schema/keys.rb +98 -0
  55. data/lib/textus/manifest/schema/validator.rb +324 -0
  56. data/lib/textus/manifest/schema/vocabulary.rb +24 -0
  57. data/lib/textus/manifest/schema.rb +27 -247
  58. data/lib/textus/manifest.rb +5 -3
  59. data/lib/textus/mcp/server.rb +9 -2
  60. data/lib/textus/ports/audit_log.rb +6 -0
  61. data/lib/textus/ports/build_lock.rb +6 -0
  62. data/lib/textus/ports/clock.rb +4 -3
  63. data/lib/textus/ports/produce_on_write_subscriber.rb +69 -0
  64. data/lib/textus/ports/publisher.rb +11 -7
  65. data/lib/textus/produce/acquire/handler.rb +29 -0
  66. data/lib/textus/produce/acquire/intake.rb +130 -0
  67. data/lib/textus/produce/acquire/projection.rb +127 -0
  68. data/lib/textus/produce/acquire/serializer/json.rb +31 -0
  69. data/lib/textus/produce/acquire/serializer/text.rb +16 -0
  70. data/lib/textus/produce/acquire/serializer/yaml.rb +31 -0
  71. data/lib/textus/produce/acquire/serializer.rb +17 -0
  72. data/lib/textus/produce/engine.rb +143 -0
  73. data/lib/textus/produce/events.rb +36 -0
  74. data/lib/textus/produce/render.rb +23 -0
  75. data/lib/textus/projection.rb +17 -6
  76. data/lib/textus/read/boot.rb +4 -2
  77. data/lib/textus/read/deps.rb +3 -3
  78. data/lib/textus/read/freshness.rb +63 -29
  79. data/lib/textus/read/get.rb +20 -102
  80. data/lib/textus/read/rdeps.rb +3 -3
  81. data/lib/textus/read/rule_explain.rb +41 -23
  82. data/lib/textus/read/rule_list.rb +25 -8
  83. data/lib/textus/read/validate_all.rb +14 -0
  84. data/lib/textus/role.rb +2 -1
  85. data/lib/textus/schemas.rb +8 -0
  86. data/lib/textus/store.rb +1 -0
  87. data/lib/textus/version.rb +1 -1
  88. data/lib/textus/write/put.rb +1 -1
  89. metadata +23 -30
  90. data/lib/textus/builder/pipeline.rb +0 -88
  91. data/lib/textus/builder/renderer/json.rb +0 -45
  92. data/lib/textus/builder/renderer/markdown.rb +0 -24
  93. data/lib/textus/builder/renderer/text.rb +0 -14
  94. data/lib/textus/builder/renderer/yaml.rb +0 -45
  95. data/lib/textus/builder/renderer.rb +0 -17
  96. data/lib/textus/cli/verb/boot.rb +0 -13
  97. data/lib/textus/cli/verb/build.rb +0 -15
  98. data/lib/textus/doctor/check/fetch_locks.rb +0 -49
  99. data/lib/textus/doctor/check/lifecycle_action_invalid.rb +0 -39
  100. data/lib/textus/domain/freshness/policy.rb +0 -18
  101. data/lib/textus/domain/lifecycle.rb +0 -83
  102. data/lib/textus/domain/outcome.rb +0 -10
  103. data/lib/textus/domain/policy/lifecycle.rb +0 -35
  104. data/lib/textus/domain/staleness/generator_check.rb +0 -109
  105. data/lib/textus/domain/staleness.rb +0 -29
  106. data/lib/textus/maintenance/tend.rb +0 -110
  107. data/lib/textus/manifest/entry/derived.rb +0 -65
  108. data/lib/textus/manifest/entry/intake.rb +0 -31
  109. data/lib/textus/manifest/entry/validators/inject_boot.rb +0 -21
  110. data/lib/textus/mcp/tools.rb +0 -14
  111. data/lib/textus/ports/fetch/detached.rb +0 -52
  112. data/lib/textus/ports/fetch/lock.rb +0 -44
  113. data/lib/textus/write/build.rb +0 -90
  114. data/lib/textus/write/fetch_events.rb +0 -42
  115. data/lib/textus/write/fetch_orchestrator.rb +0 -101
  116. data/lib/textus/write/fetch_worker.rb +0 -127
  117. data/lib/textus/write/intake_fetch.rb +0 -25
  118. data/lib/textus/write/materializer.rb +0 -51
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29a370d0ed895357e5a2e70d5f9b41542b0a4e5036472aa6f728f7a0bc459838
4
- data.tar.gz: 87bf64e383226fad74ca8534fca9f8b8de707f1e01bc910c05ad1266e5de032a
3
+ metadata.gz: 5832639952f2e74862490a982a1bed11beb0d8b46febb18d3db60ab03a7c533e
4
+ data.tar.gz: e959c0f82d1b7b2b50161f1089d3851cdf2f0452147efd80563f8c5a60299d29
5
5
  SHA512:
6
- metadata.gz: d3f2279c8660c754b64defe04aeac05e198095ef056b7a4aee534d3bd290baac7bf13d206999872a82c6bc61f723771f852bbfde2594fbaa17f80a19082df877
7
- data.tar.gz: e11ce9e704b3ea33ef94012d8425624dd3391152869d1ecb6b00e5422042a50d4c3c75612227ddd61f9ed8c10720e6f254c818e1492056a2ca3d00751500554b
6
+ metadata.gz: cdddc5128eddb2790a4a449bd53a981f18835f85fe19026a036bb95c17d9ac71bd430112e911f8c014a31cfec0a11e40874eff4fc5597353a1ed1a0844138d4b
7
+ data.tar.gz: 454407cf83ac596d39a78bd31dc4233289153e98a82dce46fdc34ec77dfa281a3637e92a7b00f5a83149d945c6a4a6bc1be59fdafe606344d530da04c13130c1
data/CHANGELOG.md CHANGED
@@ -9,6 +9,51 @@ The **gem version** (`0.x.y`) is distinct from the **protocol version**
9
9
  bump is a breaking change that requires a store migration; the gem version
10
10
  tracks both additive improvements and breaking protocol bumps independently.
11
11
 
12
+ ## 0.51.0 — 2026-06-08 — The reconcile era: one engine for produce + sweep, `source`/`retention`, produced reference docs (ADR 0087, 0090–0095, 0097)
13
+
14
+ `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.
15
+
16
+ ### Changed (breaking)
17
+
18
+ - **`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.
19
+ - **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.
20
+ - **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.
21
+ - **`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.
22
+ - **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.
23
+ - **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.
24
+
25
+ ### Added
26
+
27
+ - **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`.
28
+ - **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.
29
+
30
+ ### Changed
31
+
32
+ - **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.
33
+
34
+ ### Internal
35
+
36
+ - **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.
37
+
38
+ ## 0.50.0 — 2026-06-04 — Observability on two axes + boot lifecycle (ADR 0083, 0084, 0085)
39
+
40
+ `freshness` collapses into `pulse`, the contract-drift guard stops deadlocking, and `boot` gains a lean session-start projection shipped via a plugin.
41
+
42
+ ### Added
43
+
44
+ - **`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).
45
+ - **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.
46
+ - **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).
47
+
48
+ ### Changed
49
+
50
+ - **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.
51
+ - **`tend` returns a health summary, not full `issues[]`** (ADR 0085) — matching `pulse`; `doctor` stays the sole owner of detailed health output.
52
+
53
+ ### Removed (breaking)
54
+
55
+ - **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.
56
+
12
57
  ## 0.49.0 — 2026-06-04 — Normalize the key-verb family + remove `migrate` (ADR 0082)
13
58
 
14
59
  The single-key mutation verbs gain the `key_` family stem, and the `migrate` orchestrator is removed.
data/README.md CHANGED
@@ -1,3 +1,4 @@
1
+ <!-- Generated from .textus/zones/knowledge/readme.md — edit there, then run `textus reconcile`. Do not hand-edit README.md (it is clobbered on reconcile and flagged by doctor). ADR 0103. -->
1
2
  <p align="center">
2
3
  <picture>
3
4
  <source media="(prefers-color-scheme: dark)" srcset="assets/branding/wordmark-dark.png">
@@ -37,11 +38,9 @@ flowchart LR
37
38
  human -->|author| knowledge["knowledge<br/>(canon)"]
38
39
  agent -->|keep| notebook["notebook<br/>(workspace)"]
39
40
  agent -->|propose| proposals["proposals<br/>(queue)"]
40
- automation -->|fetch| feeds["feeds<br/>(quarantine)"]
41
- automation -->|build| artifacts["artifacts<br/>(derived)"]
41
+ automation -->|reconcile| artifacts["artifacts<br/>(machine)"]
42
42
 
43
43
  proposals ==>|human accept| knowledge
44
- feeds -.->|projection source| artifacts
45
44
  knowledge -.->|projection source| artifacts
46
45
 
47
46
  classDef actor fill:#238636,stroke:#2ea043,color:#fff;
@@ -65,23 +64,22 @@ DURABLE │ notebook │ knowledge ★ the goal
65
64
  (kept) │ agent's working truth │ canon — a human authors │
66
65
  │ durable, but low-trust │ here · the context you ship │
67
66
  ├──────────────────────────┼───────────────────────────────┤
68
- TRANSIENT │ feeds │ proposals (queue) │
67
+ TRANSIENT │ artifacts.feeds.* │ proposals (queue) │
69
68
  (staging) │ raw external input, │ a candidate, in review │
70
- │ unverified │ ▲ climbs via human accept │
69
+ │ unverified (machine) │ ▲ climbs via human accept │
71
70
  └──────────────────────────┴───────────────────────────────┘
72
71
  raw material ──── propose ────► a human accept lifts it to canon
73
72
  ```
74
73
 
75
- *(The fifth lane, `artifacts`, isn't on this grid — it's a derived **output**, computed from the lanes rather than an input climbing toward trust.)*
74
+ *(The `machine` lane's other half, `artifacts.derived.*`, isn't on this grid — it's a computed **output** projected from the lanes, not an input climbing toward trust.)*
76
75
 
77
76
  Without coordination, they overwrite each other and nothing remembers why. textus gives each actor a **lane** — called a **zone** in the manifest and CLI, the term used everywhere technical from here on — routes everything they can't write directly through a **proposals queue**, and writes every successful change to an **append-only audit log**. The lanes are enforced at the protocol level, not by convention.
78
77
 
79
78
  ```
80
79
  knowledge/ author only — who you are, what you decide, how you sound (knowledge.identity.* for identity facts)
81
80
  notebook/ keep only — agent's own durable lane (agents keep theirs; bytes climb to knowledge only via propose→accept)
82
- feeds/ fetch only — declared external inputs
83
81
  proposals/ propose (agent + human) — proposals waiting on a human accept
84
- artifacts/ build only — computed, published artifacts
82
+ artifacts/ reconcile only machine-maintained: external inputs (artifacts.feeds.*) + computed outputs (artifacts.derived.*)
85
83
  ```
86
84
 
87
85
  An agent that tries to write directly into `knowledge/` gets `write_forbidden`. It writes to `proposals/` (to change authoritative content) or its own `notebook/` (for working memory). You accept the good proposals; textus promotes them, records the move, and audits both halves. Stable per-entry `uid:` means a reorganization doesn't break references. A monotonic audit cursor (`textus pulse --since=N`) means the next session — possibly a different agent, possibly a different model — picks up exactly where the last one left off.
@@ -111,7 +109,7 @@ Try the gate the other way (`textus put knowledge.notes.X --as=agent`) and you g
111
109
 
112
110
  ## Try it
113
111
 
114
- - **Worked end-to-end store** — the role gate (propose → accept), build/publish (`CLAUDE.md` / `AGENTS.md` generated from knowledge entries), schemas, templates, and a hook: [`examples/project/`](examples/project/)
112
+ - **Worked end-to-end store** — the role gate (propose → accept), reconcile/publish (`CLAUDE.md` / `AGENTS.md` generated from knowledge entries), schemas, templates, and a hook: [`examples/project/`](examples/project/)
115
113
  - **Wire textus into Claude Code via MCP** — 4 steps, ~5 minutes: [`docs/how-to/agents-mcp.md`](docs/how-to/agents-mcp.md)
116
114
 
117
115
  ## Protocol, not just a gem
@@ -139,20 +137,19 @@ bundle exec exe/textus --help
139
137
 
140
138
  ## What `textus init` gives you
141
139
 
142
- You get `.textus/` with all five zone directories, baseline schemas, a starter manifest, and a gitignored `.run/` for disposable runtime state (the audit log, per-role cursors, fetch/build locks). Roles declare capabilities; each zone declares a `kind:`, and write authority is derived from the role's capabilities crossed with the zone's kind:
140
+ You get `.textus/` with all four zone directories, baseline schemas, a starter manifest, and a gitignored `.run/` for disposable runtime state (the audit log, per-role cursors, produce locks). Roles declare capabilities; each zone declares a `kind:`, and write authority is derived from the role's capabilities crossed with the zone's kind:
143
141
 
144
142
  ```yaml
145
143
  roles:
146
144
  - { name: human, can: [author, propose] }
147
145
  - { name: agent, can: [propose, keep] }
148
- - { name: automation, can: [fetch, build] }
146
+ - { name: automation, can: [reconcile] }
149
147
 
150
148
  zones:
151
- - { name: knowledge, kind: canon } # author — canonical truth
152
- - { name: notebook, kind: workspace } # keep — agent's own durable lane
153
- - { name: feeds, kind: quarantine } # fetch declared external inputs
154
- - { name: proposals, kind: queue } # proposeproposals awaiting accept
155
- - { name: artifacts, kind: derived } # build — computed outputs
149
+ - { name: knowledge, kind: canon } # author — canonical truth
150
+ - { name: notebook, kind: workspace } # keep — agent's own durable lane
151
+ - { name: proposals, kind: queue } # propose proposals awaiting accept
152
+ - { name: artifacts, kind: machine } # reconcileexternal inputs (artifacts.feeds.*) + computed outputs (artifacts.derived.*)
156
153
  ```
157
154
 
158
155
  ```
@@ -165,14 +162,13 @@ zones:
165
162
  zones/ # one dir per zone; kinds + capabilities are in the manifest above
166
163
  knowledge/ # e.g. identity (knowledge.identity.*), voice, decisions, notes
167
164
  notebook/
168
- feeds/
169
165
  proposals/
170
- artifacts/
166
+ artifacts/ # machine lane: feeds/ (external inputs) + derived/ (computed outputs)
171
167
  .run/ # disposable runtime state — gitignored, safe to delete (ADR 0038)
172
168
  audit/audit.log # append-only NDJSON event ledger, every write (rotates at ~50 MB)
173
169
  state/cursor.<role> # per-role pulse cursor — where `pulse --since` resumes
174
- locks/ build.lock # per-key fetch locks + the build mutex
175
- sentinels/ # publish bookkeeping (target sha) — regenerated on build (ADR 0070)
170
+ locks/ # per-key produce locks + the produce mutex
171
+ sentinels/ # publish bookkeeping (target sha) — regenerated on reconcile (ADR 0070)
176
172
  ```
177
173
 
178
174
  Manifest `path:` fields are relative to `.textus/zones/`. So `knowledge.notes.org.jane` lives at `.textus/zones/knowledge/notes/org/jane.md`.
@@ -184,20 +180,20 @@ textus get knowledge.notes.org.jane
184
180
  textus list --zone=knowledge
185
181
  printf '%s' '{"_meta":{"name":"bob","org":"acme"},"body":"hi\n"}' \
186
182
  | textus put knowledge.notes.bob --as=human --stdin
187
- textus freshness --zone=artifacts # per-entry fresh/expired/no_policy + on_expire action
183
+ textus reconcile --as=automation # re-pull stale inputs + recompute derived outputs
188
184
  textus rule list # show every rule block
189
185
  textus audit --limit=20 # query the audit log
190
186
  ```
191
187
 
192
188
  (All verbs return JSON envelopes; `--output=json` is the default and the only format in v1.)
193
189
 
194
- For a worked store — knowledge entries, a staged proposal, schemas, a template, and a build that publishes `CLAUDE.md` / `AGENTS.md` — see [`examples/project/`](examples/project/).
190
+ For a worked store — knowledge entries, a staged proposal, schemas, a template, and a `reconcile` that publishes `CLAUDE.md` / `AGENTS.md` — see [`examples/project/`](examples/project/).
195
191
 
196
192
  ## What's shipped
197
193
 
198
194
  - **Per-entry formats & publish.** `format: markdown|json|yaml|text` per entry; a typed `publish:` block (`to:` for file fan-out, `tree:` for a whole-subtree mirror) byte-copies derived files to their consumer paths. ([SPEC §5.2–5.3](SPEC.md))
199
195
  - **Stable identity.** Auto-minted `uid:` survives writes and `textus key mv`; reorganising never breaks references.
200
- - **Capability × zone-kind gate.** Writes carry `--as=<role>`; a role may write a zone iff it holds the capability the zone's `kind:` requires (`canon`→`author`, `workspace`→`keep`, `quarantine`→`fetch`, `queue`→`propose`, `derived`→`build`). The wrong role gets `write_forbidden` naming the capability needed and the roles that hold it. ([SPEC §5](SPEC.md))
196
+ - **Capability × zone-kind gate.** Writes carry `--as=<role>`; a role may write a zone iff it holds the capability the zone's `kind:` requires (`canon`→`author`, `workspace`→`keep`, `machine`→`reconcile`, `queue`→`propose`). The wrong role gets `write_forbidden` naming the capability needed and the roles that hold it. ([SPEC §5](SPEC.md))
201
197
  - **Agent loop.** `textus boot` orients a fresh session; `textus pulse --since=N` is the per-turn heartbeat (changed entries, stale keys, pending proposals). ([docs/how-to/agents-mcp.md](docs/how-to/agents-mcp.md))
202
198
  - **`textus doctor`.** Health checks across schemas, hooks, keys, sentinels, and the audit log.
203
199
 
@@ -210,13 +206,15 @@ Every command operates on one store, located in this order: `--root <path>` flag
210
206
 
211
207
  `textus boot` prints the same information for the current store: zones, entry families with schemas, registered hooks, write flows, and the verb catalog. Run it inside a store and you get the live picture; reach for the SPEC when you want the contract.
212
208
 
213
- ## Compute and publish
209
+ ## Produce and publish
214
210
 
215
- Derived entries declare `compute: { kind: projection, select: ..., pluck: ..., sort_by: ..., limit: ..., transform: name }` and either a template under `.textus/templates/` (markdown/text) or a templateless path that lets a transform hook shape the output directly (json/yaml). Projections cap at 1000 rows; the vendored Mustache subset caps at depth 8. No partials, no lambdas, no HTML escaping.
211
+ Produced entries (`kind: produced`) declare how they're acquired in one `source:` block (ADR 0093/0094); `reconcile` materialises them:
216
212
 
217
- For externally-generated entries, declare `compute: { kind: external, sources: [...] }` — textus tracks the declared sources for staleness; the build automation produces the file.
213
+ - **`source: { from: project, select: [...], pluck:, sort_by:, limit:, transform: name }`**a *projection*: textus computes the entry's data from other entries, then renders it through a template under `.textus/templates/` (markdown/text) or a templateless path that lets a transform hook shape the output directly (json/yaml). Projections cap at 1000 rows; the vendored Mustache subset caps at depth 8. No partials, no lambdas, no HTML escaping.
214
+ - **`source: { from: handler, handler: name, ttl: 1h, config: {...} }`** — *intake*: an RPC handler pulls external bytes on a `ttl` cadence; `reconcile` re-pulls when the entry goes stale.
215
+ - **`source: { from: command, sources: [...] }`** — *externally generated*: an out-of-band command writes the file; textus tracks the declared `sources` for staleness.
218
216
 
219
- Publishing is one typed `publish:` block (ADR 0052). `publish: { to: [path, ...] }` byte-copies a single derived file to one or more targets. `publish: { tree: "dir" }` on a nested entry mirrors its whole stored subtree to one target directory, preserving layout (path-driven — no keys or template variables). Sentinels for every published file live under `.textus/.run/sentinels/` (git-ignored runtime state, regenerated on build — ADR 0070). See SPEC §5.2, §5.3, §5.12.
217
+ Publishing is one typed `publish:` block (ADR 0052). `publish: { to: [path, ...] }` byte-copies a single produced file to one or more targets. `publish: { tree: "dir" }` on a nested entry mirrors its whole stored subtree to one target directory, preserving layout (path-driven — no keys or template variables). Sentinels for every published file live under `.textus/.run/sentinels/` (git-ignored runtime state, regenerated on reconcile — ADR 0070). See SPEC §5.2, §5.3, §5.12.
220
218
 
221
219
  ## Extension points
222
220
 
@@ -226,7 +224,7 @@ textus exposes a hook DSL. Drop `.rb` files into `.textus/hooks/` (subdirectorie
226
224
 
227
225
  | Event | Fires when | You return |
228
226
  |---|---|---|
229
- | `:resolve_intake` | a fetch needs bytes | `{_meta:, body:}` |
227
+ | `:resolve_handler` | an intake needs bytes | `{_meta:, body:}` |
230
228
  | `:transform_rows` | a projection builds | the reshaped rows |
231
229
  | `:validate` | `textus doctor` runs | doctor issues (or none) |
232
230
 
@@ -234,19 +232,19 @@ textus exposes a hook DSL. Drop `.rb` files into `.textus/hooks/` (subdirectorie
234
232
 
235
233
  | Event(s) | Fires when |
236
234
  |---|---|
237
- | `:entry_put` · `:entry_deleted` · `:entry_renamed` | a write lands |
238
- | `:entry_fetched` | a fetch-driven write lands |
239
- | `:build_completed` | a derived entry materializes |
240
- | `:file_published` | a derived file is copied to its target |
235
+ | `:entry_written` · `:entry_deleted` · `:entry_renamed` | a write lands |
236
+ | `:entry_fetched` | an intake-driven write lands |
237
+ | `:entry_produced` | a produced entry materializes |
238
+ | `:entry_published` | a produced file is copied to its target |
241
239
  | `:proposal_accepted` · `:proposal_rejected` | a proposal is resolved |
242
- | `:fetch_started` · `:fetch_failed` · `:fetch_backgrounded` | background-fetch lifecycle |
243
- | `:store_loaded` | the store finishes loading |
240
+ | `:entry_fetch_started` · `:entry_fetch_failed` · `:produce_failed` · `:reconcile_failed` | produce lifecycle |
241
+ | `:store_loaded` · `:session_opened` | the store loads · a role connects |
244
242
 
245
243
  ```ruby
246
244
  # Inside .textus/hooks/local_file.rb
247
245
  Textus.hook do |reg|
248
- reg.on(:resolve_intake, :local_file) do |config:, args:, **|
249
- path = config["path"] or raise "local-file requires intake.config.path"
246
+ reg.on(:resolve_handler, :local_file) do |config:, args:, **|
247
+ path = config["path"] or raise "local-file requires source.config.path"
250
248
  {
251
249
  _meta: { "last_fetched_at" => Time.now.utc.iso8601, "source_path" => path },
252
250
  body: File.read(File.expand_path(path)),
@@ -263,14 +261,14 @@ Textus.hook do |reg|
263
261
  end
264
262
  ```
265
263
 
266
- Stale intake entries refresh lazily on read: a `textus get KEY` whose matched
267
- `lifecycle` rule says `on_expire: refresh` re-pulls the source in-process before
268
- returning. To find what is due, list the expired entries:
264
+ Stale intake entries are re-pulled by `reconcile`, not by reads `get` is a pure
265
+ read that annotates the returned envelope with a freshness verdict (ADR 0089).
266
+ `reconcile` re-pulls anything past its `source.ttl` and recomputes derived outputs:
269
267
 
270
268
  ```sh
271
- textus freshness --zone=feeds --output=json # rows with status: "expired"
272
- # then read each one (read-through refresh per its lifecycle rule):
273
- textus get feeds.calendar.events --as=automation
269
+ textus reconcile --as=automation # re-pull every stale intake + recompute derived
270
+ textus reconcile artifacts.feeds --as=automation # scope to one prefix
271
+ textus get artifacts.feeds.calendar.events # a pure read; carries a freshness verdict
274
272
  ```
275
273
 
276
274
  See SPEC.md §5.10 for the full hook contract.
@@ -281,7 +279,7 @@ See [`docs/how-to/agents-mcp.md`](docs/how-to/agents-mcp.md) for the agent boot
281
279
 
282
280
  ## Examples
283
281
 
284
- [`examples/project/`](examples/project/) — textus as a project's own context store (a fictional Rails service, `ledger`). Human-authored `knowledge/` (project facts, runbooks), a staged ADR in `proposals/` showing the agent-propose / human-accept loop, schemas validating each family, a mustache template plus a `:transform_rows` hook, and a `build` that publishes the `artifacts/orientation` projection to `CLAUDE.md` and `AGENTS.md`. Includes a copy-paste adoption recipe for your own repo.
282
+ [`examples/project/`](examples/project/) — textus as a project's own context store (a fictional Rails service, `ledger`). Human-authored `knowledge/` (project facts, runbooks), a staged ADR in `proposals/` showing the agent-propose / human-accept loop, schemas validating each family, a mustache template plus a `:transform_rows` hook, and a `reconcile` that publishes the `artifacts.derived.orientation` projection to `CLAUDE.md` and `AGENTS.md`. Includes a copy-paste adoption recipe for your own repo.
285
283
 
286
284
  ## Tests
287
285