textus 0.54.2 → 0.55.2
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 -1
- data/README.md +17 -10
- data/SPEC.md +39 -11
- data/docs/architecture/README.md +23 -11
- data/docs/reference/conventions.md +5 -2
- data/exe/textus +1 -1
- data/lib/textus/boot.rb +64 -93
- data/lib/textus/dispatch/binder.rb +48 -0
- data/lib/textus/dispatch/contracts.rb +63 -0
- data/lib/textus/dispatch/handler_registry.rb +21 -0
- data/lib/textus/dispatch/middleware/audit_index.rb +51 -0
- data/lib/textus/dispatch/middleware/auth.rb +40 -0
- data/lib/textus/dispatch/middleware/base.rb +26 -0
- data/lib/textus/dispatch/middleware/binder.rb +20 -0
- data/lib/textus/dispatch/middleware/cascade.rb +53 -0
- data/lib/textus/dispatch/pipeline.rb +35 -0
- data/lib/textus/doctor/check/audit_log.rb +2 -2
- data/lib/textus/doctor/check/generator_drift.rb +4 -4
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +3 -3
- data/lib/textus/doctor/check/protocol_version.rb +2 -2
- data/lib/textus/doctor/check/raw_asset_paths.rb +1 -1
- data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +2 -2
- data/lib/textus/doctor/check/schemas.rb +1 -1
- data/lib/textus/doctor/check/{notebook_sources.rb → scratchpad_sources.rb} +10 -5
- data/lib/textus/doctor/check/sentinels.rb +4 -4
- data/lib/textus/doctor/check/templates.rb +1 -1
- data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
- data/lib/textus/doctor/check.rb +9 -10
- data/lib/textus/doctor.rb +2 -2
- data/lib/textus/errors.rb +8 -0
- data/lib/textus/format/base.rb +34 -10
- data/lib/textus/format/json.rb +5 -27
- data/lib/textus/format/markdown.rb +5 -27
- data/lib/textus/format/text.rb +5 -4
- data/lib/textus/format/yaml.rb +30 -27
- data/lib/textus/format.rb +22 -1
- data/lib/textus/handlers/maintenance/boot_store.rb +15 -0
- data/lib/textus/handlers/maintenance/doctor_store.rb +15 -0
- data/lib/textus/handlers/maintenance/drain_store.rb +21 -0
- data/lib/textus/handlers/maintenance/ingest_entry.rb +159 -0
- data/lib/textus/handlers/maintenance/jobs_action.rb +21 -0
- data/lib/textus/handlers/maintenance/published_entries.rb +17 -0
- data/lib/textus/handlers/maintenance/rule_explain.rb +77 -0
- data/lib/textus/handlers/maintenance/rule_lint.rb +54 -0
- data/lib/textus/handlers/maintenance/rule_list.rb +32 -0
- data/lib/textus/handlers/maintenance/schema_envelope.rb +19 -0
- data/lib/textus/handlers/read/audit_entries.rb +48 -0
- data/lib/textus/handlers/read/blame_entry.rb +71 -0
- data/lib/textus/handlers/read/deps_entry.rb +17 -0
- data/lib/textus/handlers/read/get_entry.rb +68 -0
- data/lib/textus/handlers/read/list_keys.rb +36 -0
- data/lib/textus/handlers/read/pulse_entries.rb +66 -0
- data/lib/textus/handlers/read/rdeps_entry.rb +21 -0
- data/lib/textus/handlers/read/uid_entry.rb +18 -0
- data/lib/textus/handlers/read/where_entry.rb +18 -0
- data/lib/textus/handlers/write/accept_proposal.rb +39 -0
- data/lib/textus/handlers/write/data_mv.rb +55 -0
- data/lib/textus/handlers/write/delete_key.rb +17 -0
- data/lib/textus/handlers/write/enqueue_job.rb +27 -0
- data/lib/textus/handlers/write/key_delete_prefix.rb +32 -0
- data/lib/textus/handlers/write/key_mv_prefix.rb +45 -0
- data/lib/textus/handlers/write/move_key.rb +80 -0
- data/lib/textus/handlers/write/propose_entry.rb +29 -0
- data/lib/textus/handlers/write/put_entry.rb +29 -0
- data/lib/textus/handlers/write/reject_proposal.rb +29 -0
- data/lib/textus/init.rb +5 -7
- data/lib/textus/jobs.rb +3 -9
- data/lib/textus/manifest/capabilities.rb +4 -4
- data/lib/textus/manifest/entry/base.rb +3 -3
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +2 -2
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -2
- data/lib/textus/manifest/policy/predicates/author_held.rb +22 -0
- data/lib/textus/manifest/policy/predicates/etag_match.rb +18 -0
- data/lib/textus/manifest/policy/predicates/fresh_within.rb +13 -0
- data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +31 -0
- data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +23 -0
- data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +25 -0
- data/lib/textus/manifest/policy/predicates/raw_write_once.rb +24 -0
- data/lib/textus/manifest/policy/predicates/schema_valid.rb +41 -0
- data/lib/textus/manifest/policy/predicates/target_is_canon.rb +20 -0
- data/lib/textus/manifest/policy/predicates.rb +54 -0
- data/lib/textus/manifest/policy/retention.rb +1 -1
- data/lib/textus/manifest/schema/semantics/cross_field.rb +53 -0
- data/lib/textus/manifest/schema/semantics/invariants.rb +125 -0
- data/lib/textus/manifest/schema/semantics/migration.rb +83 -0
- data/lib/textus/manifest/schema/semantics.rb +11 -216
- data/lib/textus/orchestration.rb +55 -0
- data/lib/textus/{ports → port}/audit_log.rb +46 -6
- data/lib/textus/{ports → port}/build_lock.rb +2 -2
- data/lib/textus/{ports → port}/clock.rb +1 -1
- data/lib/textus/{ports → port}/publisher.rb +5 -5
- data/lib/textus/{core → port}/sentinel.rb +1 -6
- data/lib/textus/{ports → port}/sentinel_store.rb +4 -4
- data/lib/textus/{ports → port}/storage/file_stat.rb +1 -1
- data/lib/textus/port/storage/file_store.rb +49 -0
- data/lib/textus/port/storage/interface.rb +17 -0
- data/lib/textus/port/store.rb +149 -0
- data/lib/textus/{ports → port}/watcher_lock.rb +3 -3
- data/lib/textus/produce/engine.rb +1 -11
- data/lib/textus/produce/publisher.rb +21 -0
- data/lib/textus/schema/registry.rb +42 -0
- data/lib/textus/schema/tools.rb +7 -10
- data/lib/textus/store/container.rb +173 -0
- data/lib/textus/store/cursor.rb +26 -0
- data/lib/textus/store/entry/reader.rb +47 -0
- data/lib/textus/store/entry/writer.rb +219 -0
- data/lib/textus/store/envelope/meta.rb +61 -0
- data/lib/textus/store/freshness/drift_detector.rb +93 -0
- data/lib/textus/store/freshness/evaluator.rb +20 -0
- data/lib/textus/store/freshness/ttl_evaluator.rb +57 -0
- data/lib/textus/{core → store}/freshness/verdict.rb +1 -11
- data/lib/textus/store/freshness.rb +8 -0
- data/lib/textus/store/index/builder.rb +76 -0
- data/lib/textus/store/index/lookup.rb +60 -0
- data/lib/textus/store/jobs/base.rb +13 -0
- data/lib/textus/store/jobs/index.rb +15 -0
- data/lib/textus/store/jobs/materialize.rb +15 -0
- data/lib/textus/store/jobs/plan.rb +11 -0
- data/lib/textus/store/jobs/planner.rb +124 -0
- data/lib/textus/store/jobs/queue.rb +162 -0
- data/lib/textus/store/jobs/registry.rb +19 -0
- data/lib/textus/store/jobs/retention/base.rb +52 -0
- data/lib/textus/store/jobs/retention/sweep.rb +55 -0
- data/lib/textus/store/jobs/retention.rb +8 -0
- data/lib/textus/store/jobs/sweep.rb +21 -0
- data/lib/textus/store/jobs/worker.rb +64 -0
- data/lib/textus/store/layout.rb +97 -0
- data/lib/textus/store.rb +63 -32
- data/lib/textus/{surfaces → surface}/cli/group/data.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/key.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/mcp.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/rule.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/schema.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/runner.rb +14 -68
- data/lib/textus/surface/cli/sources.rb +41 -0
- data/lib/textus/{surfaces → surface}/cli/verb/doctor.rb +5 -6
- data/lib/textus/{surfaces → surface}/cli/verb/get.rb +7 -5
- data/lib/textus/{surfaces → surface}/cli/verb/init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/mcp_serve.rb +3 -3
- data/lib/textus/surface/cli/verb/put.rb +27 -0
- data/lib/textus/{surfaces → surface}/cli/verb/schema_diff.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_migrate.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/watch.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/verb.rb +3 -8
- data/lib/textus/{surfaces → surface}/cli.rb +1 -1
- data/lib/textus/surface/mcp/catalog.rb +58 -0
- data/lib/textus/{surfaces → surface}/mcp/errors.rb +1 -11
- data/lib/textus/surface/mcp/projector.rb +20 -0
- data/lib/textus/{surfaces → surface}/mcp/server.rb +24 -35
- data/lib/textus/{surfaces → surface}/mcp.rb +2 -2
- data/lib/textus/{surfaces → surface}/watcher.rb +8 -8
- data/lib/textus/value/call.rb +30 -0
- data/lib/textus/value/command.rb +16 -0
- data/lib/textus/{core → value}/duration.rb +1 -4
- data/lib/textus/value/envelope.rb +90 -0
- data/lib/textus/value/etag.rb +39 -0
- data/lib/textus/value/payload.rb +7 -0
- data/lib/textus/value/result.rb +46 -0
- data/lib/textus/value/role.rb +38 -0
- data/lib/textus/value/types.rb +13 -0
- data/lib/textus/{uid.rb → value/uid.rb} +9 -7
- data/lib/textus/verb_registry.rb +417 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/loader.rb +4 -4
- data/lib/textus/workflow/runner.rb +11 -33
- data/lib/textus.rb +4 -69
- metadata +142 -105
- data/lib/textus/action/accept.rb +0 -44
- data/lib/textus/action/audit.rb +0 -131
- data/lib/textus/action/base.rb +0 -42
- data/lib/textus/action/blame.rb +0 -97
- data/lib/textus/action/boot.rb +0 -17
- data/lib/textus/action/data_mv.rb +0 -69
- data/lib/textus/action/deps.rb +0 -34
- data/lib/textus/action/doctor.rb +0 -24
- data/lib/textus/action/drain.rb +0 -39
- data/lib/textus/action/enqueue.rb +0 -53
- data/lib/textus/action/get.rb +0 -78
- data/lib/textus/action/ingest.rb +0 -203
- data/lib/textus/action/jobs.rb +0 -36
- data/lib/textus/action/key_delete.rb +0 -36
- data/lib/textus/action/key_delete_prefix.rb +0 -44
- data/lib/textus/action/key_mv.rb +0 -129
- data/lib/textus/action/key_mv_prefix.rb +0 -57
- data/lib/textus/action/list.rb +0 -42
- data/lib/textus/action/propose.rb +0 -52
- data/lib/textus/action/published.rb +0 -24
- data/lib/textus/action/pulse.rb +0 -56
- data/lib/textus/action/put.rb +0 -52
- data/lib/textus/action/rdeps.rb +0 -39
- data/lib/textus/action/reject.rb +0 -37
- data/lib/textus/action/rule_explain.rb +0 -91
- data/lib/textus/action/rule_lint.rb +0 -68
- data/lib/textus/action/rule_list.rb +0 -42
- data/lib/textus/action/schema_envelope.rb +0 -29
- data/lib/textus/action/uid.rb +0 -33
- data/lib/textus/action/where.rb +0 -36
- data/lib/textus/action/write_verb.rb +0 -44
- data/lib/textus/call.rb +0 -28
- data/lib/textus/command.rb +0 -41
- data/lib/textus/container.rb +0 -26
- data/lib/textus/contract/around.rb +0 -29
- data/lib/textus/contract/binder.rb +0 -88
- data/lib/textus/contract/resources/build_lock.rb +0 -17
- data/lib/textus/contract/resources/cursor.rb +0 -26
- data/lib/textus/contract/sources.rb +0 -39
- data/lib/textus/contract/view.rb +0 -15
- data/lib/textus/contract.rb +0 -174
- data/lib/textus/core/freshness/evaluator.rb +0 -150
- data/lib/textus/core/freshness.rb +0 -11
- data/lib/textus/core/retention/sweep.rb +0 -57
- data/lib/textus/core/retention.rb +0 -11
- data/lib/textus/cursor_store.rb +0 -24
- data/lib/textus/envelope/reader.rb +0 -46
- data/lib/textus/envelope/writer.rb +0 -209
- data/lib/textus/envelope.rb +0 -79
- data/lib/textus/etag.rb +0 -36
- data/lib/textus/gate/auth.rb +0 -227
- data/lib/textus/gate.rb +0 -116
- data/lib/textus/jobs/base.rb +0 -23
- data/lib/textus/jobs/materialize.rb +0 -20
- data/lib/textus/jobs/plan.rb +0 -9
- data/lib/textus/jobs/planner.rb +0 -101
- data/lib/textus/jobs/retention.rb +0 -48
- data/lib/textus/jobs/sweep.rb +0 -27
- data/lib/textus/jobs/worker.rb +0 -67
- data/lib/textus/layout.rb +0 -91
- data/lib/textus/ports/job_store/job.rb +0 -65
- data/lib/textus/ports/job_store.rb +0 -123
- data/lib/textus/ports/raw_index.rb +0 -61
- data/lib/textus/ports/storage/file_store.rb +0 -26
- data/lib/textus/role.rb +0 -36
- data/lib/textus/schemas.rb +0 -54
- data/lib/textus/session.rb +0 -35
- data/lib/textus/surfaces/cli/verb/put.rb +0 -30
- data/lib/textus/surfaces/mcp/catalog.rb +0 -111
- data/lib/textus/surfaces/role_scope.rb +0 -34
- data/lib/textus/types.rb +0 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 68d4176e50949378dcf62b98617c28bcf6f553c196b24f9263a97d8d5c6f382f
|
|
4
|
+
data.tar.gz: 7de0c2d9ee56d71e09cc0821d19b8ffcc00e3573fccc20ef3e610988ec5ff43c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 34322a4bfd59c817bdbaec436caba5cd8a25a7896065e16633270eb22bed7db20af339f45f7ee8ed632732f3679469328939d45490e3a6ce3528f654b00feba9
|
|
7
|
+
data.tar.gz: 6c4f94a584d8334e7dc68e543852bb687fa662c6e6eb1334cadf9d70ff01576552f71d773d462f03f3c4f0955496fe094c167df090880f473d3e850469f4c3cd
|
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,43 @@ 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.55.1 — 2026-06-22 — CI fix: converge_now purges done jobs
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **publish_tree prune spec** — `converge_now` now purges "done" jobs before reseeding. The SQLite-backed job queue (`INSERT OR IGNORE`) was silently skipping re-enqueues for already-done jobs, so the second materialize never ran and the source-removed file was never pruned from the published tree.
|
|
17
|
+
|
|
18
|
+
## 0.55.0 — 2026-06-22 — Architecture Deepening Phase 2
|
|
19
|
+
|
|
20
|
+
**ADR-0119**: dry-monad actions, container split, geometry authority.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **dry-monad Success/Failure actions** (PR #234): all 30 actions now return `Success(result)` or `Failure(code:, message:)` via `Dry::Monads`. `Value::Result.unwrap` converts Failure → `ActionError`. `be_success`/`be_failure` RSpec matchers replace `spec/unit/` tests.
|
|
25
|
+
- **`Container::Infrastructure` / `Container::Coordination`** (PR #234): single `LazyContainer` replaced by two `Data.define` records — Infrastructure owns manifest, file_store, geometry; Coordination owns gate, compositor, workflows, audit_log. Wired at boot via `wire_gate!`.
|
|
26
|
+
- **`Geometry` as sole path authority** (PR #234): `root`, `schemas_dir`, `hooks_dir`, `store_db_path` delegated to `Geometry`. Removed `root` from Infrastructure. High-value `File.join` callers converted (data_mv, ingest, base, loader, etag, schema/tools, 6 doctor checks).
|
|
27
|
+
- **`bin/db` helper**: sqlite3 wrapper for `.textus/.state/store.db` — `bin/db "SELECT * FROM jobs"` or just `bin/db` for interactive shell.
|
|
28
|
+
- **Centralized SQLite index** (PR #228, #233): `Store::Index::Builder` manages entries FTS index in `store.db`.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **Gate reads `Action::VERBS` directly** (PR #234): `Gate::VERB_ACTIONS` deleted. Gate dispatches via `Action::VERBS[cmd.verb]`.
|
|
33
|
+
- **Composite actions** (PR #232): `Accept`, `Propose`, `Reject` separated as composite verbs with declarative `chain` steps, never instantiating sibling action classes directly.
|
|
34
|
+
- **`knowledge.decisions` → keyless nested mode**: decision files no longer enumerate in `list`; accessible via `get` with full key.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **CLI `--args` JSON parsing** (PR #233): `Runner.coerce` now `JSON.parse(raw)` for `Hash`-typed args — fixes `textus enqueue --args='{"key":"..."}'` which previously passed the raw JSON string to `Queue::Job`.
|
|
39
|
+
- **adr-log workflow convention drift** (PR #233): `Get.new(key:).call(...)` → `Get.call(key: k, ...)` — the old pattern raised silently since actions became class-method-only.
|
|
40
|
+
- **RuboCop `Lint/NoReturnInBeginEndBlocks`**: enqueue action restructured with method-level `rescue`.
|
|
41
|
+
|
|
42
|
+
### Removed
|
|
43
|
+
|
|
44
|
+
- `Gate::VERB_ACTIONS` constant (replaced by `Action::VERBS`)
|
|
45
|
+
- `LazyContainer` (replaced by `Container::Infrastructure`/`Coordination`)
|
|
46
|
+
- `spec/unit/` directory + `spec/support/spec_layout.rb` (378 examples removed)
|
|
47
|
+
- Orphaned `.textus/.state/jobs.sqlite3` database file
|
|
48
|
+
|
|
12
49
|
## 0.54.2 — 2026-06-18 — ingest dedup, .run → .state rename
|
|
13
50
|
|
|
14
51
|
### Added
|
|
@@ -867,7 +904,7 @@ Protocol remains `textus/3`.
|
|
|
867
904
|
`refresh`, `refresh_stale`, `schema`, `rules`). Session state (cursor,
|
|
868
905
|
role, manifest_etag) held server-side. Manifest drift surfaces as
|
|
869
906
|
`ContractDrift` (-32001); cursor expiry as `CursorExpired` (-32002).
|
|
870
|
-
See [
|
|
907
|
+
See [ADR 0015](docs/architecture/decisions/0015-agent-gate-mcp.md).
|
|
871
908
|
- `examples/claude-plugin/.mcp.json` and migrated skills/commands/agents —
|
|
872
909
|
zero `textus <verb>` shell strings remain in plugin markdown.
|
|
873
910
|
|
data/README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
<!-- Generated from .
|
|
1
|
+
<!-- Generated from knowledge.readme.* fragments by the artifacts.docs.readme workflow. Edit the fragments under .textus/data/knowledge/readme/ and run `textus drain`. -->
|
|
2
|
+
<!-- This is a fragment of the README. Edit here; the README is composed from fragments by a workflow. -->
|
|
2
3
|
<p align="center">
|
|
3
4
|
<picture>
|
|
4
5
|
<source media="(prefers-color-scheme: dark)" srcset="assets/branding/wordmark-dark.png">
|
|
@@ -18,6 +19,8 @@
|
|
|
18
19
|
|
|
19
20
|
*textus* is Latin for "the fabric a text is woven from" — same root as *context*, from *con-texere*, "to weave together."
|
|
20
21
|
|
|
22
|
+
|
|
23
|
+
<!-- This is a fragment of the README. Edit here; the README is composed from fragments by a workflow. -->
|
|
21
24
|
## The idea
|
|
22
25
|
|
|
23
26
|
Three actors write to your repo today:
|
|
@@ -36,7 +39,7 @@ flowchart LR
|
|
|
36
39
|
end
|
|
37
40
|
|
|
38
41
|
human -->|author| knowledge["knowledge<br/>(canon)"]
|
|
39
|
-
agent -->|keep|
|
|
42
|
+
agent -->|keep| scratchpad["scratchpad<br/>(workspace)"]
|
|
40
43
|
agent -->|propose| proposals["proposals<br/>(queue)"]
|
|
41
44
|
automation -->|drain| artifacts["artifacts<br/>(machine)"]
|
|
42
45
|
human -->|ingest| raw["raw<br/>(intake)"]
|
|
@@ -65,7 +68,7 @@ The point of those lanes is to **build context you can trust**. Place each lane
|
|
|
65
68
|
LOW TRUST HIGH TRUST
|
|
66
69
|
(unreviewed) (authoritative)
|
|
67
70
|
┌──────────────────────────┬───────────────────────────────┐
|
|
68
|
-
DURABLE │
|
|
71
|
+
DURABLE │ scratchpad │ knowledge ★ the goal │
|
|
69
72
|
(kept) │ agent's working truth │ canon — a human authors │
|
|
70
73
|
│ durable, but low-trust │ here · the context you ship │
|
|
71
74
|
├──────────────────────────┼───────────────────────────────┤
|
|
@@ -81,15 +84,17 @@ Without coordination, they overwrite each other and nothing remembers why. textu
|
|
|
81
84
|
|
|
82
85
|
```
|
|
83
86
|
knowledge/ author only — who you are, what you decide, how you sound
|
|
84
|
-
|
|
87
|
+
scratchpad/ keep only — agent's own durable lane (bytes climb to knowledge only via propose→accept)
|
|
85
88
|
proposals/ propose (agent+human) — proposals waiting on a human accept
|
|
86
89
|
artifacts/ converge only — machine-maintained: computed outputs + external inputs
|
|
87
90
|
```
|
|
88
91
|
|
|
89
|
-
An agent that tries to write directly into `knowledge/` gets `write_forbidden`. It writes to `proposals/` (to change authoritative content) or its own `
|
|
92
|
+
An agent that tries to write directly into `knowledge/` gets `write_forbidden`. It writes to `proposals/` (to change authoritative content) or its own `scratchpad/` (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.
|
|
90
93
|
|
|
91
94
|
That's the load-bearing claim: **coordination is a protocol invariant, not a library convenience.**
|
|
92
95
|
|
|
96
|
+
|
|
97
|
+
<!-- This is a fragment of the README. Edit here; the README is composed from fragments by a workflow. -->
|
|
93
98
|
## See it in four commands
|
|
94
99
|
|
|
95
100
|
```sh
|
|
@@ -151,7 +156,7 @@ roles:
|
|
|
151
156
|
|
|
152
157
|
lanes:
|
|
153
158
|
- { name: knowledge, kind: canon } # author — canonical truth
|
|
154
|
-
- { name:
|
|
159
|
+
- { name: scratchpad, kind: workspace } # keep — agent's own durable lane
|
|
155
160
|
- { name: proposals, kind: queue } # propose — proposals awaiting accept
|
|
156
161
|
- { name: artifacts, kind: machine } # converge — computed outputs + external inputs
|
|
157
162
|
```
|
|
@@ -165,7 +170,7 @@ lanes:
|
|
|
165
170
|
.gitignore # generated — ignores .state/ and any tracked:false entries
|
|
166
171
|
data/ # one dir per lane; kinds + capabilities are in the manifest above
|
|
167
172
|
knowledge/ # e.g. identity (knowledge.identity.*), voice, decisions, notes
|
|
168
|
-
|
|
173
|
+
scratchpad/
|
|
169
174
|
proposals/
|
|
170
175
|
artifacts/ # machine lane: computed outputs + external inputs
|
|
171
176
|
.state/ # disposable runtime state — gitignored, safe to delete (ADR 0038)
|
|
@@ -202,7 +207,7 @@ For a worked store — knowledge entries, a staged proposal, schemas, ERB templa
|
|
|
202
207
|
- **Agent loop.** `textus boot` orients a fresh session; `textus pulse --since=N` is the per-turn heartbeat (changed entries, pending proposals, index etag for catalog drift detection). ([docs/how-to/agents-mcp.md](docs/how-to/agents-mcp.md))
|
|
203
208
|
- **MCP surface.** The official `mcp` Ruby SDK drives the stdio JSON-RPC server; protocol version auto-negotiated up to `2025-11-25`. Wire textus into Claude Code, Cursor, or any MCP host in one config block.
|
|
204
209
|
- **`textus doctor`.** Health checks across schemas, workflow registrations, keys, sentinels, and the audit log.
|
|
205
|
-
- **`raw` lane and `ingest` verb.** Write-once intake lane for external URL bookmarks, files, and binary assets. Three source kinds (`url`/`file`/`asset`); daily key derivation;
|
|
210
|
+
- **`raw` lane and `ingest` verb.** Write-once intake lane for external URL bookmarks, files, and binary assets. Three source kinds (`url`/`file`/`asset`); daily key derivation; scratchpad stub per ingest. See "Intake and ingest" section below.
|
|
206
211
|
|
|
207
212
|
## CLI and lanes
|
|
208
213
|
|
|
@@ -276,8 +281,8 @@ textus ingest url agentskills-io-brainstorming \
|
|
|
276
281
|
# see what landed in the raw lane
|
|
277
282
|
textus list --lane=raw
|
|
278
283
|
|
|
279
|
-
# a
|
|
280
|
-
textus get
|
|
284
|
+
# a scratchpad stub was created alongside — annotate it
|
|
285
|
+
textus get scratchpad.notes.raw
|
|
281
286
|
```
|
|
282
287
|
|
|
283
288
|
Stale produced entries are re-materialised by `drain`, not by reads — `get` is a pure read (ADR 0089).
|
|
@@ -316,3 +321,5 @@ Lefthook hooks (`brew bundle install` then `lefthook install`) run rubocop on `p
|
|
|
316
321
|
## License
|
|
317
322
|
|
|
318
323
|
[MIT](LICENSE)
|
|
324
|
+
|
|
325
|
+
|
data/SPEC.md
CHANGED
|
@@ -57,7 +57,7 @@ You **shape your own memory structure** inside `.textus/`. The protocol manages
|
|
|
57
57
|
textus/4 names its concepts along six axes. Reviewers who internalize these can map any part of the spec to the right category:
|
|
58
58
|
|
|
59
59
|
- **Actor** — who is interacting: roles such as `human`, `agent`, `automation`, each holding a set of capabilities (`propose`, `author`, `keep`, `converge`).
|
|
60
|
-
- **Place** — where data lives: lanes such as `knowledge`, `
|
|
60
|
+
- **Place** — where data lives: lanes such as `knowledge`, `scratchpad`, `raw`, `proposals`, `artifacts`.
|
|
61
61
|
- **Thing** — what is stored: entries, fields, keys.
|
|
62
62
|
- **Operation** — how you act on things: RPC and CLI verbs (`get`, `put`, `drain`, `serve`, `ingest`, …).
|
|
63
63
|
- **Event** — what gets fired after an operation: pub-sub events (`:entry_written`, `:entry_produced`, `:entry_published`, …).
|
|
@@ -111,7 +111,7 @@ The root is `.textus/` at the project working directory. A typical tree:
|
|
|
111
111
|
sentinels/ # byte-copied publish bookkeeping (see §5.3)
|
|
112
112
|
data/ # ALL user content lives here
|
|
113
113
|
knowledge/ # lane: knowledge (kind: canon — author-holders write)
|
|
114
|
-
|
|
114
|
+
scratchpad/ # lane: scratchpad (kind: workspace — keep-holders write; agent's own durable lane)
|
|
115
115
|
proposals/ # lane: proposals (kind: queue — propose-holders write)
|
|
116
116
|
artifacts/ # lane: artifacts (kind: machine — converge-holders write)
|
|
117
117
|
raw/ # lane: raw (kind: raw — ingest-holders write; write-once)
|
|
@@ -149,7 +149,7 @@ roles:
|
|
|
149
149
|
lanes:
|
|
150
150
|
- name: knowledge
|
|
151
151
|
kind: canon
|
|
152
|
-
- name:
|
|
152
|
+
- name: scratchpad
|
|
153
153
|
kind: workspace
|
|
154
154
|
owner: agent # optional, informational — agent's own lane
|
|
155
155
|
desc: "agent's durable working memory; bytes climb to knowledge only via propose→accept"
|
|
@@ -241,7 +241,7 @@ Default scaffold — Setup-1 (roles `human=[author, propose, ingest]`, `agent=[p
|
|
|
241
241
|
| Lane | `kind` | Required capability | Writable by (default) | Use case |
|
|
242
242
|
|---|---|---|---|---|
|
|
243
243
|
| `knowledge` | `canon` | `author` | `human` | Authored truth: identity, voice, decisions, network. |
|
|
244
|
-
| `
|
|
244
|
+
| `scratchpad` | `workspace` | `keep` | `agent` | Agent's own durable working memory. Bytes climb to `knowledge` only via propose→accept. |
|
|
245
245
|
| `proposals` | `queue` | `propose` | `agent`, `human` | Proposals awaiting human review via `textus accept`. |
|
|
246
246
|
| `artifacts` | `machine` | `converge` | `automation` | Computed outputs produced by `drain` via the workflow DSL. |
|
|
247
247
|
| `raw` | `raw` | `ingest` | `human`, `agent`, `automation` | Write-once external source material: URL bookmarks, files, binary assets. |
|
|
@@ -284,7 +284,7 @@ Every successful write records the resolved role and a wall-clock timestamp in `
|
|
|
284
284
|
|
|
285
285
|
#### 5.1.1 Capabilities
|
|
286
286
|
|
|
287
|
-
Roles declare **capabilities** — verbs from a closed
|
|
287
|
+
Roles declare **capabilities** — verbs from a closed five-element set. A
|
|
288
288
|
manifest declares a `roles:` block mapping each role name to the capabilities
|
|
289
289
|
it holds via `can:`:
|
|
290
290
|
|
|
@@ -296,7 +296,7 @@ roles:
|
|
|
296
296
|
- { name: keeper, can: [keep] }
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
-
Capability allow-list: `propose`, `author`, `keep`, `converge`. The mapping from
|
|
299
|
+
Capability allow-list: `propose`, `author`, `keep`, `converge`, `ingest`. The mapping from
|
|
300
300
|
lane-kind to its required capability is a **bijection** (ADR 0091, which folded
|
|
301
301
|
the former `quarantine` + `derived` kinds back into one `machine` kind — undoing
|
|
302
302
|
the two-kind split of ADR 0090): each capability authorizes exactly one
|
|
@@ -308,8 +308,9 @@ lane-kind:
|
|
|
308
308
|
| `keep` | `workspace` |
|
|
309
309
|
| `propose` | `queue` |
|
|
310
310
|
| `converge` | `machine` |
|
|
311
|
+
| `ingest` | `raw` |
|
|
311
312
|
|
|
312
|
-
A manifest naming a folded capability — `
|
|
313
|
+
A manifest naming a folded capability — `build` or the pre-0088
|
|
313
314
|
spelling `fetch` — in a `can:` list is rejected at load with a hint pointing to
|
|
314
315
|
`converge` (ADR 0090, 0091, 0111).
|
|
315
316
|
|
|
@@ -425,7 +426,7 @@ The `raw` lane (`kind: raw`) is a write-once intake lane for external source mat
|
|
|
425
426
|
|
|
426
427
|
**`access` field** — entries MAY carry `source.access: public | private` (field is `maintained_by: human`). Set `private` for sources not safe to reproduce publicly.
|
|
427
428
|
|
|
428
|
-
**Notebook stub** — every ingest creates a `
|
|
429
|
+
**Notebook stub** — every ingest creates a `scratchpad.notes` stub with a backlink (`Ingested from raw.<key>`) so the agent or human can annotate the ingested material without touching the write-once record.
|
|
429
430
|
|
|
430
431
|
**Example — URL bookmark:**
|
|
431
432
|
|
|
@@ -438,6 +439,22 @@ textus ingest url agentskills-io-brainstorming \
|
|
|
438
439
|
|
|
439
440
|
A `get` on a raw entry is a pure read — it returns the entry as stored and never re-fetches (ADR 0089).
|
|
440
441
|
|
|
442
|
+
**Raw entry shape.** Every raw-lane entry has a set of required fields
|
|
443
|
+
enforced by the format layer on write:
|
|
444
|
+
|
|
445
|
+
| Field | Required | Description |
|
|
446
|
+
|-------|----------|-------------|
|
|
447
|
+
| `ingested_at` | yes | ISO-8601 timestamp of ingestion |
|
|
448
|
+
| `content_hash` | yes | `sha256:<hex>` of the source content |
|
|
449
|
+
| `source.kind` | yes | One of `url`, `file`, `asset` |
|
|
450
|
+
| `source.url` | conditional | Required when `kind=url` |
|
|
451
|
+
| `source.label` | no | Human-readable label |
|
|
452
|
+
| `body` | no | Present only for `kind=file` |
|
|
453
|
+
| `asset` | no | Present only for `kind=asset` |
|
|
454
|
+
|
|
455
|
+
Tombstone entries (carrying `superseded_by:`) are exempt from shape
|
|
456
|
+
validation — they are stripped-down records pointing to the live entry.
|
|
457
|
+
|
|
441
458
|
### 5.5 Pending / accept workflow
|
|
442
459
|
|
|
443
460
|
Proposal entries are full patches authored into the `proposals` queue lane (writable by `propose`-holders: `agent` and `human` by default) — `proposals` in the default scaffold (Setup-1) — typically by agents. The entry's frontmatter describes the patch it proposes against another lane:
|
|
@@ -646,6 +663,13 @@ The frontmatter `name:` field, when present, must match the file's basename (wit
|
|
|
646
663
|
- Existing files without a uid continue to work. The envelope shows `"uid": null` until a put mints one.
|
|
647
664
|
- `text` entries have no metadata channel and therefore no uid; their envelope always shows `"uid": null`.
|
|
648
665
|
|
|
666
|
+
**`sources:` (Source references).** Entries MAY carry a `sources` array in
|
|
667
|
+
their frontmatter to declare external provenance. Each element is a raw-lane
|
|
668
|
+
key string (starting with `raw.`). The array is preserved on write —
|
|
669
|
+
existing sources carry forward if no new `sources` are provided. The
|
|
670
|
+
envelope returns `sources` as a top-level array when non-empty; omitted
|
|
671
|
+
when absent.
|
|
672
|
+
|
|
649
673
|
Entries in a `produced` lane SHOULD additionally carry the `generated:` block defined in §5.2. Implementations MUST treat unknown frontmatter fields as warnings, not errors, so build tooling can extend the metadata without breaking conformance.
|
|
650
674
|
|
|
651
675
|
## 8. Envelope (the wire format)
|
|
@@ -665,6 +689,9 @@ Every successful CLI response (`--output=json`) is a single JSON envelope:
|
|
|
665
689
|
"etag": "sha256:8f3c…",
|
|
666
690
|
"schema_ref": "person",
|
|
667
691
|
"uid": "a1b2c3d4e5f60718",
|
|
692
|
+
"sources": [
|
|
693
|
+
{ "key": "raw.2026.06.20.url-mcp-spec", "etag": "sha256:1a2b…", "suspended": false }
|
|
694
|
+
],
|
|
668
695
|
"stale": false,
|
|
669
696
|
"stale_reason": null,
|
|
670
697
|
"fetching": false
|
|
@@ -674,7 +701,7 @@ Every successful CLI response (`--output=json`) is a single JSON envelope:
|
|
|
674
701
|
**Field rules:**
|
|
675
702
|
- `protocol` MUST be the exact string `textus/4`.
|
|
676
703
|
- `key` MUST be the canonical resolved key.
|
|
677
|
-
- `lane` MUST be one of the lanes declared in the manifest (`knowledge`, `
|
|
704
|
+
- `lane` MUST be one of the lanes declared in the manifest (`knowledge`, `scratchpad`, `proposals`, `artifacts`, `raw` in the default Setup-1 scaffold).
|
|
678
705
|
- `path` MUST be an absolute filesystem path.
|
|
679
706
|
- `format` MUST be one of `markdown`, `json`, `yaml`, `text` (§5.12). Absent envelopes are treated as `markdown` for back-compat.
|
|
680
707
|
- `body` is the raw on-disk bytes as a UTF-8 string for every format.
|
|
@@ -682,6 +709,7 @@ Every successful CLI response (`--output=json`) is a single JSON envelope:
|
|
|
682
709
|
- `etag` MUST be `sha256:<hex>` of the raw file bytes, computed identically for every format.
|
|
683
710
|
- `schema_ref` MAY be `null` for entries in subtrees with `schema: null`.
|
|
684
711
|
- `uid` is the stable Textus UID (§7) if the entry carries one, else `null`. Always present in the envelope.
|
|
712
|
+
- `sources` is an array of source objects. Each object has `key` (the referenced entry's key), `etag` (sha256 snapshot taken at write time, or absent when no snapshot exists), and `suspended` (`true` when the referenced entry's current on-disk etag differs from the stored snapshot — the source changed after this entry was last written). Present only when non-empty.
|
|
685
713
|
- `stale` is `true` when the entry's `source.ttl` has elapsed and the entry has not yet been re-materialised; `false` otherwise. Only populated for produced entries with a declared `ttl`; always `false` for other entries.
|
|
686
714
|
- `stale_reason` is a short human-readable string describing why the entry is stale (e.g. `"ttl_exceeded"`, `"never_fetched"`), or `null` when `stale` is `false`.
|
|
687
715
|
- `fetching` is `true` when a background re-pull is in flight for this entry; `false` otherwise. Callers observing `stale: true, fetching: true` SHOULD retry after a short delay.
|
|
@@ -764,7 +792,7 @@ All verbs accept `--output=json` and emit a canonical envelope (success or error
|
|
|
764
792
|
|
|
765
793
|
`read_verbs` is derived from the MCP verb catalog — the verbs the agent can actually call over its transport — so it lists the read/discovery verbs (`schema_show` for an entry's field shape, `rule_explain` for its retention/guard policy, and the graph reads `where`/`deps`/`rdeps`, ADR 0060) and never the CLI-only `audit`/`doctor`, nor `freshness` (the Ruby-only internal lifecycle scan, ADR 0085) (ADR 0056). An agent learns an entry's `_meta` shape by calling the `schema_show` verb before a `put`/`propose`, not by shelling out to a CLI. The graph reads `deps`/`rdeps` return a structured `{key, deps}`/`{key, rdeps}` envelope on every surface (CLI, Ruby, MCP) — a hash, not a bare array, consistent with the other structured read responses such as `where` (ADR 0060 amendment).
|
|
766
794
|
|
|
767
|
-
The agent's MCP write surface includes the single-key `key_delete` and `key_mv` tools alongside their bulk `key_delete_prefix`/`key_mv_prefix` cousins (ADR 0060 amendment; the single-key tools were renamed from `delete`/`mv` to share the `key_` family stem in ADR 0082, which also removed the `migrate` YAML-plan orchestrator — its `
|
|
795
|
+
The agent's MCP write surface includes the single-key `key_delete` and `key_mv` tools alongside their bulk `key_delete_prefix`/`key_mv_prefix` cousins (ADR 0060 amendment; the single-key tools were renamed from `delete`/`mv` to share the `key_` family stem in ADR 0082, which also removed the `migrate` YAML-plan orchestrator — its `data_mv`/`key_mv_prefix`/`key_delete_prefix` ops remain individually callable). The structural mutation verbs (`key_mv`, `key_mv_prefix`, `key_delete_prefix`, `data_mv`) accept `dry_run: true` as an opt-in preview that returns a Plan without mutating (ADR 0071). `drain` does not support `dry_run` (it is async-only, ADR 0110). Single-key `key_delete` additionally accepts an optional `if_etag` optimistic-concurrency check. The blast-radius reads (`where`/`deps`/`rdeps`) remain on MCP so an agent can look before it leaps. The promotion verbs `accept` and `reject` are also on MCP (ADR 0072): they are gated by the `author_held` capability floor, not by transport absence — a default-`agent` connection is refused, while a connection launched as a role holding `author` (`--as`/`TEXTUS_ROLE`/`.textus/role`, resolved once at launch per ADR 0040) can promote, closing the propose→accept loop over one transport. `drain` is also on MCP (ADR 0076, ADR 0087, ADR 0110): it is caller-agnostic and its produce jobs self-elevate — materialization always runs as the manifest's `converge`-capable actor regardless of the calling role, granting no authority over content (materialization is a pure, idempotent function of already-accepted canon, ADR 0070); the destructive retention sweep runs as the caller. Each produce job self-acquires the single-writer build lock, so a concurrent CLI, reactive, or background pass cannot collide with an MCP-triggered one — a held lock is a graceful soft-miss (ADR 0110).
|
|
768
796
|
|
|
769
797
|
`latest_seq` is the current high-water mark of the audit log; agents should use it as the starting cursor for `pulse`.
|
|
770
798
|
|
|
@@ -792,7 +820,7 @@ The agent's MCP write surface includes the single-key `key_delete` and `key_mv`
|
|
|
792
820
|
|
|
793
821
|
`if_etag` is optional on both `put` and `key_delete`. When provided, the write fails with `etag_mismatch` if the on-disk file's etag differs. When omitted, the write is unconditional (last-writer-wins).
|
|
794
822
|
|
|
795
|
-
The lifecycle scan
|
|
823
|
+
The lifecycle scan runs per-entry at `get` time — each `get` response carries `stale`/`stale_reason` when the entry has a TTL rule. ADR 0085 removed the standalone `freshness` verb; human drill-down into a single entry's verdict is `textus get KEY` (carries `stale`/`stale_reason`) plus `textus rule_explain KEY` (the `source.ttl` and retention policy). `pulse` does not include a `stale` list. `textus drain` enqueues the convergence jobs — produce every in-scope derived entry, re-pull every stale intake entry, and a retention sweep — then drains the queue to empty (§5.11). Convergence is async-only (ADR 0110): there is no `--dry-run`.
|
|
796
824
|
|
|
797
825
|
`textus accept K --as=human` promotes a pending entry into its target lane: it copies the patch body into the target key, deletes the pending entry, and writes one audit line per side (§audit). Only a role holding the `author` capability (the trust anchor — `human` by default) may invoke `accept`.
|
|
798
826
|
|
data/docs/architecture/README.md
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
```mermaid
|
|
7
7
|
flowchart TD
|
|
8
8
|
surfaces["Surfaces — CLI · MCP · RoleScope"]
|
|
9
|
-
contract["Contract — per-verb DSL (
|
|
9
|
+
contract["Contract — per-verb DSL (Arg, Spec, DSL)"]
|
|
10
|
+
dispatch["Dispatch — Binder · Around · CommandBuilder · Dispatcher"]
|
|
10
11
|
gate["Gate — routing (VERB_COMMAND) · Auth"]
|
|
11
12
|
action["Actions — per-verb use cases (action/)"]
|
|
12
13
|
jobs["Jobs — Worker · Planner · Materialize · Retention · Sweep"]
|
|
@@ -15,8 +16,10 @@ flowchart TD
|
|
|
15
16
|
manifest["Manifest — declarative config, no IO (policy/, schema/, entry/)"]
|
|
16
17
|
core["Core — pure value types (Freshness, Retention, Duration, Sentinel)"]
|
|
17
18
|
ports["Ports — IO adapters (FileStore, AuditLog, JobStore, Publisher…)"]
|
|
18
|
-
surfaces -->
|
|
19
|
-
|
|
19
|
+
surfaces --> dispatch
|
|
20
|
+
surfaces -.->|declare| contract
|
|
21
|
+
contract --> dispatch
|
|
22
|
+
dispatch --> gate
|
|
20
23
|
gate --> action
|
|
21
24
|
action --> jobs
|
|
22
25
|
action --> produce
|
|
@@ -27,7 +30,7 @@ flowchart TD
|
|
|
27
30
|
jobs --> produce
|
|
28
31
|
```
|
|
29
32
|
|
|
30
|
-
*Dependency rule: inward only.* Surfaces →
|
|
33
|
+
*Dependency rule: inward only.* Surfaces → Dispatch → Gate → Actions → inner layers. Actions never reference surfaces.
|
|
31
34
|
|
|
32
35
|
### What lives in each layer
|
|
33
36
|
|
|
@@ -35,18 +38,27 @@ flowchart TD
|
|
|
35
38
|
|
|
36
39
|
```
|
|
37
40
|
surfaces/cli/ CLI command generation from contracts
|
|
41
|
+
sources.rb CLI-only input acquisition (stdin parsing, file sourcing, coercion)
|
|
38
42
|
surfaces/mcp/ MCP server — stdio JSON-RPC 2.0, tools derived from contracts
|
|
39
43
|
surfaces/role_scope.rb
|
|
40
|
-
(Store#
|
|
44
|
+
(Store#with_role(role)) — holds (container, role, dry_run, correlation_id);
|
|
41
45
|
all verb methods injected via define_method in textus.rb
|
|
42
46
|
```
|
|
43
47
|
|
|
44
|
-
**Contract**
|
|
48
|
+
**Contract + Dispatch**
|
|
45
49
|
|
|
46
50
|
```
|
|
47
|
-
contract/ Per-verb DSL — verb, summary, surfaces, arg, view.
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
contract/ Per-verb DSL — verb, summary, surfaces, arg, view, around.
|
|
52
|
+
Pure declaration: Spec and DSL only.
|
|
53
|
+
|
|
54
|
+
dispatch/ Runtime machinery consumed by every surface.
|
|
55
|
+
binder.rb Binder — validates required args, resolves session/literal defaults.
|
|
56
|
+
around.rb Around — registry of stateful wrappers (cursor, build_lock).
|
|
57
|
+
view.rb View.render(spec, surface, result, inputs) — output shaping.
|
|
58
|
+
command_builder.rb
|
|
59
|
+
CommandBuilder — command construction + role injection.
|
|
60
|
+
dispatcher.rb Dispatcher — unified pipeline: Around → Binder → Command → Gate.
|
|
61
|
+
resources/ Around resource implementations (Cursor, BuildLock).
|
|
50
62
|
```
|
|
51
63
|
|
|
52
64
|
**Gate + Auth**
|
|
@@ -202,7 +214,7 @@ Both are built from a `Container` via named constructors — `Writer.from(contai
|
|
|
202
214
|
|
|
203
215
|
`Action::Get` is a **pure read** — it resolves the path, reads bytes, parses the envelope, and annotates a freshness verdict. It never ingests and never mutates.
|
|
204
216
|
|
|
205
|
-
1. CLI/MCP surface calls `store.
|
|
217
|
+
1. CLI/MCP surface calls `store.with_role(role).get(key)`.
|
|
206
218
|
2. `Gate#dispatch` runs Auth → `Action::Get#call`.
|
|
207
219
|
3. `Get` resolves path via `manifest.resolver`, reads bytes via `file_store`, parses the envelope, annotates `freshness` based on retention-rule TTL (if any).
|
|
208
220
|
|
|
@@ -210,7 +222,7 @@ Staleness is age-based (retention-rule TTL vs file mtime). A stale entry is retu
|
|
|
210
222
|
|
|
211
223
|
## Write path (`store.put(key, ...)`)
|
|
212
224
|
|
|
213
|
-
1. CLI/MCP surface calls `store.
|
|
225
|
+
1. CLI/MCP surface calls `store.with_role(role).put(key, meta:, body:)`.
|
|
214
226
|
2. `Gate#dispatch` runs Auth → `Action::Put#call`.
|
|
215
227
|
3. `Put` validates, resolves manifest entry, delegates to `Envelope::Writer#put` (serialize → schema-validate → etag-check → FileStore#write → AuditLog#append).
|
|
216
228
|
4. `WriteVerb#cascade_to_rdeps` enqueues `materialize` jobs for any entries with publish_tree that depend on the written key.
|
|
@@ -24,7 +24,7 @@ Recommended top-level layout — the spec allows alternatives, but this is what
|
|
|
24
24
|
templates/ # ERB templates for publish rendering
|
|
25
25
|
data/
|
|
26
26
|
knowledge/ # authored truth: identity, voice, decisions — author-holders write
|
|
27
|
-
|
|
27
|
+
scratchpad/ # agent's own durable lane (workspace) — keep-holders write
|
|
28
28
|
proposals/ # AI proposals awaiting accept (propose)
|
|
29
29
|
artifacts/ # computed outputs produced by drain — never edit by hand
|
|
30
30
|
raw/ # write-once external source material (ingest)
|
|
@@ -126,7 +126,10 @@ For multi-writer environments, **always pass `if_etag`** on `put`. The gem treat
|
|
|
126
126
|
The application layer is organised around three shapes — `Manifest` as a composition record, a single `Container` capability record handed to every use case, and a split envelope reader/writer. See [ADR 0018](../architecture/decisions/0018-manifest-carving.md), [ADR 0017](../architecture/decisions/0017-envelope-io-split.md), [ADR 0022](../architecture/decisions/0022-container-call-dispatcher.md), and [ADR 0023](../architecture/decisions/0023-uniform-use-case-shape.md).
|
|
127
127
|
|
|
128
128
|
- **`Manifest` is a composition record** (`Data.define(:data, :resolver, :policy, :rules)`). Reach individual concerns through the field accessors: `manifest.data.entries`, `manifest.policy.permission_for(lane)`, `manifest.resolver.resolve(key)`, `manifest.rules.for(key)`.
|
|
129
|
-
-
|
|
129
|
+
- **`Store` is the single dispatch module.** `Store` holds the `Container`, the active role, session state (cursor, propose_lane, contract_etag), and dispatches verbs through `method_missing`. Each verb call (`store.get(key: ...)`, `store.put(key:, meta:, body:)`) goes through `VerbRegistry`, binds inputs via `Dispatch::Binder`, routes through `Dispatch::Pipeline` (middleware chain: auth, cascade, audit), and reaches the handler. `Store#with_role(role)` returns a new `Store` bound to that role.
|
|
130
|
+
- **`Container` is a `Data.define` composition** of `Infrastructure` (`file_store`, `schemas`, `audit_log`, `job_store`, `geometry`) and `Coordination` (`manifest`, `workflows`, `pipeline`). Built in a single pass — no circular dependencies or lazy proxies.
|
|
131
|
+
- **Dispatch lives in `lib/textus/dispatch/`**: `Dispatch.dispatch` orchestrates verb dispatch, `Dispatch::Pipeline` runs middleware then invokes the handler, `Dispatch::Binder` handles input binding (wire format, CLI flags, session defaults). Auth predicates moved to `Manifest::Policy::Predicates`.
|
|
132
|
+
- **Handlers receive `container:`** and access I/O via `@container.pipeline.read/write/delete/move` and manifest config via `@container.manifest`.
|
|
130
133
|
- **Write path is split**: `Envelope::IO::Reader` owns read/parse (existing-uid lookup, raw read, parse), and `Envelope::IO::Writer` owns put/delete/move + the audit-append invariant (every public method's final action is `@audit_log.append(...)`).
|
|
131
134
|
|
|
132
135
|
The user-facing CLI surface, the wire envelope shape, and the protocol version (`textus/4`) are unchanged.
|
data/exe/textus
CHANGED