unique-sdk 2026.40.0.dev1__tar.gz → 2026.40.0.dev2__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 (91) hide show
  1. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/PKG-INFO +1 -1
  2. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/pyproject.toml +1 -1
  3. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/commands/cite_file.py +3 -2
  4. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/commands/mcp.py +11 -6
  5. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/commands/search.py +3 -2
  6. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/commands/web_search.py +4 -1
  7. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/state.py +2 -1
  8. unique_sdk-2026.40.0.dev2/unique_sdk/cli/workspace.py +79 -0
  9. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/README.md +0 -0
  10. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/__init__.py +0 -0
  11. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/_api_requestor.py +0 -0
  12. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/_api_resource.py +0 -0
  13. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/_api_version.py +0 -0
  14. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/_error.py +0 -0
  15. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/_http_client.py +0 -0
  16. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/_list_object.py +0 -0
  17. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/_object_classes.py +0 -0
  18. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/_request_options.py +0 -0
  19. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/_unique_object.py +0 -0
  20. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/_unique_ql.py +0 -0
  21. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/_unique_response.py +0 -0
  22. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/_util.py +0 -0
  23. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/_version.py +0 -0
  24. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/_webhook.py +0 -0
  25. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/__init__.py +0 -0
  26. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_acronyms.py +0 -0
  27. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_agentic_table.py +0 -0
  28. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_analytics_order.py +0 -0
  29. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_benchmarking.py +0 -0
  30. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_briefing.py +0 -0
  31. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_chat_completion.py +0 -0
  32. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_content.py +0 -0
  33. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_context_memory.py +0 -0
  34. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_elicitation.py +0 -0
  35. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_embedding.py +0 -0
  36. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_event.py +0 -0
  37. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_folder.py +0 -0
  38. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_group.py +0 -0
  39. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_integrated.py +0 -0
  40. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_llm_models.py +0 -0
  41. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_mcp.py +0 -0
  42. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_message.py +0 -0
  43. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_message_assessment.py +0 -0
  44. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_message_execution.py +0 -0
  45. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_message_log.py +0 -0
  46. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_message_tool.py +0 -0
  47. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_module.py +0 -0
  48. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_scheduled_task.py +0 -0
  49. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_search.py +0 -0
  50. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_search_string.py +0 -0
  51. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_short_term_memory.py +0 -0
  52. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_space.py +0 -0
  53. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_user.py +0 -0
  54. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/api_resources/_web_search.py +0 -0
  55. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/__init__.py +0 -0
  56. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/__main__.py +0 -0
  57. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/cli.py +0 -0
  58. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/commands/__init__.py +0 -0
  59. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
  60. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/commands/agentic_table.py +0 -0
  61. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/commands/agentic_table_write.py +0 -0
  62. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/commands/browser.py +0 -0
  63. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/commands/elicitation.py +0 -0
  64. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/commands/files.py +0 -0
  65. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/commands/folders.py +0 -0
  66. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/commands/navigation.py +0 -0
  67. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/commands/read.py +0 -0
  68. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
  69. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/commands/subagent.py +0 -0
  70. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/commands/web_search_config.py +0 -0
  71. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/config.py +0 -0
  72. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/formatting.py +0 -0
  73. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/identity.py +0 -0
  74. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/metadata_filter.py +0 -0
  75. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/shell.py +0 -0
  76. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/skills/unique-cli-agentic-table/SKILL.md +0 -0
  77. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
  78. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +0 -0
  79. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
  80. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
  81. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
  82. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
  83. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/skills/unique-cli-uploaded-search/SKILL.md +0 -0
  84. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
  85. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/utils/analytics_order_run.py +0 -0
  86. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/utils/benchmarking_run.py +0 -0
  87. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/utils/chat_history.py +0 -0
  88. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/utils/chat_in_space.py +0 -0
  89. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/utils/file_io.py +0 -0
  90. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/utils/sources.py +0 -0
  91. {unique_sdk-2026.40.0.dev1 → unique_sdk-2026.40.0.dev2}/unique_sdk/utils/token.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-sdk
3
- Version: 2026.40.0.dev1
3
+ Version: 2026.40.0.dev2
4
4
  Summary:
5
5
  Author: Martin Fadler, Konstantin Krauss, Andreas Hauri
6
6
  Author-email: Martin Fadler <martin.fadler@unique.ch>, Konstantin Krauss <konstantin@unique.ch>, Andreas Hauri <andreas@unique.ch>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_sdk"
3
- version = "2026.40.0.dev1"
3
+ version = "2026.40.0.dev2"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -13,6 +13,7 @@ from unique_sdk.cli.commands._citation_manifest import (
13
13
  )
14
14
  from unique_sdk.cli.commands.files import _resolve_content_id
15
15
  from unique_sdk.cli.state import ShellState
16
+ from unique_sdk.cli.workspace import manifest_path as workspace_manifest_path
16
17
 
17
18
  CITE_ERROR_PREFIX = "cite:"
18
19
 
@@ -153,7 +154,7 @@ def _resolve_content_id_with_manifest(
153
154
  )
154
155
  return name_or_id, state.resolve_content_title(name_or_id)
