nexus-coding-agent 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 (131) hide show
  1. nexus/__init__.py +4 -0
  2. nexus/application/__init__.py +2 -0
  3. nexus/application/approval_service.py +130 -0
  4. nexus/application/diff_service.py +177 -0
  5. nexus/application/event_publisher.py +180 -0
  6. nexus/application/execution_context.py +29 -0
  7. nexus/application/execution_ledger.py +126 -0
  8. nexus/application/model_call_context.py +29 -0
  9. nexus/application/observed_model_gateway.py +194 -0
  10. nexus/application/plan_approval_service.py +250 -0
  11. nexus/application/planning.py +668 -0
  12. nexus/application/runtime.py +624 -0
  13. nexus/application/session_service.py +411 -0
  14. nexus/application/structured_output.py +96 -0
  15. nexus/application/telemetry.py +396 -0
  16. nexus/application/telemetry_subscriber.py +216 -0
  17. nexus/application/tool_runtime.py +496 -0
  18. nexus/application/tracing_dispatcher.py +283 -0
  19. nexus/application/validation.py +293 -0
  20. nexus/config/__init__.py +17 -0
  21. nexus/config/loader.py +253 -0
  22. nexus/config/models.py +310 -0
  23. nexus/context/__init__.py +6 -0
  24. nexus/context/budget.py +24 -0
  25. nexus/context/builder.py +92 -0
  26. nexus/context/chunking.py +65 -0
  27. nexus/context/explorer.py +246 -0
  28. nexus/context/integration.py +63 -0
  29. nexus/context/manager.py +328 -0
  30. nexus/context/retrieval.py +261 -0
  31. nexus/domain/__init__.py +2 -0
  32. nexus/domain/agent_decision.py +113 -0
  33. nexus/domain/agent_state.py +49 -0
  34. nexus/domain/approvals.py +64 -0
  35. nexus/domain/context.py +108 -0
  36. nexus/domain/exploration.py +86 -0
  37. nexus/domain/mcp.py +51 -0
  38. nexus/domain/model.py +173 -0
  39. nexus/domain/observability.py +326 -0
  40. nexus/domain/persistence.py +122 -0
  41. nexus/domain/planning.py +329 -0
  42. nexus/domain/ports/__init__.py +61 -0
  43. nexus/domain/ports/agent_decision.py +9 -0
  44. nexus/domain/ports/approval_unit_of_work.py +27 -0
  45. nexus/domain/ports/checkpoint_provider.py +11 -0
  46. nexus/domain/ports/context.py +71 -0
  47. nexus/domain/ports/events.py +21 -0
  48. nexus/domain/ports/graph_runtime.py +30 -0
  49. nexus/domain/ports/mcp.py +23 -0
  50. nexus/domain/ports/model_gateway.py +21 -0
  51. nexus/domain/ports/model_input_budget.py +15 -0
  52. nexus/domain/ports/planning.py +62 -0
  53. nexus/domain/ports/repositories.py +54 -0
  54. nexus/domain/ports/repository_context.py +18 -0
  55. nexus/domain/ports/session_unit_of_work.py +33 -0
  56. nexus/domain/ports/skills.py +41 -0
  57. nexus/domain/ports/tooling.py +39 -0
  58. nexus/domain/ports/tracer.py +13 -0
  59. nexus/domain/ports/validation.py +32 -0
  60. nexus/domain/runtime_events.py +445 -0
  61. nexus/domain/skills.py +129 -0
  62. nexus/domain/tooling.py +163 -0
  63. nexus/domain/validation.py +161 -0
  64. nexus/errors/__init__.py +82 -0
  65. nexus/evaluation/__init__.py +7 -0
  66. nexus/evaluation/assertions.py +4 -0
  67. nexus/evaluation/evaluator.py +263 -0
  68. nexus/evaluation/fixtures.py +36 -0
  69. nexus/evaluation/harness.py +606 -0
  70. nexus/evaluation/loader.py +415 -0
  71. nexus/evaluation/metrics.py +27 -0
  72. nexus/evaluation/models.py +362 -0
  73. nexus/evaluation/report.py +43 -0
  74. nexus/evaluation/runner.py +480 -0
  75. nexus/infrastructure/__init__.py +2 -0
  76. nexus/infrastructure/asyncio_compat.py +13 -0
  77. nexus/infrastructure/bootstrap/__init__.py +17 -0
  78. nexus/infrastructure/bootstrap/composition.py +663 -0
  79. nexus/infrastructure/checkpoint/__init__.py +5 -0
  80. nexus/infrastructure/checkpoint/postgres.py +62 -0
  81. nexus/infrastructure/database.py +39 -0
  82. nexus/infrastructure/embedding.py +61 -0
  83. nexus/infrastructure/graph/__init__.py +6 -0
  84. nexus/infrastructure/graph/day4_runtime.py +966 -0
  85. nexus/infrastructure/graph/langgraph_runtime.py +172 -0
  86. nexus/infrastructure/mcp/__init__.py +5 -0
  87. nexus/infrastructure/mcp/manager.py +380 -0
  88. nexus/infrastructure/model_gateway/__init__.py +6 -0
  89. nexus/infrastructure/model_gateway/openai_compatible.py +345 -0
  90. nexus/infrastructure/observability/__init__.py +7 -0
  91. nexus/infrastructure/observability/console.py +31 -0
  92. nexus/infrastructure/observability/langsmith.py +236 -0
  93. nexus/infrastructure/observability/structured_logger.py +70 -0
  94. nexus/infrastructure/persistence/__init__.py +13 -0
  95. nexus/infrastructure/persistence/approval_unit_of_work.py +74 -0
  96. nexus/infrastructure/persistence/models.py +146 -0
  97. nexus/infrastructure/persistence/repositories.py +326 -0
  98. nexus/infrastructure/persistence/unit_of_work.py +82 -0
  99. nexus/infrastructure/repository_files.py +117 -0
  100. nexus/infrastructure/sandbox/__init__.py +5 -0
  101. nexus/infrastructure/sandbox/local_process.py +353 -0
  102. nexus/infrastructure/semantic.py +276 -0
  103. nexus/infrastructure/semantic_models.py +56 -0
  104. nexus/interfaces/__init__.py +2 -0
  105. nexus/interfaces/cli/__init__.py +2 -0
  106. nexus/interfaces/cli/__main__.py +6 -0
  107. nexus/interfaces/cli/app.py +266 -0
  108. nexus/interfaces/cli/renderer.py +118 -0
  109. nexus/security/__init__.py +14 -0
  110. nexus/security/approval_policies.py +73 -0
  111. nexus/security/command_policy.py +117 -0
  112. nexus/security/executables.py +107 -0
  113. nexus/security/git_commands.py +65 -0
  114. nexus/security/workspace.py +93 -0
  115. nexus/skills/__init__.py +7 -0
  116. nexus/skills/builtin/__init__.py +1 -0
  117. nexus/skills/builtin/debug-python/SKILL.md +26 -0
  118. nexus/skills/builtin/review-repository/SKILL.md +26 -0
  119. nexus/skills/builtin/write-tests/SKILL.md +26 -0
  120. nexus/skills/loader.py +439 -0
  121. nexus/skills/registry.py +146 -0
  122. nexus/skills/selector.py +152 -0
  123. nexus/tools/__init__.py +6 -0
  124. nexus/tools/editing.py +312 -0
  125. nexus/tools/mcp.py +160 -0
  126. nexus/tools/native.py +654 -0
  127. nexus/tools/registry.py +27 -0
  128. nexus_coding_agent-0.1.0.dist-info/METADATA +142 -0
  129. nexus_coding_agent-0.1.0.dist-info/RECORD +131 -0
  130. nexus_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  131. nexus_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
