textus 0.50.0 → 0.52.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/CHANGELOG.md +38 -0
- data/README.md +41 -43
- data/SPEC.md +176 -176
- data/docs/architecture/README.md +46 -42
- data/docs/reference/conventions.md +31 -26
- data/lib/textus/boot.rb +15 -17
- data/lib/textus/call.rb +1 -1
- data/lib/textus/cli/runner.rb +15 -10
- data/lib/textus/cli/verb/get.rb +1 -3
- data/lib/textus/cli/verb/hook_run.rb +1 -1
- data/lib/textus/cli/verb/put.rb +4 -20
- data/lib/textus/cli/verb/serve.rb +19 -0
- data/lib/textus/cli.rb +1 -3
- data/lib/textus/dispatcher.rb +3 -3
- data/lib/textus/doctor/check/generator_drift.rb +4 -3
- data/lib/textus/doctor/check/handler_allowlist.rb +1 -1
- data/lib/textus/doctor/check/intake_registration.rb +5 -5
- data/lib/textus/doctor/check/rule_ambiguity.rb +3 -3
- data/lib/textus/doctor/check/sentinels.rb +2 -2
- data/lib/textus/doctor/check/templates.rb +13 -11
- data/lib/textus/doctor.rb +0 -2
- data/lib/textus/domain/freshness/evaluator.rb +150 -14
- data/lib/textus/domain/freshness/verdict.rb +28 -6
- data/lib/textus/domain/freshness.rb +4 -33
- data/lib/textus/domain/jobs/job.rb +58 -0
- data/lib/textus/domain/jobs/registry.rb +37 -0
- data/lib/textus/domain/policy/base_guards.rb +1 -1
- data/lib/textus/domain/policy/predicates/fresh_within.rb +1 -1
- data/lib/textus/domain/policy/publish_target.rb +34 -0
- data/lib/textus/domain/policy/retention.rb +29 -0
- data/lib/textus/domain/policy/source.rb +73 -0
- data/lib/textus/domain/retention/sweep.rb +57 -0
- data/lib/textus/domain/retention.rb +11 -0
- data/lib/textus/errors.rb +4 -4
- data/lib/textus/hooks/builtin.rb +5 -5
- data/lib/textus/hooks/catalog.rb +7 -7
- data/lib/textus/hooks/context.rb +5 -10
- data/lib/textus/init/templates/machine_intake.rb +4 -4
- data/lib/textus/init.rb +47 -47
- data/lib/textus/jobs/handlers.rb +62 -0
- data/lib/textus/jobs/scheduler.rb +36 -0
- data/lib/textus/jobs/seeder.rb +57 -0
- data/lib/textus/key/matching.rb +24 -0
- data/lib/textus/layout.rb +8 -0
- data/lib/textus/maintenance/drain.rb +42 -0
- data/lib/textus/maintenance/retention/apply.rb +52 -0
- data/lib/textus/maintenance/serve.rb +30 -0
- data/lib/textus/maintenance/worker.rb +74 -0
- data/lib/textus/manifest/capabilities.rb +1 -1
- data/lib/textus/manifest/data.rb +18 -3
- data/lib/textus/manifest/entry/base.rb +28 -9
- data/lib/textus/manifest/entry/nested.rb +3 -4
- data/lib/textus/manifest/entry/parser.rb +25 -21
- data/lib/textus/manifest/entry/produced.rb +56 -0
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +7 -6
- data/lib/textus/manifest/entry/publish/to_paths.rb +62 -11
- data/lib/textus/manifest/entry/validators/format_matrix.rb +3 -11
- data/lib/textus/manifest/entry/validators/publish.rb +3 -1
- data/lib/textus/manifest/entry/validators.rb +0 -1
- data/lib/textus/manifest/policy.rb +16 -4
- data/lib/textus/manifest/resolver.rb +10 -4
- data/lib/textus/manifest/rules.rb +37 -36
- data/lib/textus/manifest/schema/keys.rb +98 -0
- data/lib/textus/manifest/schema/validator.rb +324 -0
- data/lib/textus/manifest/schema/vocabulary.rb +24 -0
- data/lib/textus/manifest/schema.rb +27 -247
- data/lib/textus/manifest.rb +5 -3
- data/lib/textus/mcp/server.rb +1 -1
- data/lib/textus/ports/audit_log.rb +6 -0
- data/lib/textus/ports/build_lock.rb +6 -0
- data/lib/textus/ports/clock.rb +4 -3
- data/lib/textus/ports/produce_on_write_subscriber.rb +73 -0
- data/lib/textus/ports/publisher.rb +11 -7
- data/lib/textus/ports/queue.rb +130 -0
- data/lib/textus/produce/acquire/handler.rb +29 -0
- data/lib/textus/produce/acquire/intake.rb +130 -0
- data/lib/textus/produce/acquire/projection.rb +127 -0
- data/lib/textus/produce/acquire/serializer/json.rb +31 -0
- data/lib/textus/produce/acquire/serializer/text.rb +16 -0
- data/lib/textus/produce/acquire/serializer/yaml.rb +31 -0
- data/lib/textus/produce/acquire/serializer.rb +17 -0
- data/lib/textus/produce/engine.rb +95 -0
- data/lib/textus/produce/events.rb +36 -0
- data/lib/textus/produce/render.rb +23 -0
- data/lib/textus/projection.rb +17 -6
- data/lib/textus/read/deps.rb +3 -3
- data/lib/textus/read/freshness.rb +61 -31
- data/lib/textus/read/get.rb +20 -102
- data/lib/textus/read/jobs.rb +31 -0
- data/lib/textus/read/rdeps.rb +3 -3
- data/lib/textus/read/rule_explain.rb +41 -23
- data/lib/textus/read/rule_list.rb +25 -8
- data/lib/textus/read/validate_all.rb +14 -0
- data/lib/textus/role.rb +2 -1
- data/lib/textus/schemas.rb +8 -0
- data/lib/textus/store.rb +1 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/enqueue.rb +50 -0
- data/lib/textus/write/put.rb +1 -1
- metadata +35 -30
- data/lib/textus/builder/pipeline.rb +0 -88
- data/lib/textus/builder/renderer/json.rb +0 -45
- data/lib/textus/builder/renderer/markdown.rb +0 -24
- data/lib/textus/builder/renderer/text.rb +0 -14
- data/lib/textus/builder/renderer/yaml.rb +0 -45
- data/lib/textus/builder/renderer.rb +0 -17
- data/lib/textus/cli/verb/boot.rb +0 -14
- data/lib/textus/cli/verb/build.rb +0 -15
- data/lib/textus/doctor/check/fetch_locks.rb +0 -49
- data/lib/textus/doctor/check/lifecycle_action_invalid.rb +0 -39
- data/lib/textus/domain/freshness/policy.rb +0 -18
- data/lib/textus/domain/lifecycle.rb +0 -83
- data/lib/textus/domain/outcome.rb +0 -10
- data/lib/textus/domain/policy/lifecycle.rb +0 -35
- data/lib/textus/domain/staleness/generator_check.rb +0 -109
- data/lib/textus/domain/staleness.rb +0 -29
- data/lib/textus/maintenance/tend.rb +0 -110
- data/lib/textus/manifest/entry/derived.rb +0 -67
- data/lib/textus/manifest/entry/intake.rb +0 -31
- data/lib/textus/manifest/entry/validators/inject_boot.rb +0 -21
- data/lib/textus/mcp/tools.rb +0 -14
- data/lib/textus/ports/fetch/detached.rb +0 -52
- data/lib/textus/ports/fetch/lock.rb +0 -44
- data/lib/textus/write/build.rb +0 -90
- data/lib/textus/write/fetch_events.rb +0 -42
- data/lib/textus/write/fetch_orchestrator.rb +0 -101
- data/lib/textus/write/fetch_worker.rb +0 -127
- data/lib/textus/write/intake_fetch.rb +0 -25
- data/lib/textus/write/materializer.rb +0 -51
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2095b69b4e135e71a1ae786759d299c8df4e78a0164a142411d0df72ce63aa4e
|
|
4
|
+
data.tar.gz: fb01669c8458fbfa0db50cff6314d30c9033abc02632998e431b2d11cb7e893c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 728c7e305b878cb68bf10af0bd93d9ca93b8b1a2be07a7ec7333de73f127415d4aeae112863bc9fa4d1cec67f51a0da25e2ad984b7b4283f4724c878241ff0c0
|
|
7
|
+
data.tar.gz: 7019ce011bf7297d430c2de9f8da591bc951d863e6834ba40c928eeca1bb0082626e2a0814c8bb6412288fb60659faedc5807e77258fbbcf3b3099672ddb19f4
|
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,44 @@ The **gem version** (`0.x.y`) is distinct from the **protocol version**
|
|
|
9
9
|
bump is a breaking change that requires a store migration; the gem version
|
|
10
10
|
tracks both additive improvements and breaking protocol bumps independently.
|
|
11
11
|
|
|
12
|
+
## 0.52.0 — 2026-06-09 — The authority model is a produced reference doc (ADR 0112)
|
|
13
|
+
|
|
14
|
+
The "who may write what" tables stop being hand-copied across the canon docs and become a fourth generated reference doc, projected from the source of truth on every `drain`.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`docs/reference/authority.md` is a produced reference doc** (ADR 0112). A new `authority` registry handler projects the `artifacts.derived.authority` entry — the zone-kind↔capability bijection (from `Schema::Vocabulary::LANES`), this manifest's zones, and its roles with the zone-kinds each can write — rendered through `authority.mustache`. It joins `verbs.md` / `schema.md` / `adr-log.md` on the ADR 0097/0102 produced-docs pattern; `GeneratorDrift` + `HandlerAllowlist` guard it (no new doctor check).
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **The authority model's SSoT splits along the produce/guard seam** (ADR 0112, refining ADR 0098). The *current-values* tables move to the generated `authority.md`; `reference/zones.md` drops its three hand-maintained tables, keeps what each capability *means* as prose, and links to the generated values. `explanation/concepts.md` and the docs index re-point accordingly; the orientation template now names four generated reference docs.
|
|
23
|
+
|
|
24
|
+
## 0.51.0 — 2026-06-08 — The reconcile era: one engine for produce + sweep, `source`/`retention`, produced reference docs (ADR 0087, 0090–0095, 0097)
|
|
25
|
+
|
|
26
|
+
`build` folds into `reconcile`, materialization becomes system-pushed, the producer kinds and machine zones collapse onto one `source` + `retention` grammar, and the machine-derivable reference docs graduate to produced artifacts.
|
|
27
|
+
|
|
28
|
+
### Changed (breaking)
|
|
29
|
+
|
|
30
|
+
- **`build` is removed; `tend` is now `reconcile`** (ADR 0087). One verb runs the full two-phase **produce → destructive-sweep** under a shared lock, and every canon write reactively rebuilds its dependent produced entries (`source.on_write: sync | async`). **Migration:** drop all `build` calls (CLI + MCP); rename scripted/cron `tend` to `reconcile` — the verb, the MCP tool id, and the audit-log verb string.
|
|
31
|
+
- **Capabilities collapse to four — `author`, `keep`, `propose`, `reconcile`** (ADR 0090). The `fetch` and `build` capabilities fold into `reconcile`; `automation` defaults to `[reconcile]`. **Migration:** a role with `can: [fetch]` / `can: [build]` (or the interim `ingest`) becomes `can: [reconcile]`; the old spellings are rejected at load with a hint.
|
|
32
|
+
- **One `machine` zone-kind and one `produced` entry-kind** (ADR 0091, 0095). Zone-kinds become `canon | workspace | machine | queue` — the former `quarantine` + `derived` zone-kinds fold into one `machine` kind (the kind ⟺ capability mapping is a bijection again; at most one `machine` zone). Entry-kinds become `leaf | nested | produced` — the former `derived` + `intake` entry-kinds fold into one `produced` kind, with the produce-method (intake / derived / external) read off `source.from`. **Migration:** `kind: quarantine` / `kind: derived` on a zone → `kind: machine`; `kind: derived` / `kind: intake` on an entry → `kind: produced`. Both are rejected at load with a fold hint; no shim.
|
|
33
|
+
- **`source:` + `retention:` replace `upkeep` / `lifecycle` / `materialize` / `intake` / `compute` / `template`** (ADR 0093). The old slots conflated *production* (how an entry's bytes are made) with *retention* (when an aged entry is retired). They split into an entry-level **`source: { from: project | handler | command }`** — one "acquire data from upstream" concept, where `from` must agree with `kind:` — and a glob-matched **`retention: { ttl, action: drop | archive }`** rule; both run through one produce engine. This is strictly more expressive (re-pull hourly *and* archive at 90 days is now sayable). The `lifecycle: { on_expire: warn }` form is removed (`warn` was dead since 0.50's pure-read `get`). **Migration:** rewrite `upkeep:` / `lifecycle:` / `materialize:` (and the old `intake:` / `compute:` / `template:` blocks) into `source:` + `retention:`. Old manifests fail at load with mechanical fold hints; no shim.
|
|
34
|
+
- **A `source` produces *data*; `publish:` is a list of targets** (ADR 0094). A produced entry's stored form is data (e.g. `.json`); rendering moves entirely to the publish path. **`publish:` is now a list** — each element a `{ to:, template?:, inject_boot?: }` file target (copies the data verbatim, or renders it through that target's own template) or a `{ tree: }` subtree mirror — so one dataset can render to differently-shaped targets (`CLAUDE.md` vs `AGENTS.md`) without bespoke handler code. Published artifacts are clean content; textus's `_meta` provenance stays in the stored entry. **Migration:** the *map* `publish: { to: [...] }` / `publish: { tree: }` forms (and the older `publish_to:` / `publish_tree:`) are rejected at load — `publish:` is a list.
|
|
35
|
+
- **Hook events renamed** (ADR 0094). RPC `:resolve_intake` → `:resolve_handler`; pub-sub `:entry_put` → `:entry_written`, `:build_completed` → `:entry_produced` (plus `:entry_published` and `produce_failed`). **Migration:** update hook subscriptions to the new names; old names fail at registration.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **Reference docs are produced, not hand-authored** (ADR 0097, 0098). `docs/reference/verbs.md` and `docs/reference/schema.md` (projected from the live verb/schema registry) and the new `docs/reference/adr-log.md` (projected from the ADR files) are `kind: produced` entries published out, with a CI gate that fails when `reconcile` is not a no-op. **Do not hand-edit them** — edit the upstream source (the verb/schema code, or an ADR) and run `reconcile`; `doctor` flags a stale hand-edit as `generator_drift`.
|
|
40
|
+
- **The root `README.md`, `CONTRIBUTING.md`, and `SECURITY.md` are canon, published out** (ADR 0103, 0104). They are authored under `.textus/zones/knowledge/` and published verbatim to the repo root with an editor banner (the same pattern as `docs/`); a hand-edit to a front-door doc is clobbered by `reconcile` and caught by the CI no-op gate.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **Docs SSoT/DRY cleanup** (ADR 0098). The duplicated ADR index is single-sourced — the mechanical status board lives in `docs/reference/adr-log.md`, and the curated decisions README becomes an annotated reading guide; the verb producer depends on `Read::Capabilities` rather than reaching into `Dispatcher` internals; a conformance guard keeps the `events` / `zones` / `mcp` projections covered.
|
|
45
|
+
|
|
46
|
+
### Internal
|
|
47
|
+
|
|
48
|
+
- **The architecture is now executable, and the produce / schema / port layers are decomposed** (ADR 0092, 0099–0101, 0105–0109). The hexagonal layering is enforced by a conformance guard with the layer map written to `lib/textus/ARCHITECTURE.md` (0106); the verb token gains a build-time route ⟺ contract bijection guard (0105); the divergent staleness comparisons collapse into one `Domain::Freshness::Evaluator` (0099); the produce pipeline is gathered under `lib/textus/produce/` with an `acquire` ÷ `render` split and the `fetch_*` fossils renamed (0100); two interface fossils are removed (0101); `manifest/schema.rb` splits its validation walk into `Schema::Validator` and its constants into `Schema::Vocabulary` + `Schema::Keys` (0107, 0109); every port becomes one shape — an instantiable class (0108, 0109); and the conformance spec tier is consolidated (67→19 loose) and coupled to the contract rather than to fixture spelling (0092). No behaviour, manifest-grammar, or verb-surface change.
|
|
49
|
+
|
|
12
50
|
## 0.50.0 — 2026-06-04 — Observability on two axes + boot lifecycle (ADR 0083, 0084, 0085)
|
|
13
51
|
|
|
14
52
|
`freshness` collapses into `pulse`, the contract-drift guard stops deadlocking, and `boot` gains a lean session-start projection shipped via a plugin.
|
data/README.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<!-- Generated from .textus/zones/knowledge/readme.md — edit there, then run `textus drain`. Do not hand-edit README.md (it is clobbered on drain and flagged by doctor). ADR 0103. -->
|
|
1
2
|
<p align="center">
|
|
2
3
|
<picture>
|
|
3
4
|
<source media="(prefers-color-scheme: dark)" srcset="assets/branding/wordmark-dark.png">
|
|
@@ -37,11 +38,9 @@ flowchart LR
|
|
|
37
38
|
human -->|author| knowledge["knowledge<br/>(canon)"]
|
|
38
39
|
agent -->|keep| notebook["notebook<br/>(workspace)"]
|
|
39
40
|
agent -->|propose| proposals["proposals<br/>(queue)"]
|
|
40
|
-
automation -->|
|
|
41
|
-
automation -->|build| artifacts["artifacts<br/>(derived)"]
|
|
41
|
+
automation -->|drain| artifacts["artifacts<br/>(machine)"]
|
|
42
42
|
|
|
43
43
|
proposals ==>|human accept| knowledge
|
|
44
|
-
feeds -.->|projection source| artifacts
|
|
45
44
|
knowledge -.->|projection source| artifacts
|
|
46
45
|
|
|
47
46
|
classDef actor fill:#238636,stroke:#2ea043,color:#fff;
|
|
@@ -65,23 +64,22 @@ DURABLE │ notebook │ knowledge ★ the goal
|
|
|
65
64
|
(kept) │ agent's working truth │ canon — a human authors │
|
|
66
65
|
│ durable, but low-trust │ here · the context you ship │
|
|
67
66
|
├──────────────────────────┼───────────────────────────────┤
|
|
68
|
-
TRANSIENT │ feeds
|
|
67
|
+
TRANSIENT │ artifacts.feeds.* │ proposals (queue) │
|
|
69
68
|
(staging) │ raw external input, │ a candidate, in review │
|
|
70
|
-
│ unverified
|
|
69
|
+
│ unverified (machine) │ ▲ climbs via human accept │
|
|
71
70
|
└──────────────────────────┴───────────────────────────────┘
|
|
72
71
|
raw material ──── propose ────► a human accept lifts it to canon
|
|
73
72
|
```
|
|
74
73
|
|
|
75
|
-
*(The
|
|
74
|
+
*(The `machine` lane's other half, `artifacts.derived.*`, isn't on this grid — it's a computed **output** projected from the lanes, not an input climbing toward trust.)*
|
|
76
75
|
|
|
77
76
|
Without coordination, they overwrite each other and nothing remembers why. textus gives each actor a **lane** — called a **zone** in the manifest and CLI, the term used everywhere technical from here on — routes everything they can't write directly through a **proposals queue**, and writes every successful change to an **append-only audit log**. The lanes are enforced at the protocol level, not by convention.
|
|
78
77
|
|
|
79
78
|
```
|
|
80
79
|
knowledge/ author only — who you are, what you decide, how you sound (knowledge.identity.* for identity facts)
|
|
81
80
|
notebook/ keep only — agent's own durable lane (agents keep theirs; bytes climb to knowledge only via propose→accept)
|
|
82
|
-
feeds/ fetch only — declared external inputs
|
|
83
81
|
proposals/ propose (agent + human) — proposals waiting on a human accept
|
|
84
|
-
artifacts/
|
|
82
|
+
artifacts/ converge only — machine-maintained: external inputs (artifacts.feeds.*) + computed outputs (artifacts.derived.*)
|
|
85
83
|
```
|
|
86
84
|
|
|
87
85
|
An agent that tries to write directly into `knowledge/` gets `write_forbidden`. It writes to `proposals/` (to change authoritative content) or its own `notebook/` (for working memory). You accept the good proposals; textus promotes them, records the move, and audits both halves. Stable per-entry `uid:` means a reorganization doesn't break references. A monotonic audit cursor (`textus pulse --since=N`) means the next session — possibly a different agent, possibly a different model — picks up exactly where the last one left off.
|
|
@@ -111,7 +109,7 @@ Try the gate the other way (`textus put knowledge.notes.X --as=agent`) and you g
|
|
|
111
109
|
|
|
112
110
|
## Try it
|
|
113
111
|
|
|
114
|
-
- **Worked end-to-end store** — the role gate (propose → accept),
|
|
112
|
+
- **Worked end-to-end store** — the role gate (propose → accept), drain/publish (`CLAUDE.md` / `AGENTS.md` generated from knowledge entries), schemas, templates, and a hook: [`examples/project/`](examples/project/)
|
|
115
113
|
- **Wire textus into Claude Code via MCP** — 4 steps, ~5 minutes: [`docs/how-to/agents-mcp.md`](docs/how-to/agents-mcp.md)
|
|
116
114
|
|
|
117
115
|
## Protocol, not just a gem
|
|
@@ -139,20 +137,19 @@ bundle exec exe/textus --help
|
|
|
139
137
|
|
|
140
138
|
## What `textus init` gives you
|
|
141
139
|
|
|
142
|
-
You get `.textus/` with all
|
|
140
|
+
You get `.textus/` with all four zone directories, baseline schemas, a starter manifest, and a gitignored `.run/` for disposable runtime state (the audit log, per-role cursors, produce locks). Roles declare capabilities; each zone declares a `kind:`, and write authority is derived from the role's capabilities crossed with the zone's kind:
|
|
143
141
|
|
|
144
142
|
```yaml
|
|
145
143
|
roles:
|
|
146
144
|
- { name: human, can: [author, propose] }
|
|
147
145
|
- { name: agent, can: [propose, keep] }
|
|
148
|
-
- { name: automation, can: [
|
|
146
|
+
- { name: automation, can: [converge] }
|
|
149
147
|
|
|
150
148
|
zones:
|
|
151
|
-
- { name: knowledge,
|
|
152
|
-
- { name: notebook,
|
|
153
|
-
- { name:
|
|
154
|
-
- { name:
|
|
155
|
-
- { name: artifacts, kind: derived } # build — computed outputs
|
|
149
|
+
- { name: knowledge, kind: canon } # author — canonical truth
|
|
150
|
+
- { name: notebook, kind: workspace } # keep — agent's own durable lane
|
|
151
|
+
- { name: proposals, kind: queue } # propose — proposals awaiting accept
|
|
152
|
+
- { name: artifacts, kind: machine } # converge — external inputs (artifacts.feeds.*) + computed outputs (artifacts.derived.*)
|
|
156
153
|
```
|
|
157
154
|
|
|
158
155
|
```
|
|
@@ -165,14 +162,13 @@ zones:
|
|
|
165
162
|
zones/ # one dir per zone; kinds + capabilities are in the manifest above
|
|
166
163
|
knowledge/ # e.g. identity (knowledge.identity.*), voice, decisions, notes
|
|
167
164
|
notebook/
|
|
168
|
-
feeds/
|
|
169
165
|
proposals/
|
|
170
|
-
artifacts/
|
|
166
|
+
artifacts/ # machine lane: feeds/ (external inputs) + derived/ (computed outputs)
|
|
171
167
|
.run/ # disposable runtime state — gitignored, safe to delete (ADR 0038)
|
|
172
168
|
audit/audit.log # append-only NDJSON event ledger, every write (rotates at ~50 MB)
|
|
173
169
|
state/cursor.<role> # per-role pulse cursor — where `pulse --since` resumes
|
|
174
|
-
locks/
|
|
175
|
-
sentinels/ # publish bookkeeping (target sha) — regenerated on
|
|
170
|
+
locks/ # per-key produce locks + the produce mutex
|
|
171
|
+
sentinels/ # publish bookkeeping (target sha) — regenerated on drain (ADR 0070)
|
|
176
172
|
```
|
|
177
173
|
|
|
178
174
|
Manifest `path:` fields are relative to `.textus/zones/`. So `knowledge.notes.org.jane` lives at `.textus/zones/knowledge/notes/org/jane.md`.
|
|
@@ -184,20 +180,20 @@ textus get knowledge.notes.org.jane
|
|
|
184
180
|
textus list --zone=knowledge
|
|
185
181
|
printf '%s' '{"_meta":{"name":"bob","org":"acme"},"body":"hi\n"}' \
|
|
186
182
|
| textus put knowledge.notes.bob --as=human --stdin
|
|
187
|
-
textus
|
|
183
|
+
textus drain --as=automation # re-pull stale inputs + recompute derived outputs
|
|
188
184
|
textus rule list # show every rule block
|
|
189
185
|
textus audit --limit=20 # query the audit log
|
|
190
186
|
```
|
|
191
187
|
|
|
192
188
|
(All verbs return JSON envelopes; `--output=json` is the default and the only format in v1.)
|
|
193
189
|
|
|
194
|
-
For a worked store — knowledge entries, a staged proposal, schemas, a template, and a
|
|
190
|
+
For a worked store — knowledge entries, a staged proposal, schemas, a template, and a `drain` that publishes `CLAUDE.md` / `AGENTS.md` — see [`examples/project/`](examples/project/).
|
|
195
191
|
|
|
196
192
|
## What's shipped
|
|
197
193
|
|
|
198
194
|
- **Per-entry formats & publish.** `format: markdown|json|yaml|text` per entry; a typed `publish:` block (`to:` for file fan-out, `tree:` for a whole-subtree mirror) byte-copies derived files to their consumer paths. ([SPEC §5.2–5.3](SPEC.md))
|
|
199
195
|
- **Stable identity.** Auto-minted `uid:` survives writes and `textus key mv`; reorganising never breaks references.
|
|
200
|
-
- **Capability × zone-kind gate.** Writes carry `--as=<role>`; a role may write a zone iff it holds the capability the zone's `kind:` requires (`canon`→`author`, `workspace`→`keep`, `
|
|
196
|
+
- **Capability × zone-kind gate.** Writes carry `--as=<role>`; a role may write a zone iff it holds the capability the zone's `kind:` requires (`canon`→`author`, `workspace`→`keep`, `machine`→`converge`, `queue`→`propose`). The wrong role gets `write_forbidden` naming the capability needed and the roles that hold it. ([SPEC §5](SPEC.md))
|
|
201
197
|
- **Agent loop.** `textus boot` orients a fresh session; `textus pulse --since=N` is the per-turn heartbeat (changed entries, stale keys, pending proposals). ([docs/how-to/agents-mcp.md](docs/how-to/agents-mcp.md))
|
|
202
198
|
- **`textus doctor`.** Health checks across schemas, hooks, keys, sentinels, and the audit log.
|
|
203
199
|
|
|
@@ -210,13 +206,15 @@ Every command operates on one store, located in this order: `--root <path>` flag
|
|
|
210
206
|
|
|
211
207
|
`textus boot` prints the same information for the current store: zones, entry families with schemas, registered hooks, write flows, and the verb catalog. Run it inside a store and you get the live picture; reach for the SPEC when you want the contract.
|
|
212
208
|
|
|
213
|
-
##
|
|
209
|
+
## Produce and publish
|
|
214
210
|
|
|
215
|
-
|
|
211
|
+
Produced entries (`kind: produced`) declare how they're acquired in one `source:` block (ADR 0093/0094); `drain` materialises them:
|
|
216
212
|
|
|
217
|
-
|
|
213
|
+
- **`source: { from: project, select: [...], pluck:, sort_by:, limit:, transform: name }`** — a *projection*: textus computes the entry's data from other entries, then renders it through a template under `.textus/templates/` (markdown/text) or a templateless path that lets a transform hook shape the output directly (json/yaml). Projections cap at 1000 rows; the vendored Mustache subset caps at depth 8. No partials, no lambdas, no HTML escaping.
|
|
214
|
+
- **`source: { from: handler, handler: name, ttl: 1h, config: {...} }`** — *intake*: an RPC handler pulls external bytes on a `ttl` cadence; `drain` re-pulls when the entry goes stale.
|
|
215
|
+
- **`source: { from: command, sources: [...] }`** — *externally generated*: an out-of-band command writes the file; textus tracks the declared `sources` for staleness.
|
|
218
216
|
|
|
219
|
-
Publishing is one typed `publish:` block (ADR 0052). `publish: { to: [path, ...] }` byte-copies a single
|
|
217
|
+
Publishing is one typed `publish:` block (ADR 0052). `publish: { to: [path, ...] }` byte-copies a single produced file to one or more targets. `publish: { tree: "dir" }` on a nested entry mirrors its whole stored subtree to one target directory, preserving layout (path-driven — no keys or template variables). Sentinels for every published file live under `.textus/.run/sentinels/` (git-ignored runtime state, regenerated on drain — ADR 0070). See SPEC §5.2, §5.3, §5.12.
|
|
220
218
|
|
|
221
219
|
## Extension points
|
|
222
220
|
|
|
@@ -226,7 +224,7 @@ textus exposes a hook DSL. Drop `.rb` files into `.textus/hooks/` (subdirectorie
|
|
|
226
224
|
|
|
227
225
|
| Event | Fires when | You return |
|
|
228
226
|
|---|---|---|
|
|
229
|
-
| `:
|
|
227
|
+
| `:resolve_handler` | an intake needs bytes | `{_meta:, body:}` |
|
|
230
228
|
| `:transform_rows` | a projection builds | the reshaped rows |
|
|
231
229
|
| `:validate` | `textus doctor` runs | doctor issues (or none) |
|
|
232
230
|
|
|
@@ -234,19 +232,19 @@ textus exposes a hook DSL. Drop `.rb` files into `.textus/hooks/` (subdirectorie
|
|
|
234
232
|
|
|
235
233
|
| Event(s) | Fires when |
|
|
236
234
|
|---|---|
|
|
237
|
-
| `:
|
|
238
|
-
| `:entry_fetched` |
|
|
239
|
-
| `:
|
|
240
|
-
| `:
|
|
235
|
+
| `:entry_written` · `:entry_deleted` · `:entry_renamed` | a write lands |
|
|
236
|
+
| `:entry_fetched` | an intake-driven write lands |
|
|
237
|
+
| `:entry_produced` | a produced entry materializes |
|
|
238
|
+
| `:entry_published` | a produced file is copied to its target |
|
|
241
239
|
| `:proposal_accepted` · `:proposal_rejected` | a proposal is resolved |
|
|
242
|
-
| `:
|
|
243
|
-
| `:store_loaded` | the store
|
|
240
|
+
| `:entry_fetch_started` · `:entry_fetch_failed` · `:produce_failed` | produce lifecycle |
|
|
241
|
+
| `:store_loaded` · `:session_opened` | the store loads · a role connects |
|
|
244
242
|
|
|
245
243
|
```ruby
|
|
246
244
|
# Inside .textus/hooks/local_file.rb
|
|
247
245
|
Textus.hook do |reg|
|
|
248
|
-
reg.on(:
|
|
249
|
-
path = config["path"] or raise "local-file requires
|
|
246
|
+
reg.on(:resolve_handler, :local_file) do |config:, args:, **|
|
|
247
|
+
path = config["path"] or raise "local-file requires source.config.path"
|
|
250
248
|
{
|
|
251
249
|
_meta: { "last_fetched_at" => Time.now.utc.iso8601, "source_path" => path },
|
|
252
250
|
body: File.read(File.expand_path(path)),
|
|
@@ -263,14 +261,14 @@ Textus.hook do |reg|
|
|
|
263
261
|
end
|
|
264
262
|
```
|
|
265
263
|
|
|
266
|
-
Stale intake entries
|
|
267
|
-
|
|
268
|
-
|
|
264
|
+
Stale intake entries are re-pulled by `drain`, not by reads — `get` is a pure
|
|
265
|
+
read that annotates the returned envelope with a freshness verdict (ADR 0089).
|
|
266
|
+
`drain` re-pulls anything past its `source.ttl` and recomputes derived outputs:
|
|
269
267
|
|
|
270
268
|
```sh
|
|
271
|
-
textus
|
|
272
|
-
|
|
273
|
-
textus get feeds.calendar.events
|
|
269
|
+
textus drain --as=automation # re-pull every stale intake + recompute derived
|
|
270
|
+
textus drain artifacts.feeds --as=automation # scope to one prefix
|
|
271
|
+
textus get artifacts.feeds.calendar.events # a pure read; carries a freshness verdict
|
|
274
272
|
```
|
|
275
273
|
|
|
276
274
|
See SPEC.md §5.10 for the full hook contract.
|
|
@@ -281,7 +279,7 @@ See [`docs/how-to/agents-mcp.md`](docs/how-to/agents-mcp.md) for the agent boot
|
|
|
281
279
|
|
|
282
280
|
## Examples
|
|
283
281
|
|
|
284
|
-
[`examples/project/`](examples/project/) — textus as a project's own context store (a fictional Rails service, `ledger`). Human-authored `knowledge/` (project facts, runbooks), a staged ADR in `proposals/` showing the agent-propose / human-accept loop, schemas validating each family, a mustache template plus a `:transform_rows` hook, and a `
|
|
282
|
+
[`examples/project/`](examples/project/) — textus as a project's own context store (a fictional Rails service, `ledger`). Human-authored `knowledge/` (project facts, runbooks), a staged ADR in `proposals/` showing the agent-propose / human-accept loop, schemas validating each family, a mustache template plus a `:transform_rows` hook, and a `drain` that publishes the `artifacts.derived.orientation` projection to `CLAUDE.md` and `AGENTS.md`. Includes a copy-paste adoption recipe for your own repo.
|
|
285
283
|
|
|
286
284
|
## Tests
|
|
287
285
|
|