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 +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +18 -5
- data/lib/textus/init/templates/machine_intake.rb +45 -0
- data/lib/textus/init.rb +41 -1
- data/lib/textus/layout.rb +17 -4
- data/lib/textus/manifest/entry/base.rb +6 -0
- data/lib/textus/manifest/schema.rb +1 -1
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +4 -0
- metadata +2 -2
- data/ARCHITECTURE.md +0 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af15d8a77f0d71c3fab21dc246545b3a7b84242361c518b1d901c491c63e55c4
|
|
4
|
+
data.tar.gz: 4644479ed6df331a6973806fed302b29446d3806d1793d8ea9d9fa661000e986
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
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"),
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
data/lib/textus/version.rb
CHANGED
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.
|
|
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