nexus/skills/loader.py ADDED
@@ -0,0 +1,439 @@
1
+ """Bounded metadata-first loader for local and packaged Skill documents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import errno
6
+ import hashlib
7
+ import io
8
+ import tomllib
9
+ import zipfile
10
+ from contextvars import ContextVar
11
+ from dataclasses import dataclass
12
+ from importlib.resources import files
13
+ from importlib.resources.abc import Traversable
14
+ from pathlib import Path
15
+ from typing import IO
16
+
17
+ from nexus.domain.skills import SkillLocation, SkillMetadata, SkillSource
18
+ from nexus.errors import ContextError
19
+
20
+ MAX_SKILL_FILE_BYTES = 262_144
21
+ MAX_FRONT_MATTER_BYTES = 16_384
22
+ _REQUIRED_KEYS = {"id", "name", "description", "usage_scenario", "version"}
23
+ _ALLOWED_KEYS = {*_REQUIRED_KEYS, "priority"}
24
+ _REQUIRED_HEADINGS = (
25
+ "## Execution Principles",
26
+ "## Recommended Tools",
27
+ "## Workflow",
28
+ "## Constraints",
29
+ )
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class _Snapshot:
34
+ size: int
35
+ modified_ns: int | None
36
+ front_hash: str
37
+
38
+
39
+ class _VersionInvalid(ValueError):
40
+ pass
41
+
42
+
43
+ class FileSkillLoader:
44
+ def __init__(
45
+ self,
46
+ repository_root: Path,
47
+ user_root: Path,
48
+ *,
49
+ builtin_package: str = "nexus.skills.builtin",
50
+ ) -> None:
51
+ self._roots = {
52
+ SkillSource.REPOSITORY: repository_root,
53
+ SkillSource.USER_GLOBAL: user_root,
54
+ }
55
+ self._builtin_package = builtin_package
56
+ self._snapshots: ContextVar[
57
+ dict[int, tuple[SkillMetadata, _Snapshot]] | None
58
+ ] = ContextVar("skill_loader_snapshots", default=None)
59
+
60
+ async def load_metadata(self, location: SkillLocation) -> SkillMetadata:
61
+ try:
62
+ before = self._document_state(location)
63
+ with self._open(location) as document:
64
+ front_bytes = _read_front_matter(document)
65
+ after = self._document_state(location)
66
+ if before != after:
67
+ raise ValueError("Skill document changed during metadata scan.")
68
+ size, modified_ns = after
69
+ metadata = _metadata(location, front_bytes)
70
+ except ContextError:
71
+ raise
72
+ except _VersionInvalid as exc:
73
+ raise ContextError(
74
+ f"Skill version is invalid for {location.source.value}:"
75
+ f"{location.relative_path}.",
76
+ code="SKILL_VERSION_INVALID",
77
+ ) from exc
78
+ except (OSError, UnicodeDecodeError, tomllib.TOMLDecodeError, ValueError) as exc:
79
+ raise ContextError(
80
+ f"Skill metadata is invalid for {location.source.value}:"
81
+ f"{location.relative_path}.",
82
+ code="SKILL_METADATA_INVALID",
83
+ ) from exc
84
+ snapshot = _Snapshot(size, modified_ns, hashlib.sha256(front_bytes).hexdigest())
85
+ snapshots = dict(self._snapshots.get() or {})
86
+ snapshots[id(metadata)] = (metadata, snapshot)
87
+ self._snapshots.set(snapshots)
88
+ return metadata
89
+
90
+ async def load_body(self, metadata: SkillMetadata) -> str:
91
+ snapshots = self._snapshots.get()
92
+ stored = None if snapshots is None else snapshots.get(id(metadata))
93
+ if stored is None or stored[0] is not metadata:
94
+ raise ContextError(
95
+ "Selected Skill metadata did not originate from the current metadata scan.",
96
+ code="SKILL_BODY_LOAD_FAILED",
97
+ )
98
+ snapshot = stored[1]
99
+ try:
100
+ size, modified_ns = self._document_state(metadata.location, selected=True)
101
+ if size != snapshot.size or modified_ns != snapshot.modified_ns:
102
+ raise ContextError(
103
+ "Selected Skill changed after metadata selection.",
104
+ code="SKILL_BODY_LOAD_FAILED",
105
+ )
106
+ with self._open(metadata.location, selected=True) as document:
107
+ content = document.read(MAX_SKILL_FILE_BYTES + 1)
108
+ if len(content) > MAX_SKILL_FILE_BYTES:
109
+ raise ContextError(
110
+ "Selected Skill exceeds the fixed document size limit.",
111
+ code="SKILL_BODY_LOAD_FAILED",
112
+ )
113
+ if self._document_state(metadata.location, selected=True) != (size, modified_ns):
114
+ raise ContextError(
115
+ "Selected Skill changed while its body was being loaded.",
116
+ code="SKILL_BODY_LOAD_FAILED",
117
+ )
118
+ stream = io.BytesIO(content)
119
+ front_bytes = _read_front_matter(stream)
120
+ if hashlib.sha256(front_bytes).hexdigest() != snapshot.front_hash:
121
+ raise ContextError(
122
+ "Selected Skill front matter changed after metadata selection.",
123
+ code="SKILL_BODY_LOAD_FAILED",
124
+ )
125
+ if _metadata(metadata.location, front_bytes) != metadata:
126
+ raise ContextError(
127
+ "Selected Skill metadata changed after selection.",
128
+ code="SKILL_BODY_LOAD_FAILED",
129
+ )
130
+ body = content[stream.tell() :].decode("utf-8")
131
+ except ContextError:
132
+ raise
133
+ except (OSError, UnicodeDecodeError, tomllib.TOMLDecodeError, ValueError) as exc:
134
+ raise ContextError(
135
+ "Selected Skill body could not be loaded safely.",
136
+ code="SKILL_BODY_LOAD_FAILED",
137
+ retryable=isinstance(exc, OSError) and _is_transient_io(exc),
138
+ ) from exc
139
+ finally:
140
+ self._discard_snapshot(metadata)
141
+ normalized = body.replace("\r\n", "\n").replace("\r", "\n").strip() + "\n"
142
+ _validate_body(normalized)
143
+ return normalized
144
+
145
+ def _reset_snapshots(self) -> None:
146
+ self._snapshots.set(None)
147
+
148
+ def _discard_snapshot(self, metadata: SkillMetadata) -> None:
149
+ snapshots = self._snapshots.get()
150
+ if snapshots is None:
151
+ return
152
+ stored = snapshots.get(id(metadata))
153
+ if stored is None or stored[0] is not metadata:
154
+ return
155
+ remaining = dict(snapshots)
156
+ del remaining[id(metadata)]
157
+ self._snapshots.set(remaining or None)
158
+
159
+ def _open(self, location: SkillLocation, *, selected: bool = False) -> IO[bytes]:
160
+ if location.source is SkillSource.BUILTIN:
161
+ resource = self._builtin_resource(location, selected=selected)
162
+ try:
163
+ return resource.open("rb")
164
+ except OSError as exc:
165
+ if selected:
166
+ raise ContextError(
167
+ "Selected builtin Skill document is unreadable.",
168
+ code="SKILL_BODY_LOAD_FAILED",
169
+ retryable=_is_transient_io(exc),
170
+ ) from exc
171
+ raise ContextError(
172
+ "Builtin Skill document is unreadable.",
173
+ code="SKILL_METADATA_INVALID",
174
+ ) from exc
175
+ document = self._filesystem_document(location, selected=selected)
176
+ try:
177
+ return document.open("rb")
178
+ except OSError as exc:
179
+ if selected:
180
+ raise ContextError(
181
+ "Selected Skill document is unreadable.",
182
+ code="SKILL_BODY_LOAD_FAILED",
183
+ retryable=_is_transient_io(exc),
184
+ ) from exc
185
+ raise
186
+
187
+ def _document_state(
188
+ self,
189
+ location: SkillLocation,
190
+ *,
191
+ selected: bool = False,
192
+ ) -> tuple[int, int | None]:
193
+ if location.source is SkillSource.BUILTIN:
194
+ resource = self._builtin_resource(location, selected=selected)
195
+ try:
196
+ if isinstance(resource, Path):
197
+ stat = resource.stat()
198
+ _validate_size(stat.st_size)
199
+ return stat.st_size, stat.st_mtime_ns
200
+ if isinstance(resource, zipfile.Path):
201
+ size = resource.root.getinfo(resource.at).file_size
202
+ else:
203
+ raise ValueError(
204
+ "Builtin Skill resource size cannot be determined without body I/O."
205
+ )
206
+ except OSError as exc:
207
+ if selected:
208
+ raise ContextError(
209
+ "Selected builtin Skill document is unreadable.",
210
+ code="SKILL_BODY_LOAD_FAILED",
211
+ retryable=_is_transient_io(exc),
212
+ ) from exc
213
+ raise
214
+ _validate_size(size)
215
+ return size, None
216
+ try:
217
+ stat = self._filesystem_document(location, selected=selected).stat()
218
+ _validate_size(stat.st_size)
219
+ return stat.st_size, stat.st_mtime_ns
220
+ except OSError as exc:
221
+ if selected:
222
+ raise ContextError(
223
+ "Selected Skill document is unreadable.",
224
+ code="SKILL_BODY_LOAD_FAILED",
225
+ retryable=_is_transient_io(exc),
226
+ ) from exc
227
+ raise
228
+
229
+ def _filesystem_document(
230
+ self,
231
+ location: SkillLocation,
232
+ *,
233
+ selected: bool = False,
234
+ ) -> Path:
235
+ root = self._roots[location.source]
236
+ try:
237
+ canonical_root = root.resolve(strict=True)
238
+ except OSError as exc:
239
+ if selected:
240
+ raise ContextError(
241
+ "Selected Skill source root is unavailable.",
242
+ code="SKILL_BODY_LOAD_FAILED",
243
+ retryable=_is_transient_io(exc),
244
+ ) from exc
245
+ raise ContextError(
246
+ f"Skill path is unsafe for {location.source.value}:"
247
+ f"{location.relative_path}.",
248
+ code="SKILL_PATH_UNSAFE",
249
+ ) from exc
250
+ try:
251
+ document = canonical_root.joinpath(*location.relative_path.split("/")).resolve(
252
+ strict=True
253
+ )
254
+ except OSError as exc:
255
+ if selected:
256
+ raise ContextError(
257
+ "Selected Skill document is unavailable.",
258
+ code="SKILL_BODY_LOAD_FAILED",
259
+ retryable=_is_transient_io(exc),
260
+ ) from exc
261
+ raise ContextError(
262
+ f"Skill path is unsafe for {location.source.value}:"
263
+ f"{location.relative_path}.",
264
+ code="SKILL_PATH_UNSAFE",
265
+ ) from exc
266
+ try:
267
+ document.relative_to(canonical_root)
268
+ except ValueError as exc:
269
+ raise ContextError(
270
+ f"Skill path is unsafe for {location.source.value}:{location.relative_path}.",
271
+ code="SKILL_PATH_UNSAFE",
272
+ ) from exc
273
+ if not document.is_file():
274
+ raise ContextError(
275
+ f"Skill path is not a regular file for {location.source.value}:"
276
+ f"{location.relative_path}.",
277
+ code="SKILL_PATH_UNSAFE",
278
+ )
279
+ return document
280
+
281
+ def _builtin_resource(
282
+ self,
283
+ location: SkillLocation,
284
+ *,
285
+ selected: bool = False,
286
+ ) -> Traversable:
287
+ try:
288
+ root = files(self._builtin_package)
289
+ resource = root.joinpath(*location.relative_path.split("/"))
290
+ if isinstance(root, Path) and isinstance(resource, Path):
291
+ canonical_root = root.resolve(strict=True)
292
+ try:
293
+ canonical_resource = resource.resolve(strict=True)
294
+ except OSError as exc:
295
+ if selected:
296
+ raise ContextError(
297
+ "Selected builtin Skill document is unavailable.",
298
+ code="SKILL_BODY_LOAD_FAILED",
299
+ retryable=_is_transient_io(exc),
300
+ ) from exc
301
+ raise
302
+ canonical_resource.relative_to(canonical_root)
303
+ resource = canonical_resource
304
+ if not resource.is_file():
305
+ if selected:
306
+ raise ContextError(
307
+ "Selected builtin Skill document is unavailable.",
308
+ code="SKILL_BODY_LOAD_FAILED",
309
+ )
310
+ raise ValueError("Builtin Skill is not a regular package resource.")
311
+ return resource
312
+ except ContextError:
313
+ raise
314
+ except ModuleNotFoundError as exc:
315
+ if selected:
316
+ raise ContextError(
317
+ "Selected builtin Skill package is unavailable.",
318
+ code="SKILL_BODY_LOAD_FAILED",
319
+ ) from exc
320
+ raise ContextError(
321
+ f"Builtin Skill path is unsafe: {location.relative_path}.",
322
+ code="SKILL_PATH_UNSAFE",
323
+ ) from exc
324
+ except (OSError, ValueError) as exc:
325
+ raise ContextError(
326
+ f"Builtin Skill path is unsafe: {location.relative_path}.",
327
+ code="SKILL_PATH_UNSAFE",
328
+ ) from exc
329
+
330
+
331
+ def _read_front_matter(document: IO[bytes]) -> bytes:
332
+ consumed = 0
333
+ first = document.readline(MAX_FRONT_MATTER_BYTES + 1)
334
+ consumed += len(first)
335
+ if first.startswith(b"\xef\xbb\xbf"):
336
+ first = first[3:]
337
+ if _line_value(first) != b"+++":
338
+ raise ValueError("Skill front matter opening delimiter is missing.")
339
+ front_lines: list[bytes] = []
340
+ while consumed <= MAX_FRONT_MATTER_BYTES:
341
+ line = document.readline(MAX_FRONT_MATTER_BYTES + 1 - consumed)
342
+ consumed += len(line)
343
+ if not line:
344
+ raise ValueError("Skill front matter closing delimiter is missing.")
345
+ if consumed > MAX_FRONT_MATTER_BYTES:
346
+ break
347
+ if _line_value(line) == b"+++":
348
+ return b"".join(front_lines)
349
+ front_lines.append(line)
350
+ raise ValueError("Skill front matter exceeds the fixed size limit.")
351
+
352
+
353
+ def _line_value(line: bytes) -> bytes:
354
+ return line.removesuffix(b"\n").removesuffix(b"\r")
355
+
356
+
357
+ def _metadata(location: SkillLocation, front_bytes: bytes) -> SkillMetadata:
358
+ document = tomllib.loads(front_bytes.decode("utf-8"))
359
+ if set(document) != _ALLOWED_KEYS or not _REQUIRED_KEYS <= set(document):
360
+ raise ValueError("Skill front matter keys are invalid.")
361
+ skill_id = document["id"]
362
+ name = document["name"]
363
+ description = document["description"]
364
+ usage_scenario = document["usage_scenario"]
365
+ priority = document.get("priority", 0)
366
+ version = document["version"]
367
+ if not isinstance(version, str):
368
+ raise _VersionInvalid("Skill version must be a string.")
369
+ if (
370
+ not isinstance(skill_id, str)
371
+ or not isinstance(name, str)
372
+ or not isinstance(description, str)
373
+ or not isinstance(usage_scenario, str)
374
+ or isinstance(priority, bool)
375
+ or not isinstance(priority, int)
376
+ ):
377
+ raise ValueError("Skill front matter field types are invalid.")
378
+ try:
379
+ return SkillMetadata(
380
+ skill_id=skill_id,
381
+ name=name,
382
+ description=description,
383
+ usage_scenario=usage_scenario,
384
+ priority=priority,
385
+ version=version,
386
+ source=location.source,
387
+ location=location,
388
+ )
389
+ except ValueError as exc:
390
+ if "SemVer" in str(exc):
391
+ raise _VersionInvalid(str(exc)) from exc
392
+ raise
393
+
394
+
395
+ def _validate_size(size: int) -> None:
396
+ if size > MAX_SKILL_FILE_BYTES:
397
+ raise ValueError("Skill document exceeds the fixed size limit.")
398
+
399
+
400
+ def _is_transient_io(error: OSError) -> bool:
401
+ return error.errno in {
402
+ errno.EINTR,
403
+ errno.EAGAIN,
404
+ errno.ENFILE,
405
+ errno.EMFILE,
406
+ errno.ETIMEDOUT,
407
+ } or getattr(error, "winerror", None) in {32, 33}
408
+
409
+
410
+ def _validate_body(body: str) -> None:
411
+ if not body.strip() or any(line == "+++" for line in body.splitlines()):
412
+ raise ContextError("Selected Skill body is invalid.", code="SKILL_BODY_INVALID")
413
+ headings: list[tuple[int, str]] = []
414
+ fence: str | None = None
415
+ lines = body.splitlines()
416
+ for index, line in enumerate(lines):
417
+ stripped = line.lstrip()
418
+ if stripped.startswith(("```", "~~~")):
419
+ marker = stripped[:3]
420
+ fence = None if fence == marker else marker if fence is None else fence
421
+ continue
422
+ if fence is None and line.startswith("## "):
423
+ headings.append((index, line))
424
+ for heading in _REQUIRED_HEADINGS:
425
+ if sum(value == heading for _, value in headings) != 1:
426
+ raise ContextError("Selected Skill body is invalid.", code="SKILL_BODY_INVALID")
427
+ positions = [
428
+ next(index for index, value in headings if value == heading)
429
+ for heading in _REQUIRED_HEADINGS
430
+ ]
431
+ if positions != sorted(positions) or any(
432
+ value not in _REQUIRED_HEADINGS and index < positions[-1]
433
+ for index, value in headings
434
+ ):
435
+ raise ContextError("Selected Skill body is invalid.", code="SKILL_BODY_INVALID")
436
+ for offset, position in enumerate(positions):
437
+ end = positions[offset + 1] if offset + 1 < len(positions) else len(lines)
438
+ if not "\n".join(lines[position + 1 : end]).strip():
439
+ raise ContextError("Selected Skill body is invalid.", code="SKILL_BODY_INVALID")
@@ -0,0 +1,146 @@
1
+ """Deterministic metadata catalog and post-selection Skill resolution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextvars import ContextVar
6
+ from importlib.resources import files
7
+ from pathlib import Path
8
+
9
+ from nexus.domain.ports.skills import SkillLoader
10
+ from nexus.domain.skills import (
11
+ SelectedSkill,
12
+ SkillLocation,
13
+ SkillMetadata,
14
+ SkillSelectionResult,
15
+ SkillSource,
16
+ )
17
+ from nexus.errors import ContextError
18
+
19
+ _PRECEDENCE = {
20
+ SkillSource.REPOSITORY: 0,
21
+ SkillSource.USER_GLOBAL: 1,
22
+ SkillSource.BUILTIN: 2,
23
+ }
24
+
25
+
26
+ class DefaultSkillRegistry:
27
+ def __init__(
28
+ self,
29
+ loader: SkillLoader,
30
+ repository_root: Path,
31
+ user_root: Path,
32
+ *,
33
+ builtin_package: str = "nexus.skills.builtin",
34
+ ) -> None:
35
+ self._loader = loader
36
+ self._roots = {
37
+ SkillSource.REPOSITORY: repository_root,
38
+ SkillSource.USER_GLOBAL: user_root,
39
+ }
40
+ self._builtin_package = builtin_package
41
+ self._metadata: ContextVar[tuple[SkillMetadata, ...]] = ContextVar(
42
+ "skill_registry_metadata",
43
+ default=(),
44
+ )
45
+
46
+ async def scan_metadata(self) -> tuple[SkillMetadata, ...]:
47
+ self._reset_loader_snapshots()
48
+ result: list[SkillMetadata] = []
49
+ for source in (
50
+ SkillSource.REPOSITORY,
51
+ SkillSource.USER_GLOBAL,
52
+ SkillSource.BUILTIN,
53
+ ):
54
+ for location in self._locations(source):
55
+ result.append(await self._loader.load_metadata(location))
56
+ seen: set[tuple[SkillSource, str]] = set()
57
+ for metadata in result:
58
+ identity = (metadata.source, metadata.skill_id)
59
+ if identity in seen:
60
+ raise ContextError(
61
+ f"Duplicate Skill identity in {metadata.source.value}:"
62
+ f"{metadata.skill_id}.",
63
+ code="SKILL_ID_DUPLICATE",
64
+ )
65
+ seen.add(identity)
66
+ catalog = tuple(result)
67
+ self._metadata.set(catalog)
68
+ return catalog
69
+
70
+ def resolve_selected(
71
+ self,
72
+ selection: SkillSelectionResult,
73
+ ) -> tuple[SkillMetadata, ...]:
74
+ grouped: dict[str, list[SkillMetadata]] = {}
75
+ for metadata in self._metadata.get():
76
+ grouped.setdefault(metadata.skill_id, []).append(metadata)
77
+ resolved: list[SkillMetadata] = []
78
+ for skill_id in selection.selected_skill_ids:
79
+ variants = grouped.get(skill_id)
80
+ if not variants:
81
+ raise ContextError(
82
+ "Skill selection referenced an unknown identity.",
83
+ code="SKILL_SELECTION_FAILED",
84
+ retryable=True,
85
+ )
86
+ resolved.append(min(variants, key=lambda value: _PRECEDENCE[value.source]))
87
+ return tuple(resolved)
88
+
89
+ async def load_selected(
90
+ self,
91
+ selection: SkillSelectionResult,
92
+ ) -> tuple[SelectedSkill, ...]:
93
+ try:
94
+ selected: list[SelectedSkill] = []
95
+ for metadata in self.resolve_selected(selection):
96
+ selected.append(
97
+ SelectedSkill(
98
+ metadata,
99
+ await self._loader.load_body(metadata),
100
+ selection.selection_reason_summary,
101
+ )
102
+ )
103
+ return tuple(selected)
104
+ finally:
105
+ self._reset_loader_snapshots()
106
+
107
+ def _reset_loader_snapshots(self) -> None:
108
+ reset = getattr(self._loader, "_reset_snapshots", None)
109
+ if callable(reset):
110
+ reset()
111
+
112
+ def _locations(self, source: SkillSource) -> tuple[SkillLocation, ...]:
113
+ if source is SkillSource.BUILTIN:
114
+ try:
115
+ root = files(self._builtin_package)
116
+ names = sorted(
117
+ item.name
118
+ for item in root.iterdir()
119
+ if item.is_dir() and item.name != "__pycache__"
120
+ )
121
+ except (ModuleNotFoundError, OSError) as exc:
122
+ raise ContextError(
123
+ "Builtin Skill root is unreadable.",
124
+ code="SKILL_PATH_UNSAFE",
125
+ ) from exc
126
+ else:
127
+ root_path = self._roots[source]
128
+ if not root_path.exists():
129
+ return ()
130
+ try:
131
+ canonical_root = root_path.resolve(strict=True)
132
+ if not canonical_root.is_dir():
133
+ raise OSError("Skill root is not a directory.")
134
+ names = sorted(item.name for item in canonical_root.iterdir() if item.is_dir())
135
+ except OSError as exc:
136
+ raise ContextError(
137
+ f"Skill root is unsafe for {source.value}.",
138
+ code="SKILL_PATH_UNSAFE",
139
+ ) from exc
140
+ try:
141
+ return tuple(SkillLocation(source, f"{name}/SKILL.md") for name in names)
142
+ except ValueError as exc:
143
+ raise ContextError(
144
+ f"Skill directory name is invalid for {source.value}.",
145
+ code="SKILL_METADATA_INVALID",
146
+ ) from exc