textus 0.30.0 → 0.38.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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +2 -241
  3. data/CHANGELOG.md +221 -0
  4. data/README.md +89 -69
  5. data/SPEC.md +359 -212
  6. data/docs/conventions.md +42 -37
  7. data/lib/textus/boot.rb +122 -87
  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/mcp_serve.rb +8 -3
  15. data/lib/textus/cli/verb/propose.rb +28 -0
  16. data/lib/textus/cli/verb/pulse.rb +12 -3
  17. data/lib/textus/cli/verb/put.rb +1 -1
  18. data/lib/textus/cli/verb/rule_list.rb +7 -7
  19. data/lib/textus/cli/verb/schema.rb +1 -1
  20. data/lib/textus/cli/verb.rb +3 -2
  21. data/lib/textus/cli.rb +2 -2
  22. data/lib/textus/container.rb +1 -2
  23. data/lib/textus/contract.rb +106 -0
  24. data/lib/textus/cursor_store.rb +24 -0
  25. data/lib/textus/dispatcher.rb +6 -4
  26. data/lib/textus/doctor/check/audit_log.rb +1 -1
  27. data/lib/textus/doctor/check/{refresh_locks.rb → fetch_locks.rb} +8 -8
  28. data/lib/textus/doctor/check/proposal_targets.rb +45 -0
  29. data/lib/textus/doctor/check/rule_ambiguity.rb +3 -3
  30. data/lib/textus/doctor.rb +2 -1
  31. data/lib/textus/domain/action.rb +3 -3
  32. data/lib/textus/domain/freshness/evaluator.rb +3 -3
  33. data/lib/textus/domain/freshness/policy.rb +2 -2
  34. data/lib/textus/domain/freshness.rb +7 -7
  35. data/lib/textus/domain/outcome.rb +2 -2
  36. data/lib/textus/domain/permission.rb +2 -10
  37. data/lib/textus/domain/policy/base_guards.rb +25 -0
  38. data/lib/textus/domain/policy/evaluation.rb +15 -0
  39. data/lib/textus/domain/policy/{refresh.rb → fetch.rb} +1 -1
  40. data/lib/textus/domain/policy/guard.rb +35 -0
  41. data/lib/textus/domain/policy/guard_factory.rb +40 -0
  42. data/lib/textus/domain/policy/predicates/author_held.rb +33 -0
  43. data/lib/textus/domain/policy/predicates/etag_match.rb +32 -0
  44. data/lib/textus/domain/policy/predicates/fresh_within.rb +58 -0
  45. data/lib/textus/domain/policy/predicates/registry.rb +39 -0
  46. data/lib/textus/domain/policy/predicates/schema_valid.rb +30 -19
  47. data/lib/textus/domain/policy/predicates/target_is_canon.rb +33 -0
  48. data/lib/textus/domain/policy/predicates/zone_writable_by.rb +39 -0
  49. data/lib/textus/domain/staleness/intake_check.rb +6 -6
  50. data/lib/textus/envelope.rb +2 -2
  51. data/lib/textus/errors.rb +25 -28
  52. data/lib/textus/hooks/event_bus.rb +4 -4
  53. data/lib/textus/init.rb +27 -18
  54. data/lib/textus/layout.rb +41 -0
  55. data/lib/textus/maintenance/key_delete_prefix.rb +9 -0
  56. data/lib/textus/maintenance/key_mv_prefix.rb +10 -0
  57. data/lib/textus/maintenance/migrate.rb +9 -0
  58. data/lib/textus/maintenance/rule_lint.rb +8 -0
  59. data/lib/textus/maintenance/zone_mv.rb +11 -1
  60. data/lib/textus/manifest/capabilities.rb +29 -0
  61. data/lib/textus/manifest/data.rb +14 -10
  62. data/lib/textus/manifest/policy.rb +37 -21
  63. data/lib/textus/manifest/rules.rb +16 -14
  64. data/lib/textus/manifest/schema.rb +48 -58
  65. data/lib/textus/manifest.rb +3 -3
  66. data/lib/textus/mcp/catalog.rb +72 -0
  67. data/lib/textus/mcp/server.rb +8 -5
  68. data/lib/textus/mcp/session.rb +3 -20
  69. data/lib/textus/mcp/tool_schemas.rb +6 -62
  70. data/lib/textus/mcp/tools.rb +4 -119
  71. data/lib/textus/ports/audit_log.rb +17 -15
  72. data/lib/textus/ports/audit_subscriber.rb +1 -1
  73. data/lib/textus/ports/build_lock.rb +1 -2
  74. data/lib/textus/ports/{refresh → fetch}/detached.rb +4 -4
  75. data/lib/textus/ports/{refresh → fetch}/lock.rb +2 -2
  76. data/lib/textus/projection.rb +1 -1
  77. data/lib/textus/read/audit.rb +3 -3
  78. data/lib/textus/read/boot.rb +6 -0
  79. data/lib/textus/read/freshness.rb +9 -9
  80. data/lib/textus/read/get.rb +16 -8
  81. data/lib/textus/read/{get_or_refresh.rb → get_or_fetch.rb} +11 -11
  82. data/lib/textus/read/list.rb +8 -0
  83. data/lib/textus/read/policy_explain.rb +14 -10
  84. data/lib/textus/read/pulse.rb +12 -4
  85. data/lib/textus/read/rules.rb +24 -0
  86. data/lib/textus/read/schema_envelope.rb +7 -0
  87. data/lib/textus/read/validator.rb +1 -1
  88. data/lib/textus/role.rb +6 -2
  89. data/lib/textus/schema/tools.rb +5 -5
  90. data/lib/textus/session.rb +24 -0
  91. data/lib/textus/store.rb +11 -0
  92. data/lib/textus/version.rb +1 -1
  93. data/lib/textus/write/accept.rb +19 -55
  94. data/lib/textus/write/delete.rb +14 -2
  95. data/lib/textus/write/{refresh_all.rb → fetch_all.rb} +14 -6
  96. data/lib/textus/write/{refresh_orchestrator.rb → fetch_orchestrator.rb} +14 -14
  97. data/lib/textus/write/{refresh_worker.rb → fetch_worker.rb} +29 -14
  98. data/lib/textus/write/mv.rb +15 -3
  99. data/lib/textus/write/propose.rb +46 -0
  100. data/lib/textus/write/put.rb +26 -2
  101. data/lib/textus/write/reject.rb +11 -5
  102. data/lib/textus.rb +4 -0
  103. metadata +36 -21
  104. data/lib/textus/cli/verb/refresh.rb +0 -14
  105. data/lib/textus/domain/authorizer.rb +0 -37
  106. data/lib/textus/domain/policy/predicates/accept_authority_signed.rb +0 -33
  107. data/lib/textus/domain/policy/promote.rb +0 -26
  108. data/lib/textus/domain/policy/promotion.rb +0 -57
  109. data/lib/textus/manifest/role_kinds.rb +0 -21
  110. data/lib/textus/write/authority_gate.rb +0 -24
