Module: Textus::Boot

Defined in:
lib/textus/boot.rb

Overview

Read-only “what’s in this store and how do I use it” envelope. A single call gives an agent the working model of a textus-managed project: zones and their write authority, entries and their flags, registered hooks, write flows, and the CLI verb catalog.

Boot is side-effect-free.

Constant Summary collapse

PROTOCOL_ID =
PROTOCOL
ZONE_PURPOSES =

Conventional zone purposes. Unknown zones (declared in the manifest but not listed here) get no ‘purpose` field.

{
  "identity" => "slow-changing identity; human-only writes",
  "working" => "active project state; humans, AI, and scripts share this surface",
  "intake" => "declared external inputs; script-refreshed via actions",
  "review" => "AI proposals awaiting human accept",
  "output" => "build-computed outputs; never hand-edited",
}.freeze
WRITE_FLOW_TEMPLATES =

Per-kind write-flow templates. Each lambda receives the user-facing role name and returns a guidance string for that role. Roles whose kind has no template (e.g. unknown future kinds) are omitted from write_flows.

{
  accept_authority: lambda do |name, _manifest|
    "edit files in identity/working zones, then 'textus put KEY --as=#{name}'"
  end,
  proposer: lambda do |name, manifest|
    authority = manifest.roles_with_kind(:accept_authority).first || "accept_authority"
    "propose changes by writing review.* entries with --as=#{name} and a 'proposal:' frontmatter block; " \
      "the #{authority} role runs 'textus accept' to apply"
  end,
  runner: lambda do |name, _manifest|
    "refresh intake entries with 'textus refresh KEY --as=#{name}' (uses the entry's declared action)"
  end,
  generator: lambda do |_name, _manifest|
    "'textus build' computes output entries from projections; output files are never hand-edited"
  end,
}.freeze
AGENT_PROTOCOL_TEMPLATE =

Static, store-independent parts of the agent-facing protocol. The ‘role_resolution` block is derived per-manifest in agent_protocol(…) because 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",
  },
  "recipes" => {
    "read" => {
      "purpose" => "find and read an entry",
      "steps" => [
        "textus list --zone=ZONE --prefix=PREFIX  # discover keys",
        "textus get KEY                            # returns envelope JSON",
      ],
    },
    "write" => {
      "purpose" => "create or update an entry",
      "steps" => [
        "textus schema get FAMILY                  # learn the _meta field shape",
        "build an envelope JSON: {_meta: {...}, body: \"...\"}",
        "echo ENVELOPE | textus put KEY --as=ROLE --stdin",
      ],
    },
    "propose" => {
      "purpose" => "agent suggests a change for human review",
      "agent_steps" => [
        "echo ENVELOPE | textus put review.KEY --as=agent --stdin",
      ],
      "human_steps" => [
        "textus accept review.KEY --as=human       # promotes the proposal to its target zone",
      ],
    },
    "refresh" => {
      "purpose" => "rebuild stale intake-zone caches from their declared actions",
      "steps" => [
        "textus freshness --zone=intake            # report fresh/stale per entry",
        "textus refresh stale --zone=intake --as=runner",
      ],
    },
  },
}.freeze
CLI_VERBS =

The CLI verb catalog. Truth lives here; do not derive dynamically. Agents that read boot should see a stable shape regardless of how verb implementations evolve.

[
  { "name" => "boot",     "summary" => "this output — orientation for agents and tools" },
  { "name" => "list",     "summary" => "enumerate keys (optional --prefix)" },
  { "name" => "get",      "summary" => "read an entry; envelope with _meta, body, uid, etag" },
  { "name" => "where",    "summary" => "resolve a key to its zone and path without reading" },
  { "name" => "schema",   "summary" => "field shape for a key family" },
  { "name" => "put",      "summary" => "write an entry; --as=<role>, --stdin payload" },
  { "name" => "accept",   "summary" => "apply a review.* proposal; --as=human only" },
  { "name" => "key",      "summary" => "key operations: 'key mv', 'key uid'" },
  { "name" => "delete",   "summary" => "delete an entry; --as=<role>" },
  { "name" => "build",    "summary" => "materialize output entries; publish_to and publish_each fan out copies" },
  { "name" => "refresh",  "summary" => "run an action for an intake entry" },
  { "name" => "freshness", "summary" => "per-entry freshness report (status, age, ttl, on_stale)" },
  { "name" => "audit", "summary" => "query .textus/audit.log with filters (key, role, since, correlation-id, ...)" },
  { "name" => "blame", "summary" => "audit rows for one key joined with git commit metadata" },
  { "name" => "rule", "summary" => "inspect effective rules: 'rule list', 'rule explain KEY'" },
  { "name" => "doctor", "summary" => "health-check the store (missing schemas, illegal keys, sentinel drift, etc.)" },
  { "name" => "hook",
    "summary" => "list and run registered hooks: 'hook list', 'hook run NAME'" },
  { "name" => "pulse",
    "summary" => "delta since cursor — changed entries, stale, pending review, doctor summary" },
].freeze

