techtree 0.1.0__py3-none-any.whl

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 (174) hide show
  1. techtree/__init__.py +35 -0
  2. techtree/__main__.py +14 -0
  3. techtree/canonical.py +239 -0
  4. techtree/catalog/__init__.py +25 -0
  5. techtree/catalog/repository.py +400 -0
  6. techtree/catalog/service.py +419 -0
  7. techtree/cli/__init__.py +1 -0
  8. techtree/cli/app.py +416 -0
  9. techtree/cli/commands/__init__.py +1 -0
  10. techtree/cli/commands/climb.py +1223 -0
  11. techtree/cli/commands/doctor.py +147 -0
  12. techtree/cli/commands/engine.py +207 -0
  13. techtree/cli/commands/proof.py +556 -0
  14. techtree/cli/commands/publish.py +447 -0
  15. techtree/cli/commands/release.py +303 -0
  16. techtree/cli/commands/run.py +1067 -0
  17. techtree/cli/commands/setup.py +181 -0
  18. techtree/cli/commands/skill.py +221 -0
  19. techtree/cli/commands/uplift.py +698 -0
  20. techtree/cli/commands/withdraw.py +212 -0
  21. techtree/cli/confirm.py +47 -0
  22. techtree/cli/context.py +96 -0
  23. techtree/cli/invoke.py +220 -0
  24. techtree/cli/output.py +280 -0
  25. techtree/constants.py +138 -0
  26. techtree/crypto.py +128 -0
  27. techtree/doctor/__init__.py +1 -0
  28. techtree/doctor/checks.py +675 -0
  29. techtree/doctor/execution_checks.py +435 -0
  30. techtree/doctor/service.py +326 -0
  31. techtree/drafts/__init__.py +32 -0
  32. techtree/drafts/source.py +146 -0
  33. techtree/drafts/store.py +992 -0
  34. techtree/engines/__init__.py +1 -0
  35. techtree/engines/bundle.py +251 -0
  36. techtree/engines/installer.py +679 -0
  37. techtree/engines/registry.py +235 -0
  38. techtree/engines/runner.py +170 -0
  39. techtree/errors.py +262 -0
  40. techtree/fs.py +234 -0
  41. techtree/harness.py +108 -0
  42. techtree/identity/__init__.py +41 -0
  43. techtree/identity/models.py +113 -0
  44. techtree/identity/service.py +199 -0
  45. techtree/identity/store.py +263 -0
  46. techtree/ids.py +85 -0
  47. techtree/manifests/__init__.py +39 -0
  48. techtree/manifests/builder.py +433 -0
  49. techtree/manifests/compare.py +376 -0
  50. techtree/models/__init__.py +282 -0
  51. techtree/models/base.py +201 -0
  52. techtree/models/campaign.py +484 -0
  53. techtree/models/catalog.py +227 -0
  54. techtree/models/cli.py +151 -0
  55. techtree/models/climb.py +254 -0
  56. techtree/models/data_policy.py +130 -0
  57. techtree/models/engine.py +156 -0
  58. techtree/models/episode_receipt.py +130 -0
  59. techtree/models/evaluation_backend.py +113 -0
  60. techtree/models/experiment.py +154 -0
  61. techtree/models/run.py +214 -0
  62. techtree/models/skill.py +156 -0
  63. techtree/models/uplift_report.py +158 -0
  64. techtree/models/validation.py +299 -0
  65. techtree/paths.py +116 -0
  66. techtree/presentation/__init__.py +31 -0
  67. techtree/presentation/build.py +1242 -0
  68. techtree/presentation/compact.py +246 -0
  69. techtree/presentation/evidence.py +169 -0
  70. techtree/presentation/models.py +358 -0
  71. techtree/presentation/rich.py +312 -0
  72. techtree/presentation/sanitize.py +156 -0
  73. techtree/publication/__init__.py +44 -0
  74. techtree/publication/address.py +180 -0
  75. techtree/publication/coordinates.py +26 -0
  76. techtree/publication/journal.py +212 -0
  77. techtree/publication/keccak.py +183 -0
  78. techtree/publication/models.py +209 -0
  79. techtree/publication/offer.py +35 -0
  80. techtree/publication/service.py +618 -0
  81. techtree/publication/transport.py +296 -0
  82. techtree/publication/verify.py +242 -0
  83. techtree/publication/withdraw.py +156 -0
  84. techtree/py.typed +0 -0
  85. techtree/receipts/__init__.py +52 -0
  86. techtree/receipts/bundle.py +578 -0
  87. techtree/receipts/compare.py +1065 -0
  88. techtree/receipts/episode.py +672 -0
  89. techtree/receipts/execution.py +630 -0
  90. techtree/receipts/observed.py +474 -0
  91. techtree/receipts/set.py +336 -0
  92. techtree/receipts/uplift.py +655 -0
  93. techtree/receipts/verify.py +1055 -0
  94. techtree/release/__init__.py +9 -0
  95. techtree/release/bootstrap.py +509 -0
  96. techtree/release/checks.py +376 -0
  97. techtree/release/document.py +125 -0
  98. techtree/release/generate.py +221 -0
  99. techtree/release/models.py +293 -0
  100. techtree/release/provenance.py +109 -0
  101. techtree/resources/catalog/campaigns/hello-world-climb.json +1 -0
  102. techtree/resources/catalog/catalog.json +32 -0
  103. techtree/resources/catalog/climbs/hello-world-climb.json +1 -0
  104. techtree/resources/catalog/data-policies/hello-world-climb.json +1 -0
  105. techtree/resources/catalog/taskset-validations/hello-world-climb.json +1 -0
  106. techtree/resources/catalog/validation-evidence/hello-world-climb.json +1 -0
  107. techtree/resources/engines/default/engine.json +20 -0
  108. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/__init__.py +7 -0
  109. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/algorithm.py +136 -0
  110. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/dataset.py +156 -0
  111. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/env.py +48 -0
  112. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/taskset.py +163 -0
  113. techtree/resources/engines/default/packages/procedure-transfer-v1/pyproject.toml +13 -0
  114. techtree/resources/engines/default/pyproject.toml +23 -0
  115. techtree/resources/engines/default/tools/inspect_taskset.py +124 -0
  116. techtree/resources/engines/default/tools/normalize_eval_output.py +470 -0
  117. techtree/resources/engines/default/tools/normalize_validation.py +222 -0
  118. techtree/resources/engines/default/uv.lock +1758 -0
  119. techtree/resources/harness/hermes-agent-0.19.0.json +69 -0
  120. techtree/resources/release/build-provenance.json +4 -0
  121. techtree/resources/release/release-core.json +24 -0
  122. techtree/runs/__init__.py +31 -0
  123. techtree/runs/artifacts.py +750 -0
  124. techtree/runs/child_registry.py +228 -0
  125. techtree/runs/events.py +478 -0
  126. techtree/runs/executor.py +140 -0
  127. techtree/runs/fake.py +741 -0
  128. techtree/runs/launcher.py +253 -0
  129. techtree/runs/machine.py +489 -0
  130. techtree/runs/real.py +789 -0
  131. techtree/runs/service.py +616 -0
  132. techtree/runs/store.py +555 -0
  133. techtree/runs/validation.py +259 -0
  134. techtree/runs/variants.py +684 -0
  135. techtree/settings.py +143 -0
  136. techtree/skills/__init__.py +14 -0
  137. techtree/skills/archive.py +282 -0
  138. techtree/skills/policy.py +62 -0
  139. techtree/skills/scanner.py +394 -0
  140. techtree/skills/service.py +752 -0
  141. techtree/skills/starter.py +434 -0
  142. techtree/tasksets/__init__.py +1 -0
  143. techtree/tasksets/membership.py +269 -0
  144. techtree/tasksets/provider.py +207 -0
  145. techtree/tasksets/resolver.py +311 -0
  146. techtree/tasksets/service.py +484 -0
  147. techtree/tasksets/verifiers_cli.py +538 -0
  148. techtree/uplift/__init__.py +20 -0
  149. techtree/uplift/context.py +544 -0
  150. techtree/uplift/derive.py +203 -0
  151. techtree/uplift/public_tasks.py +151 -0
  152. techtree/uplift/service.py +719 -0
  153. techtree/uplift/source.py +160 -0
  154. techtree/verifiers/__init__.py +31 -0
  155. techtree/verifiers/budget.py +219 -0
  156. techtree/verifiers/child.py +633 -0
  157. techtree/verifiers/compiler.py +432 -0
  158. techtree/verifiers/config.py +365 -0
  159. techtree/verifiers/credentials.py +321 -0
  160. techtree/verifiers/image.py +126 -0
  161. techtree/verifiers/models.py +527 -0
  162. techtree/verifiers/outputs.py +368 -0
  163. techtree/verifiers/progress.py +192 -0
  164. techtree/verifiers/supervisor.py +341 -0
  165. techtree/verifiers/verify.py +782 -0
  166. techtree/version.py +39 -0
  167. techtree/worker/__init__.py +18 -0
  168. techtree/worker/execute.py +487 -0
  169. techtree/worker/main.py +57 -0
  170. techtree-0.1.0.dist-info/METADATA +344 -0
  171. techtree-0.1.0.dist-info/RECORD +174 -0
  172. techtree-0.1.0.dist-info/WHEEL +4 -0
  173. techtree-0.1.0.dist-info/entry_points.txt +3 -0
  174. techtree-0.1.0.dist-info/licenses/LICENSE +21 -0
