agent-bios 0.16.0 → 0.17.1

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.
@@ -9,8 +9,11 @@ from __future__ import annotations
9
9
  import difflib
10
10
  import json
11
11
  import re
12
+ from pathlib import PurePosixPath
12
13
  from typing import Any
13
- from urllib.parse import unquote
14
+ from urllib.parse import quote, unquote
15
+
16
+ from rich.text import Text
14
17
 
15
18
  from textual.app import App, ComposeResult
16
19
  from textual.binding import Binding
@@ -31,15 +34,55 @@ from textual.widgets import (
31
34
  )
32
35
 
33
36
  try:
34
- from corpus_catalog import CLAUDE_HOOK_EVENTS
37
+ from corpus_catalog import HOOK_EVENTS
35
38
  except ImportError: # pragma: no cover - package import from repository root
36
- from .corpus_catalog import CLAUDE_HOOK_EVENTS
39
+ from .corpus_catalog import HOOK_EVENTS
37
40
 
38
41
 
39
42
  SURFACES = ("always", "relevant", "requested", "event", "delegated")
40
43
  VIEWS = ("effective", "installed", "change", "diff", "history")
41
44
 
42
45
 
46
+ def guide_pointers(item: dict[str, Any], inventory: list[dict[str, Any]]) -> list[dict[str, Any]]:
47
+ """Display literal guide references, not inferred dependencies or new routing."""
48
+ if item.get("kind") != "rule":
49
+ return []
50
+ paths = dict.fromkeys(re.findall(r"\bguides/[A-Za-z0-9][A-Za-z0-9_./-]*\.md\b", item.get("body", "")))
51
+ result = []
52
+ for path in paths:
53
+ if ".." in PurePosixPath(path).parts:
54
+ continue
55
+ matches = [candidate for candidate in inventory
56
+ if candidate.get("package_id") == item.get("package_id")
57
+ and candidate.get("kind") == "guide" and path in candidate.get("members", {})]
58
+ result.append({"path": path, "target": matches[0] if len(matches) == 1 else None,
59
+ "problem": "Ambiguous guide reference" if matches else "Not found in this package"})
60
+ return result
61
+
62
+
63
+ def library_label(item: dict[str, Any], inventory: list[dict[str, Any]]) -> Text:
64
+ pointers = guide_pointers(item, inventory)
65
+ label = Text()
66
+ if pointers:
67
+ label.append("→ GUIDE ", style="bold cyan")
68
+ label.append(", ".join(PurePosixPath(pointer["path"]).stem for pointer in pointers))
69
+ label.append(" · ")
70
+ label.append(f"{item.get('title', item.get('ref'))} · {item.get('surface', '?')}")
71
+ return label
72
+
73
+
74
+ def render_member_body(body: str, member: str | None, kind: str | None = None) -> str:
75
+ """Render prose as Markdown and preserve source files as literal code."""
76
+ suffix = PurePosixPath(member).suffix.lower() if member else ""
77
+ if suffix in {".md", ".markdown"} or (not member and kind != "hook"):
78
+ return body
79
+ language = {".py": "python", ".json": "json", ".toml": "toml",
80
+ ".sh": "bash", ".zsh": "bash", ".yaml": "yaml", ".yml": "yaml",
81
+ ".js": "javascript", ".ts": "typescript"}.get(suffix, "")
82
+ fence = "`" * max(3, 1 + max((len(run) for run in re.findall(r"`+", body)), default=0))
83
+ return f"{fence}{language}\n{body}\n{fence}"
84
+
85
+
43
86
  class Confirmation(ModalScreen[bool]):
44
87
  """One bounded confirmation; the caller decides what the answer means."""
45
88
 
@@ -70,6 +113,9 @@ class CorpusMarkdownViewer(MarkdownViewer):
70
113
  """Keep all links inside Studio; never hand a URL to the operating system."""
71
114
 
72
115
  async def _on_markdown_link_clicked(self, message: Markdown.LinkClicked) -> None:
116
+ # stop() prevents bubbling, not the inherited MarkdownViewer handler.
117
+ # Its default go() would try to load corpus:// as a filesystem path.
118
+ message.prevent_default()
73
119
  message.stop()
74
120
  if message.href.startswith("corpus://"):
75
121
  ref = unquote(message.href.removeprefix("corpus://"))
@@ -181,8 +227,8 @@ class CorpusStudio(App[None]):
181
227
  id="editor-primary-member")
182
228
  with Horizontal(id="hook-binding-row"):
183
229
  yield Select(
184
- [(event, event) for event in sorted(CLAUDE_HOOK_EVENTS)],
185
- value=sorted(CLAUDE_HOOK_EVENTS)[0], allow_blank=False, id="editor-hook-event",
230
+ [(event, event) for event in sorted(HOOK_EVENTS)],
231
+ value=sorted(HOOK_EVENTS)[0], allow_blank=False, id="editor-hook-event",
186
232
  )
187
233
  yield Input(placeholder="Hook matcher", id="editor-hook-matcher")
188
234
  yield TextArea(language="markdown", id="editor-body")
@@ -239,7 +285,7 @@ class CorpusStudio(App[None]):
239
285
  groups[key] = state_nodes[state].add(package)
