textus 0.10.5 → 0.12.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -3
  3. data/README.md +39 -26
  4. data/SPEC.md +222 -144
  5. data/lib/textus/application/reads/freshness.rb +2 -2
  6. data/lib/textus/application/reads/get.rb +1 -1
  7. data/lib/textus/application/reads/policy_explain.rb +2 -2
  8. data/lib/textus/application/refresh/orchestrator.rb +1 -1
  9. data/lib/textus/application/refresh/worker.rb +5 -5
  10. data/lib/textus/application/writes/accept.rb +19 -1
  11. data/lib/textus/application/writes/build.rb +5 -5
  12. data/lib/textus/application/writes/delete.rb +1 -1
  13. data/lib/textus/application/writes/publish.rb +1 -1
  14. data/lib/textus/application/writes/put.rb +1 -1
  15. data/lib/textus/builder/pipeline.rb +1 -1
  16. data/lib/textus/builder/renderer/json.rb +1 -1
  17. data/lib/textus/builder/renderer/yaml.rb +1 -1
  18. data/lib/textus/cli/group/key.rb +1 -1
  19. data/lib/textus/cli/group/refresh.rb +21 -0
  20. data/lib/textus/cli/group/rule.rb +11 -0
  21. data/lib/textus/cli/verb/build.rb +1 -1
  22. data/lib/textus/cli/verb/hook_run.rb +3 -2
  23. data/lib/textus/cli/verb/hooks.rb +1 -1
  24. data/lib/textus/cli/verb/{migrate_keys.rb → key_normalize.rb} +1 -1
  25. data/lib/textus/cli/verb/put.rb +1 -1
  26. data/lib/textus/cli/verb/{policy_explain.rb → rule_explain.rb} +1 -1
  27. data/lib/textus/cli/verb/{policy_list.rb → rule_list.rb} +3 -3
  28. data/lib/textus/cli/verb.rb +3 -2
  29. data/lib/textus/cli.rb +6 -6
  30. data/lib/textus/doctor/check/handler_allowlist.rb +1 -1
  31. data/lib/textus/doctor/check/illegal_keys.rb +39 -16
  32. data/lib/textus/doctor/check/intake_registration.rb +4 -4
  33. data/lib/textus/doctor/check/protocol_version.rb +47 -0
  34. data/lib/textus/doctor/check/{policy_ambiguity.rb → rule_ambiguity.rb} +6 -6
  35. data/lib/textus/doctor.rb +5 -4
  36. data/lib/textus/domain/permission.rb +4 -4
  37. data/lib/textus/domain/policy/predicates/human_accept.rb +31 -0
  38. data/lib/textus/domain/policy/predicates/schema_valid.rb +50 -0
  39. data/lib/textus/domain/policy/promotion.rb +45 -0
  40. data/lib/textus/errors.rb +24 -5
  41. data/lib/textus/hooks/builtin.rb +5 -5
  42. data/lib/textus/hooks/dispatcher.rb +1 -1
  43. data/lib/textus/hooks/dsl.rb +3 -10
  44. data/lib/textus/hooks/loader.rb +1 -2
  45. data/lib/textus/hooks/registry.rb +22 -21
  46. data/lib/textus/infra/refresh/detached.rb +1 -1
  47. data/lib/textus/init.rb +25 -34
  48. data/lib/textus/intro.rb +9 -9
  49. data/lib/textus/manifest/entry.rb +33 -6
  50. data/lib/textus/manifest/{policies.rb → rules.rb} +12 -10
  51. data/lib/textus/manifest/schema.rb +49 -0
  52. data/lib/textus/manifest.rb +45 -9
  53. data/lib/textus/migrate_keys.rb +1 -1
  54. data/lib/textus/projection.rb +4 -4
  55. data/lib/textus/refresh.rb +1 -1
  56. data/lib/textus/store/mover.rb +1 -1
  57. data/lib/textus/store/staleness/intake_check.rb +1 -1
  58. data/lib/textus/store/writer.rb +1 -1
  59. data/lib/textus/store.rb +1 -1
  60. data/lib/textus/version.rb +2 -2
  61. data/lib/textus.rb +1 -0
  62. metadata +13 -7
  63. data/lib/textus/cli/group/policy.rb +0 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79987029ce43500b025495ef26cefc285f7a748cdcd43388aff2fefafdf4c0ca
