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,1223 @@
|
|
|
1
|
+
"""``techtree climb list``, ``show``, and ``prepare``. Spec 12.6 and PR6 §6.9.
|
|
2
|
+
|
|
3
|
+
No command here decides anything about a Climb. The catalog service resolves
|
|
4
|
+
the graph and answers whether this machine could run it; the preparation
|
|
5
|
+
service turns a directory into an immutable draft. These functions turn those
|
|
6
|
+
answers into one envelope, some warnings, and at most three next steps.
|
|
7
|
+
|
|
8
|
+
Four translations are worth naming.
|
|
9
|
+
|
|
10
|
+
A compatibility issue becomes a next action only when something runnable would
|
|
11
|
+
address it. An absent engine has an install command; an unsupported machine has
|
|
12
|
+
nothing Techtree could offer to run, so it is stated and no action is invented.
|
|
13
|
+
|
|
14
|
+
A development Climb is announced as a warning in both output modes rather than
|
|
15
|
+
only in the human rendering. A host agent reading JSON is exactly the caller
|
|
16
|
+
most likely to treat a fixture result as evidence, so the caveat travels with
|
|
17
|
+
the data.
|
|
18
|
+
|
|
19
|
+
``show`` returns a payload rather than a bare summary. Four facts a reader
|
|
20
|
+
needs before entering a Climb — which model answers, where it runs, which
|
|
21
|
+
reward decides the comparison, and who owns a submitted skill — have no field
|
|
22
|
+
on :class:`~techtree.models.catalog.ClimbSummary`, and a host agent should not
|
|
23
|
+
have to read them out of a rendered table.
|
|
24
|
+
|
|
25
|
+
``prepare`` writes the draft and stops. The start action it offers names the
|
|
26
|
+
draft and nothing else, and is marked as requiring a person's confirmation,
|
|
27
|
+
because starting a run commits to both rights and work.
|
|
28
|
+
|
|
29
|
+
``start`` is where that commitment is collected. Decisions document 0019
|
|
30
|
+
section 2 makes it one gesture rather than two handles: the five things a
|
|
31
|
+
person has to weigh — how much work this is, the most the Campaign declares it
|
|
32
|
+
may cost, that the Skill is the only scientific change, where the model calls
|
|
33
|
+
go, and what an upload would and would not carry — are printed, the rights
|
|
34
|
+
summary is printed
|
|
35
|
+
under them, and the
|
|
36
|
+
answer is a plain ``y``. An operator who cannot be asked passes ``--yes``
|
|
37
|
+
instead, which is an explicit act by a person configuring a machine and never a
|
|
38
|
+
shortcut a model may take on somebody's behalf.
|
|
39
|
+
|
|
40
|
+
The same review can also be answered somewhere else. When the plugin starts a
|
|
41
|
+
draft, Hermes has already shown the review and taken the person's confirmation
|
|
42
|
+
through its own dispatch gate, and this command is only the thing that writes
|
|
43
|
+
the record; ``--reviewed-on host-agent`` is how that is said, so the run
|
|
44
|
+
records the surface the answer was really given on rather than the surface the
|
|
45
|
+
writing happened on. Either way the run records that the review was shown and
|
|
46
|
+
accepted, and its ``run.approved`` event records who gave the answer.
|
|
47
|
+
|
|
48
|
+
The command returns as soon as the worker is launched. The run continues after
|
|
49
|
+
this process exits, which is the whole point, and the response says where to
|
|
50
|
+
look rather than waiting to find out.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
from dataclasses import dataclass
|
|
56
|
+
from enum import StrEnum
|
|
57
|
+
from pathlib import Path
|
|
58
|
+
from typing import Annotated, Final, Literal
|
|
59
|
+
|
|
60
|
+
import typer
|
|
61
|
+
from pydantic import PositiveFloat
|
|
62
|
+
from rich.console import Console
|
|
63
|
+
from rich.table import Table
|
|
64
|
+
|
|
65
|
+
from techtree.catalog.repository import EmbeddedCatalogRepository, climb_reference
|
|
66
|
+
from techtree.catalog.service import (
|
|
67
|
+
CatalogService,
|
|
68
|
+
InstalledEngineStatus,
|
|
69
|
+
current_host_info,
|
|
70
|
+
)
|
|
71
|
+
from techtree.cli.commands.run import build_run_service
|
|
72
|
+
from techtree.cli.confirm import confirmed
|
|
73
|
+
from techtree.cli.context import CliContext, cli_context
|
|
74
|
+
from techtree.cli.invoke import CommandResult, invoke_command
|
|
75
|
+
from techtree.cli.output import human_console, render_pairs
|
|
76
|
+
from techtree.drafts.source import CampaignSource
|
|
77
|
+
from techtree.drafts.store import DraftStore, utc_now
|
|
78
|
+
from techtree.errors import (
|
|
79
|
+
NotFoundError,
|
|
80
|
+
PolicyError,
|
|
81
|
+
PrerequisiteError,
|
|
82
|
+
UsageError,
|
|
83
|
+
)
|
|
84
|
+
from techtree.models.base import (
|
|
85
|
+
Digest,
|
|
86
|
+
NonEmptyString,
|
|
87
|
+
ProtocolModel,
|
|
88
|
+
)
|
|
89
|
+
from techtree.models.campaign import CampaignSpec, ModelSpec, RuntimeSpec
|
|
90
|
+
from techtree.models.catalog import (
|
|
91
|
+
ClimbSummary,
|
|
92
|
+
CompatibilityResult,
|
|
93
|
+
EngineCompatibilityStatus,
|
|
94
|
+
)
|
|
95
|
+
from techtree.models.cli import CliMessage, MessageLevel, NextAction
|
|
96
|
+
from techtree.models.climb import ResolvedClimb
|
|
97
|
+
from techtree.models.run import (
|
|
98
|
+
PolicyAcknowledgement,
|
|
99
|
+
RunPhase,
|
|
100
|
+
RunRequest,
|
|
101
|
+
RunStatus,
|
|
102
|
+
)
|
|
103
|
+
from techtree.models.skill import PolicyAcceptanceRequirement, SubmissionDraft
|
|
104
|
+
from techtree.runs.service import POLICY_ACCEPTANCE_REQUIRED, ApprovalActor
|
|
105
|
+
from techtree.skills.service import PreparedDraft, SkillPreparationService
|
|
106
|
+
|
|
107
|
+
__all__ = [
|
|
108
|
+
"LIST_COMMAND",
|
|
109
|
+
"PREPARE_COMMAND",
|
|
110
|
+
"REVIEW_SURFACE_NOT_APPROVED",
|
|
111
|
+
"SHOW_COMMAND",
|
|
112
|
+
"START_COMMAND",
|
|
113
|
+
"ClimbPreparePayload",
|
|
114
|
+
"ClimbShowPayload",
|
|
115
|
+
"ClimbStartPayload",
|
|
116
|
+
"PreparedComparison",
|
|
117
|
+
"ReviewSurface",
|
|
118
|
+
"RunApproval",
|
|
119
|
+
"abbreviated_digest",
|
|
120
|
+
"approve_run",
|
|
121
|
+
"build_catalog_service",
|
|
122
|
+
"build_preparation_service",
|
|
123
|
+
"list_climbs_command",
|
|
124
|
+
"phrase",
|
|
125
|
+
"prepare_climb_command",
|
|
126
|
+
"review_lines",
|
|
127
|
+
"show_climb_command",
|
|
128
|
+
"start_climb_command",
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
LIST_COMMAND: Final = "climb list"
|
|
132
|
+
SHOW_COMMAND: Final = "climb show"
|
|
133
|
+
PREPARE_COMMAND: Final = "climb prepare"
|
|
134
|
+
START_COMMAND: Final = "climb start"
|
|
135
|
+
|
|
136
|
+
#: What a reader is told when the build ships no Climbs at all. The packaged
|
|
137
|
+
#: catalog is generated, so an empty one means this build was assembled without
|
|
138
|
+
#: running the generator rather than that there is nothing to run.
|
|
139
|
+
_NO_CLIMBS = "This build does not include any Climbs yet."
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
#: What a start says when a surface was declared but nothing was approved.
|
|
143
|
+
REVIEW_SURFACE_NOT_APPROVED: Final = "review_surface_not_approved"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ReviewSurface(StrEnum):
|
|
147
|
+
"""Where the person who approved this run answered.
|
|
148
|
+
|
|
149
|
+
Decisions document 0019 section 2 keeps two approval surfaces: this command
|
|
150
|
+
line, and the host agent's own confirmation UI. The process that writes the
|
|
151
|
+
run's record is not always the process that asked the question — when the
|
|
152
|
+
plugin starts a draft, Hermes asked and the CLI writes — so which surface
|
|
153
|
+
it was is declared rather than inferred from the fact that a flag was used.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
CLI = "cli"
|
|
157
|
+
HOST_AGENT = "host-agent"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class ClimbShowPayload(ProtocolModel):
|
|
161
|
+
"""What ``climb show`` returns: the summary, plus the Campaign facts.
|
|
162
|
+
|
|
163
|
+
The five extra fields are read straight off the resolved graph. They are
|
|
164
|
+
carried here rather than added to ``ClimbSummary`` because the summary is a
|
|
165
|
+
published protocol object with an exported schema, and this is one
|
|
166
|
+
command's response shape.
|
|
167
|
+
|
|
168
|
+
Decisions document 0007 R3 asks that a machine reader get both complete
|
|
169
|
+
digests. The Campaign's is on the summary, where it has always been and
|
|
170
|
+
where ``climb list`` also carries it; the DataPolicy's is here, because
|
|
171
|
+
the summary describes the rights in words but never named the document
|
|
172
|
+
they come from. Neither is ever abbreviated in the JSON — the shortening
|
|
173
|
+
is a courtesy to a terminal, and a caller comparing digests needs all of
|
|
174
|
+
both.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
climb: ClimbSummary
|
|
178
|
+
data_policy_digest: Digest
|
|
179
|
+
subject_model: ModelSpec
|
|
180
|
+
subject_runtime: RuntimeSpec
|
|
181
|
+
primary_reward: NonEmptyString
|
|
182
|
+
candidate_skill_ownership: Literal["participant", "account", "shared"]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class PreparedComparison(ProtocolModel):
|
|
186
|
+
"""The controlled-comparison result, as a reader needs to check it."""
|
|
187
|
+
|
|
188
|
+
controlled: bool
|
|
189
|
+
differences: list[NonEmptyString]
|
|
190
|
+
allowed_differences: list[NonEmptyString]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class ClimbPreparePayload(ProtocolModel):
|
|
194
|
+
"""What ``climb prepare`` returns: the draft, and what it commits to."""
|
|
195
|
+
|
|
196
|
+
draft_id: NonEmptyString
|
|
197
|
+
draft_digest: Digest
|
|
198
|
+
climb_reference: NonEmptyString
|
|
199
|
+
climb_digest: Digest
|
|
200
|
+
campaign_spec_digest: Digest
|
|
201
|
+
data_policy_digest: Digest
|
|
202
|
+
candidate_label: NonEmptyString
|
|
203
|
+
skill_root_digest: Digest
|
|
204
|
+
included_files: list[NonEmptyString]
|
|
205
|
+
baseline_skill_count: int
|
|
206
|
+
candidate_skill_count: int
|
|
207
|
+
estimated_episodes: int
|
|
208
|
+
# Read off the Campaign this draft was prepared against, so a caller that
|
|
209
|
+
# renders a review shows the maximum this run is held to and never a figure
|
|
210
|
+
# from somewhere else. ``None`` is a Campaign that declares no maximum, and
|
|
211
|
+
# says so: there is then no figure to hold it to. Decision 0019 section 2
|
|
212
|
+
# puts the budget in the plugin's review the way it is already in the
|
|
213
|
+
# terminal's, and a review that has to invent the number is a review that
|
|
214
|
+
# would be wrong for the next Campaign.
|
|
215
|
+
campaign_maximum_usd: PositiveFloat | None
|
|
216
|
+
candidate_ownership: Literal["participant", "account", "shared"]
|
|
217
|
+
candidate_public_release: Literal[
|
|
218
|
+
"required_for_climb", "allowed", "prohibited", "consent_required"
|
|
219
|
+
]
|
|
220
|
+
raw_episode_server_upload: Literal["allowed", "prohibited", "consent_required"]
|
|
221
|
+
raw_episode_training_use: Literal["allowed", "prohibited", "consent_required"]
|
|
222
|
+
proof_grade: Literal["development_only", "P1"]
|
|
223
|
+
policy_acceptance: PolicyAcceptanceRequirement
|
|
224
|
+
comparison: PreparedComparison
|
|
225
|
+
warnings: list[NonEmptyString]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class ClimbStartPayload(ProtocolModel):
|
|
229
|
+
"""What ``climb start`` returns, as soon as the worker is running."""
|
|
230
|
+
|
|
231
|
+
run_id: NonEmptyString
|
|
232
|
+
draft_id: NonEmptyString
|
|
233
|
+
draft_digest: Digest
|
|
234
|
+
phase: RunPhase
|
|
235
|
+
worker_pid: int | None
|
|
236
|
+
campaign_spec_digest: Digest
|
|
237
|
+
data_policy_digest: Digest
|
|
238
|
+
policy_acknowledgement_method: Literal[
|
|
239
|
+
"explicit_cli_review",
|
|
240
|
+
"host_agent_confirmation",
|
|
241
|
+
]
|
|
242
|
+
approved_by: ApprovalActor
|
|
243
|
+
#: Whether this run used the fake executor, and so called no model at all.
|
|
244
|
+
#: It is not the Climb's proof grade: a Climb whose results may never be
|
|
245
|
+
#: published is still run for real, against a real model, at real cost.
|
|
246
|
+
fake_executor: bool
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def build_catalog_service(context: CliContext) -> CatalogService:
|
|
250
|
+
"""Construct the service every command reads the catalog through."""
|
|
251
|
+
return CatalogService(
|
|
252
|
+
EmbeddedCatalogRepository.packaged(),
|
|
253
|
+
current_host_info(),
|
|
254
|
+
InstalledEngineStatus(context.paths),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def build_preparation_service(context: CliContext) -> SkillPreparationService:
|
|
259
|
+
"""Construct the service ``prepare`` builds a draft through."""
|
|
260
|
+
return SkillPreparationService(
|
|
261
|
+
paths=context.paths,
|
|
262
|
+
catalog=build_catalog_service(context),
|
|
263
|
+
draft_store=DraftStore(context.paths),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def list_climbs_command(ctx: typer.Context) -> None:
|
|
268
|
+
"""List public wrappers with resolved Campaign compatibility."""
|
|
269
|
+
context = cli_context(ctx)
|
|
270
|
+
|
|
271
|
+
def action() -> CommandResult[list[ClimbSummary]]:
|
|
272
|
+
summaries = build_catalog_service(context).list_climbs()
|
|
273
|
+
|
|
274
|
+
if not summaries:
|
|
275
|
+
return CommandResult(
|
|
276
|
+
data=summaries,
|
|
277
|
+
messages=[
|
|
278
|
+
CliMessage(
|
|
279
|
+
level=MessageLevel.INFO,
|
|
280
|
+
code="no_climbs_available",
|
|
281
|
+
text=_NO_CLIMBS,
|
|
282
|
+
)
|
|
283
|
+
],
|
|
284
|
+
next_actions=[_check_environment()],
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return CommandResult(
|
|
288
|
+
data=summaries,
|
|
289
|
+
messages=[
|
|
290
|
+
CliMessage(
|
|
291
|
+
level=MessageLevel.INFO,
|
|
292
|
+
code="climbs_available",
|
|
293
|
+
text=_available_summary(len(summaries)),
|
|
294
|
+
)
|
|
295
|
+
],
|
|
296
|
+
warnings=_development_warnings(summaries),
|
|
297
|
+
next_actions=[_show_climb(summaries[0].reference)],
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
invoke_command(context, LIST_COMMAND, action, render_data=_render_list)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def show_climb_command(
|
|
304
|
+
ctx: typer.Context,
|
|
305
|
+
reference: Annotated[
|
|
306
|
+
str,
|
|
307
|
+
typer.Argument(
|
|
308
|
+
metavar="REFERENCE",
|
|
309
|
+
help="A Climb slug, slug@version, or public identifier.",
|
|
310
|
+
),
|
|
311
|
+
],
|
|
312
|
+
) -> None:
|
|
313
|
+
"""Show public policy, Campaign summary, data rights, and compatibility."""
|
|
314
|
+
context = cli_context(ctx)
|
|
315
|
+
|
|
316
|
+
def action() -> CommandResult[ClimbShowPayload]:
|
|
317
|
+
service = build_catalog_service(context)
|
|
318
|
+
try:
|
|
319
|
+
resolved = service.get_climb(reference)
|
|
320
|
+
except NotFoundError as error:
|
|
321
|
+
# The repository knows which Climbs exist; what to do about a name
|
|
322
|
+
# that is not one of them is the CLI's call. A catalog that is
|
|
323
|
+
# itself broken is a different failure and keeps its own repair.
|
|
324
|
+
if error.code == "climb_not_found":
|
|
325
|
+
error.next_actions = _unknown_climb_actions(error)
|
|
326
|
+
raise
|
|
327
|
+
summary = service.climb_summary(resolved)
|
|
328
|
+
|
|
329
|
+
return CommandResult(
|
|
330
|
+
data=_show_payload(resolved, summary),
|
|
331
|
+
warnings=_development_warnings([summary])
|
|
332
|
+
+ [
|
|
333
|
+
CliMessage(
|
|
334
|
+
level=MessageLevel.WARNING,
|
|
335
|
+
code=issue.code,
|
|
336
|
+
text=issue.message,
|
|
337
|
+
)
|
|
338
|
+
for issue in summary.compatibility.issues
|
|
339
|
+
],
|
|
340
|
+
next_actions=_show_next_actions(summary.compatibility),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
invoke_command(context, SHOW_COMMAND, action, render_data=_render_show)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def prepare_climb_command(
|
|
347
|
+
ctx: typer.Context,
|
|
348
|
+
reference: Annotated[
|
|
349
|
+
str,
|
|
350
|
+
typer.Argument(
|
|
351
|
+
metavar="REFERENCE",
|
|
352
|
+
help="A Climb slug, slug@version, or public identifier.",
|
|
353
|
+
),
|
|
354
|
+
],
|
|
355
|
+
skill: Annotated[
|
|
356
|
+
Path,
|
|
357
|
+
typer.Option(
|
|
358
|
+
"--skill",
|
|
359
|
+
metavar="PATH",
|
|
360
|
+
help="The candidate skill directory, or its SKILL.md.",
|
|
361
|
+
),
|
|
362
|
+
],
|
|
363
|
+
label: Annotated[
|
|
364
|
+
str | None,
|
|
365
|
+
typer.Option(
|
|
366
|
+
"--label",
|
|
367
|
+
metavar="LABEL",
|
|
368
|
+
help="What to call this candidate. Defaults to the directory name.",
|
|
369
|
+
),
|
|
370
|
+
] = None,
|
|
371
|
+
) -> None:
|
|
372
|
+
"""Resolve the Climb graph and prepare one candidate skill draft."""
|
|
373
|
+
context = cli_context(ctx)
|
|
374
|
+
|
|
375
|
+
def action() -> CommandResult[ClimbPreparePayload]:
|
|
376
|
+
service = build_preparation_service(context)
|
|
377
|
+
try:
|
|
378
|
+
prepared = service.prepare(
|
|
379
|
+
climb_reference=reference,
|
|
380
|
+
skill_path=skill,
|
|
381
|
+
candidate_label=label,
|
|
382
|
+
)
|
|
383
|
+
except NotFoundError as error:
|
|
384
|
+
if error.code == "climb_not_found":
|
|
385
|
+
error.next_actions = _unknown_climb_actions(error)
|
|
386
|
+
raise
|
|
387
|
+
except PrerequisiteError as error:
|
|
388
|
+
# An absent engine has a command that fixes it. An unsupported
|
|
389
|
+
# machine does not, and inventing one would waste the caller's
|
|
390
|
+
# time; the reason is already in the error message.
|
|
391
|
+
blocking = error.details.get("blocking_issues")
|
|
392
|
+
if isinstance(blocking, list) and "engine_not_installed" in blocking:
|
|
393
|
+
error.next_actions = [_install_engine()]
|
|
394
|
+
raise
|
|
395
|
+
|
|
396
|
+
payload = _prepare_payload(reference, prepared)
|
|
397
|
+
return CommandResult(
|
|
398
|
+
data=payload,
|
|
399
|
+
messages=[
|
|
400
|
+
CliMessage(
|
|
401
|
+
level=MessageLevel.INFO,
|
|
402
|
+
code="draft_prepared",
|
|
403
|
+
text=(
|
|
404
|
+
f"Prepared {payload.candidate_label} for "
|
|
405
|
+
f"{payload.climb_reference}. Nothing has run yet."
|
|
406
|
+
),
|
|
407
|
+
)
|
|
408
|
+
],
|
|
409
|
+
warnings=[
|
|
410
|
+
CliMessage(
|
|
411
|
+
level=MessageLevel.WARNING,
|
|
412
|
+
code="draft_warning",
|
|
413
|
+
text=warning,
|
|
414
|
+
)
|
|
415
|
+
for warning in payload.warnings
|
|
416
|
+
],
|
|
417
|
+
next_actions=[_start_draft(payload)],
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
invoke_command(context, PREPARE_COMMAND, action, render_data=_render_prepare)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def start_climb_command(
|
|
424
|
+
ctx: typer.Context,
|
|
425
|
+
draft_id: Annotated[
|
|
426
|
+
str,
|
|
427
|
+
typer.Argument(
|
|
428
|
+
metavar="DRAFT_ID",
|
|
429
|
+
help="The prepared draft to start.",
|
|
430
|
+
),
|
|
431
|
+
],
|
|
432
|
+
yes: Annotated[
|
|
433
|
+
bool,
|
|
434
|
+
typer.Option(
|
|
435
|
+
"--yes",
|
|
436
|
+
help=(
|
|
437
|
+
"Approve this run without being asked. For an operator running "
|
|
438
|
+
"Techtree where nobody can answer a prompt; it is never a "
|
|
439
|
+
"shortcut for an agent to take on a person's behalf."
|
|
440
|
+
),
|
|
441
|
+
),
|
|
442
|
+
] = False,
|
|
443
|
+
reviewed_on: Annotated[
|
|
444
|
+
ReviewSurface,
|
|
445
|
+
typer.Option(
|
|
446
|
+
"--reviewed-on",
|
|
447
|
+
help=(
|
|
448
|
+
"Where the person who approved this run answered. Pass "
|
|
449
|
+
"host-agent when the review was shown in a conversation and "
|
|
450
|
+
"confirmed there before the run was dispatched, so the run "
|
|
451
|
+
"records the surface the answer was actually given on. Like "
|
|
452
|
+
"--yes, and for the same reason, it states what a person "
|
|
453
|
+
"already did and is never a shortcut a model may take."
|
|
454
|
+
),
|
|
455
|
+
),
|
|
456
|
+
] = ReviewSurface.CLI,
|
|
457
|
+
) -> None:
|
|
458
|
+
"""Review a prepared draft, approve it, and start a detached run."""
|
|
459
|
+
context = cli_context(ctx)
|
|
460
|
+
|
|
461
|
+
def action() -> CommandResult[ClimbStartPayload]:
|
|
462
|
+
service = build_run_service(context)
|
|
463
|
+
store = DraftStore(context.paths)
|
|
464
|
+
draft = store.get(draft_id)
|
|
465
|
+
source = store.get_source(draft_id)
|
|
466
|
+
approval = approve_run(
|
|
467
|
+
context,
|
|
468
|
+
draft=draft,
|
|
469
|
+
campaign=source.campaign,
|
|
470
|
+
assume_yes=yes,
|
|
471
|
+
reviewed_on=reviewed_on,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
status = service.start(
|
|
475
|
+
draft_id=draft_id,
|
|
476
|
+
policy_acknowledgement=approval.acknowledgement,
|
|
477
|
+
approved_by=approval.actor,
|
|
478
|
+
)
|
|
479
|
+
request = service.request(status.state.run_id)
|
|
480
|
+
payload = _start_payload(draft, status, approval, request)
|
|
481
|
+
|
|
482
|
+
return CommandResult(
|
|
483
|
+
data=payload,
|
|
484
|
+
messages=[
|
|
485
|
+
CliMessage(
|
|
486
|
+
level=MessageLevel.INFO,
|
|
487
|
+
code="run_started",
|
|
488
|
+
text=(
|
|
489
|
+
f"Run {payload.run_id} is going. It continues whether "
|
|
490
|
+
"or not this command is still open."
|
|
491
|
+
),
|
|
492
|
+
)
|
|
493
|
+
],
|
|
494
|
+
warnings=_start_warnings(payload, source=source),
|
|
495
|
+
next_actions=[_watch_run(payload.run_id)],
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
invoke_command(context, START_COMMAND, action, render_data=_render_start)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@dataclass(frozen=True)
|
|
502
|
+
class RunApproval:
|
|
503
|
+
"""The answer a start was given, and who gave it."""
|
|
504
|
+
|
|
505
|
+
acknowledgement: PolicyAcknowledgement
|
|
506
|
+
actor: ApprovalActor
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
#: The one scientific claim the whole comparison rests on, said in the words a
|
|
510
|
+
#: reader can check it in. Decisions document 0019 section 3, statement 2.
|
|
511
|
+
ONLY_CHANGE_LINE: Final = "The Skill is the only scientific change."
|
|
512
|
+
|
|
513
|
+
#: What starting a run sends, and what a later choice would send. Decision 0013
|
|
514
|
+
#: section 1.4 fixes both halves of the privacy claim; they are two lines
|
|
515
|
+
#: because a reader meets them as two facts, and the model-calls line above is
|
|
516
|
+
#: what stops this one from being read as "nothing leaves this machine".
|
|
517
|
+
#:
|
|
518
|
+
#: This line used to say that Techtree does not upload the participant's
|
|
519
|
+
#: episodes, traces, receipts, proof bundles or Skill proposals, which was true
|
|
520
|
+
#: because there was nowhere to send them. Decisions 0038 built ``techtree
|
|
521
|
+
#: publish``, so the line says the two things that are true now: starting a run
|
|
522
|
+
#: sends none of it, because publishing is a separate act on a finished run;
|
|
523
|
+
#: and the episodes never travel even then, because they are not in the proof
|
|
524
|
+
#: directory at all.
|
|
525
|
+
#:
|
|
526
|
+
#: It is deliberately not phrased as "nothing is uploaded". A sweeping negative
|
|
527
|
+
#: is the sentence decision 0013 spent its length warning about, and stating
|
|
528
|
+
#: what publishing actually carries tells a reader more than denying that
|
|
529
|
+
#: anything does.
|
|
530
|
+
PUBLICATION_STEP_LINE: Final = (
|
|
531
|
+
"Publishing is a separate step, taken after a run finishes and only if you "
|
|
532
|
+
"choose to: what travels then is the run's proof — the signed report and "
|
|
533
|
+
"its receipts — and never the episodes."
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
#: What a DataPolicy's publication terms mean in this build, shown wherever
|
|
537
|
+
#: those terms are shown.
|
|
538
|
+
#:
|
|
539
|
+
#: A Climb's DataPolicy describes a result that has been published: it says
|
|
540
|
+
#: that entering requires releasing the candidate Skill and that the uplift
|
|
541
|
+
#: report is public. Read on its own, next to the raw-episode terms that
|
|
542
|
+
#: prohibit upload outright, that reads as a plan to publish somebody's Skill
|
|
543
|
+
#: and their numbers — and two readers stopped and refused to start a run over
|
|
544
|
+
#: exactly that.
|
|
545
|
+
#:
|
|
546
|
+
#: The answer used to be that nothing in this build could publish anything,
|
|
547
|
+
#: which was true while there was no command that could. Decisions 0038 built
|
|
548
|
+
#: one. What is still true, and is what those two readers actually needed, is
|
|
549
|
+
#: that publishing is a separate act on a finished run: starting one publishes
|
|
550
|
+
#: nothing at all, and a person who never runs ``techtree publish`` never sends
|
|
551
|
+
#: anything.
|
|
552
|
+
#:
|
|
553
|
+
#: The last clause is not decoration. Decision 0013 section 1.4: a sentence
|
|
554
|
+
#: about what stays here is read as a claim that nothing goes anywhere, and
|
|
555
|
+
#: model calls do.
|
|
556
|
+
PUBLICATION_TERMS_LINE: Final = (
|
|
557
|
+
"These are the terms this Climb sets for a published result. Nothing is "
|
|
558
|
+
"published unless you publish a finished run yourself, and what travels "
|
|
559
|
+
"then is the run's proof — the signed report and its receipts — and never "
|
|
560
|
+
"the episodes. Your Skill and your episodes stay on this machine, and "
|
|
561
|
+
"model calls still go to the model provider you configured."
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def review_lines(*, draft: SubmissionDraft, campaign: CampaignSpec) -> list[str]:
|
|
566
|
+
"""Return the five things a person weighs before a run starts.
|
|
567
|
+
|
|
568
|
+
Decisions document 0019 section 2 fixes the list and the order: how much
|
|
569
|
+
work this is, the most the Campaign declares it may cost, what is being
|
|
570
|
+
changed, where the model calls go, and what an upload would carry. Every
|
|
571
|
+
value
|
|
572
|
+
is read off the draft or the Campaign it was prepared against, so the
|
|
573
|
+
review describes this run and cannot describe a different one.
|
|
574
|
+
|
|
575
|
+
The cost line says what is actually done about the spend. Since decisions
|
|
576
|
+
document 0029 there is a real check before a run starts: the most the
|
|
577
|
+
comparison can cost under the Campaign's enforced per-episode limits is
|
|
578
|
+
computed, and a Campaign that could amount to more than its declared
|
|
579
|
+
maximum is refused instead of started. What there still is not is a meter —
|
|
580
|
+
nothing counts the spend while a run is under way and nothing ends a run
|
|
581
|
+
part-way through over it — so the line says what the check is, and
|
|
582
|
+
decision 0025 still forbids any wording that would leave a reader expecting
|
|
583
|
+
a running total or a mid-run cut-off.
|
|
584
|
+
"""
|
|
585
|
+
return [
|
|
586
|
+
f"This runs {draft.estimated_episodes} episodes: the same tasks once "
|
|
587
|
+
"for each side of the comparison.",
|
|
588
|
+
_cost_line(campaign),
|
|
589
|
+
ONLY_CHANGE_LINE,
|
|
590
|
+
f"Model calls go to {campaign.subject.model.provider}, under that "
|
|
591
|
+
"provider's policies.",
|
|
592
|
+
PUBLICATION_STEP_LINE,
|
|
593
|
+
]
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _cost_line(campaign: CampaignSpec) -> str:
|
|
597
|
+
"""Say what is checked about the spend before the run starts, and what is not.
|
|
598
|
+
|
|
599
|
+
The declared maximum stays a US-dollar figure, because that is what the
|
|
600
|
+
Campaign declares. What the sentence around it may not do is read as though
|
|
601
|
+
everybody gets a bill: the run spends model tokens on inference, and only a
|
|
602
|
+
provider that charges for tokens turns that into money.
|
|
603
|
+
"""
|
|
604
|
+
ceiling = campaign.budgets.maximum_usd
|
|
605
|
+
if ceiling is None:
|
|
606
|
+
return (
|
|
607
|
+
"This run spends model tokens on inference. This Campaign declares "
|
|
608
|
+
"no maximum, so there is no figure for "
|
|
609
|
+
"Techtree to hold it to. Each episode still has enforced turn, "
|
|
610
|
+
"token, and time limits. Nothing keeps a running total while the "
|
|
611
|
+
"run is under way and nothing ends it part-way through: a provider "
|
|
612
|
+
"that charges for tokens bills the episodes above to your own "
|
|
613
|
+
"account, and a model you run yourself sends no bill."
|
|
614
|
+
)
|
|
615
|
+
return (
|
|
616
|
+
"This run spends model tokens on inference. Before anything starts, "
|
|
617
|
+
"Techtree checks that this Campaign's enforced "
|
|
618
|
+
f"per-episode limits cannot add up past the ${ceiling:.2f} maximum it "
|
|
619
|
+
"declares, and refuses to run it if they could. Each "
|
|
620
|
+
"episode has enforced turn, token, and time limits. Nothing keeps a "
|
|
621
|
+
"running total while the run is under way and nothing ends it part-way "
|
|
622
|
+
"through: a provider that charges for tokens bills the episodes above "
|
|
623
|
+
"to your own account, and a model you run yourself sends no bill."
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def approve_run(
|
|
628
|
+
context: CliContext,
|
|
629
|
+
*,
|
|
630
|
+
draft: SubmissionDraft,
|
|
631
|
+
campaign: CampaignSpec,
|
|
632
|
+
assume_yes: bool,
|
|
633
|
+
reviewed_on: ReviewSurface = ReviewSurface.CLI,
|
|
634
|
+
) -> RunApproval:
|
|
635
|
+
"""Show the review, collect the answer, or refuse to start.
|
|
636
|
+
|
|
637
|
+
Somebody who passed ``--yes`` has answered already, and ``--reviewed-on``
|
|
638
|
+
says where. Otherwise a person is shown the review and the rights summary
|
|
639
|
+
and answers here; where nobody can be asked, the command stops and names
|
|
640
|
+
the flag rather than inventing an approval nobody gave.
|
|
641
|
+
"""
|
|
642
|
+
if assume_yes:
|
|
643
|
+
if reviewed_on is ReviewSurface.HOST_AGENT:
|
|
644
|
+
return _approved(draft, "host_agent_confirmation", "human_via_hermes")
|
|
645
|
+
return _approved(draft, "explicit_cli_review", "operator_via_flag")
|
|
646
|
+
|
|
647
|
+
if reviewed_on is not ReviewSurface.CLI:
|
|
648
|
+
# The answer is about to be given here, so a run that recorded it as
|
|
649
|
+
# given somewhere else would name a surface nobody used.
|
|
650
|
+
raise UsageError(
|
|
651
|
+
"--reviewed-on says where an approval was already given, so it "
|
|
652
|
+
"goes with --yes; without it the review is shown here and answered "
|
|
653
|
+
"here",
|
|
654
|
+
code=REVIEW_SURFACE_NOT_APPROVED,
|
|
655
|
+
details={"draft_id": draft.id, "reviewed_on": reviewed_on.value},
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
if context.no_input:
|
|
659
|
+
raise PolicyError(
|
|
660
|
+
"starting this draft accepts its data policy and spends the run it "
|
|
661
|
+
"describes, so somebody has to approve it. Nothing here can be "
|
|
662
|
+
"asked, so say so with --yes",
|
|
663
|
+
code=POLICY_ACCEPTANCE_REQUIRED,
|
|
664
|
+
details={
|
|
665
|
+
"draft_id": draft.id,
|
|
666
|
+
"data_policy_digest": draft.policy_acceptance.data_policy_digest,
|
|
667
|
+
},
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
console = human_console(no_color=context.no_color)
|
|
671
|
+
for line in review_lines(draft=draft, campaign=campaign):
|
|
672
|
+
console.print(line)
|
|
673
|
+
console.print()
|
|
674
|
+
console.print(draft.policy_acceptance.summary)
|
|
675
|
+
console.print(PUBLICATION_TERMS_LINE)
|
|
676
|
+
console.print()
|
|
677
|
+
if not confirmed("Start this run?"):
|
|
678
|
+
raise PolicyError(
|
|
679
|
+
"the run was not approved, so nothing was started",
|
|
680
|
+
code=POLICY_ACCEPTANCE_REQUIRED,
|
|
681
|
+
details={"draft_id": draft.id},
|
|
682
|
+
)
|
|
683
|
+
return _approved(draft, "explicit_cli_review", "human_via_cli")
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _approved(
|
|
687
|
+
draft: SubmissionDraft,
|
|
688
|
+
method: Literal["explicit_cli_review", "host_agent_confirmation"],
|
|
689
|
+
actor: ApprovalActor,
|
|
690
|
+
) -> RunApproval:
|
|
691
|
+
"""Return the acknowledgement and the actor one approval produced."""
|
|
692
|
+
return RunApproval(
|
|
693
|
+
acknowledgement=PolicyAcknowledgement(
|
|
694
|
+
data_policy_digest=draft.policy_acceptance.data_policy_digest,
|
|
695
|
+
method=method,
|
|
696
|
+
acknowledged_at=utc_now(),
|
|
697
|
+
),
|
|
698
|
+
actor=actor,
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _start_payload(
|
|
703
|
+
draft: SubmissionDraft,
|
|
704
|
+
status: RunStatus,
|
|
705
|
+
approval: RunApproval,
|
|
706
|
+
request: RunRequest,
|
|
707
|
+
) -> ClimbStartPayload:
|
|
708
|
+
"""Project the run that was just created, reading its own record for what it is.
|
|
709
|
+
|
|
710
|
+
``fake_executor`` is the run's executor and nothing else, read from the
|
|
711
|
+
request the start just wrote — the same source ``run status`` answers from,
|
|
712
|
+
so the two can never disagree about the same run.
|
|
713
|
+
"""
|
|
714
|
+
return ClimbStartPayload(
|
|
715
|
+
run_id=status.state.run_id,
|
|
716
|
+
draft_id=draft.id,
|
|
717
|
+
draft_digest=request.draft_digest,
|
|
718
|
+
phase=status.state.phase,
|
|
719
|
+
worker_pid=status.state.worker_pid,
|
|
720
|
+
campaign_spec_digest=draft.campaign_spec_digest,
|
|
721
|
+
data_policy_digest=draft.data_policy_digest,
|
|
722
|
+
policy_acknowledgement_method=approval.acknowledgement.method,
|
|
723
|
+
approved_by=approval.actor,
|
|
724
|
+
fake_executor=request.executor_kind == "fake",
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
# ---------------------------------------------------------------------------
|
|
729
|
+
# Messages, warnings, and next actions
|
|
730
|
+
# ---------------------------------------------------------------------------
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _available_summary(count: int) -> str:
|
|
734
|
+
if count == 1:
|
|
735
|
+
return "One Climb is available in this build."
|
|
736
|
+
return f"{count} Climbs are available in this build."
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def _development_warnings(summaries: list[ClimbSummary]) -> list[CliMessage]:
|
|
740
|
+
"""Warn once per development Climb that its results prove nothing."""
|
|
741
|
+
return [
|
|
742
|
+
CliMessage(
|
|
743
|
+
level=MessageLevel.WARNING,
|
|
744
|
+
code="development_climb",
|
|
745
|
+
text=(
|
|
746
|
+
f"{summary.reference} is a development Climb. Its results are "
|
|
747
|
+
"for trying the flow out and are not comparable evidence."
|
|
748
|
+
),
|
|
749
|
+
)
|
|
750
|
+
for summary in summaries
|
|
751
|
+
if summary.status == "development"
|
|
752
|
+
]
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _show_next_actions(compatibility: CompatibilityResult) -> list[NextAction]:
|
|
756
|
+
"""Offer the one step that moves this Climb forward on this machine."""
|
|
757
|
+
if not compatibility.host_supported:
|
|
758
|
+
# Nothing Techtree can run fixes the wrong machine, so nothing is
|
|
759
|
+
# offered. The reason is already in the compatibility warning.
|
|
760
|
+
return []
|
|
761
|
+
if compatibility.engine_status is EngineCompatibilityStatus.NOT_INSTALLED:
|
|
762
|
+
return [_install_engine()]
|
|
763
|
+
if compatibility.engine_status is EngineCompatibilityStatus.INSTALLED_UNVERIFIED:
|
|
764
|
+
return [_verify_engine()]
|
|
765
|
+
return [_get_starter_skill()]
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def _unknown_climb_actions(error: NotFoundError) -> list[NextAction]:
|
|
769
|
+
"""Offer the listing when there is one, and Doctor when there is not."""
|
|
770
|
+
available = error.details.get("available")
|
|
771
|
+
if isinstance(available, list) and available:
|
|
772
|
+
return [_browse_climbs()]
|
|
773
|
+
return [_check_environment()]
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def _browse_climbs() -> NextAction:
|
|
777
|
+
return NextAction(
|
|
778
|
+
id="list_climbs",
|
|
779
|
+
label="See which Climbs this build ships",
|
|
780
|
+
reason="A Climb is named by its slug, or by slug and version.",
|
|
781
|
+
cli=["techtree", "climb", "list"],
|
|
782
|
+
hermes_tool=None,
|
|
783
|
+
hermes_args=None,
|
|
784
|
+
requires_user_confirmation=False,
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _show_climb(reference: str) -> NextAction:
|
|
789
|
+
return NextAction(
|
|
790
|
+
id="show_climb",
|
|
791
|
+
label=f"Look at {reference} in detail",
|
|
792
|
+
reason="Shows what it measures, the data rights it carries, and "
|
|
793
|
+
"whether this machine can run it.",
|
|
794
|
+
cli=["techtree", "climb", "show", reference],
|
|
795
|
+
hermes_tool=None,
|
|
796
|
+
hermes_args=None,
|
|
797
|
+
requires_user_confirmation=False,
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _install_engine() -> NextAction:
|
|
802
|
+
return NextAction(
|
|
803
|
+
id="install_engine",
|
|
804
|
+
label="Install the evaluation engine",
|
|
805
|
+
reason="Preparing a submission for this Climb needs it.",
|
|
806
|
+
cli=["techtree", "engine", "install"],
|
|
807
|
+
hermes_tool=None,
|
|
808
|
+
hermes_args=None,
|
|
809
|
+
requires_user_confirmation=False,
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _verify_engine() -> NextAction:
|
|
814
|
+
return NextAction(
|
|
815
|
+
id="verify_engine",
|
|
816
|
+
label="Check that the installed evaluation engine is intact",
|
|
817
|
+
reason="A result is only worth as much as the engine that produced it.",
|
|
818
|
+
cli=["techtree", "engine", "verify"],
|
|
819
|
+
hermes_tool=None,
|
|
820
|
+
hermes_args=None,
|
|
821
|
+
requires_user_confirmation=False,
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def _get_starter_skill() -> NextAction:
|
|
826
|
+
return NextAction(
|
|
827
|
+
id="get_starter_skill",
|
|
828
|
+
label="Get the pinned starter Skill",
|
|
829
|
+
reason=(
|
|
830
|
+
"The starter Skill is the candidate used for the introductory "
|
|
831
|
+
"Climb, and its next step is the exact prepare command."
|
|
832
|
+
),
|
|
833
|
+
cli=["techtree", "skill", "starter"],
|
|
834
|
+
hermes_tool=None,
|
|
835
|
+
hermes_args=None,
|
|
836
|
+
requires_user_confirmation=False,
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def _start_draft(payload: ClimbPreparePayload) -> NextAction:
|
|
841
|
+
"""Offer the start, and say what answering it commits to.
|
|
842
|
+
|
|
843
|
+
The action names the draft and nothing else. What the run would do is shown
|
|
844
|
+
when the start is run, and answering it is what accepts the rights policy,
|
|
845
|
+
so this is marked as needing a person rather than carrying anything a
|
|
846
|
+
caller could pass instead of one.
|
|
847
|
+
"""
|
|
848
|
+
return NextAction(
|
|
849
|
+
id="start_climb",
|
|
850
|
+
label=f"Start {payload.candidate_label} on {payload.climb_reference}",
|
|
851
|
+
reason=(
|
|
852
|
+
f"Runs {payload.estimated_episodes} episodes. It shows you the "
|
|
853
|
+
"spending limit the Campaign declares and what this changes, and "
|
|
854
|
+
"starts only if you say yes."
|
|
855
|
+
),
|
|
856
|
+
cli=["techtree", "climb", "start", payload.draft_id],
|
|
857
|
+
hermes_tool=None,
|
|
858
|
+
hermes_args=None,
|
|
859
|
+
requires_user_confirmation=True,
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def _start_warnings(
|
|
864
|
+
payload: ClimbStartPayload, *, source: CampaignSource
|
|
865
|
+
) -> list[CliMessage]:
|
|
866
|
+
"""Say plainly, in both output modes, what this run is going to produce.
|
|
867
|
+
|
|
868
|
+
Two separate facts, each read off the run rather than stated here. Whether
|
|
869
|
+
a model is called at all is the executor the run's own request records.
|
|
870
|
+
Whether the report may be published is the Climb's proof grade. They are
|
|
871
|
+
independent: the Climb this build ships is a real evaluation that is paid
|
|
872
|
+
for and is still not publication eligible, and a single sentence that
|
|
873
|
+
assumed one from the other is how this surface came to tell people no
|
|
874
|
+
model would be called on the screen where they had just agreed to pay for
|
|
875
|
+
the calls.
|
|
876
|
+
"""
|
|
877
|
+
warnings: list[CliMessage] = []
|
|
878
|
+
|
|
879
|
+
if payload.fake_executor:
|
|
880
|
+
warnings.append(
|
|
881
|
+
CliMessage(
|
|
882
|
+
level=MessageLevel.WARNING,
|
|
883
|
+
code="fake_executor_run",
|
|
884
|
+
text=(
|
|
885
|
+
"No agent is evaluated and no model is called on this run. "
|
|
886
|
+
"The numbers in the report it produces are invented."
|
|
887
|
+
),
|
|
888
|
+
)
|
|
889
|
+
)
|
|
890
|
+
else:
|
|
891
|
+
warnings.append(
|
|
892
|
+
CliMessage(
|
|
893
|
+
level=MessageLevel.WARNING,
|
|
894
|
+
code="paid_evaluation_run",
|
|
895
|
+
text=(
|
|
896
|
+
"This run evaluates the agent for real and spends model "
|
|
897
|
+
"tokens on inference with "
|
|
898
|
+
f"{source.campaign.subject.model.provider}. If that "
|
|
899
|
+
"provider charges for tokens, what you pay is whatever it "
|
|
900
|
+
"charges; a model you run yourself sends no bill."
|
|
901
|
+
),
|
|
902
|
+
)
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
if source.climb is not None and (
|
|
906
|
+
source.climb.publication.proof_grade == "development_only"
|
|
907
|
+
):
|
|
908
|
+
warnings.append(
|
|
909
|
+
CliMessage(
|
|
910
|
+
level=MessageLevel.WARNING,
|
|
911
|
+
code="not_publication_eligible",
|
|
912
|
+
text=(
|
|
913
|
+
f"{climb_reference(source.climb)} is a development Climb. "
|
|
914
|
+
"Its report is not publication eligible, and its result is "
|
|
915
|
+
"not comparable evidence."
|
|
916
|
+
),
|
|
917
|
+
)
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
return warnings
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def _watch_run(run_id: str) -> NextAction:
|
|
924
|
+
return NextAction(
|
|
925
|
+
id="run_status",
|
|
926
|
+
label="Check how the run is going",
|
|
927
|
+
reason="The run continues after this command returns.",
|
|
928
|
+
cli=["techtree", "run", "status", run_id],
|
|
929
|
+
hermes_tool=None,
|
|
930
|
+
hermes_args=None,
|
|
931
|
+
requires_user_confirmation=False,
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def _check_environment() -> NextAction:
|
|
936
|
+
return NextAction(
|
|
937
|
+
id="check_environment",
|
|
938
|
+
label="Check that this machine is ready",
|
|
939
|
+
reason="Doctor reports what is installed, what is missing, and what "
|
|
940
|
+
"would block a run.",
|
|
941
|
+
cli=["techtree", "doctor"],
|
|
942
|
+
hermes_tool=None,
|
|
943
|
+
hermes_args=None,
|
|
944
|
+
requires_user_confirmation=False,
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
# ---------------------------------------------------------------------------
|
|
949
|
+
# Human rendering
|
|
950
|
+
# ---------------------------------------------------------------------------
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def _render_list(data: object, console: Console) -> None:
|
|
954
|
+
"""Print one row per Climb, or nothing when there are none."""
|
|
955
|
+
if not isinstance(data, list) or not data:
|
|
956
|
+
return
|
|
957
|
+
|
|
958
|
+
table = Table(box=None, pad_edge=False, padding=(0, 2))
|
|
959
|
+
table.add_column("Climb", no_wrap=True)
|
|
960
|
+
table.add_column("Title", overflow="fold")
|
|
961
|
+
table.add_column("Status", no_wrap=True)
|
|
962
|
+
table.add_column("Tasks", justify="right", no_wrap=True)
|
|
963
|
+
table.add_column("Runs here", no_wrap=True)
|
|
964
|
+
|
|
965
|
+
for summary in data:
|
|
966
|
+
table.add_row(
|
|
967
|
+
summary.reference,
|
|
968
|
+
summary.title,
|
|
969
|
+
summary.status,
|
|
970
|
+
str(summary.task_count),
|
|
971
|
+
"yes" if summary.compatibility.compatible else "no",
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
console.print(table)
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
def _render_show(data: object, console: Console) -> None:
|
|
978
|
+
"""Print everything a person needs before entering a Climb."""
|
|
979
|
+
if not isinstance(data, ClimbShowPayload):
|
|
980
|
+
return
|
|
981
|
+
summary = data.climb
|
|
982
|
+
runtime = data.subject_runtime
|
|
983
|
+
platforms = ", ".join(runtime.supported_platforms)
|
|
984
|
+
|
|
985
|
+
console.print(summary.title)
|
|
986
|
+
console.print(summary.summary)
|
|
987
|
+
console.print()
|
|
988
|
+
|
|
989
|
+
render_pairs(
|
|
990
|
+
[
|
|
991
|
+
("Climb", summary.reference),
|
|
992
|
+
("Status", summary.status),
|
|
993
|
+
("Purpose", phrase(summary.purpose)),
|
|
994
|
+
("Taskset", f"{summary.taskset_id} ({summary.task_count} tasks)"),
|
|
995
|
+
(
|
|
996
|
+
"Subject harness",
|
|
997
|
+
f"{summary.subject_harness} {summary.subject_harness_version}",
|
|
998
|
+
),
|
|
999
|
+
(
|
|
1000
|
+
"Subject model",
|
|
1001
|
+
f"{data.subject_model.provider}/{data.subject_model.model_id}",
|
|
1002
|
+
),
|
|
1003
|
+
("Subject runtime", f"{runtime.type} {runtime.image} ({platforms})"),
|
|
1004
|
+
("Primary reward", data.primary_reward),
|
|
1005
|
+
("Candidate ownership", data.candidate_skill_ownership),
|
|
1006
|
+
("Evaluated by", summary.evaluation_backend.value),
|
|
1007
|
+
("Allowed change", phrase(summary.mutation_kind)),
|
|
1008
|
+
("Proof grade", phrase(summary.proof_grade)),
|
|
1009
|
+
],
|
|
1010
|
+
console,
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
console.print()
|
|
1014
|
+
console.print("Data rights")
|
|
1015
|
+
render_pairs(
|
|
1016
|
+
[
|
|
1017
|
+
("Candidate skills", summary.candidate_skill_visibility),
|
|
1018
|
+
(
|
|
1019
|
+
"Public release",
|
|
1020
|
+
phrase(summary.data_policy.candidate_skill_public_release),
|
|
1021
|
+
),
|
|
1022
|
+
(
|
|
1023
|
+
"Raw episode upload",
|
|
1024
|
+
phrase(summary.data_policy.raw_episode_server_upload),
|
|
1025
|
+
),
|
|
1026
|
+
("Training use", phrase(summary.data_policy.raw_episode_training_use)),
|
|
1027
|
+
("Uplift report", phrase(summary.data_policy.uplift_report_visibility)),
|
|
1028
|
+
],
|
|
1029
|
+
console,
|
|
1030
|
+
)
|
|
1031
|
+
console.print(PUBLICATION_TERMS_LINE)
|
|
1032
|
+
|
|
1033
|
+
console.print()
|
|
1034
|
+
console.print("This machine")
|
|
1035
|
+
render_pairs(
|
|
1036
|
+
[
|
|
1037
|
+
("Host platform", summary.compatibility.host_platform),
|
|
1038
|
+
("Engine", phrase(summary.compatibility.engine_status.value)),
|
|
1039
|
+
("Runs here", "yes" if summary.compatibility.compatible else "no"),
|
|
1040
|
+
],
|
|
1041
|
+
console,
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
console.print()
|
|
1045
|
+
console.print("Technical IDs")
|
|
1046
|
+
render_pairs(
|
|
1047
|
+
[
|
|
1048
|
+
("Campaign digest", abbreviated_digest(summary.campaign_spec_digest)),
|
|
1049
|
+
("Data policy digest", abbreviated_digest(data.data_policy_digest)),
|
|
1050
|
+
],
|
|
1051
|
+
console,
|
|
1052
|
+
)
|
|
1053
|
+
console.print(
|
|
1054
|
+
" Shortened to fit. Run this command with --json for the complete digests."
|
|
1055
|
+
)
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
def _render_prepare(data: object, console: Console) -> None:
|
|
1059
|
+
"""Print everything spec PR6 §6.9 requires before a person confirms."""
|
|
1060
|
+
if not isinstance(data, ClimbPreparePayload):
|
|
1061
|
+
return
|
|
1062
|
+
|
|
1063
|
+
render_pairs(
|
|
1064
|
+
[
|
|
1065
|
+
("Draft", data.draft_id),
|
|
1066
|
+
("Climb", data.climb_reference),
|
|
1067
|
+
("Climb digest", data.climb_digest),
|
|
1068
|
+
("Campaign digest", data.campaign_spec_digest),
|
|
1069
|
+
("Data policy digest", data.data_policy_digest),
|
|
1070
|
+
("Candidate", data.candidate_label),
|
|
1071
|
+
("Skill content digest", data.skill_root_digest),
|
|
1072
|
+
],
|
|
1073
|
+
console,
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
console.print()
|
|
1077
|
+
console.print(f"Included files ({len(data.included_files)})")
|
|
1078
|
+
for path in data.included_files:
|
|
1079
|
+
console.print(f" {path}")
|
|
1080
|
+
|
|
1081
|
+
console.print()
|
|
1082
|
+
console.print("The comparison")
|
|
1083
|
+
render_pairs(
|
|
1084
|
+
[
|
|
1085
|
+
("Allowed difference", ", ".join(data.comparison.allowed_differences)),
|
|
1086
|
+
("Found difference", ", ".join(data.comparison.differences)),
|
|
1087
|
+
("Baseline skills", str(data.baseline_skill_count)),
|
|
1088
|
+
("Candidate skills", str(data.candidate_skill_count)),
|
|
1089
|
+
("Controlled", "yes" if data.comparison.controlled else "no"),
|
|
1090
|
+
("Estimated episodes", str(data.estimated_episodes)),
|
|
1091
|
+
("Proof grade", phrase(data.proof_grade)),
|
|
1092
|
+
],
|
|
1093
|
+
console,
|
|
1094
|
+
)
|
|
1095
|
+
|
|
1096
|
+
console.print()
|
|
1097
|
+
console.print("Data rights")
|
|
1098
|
+
render_pairs(
|
|
1099
|
+
[
|
|
1100
|
+
("Candidate ownership", data.candidate_ownership),
|
|
1101
|
+
("Public release", phrase(data.candidate_public_release)),
|
|
1102
|
+
("Raw episode upload", phrase(data.raw_episode_server_upload)),
|
|
1103
|
+
("Training use", phrase(data.raw_episode_training_use)),
|
|
1104
|
+
(
|
|
1105
|
+
"Acceptance",
|
|
1106
|
+
"required before starting"
|
|
1107
|
+
if data.policy_acceptance.required
|
|
1108
|
+
else "not required",
|
|
1109
|
+
),
|
|
1110
|
+
],
|
|
1111
|
+
console,
|
|
1112
|
+
)
|
|
1113
|
+
console.print(data.policy_acceptance.summary)
|
|
1114
|
+
console.print(PUBLICATION_TERMS_LINE)
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
def _render_start(data: object, console: Console) -> None:
|
|
1118
|
+
"""Print what was started and where it can be followed."""
|
|
1119
|
+
if not isinstance(data, ClimbStartPayload):
|
|
1120
|
+
return
|
|
1121
|
+
|
|
1122
|
+
render_pairs(
|
|
1123
|
+
[
|
|
1124
|
+
("Run", data.run_id),
|
|
1125
|
+
("Draft", data.draft_id),
|
|
1126
|
+
("Phase", data.phase.value),
|
|
1127
|
+
("Worker", "not started" if data.worker_pid is None else "running"),
|
|
1128
|
+
("Campaign digest", data.campaign_spec_digest),
|
|
1129
|
+
("Data policy digest", data.data_policy_digest),
|
|
1130
|
+
("Approved", phrase(data.policy_acknowledgement_method)),
|
|
1131
|
+
("Approved by", phrase(data.approved_by)),
|
|
1132
|
+
],
|
|
1133
|
+
console,
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
#: How much of a digest a person is shown when the point is recognition
|
|
1138
|
+
#: rather than comparison. Twelve hexadecimal characters distinguish every
|
|
1139
|
+
#: object a build could plausibly hold, and the full value is one --json away.
|
|
1140
|
+
ABBREVIATED_DIGEST_CHARACTERS: Final = 12
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def abbreviated_digest(digest: str) -> str:
|
|
1144
|
+
"""Shorten one digest for a terminal, visibly.
|
|
1145
|
+
|
|
1146
|
+
Decisions document 0007 R3 puts abbreviated digests in ``climb show``'s
|
|
1147
|
+
human output and complete ones in its JSON. The ellipsis is what keeps
|
|
1148
|
+
that honest: a shortened digest that looked whole would be copied into a
|
|
1149
|
+
comparison and quietly fail it.
|
|
1150
|
+
"""
|
|
1151
|
+
algorithm, _, hexadecimal = digest.partition(":")
|
|
1152
|
+
return f"{algorithm}:{hexadecimal[:ABBREVIATED_DIGEST_CHARACTERS]}…"
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def phrase(value: str) -> str:
|
|
1156
|
+
"""Render a protocol value as words rather than as an identifier.
|
|
1157
|
+
|
|
1158
|
+
The machine payload keeps the exact spelling; a person reading a terminal
|
|
1159
|
+
is better served by "required for climb" than by the same string with an
|
|
1160
|
+
underscore in it.
|
|
1161
|
+
"""
|
|
1162
|
+
return value.replace("_", " ")
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
# ---------------------------------------------------------------------------
|
|
1166
|
+
# Payloads
|
|
1167
|
+
# ---------------------------------------------------------------------------
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
def _show_payload(resolved: ResolvedClimb, summary: ClimbSummary) -> ClimbShowPayload:
|
|
1171
|
+
"""Return the summary plus the Campaign facts it has no field for."""
|
|
1172
|
+
subject = resolved.campaign.subject
|
|
1173
|
+
return ClimbShowPayload(
|
|
1174
|
+
climb=summary,
|
|
1175
|
+
data_policy_digest=resolved.data_policy_digest,
|
|
1176
|
+
subject_model=subject.model,
|
|
1177
|
+
subject_runtime=subject.runtime,
|
|
1178
|
+
primary_reward=resolved.campaign.scoring.primary_reward,
|
|
1179
|
+
candidate_skill_ownership=resolved.data_policy.candidate_skill.ownership,
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def _prepare_payload(reference: str, prepared: PreparedDraft) -> ClimbPreparePayload:
|
|
1184
|
+
"""Project a prepared draft into the response a caller acts on."""
|
|
1185
|
+
draft = prepared.draft
|
|
1186
|
+
source = prepared.source
|
|
1187
|
+
# ``climb prepare`` only ever prepares against a public Climb; the local
|
|
1188
|
+
# Climb-free flow is ``uplift prepare`` and returns its own payload.
|
|
1189
|
+
assert source.climb is not None and source.climb_digest is not None
|
|
1190
|
+
data_policy = source.data_policy
|
|
1191
|
+
comparison = prepared.manifest_comparison
|
|
1192
|
+
|
|
1193
|
+
return ClimbPreparePayload(
|
|
1194
|
+
draft_id=draft.id,
|
|
1195
|
+
draft_digest=prepared.draft_digest,
|
|
1196
|
+
climb_reference=climb_reference(source.climb),
|
|
1197
|
+
climb_digest=source.climb_digest,
|
|
1198
|
+
campaign_spec_digest=draft.campaign_spec_digest,
|
|
1199
|
+
data_policy_digest=draft.data_policy_digest,
|
|
1200
|
+
candidate_label=draft.skill_artifact.name,
|
|
1201
|
+
skill_root_digest=draft.skill_artifact.root_digest,
|
|
1202
|
+
included_files=list(draft.included_files),
|
|
1203
|
+
# Read off the Campaign rather than assumed. Decisions document 0019
|
|
1204
|
+
# section 1: a baseline is a role, and how many Skills it carries is
|
|
1205
|
+
# something the Campaign says, not something the count of a public
|
|
1206
|
+
# submission happens to be today.
|
|
1207
|
+
baseline_skill_count=len(source.campaign.subject.harness.skills),
|
|
1208
|
+
candidate_skill_count=1,
|
|
1209
|
+
estimated_episodes=draft.estimated_episodes,
|
|
1210
|
+
campaign_maximum_usd=source.campaign.budgets.maximum_usd,
|
|
1211
|
+
candidate_ownership=data_policy.candidate_skill.ownership,
|
|
1212
|
+
candidate_public_release=data_policy.candidate_skill.public_release,
|
|
1213
|
+
raw_episode_server_upload=data_policy.raw_episodes.server_upload,
|
|
1214
|
+
raw_episode_training_use=data_policy.raw_episodes.training_use,
|
|
1215
|
+
proof_grade=source.climb.publication.proof_grade,
|
|
1216
|
+
policy_acceptance=draft.policy_acceptance,
|
|
1217
|
+
comparison=PreparedComparison(
|
|
1218
|
+
controlled=comparison.controlled,
|
|
1219
|
+
differences=[difference.pointer for difference in comparison.differences],
|
|
1220
|
+
allowed_differences=list(comparison.allowed_differences),
|
|
1221
|
+
),
|
|
1222
|
+
warnings=list(draft.warnings),
|
|
1223
|
+
)
|