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
@@ -0,0 +1,181 @@
1
+ """``techtree setup``. Spec section 12.9.
2
+
3
+ One command that takes a machine from "Techtree is installed" to "Techtree can
4
+ run a Climb here": create the local layout, refuse early if a prerequisite is
5
+ missing, install the engine this build ships, make it the active one, verify
6
+ it, settle the local signing key, and say what to do next.
7
+
8
+ The signing key is created here rather than on first use, and it is announced
9
+ rather than assumed: a person running setup is asking this machine to be made
10
+ ready, which is the moment to tell them a key exists, what it is for, and
11
+ where each half of it goes.
12
+
13
+ It does not install the Hermes plugin. ``--hermes`` is reserved so that the
14
+ name means one thing when it does exist, and until then it says so.
15
+
16
+ Prerequisites are checked before anything is downloaded. Finding out that the
17
+ host is unsupported after a several-hundred-megabyte install would be a worse
18
+ version of the same answer.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import Annotated
24
+
25
+ import typer
26
+ from rich.console import Console
27
+
28
+ from techtree.cli.commands.engine import render_engine_status
29
+ from techtree.cli.context import CliContext, cli_context
30
+ from techtree.cli.invoke import CommandResult, invoke_command, not_implemented_error
31
+ from techtree.doctor.service import DoctorService
32
+ from techtree.engines.installer import (
33
+ EngineInstaller,
34
+ InterruptedInstall,
35
+ find_uv,
36
+ )
37
+ from techtree.engines.registry import EngineRegistry
38
+ from techtree.errors import PrerequisiteError
39
+ from techtree.identity.service import IdentityService
40
+ from techtree.identity.store import IdentityStore
41
+ from techtree.models.cli import CliMessage, MessageLevel, NextAction
42
+ from techtree.models.engine import EngineStatus
43
+ from techtree.paths import ensure_path_layout
44
+
45
+ __all__ = ["LOCAL_SIGNING_KEY_NOTICE", "setup_command"]
46
+
47
+ COMMAND = "setup"
48
+
49
+ #: Spec section 7.5. Printed whenever setup settles this machine's identity,
50
+ #: whether it made one or found one, because the sentence a person needs is
51
+ #: what the key is for and where each half of it goes.
52
+ #:
53
+ #: The two halves used to be summarised as "the key is not uploaded", which was
54
+ #: true and, since decisions 0038 built ``techtree publish``, is now the kind
55
+ #: of sentence a reader could take further than it goes. A published proof
56
+ #: carries the public half inside the envelopes it signs — that is what makes
57
+ #: the signature checkable by somebody who does not trust us — so the notice
58
+ #: says which half travels rather than implying neither does.
59
+ LOCAL_SIGNING_KEY_NOTICE = (
60
+ "Techtree keeps a local signing key, used only to detect changes to your "
61
+ "local receipts. The private half never leaves the key directory. The "
62
+ "public half travels inside the proofs it signs, which is what lets "
63
+ "anybody check one.\n"
64
+ "Key: {key_id}"
65
+ )
66
+
67
+
68
+ def setup_command(
69
+ ctx: typer.Context,
70
+ hermes: Annotated[
71
+ bool,
72
+ typer.Option(
73
+ "--hermes",
74
+ help="Reserved for installing the Hermes plugin. Not available yet.",
75
+ ),
76
+ ] = False,
77
+ ) -> None:
78
+ """Prepare this machine to run a Climb."""
79
+ context = cli_context(ctx)
80
+
81
+ def action() -> CommandResult[EngineStatus]:
82
+ if hermes:
83
+ raise not_implemented_error("setup --hermes")
84
+
85
+ ensure_path_layout(context.paths)
86
+ _check_prerequisites(context)
87
+
88
+ registry = EngineRegistry(context.paths, context.settings)
89
+ installer = EngineInstaller(context.paths, registry, find_uv())
90
+
91
+ # Decisions 0004, ratified as 0007 R7: an install that was killed is
92
+ # found here, said out loud, and discarded by the install that
93
+ # follows. Reported before the install so the sentence a person reads
94
+ # is about the machine they left behind, not about this run.
95
+ interrupted = installer.interrupted_installs()
96
+ installed = installer.install()
97
+ registry.set_active(installed.digest)
98
+ status = installer.verify(installed.digest)
99
+ identity = IdentityService(IdentityStore(context.paths)).ensure()
100
+
101
+ return CommandResult(
102
+ data=status,
103
+ messages=[
104
+ CliMessage(
105
+ level=MessageLevel.INFO,
106
+ code="local_signing_key",
107
+ text=LOCAL_SIGNING_KEY_NOTICE.format(key_id=identity.key_id),
108
+ ),
109
+ CliMessage(
110
+ level=MessageLevel.INFO,
111
+ code="setup_complete",
112
+ text=(
113
+ f"This machine is ready. Evaluation engine "
114
+ f"{status.digest} is installed, verified, and active."
115
+ ),
116
+ ),
117
+ ],
118
+ warnings=[_interrupted_notice(install) for install in interrupted],
119
+ next_actions=[_browse_climbs()],
120
+ )
121
+
122
+ invoke_command(context, COMMAND, action, render_data=_render)
123
+
124
+
125
+ def _interrupted_notice(install: InterruptedInstall) -> CliMessage:
126
+ """Say that an earlier install did not finish, and what became of it."""
127
+ when = "" if install.started_at is None else f", started {install.started_at}"
128
+ return CliMessage(
129
+ level=MessageLevel.WARNING,
130
+ code="engine_install_interrupted",
131
+ text=(
132
+ f"An earlier install of evaluation engine {install.digest} did not "
133
+ f"finish{when}. What it left behind was removed and the engine was "
134
+ "installed again from scratch."
135
+ ),
136
+ )
137
+
138
+
139
+ def _check_prerequisites(context: CliContext) -> None:
140
+ """Stop before installing anything if this host cannot run a Climb."""
141
+ doctor = DoctorService(context.paths, context.settings)
142
+ blocking = doctor.blocking_failures(doctor.run())
143
+ if not blocking:
144
+ return
145
+
146
+ identifiers = [check.id for check in blocking]
147
+ raise PrerequisiteError(
148
+ "this host is not ready to run a Climb: " + ", ".join(identifiers),
149
+ code="environment_not_ready",
150
+ details={"failed_checks": list(identifiers)},
151
+ next_actions=[_run_doctor()],
152
+ )
153
+
154
+
155
+ def _render(data: object, console: Console) -> None:
156
+ if isinstance(data, EngineStatus):
157
+ render_engine_status(data, console)
158
+
159
+
160
+ def _run_doctor() -> NextAction:
161
+ return NextAction(
162
+ id="run_doctor",
163
+ label="See what this machine is missing",
164
+ reason="Doctor lists every prerequisite and how to satisfy it.",
165
+ cli=["techtree", "doctor"],
166
+ hermes_tool=None,
167
+ hermes_args=None,
168
+ requires_user_confirmation=False,
169
+ )
170
+
171
+
172
+ def _browse_climbs() -> NextAction:
173
+ return NextAction(
174
+ id="list_climbs",
175
+ label="Browse the available Climbs",
176
+ reason="This host is ready.",
177
+ cli=["techtree", "climb", "list"],
178
+ hermes_tool=None,
179
+ hermes_args=None,
180
+ requires_user_confirmation=False,
181
+ )
@@ -0,0 +1,221 @@
1
+ """``techtree skill starter``. Spec section 10.5, decisions 0008.
2
+
3
+ One command, and it does one thing: put the starter Skill this release pins
4
+ on this machine, and say where it is. The guided first run needs a Skill
5
+ before it can prepare anything, and until this command existed the only way
6
+ to get one was for a person to already have the file.
7
+
8
+ Run with no options it needs nothing from the caller at all: the release names
9
+ both which Skill it measured and the address that Skill is published at, so
10
+ ``--from-file`` and ``--from-url`` are for the cases where somebody has a copy
11
+ of their own. Whichever route the bytes take, they are proved against the
12
+ release's digest before they are kept.
13
+
14
+ What it returns goes through ``climb prepare`` like anybody else's Skill: the
15
+ same scanner, the same policy, the same draft, the same confirmation. There is
16
+ no privileged route for a Skill because a release named it — the release's
17
+ authority is over *which* Skill, not over what Techtree will accept.
18
+
19
+ The command writes only into the Techtree home. A materialized Skill is
20
+ cached under the home's own cache directory, in a directory named by the
21
+ digest it was verified against, so a second guided run reuses it and a cached
22
+ directory somebody edited is re-verified rather than trusted.
23
+
24
+ Decisions document 0010 item 5 governs what comes back. Where the Skill *is*,
25
+ what it is *called*, and what a candidate carrying it is *labelled* are three
26
+ separate values, and the answer states all three separately. They are not
27
+ interchangeable and none of them is computed from another: a path is a fact
28
+ about this machine, a name is the Skill's own, and a label is a short public
29
+ string a comparison is filed under. Deriving any of the latter two from the
30
+ first is how a cache directory's digest, a temporary directory, or a download
31
+ URL ends up printed as the name of somebody's work.
32
+
33
+ The answer also says what the Skill is *for*. Decisions document 0010 item 2
34
+ rules that the starter Skill's own text stays silent about the gap it was
35
+ written with, and that the release metadata discloses instead — the public
36
+ Climb page, this command, and the calibration record all carry the same
37
+ sentence. So the purpose is printed beside the name here: nobody is handed the
38
+ starter Skill without being told it is an introductory one that is incomplete
39
+ on purpose.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ from pathlib import Path
45
+ from typing import Annotated, Final, Literal
46
+
47
+ import typer
48
+ from rich.console import Console
49
+
50
+ from techtree.cli.context import cli_context
51
+ from techtree.cli.invoke import CommandResult, invoke_command
52
+ from techtree.cli.output import render_pairs
53
+ from techtree.constants import (
54
+ STARTER_SKILL_CANDIDATE_LABEL,
55
+ STARTER_SKILL_NAME,
56
+ STARTER_SKILL_PURPOSE,
57
+ )
58
+ from techtree.models.base import Digest, NonEmptyString, ProtocolModel
59
+ from techtree.models.cli import CliMessage, MessageLevel, NextAction
60
+ from techtree.release.document import packaged_release_core_bytes, parse_release_core
61
+ from techtree.skills.starter import MaterializedStarterSkill, StarterSkillService
62
+
63
+ __all__ = [
64
+ "STARTER_COMMAND",
65
+ "StarterSkillPayload",
66
+ "starter_skill_command",
67
+ ]
68
+
69
+ STARTER_COMMAND: Final = "skill starter"
70
+
71
+
72
+ class StarterSkillPayload(ProtocolModel):
73
+ """Where the pinned starter Skill is, what it is called, and what it proved.
74
+
75
+ ``skill_path`` is where the Skill's entry file sits on this machine.
76
+ ``skill_name`` is what the Skill calls itself. ``candidate_label`` is what
77
+ a comparison carrying it is filed under. Decisions document 0010 item 5
78
+ keeps the three apart: a reader and a host agent both need to be able to
79
+ take the label without ever looking at the path.
80
+
81
+ ``skill_purpose`` is the fourth, and it is about the artifact rather than
82
+ about this machine: decisions document 0010 item 2 puts the starter
83
+ Skill's disclosure in the release metadata, so whoever is handed the Skill
84
+ is told what it is for before they run it.
85
+ """
86
+
87
+ release_id: NonEmptyString
88
+ skill_root_digest: Digest
89
+ skill_path: NonEmptyString
90
+ skill_name: NonEmptyString
91
+ skill_purpose: NonEmptyString
92
+ candidate_label: NonEmptyString
93
+ file_count: int
94
+ total_bytes: int
95
+ origin: Literal["cache", "local_file", "download", "release"]
96
+ intro_climb_reference: NonEmptyString
97
+
98
+
99
+ def starter_skill_command(
100
+ ctx: typer.Context,
101
+ from_file: Annotated[
102
+ Path | None,
103
+ typer.Option(
104
+ "--from-file",
105
+ metavar="PATH",
106
+ help="A local copy of the starter Skill: its directory or SKILL.md.",
107
+ ),
108
+ ] = None,
109
+ from_url: Annotated[
110
+ str | None,
111
+ typer.Option(
112
+ "--from-url",
113
+ metavar="URL",
114
+ help=(
115
+ "Where to fetch the starter Skill from, instead of the address "
116
+ "this release publishes it at."
117
+ ),
118
+ ),
119
+ ] = None,
120
+ ) -> None:
121
+ """Put the starter Skill this release pins on this machine."""
122
+ context = cli_context(ctx)
123
+
124
+ def action() -> CommandResult[StarterSkillPayload]:
125
+ release = parse_release_core(packaged_release_core_bytes())
126
+ materialized = StarterSkillService(context.paths).materialize(
127
+ release=release, local_file=from_file, url=from_url
128
+ )
129
+ payload = StarterSkillPayload(
130
+ release_id=release.release_id,
131
+ skill_root_digest=materialized.root_digest,
132
+ skill_path=str(materialized.entrypoint),
133
+ skill_name=STARTER_SKILL_NAME,
134
+ skill_purpose=STARTER_SKILL_PURPOSE,
135
+ candidate_label=STARTER_SKILL_CANDIDATE_LABEL,
136
+ file_count=materialized.file_count,
137
+ total_bytes=materialized.total_bytes,
138
+ origin=materialized.origin,
139
+ intro_climb_reference=release.intro_climb_reference,
140
+ )
141
+ return CommandResult(
142
+ data=payload,
143
+ messages=[
144
+ CliMessage(
145
+ level=MessageLevel.INFO,
146
+ code="starter_skill_ready",
147
+ text=_summary(materialized),
148
+ )
149
+ ],
150
+ next_actions=[_prepare_action(payload)],
151
+ )
152
+
153
+ invoke_command(context, STARTER_COMMAND, action, render_data=_render)
154
+
155
+
156
+ def _summary(materialized: MaterializedStarterSkill) -> str:
157
+ """Say in one sentence what happened and what was proved."""
158
+ how = {
159
+ "cache": "was already on this machine",
160
+ "local_file": "was read from the file you named",
161
+ "download": "was fetched from the source you named",
162
+ "release": "was fetched from the address this release publishes it at",
163
+ }[materialized.origin]
164
+ return (
165
+ f"The starter Skill {how} and matches the digest this release pins. "
166
+ "It is prepared the same way any other Skill is."
167
+ )
168
+
169
+
170
+ def _prepare_action(payload: StarterSkillPayload) -> NextAction:
171
+ """Point at the ordinary preparation path, because that is the only one.
172
+
173
+ The label is stated rather than left to default. ``climb prepare`` falls
174
+ back to the skill directory's own name, and this Skill lives in a directory
175
+ named by the digest it was verified against: seventy-one characters, longer
176
+ than a label may be, and meaningless to a reader. Decisions document 0010
177
+ item 5 rules that out for good — the label here is the release's own short
178
+ name for the candidate, never anything read off the path beside it — and
179
+ naming it is also what makes these arguments runnable exactly as printed.
180
+ """
181
+ return NextAction(
182
+ id="prepare_starter_skill",
183
+ label="Prepare Techtree Hello World with the starter Skill",
184
+ reason=(
185
+ "The starter Skill is scanned, checked against the Climb's policy, "
186
+ "and shown to you before anything runs."
187
+ ),
188
+ cli=[
189
+ "techtree",
190
+ "climb",
191
+ "prepare",
192
+ payload.intro_climb_reference,
193
+ "--skill",
194
+ payload.skill_path,
195
+ "--label",
196
+ payload.candidate_label,
197
+ ],
198
+ hermes_tool=None,
199
+ hermes_args=None,
200
+ requires_user_confirmation=True,
201
+ )
202
+
203
+
204
+ def _render(data: object, console: Console) -> None:
205
+ """Print where the Skill is and what it was verified against."""
206
+ if not isinstance(data, StarterSkillPayload):
207
+ return
208
+ render_pairs(
209
+ [
210
+ ("Release", data.release_id),
211
+ ("Skill", data.skill_name),
212
+ ("Purpose", data.skill_purpose),
213
+ ("Candidate label", data.candidate_label),
214
+ ("Skill content digest", data.skill_root_digest),
215
+ ("Skill file", data.skill_path),
216
+ ("Files", str(data.file_count)),
217
+ ("Size", f"{data.total_bytes} bytes"),
218
+ ("Obtained", data.origin.replace("_", " ")),
219
+ ],
220
+ console,
221
+ )