textus 0.30.0 → 0.35.1

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +2 -241
  3. data/CHANGELOG.md +113 -0
  4. data/README.md +83 -62
  5. data/SPEC.md +352 -211
  6. data/docs/conventions.md +42 -37
  7. data/lib/textus/boot.rb +89 -74
  8. data/lib/textus/cli/group/{refresh.rb → fetch.rb} +4 -4
  9. data/lib/textus/cli/verb/build.rb +1 -1
  10. data/lib/textus/cli/verb/fetch.rb +14 -0
  11. data/lib/textus/cli/verb/{refresh_stale.rb → fetch_stale.rb} +3 -3
  12. data/lib/textus/cli/verb/get.rb +1 -1
  13. data/lib/textus/cli/verb/hooks.rb +1 -1
  14. data/lib/textus/cli/verb/put.rb +1 -1
  15. data/lib/textus/cli/verb/rule_list.rb +7 -7
  16. data/lib/textus/cli.rb +2 -2
  17. data/lib/textus/container.rb +1 -2
  18. data/lib/textus/dispatcher.rb +3 -3
  19. data/lib/textus/doctor/check/{refresh_locks.rb → fetch_locks.rb} +7 -7
  20. data/lib/textus/doctor/check/proposal_targets.rb +45 -0
  21. data/lib/textus/doctor/check/rule_ambiguity.rb +3 -3
  22. data/lib/textus/doctor.rb +2 -1
  23. data/lib/textus/domain/action.rb +3 -3
  24. data/lib/textus/domain/freshness/evaluator.rb +3 -3
  25. data/lib/textus/domain/freshness/policy.rb +2 -2
  26. data/lib/textus/domain/freshness.rb +7 -7
  27. data/lib/textus/domain/outcome.rb +2 -2
  28. data/lib/textus/domain/permission.rb +2 -10
  29. data/lib/textus/domain/policy/base_guards.rb +25 -0
  30. data/lib/textus/domain/policy/evaluation.rb +18 -0
  31. data/lib/textus/domain/policy/{refresh.rb → fetch.rb} +1 -1
  32. data/lib/textus/domain/policy/guard.rb +35 -0
  33. data/lib/textus/domain/policy/guard_factory.rb +40 -0
  34. data/lib/textus/domain/policy/predicates/author_held.rb +33 -0
  35. data/lib/textus/domain/policy/predicates/etag_match.rb +32 -0
  36. data/lib/textus/domain/policy/predicates/fresh_within.rb +58 -0
  37. data/lib/textus/domain/policy/predicates/registry.rb +39 -0
  38. data/lib/textus/domain/policy/predicates/schema_valid.rb +30 -19
  39. data/lib/textus/domain/policy/predicates/target_is_canon.rb +33 -0
  40. data/lib/textus/domain/policy/predicates/zone_writable_by.rb +39 -0
  41. data/lib/textus/domain/staleness/intake_check.rb +6 -6
  42. data/lib/textus/envelope.rb +2 -2
  43. data/lib/textus/errors.rb +25 -28
  44. data/lib/textus/hooks/event_bus.rb +4 -4
  45. data/lib/textus/init.rb +23 -18
  46. data/lib/textus/maintenance/zone_mv.rb +1 -1
  47. data/lib/textus/manifest/capabilities.rb +29 -0
  48. data/lib/textus/manifest/data.rb +14 -10
  49. data/lib/textus/manifest/policy.rb +37 -21
  50. data/lib/textus/manifest/rules.rb +16 -14
  51. data/lib/textus/manifest/schema.rb +48 -58
  52. data/lib/textus/manifest.rb +3 -3
  53. data/lib/textus/mcp/server.rb +1 -1
  54. data/lib/textus/mcp/tool_schemas.rb +3 -3
  55. data/lib/textus/mcp/tools.rb +7 -7
  56. data/lib/textus/ports/audit_subscriber.rb +1 -1
  57. data/lib/textus/ports/{refresh → fetch}/detached.rb +4 -4
  58. data/lib/textus/ports/{refresh → fetch}/lock.rb +1 -1
  59. data/lib/textus/projection.rb +1 -1
  60. data/lib/textus/read/freshness.rb +9 -9
  61. data/lib/textus/read/get.rb +8 -8
  62. data/lib/textus/read/{get_or_refresh.rb → get_or_fetch.rb} +11 -11
  63. data/lib/textus/read/policy_explain.rb +14 -10
  64. data/lib/textus/read/pulse.rb +5 -4
  65. data/lib/textus/read/validator.rb +1 -1
  66. data/lib/textus/schema/tools.rb +5 -5
  67. data/lib/textus/version.rb +1 -1
  68. data/lib/textus/write/accept.rb +19 -55
  69. data/lib/textus/write/delete.rb +14 -2
  70. data/lib/textus/write/{refresh_all.rb → fetch_all.rb} +6 -6
  71. data/lib/textus/write/{refresh_orchestrator.rb → fetch_orchestrator.rb} +14 -14
  72. data/lib/textus/write/{refresh_worker.rb → fetch_worker.rb} +21 -14
  73. data/lib/textus/write/mv.rb +15 -3
  74. data/lib/textus/write/put.rb +14 -2
  75. data/lib/textus/write/reject.rb +11 -5
  76. metadata +24 -18
  77. data/lib/textus/cli/verb/refresh.rb +0 -14
  78. data/lib/textus/domain/authorizer.rb +0 -37
  79. data/lib/textus/domain/policy/predicates/accept_authority_signed.rb +0 -33
  80. data/lib/textus/domain/policy/promote.rb +0 -26
  81. data/lib/textus/domain/policy/promotion.rb +0 -57
  82. data/lib/textus/manifest/role_kinds.rb +0 -21
  83. data/lib/textus/write/authority_gate.rb +0 -24