4
- data.tar.gz: 44a9d40720a84e4e942a1c4fde9e0ed9e7773126f443b247f6d8cb27d630c204
3
+ metadata.gz: e14d58006851be5feba9ffa7016f5860b0ea380cef09cbf324a897d24ae56ee8
4
+ data.tar.gz: f928e250b5c3bc6e072c1ffc1928e68fdf61d2965d1746c121657ad4bd07ac75
5
5
  SHA512:
6
- metadata.gz: a539030b1d406226bbe3d3fad03b3daee59094d3d8db351d97c9c5adc445ca8c188a7d13a16cdfaaae0c933091e6a8858c14a91362f461f25a1dce52aa04a60f
7
- data.tar.gz: 83667dab7c91d4c2e9def3c2a2d605cf45d84f3fdf89bf0e6d8d25ff2b8fe668d6b71ceae0488e7ffe24970bf80e0f5eeeb13c308b2d16c9d11fc41633160dd6
6
+ metadata.gz: 6eb5690e16fc78f71b42a5f4308d7698684b5aa9f43a92534f73edca5fc36494be65bfab6df2d8d0832f788e7fe7496638ad23f83ee6f69f5a9950b419237745
7
+ data.tar.gz: aa3b5903ff1fb44dcc5d1bb3f0676bffb9d8cce99c6e6237f2d947b3486e3d0cfce53271fcf282b927ea1b2e9b1164e721ed0800ddd5ffde1644f85dd660339a
data/CHANGELOG.md CHANGED
@@ -5,10 +5,111 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  The **gem version** (`0.x.y`) is distinct from the **protocol version**
8
- (currently `textus/2`, embedded in every envelope as `protocol`). The protocol
9
- is additive within a major; a new major would change the wire string.
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.
10
11
 
