Module: Textus::Init
- Defined in:
- lib/textus/init.rb
Constant Summary collapse
- ZONES =
%w[knowledge notebook feeds proposals artifacts].freeze
- DEFAULT_MANIFEST =
<<~YAML version: textus/3 roles: - { name: human, can: [author, propose] } - { name: agent, can: [propose, keep] } - { name: automation, can: [fetch, build] } zones: - { 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: feeds, kind: quarantine, desc: "external inputs pulled in" } - { name: proposals, kind: queue, desc: "changes awaiting your accept" } - { name: artifacts, kind: derived, desc: "computed, shippable outputs" } entries: - { key: knowledge.identity, path: knowledge/identity.md, zone: knowledge, schema: null, owner: human:self, kind: leaf } - { key: knowledge.notes, path: knowledge/notes, zone: knowledge, schema: null, owner: human:self, nested: true, kind: nested } - { key: notebook.notes, path: notebook/notes, zone: notebook, schema: null, owner: agent:self, nested: true, kind: nested } - { key: proposals.notes, path: proposals/notes, zone: proposals, schema: null, owner: agent:self, nested: true, kind: nested } # A per-host snapshot, pulled by `textus fetch feeds.machines.local --as=automation`. # Nested so it grows to a fleet — add 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 feeds.machines.local`. Delete to opt out. (ADR 0043) - key: feeds.machines path: feeds/machines zone: feeds format: yaml nested: true tracked: false kind: intake intake: handler: machines config: machines: local: { via: local } rules: - match: feeds.machines.** fetch: { ttl: 1h, on_stale: warn } # meaningful on a long-running server YAML
- HOOKS_README =
<<~MD # Hooks Drop one Ruby file per hook. All hooks register through one DSL. Files anywhere under `.textus/hooks/` (including subdirectories) are loaded at startup in alphabetical order by full path. Subdirectory names are organizational only — the registered event and name come from the DSL call, not the file path. ## DSL ```ruby Textus.hook do |reg| reg.on(:resolve_intake, :my_source) do |config:, args:, **| { _meta: { "last_fetched_at" => Time.now.utc.iso8601 }, body: "…" } end reg.on(:transform_rows, :my_source) { |rows:, **| rows.map { |r| r.merge(processed: true) } } reg.on(:validate, :my_check) { |caps:, **| [] } reg.on(:entry_put, :my_listener, keys: ["knowledge.*"]) { |key:, envelope:, **| } # Run a side-effect every time textus writes a file to your repo: reg.on(:file_published, :notify) do |key:, target:, **| warn "wrote \#{target} (from \#{key})" end end ``` The intake handler above is paired with a manifest entry plus a top-level `rules:` block for freshness (ttl/on_stale live in rules, not in the entry): ```yaml entries: - key: feeds.foo kind: intake path: feeds/foo.md zone: feeds intake: handler: my_source rules: - match: feeds.foo fetch: ttl: 10m on_stale: timed_sync # warn | sync | timed_sync (default: warn) ``` Events: :resolve_intake, :transform_rows, :validate (rpc — return value used) :entry_put, :entry_deleted, :entry_fetched, :entry_renamed, :build_completed, :proposal_accepted, :proposal_rejected, :file_published, :store_loaded, :session_opened, :fetch_started, :fetch_failed, :fetch_backgrounded (pub-sub — return discarded) See SPEC.md §5.10 for the full table. MD
- AGENT_ENTRIES =
<<~YAML.gsub(/^/, " ") # --with-agent profile: project facts + runbooks feed the orientation # projection below, which `textus build` renders to CLAUDE.md/AGENTS.md. - { key: knowledge.project, path: knowledge/project.md, zone: knowledge, schema: project, owner: human:self, kind: leaf } - { key: knowledge.runbooks, path: knowledge/runbooks, zone: knowledge, schema: runbook, owner: human:self, nested: true, kind: nested } - key: artifacts.orientation path: artifacts/orientation.md zone: artifacts template: orientation.mustache inject_boot: true publish: to: - CLAUDE.md - AGENTS.md compute: kind: projection select: - knowledge.project - knowledge.runbooks transform: orientation_reducer kind: derived YAML
Class Method Summary collapse
-
.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_profile(target_root, scaffold_dir) ⇒ Object
Copies the proven orientation bundle into a freshly-init’d store.
-
.write_mcp_config(target_root, scaffold_dir) ⇒ Object
The one file init writes outside .textus/: a starter .mcp.json at the project root.
Class Method Details
.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).
192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/textus/init.rb', line 192 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 (feeds.machines.* → zones/feeds/machines/) "#{File.join("zones", e.path)}/" 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.
159 160 161 162 163 |
# File 'lib/textus/init.rb', line 159 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
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/textus/init.rb', line 126 def self.run(target_root, with_agent: false) raise UsageError.new(".textus/ already exists at #{target_root}") if File.directory?(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, "hooks")) ZONES.each do |z| dir = File.join(target_root, "zones", z) FileUtils.mkdir_p(dir) File.write(File.join(dir, ".gitkeep"), "") end File.write(File.join(target_root, "hooks", "README.md"), HOOKS_README) scaffold_dir = File.("init/templates", __dir__) File.write(File.join(target_root, "hooks", "machine_intake.rb"), File.read(File.join(scaffold_dir, "machine_intake.rb"))) File.write(File.join(target_root, "manifest.yaml"), manifest_yaml(with_agent: with_agent)) mcp_status = nil if with_agent scaffold_agent_profile(target_root, scaffold_dir) mcp_status = write_mcp_config(target_root, scaffold_dir) end 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)) File.write(File.join(target_root, ".gitignore"), derived_gitignore(target_root)) result = { "protocol" => PROTOCOL, "initialized" => target_root, "profile" => with_agent ? "agent" : "default" } result["mcp_config"] = mcp_status if with_agent result end |
.scaffold_agent_profile(target_root, scaffold_dir) ⇒ Object
Copies the proven orientation bundle into a freshly-init’d store.
166 167 168 169 170 171 172 173 174 175 |
# File 'lib/textus/init.rb', line 166 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("hooks", "orientation_reducer.rb"), }.each do |src, dest| File.write(File.join(target_root, dest), File.read(File.join(scaffold_dir, src))) end 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.
181 182 183 184 185 186 187 |
# File 'lib/textus/init.rb', line 181 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 |