textus 0.52.0 → 0.53.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 +25 -0
- data/README.md +62 -54
- data/SPEC.md +62 -187
- data/docs/architecture/README.md +88 -77
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +53 -0
- data/lib/textus/action/audit.rb +133 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +30 -22
- data/lib/textus/action/boot.rb +26 -0
- data/lib/textus/action/data_mv.rb +71 -0
- data/lib/textus/action/deps.rb +48 -0
- data/lib/textus/action/doctor.rb +26 -0
- data/lib/textus/action/drain.rb +41 -0
- data/lib/textus/action/enqueue.rb +55 -0
- data/lib/textus/action/get.rb +80 -0
- data/lib/textus/action/jobs.rb +38 -0
- data/lib/textus/action/key_delete.rb +46 -0
- data/lib/textus/action/key_delete_prefix.rb +46 -0
- data/lib/textus/action/key_mv.rb +143 -0
- data/lib/textus/action/key_mv_prefix.rb +59 -0
- data/lib/textus/action/list.rb +44 -0
- data/lib/textus/action/propose.rb +54 -0
- data/lib/textus/action/published.rb +26 -0
- data/lib/textus/action/pulse/scanner.rb +118 -0
- data/lib/textus/action/pulse.rb +87 -0
- data/lib/textus/action/put.rb +63 -0
- data/lib/textus/action/rdeps.rb +49 -0
- data/lib/textus/action/reject.rb +49 -0
- data/lib/textus/action/rule_explain.rb +95 -0
- data/lib/textus/action/rule_lint.rb +70 -0
- data/lib/textus/action/rule_list.rb +46 -0
- data/lib/textus/action/schema_envelope.rb +31 -0
- data/lib/textus/action/uid.rb +35 -0
- data/lib/textus/action/where.rb +38 -0
- data/lib/textus/action/write_verb.rb +58 -0
- data/lib/textus/background/job/base.rb +27 -0
- data/lib/textus/background/job/materialize.rb +31 -0
- data/lib/textus/background/job/refresh.rb +22 -0
- data/lib/textus/background/job/sweep.rb +31 -0
- data/lib/textus/background/job.rb +19 -0
- data/lib/textus/background/plan.rb +9 -0
- data/lib/textus/background/planner/plan.rb +113 -0
- data/lib/textus/{maintenance → background}/retention/apply.rb +7 -9
- data/lib/textus/background/worker.rb +67 -0
- data/lib/textus/boot.rb +53 -45
- data/lib/textus/command.rb +36 -0
- data/lib/textus/container.rb +1 -1
- data/lib/textus/{domain → core}/duration.rb +1 -1
- data/lib/textus/{domain → core}/freshness/evaluator.rb +11 -11
- data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
- data/lib/textus/{domain → core}/freshness.rb +2 -2
- data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
- data/lib/textus/{domain → core}/retention.rb +2 -2
- data/lib/textus/{domain → core}/sentinel.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +1 -1
- data/lib/textus/doctor/check/handler_permit.rb +34 -0
- data/lib/textus/doctor/check/hooks.rb +11 -18
- data/lib/textus/doctor/check/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/intake_registration.rb +5 -5
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_violations.rb +8 -2
- data/lib/textus/doctor/check.rb +12 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +6 -6
- data/lib/textus/envelope/io/writer.rb +65 -36
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +17 -9
- data/lib/textus/events.rb +21 -0
- data/lib/textus/gate/auth.rb +181 -0
- data/lib/textus/gate.rb +114 -0
- data/lib/textus/init/templates/machine_intake.rb +39 -35
- data/lib/textus/init/templates/orientation_reducer.rb +15 -11
- data/lib/textus/init.rb +90 -73
- data/lib/textus/key/path.rb +9 -2
- data/lib/textus/layout.rb +13 -0
- data/lib/textus/manifest/data.rb +14 -14
- data/lib/textus/manifest/entry/base.rb +15 -11
- data/lib/textus/manifest/entry/parser.rb +6 -6
- data/lib/textus/manifest/entry/produced.rb +3 -2
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -1
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/{domain/policy/handler_allowlist.rb → manifest/policy/handler_permit.rb} +4 -4
- data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
- data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
- data/lib/textus/manifest/policy/react.rb +30 -0
- data/lib/textus/{domain → manifest}/policy/retention.rb +3 -3
- data/lib/textus/{domain → manifest}/policy/source.rb +24 -19
- data/lib/textus/manifest/policy.rb +36 -48
- data/lib/textus/manifest/resolver.rb +3 -2
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +17 -11
- data/lib/textus/manifest/schema/validator.rb +24 -22
- data/lib/textus/manifest/schema/vocabulary.rb +1 -1
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/handler.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/intake.rb +22 -20
- data/lib/textus/{produce → pipeline}/acquire/projection.rb +13 -11
- data/lib/textus/{produce → pipeline}/acquire/serializer/json.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer/text.rb +1 -1
- data/lib/textus/{produce → pipeline}/acquire/serializer/yaml.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer.rb +1 -1
- data/lib/textus/{produce → pipeline}/engine.rb +7 -5
- data/lib/textus/{produce → pipeline}/render.rb +3 -1
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/ports/audit_subscriber.rb +4 -4
- data/lib/textus/{domain/jobs → ports/queue}/job.rb +19 -12
- data/lib/textus/ports/queue.rb +1 -1
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/projection.rb +8 -8
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/step/base.rb +35 -0
- data/lib/textus/step/builtin/csv_fetch.rb +19 -0
- data/lib/textus/step/builtin/ical_events_fetch.rb +30 -0
- data/lib/textus/step/builtin/json_fetch.rb +18 -0
- data/lib/textus/step/builtin/markdown_links_fetch.rb +20 -0
- data/lib/textus/step/builtin/rss_fetch.rb +26 -0
- data/lib/textus/step/builtin.rb +22 -0
- data/lib/textus/{hooks → step}/catalog.rb +3 -3
- data/lib/textus/{hooks → step}/context.rb +15 -13
- data/lib/textus/step/discovery.rb +24 -0
- data/lib/textus/{hooks → step}/error_log.rb +1 -1
- data/lib/textus/{hooks → step}/event_bus.rb +15 -16
- data/lib/textus/step/fetch.rb +13 -0
- data/lib/textus/{hooks → step}/fire_report.rb +1 -1
- data/lib/textus/step/loader.rb +108 -0
- data/lib/textus/step/observe.rb +31 -0
- data/lib/textus/step/registry_store.rb +66 -0
- data/lib/textus/{hooks → step}/signature.rb +1 -1
- data/lib/textus/step/transform.rb +12 -0
- data/lib/textus/step/validate.rb +11 -0
- data/lib/textus/step.rb +10 -0
- data/lib/textus/store.rb +17 -15
- data/lib/textus/surfaces/cli/group/data.rb +11 -0
- data/lib/textus/surfaces/cli/group/key.rb +11 -0
- data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
- data/lib/textus/surfaces/cli/group/rule.rb +11 -0
- data/lib/textus/surfaces/cli/group/schema.rb +11 -0
- data/lib/textus/surfaces/cli/group.rb +50 -0
- data/lib/textus/surfaces/cli/runner.rb +236 -0
- data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
- data/lib/textus/surfaces/cli/verb/get.rb +21 -0
- data/lib/textus/surfaces/cli/verb/init.rb +20 -0
- data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
- data/lib/textus/surfaces/cli/verb/put.rb +30 -0
- data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
- data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
- data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
- data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
- data/lib/textus/surfaces/cli/verb.rb +111 -0
- data/lib/textus/surfaces/cli.rb +148 -0
- data/lib/textus/surfaces/mcp/catalog.rb +99 -0
- data/lib/textus/surfaces/mcp/errors.rb +34 -0
- data/lib/textus/surfaces/mcp/server.rb +145 -0
- data/lib/textus/surfaces/mcp/session.rb +9 -0
- data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
- data/lib/textus/surfaces/mcp.rb +8 -0
- data/lib/textus/surfaces/role_scope.rb +38 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +64 -22
- metadata +132 -118
- data/lib/textus/cli/group/hook.rb +0 -9
- data/lib/textus/cli/group/key.rb +0 -9
- data/lib/textus/cli/group/mcp.rb +0 -9
- data/lib/textus/cli/group/rule.rb +0 -9
- data/lib/textus/cli/group/schema.rb +0 -9
- data/lib/textus/cli/group/zone.rb +0 -9
- data/lib/textus/cli/group.rb +0 -48
- data/lib/textus/cli/runner.rb +0 -193
- data/lib/textus/cli/verb/doctor.rb +0 -17
- data/lib/textus/cli/verb/get.rb +0 -18
- data/lib/textus/cli/verb/hook_run.rb +0 -48
- data/lib/textus/cli/verb/hooks.rb +0 -50
- data/lib/textus/cli/verb/init.rb +0 -18
- data/lib/textus/cli/verb/mcp_serve.rb +0 -22
- data/lib/textus/cli/verb/put.rb +0 -30
- data/lib/textus/cli/verb/schema_diff.rb +0 -15
- data/lib/textus/cli/verb/schema_init.rb +0 -19
- data/lib/textus/cli/verb/schema_migrate.rb +0 -19
- data/lib/textus/cli/verb/serve.rb +0 -19
- data/lib/textus/cli/verb.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -54
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/domain/action.rb +0 -9
- data/lib/textus/domain/jobs/registry.rb +0 -37
- data/lib/textus/domain/permission.rb +0 -7
- data/lib/textus/domain/policy/base_guards.rb +0 -25
- data/lib/textus/domain/policy/evaluation.rb +0 -15
- data/lib/textus/domain/policy/guard.rb +0 -35
- data/lib/textus/domain/policy/guard_factory.rb +0 -40
- data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
- data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
- data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
- data/lib/textus/domain/policy/predicates/registry.rb +0 -39
- data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
- data/lib/textus/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/jobs/handlers.rb +0 -62
- data/lib/textus/jobs/scheduler.rb +0 -36
- data/lib/textus/jobs/seeder.rb +0 -57
- data/lib/textus/maintenance/drain.rb +0 -42
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/serve.rb +0 -30
- data/lib/textus/maintenance/worker.rb +0 -74
- data/lib/textus/maintenance/zone_mv.rb +0 -64
- data/lib/textus/maintenance.rb +0 -15
- data/lib/textus/mcp/catalog.rb +0 -70
- data/lib/textus/mcp/errors.rb +0 -32
- data/lib/textus/mcp/server.rb +0 -138
- data/lib/textus/mcp/session.rb +0 -7
- data/lib/textus/mcp/tool_schemas.rb +0 -15
- data/lib/textus/mcp.rb +0 -6
- data/lib/textus/mustache.rb +0 -117
- data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
- data/lib/textus/produce/events.rb +0 -36
- data/lib/textus/read/audit.rb +0 -130
- data/lib/textus/read/boot.rb +0 -26
- data/lib/textus/read/capabilities.rb +0 -70
- data/lib/textus/read/deps.rb +0 -38
- data/lib/textus/read/doctor.rb +0 -27
- data/lib/textus/read/freshness.rb +0 -152
- data/lib/textus/read/get.rb +0 -73
- data/lib/textus/read/jobs.rb +0 -31
- data/lib/textus/read/list.rb +0 -24
- data/lib/textus/read/published.rb +0 -22
- data/lib/textus/read/pulse.rb +0 -98
- data/lib/textus/read/rdeps.rb +0 -39
- data/lib/textus/read/rule_explain.rb +0 -96
- data/lib/textus/read/rule_list.rb +0 -54
- data/lib/textus/read/schema_envelope.rb +0 -25
- data/lib/textus/read/uid.rb +0 -29
- data/lib/textus/read/validate_all.rb +0 -36
- data/lib/textus/read/where.rb +0 -24
- data/lib/textus/role_scope.rb +0 -78
- data/lib/textus/write/accept.rb +0 -58
- data/lib/textus/write/enqueue.rb +0 -50
- data/lib/textus/write/key_delete.rb +0 -65
- data/lib/textus/write/key_mv.rb +0 -141
- data/lib/textus/write/propose.rb +0 -54
- data/lib/textus/write/put.rb +0 -74
- data/lib/textus/write/reject.rb +0 -68
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c7df129e69143765baa5302608a7813c0666d3c9d91266e0a47a4ece61a4f9f6
|
|
4
|
+
data.tar.gz: 2362c085078165a6da46e55dac29b75346d0f14527de514898f1f5136b094b21
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a120d5d78f679fb9019817bf8491e00abe1145416c95faf6f80d5f62d078dbe8a054d15e5ec080ff5544eb3b2e02466130617db2d7d4fbf32dab05d3d8295fe1
|
|
7
|
+
data.tar.gz: a4dd565844225f88ae27683e37b6f508a3e64a6828a4b09ee472b815576b57eb9fd1ccf86d4cea91ecd92dde7ca59f0288ac01abf786afb9cce4833c28bb088c
|
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,31 @@ 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.53.0 — 2026-06-15 — Auth consolidation and code quality
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`bin/dev`**: local script for syncing skills from GitHub during development.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **Gate dispatch consolidated** (PR #215): removed redundant `container:` param from `Gate#dispatch` (always `@container`); removed dead `Textus::Dispatch` module; removed double auth call in `Accept`; removed dead `else` branch and `role_filter` from `RoleScope` verb dispatch.
|
|
21
|
+
- **`Background::Planner::Planner` renamed** to `Planner::Plan` to resolve the double-name antipattern.
|
|
22
|
+
- **`Writer#put` decomposed** (38→12 lines): extracted into 8 single-purpose sub-methods (resolve, uid, serialize, schema-validate, etag-check, persist, envelope, audit).
|
|
23
|
+
- **`Gate::Auth#check!` / `check_action!` deduplicated**: shared predicate-evaluation logic extracted into `evaluate_predicates`.
|
|
24
|
+
- **`Init.run` split** (30→10 lines): 8 focused class methods (`check_target!`, `create_directories`, `write_steps_readme`, `write_manifest`, `scaffold_agent`, `setup_state_dirs`, `write_gitignore`, `build_result`).
|
|
25
|
+
- **`auth`/`writer`/`reader` helpers pulled into `WriteVerb`**: removed 4-file duplication from `Put`, `KeyMv`, `KeyDelete`, `Reject`.
|
|
26
|
+
- **`Action::Base#args` auto-generated** from `initialize` params: ~115 lines of boilerplate removed across 25 action classes.
|
|
27
|
+
- **`AuditLog#verify_integrity`**: per-line checking extracted into `check_line_integrity` + `iterate_with_prev_seq`.
|
|
28
|
+
- **`Error#initialize`**: 5-param constructor replaced with `ErrorInfo` parameter object (backward-compatible).
|
|
29
|
+
- **`EventBus#invoke`**: replaced `Thread.new`/`kill` with `Timeout.timeout`.
|
|
30
|
+
- **Hand-rolled Mustache replaced** with `mustache` gem: 117 lines of template engine deleted.
|
|
31
|
+
- **RuboCop formatting** applied across the codebase.
|
|
32
|
+
|
|
33
|
+
### Changed (breaking)
|
|
34
|
+
|
|
35
|
+
- **The proposal payload key `frontmatter` → `_meta`** ([ADR 0113](docs/architecture/decisions/0113-proposal-block-meta-key.md)). A proposal entry's proposed metadata moved from the top-level `frontmatter:` key to `_meta:`, so a proposal now carries the exact `{ _meta, body }` envelope shape `accept` replays — retiring the only place the on-disk word "frontmatter" appeared as a runtime data key. `accept` reads `env.meta["_meta"]` and the `schema_valid` special case shrinks to a dig-with-fallback. No shim (house style): a proposal authored with the old key accepts with empty metadata and fails schema validation loudly. **Migration:** re-author any in-flight proposal with `_meta:` instead of `frontmatter:` — proposals are transient `queue` entries, not durable canon.
|
|
36
|
+
|
|
12
37
|
## 0.52.0 — 2026-06-09 — The authority model is a produced reference doc (ADR 0112)
|
|
13
38
|
|
|
14
39
|
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`.
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- Generated from .textus/
|
|
1
|
+
<!-- Generated from .textus/data/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. -->
|
|
2
2
|
<p align="center">
|
|
3
3
|
<picture>
|
|
4
4
|
<source media="(prefers-color-scheme: dark)" srcset="assets/branding/wordmark-dark.png">
|
|
@@ -73,7 +73,7 @@ TRANSIENT │ artifacts.feeds.* │ proposals (queue) │
|
|
|
73
73
|
|
|
74
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.)*
|
|
75
75
|
|
|
76
|
-
Without coordination, they overwrite each other and nothing remembers why. textus gives each actor a **lane** —
|
|
76
|
+
Without coordination, they overwrite each other and nothing remembers why. textus gives each actor a **lane** — enforced at the protocol level, not by convention — routes everything they can't write directly through a **proposals queue**, and writes every successful change to an **append-only audit log**.
|
|
77
77
|
|
|
78
78
|
```
|
|
79
79
|
knowledge/ author only — who you are, what you decide, how you sound (knowledge.identity.* for identity facts)
|
|
@@ -90,7 +90,7 @@ That's the load-bearing claim: **coordination is a protocol invariant, not a lib
|
|
|
90
90
|
|
|
91
91
|
```sh
|
|
92
92
|
gem install textus
|
|
93
|
-
textus init # creates .textus/ with
|
|
93
|
+
textus init # creates .textus/ with lanes + schemas
|
|
94
94
|
|
|
95
95
|
# an agent proposes a change — it targets a knowledge entry, but lands in proposals/
|
|
96
96
|
textus propose notes.oncall --as=agent --stdin <<'JSON'
|
|
@@ -109,12 +109,12 @@ Try the gate the other way (`textus put knowledge.notes.X --as=agent`) and you g
|
|
|
109
109
|
|
|
110
110
|
## Try it
|
|
111
111
|
|
|
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: [
|
|
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: [`.textus/`](.textus/)
|
|
113
113
|
- **Wire textus into Claude Code via MCP** — 4 steps, ~5 minutes: [`docs/how-to/agents-mcp.md`](docs/how-to/agents-mcp.md)
|
|
114
114
|
|
|
115
115
|
## Protocol, not just a gem
|
|
116
116
|
|
|
117
|
-
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/
|
|
117
|
+
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/lane 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.
|
|
118
118
|
|
|
119
119
|
- Specification: [`SPEC.md`](SPEC.md)
|
|
120
120
|
- Architecture: [`docs/architecture/README.md`](docs/architecture/README.md)
|
|
@@ -137,7 +137,7 @@ bundle exec exe/textus --help
|
|
|
137
137
|
|
|
138
138
|
## What `textus init` gives you
|
|
139
139
|
|
|
140
|
-
You get `.textus/` with all four
|
|
140
|
+
You get `.textus/` with all four lane directories under `data/`, 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 lane declares a `kind:`, and write authority is derived from the role's capabilities crossed with the lane's kind:
|
|
141
141
|
|
|
142
142
|
```yaml
|
|
143
143
|
roles:
|
|
@@ -145,7 +145,7 @@ roles:
|
|
|
145
145
|
- { name: agent, can: [propose, keep] }
|
|
146
146
|
- { name: automation, can: [converge] }
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
lanes:
|
|
149
149
|
- { name: knowledge, kind: canon } # author — canonical truth
|
|
150
150
|
- { name: notebook, kind: workspace } # keep — agent's own durable lane
|
|
151
151
|
- { name: proposals, kind: queue } # propose — proposals awaiting accept
|
|
@@ -154,12 +154,12 @@ zones:
|
|
|
154
154
|
|
|
155
155
|
```
|
|
156
156
|
.textus/
|
|
157
|
-
manifest.yaml # role capabilities +
|
|
157
|
+
manifest.yaml # role capabilities + lane kinds + key-to-path mapping
|
|
158
158
|
schemas/ # YAML field shapes per entry family
|
|
159
159
|
templates/ # mustache templates for derived entries
|
|
160
|
-
|
|
160
|
+
steps/ # step subclasses: fetch/, transform/, validate/, observe/
|
|
161
161
|
.gitignore # generated — ignores .run/ and any tracked:false entries
|
|
162
|
-
|
|
162
|
+
data/ # one dir per lane; kinds + capabilities are in the manifest above
|
|
163
163
|
knowledge/ # e.g. identity (knowledge.identity.*), voice, decisions, notes
|
|
164
164
|
notebook/
|
|
165
165
|
proposals/
|
|
@@ -171,13 +171,13 @@ zones:
|
|
|
171
171
|
sentinels/ # publish bookkeeping (target sha) — regenerated on drain (ADR 0070)
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
-
Manifest `path:` fields are relative to `.textus/
|
|
174
|
+
Manifest `path:` fields are relative to `.textus/data/`. So `knowledge.notes.org.jane` lives at `.textus/data/knowledge/notes/org/jane.md`.
|
|
175
175
|
|
|
176
176
|
Read and write:
|
|
177
177
|
|
|
178
178
|
```sh
|
|
179
179
|
textus get knowledge.notes.org.jane
|
|
180
|
-
textus list --
|
|
180
|
+
textus list --lane=knowledge
|
|
181
181
|
printf '%s' '{"_meta":{"name":"bob","org":"acme"},"body":"hi\n"}' \
|
|
182
182
|
| textus put knowledge.notes.bob --as=human --stdin
|
|
183
183
|
textus drain --as=automation # re-pull stale inputs + recompute derived outputs
|
|
@@ -187,80 +187,90 @@ textus audit --limit=20 # query the audit log
|
|
|
187
187
|
|
|
188
188
|
(All verbs return JSON envelopes; `--output=json` is the default and the only format in v1.)
|
|
189
189
|
|
|
190
|
-
For a worked store — knowledge entries, a staged proposal, schemas, a template, and a `drain` that publishes `CLAUDE.md` / `AGENTS.md` — see [
|
|
190
|
+
For a worked store — knowledge entries, a staged proposal, schemas, a template, and a `drain` that publishes `CLAUDE.md` / `AGENTS.md` — see [`.textus/`](.textus/).
|
|
191
191
|
|
|
192
192
|
## What's shipped
|
|
193
193
|
|
|
194
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))
|
|
195
195
|
- **Stable identity.** Auto-minted `uid:` survives writes and `textus key mv`; reorganising never breaks references.
|
|
196
|
-
- **Capability ×
|
|
196
|
+
- **Capability × lane-kind gate.** Writes carry `--as=<role>`; a role may write a lane iff it holds the capability the lane'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))
|
|
197
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))
|
|
198
|
-
- **`textus doctor`.** Health checks across schemas,
|
|
198
|
+
- **`textus doctor`.** Health checks across schemas, step registrations, keys, sentinels, and the audit log.
|
|
199
199
|
|
|
200
|
-
## CLI and
|
|
200
|
+
## CLI and lanes
|
|
201
201
|
|
|
202
202
|
Every command operates on one store, located in this order: `--root <path>` flag → **`TEXTUS_ROOT`** env → walk up from the working directory for a `.textus/` ([SPEC §3.1](SPEC.md)). Write verbs require `--as=<role>`, resolved as: `--as` flag → **`TEXTUS_ROLE`** env → `.textus/role` file → default `human` ([SPEC §5.1](SPEC.md)). Default roles: `human`, `agent`, `automation` (rename or add your own in the manifest's `roles:` block). All verbs accept `--output=json` and return the envelope defined in [SPEC §8](SPEC.md).
|
|
203
203
|
|
|
204
204
|
- Full verb table — read, write, health, scaffolding — is in [SPEC §9](SPEC.md).
|
|
205
|
-
-
|
|
205
|
+
- Lane semantics and the capability × lane-kind mapping live in [SPEC §5](SPEC.md), with the reference in [`docs/reference/zones.md`](docs/reference/zones.md).
|
|
206
206
|
|
|
207
|
-
`textus boot` prints the same information for the current store:
|
|
207
|
+
`textus boot` prints the same information for the current store: lanes, entry families with schemas, registered steps, 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.
|
|
208
208
|
|
|
209
209
|
## Produce and publish
|
|
210
210
|
|
|
211
211
|
Produced entries (`kind: produced`) declare how they're acquired in one `source:` block (ADR 0093/0094); `drain` materialises them:
|
|
212
212
|
|
|
213
|
-
- **`source: { from:
|
|
214
|
-
- **`source: { from:
|
|
215
|
-
- **`source: { from:
|
|
213
|
+
- **`source: { from: derive, select: [...], pluck:, sort_by:, limit:, transform: name }`** — a *derived* entry: textus computes its 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: fetch, handler: name, ttl: 1h, config: {...} }`** — *intake*: a Step::Fetch handler pulls external bytes on a `ttl` cadence; `drain` re-pulls when the entry goes stale.
|
|
215
|
+
- **`source: { from: external, sources: [...] }`** — *externally managed*: an out-of-band command writes the file; textus tracks the declared `sources` for staleness.
|
|
216
216
|
|
|
217
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.
|
|
218
218
|
|
|
219
219
|
## Extension points
|
|
220
220
|
|
|
221
|
-
textus
|
|
221
|
+
textus extends through **steps** — subclass the right base, place the file in `.textus/steps/<kind>/`, and `drain` discovers it. Three kinds:
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
**`Step::Fetch`** — acquires bytes for an intake entry (`from: fetch`). One class per named handler; `drain` invokes it when the entry is stale.
|
|
224
224
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
225
|
+
```ruby
|
|
226
|
+
# .textus/steps/fetch/local_file.rb
|
|
227
|
+
module Textus
|
|
228
|
+
module Step
|
|
229
|
+
class LocalFileFetch < Fetch
|
|
230
|
+
def call(config:, args:, **)
|
|
231
|
+
path = config["path"] or raise "local-file requires source.config.path"
|
|
232
|
+
{ "_meta" => { "last_fetched_at" => Time.now.utc.iso8601 },
|
|
233
|
+
"body" => File.read(File.expand_path(path)) }
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
```
|
|
232
239
|
|
|
233
|
-
|
|
234
|
-
|---|---|
|
|
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 |
|
|
239
|
-
| `:proposal_accepted` · `:proposal_rejected` | a proposal is resolved |
|
|
240
|
-
| `:entry_fetch_started` · `:entry_fetch_failed` · `:produce_failed` | produce lifecycle |
|
|
241
|
-
| `:store_loaded` · `:session_opened` | the store loads · a role connects |
|
|
240
|
+
**`Step::Transform`** — reshapes projected rows for a `from: derive` entry.
|
|
242
241
|
|
|
243
242
|
```ruby
|
|
244
|
-
#
|
|
245
|
-
Textus
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
243
|
+
# .textus/steps/transform/rank_by_recency.rb
|
|
244
|
+
module Textus
|
|
245
|
+
module Step
|
|
246
|
+
class RankByRecencyTransform < Transform
|
|
247
|
+
def call(rows:, config:, **)
|
|
248
|
+
rows.sort_by { |r| r["updated_at"].to_s }.reverse
|
|
249
|
+
end
|
|
250
|
+
end
|
|
252
251
|
end
|
|
253
252
|
end
|
|
254
253
|
```
|
|
255
254
|
|
|
255
|
+
**`Step::Observe`** — reacts to lifecycle events (fire-and-forget, 0..N per event):
|
|
256
|
+
|
|
256
257
|
```ruby
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
258
|
+
# .textus/steps/observe/log_writes.rb
|
|
259
|
+
module Textus
|
|
260
|
+
module Step
|
|
261
|
+
class LogWritesObserve < Observe
|
|
262
|
+
on :entry_written
|
|
263
|
+
|
|
264
|
+
def call(key:, envelope:, **)
|
|
265
|
+
$textus_event_log << [key, envelope.etag]
|
|
266
|
+
end
|
|
267
|
+
end
|
|
260
268
|
end
|
|
261
269
|
end
|
|
262
270
|
```
|
|
263
271
|
|
|
272
|
+
Observable events: `:entry_written`, `:entry_deleted`, `:entry_fetched`, `:entry_renamed`, `:entry_produced`, `:entry_published`, `:produce_failed`, `:proposal_accepted`, `:proposal_rejected`, `:store_loaded`, `:session_opened`, `:entry_fetch_started`, `:entry_fetch_failed`.
|
|
273
|
+
|
|
264
274
|
Stale intake entries are re-pulled by `drain`, not by reads — `get` is a pure
|
|
265
275
|
read that annotates the returned envelope with a freshness verdict (ADR 0089).
|
|
266
276
|
`drain` re-pulls anything past its `source.ttl` and recomputes derived outputs:
|
|
@@ -268,18 +278,16 @@ read that annotates the returned envelope with a freshness verdict (ADR 0089).
|
|
|
268
278
|
```sh
|
|
269
279
|
textus drain --as=automation # re-pull every stale intake + recompute derived
|
|
270
280
|
textus drain artifacts.feeds --as=automation # scope to one prefix
|
|
271
|
-
textus get artifacts.feeds.calendar.events
|
|
281
|
+
textus get artifacts.feeds.calendar.events # a pure read; carries a freshness verdict
|
|
272
282
|
```
|
|
273
283
|
|
|
274
|
-
See SPEC.md §5.10 for the full hook contract.
|
|
275
|
-
|
|
276
284
|
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.
|
|
277
285
|
|
|
278
286
|
See [`docs/how-to/agents-mcp.md`](docs/how-to/agents-mcp.md) for the agent boot → pulse loop.
|
|
279
287
|
|
|
280
288
|
## Examples
|
|
281
289
|
|
|
282
|
-
[
|
|
290
|
+
[`.textus/`](.textus/) — 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 `Step::Transform` step, 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.
|
|
283
291
|
|
|
284
292
|
## Tests
|
|
285
293
|
|