11
- ## [Unreleased]
12
+ ## 0.12.1 — textus/2 hint fix (2026-05-26)
13
+
14
+ ### Fixed
15
+ - Manifest parser now points textus/2 stores at the 0.11.x stepping-stone
16
+ migrator instead of the misleading "check YAML frontmatter for syntax errors"
17
+ hint. The protocol_version doctor check carried the correct hint already, but
18
+ was unreachable on textus/2 stores because `Store.discover` → `Manifest.load`
19
+ raises before doctor checks run. Surfaced by v0.12.0 release smoke testing.
20
+
21
+ ## 0.12.0 — legacy sweep (2026-05-25)
22
+
23
+ ### Removed (breaking)
24
+ - `Role::LEGACY_RENAMES` (`ai`/`script`/`build` → friendly error). Legacy role
25
+ names now fail with the generic `InvalidRole` error.
26
+ - `Manifest::LEGACY_ZONE_RENAMES` (`inbox` → friendly error).
27
+ - `Hooks::Registry::LEGACY_EVENT_RENAMES` (14 legacy event names → friendly
28
+ error). Legacy events now fail with `unknown event: <name>`.
29
+ - `CLI::LEGACY_VERB_RENAMES` / `CLI::LEGACY_GROUP_RENAMES` and the
30
+ `CommandRenamed` error class.
31
+ - `textus migrate --to=textus/3` verb and `lib/textus/migration/**` (eight
32
+ files, ~924 lines).
33
+ - Eight ad-hoc legacy-key guards in `manifest.rb` / `manifest/entry.rb` /
34
+ `manifest/rules.rb`.
35
+
36
+ ### Added
37
+ - `Manifest::Schema.validate!` — strict-unknown-keys parser. Manifests with
38
+ any unrecognized key fail uniformly with `unknown key 'X' at '<jsonpath>'`.
39
+ - ADR 0003 documenting the sweep and the 0.11.x stepping-stone path.
40
+
41
+ ### Changed
42
+ - `Doctor::Check::ProtocolVersion` hint no longer suggests `textus migrate`
43
+ (the verb is gone); points at 0.11.x docs instead.
44
+ - Test suite consolidated: five batches of disciplined deletions/merges
45
+ (−4 files, −134 LOC from the post-P6 peak). Net effect across the release:
46
+ test suite grew +8.2% LOC to cover new behavior (schema walker, permissive
47
+ audit-log tolerance).
48
+
49
+ ### Migration
50
+ - **From textus/2 (gem ≤0.10.x):** install textus 0.11.x first; run
51
+ `textus migrate --to=textus/3`; then upgrade to 0.12.0.
52
+ - **From 0.11.x:** drop-in upgrade.
53
+
54
+ ## 0.11.0 — textus/3 vocabulary redesign (2026-05-25)
55
+
56
+ **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.
57
+
58
+ ### Renamed — actors
59
+
60
+ - `ai` → `agent`, `script` → `runner`, `build` → `builder`. `Role.resolve` rejects legacy names with a one-line migration hint pointing at `--as=<new>`.
61
+
62
+ ### Renamed — zone
63
+
64
+ - `inbox` → `intake`. Directory rename + key prefix update + manifest field handled by the migrator.
65
+
66
+ ### Renamed — manifest schema
67
+
68
+ - `writable_by:` → `write_policy:`; new explicit `read_policy:` on zones (default `[all]`).
69
+ - `policies:` (top-level) → `rules:`. Class rename: `Manifest::Policies` → `Manifest::Rules`.
70
+ - `projection:` and `generator:` unified under `compute: { kind: projection|external, ... }`.
71
+ - `reduce:` (inside compute/projection) → `transform:`.
72
+ - `handler_allowlist:` → `intake_handler_allowlist:`.
73
+ - `promote_requires:` (reserved in textus/2) → `promotion: { requires: [...] }` and is now **enforced** during `textus accept`.
74
+
75
+ ### Renamed — hook events
76
+
77
+ - RPC: `:intake` → `:resolve_intake`, `:reduce` → `:transform_rows`, `:check` → `:validate`.
78
+ - 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.
79
+ - DSL: single `Textus.on(event, name, **opts) { ... }`. Sugar methods (`Textus.intake`, `Textus.reduce`, `Textus.check`, etc.) and the generic `Textus.hook(...)` form removed.
80
+
81
+ ### Renamed — CLI
82
+
83
+ - 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`).
84
+ - Top-level mutator `textus mv` removed (use `textus key mv`).
85
+ - Envelope-render flag `--format=json` → `--output=json`. Entry-level `format:` in the manifest is unchanged.
86
+ - Legacy spellings emit a `CommandRenamed` envelope (`code: "command_renamed"`); legacy flags emit `FlagRenamed`.
87
+
88
+ ### Added
89
+
90
+ - `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).
91
+ - `textus doctor` check `protocol_version`: refuses textus/2 stores.
92
+ - `promotion.requires` predicates: `schema_valid`, `human_accept`. Enforced by `textus accept` for matching rules.
93
+
94
+ ### Internal
95
+
96
+ - `Manifest::Policies` → `Manifest::Rules` (class + file + accessor + doctor check).
97
+ - New errors: `Textus::BadManifest`, `Textus::CommandRenamed`, `Textus::FlagRenamed`.
98
+ - Two new domain classes under `Textus::Domain::Policy::Predicates::` for promotion gating.
99
+ - Migration toolkit under `Textus::Migration::V3::`.
100
+
101
+ ### Migration notes for 0.10.x users
102
+
103
+ 1. Update `Gemfile`: `gem "textus", "~> 0.11"`.
104
+ 2. `bundle update textus`.
105
+ 3. `cd` to each textus store and run `textus migrate --to=textus/3`.
106
+ 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.
107
+ 5. Run `textus doctor` — should report `ok: true`.
108
+ 6. Commit the rewritten `.textus/` directory (manifest, audit marker, possibly renamed zone dir).
109
+
110
+ ### Fixed
111
+
112
+ - **`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`.
12
113
 
13
114
  ## 0.10.5 — tech-debt cleanup + `index_filename:` + docs polish (2026-05-25)
14
115
 
data/README.md CHANGED
@@ -7,17 +7,27 @@
7
7
 
8
8
  A context store for codebases that humans and AI agents both have to read and write. Dotted keys, schema-validated entries, role-gated writes, byte-copy publish, an audit log of every change. Built so an agent landing in your repo can run one command (`textus intro`) and know what to read, what to write, and what's off-limits.
9
9
 
10
- Reference implementation in Ruby. Wire format `textus/2`. SPEC: [`SPEC.md`](SPEC.md). Implementation notes: [`docs/`](docs/).
10
+ Reference implementation in Ruby. Wire format `textus/3`. SPEC: [`SPEC.md`](SPEC.md). Implementation notes: [`docs/`](docs/).
11
11
 
