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.
- package/DEPENDENCIES.md +3 -2
- package/README.md +106 -24
- package/claude/CLAUDE.md +1 -1
- package/claude/guides/tooling-gotchas.md +1 -1
- package/claude/hooks/tooling-gotchas-hook.py +9 -16
- package/claude/skills/understand/SKILL.md +83 -0
- package/codex/AGENTS.md +1 -1
- package/codex/guides/tooling-gotchas.md +1 -1
- package/compose/bootstrap/SKILL.md +12 -2
- package/compose/corpus.py +6 -6
- package/compose/corpus_catalog.py +74 -25
- package/compose/corpus_install.py +66 -13
- package/compose/corpus_session.py +105 -6
- package/compose/corpus_store.py +11 -2
- package/compose/corpus_transaction.py +20 -0
- package/compose/corpus_ui.py +76 -16
- package/compose/corpus_understand.py +522 -0
- package/compose/domains.json +1 -0
- package/compose/register-hooks.py +6 -8
- package/install.sh +21 -0
- package/launch/agent-launch.py +201 -18
- package/launch/agent-launch.zsh +16 -2
- package/launch/i18n/en.toml +23 -0
- package/launch/i18n/ja.toml +23 -0
- package/launch/i18n/ko.toml +23 -0
- package/launch/shell_integration.py +267 -0
- package/package.json +4 -2
- package/provenance.json +1 -1
package/compose/corpus_ui.py
CHANGED
|
@@ -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
|
|
37
|
+
from corpus_catalog import HOOK_EVENTS
|
|
35
38
|
except ImportError: # pragma: no cover - package import from repository root
|
|
36
|
-
from .corpus_catalog import
|
|
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(
|
|
185
|
-
value=sorted(
|
|
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
|
-
|
|
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
|
-
|
|
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{
|
|
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
|
-
|
|
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(
|
|
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
|
|
559
|
-
raise ValueError("Choose a supported
|
|
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}
|