hivekit 0.8.0__tar.gz → 0.8.2__tar.gz

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 (58) hide show
  1. {hivekit-0.8.0 → hivekit-0.8.2}/PKG-INFO +2 -1
  2. {hivekit-0.8.0 → hivekit-0.8.2}/pyproject.toml +2 -1
  3. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/config.py +69 -73
  4. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/experiment.py +0 -2
  5. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/main.py +60 -4
  6. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/skills/hive-setup/SKILL.md +9 -1
  7. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/skills/hive-setup/references/configuration.md +20 -0
  8. hivekit-0.8.2/src/cli/update_check.py +136 -0
  9. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/archive.py +35 -2
  10. hivekit-0.8.2/src/cli/utils/attachments.py +225 -0
  11. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/config_paths.py +0 -8
  12. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/upload.py +19 -9
  13. {hivekit-0.8.0 → hivekit-0.8.2}/src/hivekit.egg-info/PKG-INFO +2 -1
  14. {hivekit-0.8.0 → hivekit-0.8.2}/src/hivekit.egg-info/SOURCES.txt +4 -0
  15. {hivekit-0.8.0 → hivekit-0.8.2}/src/hivekit.egg-info/requires.txt +1 -0
  16. hivekit-0.8.2/tests/test_attachments.py +171 -0
  17. {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_config.py +209 -116
  18. {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_experiment.py +8 -1
  19. {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_main.py +293 -0
  20. hivekit-0.8.2/tests/test_update_check.py +220 -0
  21. {hivekit-0.8.0 → hivekit-0.8.2}/LICENSE +0 -0
  22. {hivekit-0.8.0 → hivekit-0.8.2}/README.md +0 -0
  23. {hivekit-0.8.0 → hivekit-0.8.2}/setup.cfg +0 -0
  24. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/__init__.py +0 -0
  25. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/auth/__init__.py +0 -0
  26. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/auth/auth_utils.py +0 -0
  27. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/auth/credential_store.py +0 -0
  28. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/auth/login_page.py +0 -0
  29. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/auth/logo.svg +0 -0
  30. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/auth/oidc_flow.py +0 -0
  31. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/auth/session_manager.py +0 -0
  32. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/auth/token_revoker.py +0 -0
  33. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/completers.py +0 -0
  34. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/http_client.py +0 -0
  35. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/skills/hive-setup/references/gpu-hardware.md +0 -0
  36. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/skills/hive-setup/references/multi-evaluator.md +0 -0
  37. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/skills_install.py +0 -0
  38. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/__init__.py +0 -0
  39. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/config_sync.py +0 -0
  40. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/docker.py +0 -0
  41. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/logger.py +0 -0
  42. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/terminal.py +0 -0
  43. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/time.py +0 -0
  44. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/url_utils.py +0 -0
  45. {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/version.py +0 -0
  46. {hivekit-0.8.0 → hivekit-0.8.2}/src/hivekit.egg-info/dependency_links.txt +0 -0
  47. {hivekit-0.8.0 → hivekit-0.8.2}/src/hivekit.egg-info/entry_points.txt +0 -0
  48. {hivekit-0.8.0 → hivekit-0.8.2}/src/hivekit.egg-info/top_level.txt +0 -0
  49. {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_completers.py +0 -0
  50. {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_config_sync.py +0 -0
  51. {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_http_client.py +0 -0
  52. {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_login.py +0 -0
  53. {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_logout.py +0 -0
  54. {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_overrides.py +0 -0
  55. {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_push_image.py +0 -0
  56. {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_time_utils.py +0 -0
  57. {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_upload.py +0 -0
  58. {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hivekit
3
- Version: 0.8.0
3
+ Version: 0.8.2
4
4
  Summary: Toolkits for using hive agents, including CLI and SDK.
5
5
  License:
6
6
  Apache License
@@ -210,6 +210,7 @@ Requires-Python: >=3.12
210
210
  Description-Content-Type: text/markdown
211
211
  License-File: LICENSE
212
212
  Requires-Dist: PyYAML>=5.1
213
+ Requires-Dist: packaging>=21.0
213
214
  Requires-Dist: ruamel-yaml>=0.18
214
215
  Requires-Dist: pydantic>=1.8.2
215
216
  Requires-Dist: gitpython>=3.1.58
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hivekit"
7
- version = "0.8.0"
7
+ version = "0.8.2"
8
8
  description = "Toolkits for using hive agents, including CLI and SDK."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -12,6 +12,7 @@ license = { file = "LICENSE" }
12
12
 
13
13
  dependencies = [
14
14
  "PyYAML>=5.1",
15
+ "packaging>=21.0",
15
16
  "ruamel-yaml>=0.18",
16
17
  "pydantic>=1.8.2",
17
18
  "gitpython>=3.1.58",
@@ -11,6 +11,7 @@ import pydantic
11
11
  import yaml
12
12
  from kubernetes.utils import parse_quantity
13
13
  from pydantic import (
14
+ AnyHttpUrl,
14
15
  ConfigDict,
15
16
  Field,
16
17
  PrivateAttr,
@@ -19,7 +20,6 @@ from pydantic import (
19
20
  model_validator,
20
21
  )
21
22
 
22
- from cli.utils.config_paths import get_coordinator_overlay_path
23
23
  from cli.utils.logger import set_log_level
24
24
  from cli.utils.time import unique_suffix
25
25
 
@@ -256,11 +256,67 @@ class PromptConfig(BaseModel):
256
256
  default=None,
257
257
  description="A list of ideas which will be randomly sampled to inject into the Hive.",
258
258
  )
259
- arxiv_ids: Optional[list[str]] = Field(
259
+ attachments: Optional[list[str]] = Field(
260
260
  default=None,
261
- description="A list of arXiv IDs to provide as reference material to the Hive.",
261
+ description=(
262
+ "A list of local PDF files/directories or HTTP/HTTPS PDF URLs to provide as "
263
+ "reference material. Directories are searched recursively for files with a "
264
+ ".pdf extension (case-insensitive). Local relative paths resolve from the "
265
+ "directory where hive is run."
266
+ ),
262
267
  )
263
268
 
269
+ @model_validator(mode="before")
270
+ @classmethod
271
+ def migrate_arxiv_ids(cls, values):
272
+ """Convert legacy IDs into attachment URLs without mutating the input."""
273
+ if not isinstance(values, dict) or "arxiv_ids" not in values:
274
+ return values
275
+ values = dict(values)
276
+ # Preserve the legacy list's type coercion while folding it into attachments.
277
+ string_list = pydantic.TypeAdapter(Optional[list[str]], config=cls.model_config)
278
+ legacy = string_list.validate_python(values.pop("arxiv_ids"))
279
+ converted_urls = [f"https://arxiv.org/pdf/{arxiv_id}" for arxiv_id in legacy or []]
280
+ if converted_urls:
281
+ attachments = string_list.validate_python(
282
+ cls.validate_attachments_list(values.get("attachments"))
283
+ )
284
+ values["attachments"] = [*(attachments or []), *converted_urls]
285
+ warning = (
286
+ "prompt.arxiv_ids is deprecated and is converted to PDF URLs in "
287
+ "prompt.attachments. Use PDF URLs in prompt.attachments and remove "
288
+ "prompt.arxiv_ids to silence this warning."
289
+ )
290
+ if converted_urls:
291
+ warning += "\nConverted PDF URLs:\n" + "\n".join(f" - {url}" for url in converted_urls)
292
+ logger.warning(warning)
293
+ return values
294
+
295
+ @field_validator("attachments", mode="before")
296
+ @classmethod
297
+ def validate_attachments_list(cls, value):
298
+ if isinstance(value, str):
299
+ raise ValueError(
300
+ "Must be a YAML list, not a plain string. Example:\n attachments:\n - paper.pdf"
301
+ )
302
+ return value
303
+
304
+ @field_validator("attachments")
305
+ @classmethod
306
+ def validate_attachment_syntax(cls, value: Optional[list[str]]) -> Optional[list[str]]:
307
+ """Check attachment syntax without accessing local files or the network."""
308
+ if not value:
309
+ return value
310
+ http_url = pydantic.TypeAdapter(AnyHttpUrl)
311
+ for source in value:
312
+ if not source.strip():
313
+ raise ValueError("Attachment paths and URLs must not be empty")
314
+ # Recognize URL prefixes without treating colons in local filenames as schemes.
315
+ if re.match(r"(?i)^(?:[a-z][a-z0-9+.-]*://|https?:)", source.lstrip()):
316
+ http_url.validate_python(source, strict=True)
317
+ # Keep configured paths and URLs unchanged, including URL query strings.
318
+ return value
319
+
264
320
 
265
321
  def _extract_path(entry: str, strip_exclude: bool = False) -> str:
266
322
  """Reduce a file-list entry to the path it references."""
@@ -630,10 +686,9 @@ class HiveConfig(BaseModel):
630
686
  coordinator_image_tag: Optional[str] = Field(
631
687
  default=None,
632
688
  description="The image tag to run the coordinator at, e.g. 'v0.6.0'. Optional — when "
633
- "omitted, in a dev environment the tag recorded by `hivectl generate config` in "
634
- "~/.hive/coordinator.yaml is used if present. If that file or tag is absent, or in a "
635
- "production environment, the backend's default coordinator image is used. This field is "
636
- "ignored in production environments.",
689
+ "omitted, the backend's default coordinator image is used. Set this in the experiment "
690
+ "config or with a command-line override. This field is ignored outside development "
691
+ "environments.",
637
692
  )
638
693
  runtime: RuntimeConfig = Field(
639
694
  default_factory=RuntimeConfig, description="Runtime configuration for the experiment."
@@ -862,71 +917,12 @@ def _reconcile_num_sandboxes_override(cfg: dict, overridden_keys: set[tuple[str,
862
917
  del runtime[loser]
863
918
 
864
919
 
865
- def _recorded_coordinator_image_tag(overlay_path: str) -> Optional[str]:
866
- """Return the coordinator image tag recorded in the overlay file, if any.
867
-
868
- A broken overlay must not stop an experiment the user asked for, so anything
869
- unusable is warned about and ignored. An absent, empty or non-scalar tag
870
- records nothing; a bool is not a scalar here, being an int subclass.
871
- """
872
- if not os.path.exists(overlay_path):
873
- return None
874
- try:
875
- with open(overlay_path, "r") as file:
876
- overlay = yaml.safe_load(file)
877
- except (OSError, yaml.YAMLError) as e:
878
- logger.warning("Ignoring %s, which could not be read: %s", overlay_path, e)
879
- return None
880
- tag = overlay.get("coordinator_image_tag") if isinstance(overlay, dict) else None
881
- if tag is None or tag == "":
882
- return None
883
- if isinstance(tag, bool) or not isinstance(tag, (str, int, float)):
884
- logger.warning(
885
- "Ignoring coordinator_image_tag: %r in %s, which must be a string or number.",
886
- tag,
887
- overlay_path,
888
- )
889
- return None
890
- return str(tag)
891
-
892
-
893
- def _resolve_coordinator_image_tag(config_data: dict, overridden: bool) -> None:
894
- """Fall back to the coordinator image tag `hivectl generate config` recorded.
895
-
896
- A tag the experiment asked for wins, even an empty one asking for the backend
897
- default. Whichever tag is used is logged with where it came from, as is any
898
- recorded tag it discards, so which coordinator image runs is never a surprise.
899
- """
900
- overlay_path = get_coordinator_overlay_path()
901
- recorded = _recorded_coordinator_image_tag(overlay_path)
902
- if "coordinator_image_tag" not in config_data:
903
- if recorded is None:
904
- return
905
- config_data["coordinator_image_tag"] = recorded
906
- logger.warning(
907
- "Using coordinator_image_tag=%s recorded in %s by `hivectl generate config`; "
908
- "set coordinator_image_tag in the experiment config to override it.",
909
- recorded,
910
- overlay_path,
911
- )
912
- return
913
- asked_for = config_data["coordinator_image_tag"]
920
+ def _log_coordinator_image_tag(config_data: dict, overridden: bool) -> None:
921
+ """Report the tag explicitly selected by the experiment config or CLI."""
922
+ asked_for = config_data.get("coordinator_image_tag")
914
923
  source = "a command-line override" if overridden else "the experiment config"
915
- # str(): an unquoted digit-only tag loads as an int. None is not compared, or
916
- # an explicit null would match the tag "None" rather than discard it.
917
- if recorded is not None and (asked_for is None or str(asked_for) != recorded):
918
- logger.warning(
919
- "Using coordinator_image_tag=%s from %s, discarding coordinator_image_tag=%s "
920
- "recorded in %s.",
921
- asked_for,
922
- source,
923
- recorded,
924
- overlay_path,
925
- )
926
- elif asked_for is not None and asked_for != "":
924
+ if asked_for is not None and asked_for != "":
927
925
  logger.warning("Using coordinator_image_tag=%s from %s.", asked_for, source)
928
- # An empty tag with nothing to discard asks for the backend's default image,
929
- # exactly as setting no tag at all does, so there is nothing to report.
930
926
 
931
927
 
932
928
  def load_config(
@@ -936,8 +932,8 @@ def load_config(
936
932
  ) -> HiveConfig:
937
933
  """Load configuration from a YAML file.
938
934
 
939
- Pass ``coordinator=False`` when no coordinator will run, so the recorded
940
- coordinator image tag is neither filled in nor reported as being used.
935
+ Pass ``coordinator=False`` when no coordinator will run, so an explicit
936
+ coordinator image tag is not reported as being used.
941
937
  """
942
938
  with open(file_path, "r") as file:
943
939
  raw_text = file.read()
@@ -948,7 +944,7 @@ def load_config(
948
944
  overridden_keys = apply_overrides(config_data, overrides)
949
945
 
950
946
  if coordinator and isinstance(config_data, dict):
951
- _resolve_coordinator_image_tag(config_data, ("coordinator_image_tag",) in overridden_keys)
947
+ _log_coordinator_image_tag(config_data, ("coordinator_image_tag",) in overridden_keys)
952
948
 
953
949
  try:
954
950
  config = HiveConfig(**config_data)
@@ -124,8 +124,6 @@ def build_experiment_crd(config: HiveConfig) -> Dict[str, Any]:
124
124
  experiment["spec"]["prompt"]["context"] = config.prompt.context
125
125
  if config.prompt.ideas:
126
126
  experiment["spec"]["prompt"]["ideas"] = config.prompt.ideas
127
- if config.prompt.arxiv_ids:
128
- experiment["spec"]["prompt"]["arxivIds"] = config.prompt.arxiv_ids
129
127
  if config.prompt.enable_evolution:
130
128
  experiment["spec"]["prompt"]["enableEvolution"] = config.prompt.enable_evolution
131
129
 
@@ -11,6 +11,7 @@ import shlex
11
11
  import sys
12
12
  import tempfile
13
13
  import time
14
+ import traceback
14
15
  import warnings
15
16
  import webbrowser
16
17
 
@@ -38,12 +39,14 @@ from cli.http_client import (
38
39
  )
39
40
  from cli.skills_install import TARGETS as SKILL_TARGETS
40
41
  from cli.skills_install import skills_install
42
+ from cli.update_check import check_for_update, print_update_footer, warn_if_outdated
41
43
  from cli.utils import docker
42
44
  from cli.utils.archive import resolve_file_list
45
+ from cli.utils.attachments import AttachmentError, prepare_attachments
43
46
  from cli.utils.config_paths import get_config_dir, get_config_path, load_organization_id
44
47
  from cli.utils.config_sync import sync_base_image
45
48
  from cli.utils.time import humanize_time
46
- from cli.utils.upload import streaming_upload
49
+ from cli.utils.upload import streaming_upload, streaming_upload_entries
47
50
  from cli.utils.url_utils import (
48
51
  build_oidc_endpoints,
49
52
  derive_identity_base_url,
@@ -251,6 +254,18 @@ def _upload_source_if_any(
251
254
  return patch_id
252
255
 
253
256
 
257
+ def _upload_attachments_if_any(
258
+ client, attachments: list[str] | None, *, dry_run: bool = False
259
+ ) -> str | None:
260
+ """Prepare and upload PDFs, keeping downloads only for the upload's lifetime."""
261
+ with prepare_attachments(attachments or []) as entries:
262
+ if not entries or dry_run:
263
+ return None
264
+ resource_id = streaming_upload_entries(client, entries)
265
+ logger.debug("Resource upload complete. resource_id=%s", resource_id)
266
+ return resource_id
267
+
268
+
254
269
  @contextlib.contextmanager
255
270
  def _resolve_source(config):
256
271
  """Yield a local directory path for repo.source, cloning if remote."""
@@ -291,14 +306,24 @@ def create_experiment(args, overrides) -> None:
291
306
  )
292
307
  sys.exit(1)
293
308
 
309
+ attachments = config.prompt.attachments if config.prompt is not None else []
310
+ try:
311
+ resource_id = _upload_attachments_if_any(client, attachments, dry_run=args.dry_run)
312
+ except AttachmentError as e:
313
+ console.print(f"[bold red]Error:[/bold red] {e}", highlight=False)
314
+ sys.exit(1)
315
+
294
316
  experiment_crd = experiment.build_experiment_crd(config)
295
317
 
296
318
  patch_id = _upload_source_if_any(
297
319
  client, config, dry_run=args.dry_run, allow_missing=args.allow_missing_files
298
320
  )
299
- if patch_id:
300
- experiment_crd.setdefault("metadata", {}).setdefault("annotations", {})
301
- experiment_crd["metadata"]["annotations"]["hiverge.ai/patch-key"] = patch_id
321
+ if patch_id or resource_id:
322
+ annotations = experiment_crd.setdefault("metadata", {}).setdefault("annotations", {})
323
+ if patch_id:
324
+ annotations["hiverge.ai/patch-key"] = patch_id
325
+ if resource_id:
326
+ annotations["hiverge.ai/resource-key"] = resource_id
302
327
 
303
328
  if args.dry_run:
304
329
  console.print(
@@ -1626,6 +1651,27 @@ def main():
1626
1651
 
1627
1652
  argcomplete.autocomplete(parser)
1628
1653
  args, overrides = parser.parse_known_args()
1654
+
1655
+ status = check_for_update()
1656
+ if status is not None and status.outdated:
1657
+ warn_if_outdated(status)
1658
+
1659
+ try:
1660
+ _dispatch(parser, args, overrides)
1661
+ except SystemExit as exc:
1662
+ if status is not None and status.outdated and _is_failure_code(exc.code):
1663
+ print_update_footer(status)
1664
+ raise
1665
+ except Exception:
1666
+ if status is None or not status.outdated:
1667
+ raise
1668
+ traceback.print_exc()
1669
+ print_update_footer(status)
1670
+ raise SystemExit(1)
1671
+
1672
+
1673
+ def _dispatch(parser, args, overrides) -> None:
1674
+ """Route parsed args to the selected command, or print help if none."""
1629
1675
  if hasattr(args, "func"):
1630
1676
  if args.func in (create_experiment, shell_command):
1631
1677
  args.func(args, overrides)
@@ -1634,3 +1680,13 @@ def main():
1634
1680
  args.func(args)
1635
1681
  else:
1636
1682
  parser.print_help()
1683
+
1684
+
1685
+ def _is_failure_code(code: object) -> bool:
1686
+ """Whether a SystemExit code denotes a failure worth footing the notice on.
1687
+
1688
+ ``0``/``None`` are success; 130 is a Ctrl-C interrupt, not a hive failure.
1689
+ Any other value (a nonzero int, or a message string ``sys.exit`` prints) is
1690
+ a failure.
1691
+ """
1692
+ return code not in (0, None, 130)
@@ -307,7 +307,13 @@ This is the agents' primary steer. Write it as an onboarding document — imagin
307
307
 
308
308
  - **Describe the problem, not the solution.** Do NOT suggest optimization approaches, likely bottlenecks, or directions to explore — that biases the Hive and narrows its search.
309
309
  - **Never describe the current code.** This context is fixed while the target evolves every iteration, so anything about how the present implementation works is stale and misleading at once.
310
- - **Distill useful background from the repo's README or design docs** into this prose — agents get context only here, never as markdown files; `repo.additional_context` is for code files only.
310
+ - **Distill useful background from the repo's README or design docs** into this prose — `repo.additional_context` is for code files only. PDF references can be attached separately (see below).
311
+
312
+ ### PDF reference material
313
+
314
+ When the user supplies research papers, technical manuals, or other PDF references, add them under `prompt.attachments`. They provide reference material for agents and are not included in candidate code. Keep the task and metric in `prompt.context`, with a short note on which references are relevant.
315
+
316
+ Use local PDF files, local directories, or PDF URLs. For arXiv papers, use the `/pdf/` URL, such as `https://arxiv.org/pdf/2605.10327`. See [PDF attachments](references/configuration.md#pdf-attachments) for an example, path handling, and limits.
311
317
 
312
318
  ### Key field notes
313
319
 
@@ -336,6 +342,8 @@ These drive cost and feasibility, so **when unsure about sandbox count, runtime,
336
342
 
337
343
  After writing `hive.yaml`, you **must** run `hive create exp -c hive.yaml --dry-run` to confirm the config is valid. Fix any errors before moving on.
338
344
 
345
+ With `prompt.attachments`, the dry-run also checks local PDFs and downloads PDF URLs for validation. Relative attachment paths resolve from the directory where you run `hive`; use absolute paths if the launch directory may differ.
346
+
339
347
  Add `--allow-missing-files` only when a `target_code`/`additional_context` path is deliberately absent from the upload — a prebuilt image with `repo.files` narrowed, `source: null`, or a file `setup_script` generates. It's accepted by `hive create exp` and `hive shell` alike; when the whole source directory is uploaded, omit it so a mistyped path still errors.
340
348
 
341
349
 
@@ -22,6 +22,7 @@ Source: https://docs.hiverge.ai/gettingstarted/cli/configuration
22
22
  | `apiversion` | string | `v1alpha1` | Schema version |
23
23
  | `experiment_name` | string | required | Valid DNS label (`[a-z0-9-]`, max 51 chars, no leading `-`); trailing `-` appends a random 7-char unique suffix, so a name ending in `-` may have at most 43 chars before the `-` |
24
24
  | `coordinator_config_name` | string | `default-coordinator-config` | |
25
+ | `coordinator_image_tag` | string | `null` | Development environments only. Set explicitly here or via `coordinator_image_tag=<tag>` on the command line; omitted or empty uses the backend default. |
25
26
 
26
27
  ## `repo`
27
28
  | Field | Type | Default | Notes |
@@ -108,6 +109,23 @@ Optional — omit for defaults. Steers the agents' search.
108
109
  |---|---|---|---|
109
110
  | `context` | string | — | Experiment-specific guidance, multi-line |
110
111
  | `ideas` | list[string] | — | Distinct directions; one randomly sampled and injected each iteration |
112
+ | `attachments` | list[string] | `null` | Local PDF files, local directories, or PDF URLs. See [PDF attachments](#pdf-attachments). |
113
+
114
+ ### PDF attachments
115
+
116
+ PDFs provide reference material for agents and are not included in candidate code.
117
+
118
+ ```yaml
119
+ prompt:
120
+ attachments:
121
+ - papers/reference.pdf
122
+ - background/
123
+ - https://arxiv.org/pdf/2605.10327
124
+ ```
125
+
126
+ - Relative paths resolve from the directory where you run `hive`, not from the location of `hive.yaml` or `repo.source`. Directories are searched recursively for PDFs.
127
+ - Each PDF can be up to **20 MiB**. URLs must return a PDF; use a paper's PDF download URL rather than its abstract or landing page.
128
+ - The CLI validates attachments before creating the experiment. `hive create exp -c hive.yaml --dry-run` also checks attachments, including downloading PDF URLs.
111
129
 
112
130
  ## Evaluator output contract
113
131
  `evaluate.py` must print a JSON object on the **final line** of stdout, and must **exit 0 even when reporting a failure** — a non-zero exit code is a crashed evaluator, not a failed candidate. Report invalid candidates with `{"status": "failed", ...}` and exit cleanly.
@@ -181,6 +199,8 @@ prompt:
181
199
  ideas:
182
200
  - "Try batching database writes"
183
201
  - "Consider async I/O for network calls"
202
+ attachments:
203
+ - https://arxiv.org/pdf/2605.10327
184
204
  ```
185
205
 
186
206
  ## CLI cheat-sheet
@@ -0,0 +1,136 @@
1
+ # Copyright (C) 2026 Hiverge
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Compare the running hivekit against the latest release on PyPI.
6
+
7
+ The check runs once at CLI startup. It is best-effort and fail-closed: any
8
+ network error, timeout, missing metadata, or unparseable version yields
9
+ an unknown status and skips the checks.
10
+ """
11
+
12
+ import logging
13
+ import os
14
+ import sys
15
+ import tomllib
16
+ from dataclasses import dataclass
17
+
18
+ import requests
19
+ from packaging.version import InvalidVersion, Version
20
+
21
+ from cli.version import _direct_url, _editable_source_dir, _package_version
22
+
23
+ logger = logging.getLogger("hivekit")
24
+
25
+ _PYPI_URL = "https://pypi.org/pypi/hivekit/json"
26
+ _TIMEOUT_SECONDS = 3.0
27
+
28
+ # Any non-empty value skips the network round-trip (CI, air-gapped, tests).
29
+ _SKIP_ENV = "HIVE_NO_UPDATE_CHECK"
30
+
31
+
32
+ # Sits after the pinned install commands in both the warning and the footer.
33
+ _SKILLS_LINE = "then optionally update the Hive Skills with `hive skills install`."
34
+
35
+
36
+ def _upgrade_commands(latest: str) -> str:
37
+ """The two pinned install commands, one per indented line.
38
+
39
+ The version pin (``hivekit==<latest>``) is required: a bare ``--upgrade
40
+ hivekit`` is a no-op whenever the resolver already considers the environment
41
+ satisfied, so the exact target version must be named.
42
+ """
43
+ return (
44
+ f" uv pip install --upgrade hivekit=={latest}\n pip install --upgrade hivekit=={latest}"
45
+ )
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class UpdateStatus:
50
+ local: str
51
+ latest: str
52
+ outdated: bool
53
+
54
+
55
+ def _local_version() -> str:
56
+ """Version to compare against PyPI.
57
+
58
+ An editable install's recorded metadata is frozen at ``pip install -e``
59
+ time, so it lags the working tree. For a dev build the true version is
60
+ whatever the checkout's ``pyproject.toml`` declares now, so prefer that;
61
+ fall back to the recorded metadata for wheel/VCS/PyPI installs and whenever
62
+ the file cannot be read or declares no static version.
63
+ """
64
+ source_dir = _editable_source_dir(_direct_url())
65
+ if source_dir is not None:
66
+ try:
67
+ with open(os.path.join(source_dir, "pyproject.toml"), "rb") as f:
68
+ version = tomllib.load(f)["project"]["version"]
69
+ logger.debug("Editable checkout: version %s from pyproject.toml", version)
70
+ return version
71
+ except (OSError, tomllib.TOMLDecodeError, KeyError, TypeError):
72
+ logger.debug("Editable checkout: pyproject.toml version unreadable", exc_info=True)
73
+ return _package_version()
74
+
75
+
76
+ def _latest_version() -> str | None:
77
+ """Return the newest hivekit version on PyPI, or ``None`` if unavailable."""
78
+ try:
79
+ resp = requests.get(_PYPI_URL, timeout=_TIMEOUT_SECONDS)
80
+ resp.raise_for_status()
81
+ latest = resp.json()["info"]["version"]
82
+ logger.debug("PyPI reports latest hivekit version %s", latest)
83
+ return latest
84
+ except Exception:
85
+ logger.debug("PyPI version check failed", exc_info=True)
86
+ return None
87
+
88
+
89
+ def check_for_update() -> UpdateStatus | None:
90
+ """Compare the installed version against PyPI's latest.
91
+
92
+ Returns ``None`` when no comparison can be made (check disabled, network
93
+ failure, or either version unparseable); otherwise an ``UpdateStatus``.
94
+ """
95
+ if os.getenv(_SKIP_ENV):
96
+ return None
97
+
98
+ local = _local_version()
99
+ logger.debug("Installed hivekit version %s", local)
100
+ latest = _latest_version()
101
+ if latest is None:
102
+ return None
103
+
104
+ try:
105
+ outdated = Version(local) < Version(latest)
106
+ except InvalidVersion:
107
+ logger.debug("Unparseable version (local=%r latest=%r)", local, latest)
108
+ return None
109
+
110
+ logger.debug("Version check: local=%s latest=%s outdated=%s", local, latest, outdated)
111
+ return UpdateStatus(local=local, latest=latest, outdated=outdated)
112
+
113
+
114
+ def warn_if_outdated(status: UpdateStatus) -> None:
115
+ """Emit a startup WARNING when the running version is behind PyPI."""
116
+ logger.warning(
117
+ "hivekit %s is out of date; the latest release is %s. Update with:\n%s\n%s",
118
+ status.local,
119
+ status.latest,
120
+ _upgrade_commands(status.latest),
121
+ _SKILLS_LINE,
122
+ )
123
+
124
+
125
+ def print_update_footer(status: UpdateStatus) -> None:
126
+ """Restate the staleness as the final lines after a failed command.
127
+
128
+ Written to stderr so it trails the traceback or error a failure already
129
+ emitted there.
130
+ """
131
+ print(
132
+ f"\nThe current version of hivekit is {status.local} the latest version "
133
+ f"is {status.latest}. Please update with:\n{_upgrade_commands(status.latest)}\n"
134
+ f"{_SKILLS_LINE}",
135
+ file=sys.stderr,
136
+ )
@@ -1,23 +1,37 @@
1
1
  import logging
2
2
  import os
3
3
  import tarfile
4
+ from dataclasses import dataclass
4
5
  from pathlib import PurePath
5
6
 
6
7
  logger = logging.getLogger("hivekit")
7
8
 
8
9
 
10
+ @dataclass(frozen=True)
11
+ class ArchiveEntry:
12
+ """One physical file and the relative name it should have in an archive."""
13
+
14
+ source_path: str
15
+ archive_name: str
16
+
17
+
9
18
  def _is_glob(pattern: str) -> bool:
10
19
  return any(c in pattern for c in "*?[")
11
20
 
12
21
 
22
+ def _validate_relative_path(rel_path: str) -> None:
23
+ """Ensure a path is a safe relative archive name."""
24
+ if os.path.isabs(rel_path) or ".." in rel_path.split(os.sep):
25
+ raise ValueError(f"Unsafe relative path: '{rel_path}'")
26
+
27
+
13
28
  def _validate_path(base_dir: str, rel_path: str) -> None:
14
29
  """Ensure rel_path resolves within base_dir and is a safe archive name."""
30
+ _validate_relative_path(rel_path)
15
31
  real_base = os.path.realpath(base_dir)
16
32
  real_full = os.path.realpath(os.path.join(base_dir, rel_path))
17
33
  if not real_full.startswith(real_base + os.sep) and real_full != real_base:
18
34
  raise ValueError(f"Path escapes base directory: '{rel_path}'")
19
- if os.path.isabs(rel_path) or ".." in rel_path.split(os.sep):
20
- raise ValueError(f"Unsafe relative path: '{rel_path}'")
21
35
 
22
36
 
23
37
  def _walk_non_hidden(base_dir: str):
@@ -125,6 +139,25 @@ def add_to_tar(tar: tarfile.TarFile, base_dir: str, files: list[str]) -> None:
125
139
  raise ValueError(f"Path is neither file nor directory: '{entry}'")
126
140
 
127
141
 
142
+ def add_entries_to_tar(tar: tarfile.TarFile, entries: list[ArchiveEntry]) -> None:
143
+ """Add explicitly mapped regular files to an open tar archive."""
144
+ seen: set[str] = set()
145
+ for entry in entries:
146
+ _validate_relative_path(entry.archive_name)
147
+ if entry.archive_name in {"", "."}:
148
+ raise ValueError(f"Unsafe archive name: '{entry.archive_name}'")
149
+ if entry.archive_name in seen:
150
+ raise ValueError(f"Duplicate archive name: '{entry.archive_name}'")
151
+ seen.add(entry.archive_name)
152
+
153
+ source_path = os.path.expanduser(entry.source_path)
154
+ if os.path.islink(source_path):
155
+ raise ValueError(f"Symlinks are not supported: '{source_path}'")
156
+ if not os.path.isfile(source_path):
157
+ raise ValueError(f"Archive source is not a file: '{source_path}'")
158
+ tar.add(source_path, arcname=entry.archive_name, recursive=False)
159
+
160
+
128
161
  def _add_directory(tar: tarfile.TarFile, base_dir: str, rel_dir: str) -> None:
129
162
  """Recursively add a directory, rejecting symlinks at any level."""
130
163
  full_dir = os.path.join(base_dir, rel_dir)