240
286
  groups[key].expand()
241
287
  groups[key].add_leaf(
242
- f"{item.get('title', item.get('ref'))} · {item.get('surface', '?')}",
288
+ library_label(item, self.items),
243
289
  data=item.get("ref"),
244
290
  )
245
291
  tree.root.expand()
@@ -314,8 +360,7 @@ class CorpusStudio(App[None]):
314
360
  members[self.current_member] = self.query_one("#editor-body", TextArea).text
315
361
  return members
316
362
 
317
- @staticmethod
318
- def _render_item(row: dict[str, Any], result: dict[str, Any], view: str) -> str:
363
+ def _render_item(self, row: dict[str, Any], result: dict[str, Any], view: str) -> str:
319
364
  if view == "effective" and isinstance(result.get("item"), dict):
320
365
  item = result["item"]
321
366
  links = "\n".join(
@@ -330,11 +375,29 @@ class CorpusStudio(App[None]):
330
375
  "Edit the body to the intended member text, or choose a primary member through the API."
331
376
  + (f" Available members: {members}." if members else "") + "\n"
332
377
  )
378
+ body = render_member_body(item.get("body", ""), item.get("primary_member"), item.get("kind"))
379
+ pointers = guide_pointers(item, self.items)
380
+ guide_links = ""
381
+ if pointers:
382
+ lines = []
383
+ for pointer in pointers:
384
+ target = pointer["target"]
385
+ if target is None:
386
+ lines.append(f"- `{pointer['path']}` — {pointer['problem']}")
387
+ else:
388
+ destination = quote(target["ref"], safe="@/:")
389
+ lines.append(f"- [{pointer['path']}](corpus://{destination}) — "
390
+ f"**{target['surface']}** · {target.get('state', 'active')}")
391
+ guide_links = (
392
+ "## Guide pointer\n\nThis rule explicitly references the following guide(s). "
393
+ "The rule keeps its own consumption surface; these links do not change "
394
+ "delivery or prove that a guide was loaded.\n\n" + "\n".join(lines) + "\n\n## Rule\n\n"
395
+ )
333
396
  return (
334
397
  f"# {item.get('title', item.get('ref'))}\n\n"
335
398
  f"`{item.get('ref')}` · **{row.get('state', 'active')}** · "
336
399
  f"{item.get('surface')} · {item.get('kind')}\n\n"
337
- f"{reconciliation}\n{item.get('body', '')}\n\n## Dependencies\n\n{links}\n"
400
+ f"{guide_links}{reconciliation}\n{body}\n\n## Dependencies\n\n{links}\n"
338
401
  + ("\n## Native consumption\n\nRequires explicit `agent-launch --corpus-native`.\n"
339
402
  if item.get("kind") == "hook" else "")
340
403
  )
@@ -373,11 +436,8 @@ class CorpusStudio(App[None]):
373
436
  body = item["members"][event.value]
374
437
  if event.value == primary:
375
438
  markdown = self._render_item(self._row() or item, {"item": item}, "effective")
376
- elif event.value.endswith(".md"):
377
- markdown = f"# {event.value}\n\n{body}"
378
439
  else:
379
- fence = "`" * (max(2, max((len(run) for run in re.findall(r'`+', body)), default=0)) + 1)
380
- markdown = f"# {event.value}\n\n{fence}\n{body}\n{fence}\n"
440
+ markdown = f"# {event.value}\n\n{render_member_body(body, event.value)}\n"
381
441
  await self.query_one("#wiki", CorpusMarkdownViewer).document.update(markdown)
382
442
 
383
443
  def editor_dirty(self) -> bool:
@@ -420,7 +480,7 @@ class CorpusStudio(App[None]):
420
480
  self.query_one("#editor-surface", Select).value = surface
421
481
  self.query_one("#hook-binding-row").styles.display = "block" if is_hook else "none"
422
482
  if is_hook:
423
- event = hook.get("event") if isinstance(hook, dict) else sorted(CLAUDE_HOOK_EVENTS)[0]
483
+ event = hook.get("event") if isinstance(hook, dict) else sorted(HOOK_EVENTS)[0]
424
484
  matcher = hook.get("matcher") if isinstance(hook, dict) else ""
425
485
  self.query_one("#editor-hook-event", Select).value = event
426
486
  self.query_one("#editor-hook-matcher", Input).value = matcher
@@ -555,8 +615,8 @@ class CorpusStudio(App[None]):
555
615
  if self.editor_item.get("kind") == "hook":
556
616
  event = self.query_one("#editor-hook-event", Select).value
557
617
  matcher = self.query_one("#editor-hook-matcher", Input).value
558
- if not isinstance(event, str) or event not in CLAUDE_HOOK_EVENTS:
559
- raise ValueError("Choose a supported Claude hook event.")
618
+ if not isinstance(event, str) or event not in HOOK_EVENTS:
619
+ raise ValueError("Choose a supported hook event.")
560
620
  if not matcher or "\n" in matcher or "\r" in matcher:
561
621
  raise ValueError("Hook matcher must be one non-empty line.")
562
622
  patch["hook"] = {"event": event, "matcher": matcher}