155
156
 
156
- manifest_path = Path.cwd() / _CHAT_FILES_MANIFEST
157
+ manifest_path = workspace_manifest_path(_CHAT_FILES_MANIFEST)
157
158
  if manifest_path.is_file():
158
159
  try:
159
160
  manifest: dict[str, str] = json.loads(
@@ -221,7 +222,7 @@ def cmd_cite_file(
221
222
  if not page_list:
222
223
  return f"{CITE_ERROR_PREFIX} invalid --pages value"
223
224
 
224
- refs_log_path = Path.cwd() / _FILE_REFS_LOG_RELATIVE_PATH
225
+ refs_log_path = workspace_manifest_path(_FILE_REFS_LOG_RELATIVE_PATH)
225
226
 
226
227
  try:
227
228
  with _locked_turn_refs_manifest(
@@ -21,6 +21,8 @@ from unique_sdk.cli.commands._citation_manifest import (
21
21
  )
22
22
  from unique_sdk.cli.formatting import format_mcp_response
23
23
  from unique_sdk.cli.state import ShellState
24
+ from unique_sdk.cli.workspace import manifest_path as workspace_manifest_path
25
+ from unique_sdk.cli.workspace import unique_dir as workspace_unique_dir
24
26
 
25
27
  _LOGGER = logging.getLogger(__name__)
26
28
 
@@ -643,7 +645,9 @@ def _annotate_mcp_results_for_citations(
643
645
  merge for title-less items only). Best-effort — returns ``[]`` on any failure
644
646
  (the tool result is unaffected; only the citation Sources block is skipped).
645
647
  """
646
- refs_log_path = refs_log_path or (Path.cwd() / _MCP_REFS_LOG_RELATIVE_PATH)
648
+ refs_log_path = refs_log_path or workspace_manifest_path(
649
+ _MCP_REFS_LOG_RELATIVE_PATH
650
+ )
647
651
  annotated: list[tuple[int, dict[str, Any]]] = []
648
652
  try:
649
653
  items = _extract_mcp_citation_items(
@@ -792,12 +796,13 @@ def _append_mcp_output_manifest(
792
796
  call when the write fails.
793
797
 
794
798
  ``output_path`` is the full path of the ``mcp-output.jsonl`` manifest. It
795
- defaults to ``Path.cwd() / .unique / mcp-output.jsonl`` so the CLI flow
796
- (where cwd is the agent workspace) is unchanged; callers that run outside
797
- the workspace cwd (e.g. the in-process tools-mode proxy) pass it explicitly.
799
+ defaults to ``<workspace root>/.unique/mcp-output.jsonl``; callers outside
800
+ the workspace (e.g. the in-process tools-mode proxy) pass it explicitly.
798
801
  """
799
802
  try:
800
- refs_log_path = output_path or (Path.cwd() / _MCP_OUTPUT_LOG_RELATIVE_PATH)
803
+ refs_log_path = output_path or workspace_manifest_path(
804
+ _MCP_OUTPUT_LOG_RELATIVE_PATH
805
+ )
801
806
  _append_turn_refs_manifest_entry(
802
807
  refs_log_path,
803
808
  {
@@ -945,7 +950,7 @@ def cmd_mcp(
945
950
  response,
946
951
  tool_name=name,
947
952
  server_name=server_name,
948
- unique_dir=Path.cwd() / ".unique",
953
+ unique_dir=workspace_unique_dir(),
949
954
  formatted_text=formatted,
950
955
  reference_mapping=state.mcp_tool_reference_mappings.get(name),
951
956
  )
@@ -26,6 +26,7 @@ from unique_sdk.cli.commands._citation_manifest import (
26
26
  _read_turn_refs_manifest,
27
27
  )
28
28
  from unique_sdk.cli.state import ShellState
29
+ from unique_sdk.cli.workspace import manifest_path as workspace_manifest_path
29
30
 
30
31
  DEFAULT_LIMIT = 200
31
32
 
@@ -283,7 +284,7 @@ def cmd_search(
283
284
  except (ValueError, unique_sdk.UniqueError) as e:
284
285
  return f"{SEARCH_ERROR_PREFIX} {e}"
285
286
 
286
- log_path = refs_log_path or (Path.cwd() / _REFS_LOG_RELATIVE_PATH)
287
+ log_path = refs_log_path or workspace_manifest_path(_REFS_LOG_RELATIVE_PATH)
287
288
  try:
288
289
  return _format_results_with_citations(results, refs_log_path=log_path)
289
290
  except UnsafeRefsLogPathError as exc:
@@ -346,7 +347,7 @@ def cmd_uploaded_search(
346
347
  except (ValueError, unique_sdk.UniqueError) as e:
347
348
  return f"{UPLOADED_SEARCH_ERROR_PREFIX} {e}"
348
349
 
349
- log_path = refs_log_path or (Path.cwd() / _REFS_LOG_RELATIVE_PATH)
350
+ log_path = refs_log_path or workspace_manifest_path(_REFS_LOG_RELATIVE_PATH)
350
351
  try:
351
352
  return _format_results_with_citations(results, refs_log_path=log_path)
352
353
  except UnsafeRefsLogPathError as exc:
@@ -35,6 +35,7 @@ from unique_sdk.cli.commands.web_search_config import (
35
35
  load_overrides,
36
36
  )
37
37
  from unique_sdk.cli.state import ShellState
38
+ from unique_sdk.cli.workspace import manifest_path as workspace_manifest_path
38
39
 
39
40
  _LOGGER = logging.getLogger(__name__)
40
41
 
@@ -110,7 +111,9 @@ def _annotate_web_results_for_citations(
110
111
  (UN-23356). The on-screen result still shows the error text so the
111
112
  agent can react to the failed fetch.
112
113
  """
113
- refs_log_path = refs_log_path or (Path.cwd() / _WEB_REFS_LOG_RELATIVE_PATH)
114
+ refs_log_path = refs_log_path or workspace_manifest_path(
115
+ _WEB_REFS_LOG_RELATIVE_PATH
116
+ )
114
117
  with _locked_turn_refs_manifest(
115
118
  refs_log_path, lock_filename=_WEB_REFS_LOCK_FILENAME
116
119
  ):
@@ -10,6 +10,7 @@ from typing import Any
10
10
  import unique_sdk
11
11
  from unique_sdk.cli.config import Config
12
12
  from unique_sdk.cli.metadata_filter import MetadataFilter
13
+ from unique_sdk.cli.workspace import manifest_path as workspace_manifest_path
13
14
 
14
15
  _SEARCH_CONFIG_FILENAME = ".unique-search.json"
15
16
  _UPLOADED_CONFIG_FILENAME = ".unique-uploaded.json"
@@ -120,7 +121,7 @@ def _load_chat_file_content_ids() -> set[str]:
120
121
  documents, so they must stay readable regardless of the per-message KB
121
122
  scope filter. Returns an empty set when the manifest is absent or invalid.
122
123
  """
123
- path = Path.cwd() / _CHAT_FILES_MANIFEST_PATH
124
+ path = workspace_manifest_path(_CHAT_FILES_MANIFEST_PATH)
124
125
  if not path.is_file():
125
126
  return set()
126
127
  try:
@@ -0,0 +1,79 @@
1
+ """Resolve the workspace root that per-turn ``.unique/`` manifests belong to.
2
+
3
+ The platform reads manifests back against a fixed root, and the agent moves
4
+ the shell's directory with ``cd``. Manifest paths anchor here; user-facing
5
+ output paths MUST NEVER, so ``cd out && unique-cli download X`` still lands
6
+ in ``out``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from pathlib import Path
13
+
14
+ from unique_sdk.cli.identity import TURN_IDENTITY_ENV_VAR
15
+
16
+ WORKSPACE_ENV_VAR = "UNIQUE_WORKSPACE_DIR"
17
+ UNIQUE_DIR_NAME = ".unique"
18
+
19
+
20
+ def workspace_root() -> Path:
21
+ """Directory the ``.unique`` manifest tree hangs off. Never raises.
22
+
23
+ ``$UNIQUE_WORKSPACE_DIR``, then the workspace holding
24
+ ``$UNIQUE_TURN_IDENTITY_FILE``, then ``$HOME``, then the cwd. NEVER add an
25
+ ancestor walk: a directory that once ran a turn keeps a stray
26
+ ``sub/.unique`` that would capture every later call made from inside it.
27
+ """
28
+ for candidate in (
29
+ _from_env(),
30
+ _from_turn_identity(),
31
+ _from_home(),
32
+ ):
33
+ if candidate is not None:
34
+ return candidate
35
+ return Path.cwd()
36
+
37
+
38
+ def unique_dir() -> Path:
39
+ """``<workspace root>/.unique``, not created here."""
40
+ return workspace_root() / UNIQUE_DIR_NAME
41
+
42
+
43
+ def manifest_path(relative: Path | str) -> Path:
44
+ """Anchor a manifest path such as ``.unique/web-refs.jsonl``."""
45
+ return workspace_root() / relative
46
+
47
+
48
+ def _is_dir(path: Path) -> bool:
49
+ try:
50
+ return path.is_dir()
51
+ except OSError:
52
+ return False
53
+
54
+
55
+ def _from_env() -> Path | None:
56
+ raw = os.environ.get(WORKSPACE_ENV_VAR, "").strip()
57
+ if not raw:
58
+ return None
59
+ candidate = Path(raw)
60
+ return candidate if _is_dir(candidate) else None
61
+
62
+
63
+ def _from_turn_identity() -> Path | None:
64
+ raw = os.environ.get(TURN_IDENTITY_ENV_VAR, "").strip()
65
+ if not raw:
66
+ return None
67
+ identity = Path(raw)
68
+ if identity.parent.name != UNIQUE_DIR_NAME:
69
+ return None
70
+ root = identity.parent.parent
71
+ return root if _is_dir(root) else None
72
+
73
+
74
+ def _from_home() -> Path | None:
75
+ raw = os.environ.get("HOME", "").strip()
76
+ if not raw:
77
+ return None
78
+ home = Path(raw)
79
+ return home if _is_dir(home / UNIQUE_DIR_NAME) else None