techtree/cli/app.py ADDED
@@ -0,0 +1,416 @@
1
+ """The Typer application. Spec section 12.4.
2
+
3
+ This module wires and nothing else. It declares the global options, builds the
4
+ :class:`~techtree.cli.context.CliContext` once in the root callback, registers
5
+ the command groups, and closes the last gap in the error boundary: a typed
6
+ failure raised while the context is still being built still owes the caller one
7
+ envelope and one documented exit code.
8
+
9
+ ``climb``, ``skill``, ``run``, ``engine``, ``proof``, ``release`` and
10
+ ``uplift`` are registered with their real names even where this build
11
+ implements only some of what each will eventually hold. ``publish`` and
12
+ ``withdraw`` are commands rather than groups, because each is one thing a
13
+ person does and neither has a second member to hold. A name
14
+ that exists and answers ``not_implemented`` is discoverable and scriptable; a
15
+ name that does not exist yet is indistinguishable from a typo. The reserved
16
+ namespaces — ``program``, ``blueprint``, ``forge``, ``verify``, ``trace``,
17
+ ``lab`` — get no group at all, because an empty group in ``--help`` is a
18
+ promise about a shape that has not been decided. ``uplift`` left that list when
19
+ spec section 7.21's improvement commands landed and it stopped being a shape
20
+ nobody had decided.
21
+
22
+ Global options are accepted anywhere on the command line. ``techtree --json
23
+ doctor`` and ``techtree doctor --json`` mean the same thing, because a caller
24
+ scripting the boundary should not have to know that one of those flags belongs
25
+ to a group and the other to a command.
26
+
27
+ Argument parsing happens before any of this. A malformed command line is
28
+ reported by the parser with exit code 2, and a bare ``techtree`` prints help;
29
+ neither reaches a command, and the one-envelope rule covers every invocation
30
+ that does.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import os
36
+ import sys
37
+ from collections.abc import Mapping, Sequence
38
+ from pathlib import Path
39
+ from typing import Annotated, Any, NoReturn
40
+
41
+ import typer
42
+ from typer.core import TyperGroup
43
+
44
+ from techtree.cli.commands.climb import (
45
+ list_climbs_command,
46
+ prepare_climb_command,
47
+ show_climb_command,
48
+ start_climb_command,
49
+ )
50
+ from techtree.cli.commands.doctor import doctor_command
51
+ from techtree.cli.commands.engine import (
52
+ install_engine_command,
53
+ status_engine_command,
54
+ verify_engine_command,
55
+ )
56
+ from techtree.cli.commands.proof import verify_proof_command
57
+ from techtree.cli.commands.publish import publish_run_command
58
+ from techtree.cli.commands.release import (
59
+ info_release_command,
60
+ verify_release_command,
61
+ )
62
+ from techtree.cli.commands.run import (
63
+ cancel_run_command,
64
+ logs_run_command,
65
+ result_run_command,
66
+ status_run_command,
67
+ )
68
+ from techtree.cli.commands.setup import setup_command
69
+ from techtree.cli.commands.skill import starter_skill_command
70
+ from techtree.cli.commands.uplift import (
71
+ context_uplift_command,
72
+ prepare_uplift_command,
73
+ skill_source_uplift_command,
74
+ start_uplift_command,
75
+ )
76
+ from techtree.cli.commands.withdraw import withdraw_run_command
77
+ from techtree.cli.context import build_cli_context
78
+ from techtree.cli.invoke import emit_boundary_failure, failure_envelope
79
+ from techtree.cli.output import write_envelope
80
+ from techtree.errors import (
81
+ TechtreeError,
82
+ UsageError,
83
+ exit_code_for,
84
+ stable_exception_message,
85
+ )
86
+ from techtree.version import package_version
87
+
88
+ __all__ = ["create_app", "main", "root_callback"]
89
+
90
+ #: Namespaces that belong to Techtree but are not part of this product surface
91
+ #: yet. Listed so the choice is visible in code review rather than implied by
92
+ #: absence, and asserted by the CLI contract tests.
93
+ RESERVED_NAMESPACES: tuple[str, ...] = (
94
+ "program",
95
+ "blueprint",
96
+ "forge",
97
+ "verify",
98
+ "uplift",
99
+ "trace",
100
+ "lab",
101
+ )
102
+
103
+
104
+ #: Global switches, and the one global option that takes a value. Both lists
105
+ #: exist so a global option can be recognized wherever it appears.
106
+ GLOBAL_FLAGS: frozenset[str] = frozenset(
107
+ {"--json", "--no-color", "--no-input", "--debug", "--version"}
108
+ )
109
+ GLOBAL_VALUE_OPTIONS: frozenset[str] = frozenset({"--home"})
110
+
111
+
112
+ def hoist_global_options(args: Sequence[str]) -> list[str]:
113
+ """Move global options to the front of the command line.
114
+
115
+ The parser binds an option to whichever command declares it, and the global
116
+ options are declared once, on the root. Moving them forward is what lets
117
+ them be written last, where a person naturally puts them. Everything after
118
+ a literal ``--`` is left exactly where it is.
119
+ """
120
+ leading: list[str] = []
121
+ trailing: list[str] = []
122
+ index = 0
123
+
124
+ while index < len(args):
125
+ token = args[index]
126
+ if token == "--":
127
+ trailing.extend(args[index:])
128
+ break
129
+ if token in GLOBAL_FLAGS or any(
130
+ token.startswith(f"{name}=") for name in GLOBAL_VALUE_OPTIONS
131
+ ):
132
+ leading.append(token)
133
+ elif token in GLOBAL_VALUE_OPTIONS and index + 1 < len(args):
134
+ leading.extend(args[index : index + 2])
135
+ index += 1
136
+ else:
137
+ trailing.append(token)
138
+ index += 1
139
+
140
+ return [*leading, *trailing]
141
+
142
+
143
+ class TechtreeGroup(TyperGroup):
144
+ """Root group that accepts global options anywhere on the command line."""
145
+
146
+ def parse_args(self, ctx: Any, args: list[str]) -> list[str]:
147
+ """Hoist global options, then parse normally."""
148
+ return list(super().parse_args(ctx, hoist_global_options(args)))
149
+
150
+
151
+ def version_callback(value: bool) -> None:
152
+ """Print the package version and exit when ``--version`` is given."""
153
+ if not value:
154
+ return
155
+ typer.echo(package_version())
156
+ raise typer.Exit()
157
+
158
+
159
+ def root_callback(
160
+ ctx: typer.Context,
161
+ home: Annotated[
162
+ Path | None,
163
+ typer.Option(
164
+ "--home",
165
+ help="Directory Techtree keeps its local state in.",
166
+ metavar="PATH",
167
+ ),
168
+ ] = None,
169
+ json_output: Annotated[
170
+ bool,
171
+ typer.Option("--json", help="Emit one JSON envelope instead of human output."),
172
+ ] = False,
173
+ no_color: Annotated[
174
+ bool,
175
+ typer.Option("--no-color", help="Never colour human output."),
176
+ ] = False,
177
+ no_input: Annotated[
178
+ bool,
179
+ typer.Option("--no-input", help="Never prompt; fail instead of asking."),
180
+ ] = False,
181
+ debug: Annotated[
182
+ bool,
183
+ typer.Option("--debug", help="Write operational detail to stderr."),
184
+ ] = False,
185
+ version: Annotated[
186
+ bool,
187
+ typer.Option(
188
+ "--version",
189
+ callback=version_callback,
190
+ is_eager=True,
191
+ help="Show the Techtree version and exit.",
192
+ ),
193
+ ] = False,
194
+ ) -> None:
195
+ """Techtree Climb command-line interface."""
196
+ if ctx.invoked_subcommand is None:
197
+ emit_boundary_failure(
198
+ json_output=json_output,
199
+ no_color=no_color,
200
+ command="techtree",
201
+ error=UsageError(
202
+ "no command was given; run `techtree --help` for the available "
203
+ "commands",
204
+ code="no_command",
205
+ ),
206
+ )
207
+
208
+ try:
209
+ ctx.obj = build_cli_context(
210
+ home=home,
211
+ json_output=json_output,
212
+ no_color=no_color,
213
+ no_input=no_input,
214
+ debug=debug,
215
+ )
216
+ except TechtreeError as error:
217
+ emit_boundary_failure(
218
+ json_output=json_output,
219
+ no_color=no_color,
220
+ command=ctx.invoked_subcommand,
221
+ error=error,
222
+ )
223
+
224
+
225
+ def create_app() -> typer.Typer:
226
+ """Construct and return the application."""
227
+ app = typer.Typer(
228
+ name="techtree",
229
+ cls=TechtreeGroup,
230
+ help="Techtree Climb command-line interface.",
231
+ no_args_is_help=True,
232
+ add_completion=False,
233
+ pretty_exceptions_enable=False,
234
+ )
235
+ app.callback(invoke_without_command=True)(root_callback)
236
+ app.command("doctor", help="Check that this machine is ready to run a Climb.")(
237
+ doctor_command
238
+ )
239
+ app.command("setup", help="Prepare this machine to run a Climb.")(setup_command)
240
+ # Publishing and withdrawing are top-level commands rather than members of
241
+ # ``proof`` (decisions 0038's founder ruling of 2026-08-27). ``proof`` is
242
+ # where a person checks evidence offline; these two are the only commands in
243
+ # Techtree that send anything anywhere, and burying them one level down made
244
+ # them read as a variation on checking a proof.
245
+ app.command(
246
+ "publish",
247
+ help="Publish a verified run's proof to the public run log.",
248
+ )(publish_run_command)
249
+ app.command(
250
+ "withdraw",
251
+ help="Withdraw a published entry from the public run log.",
252
+ )(withdraw_run_command)
253
+
254
+ app.add_typer(_climb_app(), name="climb")
255
+ app.add_typer(_skill_app(), name="skill")
256
+ app.add_typer(_run_app(), name="run")
257
+ app.add_typer(_engine_app(), name="engine")
258
+ app.add_typer(_proof_app(), name="proof")
259
+ app.add_typer(_release_app(), name="release")
260
+ app.add_typer(_uplift_app(), name="uplift")
261
+ return app
262
+
263
+
264
+ def main() -> None:
265
+ """Run the application."""
266
+ try:
267
+ create_app()()
268
+ except TechtreeError as error:
269
+ _last_resort(error)
270
+ except Exception as unexpected:
271
+ _last_resort(
272
+ TechtreeError(
273
+ stable_exception_message(unexpected),
274
+ code="internal_error",
275
+ details={"exception_type": type(unexpected).__name__},
276
+ )
277
+ )
278
+
279
+
280
+ # ---------------------------------------------------------------------------
281
+ # Command groups
282
+ # ---------------------------------------------------------------------------
283
+
284
+
285
+ def _climb_app() -> typer.Typer:
286
+ app = typer.Typer(help="Browse and enter Climbs.", no_args_is_help=True)
287
+
288
+ app.command("list", help="Show the Climbs available in this build.")(
289
+ list_climbs_command
290
+ )
291
+ app.command(
292
+ "show",
293
+ help="Show what a Climb measures, the data rights it carries, and "
294
+ "whether this machine can run it.",
295
+ )(show_climb_command)
296
+
297
+ app.command("prepare", help="Prepare a skill for submission to a Climb.")(
298
+ prepare_climb_command
299
+ )
300
+ app.command("start", help="Start a prepared submission running.")(
301
+ start_climb_command
302
+ )
303
+ return app
304
+
305
+
306
+ def _run_app() -> typer.Typer:
307
+ app = typer.Typer(help="Follow and control your runs.", no_args_is_help=True)
308
+
309
+ app.command("status", help="Show how a run is progressing.")(status_run_command)
310
+ app.command("logs", help="Show the log output of a run.")(logs_run_command)
311
+ app.command("cancel", help="Stop a run that is still in progress.")(
312
+ cancel_run_command
313
+ )
314
+ app.command("result", help="Show the finished report for a run.")(
315
+ result_run_command
316
+ )
317
+ return app
318
+
319
+
320
+ def _skill_app() -> typer.Typer:
321
+ app = typer.Typer(help="Obtain the Skills a release names.", no_args_is_help=True)
322
+
323
+ app.command(
324
+ "starter",
325
+ help="Put the starter Skill this release pins on this machine.",
326
+ )(starter_skill_command)
327
+ return app
328
+
329
+
330
+ def _uplift_app() -> typer.Typer:
331
+ app = typer.Typer(help="Improve a Skill from a finished run.", no_args_is_help=True)
332
+
333
+ app.command(
334
+ "context",
335
+ help="Export the sanitized improvement context for a finished run.",
336
+ )(context_uplift_command)
337
+ app.command(
338
+ "skill-source",
339
+ help="Show the verified text of the Skill a finished run measured.",
340
+ )(skill_source_uplift_command)
341
+ app.command(
342
+ "prepare",
343
+ help="Prepare a comparison between a run's Skill and a revision of it.",
344
+ )(prepare_uplift_command)
345
+ app.command("start", help="Start a prepared Skill-against-Skill comparison.")(
346
+ start_uplift_command
347
+ )
348
+ return app
349
+
350
+
351
+ def _proof_app() -> typer.Typer:
352
+ app = typer.Typer(help="Check local proofs.", no_args_is_help=True)
353
+
354
+ app.command(
355
+ "verify",
356
+ help="Check a local proof offline, from the bytes the run stored.",
357
+ )(verify_proof_command)
358
+ return app
359
+
360
+
361
+ def _release_app() -> typer.Typer:
362
+ app = typer.Typer(
363
+ help="Show and check the release this build belongs to.",
364
+ no_args_is_help=True,
365
+ )
366
+
367
+ app.command("info", help="Show the release coordinates this build carries.")(
368
+ info_release_command
369
+ )
370
+ app.command(
371
+ "verify",
372
+ help="Check that every release coordinate still names what it claims.",
373
+ )(verify_release_command)
374
+ return app
375
+
376
+
377
+ def _engine_app() -> typer.Typer:
378
+ app = typer.Typer(
379
+ help="Install and check the evaluation engine.", no_args_is_help=True
380
+ )
381
+ app.command("install", help="Install the evaluation engine on this machine.")(
382
+ install_engine_command
383
+ )
384
+ app.command("status", help="Show which evaluation engine is installed and in use.")(
385
+ status_engine_command
386
+ )
387
+ app.command("verify", help="Confirm the installed evaluation engine is intact.")(
388
+ verify_engine_command
389
+ )
390
+ return app
391
+
392
+
393
+ # ---------------------------------------------------------------------------
394
+ # Last-resort boundary
395
+ # ---------------------------------------------------------------------------
396
+
397
+
398
+ def _last_resort(error: TechtreeError) -> NoReturn:
399
+ """Report a failure that escaped every command's own boundary.
400
+
401
+ Nothing here can consult a ``CliContext`` — the failure may be the reason
402
+ there is not one — so the output mode is read straight off the command line
403
+ and the environment. This path is for defects; every ordinary failure is
404
+ handled where the context exists.
405
+ """
406
+ write_envelope(
407
+ failure_envelope(command="techtree", error=error),
408
+ json_output=_machine_mode(sys.argv[1:], os.environ),
409
+ no_color=True,
410
+ )
411
+ raise SystemExit(exit_code_for(error))
412
+
413
+
414
+ def _machine_mode(argv: Sequence[str], environ: Mapping[str, str]) -> bool:
415
+ """Decide the output mode without a parsed command line."""
416
+ return "--json" in argv or environ.get("TECHTREE_OUTPUT_MODE") == "json"
@@ -0,0 +1 @@
1
+ """Command functions. Each one is plumbing: parse, delegate, emit, exit."""