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.
- techtree/__init__.py +35 -0
- techtree/__main__.py +14 -0
- techtree/canonical.py +239 -0
- techtree/catalog/__init__.py +25 -0
- techtree/catalog/repository.py +400 -0
- techtree/catalog/service.py +419 -0
- techtree/cli/__init__.py +1 -0
- techtree/cli/app.py +416 -0
- techtree/cli/commands/__init__.py +1 -0
- techtree/cli/commands/climb.py +1223 -0
- techtree/cli/commands/doctor.py +147 -0
- techtree/cli/commands/engine.py +207 -0
- techtree/cli/commands/proof.py +556 -0
- techtree/cli/commands/publish.py +447 -0
- techtree/cli/commands/release.py +303 -0
- techtree/cli/commands/run.py +1067 -0
- techtree/cli/commands/setup.py +181 -0
- techtree/cli/commands/skill.py +221 -0
- techtree/cli/commands/uplift.py +698 -0
- techtree/cli/commands/withdraw.py +212 -0
- techtree/cli/confirm.py +47 -0
- techtree/cli/context.py +96 -0
- techtree/cli/invoke.py +220 -0
- techtree/cli/output.py +280 -0
- techtree/constants.py +138 -0
- techtree/crypto.py +128 -0
- techtree/doctor/__init__.py +1 -0
- techtree/doctor/checks.py +675 -0
- techtree/doctor/execution_checks.py +435 -0
- techtree/doctor/service.py +326 -0
- techtree/drafts/__init__.py +32 -0
- techtree/drafts/source.py +146 -0
- techtree/drafts/store.py +992 -0
- techtree/engines/__init__.py +1 -0
- techtree/engines/bundle.py +251 -0
- techtree/engines/installer.py +679 -0
- techtree/engines/registry.py +235 -0
- techtree/engines/runner.py +170 -0
- techtree/errors.py +262 -0
- techtree/fs.py +234 -0
- techtree/harness.py +108 -0
- techtree/identity/__init__.py +41 -0
- techtree/identity/models.py +113 -0
- techtree/identity/service.py +199 -0
- techtree/identity/store.py +263 -0
- techtree/ids.py +85 -0
- techtree/manifests/__init__.py +39 -0
- techtree/manifests/builder.py +433 -0
- techtree/manifests/compare.py +376 -0
- techtree/models/__init__.py +282 -0
- techtree/models/base.py +201 -0
- techtree/models/campaign.py +484 -0
- techtree/models/catalog.py +227 -0
- techtree/models/cli.py +151 -0
- techtree/models/climb.py +254 -0
- techtree/models/data_policy.py +130 -0
- techtree/models/engine.py +156 -0
- techtree/models/episode_receipt.py +130 -0
- techtree/models/evaluation_backend.py +113 -0
- techtree/models/experiment.py +154 -0
- techtree/models/run.py +214 -0
- techtree/models/skill.py +156 -0
- techtree/models/uplift_report.py +158 -0
- techtree/models/validation.py +299 -0
- techtree/paths.py +116 -0
- techtree/presentation/__init__.py +31 -0
- techtree/presentation/build.py +1242 -0
- techtree/presentation/compact.py +246 -0
- techtree/presentation/evidence.py +169 -0
- techtree/presentation/models.py +358 -0
- techtree/presentation/rich.py +312 -0
- techtree/presentation/sanitize.py +156 -0
- techtree/publication/__init__.py +44 -0
- techtree/publication/address.py +180 -0
- techtree/publication/coordinates.py +26 -0
- techtree/publication/journal.py +212 -0
- techtree/publication/keccak.py +183 -0
- techtree/publication/models.py +209 -0
- techtree/publication/offer.py +35 -0
- techtree/publication/service.py +618 -0
- techtree/publication/transport.py +296 -0
- techtree/publication/verify.py +242 -0
- techtree/publication/withdraw.py +156 -0
- techtree/py.typed +0 -0
- techtree/receipts/__init__.py +52 -0
- techtree/receipts/bundle.py +578 -0
- techtree/receipts/compare.py +1065 -0
- techtree/receipts/episode.py +672 -0
- techtree/receipts/execution.py +630 -0
- techtree/receipts/observed.py +474 -0
- techtree/receipts/set.py +336 -0
- techtree/receipts/uplift.py +655 -0
- techtree/receipts/verify.py +1055 -0
- techtree/release/__init__.py +9 -0
- techtree/release/bootstrap.py +509 -0
- techtree/release/checks.py +376 -0
- techtree/release/document.py +125 -0
- techtree/release/generate.py +221 -0
- techtree/release/models.py +293 -0
- techtree/release/provenance.py +109 -0
- techtree/resources/catalog/campaigns/hello-world-climb.json +1 -0
- techtree/resources/catalog/catalog.json +32 -0
- techtree/resources/catalog/climbs/hello-world-climb.json +1 -0
- techtree/resources/catalog/data-policies/hello-world-climb.json +1 -0
- techtree/resources/catalog/taskset-validations/hello-world-climb.json +1 -0
- techtree/resources/catalog/validation-evidence/hello-world-climb.json +1 -0
- techtree/resources/engines/default/engine.json +20 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/__init__.py +7 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/algorithm.py +136 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/dataset.py +156 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/env.py +48 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/taskset.py +163 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/pyproject.toml +13 -0
- techtree/resources/engines/default/pyproject.toml +23 -0
- techtree/resources/engines/default/tools/inspect_taskset.py +124 -0
- techtree/resources/engines/default/tools/normalize_eval_output.py +470 -0
- techtree/resources/engines/default/tools/normalize_validation.py +222 -0
- techtree/resources/engines/default/uv.lock +1758 -0
- techtree/resources/harness/hermes-agent-0.19.0.json +69 -0
- techtree/resources/release/build-provenance.json +4 -0
- techtree/resources/release/release-core.json +24 -0
- techtree/runs/__init__.py +31 -0
- techtree/runs/artifacts.py +750 -0
- techtree/runs/child_registry.py +228 -0
- techtree/runs/events.py +478 -0
- techtree/runs/executor.py +140 -0
- techtree/runs/fake.py +741 -0
- techtree/runs/launcher.py +253 -0
- techtree/runs/machine.py +489 -0
- techtree/runs/real.py +789 -0
- techtree/runs/service.py +616 -0
- techtree/runs/store.py +555 -0
- techtree/runs/validation.py +259 -0
- techtree/runs/variants.py +684 -0
- techtree/settings.py +143 -0
- techtree/skills/__init__.py +14 -0
- techtree/skills/archive.py +282 -0
- techtree/skills/policy.py +62 -0
- techtree/skills/scanner.py +394 -0
- techtree/skills/service.py +752 -0
- techtree/skills/starter.py +434 -0
- techtree/tasksets/__init__.py +1 -0
- techtree/tasksets/membership.py +269 -0
- techtree/tasksets/provider.py +207 -0
- techtree/tasksets/resolver.py +311 -0
- techtree/tasksets/service.py +484 -0
- techtree/tasksets/verifiers_cli.py +538 -0
- techtree/uplift/__init__.py +20 -0
- techtree/uplift/context.py +544 -0
- techtree/uplift/derive.py +203 -0
- techtree/uplift/public_tasks.py +151 -0
- techtree/uplift/service.py +719 -0
- techtree/uplift/source.py +160 -0
- techtree/verifiers/__init__.py +31 -0
- techtree/verifiers/budget.py +219 -0
- techtree/verifiers/child.py +633 -0
- techtree/verifiers/compiler.py +432 -0
- techtree/verifiers/config.py +365 -0
- techtree/verifiers/credentials.py +321 -0
- techtree/verifiers/image.py +126 -0
- techtree/verifiers/models.py +527 -0
- techtree/verifiers/outputs.py +368 -0
- techtree/verifiers/progress.py +192 -0
- techtree/verifiers/supervisor.py +341 -0
- techtree/verifiers/verify.py +782 -0
- techtree/version.py +39 -0
- techtree/worker/__init__.py +18 -0
- techtree/worker/execute.py +487 -0
- techtree/worker/main.py +57 -0
- techtree-0.1.0.dist-info/METADATA +344 -0
- techtree-0.1.0.dist-info/RECORD +174 -0
- techtree-0.1.0.dist-info/WHEEL +4 -0
- techtree-0.1.0.dist-info/entry_points.txt +3 -0
- 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
|
+
)
|