textus 0.39.0 → 0.39.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15b2e8bcfb3e83425617ee187705b88ad99c1f96671a7ad04a8394073f98065d
4
- data.tar.gz: e0711f287c8739fcc2e5f9507fd5637b9c63159b2d8e364f9d65f338138ff432
3
+ metadata.gz: af15d8a77f0d71c3fab21dc246545b3a7b84242361c518b1d901c491c63e55c4
4
+ data.tar.gz: 4644479ed6df331a6973806fed302b29446d3806d1793d8ea9d9fa661000e986
5
5
  SHA512:
6
- metadata.gz: 74ec9edba22fdd7884c7bf1a16b9f73a636524c11f889824a516775857294ea674ebd6f6fe9b0a4d98028d30cf5673b51c376760968baa4d8413c83a33475484
7
- data.tar.gz: c097ccd942039828754bd70ae2f457e172ca88166c318c72199138e126301aa4432cec318ff77c456a73de4176c0e6fe65160cca899111dcd27785adc9eff62c
6
+ metadata.gz: ee1cf4024e3583c1e59791b279a2fd0f8c00e426f05391267db0239d1a7d2909c521b94bc27caf62d549d902b0dc7f62847178f0ea17973e14b26689a981302a
7
+ data.tar.gz: 775daa6d3992b0b93d2a57c230fbfc5705b50fec592327356f4fa6ccc4dc39aaacdcbd1c24d2dd878a1b6612fd5e9c3416ede0b42d9ba90978dc8f8461c6d014
data/CHANGELOG.md CHANGED
@@ -11,6 +11,35 @@ tracks both additive improvements and breaking protocol bumps independently.
11
11
 
12
12
  ## Unreleased
13
13
 
14
+ ## 0.39.1 — 2026-06-01 — Feed ergonomics: `feeds.machine` env snapshot + intake cookbook ([ADR 0043](docs/architecture/decisions/0043-feed-ergonomics-without-breaking-core-purity.md))
15
+
16
+ No `textus/3` wire-format change. `textus init` scaffolds an additional
17
+ `nested` feed entry; core intake still makes no implicit network calls
18
+ (SPEC §5.4).
19
+
20
+ ### Added
21
+
22
+ - **`textus init` scaffolds `feeds.machines.*` with a local env snapshot
23
+ (ADR 0043).** Generated stores get a `nested` feed entry capturing ambient
24
+ machine context (git HEAD/branch/dirty state, `now`, versions) as an explicit,
25
+ user-owned snapshot — keeping ambient state out of `boot`/`pulse` (which stay
26
+ side-effect-free per ADR 0037) and out of `quarantine` (which means external
27
+ bytes pending validation, where the freshness model does not apply).
28
+
29
+ ### Documentation
30
+
31
+ - **Multi-machine environment-scan cookbook recipe** demonstrating the nested
32
+ `feeds.machines.*` pattern.
33
+ - **Examples** updated to use the `feeds.machine` env snapshot, matching
34
+ `textus init` output.
35
+ - **README flow diagram** redesigned to group writers and colour-code roles.
36
+ - **How-to fixes** for zone-rename drift in the agents-mcp guide and the
37
+ `:publish` event name.
38
+
39
+ ### Internal
40
+
41
+ - Removed the legacy `ARCHITECTURE.md` redirect stub.
42
+
14
43
  ## 0.39.0 — 2026-06-01 — Native ignore patterns for entry enumeration ([ADR 0042](docs/architecture/decisions/0042-native-ignore-patterns-for-entry-enumeration.md))
15
44
 
16
45
  No `textus/3` wire-format change. Manifest schema gains one optional, backward-compatible key (`ignore:`); existing manifests are unaffected.
data/README.md CHANGED
@@ -27,20 +27,33 @@ Three actors write to your repo today:
27
27
 
