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,698 @@
1
+ """``techtree uplift context``, ``skill-source``, ``prepare``, ``start``.
2
+
3
+ Spec section 7.21. Four commands, and together they are the local half of the
4
+ improvement loop: say what a finished run showed, hand over the verified text
5
+ of the Skill it measured, set up the comparison between that Skill and a
6
+ revision of it, and start that comparison running.
7
+
8
+ ``skill-source`` exists so that verification stays inside Techtree. Decisions
9
+ document 0007 R2 has a host agent read the run-owned snapshot of SKILL.md and
10
+ re-verify it; this is the surface it reads through, so nothing outside has to
11
+ compose a path into a run directory or reimplement the digest check.
12
+
13
+ What is deliberately *not* here is the revision itself. No command in this file
14
+ calls a model, asks anything to write a Skill, or reads one that a model wrote
15
+ without a person putting it there. ``uplift context`` produces the material a
16
+ host agent reasons over and stops; ``uplift prepare`` takes a directory a
17
+ person names and stops. Spec section 7.3 puts the reasoning turn in WP9+, and
18
+ the seam between the two is exactly this file's boundary.
19
+
20
+ Two things are kept as they are for public submissions, because the second run
21
+ is a real run and nothing about it is smaller.
22
+
23
+ *The rights policy is accepted again.* A second evaluation is a second use of
24
+ the participant's material, so ``uplift start`` collects approval the same way
25
+ ``climb start`` does: the review of what the run would do is shown, the rights
26
+ summary is shown under it, and a person answers — or an operator who cannot be
27
+ asked passes ``--yes``. Spec section 7.20 requires it explicitly.
28
+
29
+ *The comparison is stated before it runs.* ``uplift prepare`` returns both
30
+ Skill digests, the derived Campaign digest, the estimated episodes, and the
31
+ DataPolicy digest, because that is what spec section 11.5 says a person must
32
+ see before approving a second run.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ from pathlib import Path
38
+ from typing import Annotated, Final, Literal
39
+
40
+ import typer
41
+ from pydantic import PositiveFloat
42
+ from rich.console import Console
43
+ from rich.table import Table
44
+
45
+ from techtree.canonical import canonical_json_bytes
46
+ from techtree.cli.commands.climb import (
47
+ PUBLICATION_TERMS_LINE,
48
+ ReviewSurface,
49
+ approve_run,
50
+ build_preparation_service,
51
+ phrase,
52
+ )
53
+ from techtree.cli.commands.run import build_run_service
54
+ from techtree.cli.context import CliContext, cli_context
55
+ from techtree.cli.invoke import CommandResult, invoke_command
56
+ from techtree.cli.output import render_pairs
57
+ from techtree.drafts.store import DraftStore
58
+ from techtree.fs import atomic_write_bytes, ensure_private_directory
59
+ from techtree.models.base import Digest, NonEmptyString, ProtocolModel
60
+ from techtree.models.cli import CliMessage, MessageLevel, NextAction
61
+ from techtree.models.run import RunPhase
62
+ from techtree.models.skill import PolicyAcceptanceRequirement
63
+ from techtree.paths import TechtreePaths
64
+ from techtree.runs.artifacts import RunArtifactStore
65
+ from techtree.runs.service import ApprovalActor
66
+ from techtree.skills.service import PreparedDraft
67
+ from techtree.uplift.context import SkillImprovementContext
68
+ from techtree.uplift.service import UpliftService
69
+
70
+ __all__ = [
71
+ "CONTEXT_COMMAND",
72
+ "PREPARE_COMMAND",
73
+ "SKILL_SOURCE_COMMAND",
74
+ "START_COMMAND",
75
+ "UpliftContextPayload",
76
+ "UpliftPreparePayload",
77
+ "UpliftSkillSourcePayload",
78
+ "UpliftStartPayload",
79
+ "build_uplift_service",
80
+ "context_uplift_command",
81
+ "prepare_uplift_command",
82
+ "skill_source_uplift_command",
83
+ "start_uplift_command",
84
+ ]
85
+
86
+ CONTEXT_COMMAND: Final = "uplift context"
87
+ SKILL_SOURCE_COMMAND: Final = "uplift skill-source"
88
+ PREPARE_COMMAND: Final = "uplift prepare"
89
+ START_COMMAND: Final = "uplift start"
90
+
91
+
92
+ class UpliftContextPayload(ProtocolModel):
93
+ """The sanitized context, and where the same bytes were written."""
94
+
95
+ context: SkillImprovementContext
96
+ relative_path: NonEmptyString
97
+
98
+
99
+ class UpliftSkillSourcePayload(ProtocolModel):
100
+ """The verified text of the Skill a run measured, with its fingerprints.
101
+
102
+ The digests are repeated beside the text on purpose. A caller that reads
103
+ this payload has everything the improvement context pins, so it can check
104
+ that the text it is about to send to a model belongs to the run the
105
+ context describes without resolving anything itself.
106
+
107
+ ``entrypoint_text`` is a plain string rather than a non-empty one. What
108
+ makes it legitimate is that it hashes to a digest the run measured, and a
109
+ response shape that second-guessed verified bytes would turn a Skill
110
+ somebody really ran into an internal error rather than into an answer.
111
+ """
112
+
113
+ source_run_id: NonEmptyString
114
+ skill_name: NonEmptyString
115
+ skill_root_digest: Digest
116
+ entrypoint_path: NonEmptyString
117
+ entrypoint_digest: Digest
118
+ entrypoint_size: int
119
+ entrypoint_text: str
120
+ file_count: int
121
+
122
+
123
+ class UpliftPreparePayload(ProtocolModel):
124
+ """What ``uplift prepare`` returns: the draft, and what it commits to.
125
+
126
+ It discloses what ``climb prepare`` discloses. Decisions document 0019
127
+ section 1 makes the two comparisons the same kind of thing — one Skill
128
+ replaced by another, one Skill added where there was none — so the screen a
129
+ person approves the second run from states the same facts as the screen
130
+ they approved the first from: how many Skills each side carries, and which
131
+ data rights govern what this run produces.
132
+ """
133
+
134
+ draft_id: NonEmptyString
135
+ draft_digest: Digest
136
+ source_run_id: NonEmptyString
137
+ campaign_spec_digest: Digest
138
+ data_policy_digest: Digest
139
+ baseline_skill_digest: Digest
140
+ candidate_skill_digest: Digest
141
+ candidate_label: NonEmptyString
142
+ included_files: list[NonEmptyString]
143
+ baseline_skill_count: int
144
+ candidate_skill_count: int
145
+ estimated_episodes: int
146
+ # Read off the Campaign this replacement was prepared against, so a caller
147
+ # that renders a review shows the maximum this second run is held to and
148
+ # never a figure from somewhere else. ``None`` is a Campaign that declares
149
+ # no maximum, and says so: there is then no figure to hold it to. Decision
150
+ # 0019 section 2 asks the same of both comparisons, and ``climb prepare``
151
+ # already carries it for the first.
152
+ campaign_maximum_usd: PositiveFloat | None
153
+ candidate_ownership: Literal["participant", "account", "shared"]
154
+ candidate_public_release: Literal[
155
+ "required_for_climb", "allowed", "prohibited", "consent_required"
156
+ ]
157
+ raw_episode_server_upload: Literal["allowed", "prohibited", "consent_required"]
158
+ raw_episode_training_use: Literal["allowed", "prohibited", "consent_required"]
159
+ controlled: bool
160
+ allowed_differences: list[NonEmptyString]
161
+ differences: list[NonEmptyString]
162
+ policy_acceptance: PolicyAcceptanceRequirement
163
+ warnings: list[NonEmptyString]
164
+
165
+
166
+ class UpliftStartPayload(ProtocolModel):
167
+ """What ``uplift start`` returns, as soon as the worker is running."""
168
+
169
+ run_id: NonEmptyString
170
+ draft_id: NonEmptyString
171
+ draft_digest: Digest
172
+ phase: RunPhase
173
+ worker_pid: int | None
174
+ campaign_spec_digest: Digest
175
+ data_policy_digest: Digest
176
+ policy_acknowledgement_method: Literal[
177
+ "explicit_cli_review",
178
+ "host_agent_confirmation",
179
+ ]
180
+ approved_by: ApprovalActor
181
+
182
+
183
+ def build_uplift_service(context: CliContext) -> UpliftService:
184
+ """Construct the service the improvement commands act through."""
185
+ return UpliftService(
186
+ paths=context.paths,
187
+ run_service=build_run_service(context),
188
+ artifact_store=RunArtifactStore(context.paths),
189
+ skill_service=build_preparation_service(context),
190
+ )
191
+
192
+
193
+ # ---------------------------------------------------------------------------
194
+ # context
195
+ # ---------------------------------------------------------------------------
196
+
197
+
198
+ def context_uplift_command(
199
+ ctx: typer.Context,
200
+ run_id: Annotated[
201
+ str,
202
+ typer.Argument(
203
+ metavar="RUN_ID",
204
+ help="The finished run to build improvement context from.",
205
+ ),
206
+ ],
207
+ ) -> None:
208
+ """Export the sanitized local context for one finished run."""
209
+ context = cli_context(ctx)
210
+
211
+ def action() -> CommandResult[UpliftContextPayload]:
212
+ improvement = build_uplift_service(context).improvement_context(run_id)
213
+ path = _write_context(context.paths, run_id, improvement)
214
+ payload = UpliftContextPayload(
215
+ context=improvement,
216
+ relative_path=path.relative_to(context.paths.run_dir(run_id)).as_posix(),
217
+ )
218
+ return CommandResult(
219
+ data=payload,
220
+ messages=[
221
+ CliMessage(
222
+ level=MessageLevel.INFO,
223
+ code="improvement_context_ready",
224
+ text=(
225
+ f"Built improvement context for {run_id} from "
226
+ f"{len(improvement.examples)} of this run's tasks."
227
+ ),
228
+ )
229
+ ],
230
+ warnings=[
231
+ CliMessage(
232
+ level=MessageLevel.WARNING,
233
+ code="improvement_context_is_not_proof",
234
+ text=(
235
+ "This context is working material, not evidence. It is "
236
+ "not signed, nothing verifies it, and nothing uploads it."
237
+ ),
238
+ )
239
+ ],
240
+ next_actions=[_prepare_replacement(run_id)],
241
+ )
242
+
243
+ invoke_command(context, CONTEXT_COMMAND, action, render_data=_render_context)
244
+
245
+
246
+ def _write_context(
247
+ paths: TechtreePaths, run_id: str, context: SkillImprovementContext
248
+ ) -> Path:
249
+ """Write the context beside the run it describes, replacing any older one.
250
+
251
+ A run can be revised from more than once, and the context is derived rather
252
+ than evidence, so it is a file that may be rewritten — unlike everything
253
+ the run's proof covers, which is written exactly once.
254
+ """
255
+ directory = paths.run_dir(run_id) / "improvement"
256
+ ensure_private_directory(directory)
257
+ path = directory / "context.json"
258
+ atomic_write_bytes(path, canonical_json_bytes(context), mode=0o600)
259
+ return path
260
+
261
+
262
+ # ---------------------------------------------------------------------------
263
+ # skill-source
264
+ # ---------------------------------------------------------------------------
265
+
266
+
267
+ def skill_source_uplift_command(
268
+ ctx: typer.Context,
269
+ run_id: Annotated[
270
+ str,
271
+ typer.Argument(
272
+ metavar="RUN_ID",
273
+ help="The finished run whose Skill text to read.",
274
+ ),
275
+ ],
276
+ ) -> None:
277
+ """Show the verified text of the Skill a finished run measured."""
278
+ context = cli_context(ctx)
279
+
280
+ def action() -> CommandResult[UpliftSkillSourcePayload]:
281
+ skill = build_uplift_service(context).verified_source_skill(run_id)
282
+ payload = UpliftSkillSourcePayload(
283
+ source_run_id=skill.run_id,
284
+ skill_name=skill.name,
285
+ skill_root_digest=skill.root_digest,
286
+ entrypoint_path=skill.entrypoint_path,
287
+ entrypoint_digest=skill.entrypoint_digest,
288
+ entrypoint_size=skill.entrypoint_size,
289
+ entrypoint_text=skill.entrypoint_text,
290
+ file_count=skill.file_count,
291
+ )
292
+ return CommandResult(
293
+ data=payload,
294
+ messages=[
295
+ CliMessage(
296
+ level=MessageLevel.INFO,
297
+ code="source_skill_verified",
298
+ text=(
299
+ f"This is run {run_id}'s own copy of "
300
+ f"{payload.entrypoint_path}, re-verified against the "
301
+ "Skill the run measured as it was read."
302
+ ),
303
+ )
304
+ ],
305
+ next_actions=[_prepare_replacement(run_id)],
306
+ )
307
+
308
+ invoke_command(
309
+ context, SKILL_SOURCE_COMMAND, action, render_data=_render_skill_source
310
+ )
311
+
312
+
313
+ # ---------------------------------------------------------------------------
314
+ # prepare
315
+ # ---------------------------------------------------------------------------
316
+
317
+
318
+ def prepare_uplift_command(
319
+ ctx: typer.Context,
320
+ from_run: Annotated[
321
+ str,
322
+ typer.Option(
323
+ "--from-run",
324
+ metavar="RUN_ID",
325
+ help="The finished run whose Skill becomes the new baseline.",
326
+ ),
327
+ ],
328
+ candidate_skill: Annotated[
329
+ Path,
330
+ typer.Option(
331
+ "--candidate-skill",
332
+ metavar="PATH",
333
+ help="The revised skill directory, or its SKILL.md.",
334
+ ),
335
+ ],
336
+ label: Annotated[
337
+ str | None,
338
+ typer.Option(
339
+ "--label",
340
+ metavar="LABEL",
341
+ help="What to call the revision. Defaults to the directory name.",
342
+ ),
343
+ ] = None,
344
+ ) -> None:
345
+ """Prepare a Skill v1 against Skill v2 comparison from a finished run."""
346
+ context = cli_context(ctx)
347
+
348
+ def action() -> CommandResult[UpliftPreparePayload]:
349
+ prepared = build_uplift_service(context).prepare_replacement(
350
+ source_run_id=from_run,
351
+ candidate_skill_path=candidate_skill,
352
+ candidate_label=label,
353
+ )
354
+ payload = _prepare_payload(from_run, prepared)
355
+ return CommandResult(
356
+ data=payload,
357
+ messages=[
358
+ CliMessage(
359
+ level=MessageLevel.INFO,
360
+ code="replacement_prepared",
361
+ text=(
362
+ f"Prepared {payload.candidate_label} against the Skill "
363
+ f"run {from_run} measured. Nothing has run yet."
364
+ ),
365
+ )
366
+ ],
367
+ warnings=[
368
+ CliMessage(
369
+ level=MessageLevel.WARNING,
370
+ code="draft_warning",
371
+ text=warning,
372
+ )
373
+ for warning in payload.warnings
374
+ ],
375
+ next_actions=[_start_replacement(payload)],
376
+ )
377
+
378
+ invoke_command(context, PREPARE_COMMAND, action, render_data=_render_prepare)
379
+
380
+
381
+ def _prepare_payload(
382
+ source_run_id: str, prepared: PreparedDraft
383
+ ) -> UpliftPreparePayload:
384
+ """Project a prepared replacement draft into the response a caller acts on."""
385
+ draft = prepared.draft
386
+ comparison = prepared.manifest_comparison
387
+ campaign = prepared.source.campaign
388
+ subject = campaign.subject
389
+ data_policy = prepared.source.data_policy
390
+ return UpliftPreparePayload(
391
+ draft_id=draft.id,
392
+ draft_digest=prepared.draft_digest,
393
+ source_run_id=source_run_id,
394
+ campaign_spec_digest=draft.campaign_spec_digest,
395
+ data_policy_digest=draft.data_policy_digest,
396
+ baseline_skill_digest=subject.harness.skills[0].digest,
397
+ candidate_skill_digest=draft.skill_artifact.root_digest,
398
+ candidate_label=draft.skill_artifact.name,
399
+ included_files=list(draft.included_files),
400
+ baseline_skill_count=len(subject.harness.skills),
401
+ candidate_skill_count=1,
402
+ estimated_episodes=draft.estimated_episodes,
403
+ campaign_maximum_usd=campaign.budgets.maximum_usd,
404
+ candidate_ownership=data_policy.candidate_skill.ownership,
405
+ candidate_public_release=data_policy.candidate_skill.public_release,
406
+ raw_episode_server_upload=data_policy.raw_episodes.server_upload,
407
+ raw_episode_training_use=data_policy.raw_episodes.training_use,
408
+ controlled=comparison.controlled,
409
+ allowed_differences=list(comparison.allowed_differences),
410
+ differences=[difference.pointer for difference in comparison.differences],
411
+ policy_acceptance=draft.policy_acceptance,
412
+ warnings=list(draft.warnings),
413
+ )
414
+
415
+
416
+ # ---------------------------------------------------------------------------
417
+ # start
418
+ # ---------------------------------------------------------------------------
419
+
420
+
421
+ def start_uplift_command(
422
+ ctx: typer.Context,
423
+ draft_id: Annotated[
424
+ str,
425
+ typer.Argument(metavar="DRAFT_ID", help="The prepared replacement to start."),
426
+ ],
427
+ yes: Annotated[
428
+ bool,
429
+ typer.Option(
430
+ "--yes",
431
+ help=(
432
+ "Approve this run without being asked. For an operator running "
433
+ "Techtree where nobody can answer a prompt; it is never a "
434
+ "shortcut for an agent to take on a person's behalf."
435
+ ),
436
+ ),
437
+ ] = False,
438
+ reviewed_on: Annotated[
439
+ ReviewSurface,
440
+ typer.Option(
441
+ "--reviewed-on",
442
+ help=(
443
+ "Where the person who approved this run answered. Pass "
444
+ "host-agent when the review was shown in a conversation and "
445
+ "confirmed there before the run was dispatched, so the run "
446
+ "records the surface the answer was actually given on. Like "
447
+ "--yes, and for the same reason, it states what a person "
448
+ "already did and is never a shortcut a model may take."
449
+ ),
450
+ ),
451
+ ] = ReviewSurface.CLI,
452
+ ) -> None:
453
+ """Review the prepared revision, approve it, and start the second run."""
454
+ context = cli_context(ctx)
455
+
456
+ def action() -> CommandResult[UpliftStartPayload]:
457
+ service = build_run_service(context)
458
+ store = DraftStore(context.paths)
459
+ draft = store.get(draft_id)
460
+ approval = approve_run(
461
+ context,
462
+ draft=draft,
463
+ campaign=store.get_source(draft_id).campaign,
464
+ assume_yes=yes,
465
+ reviewed_on=reviewed_on,
466
+ )
467
+ status = service.start(
468
+ draft_id=draft_id,
469
+ policy_acknowledgement=approval.acknowledgement,
470
+ approved_by=approval.actor,
471
+ )
472
+ request = service.request(status.state.run_id)
473
+ payload = UpliftStartPayload(
474
+ run_id=status.state.run_id,
475
+ draft_id=draft.id,
476
+ draft_digest=request.draft_digest,
477
+ phase=status.state.phase,
478
+ worker_pid=status.state.worker_pid,
479
+ campaign_spec_digest=draft.campaign_spec_digest,
480
+ data_policy_digest=draft.data_policy_digest,
481
+ policy_acknowledgement_method=approval.acknowledgement.method,
482
+ approved_by=approval.actor,
483
+ )
484
+ return CommandResult(
485
+ data=payload,
486
+ messages=[
487
+ CliMessage(
488
+ level=MessageLevel.INFO,
489
+ code="run_started",
490
+ text=(
491
+ f"Run {payload.run_id} is going. It continues whether "
492
+ "or not this command is still open."
493
+ ),
494
+ )
495
+ ],
496
+ next_actions=[_watch_run(payload.run_id)],
497
+ )
498
+
499
+ invoke_command(context, START_COMMAND, action, render_data=_render_start)
500
+
501
+
502
+ # ---------------------------------------------------------------------------
503
+ # Next actions
504
+ # ---------------------------------------------------------------------------
505
+
506
+
507
+ def _prepare_replacement(run_id: str) -> NextAction:
508
+ return NextAction(
509
+ id="prepare_replacement",
510
+ label="Compare a revised Skill against the one this run measured",
511
+ reason=(
512
+ "Point --candidate-skill at the revised skill directory. The "
513
+ "baseline is pinned to the Skill this run actually evaluated."
514
+ ),
515
+ cli=[
516
+ "techtree",
517
+ "uplift",
518
+ "prepare",
519
+ "--from-run",
520
+ run_id,
521
+ "--candidate-skill",
522
+ "PATH",
523
+ ],
524
+ hermes_tool=None,
525
+ hermes_args=None,
526
+ requires_user_confirmation=True,
527
+ )
528
+
529
+
530
+ def _start_replacement(payload: UpliftPreparePayload) -> NextAction:
531
+ return NextAction(
532
+ id="start_replacement",
533
+ label=f"Start {payload.candidate_label} against the previous Skill",
534
+ reason=(
535
+ f"Runs {payload.estimated_episodes} episodes. It shows you the "
536
+ "spending limit the Campaign declares and what this changes, and "
537
+ "starts only if you say yes."
538
+ ),
539
+ cli=["techtree", "uplift", "start", payload.draft_id],
540
+ hermes_tool=None,
541
+ hermes_args=None,
542
+ requires_user_confirmation=True,
543
+ )
544
+
545
+
546
+ def _watch_run(run_id: str) -> NextAction:
547
+ return NextAction(
548
+ id="run_status",
549
+ label="Check how the run is going",
550
+ reason="The run continues after this command returns.",
551
+ cli=["techtree", "run", "status", run_id],
552
+ hermes_tool=None,
553
+ hermes_args=None,
554
+ requires_user_confirmation=False,
555
+ )
556
+
557
+
558
+ # ---------------------------------------------------------------------------
559
+ # Human rendering
560
+ # ---------------------------------------------------------------------------
561
+
562
+
563
+ def _render_context(data: object, console: Console) -> None:
564
+ """Print a short summary and where the full context was written."""
565
+ if not isinstance(data, UpliftContextPayload):
566
+ return
567
+ improvement = data.context
568
+
569
+ render_pairs(
570
+ [
571
+ ("Run", improvement.source_run_id),
572
+ ("Skill being revised", improvement.parent_skill_digest),
573
+ ("Tasks included", str(len(improvement.examples))),
574
+ ("Written to", data.relative_path),
575
+ ],
576
+ console,
577
+ )
578
+ console.print()
579
+ console.print(improvement.objective)
580
+
581
+ console.print()
582
+ console.print("Tasks worth looking at")
583
+ table = Table(box=None, pad_edge=False, padding=(0, 2))
584
+ table.add_column("Task", no_wrap=True)
585
+ table.add_column("Outcome", no_wrap=True)
586
+ table.add_column("Reward", justify="right", no_wrap=True)
587
+ for example in improvement.examples:
588
+ table.add_row(
589
+ example.task_label,
590
+ example.outcome.replace("_", " "),
591
+ f"{example.reward:.3f}",
592
+ )
593
+ console.print(table)
594
+
595
+ console.print()
596
+ console.print("Not included")
597
+ for item in improvement.prohibited_material:
598
+ console.print(f" {item}")
599
+
600
+
601
+ def _render_skill_source(data: object, console: Console) -> None:
602
+ """Print what the text was verified against, then the text itself."""
603
+ if not isinstance(data, UpliftSkillSourcePayload):
604
+ return
605
+
606
+ render_pairs(
607
+ [
608
+ ("Run", data.source_run_id),
609
+ ("Skill", data.skill_name),
610
+ ("Skill content digest", data.skill_root_digest),
611
+ ("Entry file", data.entrypoint_path),
612
+ ("Entry file digest", data.entrypoint_digest),
613
+ ("Files in this Skill", str(data.file_count)),
614
+ ],
615
+ console,
616
+ )
617
+
618
+ console.print()
619
+ console.print(f"{data.entrypoint_path} ({data.entrypoint_size} bytes)")
620
+ console.print()
621
+ console.print(data.entrypoint_text)
622
+
623
+
624
+ def _render_prepare(data: object, console: Console) -> None:
625
+ """Print everything a person needs before approving a second run."""
626
+ if not isinstance(data, UpliftPreparePayload):
627
+ return
628
+
629
+ render_pairs(
630
+ [
631
+ ("Draft", data.draft_id),
632
+ ("From run", data.source_run_id),
633
+ ("Campaign digest", data.campaign_spec_digest),
634
+ ("Data policy digest", data.data_policy_digest),
635
+ ("Baseline skill", data.baseline_skill_digest),
636
+ ("Candidate skill", data.candidate_skill_digest),
637
+ ("Candidate", data.candidate_label),
638
+ ],
639
+ console,
640
+ )
641
+
642
+ console.print()
643
+ console.print(f"Included files ({len(data.included_files)})")
644
+ for path in data.included_files:
645
+ console.print(f" {path}")
646
+
647
+ console.print()
648
+ console.print("The comparison")
649
+ render_pairs(
650
+ [
651
+ ("Allowed difference", ", ".join(data.allowed_differences)),
652
+ ("Found difference", ", ".join(data.differences)),
653
+ ("Baseline skills", str(data.baseline_skill_count)),
654
+ ("Candidate skills", str(data.candidate_skill_count)),
655
+ ("Controlled", "yes" if data.controlled else "no"),
656
+ ("Estimated episodes", str(data.estimated_episodes)),
657
+ ],
658
+ console,
659
+ )
660
+
661
+ console.print()
662
+ console.print("Data rights")
663
+ render_pairs(
664
+ [
665
+ ("Candidate ownership", data.candidate_ownership),
666
+ ("Public release", phrase(data.candidate_public_release)),
667
+ ("Raw episode upload", phrase(data.raw_episode_server_upload)),
668
+ ("Training use", phrase(data.raw_episode_training_use)),
669
+ (
670
+ "Acceptance",
671
+ "required before starting"
672
+ if data.policy_acceptance.required
673
+ else "not required",
674
+ ),
675
+ ],
676
+ console,
677
+ )
678
+ console.print(data.policy_acceptance.summary)
679
+ console.print(PUBLICATION_TERMS_LINE)
680
+
681
+
682
+ def _render_start(data: object, console: Console) -> None:
683
+ """Print what was started and where it can be followed."""
684
+ if not isinstance(data, UpliftStartPayload):
685
+ return
686
+ render_pairs(
687
+ [
688
+ ("Run", data.run_id),
689
+ ("Draft", data.draft_id),
690
+ ("Phase", data.phase.value),
691
+ ("Worker", "not started" if data.worker_pid is None else "running"),
692
+ ("Campaign digest", data.campaign_spec_digest),
693
+ ("Data policy digest", data.data_policy_digest),
694
+ ("Approved", phrase(data.policy_acknowledgement_method)),
695
+ ("Approved by", phrase(data.approved_by)),
696
+ ],
697
+ console,
698
+ )