12
12
  ## Versioning
13
13
 
14
14
  Two versions, deliberately independent:
15
15
 
16
- - **Protocol wire string:** `textus/2`. Stable; breaking changes require `textus/3`.
17
- - **Gem version:** semver, currently `0.10.3`. The gem version is decoupled from the protocol string — internal refactors bump the gem; only wire-format changes bump the protocol.
16
+ - **Protocol wire string:** `textus/3`. Stable; breaking changes require `textus/4`.
17
+ - **Gem version:** semver, currently `0.11.0`. The gem version is decoupled from the protocol string — internal refactors bump the gem; only wire-format changes bump the protocol.
18
18
 
19
19
  Envelope payloads carry the `protocol` field. The gem version is irrelevant to the wire format.
20
20
 
21
+ ### Upgrading from textus/2
22
+
23
+ textus 0.12.0 does not include a built-in migrator. If you are upgrading from
24
+ a textus/2 store (gem versions ≤ 0.10.x), first install textus 0.11.x and run:
25
+
26
+ textus migrate --to=textus/3
27
+
28
+ Then upgrade to 0.12.0. Pre-0.11.0 audit-log rows with `role: ai|script|build`
29
+ are tolerated verbatim by the reader — no rewrite step required.
30
+
21
31
  ## Install
22
32
 
23
33
  ```sh
@@ -49,10 +59,10 @@ You get `.textus/` with all five zone directories, baseline schemas, an empty au
49
59
  sentinels/ # publish bookkeeping
50
60
  zones/
51
61
  identity/ # human-only — identity, voice, decisions
52
- working/ # human / ai / script — day-to-day catalog
53
- inbox/ # script — declared external inputs (actions)
54
- review/ # ai + human — proposals awaiting accept
55
- output/ # build only — computed outputs
62
+ working/ # human / agent / runner — day-to-day catalog
63
+ intake/ # runner — declared external inputs (actions)
64
+ review/ # agent + human — proposals awaiting accept
65
+ output/ # builder only — computed outputs
56
66
  ```
57
67
 
58
68
  Manifest `path:` fields are relative to `.textus/zones/`. So `working.network.org.jane` lives at `.textus/zones/working/network/org/jane.md`.
@@ -65,38 +75,41 @@ textus list --zone=working
65
75
  echo '{"_meta":{"name":"bob","org":"acme"},"body":"hi\n"}' \
66
76
  | textus put working.network.org.bob --as=human --stdin
67
77
  textus freshness --zone=output # per-entry fresh/stale/never_refreshed/no_policy
68
- textus policy list # show every policy block
78
+ textus rule list # show every rule block
69
79
  textus audit --limit=20 # query the audit log
70
80
  ```
71
81
 
72
- (All verbs return JSON envelopes by default; pass `--format=json` explicitly if you prefer.)
82
+ (All verbs return JSON envelopes by default; pass `--output=json` explicitly if you prefer.)
73
83
 
74
84
  For the full shape — Claude plugin with agents, skills, commands, pending walkthrough, intake action — see [`examples/claude-plugin/`](examples/claude-plugin/).
75
85
 
76
86
  ## What ships today
77
87
 
78
- - **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`, `reducer`).
88
+ - **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`).
79
89
  - **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.
80
90
  - **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.
81
- - **Strict key grammar.** `/^[a-z0-9][a-z0-9-]*$/`, max 8 segments × 64 chars. `textus key migrate --dry-run|--write` rewrites existing stores with illegal segments deterministically.
91
+ - **Strict key grammar.** `/^[a-z0-9][a-z0-9-]*$/`, max 8 segments × 64 chars. `textus key normalize --dry-run|--write` rewrites existing stores with illegal segments deterministically.
82
92
  - **`textus intro`.** 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. The boot signal for any agent — one tool call and it knows your store.
83
93
  - **`textus doctor`.** Health check across 9 categories: 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.
84
94
  - **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.
95
+ - **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.
85
96
 
86
97
  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.
87
98
 
88
99
  ## CLI and zones
89
100
 
90
- All verbs accept `--format=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`).
101
+ 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`.
91
102
 
