Module: Textus::Boot
- Defined in:
- lib/textus/boot.rb
Overview
Read-only “what’s in this store and how do I use it” envelope. Boot is side-effect-free. Reads from pre-computed artifacts and the store catalog rather than computing inline.
Constant Summary collapse
- PROTOCOL_ID =
PROTOCOL- AGENT_PROTOCOL_TEMPLATE =
Static, store-independent parts of the agent-facing protocol. The ‘recipes` and `role_resolution` blocks are derived per-manifest in agent_protocol(…) because lane and role names are user-configurable.
{ "envelope_shape" => { "summary" => "every read/write payload is a JSON envelope with _meta, body, uid, and etag", "fields" => { "_meta" => "hash of structured frontmatter; schema-validated per entry family", "body" => "string payload (markdown/text) or nil for json/yaml formats where body lives in _meta", "uid" => "stable 16-char hex identifier; preserved across writes and key renames", "etag" => "content hash; pass back on writes to detect concurrent edits", }, "ref" => "SPEC.md §8", }, }.freeze
- CURATED_CLI_VERBS =
Curated agent-facing verb catalog. This declares which verbs the operator CLI surfaces and in what order — the editorial presentation. The summary of each verb is a fact, not presentation: it is derived from ‘contract.summary` at load time (ADR 0039). A literal “summary” survives here only for grouped CLI tokens (schema/key/rule/hook) that aggregate several sub-contracts and so have no single contract to derive from. CLI_VERBS itself is assigned in textus.rb after Zeitwerk eager_load so all contract files are present.
[ { "name" => "boot" }, { "name" => "list" }, { "name" => "get" }, { "name" => "ingest" }, { "name" => "where" }, { "name" => "schema", "summary" => "schema operations: 'schema show KEY', 'schema diff', 'schema init', 'schema migrate'" }, { "name" => "put" }, { "name" => "propose" }, { "name" => "accept" }, { "name" => "enqueue" }, { "name" => "key", "summary" => "key operations: 'key delete', 'key mv', 'key uid'" }, { "name" => "drain" }, { "name" => "audit" }, { "name" => "blame" }, { "name" => "rule", "summary" => "inspect effective rules: 'rule list', 'rule explain KEY'" }, { "name" => "doctor" }, { "name" => "jobs" }, { "name" => "pulse" }, ].freeze
- CLI_VERBS =
Derive CLI_VERBS after VERBS is available.
Textus::Boot.build_cli_verbs.freeze
Class Method Summary collapse
- .agent_protocol(manifest) ⇒ Object
- .agent_quickstart(manifest, audit_log) ⇒ Object
- .build(container:) ⇒ Object
-
.build_cli_verbs ⇒ Object
Build the CLI verb catalog: each summary is derived from its contract when one exists, falling back to the curated editorial string for grouped tokens (schema/key/rule/hook).
-
.contract_summaries ⇒ Object
verb token => contract.summary, for every Dispatcher verb that carries a contract.
-
.lane_label(manifest, kind, fallback) ⇒ Object
Human-readable name(s) for the live lane(s) of a given kind, or ‘fallback` when the manifest declares none.
- .lanes_for(manifest) ⇒ Object
- .read_artifact_content(container, key) ⇒ Object
- .read_boot_context(container) ⇒ Object
- .recipes(manifest) ⇒ Object
Class Method Details
.agent_protocol(manifest) ⇒ Object
139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/textus/boot.rb', line 139 def self.agent_protocol(manifest) AGENT_PROTOCOL_TEMPLATE.merge( "recipes" => recipes(manifest), "role_resolution" => { "summary" => "write role is resolved in order: --as flag, TEXTUS_ROLE env var, .textus/role file, " \ "then a transport default ('human' for CLI, 'agent' for MCP)", "roles" => manifest.data.role_caps.keys, "ref" => "SPEC.md §5", }, ) end |
.agent_quickstart(manifest, audit_log) ⇒ Object
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/textus/boot.rb', line 79 def self.agent_quickstart(manifest, audit_log) agent_role = manifest.policy.proposer_role writable_lanes = manifest.data.declared_lane_kinds.keys.each_with_object([]) do |lane_name, acc| next unless agent_role verb = manifest.policy.verb_for_lane(lane_name) writers = manifest.policy.roles_with_capability(verb) acc << lane_name if writers.include?(agent_role) end propose_lane = manifest.policy.propose_lane_for(agent_role) { "read_verbs" => Textus::Surfaces::MCP::Catalog.read_verbs, "write_verbs" => agent_role ? Textus::Surfaces::MCP::Catalog.write_verbs : [], "writable_lanes" => writable_lanes, "propose_lane" => propose_lane, "latest_seq" => audit_log.latest_seq, } end |
.build(container:) ⇒ Object
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/textus/boot.rb', line 151 def self.build(container:) manifest = container.manifest etag = Textus::Etag.for_contract(container.root) { "protocol" => PROTOCOL_ID, "store_root" => container.root, "contract_etag" => etag, "lanes" => lanes_for(manifest), "agent_quickstart" => agent_quickstart(manifest, container.audit_log), "orientation" => read_artifact_content(container, "artifacts.orientation"), "context" => read_boot_context(container), "index_key" => "artifacts.index", "agent_protocol" => agent_protocol(manifest), }.compact end |
.build_cli_verbs ⇒ Object
Build the CLI verb catalog: each summary is derived from its contract when one exists, falling back to the curated editorial string for grouped tokens (schema/key/rule/hook). Called once from textus.rb after eager_load.
71 72 73 74 75 76 77 |
# File 'lib/textus/boot.rb', line 71 def self.build_cli_verbs summaries = contract_summaries CURATED_CLI_VERBS.map do |entry| derived = summaries[entry["name"]] derived ? entry.merge("summary" => derived) : entry end end |
.contract_summaries ⇒ Object
verb token => contract.summary, for every Dispatcher verb that carries a contract. The single source for a verb’s one-line summary (ADR 0039).
62 63 64 65 66 |
# File 'lib/textus/boot.rb', line 62 def self.contract_summaries Textus::Action::VERBS.values .select { |k| k.respond_to?(:contract?) && k.contract? } .to_h { |k| [k.contract.verb.to_s, k.contract.summary] } end |
.lane_label(manifest, kind, fallback) ⇒ Object
Human-readable name(s) for the live lane(s) of a given kind, or ‘fallback` when the manifest declares none. Lets write-flow guidance name the live lane by kind instead of a hardcoded instance name (ADR 0034).
11 12 13 14 |
# File 'lib/textus/boot.rb', line 11 def self.lane_label(manifest, kind, fallback) lanes = manifest.policy.lanes_of_kind(kind) lanes.empty? ? fallback : lanes.join(", ") end |
.lanes_for(manifest) ⇒ Object
191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/textus/boot.rb', line 191 def self.lanes_for(manifest) manifest.data.declared_lane_kinds.keys.map do |name| verb = manifest.policy.verb_for_lane(name) row = { "name" => name, "writers" => manifest.policy.roles_with_capability(verb) } kind = manifest.policy.declared_kind(name) row["kind"] = kind.to_s if kind purpose = manifest.data.lane_descs[name] row["purpose"] = purpose if purpose && !purpose.empty? row end end |
.read_artifact_content(container, key) ⇒ Object
168 169 170 171 172 173 174 175 176 177 |
# File 'lib/textus/boot.rb', line 168 def self.read_artifact_content(container, key) res = container.manifest.resolver.resolve(key) return nil unless res.path && File.exist?(res.path) call = Textus::Call.build(role: Textus::Role::DEFAULT) env = Textus::Action::Get.new(key: key).call(container: container, call: call) env&.content rescue Textus::Error nil end |
.read_boot_context(container) ⇒ Object
179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/textus/boot.rb', line 179 def self.read_boot_context(container) res = container.manifest.resolver.resolve("knowledge.boot") return nil unless res.path && File.exist?(res.path) call = Textus::Call.build(role: Textus::Role::DEFAULT) env = Textus::Action::Get.new(key: "knowledge.boot").call(container: container, call: call) body = env&.body&.strip body.nil? || body.empty? ? nil : body rescue Textus::Error nil end |
.recipes(manifest) ⇒ Object
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/textus/boot.rb', line 101 def self.recipes(manifest) queue = manifest.policy.queue_lane feeds = lane_label(manifest, :machine, "the machine lane") { "read" => { "purpose" => "find and read an entry", "steps" => [ "list (lane:, prefix:) — discover keys without reading bodies", "get KEY — returns the entry envelope", ], }, "write" => { "purpose" => "create or update an entry", "steps" => [ "schema KEY — learn the _meta field shape (required, optional, field types) before writing", "assemble an envelope: { _meta: {…}, body: \"…\" }", "put KEY — persist it (role-gated); pass if_etag to guard a concurrent edit", ], }, "propose" => { "purpose" => "agent suggests a change for human review", "agent_steps" => [ "propose KEY — writes the change into the #{queue} lane for review", ], "human_steps" => [ "accept #{queue}.KEY — promotes the proposal into its target lane", ], }, "drain" => { "purpose" => "keep the machine-maintained lanes fresh — re-pull stale intake entries from their declared source", "steps" => [ "pulse — its `stale` list names entries past their ttl", "drain (lane: #{feeds}) — re-pull the stale entries", ], }, } end |