data/README.md CHANGED
@@ -1,13 +1,21 @@
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://rubygems.org/gems/textus"><img src="https://img.shields.io/gem/dt/textus.svg" alt="Gem Downloads"></a>
12
+ <a href="https://www.ruby-lang.org/"><img src="https://img.shields.io/badge/ruby-%E2%89%A53.3-CC342D.svg" alt="Ruby"></a>
13
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
14
+ </p>
7
15
 
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.
16
+ **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
17
 
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.
18
+ *textus* is Latin for "the fabric a text is woven from" — same root as *context*, from *con-texere*, "to weave together."
11
19
 
12
20
  ## The idea
13
21
 
@@ -15,19 +23,31 @@ Three actors write to your repo today:
15
23
 
16
24
  - **Humans** — you, your team. Authoritative on identity, decisions, voice.
17
25
  - **Agents** — Claude, Cursor, custom assistants. Smart, fast, forgetful, and not always right.
18
- - **Runners** — cron jobs, fetchers, CI. Bring outside data in.
26
+ - **Automation** — cron jobs, fetchers, CI. Bring outside data in and compile published artifacts.
27
+
28
+ ```mermaid
29
+ flowchart LR
30
+ human(["human"]) -->|author| knowledge["knowledge<br/>(canon)"]
31
+ agent(["agent"]) -->|keep| notebook["notebook<br/>(workspace)"]
32
+ agent -->|propose| proposals["proposals<br/>(queue)"]
33
+ proposals -->|human accepts| knowledge
34
+ automation(["automation"]) -->|fetch| feeds["feeds<br/>(quarantine)"]
35
+ automation -->|build| artifacts["artifacts<br/>(derived)"]
36
+ ```
37
+
38
+ *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
39
 
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.
40
+ 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
41
 
22
42
  ```
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
43
+ knowledge/ author only — who you are, what you decide, how you sound (knowledge.identity.* for identity facts)
44
+ notebook/ keep only — agent's own durable lane (agents keep theirs; bytes climb to knowledge only via propose→accept)
45
+ feeds/ fetch only — declared external inputs
46
+ proposals/ propose (agent + human) — proposals waiting on a human accept
47
+ artifacts/ build only — computed, published artifacts
28
48
  ```
29
49
 
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.
50
+ 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
51
 
