textus 0.52.0 → 0.53.0

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.
Files changed (254) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +62 -54
  4. data/SPEC.md +62 -187
  5. data/docs/architecture/README.md +88 -77
  6. data/exe/textus +1 -1
  7. data/lib/textus/action/accept.rb +53 -0
  8. data/lib/textus/action/audit.rb +133 -0
  9. data/lib/textus/action/base.rb +42 -0
  10. data/lib/textus/{read → action}/blame.rb +30 -22
  11. data/lib/textus/action/boot.rb +26 -0
  12. data/lib/textus/action/data_mv.rb +71 -0
  13. data/lib/textus/action/deps.rb +48 -0
  14. data/lib/textus/action/doctor.rb +26 -0
  15. data/lib/textus/action/drain.rb +41 -0
  16. data/lib/textus/action/enqueue.rb +55 -0
  17. data/lib/textus/action/get.rb +80 -0
  18. data/lib/textus/action/jobs.rb +38 -0
  19. data/lib/textus/action/key_delete.rb +46 -0
  20. data/lib/textus/action/key_delete_prefix.rb +46 -0
  21. data/lib/textus/action/key_mv.rb +143 -0
  22. data/lib/textus/action/key_mv_prefix.rb +59 -0
  23. data/lib/textus/action/list.rb +44 -0
  24. data/lib/textus/action/propose.rb +54 -0
  25. data/lib/textus/action/published.rb +26 -0
  26. data/lib/textus/action/pulse/scanner.rb +118 -0
  27. data/lib/textus/action/pulse.rb +87 -0
  28. data/lib/textus/action/put.rb +63 -0
  29. data/lib/textus/action/rdeps.rb +49 -0
  30. data/lib/textus/action/reject.rb +49 -0
  31. data/lib/textus/action/rule_explain.rb +95 -0
  32. data/lib/textus/action/rule_lint.rb +70 -0
  33. data/lib/textus/action/rule_list.rb +46 -0
  34. data/lib/textus/action/schema_envelope.rb +31 -0
  35. data/lib/textus/action/uid.rb +35 -0
  36. data/lib/textus/action/where.rb +38 -0
  37. data/lib/textus/action/write_verb.rb +58 -0
  38. data/lib/textus/background/job/base.rb +27 -0
  39. data/lib/textus/background/job/materialize.rb +31 -0
  40. data/lib/textus/background/job/refresh.rb +22 -0
  41. data/lib/textus/background/job/sweep.rb +31 -0
  42. data/lib/textus/background/job.rb +19 -0
  43. data/lib/textus/background/plan.rb +9 -0
  44. data/lib/textus/background/planner/plan.rb +113 -0
  45. data/lib/textus/{maintenance → background}/retention/apply.rb +7 -9
  46. data/lib/textus/background/worker.rb +67 -0
  47. data/lib/textus/boot.rb +53 -45
  48. data/lib/textus/command.rb +36 -0
  49. data/lib/textus/container.rb +1 -1
  50. data/lib/textus/{domain → core}/duration.rb +1 -1
  51. data/lib/textus/{domain → core}/freshness/evaluator.rb +11 -11
  52. data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
  53. data/lib/textus/{domain → core}/freshness.rb +2 -2
  54. data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
  55. data/lib/textus/{domain → core}/retention.rb +2 -2
  56. data/lib/textus/{domain → core}/sentinel.rb +1 -1
  57. data/lib/textus/doctor/check/generator_drift.rb +1 -1
  58. data/lib/textus/doctor/check/handler_permit.rb +34 -0
  59. data/lib/textus/doctor/check/hooks.rb +11 -18
  60. data/lib/textus/doctor/check/illegal_keys.rb +1 -1
  61. data/lib/textus/doctor/check/intake_registration.rb +5 -5
  62. data/lib/textus/doctor/check/proposal_targets.rb +3 -3
  63. data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
  64. data/lib/textus/doctor/check/schema_violations.rb +8 -2
  65. data/lib/textus/doctor/check.rb +12 -9
  66. data/lib/textus/{read → doctor}/validator.rb +22 -13
  67. data/lib/textus/doctor.rb +6 -6
  68. data/lib/textus/envelope/io/writer.rb +65 -36
  69. data/lib/textus/envelope.rb +5 -3
  70. data/lib/textus/errors.rb +17 -9
  71. data/lib/textus/events.rb +21 -0
  72. data/lib/textus/gate/auth.rb +181 -0
  73. data/lib/textus/gate.rb +114 -0
  74. data/lib/textus/init/templates/machine_intake.rb +39 -35
  75. data/lib/textus/init/templates/orientation_reducer.rb +15 -11
  76. data/lib/textus/init.rb +90 -73
  77. data/lib/textus/key/path.rb +9 -2
  78. data/lib/textus/layout.rb +13 -0
  79. data/lib/textus/manifest/data.rb +14 -14
  80. data/lib/textus/manifest/entry/base.rb +15 -11
  81. data/lib/textus/manifest/entry/parser.rb +6 -6
  82. data/lib/textus/manifest/entry/produced.rb +3 -2
  83. data/lib/textus/manifest/entry/publish/mode.rb +1 -1
  84. data/lib/textus/manifest/entry/publish/to_paths.rb +2 -1
  85. data/lib/textus/manifest/entry/validators/events.rb +1 -1
  86. data/lib/textus/{domain/policy/handler_allowlist.rb → manifest/policy/handler_permit.rb} +4 -4
  87. data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
  88. data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
  89. data/lib/textus/manifest/policy/react.rb +30 -0
  90. data/lib/textus/{domain → manifest}/policy/retention.rb +3 -3
  91. data/lib/textus/{domain → manifest}/policy/source.rb +24 -19
  92. data/lib/textus/manifest/policy.rb +36 -48
  93. data/lib/textus/manifest/resolver.rb +3 -2
  94. data/lib/textus/manifest/rules.rb +4 -4
  95. data/lib/textus/manifest/schema/keys.rb +17 -11
  96. data/lib/textus/manifest/schema/validator.rb +24 -22
  97. data/lib/textus/manifest/schema/vocabulary.rb +1 -1
  98. data/lib/textus/manifest/schema.rb +2 -2
  99. data/lib/textus/manifest.rb +2 -2
  100. data/lib/textus/{produce → pipeline}/acquire/handler.rb +2 -2
  101. data/lib/textus/{produce → pipeline}/acquire/intake.rb +22 -20
  102. data/lib/textus/{produce → pipeline}/acquire/projection.rb +13 -11
  103. data/lib/textus/{produce → pipeline}/acquire/serializer/json.rb +2 -2
  104. data/lib/textus/{produce → pipeline}/acquire/serializer/text.rb +1 -1
  105. data/lib/textus/{produce → pipeline}/acquire/serializer/yaml.rb +2 -2
  106. data/lib/textus/{produce → pipeline}/acquire/serializer.rb +1 -1
  107. data/lib/textus/{produce → pipeline}/engine.rb +7 -5
  108. data/lib/textus/{produce → pipeline}/render.rb +3 -1
  109. data/lib/textus/ports/audit_log.rb +31 -5
  110. data/lib/textus/ports/audit_subscriber.rb +4 -4
  111. data/lib/textus/{domain/jobs → ports/queue}/job.rb +19 -12
  112. data/lib/textus/ports/queue.rb +1 -1
  113. data/lib/textus/ports/sentinel_store.rb +2 -2
  114. data/lib/textus/ports/watcher_lock.rb +48 -0
  115. data/lib/textus/projection.rb +8 -8
  116. data/lib/textus/schema/tools.rb +4 -3
  117. data/lib/textus/session.rb +6 -3
  118. data/lib/textus/step/base.rb +35 -0
  119. data/lib/textus/step/builtin/csv_fetch.rb +19 -0
  120. data/lib/textus/step/builtin/ical_events_fetch.rb +30 -0
  121. data/lib/textus/step/builtin/json_fetch.rb +18 -0
  122. data/lib/textus/step/builtin/markdown_links_fetch.rb +20 -0
  123. data/lib/textus/step/builtin/rss_fetch.rb +26 -0
  124. data/lib/textus/step/builtin.rb +22 -0
  125. data/lib/textus/{hooks → step}/catalog.rb +3 -3
  126. data/lib/textus/{hooks → step}/context.rb +15 -13
  127. data/lib/textus/step/discovery.rb +24 -0
  128. data/lib/textus/{hooks → step}/error_log.rb +1 -1
  129. data/lib/textus/{hooks → step}/event_bus.rb +15 -16
  130. data/lib/textus/step/fetch.rb +13 -0
  131. data/lib/textus/{hooks → step}/fire_report.rb +1 -1
  132. data/lib/textus/step/loader.rb +108 -0
  133. data/lib/textus/step/observe.rb +31 -0
  134. data/lib/textus/step/registry_store.rb +66 -0
  135. data/lib/textus/{hooks → step}/signature.rb +1 -1
  136. data/lib/textus/step/transform.rb +12 -0
  137. data/lib/textus/step/validate.rb +11 -0
  138. data/lib/textus/step.rb +10 -0
  139. data/lib/textus/store.rb +17 -15
  140. data/lib/textus/surfaces/cli/group/data.rb +11 -0
  141. data/lib/textus/surfaces/cli/group/key.rb +11 -0
  142. data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
  143. data/lib/textus/surfaces/cli/group/rule.rb +11 -0
  144. data/lib/textus/surfaces/cli/group/schema.rb +11 -0
  145. data/lib/textus/surfaces/cli/group.rb +50 -0
  146. data/lib/textus/surfaces/cli/runner.rb +236 -0
  147. data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
  148. data/lib/textus/surfaces/cli/verb/get.rb +21 -0
  149. data/lib/textus/surfaces/cli/verb/init.rb +20 -0
  150. data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
  151. data/lib/textus/surfaces/cli/verb/put.rb +30 -0
  152. data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
  153. data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
  154. data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
  155. data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
  156. data/lib/textus/surfaces/cli/verb.rb +111 -0
  157. data/lib/textus/surfaces/cli.rb +148 -0
  158. data/lib/textus/surfaces/mcp/catalog.rb +99 -0
  159. data/lib/textus/surfaces/mcp/errors.rb +34 -0
  160. data/lib/textus/surfaces/mcp/server.rb +145 -0
  161. data/lib/textus/surfaces/mcp/session.rb +9 -0
  162. data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
  163. data/lib/textus/surfaces/mcp.rb +8 -0
  164. data/lib/textus/surfaces/role_scope.rb +38 -0
  165. data/lib/textus/surfaces/watcher.rb +38 -0
  166. data/lib/textus/version.rb +1 -1
  167. data/lib/textus.rb +64 -22
  168. metadata +132 -118
  169. data/lib/textus/cli/group/hook.rb +0 -9
  170. data/lib/textus/cli/group/key.rb +0 -9
  171. data/lib/textus/cli/group/mcp.rb +0 -9
  172. data/lib/textus/cli/group/rule.rb +0 -9
  173. data/lib/textus/cli/group/schema.rb +0 -9
  174. data/lib/textus/cli/group/zone.rb +0 -9
  175. data/lib/textus/cli/group.rb +0 -48
  176. data/lib/textus/cli/runner.rb +0 -193
  177. data/lib/textus/cli/verb/doctor.rb +0 -17
  178. data/lib/textus/cli/verb/get.rb +0 -18
  179. data/lib/textus/cli/verb/hook_run.rb +0 -48
  180. data/lib/textus/cli/verb/hooks.rb +0 -50
  181. data/lib/textus/cli/verb/init.rb +0 -18
  182. data/lib/textus/cli/verb/mcp_serve.rb +0 -22
  183. data/lib/textus/cli/verb/put.rb +0 -30
  184. data/lib/textus/cli/verb/schema_diff.rb +0 -15
  185. data/lib/textus/cli/verb/schema_init.rb +0 -19
  186. data/lib/textus/cli/verb/schema_migrate.rb +0 -19
  187. data/lib/textus/cli/verb/serve.rb +0 -19
  188. data/lib/textus/cli/verb.rb +0 -116
  189. data/lib/textus/cli.rb +0 -138
  190. data/lib/textus/dispatcher.rb +0 -54
  191. data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
  192. data/lib/textus/domain/action.rb +0 -9
  193. data/lib/textus/domain/jobs/registry.rb +0 -37
  194. data/lib/textus/domain/permission.rb +0 -7
  195. data/lib/textus/domain/policy/base_guards.rb +0 -25
  196. data/lib/textus/domain/policy/evaluation.rb +0 -15
  197. data/lib/textus/domain/policy/guard.rb +0 -35
  198. data/lib/textus/domain/policy/guard_factory.rb +0 -40
  199. data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
  200. data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
  201. data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
  202. data/lib/textus/domain/policy/predicates/registry.rb +0 -39
  203. data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
  204. data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
  205. data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
  206. data/lib/textus/hooks/builtin.rb +0 -70
  207. data/lib/textus/hooks/loader.rb +0 -54
  208. data/lib/textus/hooks/rpc_registry.rb +0 -43
  209. data/lib/textus/jobs/handlers.rb +0 -62
  210. data/lib/textus/jobs/scheduler.rb +0 -36
  211. data/lib/textus/jobs/seeder.rb +0 -57
  212. data/lib/textus/maintenance/drain.rb +0 -42
  213. data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
  214. data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
  215. data/lib/textus/maintenance/rule_lint.rb +0 -66
  216. data/lib/textus/maintenance/serve.rb +0 -30
  217. data/lib/textus/maintenance/worker.rb +0 -74
  218. data/lib/textus/maintenance/zone_mv.rb +0 -64
  219. data/lib/textus/maintenance.rb +0 -15
  220. data/lib/textus/mcp/catalog.rb +0 -70
  221. data/lib/textus/mcp/errors.rb +0 -32
  222. data/lib/textus/mcp/server.rb +0 -138
  223. data/lib/textus/mcp/session.rb +0 -7
  224. data/lib/textus/mcp/tool_schemas.rb +0 -15
  225. data/lib/textus/mcp.rb +0 -6
  226. data/lib/textus/mustache.rb +0 -117
  227. data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
  228. data/lib/textus/produce/events.rb +0 -36
  229. data/lib/textus/read/audit.rb +0 -130
  230. data/lib/textus/read/boot.rb +0 -26
  231. data/lib/textus/read/capabilities.rb +0 -70
  232. data/lib/textus/read/deps.rb +0 -38
  233. data/lib/textus/read/doctor.rb +0 -27
  234. data/lib/textus/read/freshness.rb +0 -152
  235. data/lib/textus/read/get.rb +0 -73
  236. data/lib/textus/read/jobs.rb +0 -31
  237. data/lib/textus/read/list.rb +0 -24
  238. data/lib/textus/read/published.rb +0 -22
  239. data/lib/textus/read/pulse.rb +0 -98
  240. data/lib/textus/read/rdeps.rb +0 -39
  241. data/lib/textus/read/rule_explain.rb +0 -96
  242. data/lib/textus/read/rule_list.rb +0 -54
  243. data/lib/textus/read/schema_envelope.rb +0 -25
  244. data/lib/textus/read/uid.rb +0 -29
  245. data/lib/textus/read/validate_all.rb +0 -36
  246. data/lib/textus/read/where.rb +0 -24
  247. data/lib/textus/role_scope.rb +0 -78
  248. data/lib/textus/write/accept.rb +0 -58
  249. data/lib/textus/write/enqueue.rb +0 -50
  250. data/lib/textus/write/key_delete.rb +0 -65
  251. data/lib/textus/write/key_mv.rb +0 -141
  252. data/lib/textus/write/propose.rb +0 -54
  253. data/lib/textus/write/put.rb +0 -74
  254. data/lib/textus/write/reject.rb +0 -68
