Module: Textus::Init
- Defined in:
- lib/textus/init.rb
Constant Summary collapse
- ZONES =
%w[knowledge notebook proposals artifacts raw].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, populated by a registered workflow. Nested so it # grows to a fleet — add leaves over SSH 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: nested rules: [] YAML
- 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 } kind: produced YAML
- ASSET_ZONES =
%w[raw].freeze
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.
Class Method Details
.build_result(target_root, with_agent:, mcp_status:) ⇒ Object
104 105 106 107 108 |
# File 'lib/textus/init.rb', line 104 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
61 62 63 |
# File 'lib/textus/init.rb', line 61 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
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/textus/init.rb', line 67 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, "workflows")) ZONES.each do |z| dir = File.join(target_root, "data", z) FileUtils.mkdir_p(dir) File.write(File.join(dir, ".gitkeep"), "") end ASSET_ZONES.each do |z| dir = File.join(target_root, "assets", 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).
145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/textus/init.rb', line 145 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.
113 114 115 116 117 |
# File 'lib/textus/init.rb', line 113 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
50 51 52 53 54 55 56 57 58 59 |
# File 'lib/textus/init.rb', line 50 def self.run(target_root, with_agent: false) check_target!(target_root) scaffold_dir = File.("init/templates", __dir__) create_directories(target_root) 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
87 88 89 90 91 92 |
# File 'lib/textus/init.rb', line 87 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.
120 121 122 123 124 125 126 127 128 |
# File 'lib/textus/init.rb', line 120 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"), }.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
94 95 96 97 98 |
# File 'lib/textus/init.rb', line 94 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
100 101 102 |
# File 'lib/textus/init.rb', line 100 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
83 84 85 |
# File 'lib/textus/init.rb', line 83 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.
134 135 136 137 138 139 140 |
# File 'lib/textus/init.rb', line 134 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 |