textus 0.43.0 → 0.43.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 +29 -0
- data/SPEC.md +7 -5
- data/docs/architecture/README.md +1 -1
- data/docs/reference/conventions.md +8 -8
- data/lib/textus/boot.rb +23 -12
- data/lib/textus/contract.rb +13 -5
- data/lib/textus/maintenance/key_delete_prefix.rb +2 -2
- data/lib/textus/maintenance/key_mv_prefix.rb +3 -3
- data/lib/textus/maintenance/migrate.rb +3 -2
- data/lib/textus/maintenance/rule_lint.rb +2 -1
- data/lib/textus/maintenance/zone_mv.rb +3 -3
- data/lib/textus/mcp/catalog.rb +30 -5
- data/lib/textus/read/get.rb +2 -1
- data/lib/textus/read/list.rb +2 -2
- data/lib/textus/read/rules.rb +2 -1
- data/lib/textus/read/schema_envelope.rb +2 -1
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/fetch_all.rb +2 -2
- data/lib/textus/write/fetch_worker.rb +2 -1
- data/lib/textus/write/propose.rb +6 -3
- data/lib/textus/write/put.rb +10 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d352b5ae1e1274a454c6488497c8bad60659b9c4f70ceb86da822f883b1559c5
|
|
4
|
+
data.tar.gz: fca73540d82d4a0e7ae9d527bdae43451c8a13a89f0da486e049232754a6b6d4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7559e8f9b1c49a2ddbf0a3bf83c5653fa951f3f575a53113274a3b1f857fabfb709649720d6137963f9b411aa87b1178f296ae4a9f907963250920e28a77880e
|
|
7
|
+
data.tar.gz: 60dea75e32ccdae818a6da8a4303ff7e14fd248e4230e0b7f54af45c24ea9539eaf0103707820e006e1e0ed925c030179b8e3483eb889a5120eb67afd030e9f7
|
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,35 @@ 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.43.2 — 2026-06-02 — Agent-legible MCP contracts ([ADR 0057](docs/architecture/decisions/0057-agent-legible-mcp-contracts.md))
|
|
13
|
+
|
|
14
|
+
No `textus/3` wire-format change. One breaking (pre-1.0) change on the MCP transport — `put`/`propose` take frontmatter under `_meta` (was `meta`) — plus per-argument descriptions on every MCP tool and a fully catalog-derived agent verb surface.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **BREAKING (pre-1.0): MCP `put`/`propose` take frontmatter under `_meta`, not `meta` ([ADR 0057](docs/architecture/decisions/0057-agent-legible-mcp-contracts.md)).** Every *read* returns the envelope under `_meta` (`get`, SPEC §8), and the CLI `--stdin` envelope already speaks `_meta` — only the MCP `put`/`propose` argument diverged as `meta`, breaking the natural read → edit → write round-trip. The contract gains a `wire_name` primitive so the wire speaks `_meta` while the use-case keeps its `meta:` kwarg (the ADR 0039 signature guard still reconciles). An MCP client that hardcoded the `meta` argument renames one key. The Ruby API (`store.put(meta:)`) and the CLI `--stdin` envelope are unchanged.
|
|
19
|
+
- **`boot.agent_quickstart.write_verbs` derives from the MCP catalog ([ADR 0057](docs/architecture/decisions/0057-agent-legible-mcp-contracts.md)).** It now returns bare verb names (`put propose fetch fetch_all`) like `read_verbs`, instead of the CLI string `"put KEY --as=agent --stdin"` (`--as`/`--stdin` are meaningless on an MCP connection — role is connection-resolved, ADR 0040). Closes the de-CLI follow-up named in ADR 0056; a symmetric guard spec fails the build if a CLI string creeps back.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Per-argument descriptions on every MCP tool ([ADR 0057](docs/architecture/decisions/0057-agent-legible-mcp-contracts.md)).** All MCP-surfaced verb arguments now carry a one-line `description:` in their `tools/list` `inputSchema` (was 2 of ~30) — including `put`'s `body`/`content` mutual-exclusivity and the maintenance `dry_run` "default false applies immediately" gotcha. They ship once in `tools/list` at no per-call cost, so an agent can form a valid call from the schema alone.
|
|
24
|
+
|
|
25
|
+
## 0.43.1 — 2026-06-02 — `boot` agent surface derives from the MCP catalog ([ADR 0056](docs/architecture/decisions/0056-boot-quickstart-speaks-the-mcp-catalog.md))
|
|
26
|
+
|
|
27
|
+
No `textus/3` wire-format change — `boot`'s agent-orientation fields are corrected to match the verbs an MCP agent can actually call.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **`boot.agent_quickstart.read_verbs` had drifted from the MCP catalog ([ADR 0056](docs/architecture/decisions/0056-boot-quickstart-speaks-the-mcp-catalog.md)).** It was a hand-maintained list that advertised CLI-only read verbs an MCP agent cannot call (`audit`, `freshness`, `doctor`) while omitting the ones it can and needs (`schema`, `rules`). It now **derives** from `MCP::Catalog` (`get list pulse schema boot rules`) and is reconciled by a guard spec, so it can no longer advertise an uncallable verb or omit a callable one. This is what led downstream skills to shell out to a CLI for schema discovery instead of calling the `schema` verb.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **`boot.agent_protocol.recipes` reference verbs, not CLI strings ([ADR 0056](docs/architecture/decisions/0056-boot-quickstart-speaks-the-mcp-catalog.md)).** Each recipe step names a verb (`get KEY`, `schema KEY`, `put KEY`, `propose KEY`, `fetch_all`) or a plain build step, instead of a `textus …` / `echo … | textus …` shell line — each transport frames the verb itself (CLI vs MCP tool). Keeps shell syntax out of the surface an MCP agent reads. `human_steps` still name `accept` (the author-only transition, not an MCP tool, by design).
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **ADR 0054 (Proposed, no implementation): entry-level `desc`.** Records the decision to add an optional one-line `desc:` to manifest entries — surfaced in `boot.entries`/`list` — turning the manifest into a navigable index so an agent finds the right data without a caller hardcoding the key. Pre-registers ADR 0055 (a `find`/search verb) as the evidence-triggered follow-up. Documentation only in this release.
|
|
40
|
+
|
|
12
41
|
## 0.43.0 — 2026-06-02 — Typed `publish:` block + remove `index_filename` ([ADR 0052](docs/architecture/decisions/0052-typed-publish-block.md), [0053](docs/architecture/decisions/0053-remove-index-filename.md))
|
|
13
42
|
|
|
14
43
|
No `textus/3` wire-format change. Two breaking (pre-1.0) changes on the publish/enumeration surface: the two top-level publish keys become one typed `publish:` block, and the unused `index_filename:` enumeration feature is removed. Both fail at load with a migration-pointing message.
|
data/SPEC.md
CHANGED
|
@@ -197,14 +197,14 @@ entries:
|
|
|
197
197
|
path: knowledge/network/org
|
|
198
198
|
zone: knowledge
|
|
199
199
|
schema: person
|
|
200
|
-
owner:
|
|
200
|
+
owner: human:network
|
|
201
201
|
nested: true
|
|
202
202
|
|
|
203
203
|
- key: artifacts.catalogs.people
|
|
204
204
|
path: artifacts/catalogs/people.md
|
|
205
205
|
zone: artifacts
|
|
206
206
|
schema: null
|
|
207
|
-
owner:
|
|
207
|
+
owner: automation:build
|
|
208
208
|
|
|
209
209
|
rules:
|
|
210
210
|
- match: feeds.**
|
|
@@ -404,7 +404,7 @@ A derived entry that is produced by a build tool *outside* textus — `rake`, `j
|
|
|
404
404
|
- key: output.catalogs.skills
|
|
405
405
|
path: output/catalogs/skills.md
|
|
406
406
|
zone: output
|
|
407
|
-
owner:
|
|
407
|
+
owner: automation:catalog-skills
|
|
408
408
|
compute:
|
|
409
409
|
kind: external
|
|
410
410
|
command: "rake catalog:skills" # informational; external automation invokes it
|
|
@@ -800,7 +800,7 @@ Every successful CLI response (`--output=json`) is a single JSON envelope:
|
|
|
800
800
|
"protocol": "textus/3",
|
|
801
801
|
"key": "knowledge.network.org.jane",
|
|
802
802
|
"zone": "knowledge",
|
|
803
|
-
"owner": "
|
|
803
|
+
"owner": "human:network",
|
|
804
804
|
"path": "/absolute/path/to/.textus/zones/knowledge/network/org/jane.md",
|
|
805
805
|
"format": "markdown",
|
|
806
806
|
"_meta": { "name": "jane", "relationship": "peer", "org": "acme" },
|
|
@@ -898,7 +898,7 @@ All verbs accept `--output=json` and emit a canonical envelope (success or error
|
|
|
898
898
|
```json
|
|
899
899
|
{
|
|
900
900
|
"agent_quickstart": {
|
|
901
|
-
"read_verbs": ["
|
|
901
|
+
"read_verbs": ["get", "list", "pulse", "schema", "boot", "rules"],
|
|
902
902
|
"write_verbs": ["put KEY --as=agent --stdin"],
|
|
903
903
|
"writable_zones": ["proposals"],
|
|
904
904
|
"propose_zone": "proposals",
|
|
@@ -907,6 +907,8 @@ All verbs accept `--output=json` and emit a canonical envelope (success or error
|
|
|
907
907
|
}
|
|
908
908
|
```
|
|
909
909
|
|
|
910
|
+
`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` for an entry's field shape, `rules` for its freshness/guard policy) and never the CLI-only `audit`/`freshness`/`doctor` (ADR 0056). An agent learns an entry's `_meta` shape by calling the `schema` verb before a `put`/`propose`, not by shelling out to a CLI.
|
|
911
|
+
|
|
910
912
|
`latest_seq` is the current high-water mark of the audit log; agents should use it as the starting cursor for `pulse`.
|
|
911
913
|
|
|
912
914
|
**`textus pulse` output shape:**
|
data/docs/architecture/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Textus architecture
|
|
2
2
|
|
|
3
3
|
> **Explanation** · for contributors · **read this first** for orientation before SPEC
|
|
4
|
-
> **SSoT for** the Ruby implementation layout (layers, container, ports, read/write/fetch paths) · **reviewed** 2026-
|
|
4
|
+
> **SSoT for** the Ruby implementation layout (layers, container, ports, read/write/fetch paths) · **reviewed** 2026-06 (v0.43)
|
|
5
5
|
|
|
6
6
|
```mermaid
|
|
7
7
|
flowchart TD
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Conventions
|
|
2
2
|
|
|
3
3
|
> **Reference** · for integrators · **read when** you're shaping a `.textus/` tree and want the idiomatic choices
|
|
4
|
-
> **SSoT for** idiomatic key naming, schema design, and automation integration · **reviewed** 2026-
|
|
4
|
+
> **SSoT for** idiomatic key naming, schema design, and automation integration · **reviewed** 2026-06 (v0.43)
|
|
5
5
|
|
|
6
6
|
Guidelines for shaping a `.textus/` tree, naming keys, organising schemas, and integrating with build automation. The spec ([`../../SPEC.md`](../../SPEC.md)) defines what's enforceable; this document captures what's *idiomatic*.
|
|
7
7
|
|
|
@@ -39,11 +39,11 @@ Inside `knowledge/`, group by **domain** (identity, people, projects, decisions,
|
|
|
39
39
|
|
|
40
40
|
## Owner strings
|
|
41
41
|
|
|
42
|
-
The `owner:` field in the manifest is **advisory metadata**, not an ACL. Use it to label *who's expected to write here
|
|
42
|
+
The `owner:` field in the manifest is **advisory metadata**, not an ACL. Use it to label *who's expected to write here*. The form is `<archetype>` or `<archetype>:<subject>`; the archetype must be one of `human`, `agent`, `automation` (validated at load — ADR 0045), and the subject is free-form:
|
|
43
43
|
|
|
44
|
-
- `
|
|
44
|
+
- `human:network` — humans curate
|
|
45
45
|
- `agent:planner` — a specific named agent
|
|
46
|
-
- `
|
|
46
|
+
- `automation:catalog-skills` — a specific build job
|
|
47
47
|
|
|
48
48
|
Tooling around `git blame` or audit logs may filter on owner; the gem itself only echoes it back in envelopes.
|
|
49
49
|
|
|
@@ -58,7 +58,7 @@ A derived entry declares a `compute:` block with a `kind:` discriminator. Two ki
|
|
|
58
58
|
path: artifacts/catalogs/people.md
|
|
59
59
|
zone: artifacts
|
|
60
60
|
schema: null
|
|
61
|
-
owner:
|
|
61
|
+
owner: automation:catalog-people
|
|
62
62
|
compute:
|
|
63
63
|
kind: projection
|
|
64
64
|
select: knowledge.network.org # prefix or list of prefixes
|
|
@@ -75,7 +75,7 @@ A derived entry declares a `compute:` block with a `kind:` discriminator. Two ki
|
|
|
75
75
|
- key: artifacts.catalogs.skills
|
|
76
76
|
path: artifacts/catalogs/skills.md
|
|
77
77
|
zone: artifacts
|
|
78
|
-
owner:
|
|
78
|
+
owner: automation:catalog-skills
|
|
79
79
|
compute:
|
|
80
80
|
kind: external
|
|
81
81
|
command: "rake catalog:skills" # informational; the automation invokes it
|
|
@@ -106,7 +106,7 @@ rules:
|
|
|
106
106
|
A typical scheduled-fetch integration shells the `fetch stale` sweep itself:
|
|
107
107
|
|
|
108
108
|
```sh
|
|
109
|
-
textus fetch stale --zone=
|
|
109
|
+
textus fetch stale --zone=feeds --as=automation # in cron / CI
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
See [`./zones.md` §6](zones.md) for the full intake contract and [`../how-to/writing-hooks.md`](../how-to/writing-hooks.md) for writing custom handlers.
|
|
@@ -137,7 +137,7 @@ For multi-writer environments, **always pass `if_etag`** on `put`. The gem treat
|
|
|
137
137
|
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).
|
|
138
138
|
|
|
139
139
|
- **`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(zone)`, `manifest.resolver.resolve(key)`, `manifest.rules.for(key)`.
|
|
140
|
-
- **Use cases are plain `(container:, call:)` classes.** Each is a single class under `lib/textus/{read,write,maintenance}/` with `def initialize(container:, call:)` and a `#call(...)` method; verbs are looked up in the static `Textus::Dispatcher::VERBS` table. `Container` is a `Data.define` record bundling the wired ports + manifest (`manifest`, `file_store`, `schemas`, `root`, `audit_log`, `events`, `rpc
|
|
140
|
+
- **Use cases are plain `(container:, call:)` classes.** Each is a single class under `lib/textus/{read,write,maintenance}/` with `def initialize(container:, call:)` and a `#call(...)` method; verbs are looked up in the static `Textus::Dispatcher::VERBS` table. `Container` is a `Data.define` record bundling the wired ports + manifest (`manifest`, `file_store`, `schemas`, `root`, `audit_log`, `events`, `rpc`); `Call` is the immutable per-invocation value (`role`, `correlation_id`, `now`, `dry_run`). A use case that emits events derives its `Hooks::Context` from `(container, call)` — nothing is injected. Use cases pull only what they need into ivars; nobody passes the raw `Store` around the application layer. `store.as(role)` returns a `RoleScope` that forwards verbs to the dispatcher.
|
|
141
141
|
- **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(...)`).
|
|
142
142
|
|
|
143
143
|
The user-facing CLI surface, the wire envelope shape, and the protocol version (`textus/3`) are unchanged.
|
data/lib/textus/boot.rb
CHANGED
|
@@ -127,14 +127,25 @@ module Textus
|
|
|
127
127
|
propose_zone = manifest.policy.propose_zone_for(agent_role)
|
|
128
128
|
|
|
129
129
|
{
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
# Both verb lists derive from the MCP catalog (ADR 0056, ADR 0057): the
|
|
131
|
+
# agent's real read and write surface, named as verbs the agent calls —
|
|
132
|
+
# not CLI strings. read_verbs can neither advertise a verb the agent
|
|
133
|
+
# cannot call (audit/freshness/doctor are CLI-only) nor omit one it can
|
|
134
|
+
# (schema/rules); write_verbs drops the old `put KEY --as=… --stdin` CLI
|
|
135
|
+
# framing (role is connection-resolved over MCP; there is no stdin).
|
|
136
|
+
# writable_zones / propose_zone below carry the agent's write authority.
|
|
137
|
+
"read_verbs" => Textus::MCP::Catalog.read_verbs,
|
|
138
|
+
"write_verbs" => agent_role ? Textus::MCP::Catalog.write_verbs : [],
|
|
132
139
|
"writable_zones" => writable_zones,
|
|
133
140
|
"propose_zone" => propose_zone,
|
|
134
141
|
"latest_seq" => audit_log.latest_seq,
|
|
135
142
|
}
|
|
136
143
|
end
|
|
137
144
|
|
|
145
|
+
# Recipes reference verbs, not a transport's CLI strings (ADR 0056): every
|
|
146
|
+
# step names a verb the agent can call (each transport frames it — CLI as
|
|
147
|
+
# `textus get KEY`, MCP as the `get` tool) or is a plain build step. This
|
|
148
|
+
# keeps shell lines out of the surface an MCP agent reads.
|
|
138
149
|
def self.recipes(manifest)
|
|
139
150
|
queue = manifest.policy.queue_zone
|
|
140
151
|
feeds = zone_label(manifest, :quarantine, "the quarantine zone")
|
|
@@ -142,32 +153,32 @@ module Textus
|
|
|
142
153
|
"read" => {
|
|
143
154
|
"purpose" => "find and read an entry",
|
|
144
155
|
"steps" => [
|
|
145
|
-
"
|
|
146
|
-
"
|
|
156
|
+
"list (zone:, prefix:) — discover keys without reading bodies",
|
|
157
|
+
"get KEY — returns the entry envelope",
|
|
147
158
|
],
|
|
148
159
|
},
|
|
149
160
|
"write" => {
|
|
150
161
|
"purpose" => "create or update an entry",
|
|
151
162
|
"steps" => [
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
163
|
+
"schema KEY — learn the _meta field shape (required, optional, field types) before writing",
|
|
164
|
+
"assemble an envelope: { _meta: {…}, body: \"…\" }",
|
|
165
|
+
"put KEY — persist it (role-gated); pass if_etag to guard a concurrent edit",
|
|
155
166
|
],
|
|
156
167
|
},
|
|
157
168
|
"propose" => {
|
|
158
169
|
"purpose" => "agent suggests a change for human review",
|
|
159
170
|
"agent_steps" => [
|
|
160
|
-
"
|
|
171
|
+
"propose KEY — writes the change into the #{queue} zone for review",
|
|
161
172
|
],
|
|
162
173
|
"human_steps" => [
|
|
163
|
-
"
|
|
174
|
+
"accept #{queue}.KEY — promotes the proposal into its target zone",
|
|
164
175
|
],
|
|
165
176
|
},
|
|
166
177
|
"fetch" => {
|
|
167
|
-
"purpose" => "
|
|
178
|
+
"purpose" => "refresh stale quarantine-zone caches from their declared intake",
|
|
168
179
|
"steps" => [
|
|
169
|
-
"
|
|
170
|
-
"
|
|
180
|
+
"pulse — its `stale` list names entries past their ttl",
|
|
181
|
+
"fetch_all (zone: #{feeds}) — re-pull the stale entries",
|
|
171
182
|
],
|
|
172
183
|
},
|
|
173
184
|
}
|
data/lib/textus/contract.rb
CHANGED
|
@@ -8,7 +8,14 @@ module Textus
|
|
|
8
8
|
# use-case as a positional (e.g. `get(key)`); otherwise as a keyword.
|
|
9
9
|
# `session_default` names a zero-arg method on `Textus::Session` (Symbol)
|
|
10
10
|
# that supplies the value when the wire arg is absent; `nil` means no default.
|
|
11
|
-
|
|
11
|
+
# `wire_name` is the name the arg carries on the wire (MCP JSON property / CLI
|
|
12
|
+
# envelope key) when it must differ from the use-case kwarg `name` — e.g. `put`
|
|
13
|
+
# takes the `meta:` kwarg but exposes `_meta` on the wire to match what `get`
|
|
14
|
+
# returns and what the CLI `--stdin` envelope already speaks (ADR 0057).
|
|
15
|
+
Arg = Data.define(:name, :type, :required, :positional, :session_default, :description, :wire_name) do
|
|
16
|
+
# The name used on the wire (defaults to the kwarg name).
|
|
17
|
+
def wire = wire_name || name
|
|
18
|
+
end
|
|
12
19
|
|
|
13
20
|
JSON_TYPES = {
|
|
14
21
|
String => "string", Integer => "integer", Hash => "object",
|
|
@@ -31,9 +38,9 @@ module Textus
|
|
|
31
38
|
props = args.to_h do |a|
|
|
32
39
|
h = { "type" => Contract.json_type(a.type) }
|
|
33
40
|
h["description"] = a.description if a.description
|
|
34
|
-
[a.
|
|
41
|
+
[a.wire.to_s, h]
|
|
35
42
|
end
|
|
36
|
-
{ type: "object", properties: props, required: required_args.map { |a| a.
|
|
43
|
+
{ type: "object", properties: props, required: required_args.map { |a| a.wire.to_s } }
|
|
37
44
|
end
|
|
38
45
|
end
|
|
39
46
|
|
|
@@ -70,12 +77,13 @@ module Textus
|
|
|
70
77
|
end
|
|
71
78
|
end
|
|
72
79
|
|
|
73
|
-
def arg(name, type, required: false, positional: false, session_default: nil, description: nil)
|
|
80
|
+
def arg(name, type, required: false, positional: false, session_default: nil, description: nil, wire_name: nil) # rubocop:disable Metrics/ParameterLists
|
|
74
81
|
raise "contract already built; declare args before reading .contract" if defined?(@__contract) && @__contract
|
|
75
82
|
|
|
76
83
|
(@__args ||= []) << Arg.new(
|
|
77
84
|
name: name, type: type, required: required,
|
|
78
|
-
positional: positional, session_default: session_default,
|
|
85
|
+
positional: positional, session_default: session_default,
|
|
86
|
+
description: description, wire_name: wire_name
|
|
79
87
|
)
|
|
80
88
|
end
|
|
81
89
|
|
|
@@ -7,8 +7,8 @@ module Textus
|
|
|
7
7
|
verb :key_delete_prefix
|
|
8
8
|
summary "Bulk-delete every leaf key under prefix."
|
|
9
9
|
surfaces :cli, :ruby, :mcp
|
|
10
|
-
arg :prefix, String, required: true
|
|
11
|
-
arg :dry_run, :boolean
|
|
10
|
+
arg :prefix, String, required: true, description: "every leaf key under this dotted prefix is deleted"
|
|
11
|
+
arg :dry_run, :boolean, description: "true returns the Plan without writing; default false applies the delete immediately"
|
|
12
12
|
response(&:to_h)
|
|
13
13
|
|
|
14
14
|
def initialize(container:, call:)
|
|
@@ -8,9 +8,9 @@ module Textus
|
|
|
8
8
|
verb :key_mv_prefix
|
|
9
9
|
summary "Bulk-rename every leaf key under from_prefix to to_prefix. Dry-run returns a Plan; apply with dry_run: false."
|
|
10
10
|
surfaces :cli, :ruby, :mcp
|
|
11
|
-
arg :from_prefix, String, required: true
|
|
12
|
-
arg :to_prefix, String, required: true
|
|
13
|
-
arg :dry_run, :boolean
|
|
11
|
+
arg :from_prefix, String, required: true, description: "dotted prefix whose leaf keys are renamed"
|
|
12
|
+
arg :to_prefix, String, required: true, description: "dotted prefix the keys are renamed to"
|
|
13
|
+
arg :dry_run, :boolean, description: "true returns the Plan without writing; default false applies the rename immediately"
|
|
14
14
|
response(&:to_h)
|
|
15
15
|
|
|
16
16
|
def initialize(container:, call:)
|
|
@@ -10,8 +10,9 @@ module Textus
|
|
|
10
10
|
verb :migrate
|
|
11
11
|
summary "Run a YAML migration plan (multi-op)."
|
|
12
12
|
surfaces :cli, :ruby, :mcp
|
|
13
|
-
arg :plan_yaml, String, required: true
|
|
14
|
-
|
|
13
|
+
arg :plan_yaml, String, required: true,
|
|
14
|
+
description: "YAML listing the migration ops (zone_mv, key_mv_prefix, key_delete_prefix) run in order"
|
|
15
|
+
arg :dry_run, :boolean, description: "true returns the combined plan without writing; default false applies every op immediately"
|
|
15
16
|
response(&:to_h)
|
|
16
17
|
|
|
17
18
|
def initialize(container:, call:)
|
|
@@ -11,7 +11,8 @@ module Textus
|
|
|
11
11
|
verb :rule_lint
|
|
12
12
|
summary "Diff candidate manifest YAML's rules against the live manifest. No writes."
|
|
13
13
|
surfaces :cli, :ruby, :mcp
|
|
14
|
-
arg :candidate_yaml, String, required: true
|
|
14
|
+
arg :candidate_yaml, String, required: true,
|
|
15
|
+
description: "manifest YAML; its `rules:` block is diffed against the live manifest (no writes)"
|
|
15
16
|
response(&:to_h)
|
|
16
17
|
|
|
17
18
|
def initialize(container:, call:)
|
|
@@ -11,9 +11,9 @@ module Textus
|
|
|
11
11
|
verb :zone_mv
|
|
12
12
|
summary "Rename a zone — manifest + files. Refuses if destination exists."
|
|
13
13
|
surfaces :cli, :ruby, :mcp
|
|
14
|
-
arg :from, String, required: true
|
|
15
|
-
arg :to, String, required: true
|
|
16
|
-
arg :dry_run, :boolean
|
|
14
|
+
arg :from, String, required: true, description: "current zone name"
|
|
15
|
+
arg :to, String, required: true, description: "new zone name; refused if a zone by this name already exists"
|
|
16
|
+
arg :dry_run, :boolean, description: "true returns the plan without writing; default false applies the rename immediately"
|
|
17
17
|
response(&:to_h)
|
|
18
18
|
|
|
19
19
|
def initialize(container:, call:)
|
data/lib/textus/mcp/catalog.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Textus
|
|
|
11
11
|
# Contracts of every MCP-surfaced verb, in Dispatcher order.
|
|
12
12
|
def specs
|
|
13
13
|
Textus::Dispatcher::VERBS.values
|
|
14
|
-
.select { |k|
|
|
14
|
+
.select { |k| mcp_surfaced?(k) }
|
|
15
15
|
.map(&:contract)
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -25,9 +25,34 @@ module Textus
|
|
|
25
25
|
specs.map { |s| s.verb.to_s }
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# MCP-surfaced read verbs, by Dispatcher class namespace — the agent's
|
|
29
|
+
# real read/discovery surface. `boot.agent_quickstart.read_verbs` derives
|
|
30
|
+
# from this so it can never advertise a verb the agent cannot call, nor
|
|
31
|
+
# omit one it can (ADR 0056). Excludes Write/Maintenance.
|
|
32
|
+
def read_verbs
|
|
33
|
+
Textus::Dispatcher::VERBS
|
|
34
|
+
.select { |_verb, klass| mcp_surfaced?(klass) && klass.name.start_with?("Textus::Read::") }
|
|
35
|
+
.keys.map(&:to_s)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# MCP-surfaced write verbs, by Dispatcher class namespace — the mirror of
|
|
39
|
+
# read_verbs for the write side. `boot.agent_quickstart.write_verbs` derives
|
|
40
|
+
# from this so it advertises bare verb names the agent can call (no `--as`/
|
|
41
|
+
# `--stdin` CLI framing), finishing the de-CLI-ing of the agent surface
|
|
42
|
+
# (ADR 0056, ADR 0057).
|
|
43
|
+
def write_verbs
|
|
44
|
+
Textus::Dispatcher::VERBS
|
|
45
|
+
.select { |_verb, klass| mcp_surfaced?(klass) && klass.name.start_with?("Textus::Write::") }
|
|
46
|
+
.keys.map(&:to_s)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def mcp_surfaced?(klass)
|
|
50
|
+
klass.respond_to?(:contract?) && klass.contract? && klass.contract.mcp?
|
|
51
|
+
end
|
|
52
|
+
|
|
28
53
|
def call(name, session:, store:, args:)
|
|
29
54
|
klass = Textus::Dispatcher::VERBS[name.to_sym]
|
|
30
|
-
raise ToolError.new("unknown tool: #{name}") unless klass
|
|
55
|
+
raise ToolError.new("unknown tool: #{name}") unless klass && mcp_surfaced?(klass)
|
|
31
56
|
|
|
32
57
|
spec = klass.contract
|
|
33
58
|
pos, kw = map_args(spec, args || {}, session)
|
|
@@ -45,14 +70,14 @@ module Textus
|
|
|
45
70
|
# the session when absent from the wire; they are never treated as missing.
|
|
46
71
|
# Positional args are emitted in contract declaration order; use-case signatures must match.
|
|
47
72
|
def map_args(spec, raw, session = nil)
|
|
48
|
-
missing = spec.required_args.map { |a| a.
|
|
73
|
+
missing = spec.required_args.map { |a| a.wire.to_s } - raw.keys
|
|
49
74
|
raise ToolError.new("#{spec.verb}: missing #{missing.join(", ")}") unless missing.empty?
|
|
50
75
|
|
|
51
76
|
positional = []
|
|
52
77
|
keyword = {}
|
|
53
78
|
spec.args.each do |a|
|
|
54
|
-
if raw.key?(a.
|
|
55
|
-
value = raw[a.
|
|
79
|
+
if raw.key?(a.wire.to_s)
|
|
80
|
+
value = raw[a.wire.to_s]
|
|
56
81
|
elsif a.session_default && session
|
|
57
82
|
value = session.public_send(a.session_default)
|
|
58
83
|
else
|
data/lib/textus/read/get.rb
CHANGED
|
@@ -11,7 +11,8 @@ module Textus
|
|
|
11
11
|
verb :get
|
|
12
12
|
summary "Read one entry. Returns the envelope (uid, etag, _meta, body, freshness)."
|
|
13
13
|
surfaces :cli, :ruby, :mcp
|
|
14
|
-
arg :key, String, required: true, positional: true
|
|
14
|
+
arg :key, String, required: true, positional: true,
|
|
15
|
+
description: "dotted entry key to read, e.g. 'knowledge.project'"
|
|
15
16
|
response(&:to_h_for_wire)
|
|
16
17
|
|
|
17
18
|
def initialize(container:, call:, evaluator: Textus::Domain::Freshness::Evaluator)
|
data/lib/textus/read/list.rb
CHANGED
|
@@ -6,8 +6,8 @@ module Textus
|
|
|
6
6
|
verb :list
|
|
7
7
|
summary "List keys filtered by zone and/or prefix."
|
|
8
8
|
surfaces :cli, :ruby, :mcp
|
|
9
|
-
arg :prefix, String
|
|
10
|
-
arg :zone, String
|
|
9
|
+
arg :prefix, String, description: "restrict to keys starting with this dotted prefix, e.g. 'knowledge.runbooks'"
|
|
10
|
+
arg :zone, String, description: "restrict to one zone by name (see `boot` zones); combine with prefix to narrow further"
|
|
11
11
|
|
|
12
12
|
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
13
13
|
@manifest = container.manifest
|
data/lib/textus/read/rules.rb
CHANGED
|
@@ -9,7 +9,8 @@ module Textus
|
|
|
9
9
|
verb :rules
|
|
10
10
|
summary "Return effective rules for a key (fetch, guard, ...)."
|
|
11
11
|
surfaces :ruby, :mcp
|
|
12
|
-
arg :key, String, required: true, positional: true
|
|
12
|
+
arg :key, String, required: true, positional: true,
|
|
13
|
+
description: "dotted key whose effective rules you want (fetch ttl/action, write guard, ...)"
|
|
13
14
|
|
|
14
15
|
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
15
16
|
@manifest = container.manifest
|
|
@@ -6,7 +6,8 @@ module Textus
|
|
|
6
6
|
verb :schema
|
|
7
7
|
summary "Return the schema (field shape) for an entry's family, by key."
|
|
8
8
|
surfaces :ruby, :mcp
|
|
9
|
-
arg :key, String, required: true, positional: true
|
|
9
|
+
arg :key, String, required: true, positional: true,
|
|
10
|
+
description: "any key in the family whose schema you want; returns required/optional fields and their types"
|
|
10
11
|
|
|
11
12
|
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
12
13
|
@manifest = container.manifest
|
data/lib/textus/version.rb
CHANGED
|
@@ -6,8 +6,8 @@ module Textus
|
|
|
6
6
|
verb :fetch_all
|
|
7
7
|
summary "Fetch all stale quarantine entries, optionally scoped by zone/prefix."
|
|
8
8
|
surfaces :cli, :ruby, :mcp
|
|
9
|
-
arg :prefix, String
|
|
10
|
-
arg :zone, String
|
|
9
|
+
arg :prefix, String, description: "only refresh stale entries whose key starts with this dotted prefix"
|
|
10
|
+
arg :zone, String, description: "only refresh stale entries in this quarantine zone (see `pulse` stale list)"
|
|
11
11
|
|
|
12
12
|
def initialize(container:, call:)
|
|
13
13
|
@container = container
|
|
@@ -8,7 +8,8 @@ module Textus
|
|
|
8
8
|
verb :fetch
|
|
9
9
|
summary "Run a fetch action for one quarantine entry."
|
|
10
10
|
surfaces :cli, :ruby, :mcp
|
|
11
|
-
arg :key, String, required: true, positional: true
|
|
11
|
+
arg :key, String, required: true, positional: true,
|
|
12
|
+
description: "quarantine-zone entry key to refresh using its declared intake action"
|
|
12
13
|
response { |outcome| { "outcome" => outcome.class.name.split("::").last.downcase } }
|
|
13
14
|
|
|
14
15
|
FETCH_TIMEOUT_SECONDS = IntakeFetch::FETCH_TIMEOUT_SECONDS
|
data/lib/textus/write/propose.rb
CHANGED
|
@@ -12,9 +12,12 @@ module Textus
|
|
|
12
12
|
surfaces :cli, :ruby, :mcp
|
|
13
13
|
arg :key, String, required: true, positional: true,
|
|
14
14
|
description: "key relative to propose_zone, e.g. 'decisions.feature-x'"
|
|
15
|
-
arg :meta, Hash, required: true
|
|
16
|
-
|
|
17
|
-
arg :
|
|
15
|
+
arg :meta, Hash, required: true, wire_name: :_meta,
|
|
16
|
+
description: "frontmatter; reads back as `_meta` from `get`. Include a 'proposal:' block naming the target_key"
|
|
17
|
+
arg :body, String,
|
|
18
|
+
description: "markdown/text payload for markdown-format entries; omit (use `content`) for json/yaml entries. Do not send both"
|
|
19
|
+
arg :content, Hash,
|
|
20
|
+
description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
|
|
18
21
|
response { |env| { "uid" => env.uid, "etag" => env.etag, "key" => env.key } }
|
|
19
22
|
|
|
20
23
|
def initialize(container:, call:)
|
data/lib/textus/write/put.rb
CHANGED
|
@@ -6,11 +6,16 @@ module Textus
|
|
|
6
6
|
verb :put
|
|
7
7
|
summary "Create or update an entry. Schema-validated. Returns {uid, etag}."
|
|
8
8
|
surfaces :cli, :ruby, :mcp
|
|
9
|
-
arg :key, String, required: true, positional: true
|
|
10
|
-
|
|
11
|
-
arg :
|
|
12
|
-
|
|
13
|
-
arg :
|
|
9
|
+
arg :key, String, required: true, positional: true,
|
|
10
|
+
description: "dotted entry key, e.g. 'knowledge.project'; must resolve to a zone the role may write"
|
|
11
|
+
arg :meta, Hash, required: true, wire_name: :_meta,
|
|
12
|
+
description: "frontmatter; reads back as `_meta` from `get`. Schema-validated — call `schema KEY` first"
|
|
13
|
+
arg :body, String,
|
|
14
|
+
description: "markdown/text payload for markdown-format entries; omit (use `content`) for json/yaml entries. Do not send both"
|
|
15
|
+
arg :content, Hash,
|
|
16
|
+
description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
|
|
17
|
+
arg :if_etag, String,
|
|
18
|
+
description: "optimistic-concurrency guard: the etag you last read; the write is rejected if the entry changed since"
|
|
14
19
|
response { |env| { "uid" => env.uid, "etag" => env.etag } }
|
|
15
20
|
|
|
16
21
|
def initialize(container:, call:)
|