32
52
  That's the load-bearing claim: **coordination is a protocol invariant, not a library convenience.**
33
53
 
@@ -36,28 +56,26 @@ That's the load-bearing claim: **coordination is a protocol invariant, not a lib
36
56
  ```sh
37
57
  gem install textus
38
58
  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
59
+ # agent proposes a change to proposals/
60
+ printf '%s' '{"_meta":{"name":"oncall","proposal":{"target_key":"knowledge.notes.oncall","action":"put"}},"body":"Patrick on call.\n"}' \
61
+ | textus put proposals.notes.oncall --as=agent --stdin
62
+ # you accept it — textus promotes to knowledge/ and audits the move
63
+ textus accept proposals.notes.oncall --as=human
44
64
  ```
45
65
 
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.
66
+ 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
67
 
48
68
  ## Try it
49
69
 
50
- - **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)
52
- - **Use textus as your own project's context store**: [`examples/project/`](examples/project/)
53
- - **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/)
70
+ - **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/)
71
+ - **Wire textus into Claude Code via MCP** — 4 steps, ~5 minutes: [`docs/agents-mcp.md`](docs/agents-mcp.md)
54
72
 
55
73
  ## Protocol, not just a gem
56
74
 
57
75
  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
76
 
59
77
  - Specification: [`SPEC.md`](SPEC.md)
60
- - Architecture: [`ARCHITECTURE.md`](ARCHITECTURE.md)
78
+ - Architecture: [`docs/architecture/README.md`](docs/architecture/README.md)
61
79
  - Per-release notes: [`CHANGELOG.md`](CHANGELOG.md)
62
80
 
63
81
  A second implementation in another language would share the same `.textus/` directory and the same audit log. That's deliberate.
@@ -75,70 +93,72 @@ bundle install
75
93
  bundle exec exe/textus --help
76
94
  ```
77
95
 
78
- ## Quick start
96
+ ## What `textus init` gives you
79
97
 
80
- ```sh
81
- textus init
82
- ```
98
+ 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
99
 
84
- You get `.textus/` with all five zone directories, baseline schemas, an empty audit log, and a starter manifest:
100
+ ```yaml
101
+ roles:
102
+ - { name: human, can: [author, propose] }
103
+ - { name: agent, can: [propose, keep] }
104
+ - { name: automation, can: [fetch, build] }
105
+
106
+ zones:
107
+ - { name: knowledge, kind: canon } # author — canonical truth
108
+ - { name: notebook, kind: workspace } # keep — agent's own durable lane
109
+ - { name: feeds, kind: quarantine } # fetch — declared external inputs
110
+ - { name: proposals, kind: queue } # propose — proposals awaiting accept
111
+ - { name: artifacts, kind: derived } # build — computed outputs
112
+ ```
85
113
 
86
114
  ```
87
115
  .textus/
88
- manifest.yaml # zone declarations + key-to-path mapping
116
+ manifest.yaml # role capabilities + zone kinds + key-to-path mapping
89
117
  audit.log # append-only NDJSON, every write
90
118
  schemas/ # YAML field shapes per entry family
91
119
  templates/ # mustache templates for derived entries
92
120
  hooks/ # one .rb per hook
93
121
  sentinels/ # publish bookkeeping
94
122
  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
123
+ knowledge/ # author — identity (knowledge.identity.*), voice, decisions, notes
124
+ notebook/ # keep agent's own durable lane (agents keep theirs)
125
+ feeds/ # fetch — declared external inputs (actions)
126
+ proposals/ # propose (agent + human) — proposals awaiting accept
127
+ artifacts/ # build — computed outputs
100
128
  ```
101
129
 
102
- Manifest `path:` fields are relative to `.textus/zones/`. So `working.notes.org.jane` lives at `.textus/zones/working/notes/org/jane.md`.
130
+ Manifest `path:` fields are relative to `.textus/zones/`. So `knowledge.notes.org.jane` lives at `.textus/zones/knowledge/notes/org/jane.md`.
103
131
 
104
132
  Read and write:
105
133
 
106
134
  ```sh
107
- textus get working.notes.org.jane
108
- textus list --zone=working
135
+ textus get knowledge.notes.org.jane
136
+ textus list --zone=knowledge
109
137
  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
138
+ | textus put knowledge.notes.bob --as=human --stdin
139
+ textus freshness --zone=artifacts # per-entry fresh/stale/never_fetched/no_policy
112
140
  textus rule list # show every rule block
113
141
  textus audit --limit=20 # query the audit log
