Module: Textus::Init
- Defined in:
- lib/textus/init.rb
Constant Summary collapse
- ZONES =
%w[knowledge notebook proposals artifacts].freeze
- DEFAULT_MANIFEST =
<<~YAML version: textus/3 roles: - { name: human, can: [author, propose] } - { name: agent, can: [propose, keep] } - { name: automation, can: [converge] } lanes: - { name: knowledge, kind: canon, desc: "the maintained source of truth (identity.* lives here)" } - { name: notebook, kind: workspace, owner: agent, desc: "the agent's own durable working notes" } - { name: proposals, kind: queue, desc: "changes awaiting your accept" } - { name: artifacts, kind: machine, desc: "machine-maintained: external inputs (artifacts.feeds.*) + computed outputs (artifacts.derived.*)" } entries: - { key: knowledge.identity, path: data/knowledge/identity.md, lane: knowledge, schema: null, owner: human:self, kind: leaf } - { key: knowledge.notes, path: data/knowledge/notes, lane: knowledge, schema: null, owner: human:self, nested: true, kind: nested } - { key: notebook.notes, path: data/notebook/notes, lane: notebook, schema: null, owner: agent:self, nested: true, kind: nested } - { key: proposals.notes, path: data/proposals/notes, lane: proposals, schema: null, owner: agent:self, nested: true, kind: nested } # A per-host snapshot, refreshed from its declared intake by `textus drain` (scheduled, or on demand). # Nested so it grows to a fleet — add artifacts.feeds.machines.<host> leaves over SSH # (see docs/cookbook/environment-scan.md) without renaming. tracked:false → # gitignored (machine info can be sensitive/noisy) but still protocol-readable # via `textus get artifacts.feeds.machines.local`. Delete to opt out. (ADR 0043) - key: artifacts.feeds.machines path: data/artifacts/feeds/machines lane: artifacts format: yaml nested: true tracked: false kind: produced source: from: fetch handler: machine-intake ttl: 1h # cadence on a long-running server config: machines: local: { via: local } rules: [] YAML
- STEPS_README =
<<~MD # Steps Drop one Ruby file per step. Steps are discovered by convention. Files under `.textus/steps/<kind>/<name>.rb` are loaded at startup and registered. ## Conventions The directory name (`<kind>`) must be one of: - `fetch`: Acquires data from outside the store. - `transform`: Reshapes projection rows. - `validate`: Validates data before writing. - `observe`: Listens to store events. The filename (`<name>.rb`) defines the step name. The class defined in the file must be a subclass of `Textus::Step::<Kind>` (e.g. `Textus::Step::Fetch`) and be wrapped in the `Textus::Step` module. ## Example ```ruby module Textus module Step class MyFetch < Fetch def call(config:, args:, caps:, **) { content: { "foo" => "bar" } } end end end end ``` Events: :fetch, :transform, :validate (rpc — return value used) :entry_written, :entry_deleted, :entry_fetched, :entry_renamed, :entry_produced, :produce_failed, :proposal_accepted, :proposal_rejected, :entry_published, :store_loaded, :session_opened, :entry_fetch_started, :entry_fetch_failed (pub-sub — return discarded) See SPEC.md §5.10 for the full table. MD
- AGENT_ENTRIES =
<<~YAML.gsub(/^/, " ") - { key: knowledge.project, path: data/knowledge/project.md, lane: knowledge, schema: project, owner: human:self, kind: leaf } - { key: knowledge.runbooks, path: data/knowledge/runbooks, lane: knowledge, schema: runbook, owner: human:self, nested: true, kind: nested } - key: artifacts.derived.orientation path: data/artifacts/derived/orientation.json lane: artifacts publish: - { to: CLAUDE.md, template: orientation.mustache, inject_boot: true } - { to: AGENTS.md, template: orientation.mustache, inject_boot: true } source: from: derive select: - knowledge.project - knowledge.runbooks transform: orientation kind: produced YAML
Class Method Summary collapse
- .build_result(target_root, with_agent:, mcp_status:) ⇒ Object
- .check_target!(target_root) ⇒ Object
- .create_directories(target_root) ⇒ Object
-
.derived_gitignore(target_root) ⇒ Object
The store’s ‘.gitignore` is generated, never hand-kept (ADR 0038), and now derived from the manifest: the run subtree plus every `tracked: false` entry’s resolved path (ADR 0043).
-
.manifest_yaml(with_agent:) ⇒ Object
Composes the agent profile by inserting AGENT_ENTRIES immediately before the top-level ‘rules:` block of DEFAULT_MANIFEST — that block is load-bearing for this `.sub`; removing it from DEFAULT_MANIFEST would silently drop the entries.
- .run(target_root, with_agent: false) ⇒ Object
- .scaffold_agent(target_root, scaffold_dir, with_agent:) ⇒ Object
-
.scaffold_agent_profile(target_root, scaffold_dir) ⇒ Object
Copies the proven orientation bundle into a freshly-init’d store.
- .setup_state_dirs(target_root) ⇒ Object
- .write_gitignore(target_root) ⇒ Object
- .write_manifest(target_root, with_agent:) ⇒ Object
-
.write_mcp_config(target_root, scaffold_dir) ⇒ Object
The one file init writes outside .textus/: a starter .mcp.json at the project root.
- .write_steps_readme(target_root, scaffold_dir) ⇒ Object
Class Method Details
.build_result(target_root, with_agent:, mcp_status:) ⇒ Object
166 167 168 169 170 |
# File 'lib/textus/init.rb', line 166 def self.build_result(target_root, with_agent:, mcp_status:) result = { "protocol" => PROTOCOL, "initialized" => target_root, "profile" => with_agent ? "agent" : "default" } result["mcp_config"] = mcp_status if with_agent result end |
.check_target!(target_root) ⇒ Object
119 120 121 |
# File 'lib/textus/init.rb', line 119 def self.check_target!(target_root) raise UsageError.new(".textus/ already exists at #{target_root}") if File.directory?(target_root) end |
.create_directories(target_root) ⇒ Object
123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/textus/init.rb', line 123 def self.create_directories(target_root) FileUtils.mkdir_p(File.join(target_root, "schemas")) FileUtils.mkdir_p(File.join(target_root, "templates")) FileUtils.mkdir_p(File.join(target_root, "steps/fetch")) FileUtils.mkdir_p(File.join(target_root, "steps/transform")) FileUtils.mkdir_p(File.join(target_root, "steps/validate")) FileUtils.mkdir_p(File.join(target_root, "steps/observe")) ZONES.each do |z| dir = File.join(target_root, "data", z) FileUtils.mkdir_p(dir) File.write(File.join(dir, ".gitkeep"), "") end end |
.derived_gitignore(target_root) ⇒ Object
The store’s ‘.gitignore` is generated, never hand-kept (ADR 0038), and now derived from the manifest: the run subtree plus every `tracked: false` entry’s resolved path (ADR 0043).
208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/textus/init.rb', line 208 def self.derived_gitignore(target_root) manifest = Textus::Manifest.load(target_root) root = Pathname.new(target_root) untracked = manifest.data.entries.reject(&:tracked?).map do |e| if e.nested? # a whole subtree of leaf files (artifacts.feeds.machines.* → data/artifacts/feeds/machines/) rel = e.path.start_with?("data/") ? e.path : File.join("data", e.path) "#{rel}/" else Pathname.new(Textus::Key::Path.resolve(manifest.data, e)).relative_path_from(root).to_s end end Textus::Layout.gitignore_body(untracked_paths: untracked) end |
.manifest_yaml(with_agent:) ⇒ Object
Composes the agent profile by inserting AGENT_ENTRIES immediately before the top-level ‘rules:` block of DEFAULT_MANIFEST — that block is load-bearing for this `.sub`; removing it from DEFAULT_MANIFEST would silently drop the entries.
175 176 177 178 179 |
# File 'lib/textus/init.rb', line 175 def self.manifest_yaml(with_agent:) return DEFAULT_MANIFEST unless with_agent DEFAULT_MANIFEST.sub(/^rules:/, "#{AGENT_ENTRIES}rules:") end |
.run(target_root, with_agent: false) ⇒ Object
107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/textus/init.rb', line 107 def self.run(target_root, with_agent: false) check_target!(target_root) scaffold_dir = File.("init/templates", __dir__) create_directories(target_root) write_steps_readme(target_root, scaffold_dir) write_manifest(target_root, with_agent:) mcp_status = scaffold_agent(target_root, scaffold_dir, with_agent:) setup_state_dirs(target_root) write_gitignore(target_root) build_result(target_root, with_agent:, mcp_status:) end |
.scaffold_agent(target_root, scaffold_dir, with_agent:) ⇒ Object
149 150 151 152 153 154 |
# File 'lib/textus/init.rb', line 149 def self.scaffold_agent(target_root, scaffold_dir, with_agent:) return nil unless with_agent scaffold_agent_profile(target_root, scaffold_dir) write_mcp_config(target_root, scaffold_dir) end |
.scaffold_agent_profile(target_root, scaffold_dir) ⇒ Object
Copies the proven orientation bundle into a freshly-init’d store.
182 183 184 185 186 187 188 189 190 191 |
# File 'lib/textus/init.rb', line 182 def self.scaffold_agent_profile(target_root, scaffold_dir) { "project.schema.yaml" => File.join("schemas", "project.yaml"), "runbook.schema.yaml" => File.join("schemas", "runbook.yaml"), "orientation.mustache" => File.join("templates", "orientation.mustache"), "orientation_reducer.rb" => File.join("steps/transform", "orientation.rb"), }.each do |src, dest| File.write(File.join(target_root, dest), File.read(File.join(scaffold_dir, src))) end end |
.setup_state_dirs(target_root) ⇒ Object
156 157 158 159 160 |
# File 'lib/textus/init.rb', line 156 def self.setup_state_dirs(target_root) FileUtils.mkdir_p(Textus::Layout.audit_dir(target_root)) FileUtils.mkdir_p(Textus::Layout.state(target_root)) FileUtils.mkdir_p(Textus::Layout.locks(target_root)) end |
.write_gitignore(target_root) ⇒ Object
162 163 164 |
# File 'lib/textus/init.rb', line 162 def self.write_gitignore(target_root) File.write(File.join(target_root, ".gitignore"), derived_gitignore(target_root)) end |
.write_manifest(target_root, with_agent:) ⇒ Object
145 146 147 |
# File 'lib/textus/init.rb', line 145 def self.write_manifest(target_root, with_agent:) File.write(File.join(target_root, "manifest.yaml"), manifest_yaml(with_agent: with_agent)) end |
.write_mcp_config(target_root, scaffold_dir) ⇒ Object
The one file init writes outside .textus/: a starter .mcp.json at the project root. Write-once — never clobber a hand-authored config. The command form assumes a gem-installed ‘textus` on PATH; the user owns the file after this first write.
197 198 199 200 201 202 203 |
# File 'lib/textus/init.rb', line 197 def self.write_mcp_config(target_root, scaffold_dir) dest = File.join(File.dirname(target_root), ".mcp.json") return "skipped" if File.exist?(dest) File.write(dest, File.read(File.join(scaffold_dir, "mcp.json"))) "written" end |
.write_steps_readme(target_root, scaffold_dir) ⇒ Object
137 138 139 140 141 142 143 |
# File 'lib/textus/init.rb', line 137 def self.write_steps_readme(target_root, scaffold_dir) File.write(File.join(target_root, "steps/README.md"), STEPS_README) File.write( File.join(target_root, "steps/fetch/machine-intake.rb"), File.read(File.join(scaffold_dir, "machine_intake.rb")), ) end |