28
28
  ```mermaid
29
29
  flowchart LR
30
- human(["human"]) -->|author| knowledge["knowledge<br/>(canon)"]
31
- agent(["agent"]) -->|keep| notebook["notebook<br/>(workspace)"]
30
+ subgraph writers["writers who can write"]
31
+ direction TB
32
+ human(["human"])
33
+ agent(["agent"])
34
+ automation(["automation"])
35
+ end
36
+
37
+ human -->|author| knowledge["knowledge<br/>(canon)"]
38
+ agent -->|keep| notebook["notebook<br/>(workspace)"]
32
39
  agent -->|propose| proposals["proposals<br/>(queue)"]
33
- proposals ==>|human accept| knowledge
34
- automation(["automation"]) -->|fetch| feeds["feeds<br/>(quarantine)"]
40
+ automation -->|fetch| feeds["feeds<br/>(quarantine)"]
35
41
  automation -->|build| artifacts["artifacts<br/>(derived)"]
42
+
43
+ proposals ==>|human accept| knowledge
36
44
  feeds -.->|projection source| artifacts
37
45
  knowledge -.->|projection source| artifacts
38
46
 
39
- classDef anchor fill:#1f6feb,stroke:#1f6feb,color:#fff;
47
+ classDef actor fill:#238636,stroke:#2ea043,color:#fff;
48
+ classDef gate fill:#9e6a03,stroke:#bb8009,color:#fff;
49
+ classDef anchor fill:#1f6feb,stroke:#388bfd,color:#fff;
50
+ class human,agent,automation actor;
51
+ class proposals gate;
40
52
  class knowledge anchor;
41
53
  ```
42
54
 