114
142
  ```
115
143
 
116
144
  (All verbs return JSON envelopes by default; pass `--output=json` explicitly if you prefer.)
117
145
 
118
- For the full shapeClaude plugin with agents, skills, commands, pending walkthrough, intake action — see [`examples/claude-plugin/`](examples/claude-plugin/).
146
+ For a worked storeknowledge entries, a staged proposal, schemas, a template, and a build that publishes `CLAUDE.md` / `AGENTS.md` — see [`examples/project/`](examples/project/).
119
147
 
120
148
  ## What's shipped
121
149
 
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.
150
+ - **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))
151
+ - **Stable identity.** Auto-minted `uid:` survives writes and `textus key mv`; reorganising never breaks references.
152
+ - **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))
153
+ - **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))
154
+ - **`textus doctor`.** Health checks across schemas, hooks, keys, sentinels, and the audit log.
135
155
 
136
156
  ## CLI and zones
137
157
 
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`.
158
+ 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
159
 
140
160
  - 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).
161
+ - 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
162
 
143
163
  `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
164
 
@@ -146,7 +166,7 @@ All verbs accept `--output=json` and return the envelope defined in [SPEC §8](S
146
166
 
147
167
  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
168
 
149
- For externally-generated entries, declare `compute: { kind: external, sources: [...] }` — textus tracks the declared sources for staleness; the build runner produces the file.
169
+ For externally-generated entries, declare `compute: { kind: external, sources: [...] }` — textus tracks the declared sources for staleness; the build automation produces the file.
150
170
 
151
171
  `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
172
 
@@ -157,8 +177,8 @@ textus exposes a hook DSL. Drop `.rb` files into `.textus/hooks/` (subdirectorie
157
177
  - `:resolve_intake` — bring bytes in from elsewhere (returns `{_meta:, body:}`)
158
178
  - `:transform_rows` — transform rows during projection (returns rows)
159
179
  - `: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
180
+ - `:entry_put`, `:entry_deleted`, `:entry_fetched`, `:build_completed`, `:proposal_accepted`, `:file_published`, `:entry_renamed`, `:proposal_rejected`, `:store_loaded` — react to lifecycle events
181
+ - `:fetch_started`, `:fetch_failed`, `:fetch_backgrounded` — background-fetch lifecycle
162
182
 
163
183
  ```ruby
164
184
  # Inside .textus/hooks/local_file.rb
@@ -166,7 +186,7 @@ Textus.hook do |reg|
166
186
  reg.on(:resolve_intake, :local_file) do |config:, args:, **|
167
187
  path = config["path"] or raise "local-file requires intake.config.path"
168
188
  {
169
- _meta: { "last_refreshed_at" => Time.now.utc.iso8601, "source_path" => path },
189
+ _meta: { "last_fetched_at" => Time.now.utc.iso8601, "source_path" => path },
170
190
  body: File.read(File.expand_path(path)),
171
191
  }
172
192
  end
@@ -184,20 +204,20 @@ end
184
204
  To keep a batch of stale intake entries current in one shot:
185
205
 
186
206
  ```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
207
+ textus fetch stale --prefix=feeds --zone=feeds --as=automation
208
+ # or just fetch everything stale in the feeds zone:
209
+ textus fetch stale --zone=feeds --as=automation
190
210
  ```
191
211
 
192
212
  See SPEC.md §5.10 for the full hook contract.
193
213
 
194
214
  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
215
 
196
- See [`docs/agent-integration.md`](docs/agent-integration.md) for the agent boot → pulse loop.
216
+ See [`docs/agents-mcp.md`](docs/agents-mcp.md) for the agent boot → pulse loop.
197
217
 
198
218
  ## Examples
199
219
 
200
- [`examples/claude-plugin/`](examples/claude-plugin/) — a Claude Code plugin (`voice-tools`) whose entire content surface agents, skills, commands, `CLAUDE.md`, `plugin.json`, `marketplace.json` — is textus-managed. Demonstrates per-entry formats, `publish_each`, intake actions, in-process transforms and hooks, the agent-propose / human-accept loop, and the `inject_boot:` flag that puts an orientation preamble at the top of `CLAUDE.md`.
220
+ [`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.
201
221
 
202
222
  ## Tests
203
223
 
@@ -205,7 +225,7 @@ See [`docs/agent-integration.md`](docs/agent-integration.md) for the agent boot
205
225
  bundle exec rspec
206
226
  ```
207
227
 
208
- ~920 examples; includes conformance fixtures A–I from SPEC §12.
228
+ Includes conformance fixtures A–I from SPEC §12.
209
229
 
210
230
  ## Code quality
211
231
 
@@ -218,4 +238,4 @@ Lefthook hooks (`brew bundle install` then `lefthook install`) run rubocop on `p
218
238
 
219
239
  ## License
220
240
 
221
- MIT.
241
+ [MIT](LICENSE).