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
recipesandrole_resolutionblocks 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.summaryat 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 =
Textus::Boot.build_cli_verbs.freeze
Class Method Summary collapse
- .agent_protocol(manifest) ⇒ 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
- .inline_boot_content(manifest, _latest_seq) ⇒ Object
-
.lane_label(manifest, kind, fallback) ⇒ Object
Human-readable name(s) for the live lane(s) of a given kind, or
fallbackwhen the manifest declares none. - .lanes_for(manifest) ⇒ Object
- .read_artifact_content(container, key) ⇒ Object
- .read_boot_context(container) ⇒ Object
Class Method Details
.agent_protocol(manifest) ⇒ Object
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/textus/boot.rb', line 123 def self.agent_protocol(manifest) queue = manifest.policy.queue_lane feeds = lane_label(manifest, :machine, "the machine lane") AGENT_PROTOCOL_TEMPLATE.merge( "recipes" => { "read" => { "purpose" => "find and read an entry", "steps" => ["list (lane:, prefix:) — discover keys", "get KEY — returns the entry envelope"] }, "write" => { "purpose" => "create or update an entry", "steps" => ["schema KEY — learn field shape", "put KEY — persist it (role-gated)"] }, "propose" => { "purpose" => "agent suggests a change for human review", "agent_steps" => ["propose KEY — writes to #{queue} lane"], "human_steps" => ["accept #{queue}.KEY — promotes to target lane"] }, "drain" => { "purpose" => "keep machine lanes fresh", "steps" => ["pulse — stale list names overdue entries", "drain (lane: #{feeds}) — re-pull stale entries"] }, }, "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 |
.build(container:) ⇒ Object
75 76 77 78 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 75 def self.build(container:) etag = Textus::Value::Etag.for_contract(container.root) latest_seq = container.audit_log.latest_seq artifact = read_artifact_content(container, "artifacts.boot") context = read_boot_context(container) # Prefer pre-computed artifact (drain computes, boot reads). # Fall back to inline manifest projection for stores that have not yet # run drain (test fixtures, fresh inits). stable = artifact || inline_boot_content(container.manifest, latest_seq) if stable["agent_quickstart"] stable = stable.merge( "agent_quickstart" => stable["agent_quickstart"].merge("latest_seq" => latest_seq), ) end payload = { "protocol" => PROTOCOL_ID, "store_root" => container.root, "contract_etag" => etag, }.merge(stable) payload["context"] = context if context payload 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.
67 68 69 70 71 72 73 |
# File 'lib/textus/boot.rb', line 67 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
60 61 62 |
# File 'lib/textus/boot.rb', line 60 def self.contract_summaries Textus::VerbRegistry.registered.to_h { |s| [s.verb.to_s, s.summary] } end |
.inline_boot_content(manifest, _latest_seq) ⇒ Object
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/textus/boot.rb', line 101 def self.inline_boot_content(manifest, _latest_seq) agent_role = manifest.policy.proposer_role writable_lanes = manifest.data.declared_lane_kinds.keys.each_with_object([]) do |ln, acc| next unless agent_role verb = manifest.policy.verb_for_lane(ln) writers = manifest.policy.roles_with_capability(verb) acc << ln if writers.include?(agent_role) end { "lanes" => lanes_for(manifest), "agent_quickstart" => { "read_verbs" => Textus::Surface::MCP::Catalog.read_verbs, "write_verbs" => agent_role ? Textus::Surface::MCP::Catalog.write_verbs : [], "writable_lanes" => writable_lanes, "propose_lane" => manifest.policy.propose_lane_for(agent_role), }, "agent_protocol" => agent_protocol(manifest), } 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
148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/textus/boot.rb', line 148 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
160 161 162 163 164 165 166 167 168 |
# File 'lib/textus/boot.rb', line 160 def self.read_artifact_content(container, key) res = container.manifest.resolver.resolve(key) return nil unless res.path && File.exist?(res.path) env = Textus::Store::Entry::Reader.from(container: container).read(key) env&.content rescue Textus::Error nil end |
.read_boot_context(container) ⇒ Object
170 171 172 173 174 175 176 177 178 179 |
# File 'lib/textus/boot.rb', line 170 def self.read_boot_context(container) res = container.manifest.resolver.resolve("knowledge.boot") return nil unless res.path && File.exist?(res.path) env = Textus::Store::Entry::Reader.from(container: container).read("knowledge.boot") body = env&.body&.strip body.nil? || body.empty? ? nil : body rescue Textus::Error nil end |