43
55
  *Each actor writes only into its own lane; low-trust input climbs to authoritative lanes only by passing a guarded transition (an agent's proposal needs a human `accept`).*
56
+ *Colour legend: **green** = writers · **amber** = the review gate (`proposals`) · **blue** = the trust anchor (`knowledge`).*
44
57
 
45
58
  The point of those lanes is to **build context you can trust**. Place each lane on two axes — how durable it is, and how much you can rely on it without review — and the value shows up as a climb: the high-trust corner (durable *and* authoritative = `knowledge`) is the one place nothing is *written* directly. It's *earned* by crossing the `accept` gate.
46
59
 
@@ -0,0 +1,45 @@
1
+ # .textus/hooks/machine_intake.rb
2
+ # Scaffolded by `textus init` — CUSTOMIZE FREELY, or delete the feeds.machines
3
+ # entry from manifest.yaml if you don't want it.
4
+ # Feeds a per-host SNAPSHOT into feeds.machines.<host> on `textus fetch` (never
5
+ # on the per-turn boot/pulse path). It is NESTED so it grows to a fleet: the
6
+ # `local` leaf scans THIS host; add ssh hosts with the cookbook recipe
7
+ # (docs/cookbook/environment-scan.md). tracked:false → gitignored. Keep this an
8
+ # ALLOWLIST of versions and counts — NEVER secrets, raw `env`, or package lists.
9
+ Textus.hook do |reg|
10
+ reg.on(:resolve_intake, :machines) do |config:, args:, **|
11
+ machine = args[:leaf_segments].first or
12
+ raise "fetch a host leaf, e.g. `textus fetch feeds.machines.local`"
13
+ spec = (config["machines"] || {}).fetch(machine) { raise "unknown machine: #{machine}" }
14
+ unless (spec["via"] || "local").to_s == "local"
15
+ raise "machine #{machine}: only `via: local` is scaffolded — see " \
16
+ "docs/cookbook/environment-scan.md for the SSH (remote) fan-out"
17
+ end
18
+
19
+ sh = ->(cmd) { `#{cmd}`.strip } # local shell-out, no network
20
+ ver = ->(cmd) { o = `#{cmd} 2>/dev/null`.strip; o.empty? ? nil : o } # nil if tool absent
21
+ count = ->(cmd) { n = `#{cmd} 2>/dev/null`.strip.lines.size; n.zero? ? nil : n }
22
+ { content: {
23
+ # git_* describe THIS repo on the control host — only meaningful for `local`.
24
+ "git_head" => sh.call("git rev-parse --short HEAD 2>/dev/null"),
25
+ "git_branch" => sh.call("git rev-parse --abbrev-ref HEAD 2>/dev/null"),
26
+ "git_dirty" => !sh.call("git status --porcelain 2>/dev/null").empty?,
27
+ "repo_root" => sh.call("git rev-parse --show-toplevel 2>/dev/null"),
28
+ "captured_at" => Time.now.utc.iso8601,
29
+ "os" => RbConfig::CONFIG["host_os"],
30
+ "arch" => RbConfig::CONFIG["host_cpu"],
31
+ "ruby_version" => RUBY_VERSION,
32
+ "runtimes" => { # versions only; nil when not installed
33
+ "node" => ver.call("node --version"),
34
+ "python" => ver.call("python3 --version"),
35
+ "go" => ver.call("go version"),
36
+ },
37
+ "packages" => { # COUNTS only — never the list (size/secrets)
38
+ "brew" => count.call("brew list --formula"), # ~1-3s on macOS; runs only on fetch, amortized by the ttl rule
39
+ "apt" => count.call("dpkg-query -f '.\n' -W"),
40
+ },
41
+ "textus_version" => Textus::VERSION,
42
+ "protocol" => Textus::PROTOCOL,
43
+ } }
44
+ end
45
+ end
data/lib/textus/init.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require "fileutils"
2
+ require "pathname"
2
3
 
3
4
  module Textus
4
5
  module Init
@@ -21,6 +22,26 @@ module Textus
21
22
  - { key: knowledge.notes, path: knowledge/notes, zone: knowledge, schema: null, owner: human:self, nested: true, kind: nested }
22
23
  - { key: notebook.notes, path: notebook/notes, zone: notebook, schema: null, owner: agent:self, nested: true, kind: nested }
23
24
  - { key: proposals.notes, path: proposals/notes, zone: proposals, schema: null, owner: agent:self, nested: true, kind: nested }
25
+ # A per-host snapshot, pulled by `textus fetch feeds.machines.local --as=automation`.
26
+ # Nested so it grows to a fleet — add feeds.machines.<host> leaves over SSH
27
+ # (see docs/cookbook/environment-scan.md) without renaming. tracked:false →
28
+ # gitignored (machine info can be sensitive/noisy) but still protocol-readable
29
+ # via `textus get feeds.machines.local`. Delete to opt out. (ADR 0043)
30
+ - key: feeds.machines
31
+ path: feeds/machines
32
+ zone: feeds
33
+ format: yaml
34
+ nested: true
35
+ tracked: false
36
+ kind: intake
37
+ intake:
38
+ handler: machines
39
+ config:
40
+ machines:
41
+ local: { via: local }
42
+ rules:
43
+ - match: feeds.machines.**
44
+ fetch: { ttl: 1h, on_stale: warn } # meaningful on a long-running server
24
45
  YAML
25
46
 
26
47
  HOOKS_README = <<~MD
@@ -91,12 +112,31 @@ module Textus
91
112
  File.write(File.join(dir, ".gitkeep"), "")
92
113
  end
93
114
  File.write(File.join(target_root, "hooks", "README.md"), HOOKS_README)
115
+ scaffold_dir = File.expand_path("init/templates", __dir__)
116
+ File.write(File.join(target_root, "hooks", "machine_intake.rb"),
117
+ File.read(File.join(scaffold_dir, "machine_intake.rb")))
94
118
  File.write(File.join(target_root, "manifest.yaml"), DEFAULT_MANIFEST)
95
119
  FileUtils.mkdir_p(Textus::Layout.audit_dir(target_root))
96
120
  FileUtils.mkdir_p(Textus::Layout.state(target_root))
97
121
  FileUtils.mkdir_p(Textus::Layout.locks(target_root))
98
- File.write(File.join(target_root, ".gitignore"), Textus::Layout::GITIGNORE)
122
+ File.write(File.join(target_root, ".gitignore"), derived_gitignore(target_root))
99
123
  { "protocol" => PROTOCOL, "initialized" => target_root }
100
124
  end
125
+
126
+ # The store's `.gitignore` is generated, never hand-kept (ADR 0038), and now
127
+ # derived from the manifest: the run subtree plus every `tracked: false`
128
+ # entry's resolved path (ADR 0043).
129
+ def self.derived_gitignore(target_root)
130
+ manifest = Textus::Manifest.load(target_root)
131
+ root = Pathname.new(target_root)
132
+ untracked = manifest.data.entries.reject(&:tracked?).map do |e|
133
+ if e.nested? # a whole subtree of leaf files (feeds.machines.* → zones/feeds/machines/)
134
+ "#{File.join("zones", e.path)}/"
135
+ else
136
+ Pathname.new(Textus::Key::Path.resolve(manifest.data, e)).relative_path_from(root).to_s
137
+ end
138
+ end
139
+ Textus::Layout.gitignore_body(untracked_paths: untracked)
140
+ end
101
141
  end
102
142
  end
data/lib/textus/layout.rb CHANGED
@@ -33,9 +33,22 @@ module Textus
33
33
  File.join(audit_dir(root), "audit.log")
34
34
  end
35
35
 
36
- GITIGNORE = <<~GITIGNORE
37
- # textus runtime artifacts safe to delete, never commit
38
- #{RUN}/
39
- GITIGNORE
36
+ # The store's `.gitignore` body. Always ignores the runtime subtree
37
+ # (`.run/`, ADR 0038); when given untracked entry paths (entries marked
38
+ # `tracked: false`), it also lists those so they stay protocol-readable but
39
+ # uncommitted (ADR 0043, refining 0038). Generated, never hand-kept — no
40
+ # drift between the manifest and the ignore file.
41
+ def self.gitignore_body(untracked_paths: [])
42
+ lines = ["# textus runtime artifacts — safe to delete, never commit",
43
+ "#{RUN}/"]
44
+ unless untracked_paths.empty?
45
+ lines << "# tracked:false entries — protocol-readable, not committed (sensitive)"
46
+ lines.concat(untracked_paths)
47
+ end
48
+ "#{lines.join("\n")}\n"
49
+ end
50
+
51
+ # Back-compat constant: the no-untracked-entries body (just the run subtree).
52
+ GITIGNORE = gitignore_body
40
53
  end
41
54
  end
@@ -31,6 +31,12 @@ module Textus
31
31
  def intake? = false
32
32
  def leaf? = false
33
33
 
34
+ # Whether git should track this entry's file. Default true; an entry
35
+ # marked `tracked: false` in the manifest stays protocol-readable but is
36
+ # listed in the generated `.gitignore` (ADR 0043). Cross-cutting, so it
37
+ # reads from raw here rather than threading through every constructor.
38
+ def tracked? = @raw["tracked"] != false
39
+
34
40
  # Nil stubs for cross-cutting optional attrs. Subclasses override the
35
41
  # ones they own. Validators and serializers can call these directly
36
42
  # without `respond_to?` guards.
@@ -25,7 +25,7 @@ module Textus
25
25
  ENTRY_KEYS = %w[
26
26
  key path zone kind schema owner nested format
27
27
  compute template publish_to publish_each
28
- intake events inject_boot index_filename ignore
28
+ intake events inject_boot index_filename ignore tracked
29
29
  ].freeze
30
30
  COMPUTE_KEYS = %w[kind select pluck sort_by limit transform command sources].freeze
31
31
  INTAKE_KEYS = %w[handler config].freeze
@@ -1,4 +1,4 @@
1
1
  module Textus
2
- VERSION = "0.39.0"
2
+ VERSION = "0.39.1"
3
3
  PROTOCOL = "textus/3"
4
4
  end
data/lib/textus.rb CHANGED
@@ -17,6 +17,10 @@ loader.inflector.inflect(
17
17
  loader.ignore(File.expand_path("textus/errors.rb", __dir__))
18
18
  loader.ignore(File.expand_path("textus/mcp.rb", __dir__))
19
19
  loader.ignore(File.expand_path("textus/mcp/errors.rb", __dir__))
20
+ # Scaffold sources copied verbatim into user stores by `textus init`. They are
21
+ # file templates (one calls `Textus.hook` at load time), not gem constants —
22
+ # Zeitwerk must not manage or eager-load them (ADR 0043).
23
+ loader.ignore(File.expand_path("textus/init/templates", __dir__))
20
24
  loader.setup
21
25
  loader.eager_load
22
26
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: textus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.39.0
4
+ version: 0.39.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick
@@ -103,7 +103,6 @@ executables:
103
103
  extensions: []
104
104
  extra_rdoc_files: []
105
105
  files:
106
- - ARCHITECTURE.md
107
106
  - CHANGELOG.md
108
107
  - README.md
109
108
  - SPEC.md
@@ -234,6 +233,7 @@ files:
234
233
  - lib/textus/hooks/rpc_registry.rb
235
234
  - lib/textus/hooks/signature.rb
236
235
  - lib/textus/init.rb
236
+ - lib/textus/init/templates/machine_intake.rb
237
237
  - lib/textus/key/distance.rb
238
238
  - lib/textus/key/grammar.rb
239
239
  - lib/textus/key/path.rb
data/ARCHITECTURE.md DELETED
@@ -1,3 +0,0 @@
1
- # Architecture
2
-
3
- Moved to [`docs/architecture/README.md`](docs/architecture/README.md).