92
103
  - Full verb table — read, write, health, scaffolding — is in [SPEC §9](SPEC.md).
93
- - Zone semantics and the role/`writable_by` mapping live in [SPEC §5](SPEC.md), with a tutorial expansion in [`docs/zones.md`](docs/zones.md).
104
+ - 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).
94
105
 
95
106
  `textus intro` 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.
96
107
 
97
108
  ## Compute and publish
98
109
 
99
- Derived entries declare a `projection:` (`select`, `pluck`, `sort_by`, `limit`, optional `reducer`) and either a template under `.textus/templates/` (markdown/text) or a templateless path that lets a reducer 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.
110
+ 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.
111
+
112
+ For externally-generated entries, declare `compute: { kind: external, sources: [...] }` — textus tracks the declared sources for staleness; the build runner produces the file.
100
113
 
101
114
  `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.
102
115
 
@@ -104,15 +117,15 @@ Derived entries declare a `projection:` (`select`, `pluck`, `sort_by`, `limit`,
104
117
 
105
118
  textus exposes a hook DSL. Drop `.rb` files into `.textus/hooks/` (subdirectories are fine; files load alphabetically by full path). Events:
106
119
 
107
- - `:intake` — bring bytes in from elsewhere (returns `{_meta:, body:}`)
108
- - `:reduce` — transform rows during projection (returns rows)
109
- - `:check` — custom doctor check (returns issues)
110
- - `:put`, `:deleted`, `:refreshed`, `:built`, `:accepted`, `:published`, `:mv`, `:reject`, `:loaded` — react to lifecycle events
111
- - `:refresh_began`, `:refresh_failed`, `:refresh_detached` — background-refresh lifecycle
120
+ - `:resolve_intake` — bring bytes in from elsewhere (returns `{_meta:, body:}`)
121
+ - `:transform_rows` — transform rows during projection (returns rows)
122
+ - `:validate` — custom doctor check (returns issues)
123
+ - `:entry_put`, `:entry_deleted`, `:entry_refreshed`, `:build_completed`, `:proposal_accepted`, `:file_published`, `:entry_renamed`, `:proposal_rejected`, `:store_loaded` — react to lifecycle events
124
+ - `:refresh_started`, `:refresh_failed`, `:refresh_backgrounded` — background-refresh lifecycle
112
125
 
113
126
  ```ruby
114
127
  # Inside .textus/hooks/local_file.rb
115
- Textus.intake(:local_file) do |config:, args:, **|
128
+ Textus.on(:resolve_intake, :local_file) do |config:, args:, **|
116
129
  path = config["path"] or raise "local-file requires intake.config.path"
117
130
  {
118
131
  _meta: { "last_refreshed_at" => Time.now.utc.iso8601, "source_path" => path },
@@ -122,7 +135,7 @@ end
122
135
  ```
123
136
 
124
137
  ```ruby
125
- Textus.reduce(:rank_by_recency) do |rows:, **|
138
+ Textus.on(:transform_rows, :rank_by_recency) do |rows:, **|
126
139
  rows.sort_by { |r| r["updated_at"].to_s }.reverse
127
140
  end
128
141
  ```
@@ -130,18 +143,18 @@ end
130
143
  To keep a batch of stale intake entries current in one shot:
131
144
 
132
145
  ```sh
133
- textus refresh-stale --prefix=working --zone=inbox --as=script
134
- # or just refresh everything stale in the inbox zone:
135
- textus refresh-stale --zone=inbox --as=script
146
+ textus refresh stale --prefix=working --zone=intake --as=runner
147
+ # or just refresh everything stale in the intake zone:
148
+ textus refresh stale --zone=intake --as=runner
136
149
  ```
137
150
 
138
- The primitive `Textus.hook(event, name, **opts) { ... }` is also supported. See SPEC.md §5.10 for the full contract.
151
+ See SPEC.md §5.10 for the full hook contract.
139
152
 
140
153
  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.
141
154
 
142
155
  ## Examples
143
156
 
144
- [`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 reducers and hooks, the AI-propose / human-accept loop, and the `inject_intro:` flag that puts an orientation preamble at the top of `CLAUDE.md`.
157
+ [`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_intro:` flag that puts an orientation preamble at the top of `CLAUDE.md`.
145
158
 
146
159
  ## Tests
147
160