data/README.md CHANGED
@@ -1,13 +1,20 @@
1
- # textus
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/branding/wordmark-dark.png">
4
+ <img src="docs/assets/branding/wordmark.png" alt="textus" width="360">
5
+ </picture>
6
+ </p>
2
7
 
3
- [![CI](https://github.com/patrick204nqh/textus/actions/workflows/ci.yml/badge.svg)](https://github.com/patrick204nqh/textus/actions/workflows/ci.yml)
4
- [![Gem Version](https://img.shields.io/gem/v/textus.svg)](https://rubygems.org/gems/textus)
5
- [![Ruby](https://img.shields.io/badge/ruby-%E2%89%A53.3-CC342D.svg)](https://www.ruby-lang.org/)
6
- [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+ <p align="center">
9
+ <a href="https://github.com/patrick204nqh/textus/actions/workflows/ci.yml"><img src="https://github.com/patrick204nqh/textus/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
10
+ <a href="https://rubygems.org/gems/textus"><img src="https://img.shields.io/gem/v/textus.svg" alt="Gem Version"></a>
11
+ <a href="https://www.ruby-lang.org/"><img src="https://img.shields.io/badge/ruby-%E2%89%A53.3-CC342D.svg" alt="Ruby"></a>
12
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
13
+ </p>
7
14
 
8
- **Durable, multi-writer context for codebases that humans and AI agents both touch.** Your agent forgets everything between sessions; your runbooks and `CLAUDE.md` get edited by whoever ran last; nobody can reconstruct who wrote what. textus is the memory that survives the model, the session, and the vendor — a shared workspace where humans, agents, and runners write into separate lanes, propose changes through a review queue, and leave an audit trail behind every byte.
15
+ **A coordination space for humans, AI, and automation.** Your agent forgets between sessions; your notes and `CLAUDE.md` get edited by whoever ran last; nobody can reconstruct who wrote what. textus is durable, multi-writer memory that stays current and survives the model, the session, and the vendor — you keep your space, agents keep theirs, automation keeps external data fresh, and every change crosses a review queue and an audit log.
9
16
 
10
- *textus* is Latin for "the fabric a text is woven from" — same root as *context*, from *con-texere*, "to weave together." The protocol weaves human edits, agent proposals, and runner intake into one durable fabric. The shape of that fabric is yours; the rules for writing into it are textus's.
17
+ *textus* is Latin for "the fabric a text is woven from" — same root as *context*, from *con-texere*, "to weave together."
11
18
 
12
19
  ## The idea
13
20
 
@@ -15,19 +22,31 @@ Three actors write to your repo today:
15
22
 
16
23
  - **Humans** — you, your team. Authoritative on identity, decisions, voice.
17
24
  - **Agents** — Claude, Cursor, custom assistants. Smart, fast, forgetful, and not always right.
18
- - **Runners** — cron jobs, fetchers, CI. Bring outside data in.
25
+ - **Automation** — cron jobs, fetchers, CI. Bring outside data in and compile published artifacts.
26
+
27
+ ```mermaid
28
+ flowchart LR
29
+ human(["human"]) -->|author| knowledge["knowledge<br/>(canon)"]
30
+ agent(["agent"]) -->|keep| notebook["notebook<br/>(workspace)"]
31
+ agent -->|propose| proposals["proposals<br/>(queue)"]
32
+ proposals -->|human accepts| knowledge
33
+ automation(["automation"]) -->|fetch| feeds["feeds<br/>(quarantine)"]
34
+ automation -->|build| artifacts["artifacts<br/>(derived)"]
35
+ ```
36
+
37
+ *Each actor writes only into its own lane; low-trust input climbs to authoritative lanes only by passing a guarded transition (an agent's proposal needs a human `accept`).*
19
38
 
20
- Without coordination, they overwrite each other and nothing remembers why. textus gives each actor a **lane** (a zone), routes everything they can't write directly through a **review queue**, and writes every successful change to an **append-only audit log**. The lanes are enforced at the protocol level, not by convention.
39
+ 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.
21
40
 
22
41
  ```
23
- identity/ human only — who you are, what you decide, how you sound
24
- working/ human only — day-to-day catalog (agents propose via review/, runners feed via intake/)
25
- intake/ runner only — declared external inputs
26
- review/ agent + human — proposals waiting on a human accept
27
- output/ builder only — computed, published artifacts
42
+ knowledge/ author only — who you are, what you decide, how you sound (knowledge.identity.* for identity facts)
43
+ notebook/ keep only — agent's own durable lane (agents keep theirs; bytes climb to knowledge only via propose→accept)
44
+ feeds/ fetch only — declared external inputs
45
+ proposals/ propose (agent + human) — proposals waiting on a human accept
46
+ artifacts/ build only — computed, published artifacts
28
47
  ```
29
48
 
30
- An agent that tries to write directly into `working/` or `identity/` gets `write_forbidden`. It writes to `review/` instead. 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.
49
+ 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.
31
50
 
32
51
  That's the load-bearing claim: **coordination is a protocol invariant, not a library convenience.**
33
52
 
@@ -36,19 +55,19 @@ That's the load-bearing claim: **coordination is a protocol invariant, not a lib
36
55
  ```sh
37
56
  gem install textus
38
57
  textus init # creates .textus/ with zones + schemas
39
- # agent proposes a change to review/
40
- printf '%s' '{"_meta":{"name":"oncall","proposal":{"target_key":"working.notes.oncall","action":"put"}},"body":"Patrick on call.\n"}' \
41
- | textus put review.notes.oncall --as=agent --stdin
42
- # you accept it — textus promotes to working/ and audits the move
43
- textus accept review.notes.oncall --as=human
58
+ # agent proposes a change to proposals/
59
+ printf '%s' '{"_meta":{"name":"oncall","proposal":{"target_key":"knowledge.notes.oncall","action":"put"}},"body":"Patrick on call.\n"}' \
60
+ | textus put proposals.notes.oncall --as=agent --stdin
61
+ # you accept it — textus promotes to knowledge/ and audits the move
62
+ textus accept proposals.notes.oncall --as=human
44
63
  ```
45
64
 
46
- Try the gate the other way (`textus put working.notes.X --as=agent`) and you get `write_forbidden`, with the role that *would* be allowed named in the error. That refusal is the whole point.
65
+ Try the gate the other way (`textus put knowledge.notes.X --as=agent`) and you get `write_forbidden`, with the role that *would* be allowed named in the error. That refusal is the whole point.
47
66
 
48
67
  ## Try it
49
68
 
50
69
  - **5-command worked demo** — single terminal scroll, no MCP, no schemas: [`examples/hello/`](examples/hello/)
51
- - **Wire textus into Claude Code via MCP** — 4 steps, ~5 minutes: [`INTEGRATE_WITH_CLAUDE.md`](INTEGRATE_WITH_CLAUDE.md)
70
+ - **Wire textus into Claude Code via MCP** — 4 steps, ~5 minutes: [`docs/agents-mcp.md`](docs/agents-mcp.md)
52
71
  - **Use textus as your own project's context store**: [`examples/project/`](examples/project/)
53
72
  - **Use textus to author a Claude plugin** (textus is the source-of-truth, build publishes to `agents/`, `skills/`, `commands/`): [`examples/claude-plugin/`](examples/claude-plugin/)
54
73
 
@@ -57,7 +76,7 @@ Try the gate the other way (`textus put working.notes.X --as=agent`) and you get
57
76
  This Ruby gem is the reference implementation of **`textus/3`** — a wire format and storage convention any language can speak. The protocol owns the envelope shape, the role/zone gate, the audit log format, and the key grammar. The gem version (semver, see badge) and the protocol version (`textus/3`) move independently; envelopes carry the `protocol` field so consumers can pin to the contract, not the implementation.
58
77
 
59
78
  - Specification: [`SPEC.md`](SPEC.md)
60
- - Architecture: [`ARCHITECTURE.md`](ARCHITECTURE.md)
79
+ - Architecture: [`docs/architecture/README.md`](docs/architecture/README.md)
61
80
  - Per-release notes: [`CHANGELOG.md`](CHANGELOG.md)
62
81
 
63
82
  A second implementation in another language would share the same `.textus/` directory and the same audit log. That's deliberate.
@@ -75,40 +94,50 @@ bundle install
75
94
  bundle exec exe/textus --help
76
95
  ```
77
96
 
78
- ## Quick start
97
+ ## What `textus init` gives you
79
98
 
80
- ```sh
81
- textus init
82
- ```
99
+ You get `.textus/` with all five zone directories, baseline schemas, an empty audit log, and a starter manifest. Roles declare capabilities; each zone declares a `kind:`, and write authority is derived from the role's capabilities crossed with the zone's kind:
83
100
 
84
- You get `.textus/` with all five zone directories, baseline schemas, an empty audit log, and a starter manifest:
101
+ ```yaml
102
+ roles:
103
+ - { name: human, can: [author, propose] }
104
+ - { name: agent, can: [propose, keep] }
105
+ - { name: automation, can: [fetch, build] }
106
+
107
+ zones:
108
+ - { name: knowledge, kind: canon } # author — canonical truth
109
+ - { name: notebook, kind: workspace } # keep — agent's own durable lane
110
+ - { name: feeds, kind: quarantine } # fetch — declared external inputs
111
+ - { name: proposals, kind: queue } # propose — proposals awaiting accept
112
+ - { name: artifacts, kind: derived } # build — computed outputs
113
+ ```
85
114
 
86
115
  ```
87
116
  .textus/
88
- manifest.yaml # zone declarations + key-to-path mapping
117
+ manifest.yaml # role capabilities + zone kinds + key-to-path mapping
89
118
  audit.log # append-only NDJSON, every write
90
119
  schemas/ # YAML field shapes per entry family
91
120
  templates/ # mustache templates for derived entries
92
121
  hooks/ # one .rb per hook
93
122
  sentinels/ # publish bookkeeping
94
123
  zones/
95
- identity/ # human-only — identity, voice, decisions
96
- working/ # human / agent / runner day-to-day catalog
97
- intake/ # runner — declared external inputs (actions)
98
- review/ # agent + human — proposals awaiting accept
99
- output/ # builder only — computed outputs
124
+ knowledge/ # author — identity (knowledge.identity.*), voice, decisions, notes
125
+ notebook/ # keep agent's own durable lane (agents keep theirs)
126
+ feeds/ # fetch — declared external inputs (actions)
127
+ proposals/ # propose (agent + human) — proposals awaiting accept
128
+ artifacts/ # build — computed outputs
100
129
  ```
101
130
 
102
- Manifest `path:` fields are relative to `.textus/zones/`. So `working.notes.org.jane` lives at `.textus/zones/working/notes/org/jane.md`.
131
+ Manifest `path:` fields are relative to `.textus/zones/`. So `knowledge.notes.org.jane` lives at `.textus/zones/knowledge/notes/org/jane.md`.
103
132
 
104
133
  Read and write:
105
134
 
106
135
  ```sh
107
- textus get working.notes.org.jane
108
- textus list --zone=working
136
+ textus get knowledge.notes.org.jane
137
+ textus list --zone=knowledge
109
138
  printf '%s' '{"_meta":{"name":"bob","org":"acme"},"body":"hi\n"}' \
110
- | textus put working.notes.bob --as=human --stdin
111
- textus freshness --zone=output # per-entry fresh/stale/never_refreshed/no_policy
139
+ | textus put knowledge.notes.bob --as=human --stdin
140
+ textus freshness --zone=artifacts # per-entry fresh/stale/never_fetched/no_policy
112
141
  textus rule list # show every rule block
113
142
  textus audit --limit=20 # query the audit log
114
143
  ```
@@ -119,26 +148,18 @@ For the full shape — Claude plugin with agents, skills, commands, pending walk
119
148
 
120
149
  ## What's shipped
121
150
 
122
- - **Per-entry formats.** `format: markdown | json | yaml | text` on a manifest entry. `cat .textus/zones/output/marketplace.json | jq .` works without going through textus — the in-store file *is* the consumer-shaped artifact. Structured outputs carry `_meta` at the top level (`generated_at`, `from`, `template`, `transform`).
123
- - **Per-leaf publishing.** Nested entries declare `publish_each: "skills/{basename}/SKILL.md"`. Every leaf byte-copies to its consumer location on `textus build`. No more hand-mirrored `agents/` / `skills/` / `commands/` directories.
124
- - **Build and publish in one pass.** `Textus::Write::Publish` materializes generator-zone entries and copies nested leaves to their `publish_each` targets. The `textus build` CLI verb dispatches to it; the wire envelope is unchanged.
125
- - **Typed envelopes.** `Textus::Envelope` is a `Data.define` value object with typed accessors (`.meta`, `.body`, `.etag`, `.uid`, `.freshness`, ). Ruby API callers get IDE help and `NoMethodError` on typos. The CLI JSON wire format is preserved byte-for-byte via `envelope.to_h_for_wire`.
126
- - **Stable identity (`uid:`).** 16-char hex, auto-minted on first `put`, preserved across writes and moves. `textus key mv old.key new.key` renames in place — uid survives, audit row records `from_key`, `to_key`, `uid`. Reorganising a tree no longer breaks references.
127
- - **Strict key grammar.** `/^[a-z0-9][a-z0-9-]*$/`, max 8 segments × 64 chars. `textus doctor` flags any illegal segments with a rename hint; `textus key mv old.key new.key` renames in place (uid survives).
128
- - **`textus boot`.** One-shot store orientation: zones with writers + purposes, entry families with schemas and publish targets, loaded hooks, write flows per role, the full CLI verb table, and an `agent_quickstart` block (read/write verbs, writable zones, propose zone, latest audit seq).
129
- - **`textus pulse [--since=N]`.** Per-turn heartbeat for agents: changed entries since cursor N, stale keys, pending review proposals, and a doctor summary. Cursor is a monotonic seq stamped on every audit row; rotation keeps the last 5 files (configurable via `audit:` in the manifest) and raises `CursorExpired` when the requested cursor has fallen off disk.
130
- - **`textus doctor`.** Health check across 15 checks — among them: missing schemas/templates, broken hooks, illegal nested keys, sentinel drift, audit log readability, unowned schema fields, schema violations, and missing manifest files. Returns `ok: true` only when nothing is wrong; warnings and info don't flip the bit.
131
- - **Actionable hints on every error.** `UnknownKey` carries ranked "did you mean" suggestions. `WriteForbidden` names the role that *would* be allowed. `BadFrontmatter` tells you exactly what to rename. Printed to stderr alongside the JSON envelope on stdout.
132
- - **Compute.** Derived entries declare `compute: { kind: projection, ... }` (declarative rows + template) or `compute: { kind: external, ... }` (build runner produces the file; textus tracks sources for staleness). Inside projection computes, `transform:` names the row-shaping hook.
133
-
134
- Symlink-mode publish was removed; publish is `FileUtils.cp` + sentinel. Sentinels for published files live under `.textus/sentinels/<target_rel>.textus-managed.json` so consumer directories stay clean. Legacy sibling sentinels auto-migrate on next publish.
151
+ - **Per-entry formats & publish.** `format: markdown|json|yaml|text` per entry; `publish_to:`/`publish_each:` byte-copy derived files to their consumer paths. ([SPEC §5.2–5.3](SPEC.md))
152
+ - **Stable identity.** Auto-minted `uid:` survives writes and `textus key mv`; reorganising never breaks references.
153
+ - **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))
154
+ - **Agent loop.** `textus boot` orients a fresh session; `textus pulse --since=N` is the per-turn heartbeat (changed entries, stale keys, pending proposals). ([docs/agents-mcp.md](docs/agents-mcp.md))
155
+ - **`textus doctor`.** 15 health checks across schemas, hooks, keys, sentinels, and the audit log.
135
156
 
136
157
  ## CLI and zones
137
158
 
138
- All verbs accept `--output=json` and return the envelope defined in [SPEC §8](SPEC.md). Write verbs require `--as=<role>` (role resolution: `--as` → `TEXTUS_ROLE` env → `.textus/role` file → default `human`). Recognized roles: `human`, `agent`, `runner`, `builder`.
159
+ All verbs accept `--output=json` and return the envelope defined in [SPEC §8](SPEC.md). Write verbs require `--as=<role>` (role resolution: `--as` → `TEXTUS_ROLE` env → `.textus/role` file → default `human`). Default roles: `human`, `agent`, `automation` (rename or add your own in the manifest's `roles:` block).
139
160
 
140
161
  - Full verb table — read, write, health, scaffolding — is in [SPEC §9](SPEC.md).
141
- - Zone semantics and the role/`write_policy` mapping live in [SPEC §5](SPEC.md), with a tutorial expansion in [`docs/zones.md`](docs/zones.md).
162
+ - Zone semantics and the capability × zone-kind mapping live in [SPEC §5](SPEC.md), with a tutorial expansion in [`docs/zones.md`](docs/zones.md).
142
163
 
143
164
  `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.
144
165
 
@@ -146,7 +167,7 @@ All verbs accept `--output=json` and return the envelope defined in [SPEC §8](S
146
167
 
147
168
  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.
148
169
 
149
- For externally-generated entries, declare `compute: { kind: external, sources: [...] }` — textus tracks the declared sources for staleness; the build runner produces the file.
170
+ For externally-generated entries, declare `compute: { kind: external, sources: [...] }` — textus tracks the declared sources for staleness; the build automation produces the file.
150
171
 
151
172
  `publish_to: [path]` byte-copies a single derived file to one target. `publish_each: "template/{basename}.md"` on a nested entry byte-copies every leaf to its templated target — substitutes `{leaf}`, `{basename}`, `{key}`, `{ext}`. Sentinels for every published file live under `.textus/sentinels/`. See SPEC §5.2, §5.3, §5.12.
152
173
 
@@ -157,8 +178,8 @@ textus exposes a hook DSL. Drop `.rb` files into `.textus/hooks/` (subdirectorie
157
178
  - `:resolve_intake` — bring bytes in from elsewhere (returns `{_meta:, body:}`)
158
179
  - `:transform_rows` — transform rows during projection (returns rows)
159
180
  - `:validate` — custom doctor check (returns issues)
160
- - `:entry_put`, `:entry_deleted`, `:entry_refreshed`, `:build_completed`, `:proposal_accepted`, `:file_published`, `:entry_renamed`, `:proposal_rejected`, `:store_loaded` — react to lifecycle events
161
- - `:refresh_started`, `:refresh_failed`, `:refresh_backgrounded` — background-refresh lifecycle
181
+ - `:entry_put`, `:entry_deleted`, `:entry_fetched`, `:build_completed`, `:proposal_accepted`, `:file_published`, `:entry_renamed`, `:proposal_rejected`, `:store_loaded` — react to lifecycle events
182
+ - `:fetch_started`, `:fetch_failed`, `:fetch_backgrounded` — background-fetch lifecycle
162
183
 
163
184
  ```ruby
164
185
  # Inside .textus/hooks/local_file.rb
@@ -166,7 +187,7 @@ Textus.hook do |reg|
166
187
  reg.on(:resolve_intake, :local_file) do |config:, args:, **|
167
188
  path = config["path"] or raise "local-file requires intake.config.path"
168
189
  {
169
- _meta: { "last_refreshed_at" => Time.now.utc.iso8601, "source_path" => path },
190
+ _meta: { "last_fetched_at" => Time.now.utc.iso8601, "source_path" => path },
170
191
  body: File.read(File.expand_path(path)),
171
192
  }
172
193
  end
@@ -184,16 +205,16 @@ end
184
205
  To keep a batch of stale intake entries current in one shot:
185
206
 
186
207
  ```sh
187
- textus refresh stale --prefix=working --zone=intake --as=runner
188
- # or just refresh everything stale in the intake zone:
189
- textus refresh stale --zone=intake --as=runner
208
+ textus fetch stale --prefix=feeds --zone=feeds --as=automation
209
+ # or just fetch everything stale in the feeds zone:
210
+ textus fetch stale --zone=feeds --as=automation
190
211
  ```
191
212
 
192
213
  See SPEC.md §5.10 for the full hook contract.
193
214
 
194
215
  Schemas (`.textus/schemas/<name>.yaml`) declare field shapes, per-field `maintained_by:` ownership, and an `evolution:` block (`added_in`, `deprecated_at`, `migrate_from`). Full contract in SPEC §5.8.
195
216
 
196
- See [`docs/agent-integration.md`](docs/agent-integration.md) for the agent boot → pulse loop.
217
+ See [`docs/agents-mcp.md`](docs/agents-mcp.md) for the agent boot → pulse loop.
197
218
 
198
219
  ## Examples
199
220