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.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +2 -241
- data/CHANGELOG.md +221 -0
- data/README.md +89 -69
- data/SPEC.md +359 -212
- data/docs/conventions.md +42 -37
- data/lib/textus/boot.rb +122 -87
- data/lib/textus/cli/group/{refresh.rb → fetch.rb} +4 -4
- data/lib/textus/cli/verb/build.rb +1 -1
- data/lib/textus/cli/verb/fetch.rb +14 -0
- data/lib/textus/cli/verb/{refresh_stale.rb → fetch_stale.rb} +3 -3
- data/lib/textus/cli/verb/get.rb +1 -1
- data/lib/textus/cli/verb/hooks.rb +1 -1
- data/lib/textus/cli/verb/mcp_serve.rb +8 -3
- data/lib/textus/cli/verb/propose.rb +28 -0
- data/lib/textus/cli/verb/pulse.rb +12 -3
- data/lib/textus/cli/verb/put.rb +1 -1
- data/lib/textus/cli/verb/rule_list.rb +7 -7
- data/lib/textus/cli/verb/schema.rb +1 -1
- data/lib/textus/cli/verb.rb +3 -2
- data/lib/textus/cli.rb +2 -2
- data/lib/textus/container.rb +1 -2
- data/lib/textus/contract.rb +106 -0
- data/lib/textus/cursor_store.rb +24 -0
- data/lib/textus/dispatcher.rb +6 -4
- data/lib/textus/doctor/check/audit_log.rb +1 -1
- data/lib/textus/doctor/check/{refresh_locks.rb → fetch_locks.rb} +8 -8
- data/lib/textus/doctor/check/proposal_targets.rb +45 -0
- data/lib/textus/doctor/check/rule_ambiguity.rb +3 -3
- data/lib/textus/doctor.rb +2 -1
- data/lib/textus/domain/action.rb +3 -3
- data/lib/textus/domain/freshness/evaluator.rb +3 -3
- data/lib/textus/domain/freshness/policy.rb +2 -2
- data/lib/textus/domain/freshness.rb +7 -7
- data/lib/textus/domain/outcome.rb +2 -2
- data/lib/textus/domain/permission.rb +2 -10
- data/lib/textus/domain/policy/base_guards.rb +25 -0
- data/lib/textus/domain/policy/evaluation.rb +15 -0
- data/lib/textus/domain/policy/{refresh.rb → fetch.rb} +1 -1
- data/lib/textus/domain/policy/guard.rb +35 -0
- data/lib/textus/domain/policy/guard_factory.rb +40 -0
- data/lib/textus/domain/policy/predicates/author_held.rb +33 -0
- data/lib/textus/domain/policy/predicates/etag_match.rb +32 -0
- data/lib/textus/domain/policy/predicates/fresh_within.rb +58 -0
- data/lib/textus/domain/policy/predicates/registry.rb +39 -0
- data/lib/textus/domain/policy/predicates/schema_valid.rb +30 -19
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +33 -0
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +39 -0
- data/lib/textus/domain/staleness/intake_check.rb +6 -6
- data/lib/textus/envelope.rb +2 -2
- data/lib/textus/errors.rb +25 -28
- data/lib/textus/hooks/event_bus.rb +4 -4
- data/lib/textus/init.rb +27 -18
- data/lib/textus/layout.rb +41 -0
- data/lib/textus/maintenance/key_delete_prefix.rb +9 -0
- data/lib/textus/maintenance/key_mv_prefix.rb +10 -0
- data/lib/textus/maintenance/migrate.rb +9 -0
- data/lib/textus/maintenance/rule_lint.rb +8 -0
- data/lib/textus/maintenance/zone_mv.rb +11 -1
- data/lib/textus/manifest/capabilities.rb +29 -0
- data/lib/textus/manifest/data.rb +14 -10
- data/lib/textus/manifest/policy.rb +37 -21
- data/lib/textus/manifest/rules.rb +16 -14
- data/lib/textus/manifest/schema.rb +48 -58
- data/lib/textus/manifest.rb +3 -3
- data/lib/textus/mcp/catalog.rb +72 -0
- data/lib/textus/mcp/server.rb +8 -5
- data/lib/textus/mcp/session.rb +3 -20
- data/lib/textus/mcp/tool_schemas.rb +6 -62
- data/lib/textus/mcp/tools.rb +4 -119
- data/lib/textus/ports/audit_log.rb +17 -15
- data/lib/textus/ports/audit_subscriber.rb +1 -1
- data/lib/textus/ports/build_lock.rb +1 -2
- data/lib/textus/ports/{refresh → fetch}/detached.rb +4 -4
- data/lib/textus/ports/{refresh → fetch}/lock.rb +2 -2
- data/lib/textus/projection.rb +1 -1
- data/lib/textus/read/audit.rb +3 -3
- data/lib/textus/read/boot.rb +6 -0
- data/lib/textus/read/freshness.rb +9 -9
- data/lib/textus/read/get.rb +16 -8
- data/lib/textus/read/{get_or_refresh.rb → get_or_fetch.rb} +11 -11
- data/lib/textus/read/list.rb +8 -0
- data/lib/textus/read/policy_explain.rb +14 -10
- data/lib/textus/read/pulse.rb +12 -4
- data/lib/textus/read/rules.rb +24 -0
- data/lib/textus/read/schema_envelope.rb +7 -0
- data/lib/textus/read/validator.rb +1 -1
- data/lib/textus/role.rb +6 -2
- data/lib/textus/schema/tools.rb +5 -5
- data/lib/textus/session.rb +24 -0
- data/lib/textus/store.rb +11 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/accept.rb +19 -55
- data/lib/textus/write/delete.rb +14 -2
- data/lib/textus/write/{refresh_all.rb → fetch_all.rb} +14 -6
- data/lib/textus/write/{refresh_orchestrator.rb → fetch_orchestrator.rb} +14 -14
- data/lib/textus/write/{refresh_worker.rb → fetch_worker.rb} +29 -14
- data/lib/textus/write/mv.rb +15 -3
- data/lib/textus/write/propose.rb +46 -0
- data/lib/textus/write/put.rb +26 -2
- data/lib/textus/write/reject.rb +11 -5
- data/lib/textus.rb +4 -0
- metadata +36 -21
- data/lib/textus/cli/verb/refresh.rb +0 -14
- data/lib/textus/domain/authorizer.rb +0 -37
- data/lib/textus/domain/policy/predicates/accept_authority_signed.rb +0 -33
- data/lib/textus/domain/policy/promote.rb +0 -26
- data/lib/textus/domain/policy/promotion.rb +0 -57
- data/lib/textus/manifest/role_kinds.rb +0 -21
- data/lib/textus/write/authority_gate.rb +0 -24
data/README.md
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
**
|
|
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."
|
|
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
|
-
- **
|
|
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**
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 `
|
|
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
|
|
40
|
-
printf '%s' '{"_meta":{"name":"oncall","proposal":{"target_key":"
|
|
41
|
-
| textus put
|
|
42
|
-
# you accept it — textus promotes to
|
|
43
|
-
textus accept
|
|
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
|
|
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
|
-
- **
|
|
51
|
-
- **Wire textus into Claude Code via MCP** — 4 steps, ~5 minutes: [`
|
|
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: [`
|
|
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
|
-
##
|
|
96
|
+
## What `textus init` gives you
|
|
79
97
|
|
|
80
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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 `
|
|
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
|
|
108
|
-
textus list --zone=
|
|
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
|
|
111
|
-
textus freshness --zone=
|
|
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
|
|
146
|
+
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/).
|
|
119
147
|
|
|
120
148
|
## What's shipped
|
|
121
149
|
|
|
122
|
-
- **Per-entry formats.** `format: markdown
|
|
123
|
-
- **
|
|
124
|
-
- **
|
|
125
|
-
- **
|
|
126
|
-
-
|
|
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`).
|
|
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
|
|
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
|
|
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`, `:
|
|
161
|
-
- `:
|
|
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: { "
|
|
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
|
|
188
|
-
# or just
|
|
189
|
-
textus
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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).
|