Class Method Summary collapse

Class Method Details

.agent_protocol(manifest) ⇒ Object



142
143
144
145
146
147
148
149
150
151
# File 'lib/textus/boot.rb', line 142

def self.agent_protocol(manifest)
  AGENT_PROTOCOL_TEMPLATE.merge(
    "role_resolution" => {
      "summary" => "write role is resolved in order: --as flag, TEXTUS_ROLE env var, .textus/role file, " \
                   "default 'human'",
      "roles" => manifest.role_mapping.keys,
      "ref" => "SPEC.md §5",
    },
  )
end

.agent_quickstart(manifest, store) ⇒ Object



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/textus/boot.rb', line 123

def self.agent_quickstart(manifest, store)
  proposer_roles = manifest.roles_with_kind(:proposer)
  agent_role = proposer_roles.first

  writable_zones = manifest.zones.each_with_object([]) do |(zname, writers), acc|
    acc << zname if agent_role && writers.include?(agent_role)
  end

  propose_zone = writable_zones.find { |z| z.include?("review") } || writable_zones.first

  {
    "read_verbs" => %w[boot get list audit pulse freshness doctor],
    "write_verbs" => agent_role ? ["put KEY --as=#{agent_role} --stdin"] : [],
    "writable_zones" => writable_zones,
    "propose_zone" => propose_zone,
    "latest_seq" => store.audit_log.latest_seq,
  }
end

.entries_for(store) ⇒ Object



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/textus/boot.rb', line 177

def self.entries_for(store)
  store.manifest.entries.map do |e|
    derived = store.manifest.zone_kinds(e.zone).include?(:generator)
    {
      "key" => e.key,
      "zone" => e.zone,
      "schema" => e.schema,
      "nested" => e.is_a?(Textus::Manifest::Entry::Nested),
      "owner" => e.owner,
      "format" => e.format,
      "derived" => derived,
      "intake" => e.is_a?(Textus::Manifest::Entry::Intake),
      "publish_to" => Array(e.publish_to),
      "publish_each" => e.publish_each,
    }
  end
end

.hooks_for(store) ⇒ Object



195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/textus/boot.rb', line 195

def self.hooks_for(store)
  bus = store.bus
  sections = {}
  Hooks::Bus::EVENTS.each do |event, spec|
    case spec[:mode]
    when :rpc
      sections[event.to_s] = bus.rpc_names(event).map(&:to_s).sort
    when :pubsub
      sections[event.to_s] = bus.pubsub_handlers(event).map { |h| h[:name].to_s }.sort
    end
  end
  sections
end

.run(store) ⇒ Object



153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/textus/boot.rb', line 153

def self.run(store)
  {
    "protocol" => PROTOCOL_ID,
    "store_root" => store.root,
    "zones" => zones_for(store),
    "entries" => entries_for(store),
    "hooks" => hooks_for(store),
    "write_flows" => write_flows_for(store.manifest),
    "cli_verbs" => CLI_VERBS.map(&:dup),
    "agent_protocol" => agent_protocol(store.manifest),
    "agent_quickstart" => agent_quickstart(store.manifest, store),
    "docs" => { "spec" => "SPEC.md", "example" => "examples/claude-plugin/" },
  }
end

.write_flows_for(manifest) ⇒ Object



41
42
43
44
45
46
# File 'lib/textus/boot.rb', line 41

def self.write_flows_for(manifest)
  manifest.role_mapping.each_with_object({}) do |(name, kind), acc|
    tmpl = WRITE_FLOW_TEMPLATES[kind]
    acc[name] = tmpl.call(name, manifest) if tmpl
  end
end

.zones_for(store) ⇒ Object



168
169
170
171
172
173
174
175
# File 'lib/textus/boot.rb', line 168

def self.zones_for(store)
  store.manifest.zones.map do |name, writers|
    row = { "name" => name, "writers" => Array(writers) }
    purpose = ZONE_PURPOSES[name]
    row["purpose"] = purpose if purpose
    row
  end
end