@@ -1,193 +0,0 @@
1
- module Textus
2
- class CLI
3
- # Generates CLI::Verb (and CLI::Group) subclasses from per-verb contracts,
4
- # so the CLI surface is a projection of the contract — the operator-facing
5
- # mirror of MCP::Catalog (ADR 0063).
6
- module Runner
7
- # Subclassable base for contract-projected verbs. Carries the verb's
8
- # contract (class attr `spec`) and the generic dispatch, exposing one
9
- # overridable seam, #invoke, that defaults to the generic projection.
10
- # Escape-hatch verbs subclass this and override #invoke to add behavior
11
- # (suggestions, --stdin, BuildLock, multi-dispatch) WITHOUT restating the
12
- # verb name — `spec.verb` remains the single source of dispatch.
13
- class Base < Verb
14
- class << self
15
- attr_accessor :spec
16
-
17
- # ADR 0064: derive the CLI command name from the contract's cli_leaf
18
- # when not set explicitly, so an escape-hatch class never restates its
19
- # own name. The reconciliation spec proves command_name == cli_leaf for
20
- # every such class, so this is an equivalence, not a behavior change.
21
- def command_name(name = nil)
22
- return super if name
23
-
24
- super() || spec&.cli_leaf
25
- end
26
- end
27
-
28
- def spec = self.class.spec
29
-
30
- def call(store)
31
- invoke(store)
32
- end
33
-
34
- # Default: pure contract projection. Override in subclasses for behavior.
35
- def invoke(store)
36
- Runner.dispatch(self, store, spec)
37
- end
38
-
39
- def flag_values(s = spec)
40
- s.args.reject(&:positional).each_with_object({}) do |a, h|
41
- raw = respond_to?(a.name) ? public_send(a.name) : nil
42
- next if raw.nil?
43
-
44
- h[a.name] = Runner.coerce(a, raw)
45
- end
46
- end
47
- end
48
-
49
- module_function
50
-
51
- # Normalize parsed CLI input into the uniform by-name inputs hash and
52
- # dispatch through RoleScope's single bind+invoke site. A missing required
53
- # arg becomes a UsageError phrased in the operator's command path (parity
54
- # with the hand-written verbs).
55
- def dispatch(verb_instance, store, spec)
56
- inputs = Textus::Contract::Binder.inputs_from_ordered(
57
- spec, verb_instance.positional, verb_instance.flag_values(spec)
58
- )
59
- inputs = inputs.merge(Textus::Contract::Sources.from_stdin(spec, verb_instance.stdin)) if spec.cli_stdin
60
- inputs = Textus::Contract::Sources.acquire(spec, inputs)
61
- inputs = apply_cli_defaults(spec, inputs)
62
- scope = verb_instance.session_for(store)
63
- begin
64
- result = scope.dispatch_bound(spec.verb, inputs)
65
- rescue Textus::Contract::MissingArgs => e
66
- raise UsageError.new("#{spec.cli_path} requires #{e.missing.first.wire}")
67
- end
68
- verb_instance.emit(shape(spec, result, inputs))
69
- end
70
-
71
- # Fill CLI-specific defaults (cli_default:) for args the operator did not
72
- # pass, where the CLI default diverges from the contract default the agent
73
- # surfaces use — e.g. migrate/zone_mv apply by default on the CLI but plan
74
- # by default for agents (ADR 0068). The divergence is legible in the
75
- # contract, not hidden in a hand class.
76
- def apply_cli_defaults(spec, inputs)
77
- spec.args.each_with_object(inputs.dup) do |a, h|
78
- next if a.cli_default == :__unset || h.key?(a.name)
79
-
80
- h[a.name] = a.cli_default
81
- end
82
- end
83
-
84
- # Shape the use-case result for the CLI wire via the verb's :cli view
85
- # (falling back to the default view). The view is called uniformly as
86
- # (result, inputs); an inputs-aware view echoes an input such as the key
87
- # (ADR 0067).
88
- def shape(spec, result, inputs)
89
- Textus::Contract::View.render(spec, :cli, result, inputs)
90
- end
91
-
92
- # The default the CLI flag is generated against — `cli_default:` when the
93
- # operator-facing default diverges from the contract default the agent
94
- # surfaces use, else the contract `default`. This drives boolean flag
95
- # polarity so a verb that applies-by-default on the CLI but plans-by-default
96
- # for agents (migrate, zone_mv) gets a `--dry-run` flag, not `--no-dry-run`.
97
- def effective_default(arg)
98
- arg.cli_default == :__unset ? arg.default : arg.cli_default
99
- end
100
-
101
- def flagspec_for(arg)
102
- wire = arg.wire.to_s.tr("_", "-")
103
- if arg.type == :boolean
104
- effective_default(arg) == true ? "--no-#{wire}" : "--#{wire}"
105
- else
106
- "--#{wire}=VALUE"
107
- end
108
- end
109
-
110
- # NB: compare arg.type by equality, not `case`/`===` — `Integer === arg.type`
111
- # is false when arg.type is the Integer *class* (it tests instance-of), so a
112
- # `when Integer` branch would silently never coerce.
113
- def coerce(arg, raw)
114
- return effective_default(arg) != true if arg.type == :boolean
115
- return Integer(raw) if arg.type == Integer
116
-
117
- raw
118
- end
119
-
120
- def ensure_group(name)
121
- const = name.split("_").map(&:capitalize).join
122
- return Group.const_get(const, false) if Group.const_defined?(const, false)
123
-
124
- g = Class.new(Group) { command_name name }
125
- Group.const_set(const, g)
126
- g
127
- end
128
-
129
- # Contract verbs whose CLI behavior is a genuine `< Runner::Base` override
130
- # — behavior the generic projection cannot express (ADR 0068/0069):
131
- # get — raises UnknownKey with resolver suggestions (a CLI-only
132
- # affordance; the agent surface deliberately returns nil)
133
- # put — reads the entry JSON from --stdin (ADR 0089: just stores bytes,
134
- # no --fetch transform)
135
- # (build removed in ADR 0087: materialization is system-pushed via drain/serve)
136
- BEHAVIORAL_HATCHES = %i[get put].freeze
137
-
138
- # Contract verbs whose CLI is a plain `< Verb` command, not a projection at
139
- # all — composite reports assembled outside the contract.
140
- # (boot removed: its contract carries surfaces :cli + the :lean arg, so the
141
- # generic projection now generates it; the hand-authored CLI::Verb::Boot is
142
- # deleted in ADR 0101.)
143
- # (doctor retained: hand-authored to preserve --check=NAME flag spelling and
144
- # the exit_code: res["ok"] ? 0 : 1 behavior — two things the generic
145
- # projection cannot yet express; kept in ADR 0101 pending a future pass.)
146
- # (fetch/fetch_all were removed in ADR 0079: Produce::Acquire::Intake is now internal,
147
- # driven by the converge sweep (drain/serve) and hook run — ADR 0089 removed the
148
- # read-through that once also drove it.)
149
- NON_PROJECTED_CLI = %i[doctor].freeze
150
-
151
- # The installer skips generation for either category.
152
- HAND_AUTHORED_VERBS = (BEHAVIORAL_HATCHES + NON_PROJECTED_CLI).freeze
153
-
154
- def hand_authored?(verb) = HAND_AUTHORED_VERBS.include?(verb)
155
-
156
- def install!
157
- @installed ||= {}
158
- Textus::Dispatcher::VERBS.each_value do |klass|
159
- next unless klass.respond_to?(:contract?) && klass.contract?
160
-
161
- spec = klass.contract
162
- next unless spec.cli?
163
- next if hand_authored?(spec.verb)
164
- next if @installed[spec.verb]
165
-
166
- install_for(spec)
167
- @installed[spec.verb] = true
168
- end
169
- end
170
-
171
- def install_for(spec)
172
- group = spec.cli_group ? ensure_group(spec.cli_group) : nil
173
- leaf = spec.cli_leaf
174
- non_positional = spec.args.reject(&:positional)
175
-
176
- klass = Class.new(Base)
177
- klass.spec = spec
178
- klass.command_name leaf
179
- klass.parent_group group if group
180
- klass.option :as_flag, "--as=ROLE"
181
- klass.option :use_stdin, "--stdin" if spec.cli_stdin
182
- non_positional.each { |a| klass.option a.name, Runner.flagspec_for(a) }
183
-
184
- # Anchor the anonymous class to a constant so descendants discovery is
185
- # stable. Name it after the verb under a Generated namespace.
186
- const_name = spec.verb.to_s.split("_").map(&:capitalize).join
187
- gen = "Gen#{const_name}"
188
- Verb.const_set(gen, klass) unless Verb.const_defined?(gen, false)
189
- klass
190
- end
191
- end
192
- end
193
- end
@@ -1,17 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Doctor < Verb
5
- command_name "doctor"
6
-
7
- option :checks, "--check=NAME"
8
-
9
- def call(store)
10
- check_list = checks&.split(",")&.map(&:strip)
11
- res = store.doctor(checks: check_list)
12
- emit(res, exit_code: res["ok"] ? 0 : 1)
13
- end
14
- end
15
- end
16
- end
17
- end
@@ -1,18 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Get < Runner::Base
5
- self.spec = Textus::Read::Get.contract
6
- option :as_flag, "--as=ROLE"
7
-
8
- def invoke(store)
9
- key = positional.shift or raise UsageError.new("get requires a key")
10
- result = session_for(store).get(key)
11
- raise Textus::UnknownKey.new(key, suggestions: store.manifest.resolver.suggestions_for(key)) if result.nil?
12
-
13
- emit(result.to_h_for_wire)
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,48 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class HookRun < Verb
5
- command_name "run"
6
- parent_group Group::Hook
7
-
8
- def parse(argv)
9
- @raw_argv = argv
10
- end
11
-
12
- def call(store)
13
- name = @raw_argv.shift
14
- raise UsageError.new("hook run requires a name") if name.nil?
15
-
16
- as_flag = nil
17
- args = {}
18
- @raw_argv.each do |tok|
19
- case tok
20
- when /\A--as=(.+)\z/ then as_flag = ::Regexp.last_match(1)
21
- when /\A--output=/ then next
22
- when /\A--format=/ then raise FlagRenamed.new("--format", "--output")
23
- when /\A--([\w-]+)=(.*)\z/ then args[::Regexp.last_match(1)] = ::Regexp.last_match(2)
24
- else
25
- raise UsageError.new("unknown arg to 'hook run #{name}': #{tok}")
26
- end
27
- end
28
-
29
- # Validate --as resolves to a declared role (raises InvalidRole); hook
30
- # run has no role-scoped authority itself, so the result is discarded.
31
- Role.resolve(flag: as_flag, env: ENV, root: store.root)
32
-
33
- begin
34
- Textus::Produce::Acquire::Handler.invoke(
35
- caps: store.container, handler: name, config: {}, args: args, label: "hook run",
36
- )
37
- rescue Textus::Error
38
- raise
39
- rescue StandardError => e
40
- raise UsageError.new("hook run '#{name}' raised: #{e.class}: #{e.message}")
41
- end
42
-
43
- emit({ "action" => name, "ok" => true })
44
- end
45
- end
46
- end
47
- end
48
- end
@@ -1,50 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Hooks < Verb
5
- command_name "list"
6
- parent_group Group::Hook
7
-
8
- option :event_filter, "--event=E"
9
-
10
- def call(store) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
11
- subcommand = positional.first
12
- if subcommand
13
- raise UsageError.new("hook requires 'list'") unless subcommand == "list"
14
-
15
- positional.shift
16
- end
17
-
18
- rows = []
19
- Textus::Hooks::Catalog::RPC.each_key do |event|
20
- store.rpc.names(event).each do |name|
21
- rows << { "event" => event.to_s, "mode" => "rpc", "name" => name.to_s }
22
- end
23
- end
24
- Textus::Hooks::Catalog::PUBSUB.each_key do |event|
25
- store.events.pubsub_handlers(event).each do |h|
26
- row = { "event" => event.to_s, "mode" => "pubsub", "name" => h[:name].to_s }
27
- row["keys"] = Array(h[:keys]) if h[:keys]
28
- rows << row
29
- end
30
- end
31
- store.manifest.data.entries.each do |e|
32
- (e.respond_to?(:events) ? e.events : {}).each do |evt, defs|
33
- Array(defs).each do |defn|
34
- next unless defn["exec"]
35
-
36
- rows << {
37
- "event" => evt.to_s, "mode" => "manifest", "exec" => defn["exec"],
38
- "key" => e.key, "as" => defn["as"] || Textus::Role::AUTOMATION
39
- }
40
- end
41
- end
42
- end
43
- rows.select! { |r| r["event"] == event_filter } if event_filter
44
-
45
- emit({ "hooks" => rows })
46
- end
47
- end
48
- end
49
- end
50
- end
@@ -1,18 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Init < Verb
5
- command_name "init"
6
-
7
- option :with_agent, "--with-agent"
8
-
9
- def self.needs_store? = false
10
-
11
- def call(_store)
12
- target = File.join(@cwd, ".textus")
13
- emit(Textus::Init.run(target, with_agent: !!with_agent))
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,22 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- # Launches the MCP stdio server in the current process. Blocks on stdin;
5
- # never returns until stdin closes. The connection acts as the `agent`
6
- # role by default (ADR 0040): the agent channel proposes, it does not
7
- # inherit the human's authority. Override per connection with --as, or
8
- # TEXTUS_ROLE / .textus/role (same chain as every other verb).
9
- class MCPServe < Verb
10
- command_name "serve"
11
- parent_group Group::MCP
12
- option :as_flag, "--as=ROLE"
13
-
14
- def call(store)
15
- role = resolved_role(store, default: Textus::Role::AGENT)
16
- Textus::MCP::Server.new(store: store, stdin: @stdin, stdout: @stdout, role: role).run
17
- 0
18
- end
19
- end
20
- end
21
- end
22
- end
@@ -1,30 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Put < Runner::Base
5
- self.spec = Textus::Write::Put.contract
6
-
7
- option :as_flag, "--as=ROLE"
8
- option :use_stdin, "--stdin"
9
-
10
- def invoke(store)
11
- key = positional.shift or raise UsageError.new("put requires a key")
12
- raise UsageError.new("put requires --stdin in v1") unless use_stdin
13
-
14
- role = resolved_role(store)
15
-
16
- # put only stores the stdin JSON (ADR 0089): no transform-on-write.
17
- # Ingest (running a handler over bytes) is system-pushed via drain/serve
18
- # and hook run, never a put flag.
19
- payload = JSON.parse(@stdin.read)
20
-
21
- meta = payload["_meta"] || {}
22
- body = payload["body"] || ""
23
- if_etag = payload["if_etag"]
24
- result = store.as(role).put(key, meta: meta, body: body, if_etag: if_etag)
25
- emit(result.to_h_for_wire)
26
- end
27
- end
28
- end
29
- end
30
- end
@@ -1,15 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class SchemaDiff < Verb
5
- command_name "diff"
6
- parent_group Group::Schema
7
-
8
- def call(store)
9
- name = positional.shift or raise UsageError.new("schema diff NAME")
10
- emit(Textus::Schema::Tools.diff(store, name: name))
11
- end
12
- end
13
- end
14
- end
15
- end
@@ -1,19 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class SchemaInit < Verb
5
- command_name "init"
6
- parent_group Group::Schema
7
-
8
- option :from_key, "--from=KEY"
9
-
10
- def call(store)
11
- name = positional.shift or raise UsageError.new("schema init NAME")
12
- raise UsageError.new("schema init requires --from=KEY") unless from_key
13
-
14
- emit(Textus::Schema::Tools.init(store, name: name, from: from_key))
15
- end
16
- end
17
- end
18
- end
19
- end
@@ -1,19 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class SchemaMigrate < Verb
5
- command_name "migrate"
6
- parent_group Group::Schema
7
-
8
- option :rename, "--rename=O:N"
9
-
10
- def call(store)
11
- name = positional.shift or raise UsageError.new("schema migrate NAME")
12
- raise UsageError.new("schema migrate requires --rename=OLD:NEW") unless rename
13
-
14
- emit(Textus::Schema::Tools.migrate(store, name: name, rename: rename))
15
- end
16
- end
17
- end
18
- end
19
- end
@@ -1,19 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- # Launches the convergence daemon in the current process. Blocks forever;
5
- # reclaims crashed leases and drains the queue each tick (Phase 3 adds
6
- # scheduled TTL re-pull/sweep). CLI-only — agents enqueue work, they do not
7
- # run daemons. Acts as the automation role (the build authority).
8
- class Serve < Verb
9
- command_name "serve"
10
-
11
- def call(store)
12
- call = Textus::Call.build(role: Textus::Role::AUTOMATION)
13
- Textus::Maintenance::Serve.new(container: store.container, call: call).run
14
- 0
15
- end
16
- end
17
- end
18
- end
19
- end
@@ -1,116 +0,0 @@
1
- require "json"
2
- require "optparse"
3
-
4
- module Textus
5
- class CLI
6
- # Subclasses must implement #call(store) and return an integer exit code.
7
- # Use #emit(obj) for normal JSON output (returns 0).
8
- # Subclasses that don't need a Textus store (e.g. Init) override
9
- # `.needs_store?` to return false; dispatch will pass nil instead.
10
- class Verb
11
- class << self
12
- def option(name, optspec)
13
- options << [name, optspec]
14
- attr_accessor(name)
15
- end
16
-
17
- def options
18
- @options ||= []
19
- end
20
-
21
- def needs_store?
22
- true
23
- end
24
-
25
- # Declarative CLI name. Reader returns the registered name (or nil
26
- # for verbs that aren't directly invokable, like the abstract
27
- # Verb/Group base classes). Writer registers it.
28
- def command_name(name = nil)
29
- if name.nil?
30
- @command_name
31
- else
32
- @command_name = name.to_s
33
- end
34
- end
35
-
36
- # Declares that this verb is a subcommand of `group_klass`. When
37
- # set, the verb is NOT a top-level CLI verb — it's listed under
38
- # the group's subcommands instead.
39
- def parent_group(group_klass = nil)
40
- if group_klass.nil?
41
- @parent_group
42
- else
43
- @parent_group = group_klass
44
- end
45
- end
46
-
47
- def inherited(subclass)
48
- super
49
- subclass.instance_variable_set(:@options, [])
50
- subclass.instance_variable_set(:@command_name, nil)
51
- subclass.instance_variable_set(:@parent_group, nil)
52
- end
53
-
54
- # Recursive subclass enumeration. Ruby 3.1 ships Class#subclasses
55
- # but not Class#descendants, so we expand it ourselves.
56
- def descendants
57
- subclasses.flat_map { |k| [k] + k.descendants }
58
- end
59
- end
60
-
61
- def initialize(stdin:, stdout:, stderr:, cwd: nil)
62
- @stdin = stdin
63
- @stdout = stdout
64
- @stderr = stderr
65
- @cwd = cwd
66
- end
67
-
68
- def parse(argv)
69
- fmt = "json"
70
- OptionParser.new do |o|
71
- self.class.options.each do |name, optspec|
72
- o.on(optspec) { |v| public_send(:"#{name}=", v) }
73
- end
74
- o.on("--output=FMT") { |v| fmt = v }
75
- o.on("--format=FMT") { |_v| raise FlagRenamed.new("--format", "--output") }
76
- end.permute!(argv)
77
- raise UsageError.new("only --output=json is supported in v1") unless fmt == "json"
78
-
79
- @positional = argv.dup
80
- end
81
-
82
- attr_reader :positional
83
-
84
- # Hashes get "protocol" => PROTOCOL prepended unless they already
85
- # carry one (Store envelopes do). Caller's value wins on collision.
86
- def emit(obj, exit_code: 0)
87
- payload = obj.is_a?(Hash) ? { "protocol" => PROTOCOL }.merge(obj) : obj
88
- @stdout.puts(JSON.generate(payload))
89
- exit_code
90
- end
91
-
92
- # Resolves the active role for this invocation. Honors the verb's
93
- # `--as` flag if declared, then TEXTUS_ROLE, then the project default.
94
- # Pass `default:` to override the fallback (e.g. MCPServe uses AGENT).
95
- def resolved_role(store, default: Role::DEFAULT)
96
- flag = respond_to?(:as_flag) ? as_flag : nil
97
- Role.resolve(flag: flag, env: ENV, root: store.root, default: default)
98
- end
99
-
100
- # Returns a Call value bound to the resolved role. Convenience for
101
- # verbs whose only pre-call boilerplate is resolving the role and
102
- # wrapping it in a Call.
103
- def context_for(store)
104
- Textus::Call.build(role: resolved_role(store))
105
- end
106
-
107
- # Returns a RoleScope bound to the resolved role.
108
- def session_for(store)
109
- store.as(resolved_role(store))
110
- end
111
-
112
- # The input stream — the source for a `cli_stdin` envelope (ADR 0068).
113
- attr_reader :stdin
114
- end
115
- end
116
- end