agent-bios 0.18.0 → 0.19.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 +236 -80
- package/INSTALL.md +112 -0
- package/README.md +187 -524
- package/claude/CLAUDE.md +1 -1
- package/claude/guides/cli-multi-model-workflow.md +1 -1
- package/claude/guides/learning-flow.md +23 -12
- package/claude/guides/session-distill-workflow.md +22 -12
- package/claude/skills/understand/SKILL.md +52 -22
- package/codex/AGENTS.md +1 -1
- package/codex/guides/cli-multi-model-workflow.md +1 -1
- package/codex/guides/learning-flow.md +23 -12
- package/codex/guides/session-distill-workflow.md +22 -12
- package/compose/app_bridge/SKILL.md +75 -0
- package/compose/app_bridge/agents/openai.yaml +2 -0
- package/compose/app_bridge/scripts/bridge.py +76 -0
- package/compose/bootstrap/SKILL.md +12 -1
- package/compose/corpus.py +31 -9
- package/compose/corpus_app.py +456 -0
- package/compose/corpus_import.py +529 -0
- package/compose/corpus_install.py +202 -18
- package/compose/corpus_session.py +27 -0
- package/compose/corpus_setup.py +676 -0
- package/compose/corpus_setup_cli.py +585 -0
- package/compose/corpus_setup_i18n.py +324 -0
- package/compose/corpus_setup_ui.py +647 -0
- package/compose/corpus_store.py +213 -32
- package/compose/corpus_transaction.py +43 -10
- package/compose/corpus_ui_runtime.py +278 -0
- package/compose/corpus_understand.py +173 -22
- package/compose/setup/START.md +158 -0
- package/compose/ui_runtime/linkify_it_py-2.2.0-py3-none-any.whl +0 -0
- package/compose/ui_runtime/manifest.json +238 -0
- package/compose/ui_runtime/markdown_it_py-4.2.0-py3-none-any.whl +0 -0
- package/compose/ui_runtime/mdit_py_plugins-0.6.1-py3-none-any.whl +0 -0
- package/compose/ui_runtime/mdurl-0.1.2-py3-none-any.whl +0 -0
- package/compose/ui_runtime/platformdirs-4.11.8-py3-none-any.whl +0 -0
- package/compose/ui_runtime/pygments-2.21.0-py3-none-any.whl +0 -0
- package/compose/ui_runtime/rich-15.0.0-py3-none-any.whl +0 -0
- package/compose/ui_runtime/textual-8.2.8-py3-none-any.whl +0 -0
- package/compose/ui_runtime/typing_extensions-4.16.0-py3-none-any.whl +0 -0
- package/docs/advanced-launch.md +131 -0
- package/docs/assets/corpus-studio.svg +227 -0
- package/docs/corpus.md +117 -0
- package/docs/recovery.md +201 -0
- package/docs/session-model.md +120 -0
- package/docs/setup.md +206 -0
- package/docs/understand.md +88 -0
- package/install.sh +75 -46
- package/launch/agent-launch.py +99 -52
- package/launch/provision-venv.sh +44 -13
- package/learn/collect-learning.py +14 -5
- package/learn/learning.schema.json +2 -2
- package/package.json +14 -2
- package/provenance.json +1 -1
- package/wrappers/claude-run.sh +10 -13
package/compose/corpus_store.py
CHANGED
|
@@ -21,9 +21,9 @@ import uuid
|
|
|
21
21
|
from typing import Any, Iterator
|
|
22
22
|
|
|
23
23
|
try:
|
|
24
|
-
from corpus_transaction import transaction_lock, guard_pending, pending_operations, operation_scope_active
|
|
24
|
+
from corpus_transaction import transaction_lock, guard_pending, pending_operations, operation_scope_active, reject_symlink_ancestors
|
|
25
25
|
except ImportError:
|
|
26
|
-
from .corpus_transaction import transaction_lock, guard_pending, pending_operations, operation_scope_active
|
|
26
|
+
from .corpus_transaction import transaction_lock, guard_pending, pending_operations, operation_scope_active, reject_symlink_ancestors
|
|
27
27
|
|
|
28
28
|
SCHEMA_VERSION = 1
|
|
29
29
|
LOCAL_PACKAGE = "@local/personal"
|
|
@@ -311,6 +311,7 @@ class CorpusStore:
|
|
|
311
311
|
# Optional, so reading an older state does not change its revision or the
|
|
312
312
|
# exact before/after documents used by prepared-transaction recovery.
|
|
313
313
|
self._enabled_overrides(state)
|
|
314
|
+
self._selection_mode(state, {})
|
|
314
315
|
return state
|
|
315
316
|
|
|
316
317
|
@staticmethod
|
|
@@ -322,6 +323,13 @@ class CorpusStore:
|
|
|
322
323
|
raise CorpusStoreError("personal state has invalid enabled_overrides")
|
|
323
324
|
return values
|
|
324
325
|
|
|
326
|
+
@staticmethod
|
|
327
|
+
def _selection_mode(user: dict[str, Any], defaults: dict[str, Any], requested: str | None = None) -> str:
|
|
328
|
+
value = requested if requested is not None else user.get("selection_mode", defaults.get("selection_mode", "default"))
|
|
329
|
+
if not isinstance(value, str) or value not in {"default", "selected", "none"}:
|
|
330
|
+
raise ValidationError("selection_mode must be default, selected, or none")
|
|
331
|
+
return value
|
|
332
|
+
|
|
325
333
|
def _write_transaction(self, tx_id: str, record: dict[str, Any]) -> None:
|
|
326
334
|
_atomic_write(self.runtime / "transactions" / tx_id / "journal.json", record)
|
|
327
335
|
|
|
@@ -474,6 +482,11 @@ class CorpusStore:
|
|
|
474
482
|
for raw in source:
|
|
475
483
|
item = self._validate_item(raw, allow_origin=True)
|
|
476
484
|
items[item["ref"]] = item
|
|
485
|
+
items = self._overlay_user_items(items, user, host, include_suppressed)
|
|
486
|
+
return [self._normalize_content(item, allow_legacy=True) for item in items.values()], inventory, defaults, baseline_ref
|
|
487
|
+
|
|
488
|
+
def _overlay_user_items(self, items: dict[str, dict[str, Any]], user: dict[str, Any],
|
|
489
|
+
host: str | None = None, include_suppressed: bool = False) -> dict[str, dict[str, Any]]:
|
|
477
490
|
for ref, raw in user["items"].items():
|
|
478
491
|
item = self._validate_item(raw, allow_origin=True)
|
|
479
492
|
if item["ref"] != ref or item["package_id"] != LOCAL_PACKAGE:
|
|
@@ -508,7 +521,7 @@ class CorpusStore:
|
|
|
508
521
|
items[ref]["active"] = False
|
|
509
522
|
else:
|
|
510
523
|
items.pop(ref, None)
|
|
511
|
-
return
|
|
524
|
+
return items
|
|
512
525
|
|
|
513
526
|
def _resolve_promotions(
|
|
514
527
|
self, selected: list[dict[str, Any]], user: dict[str, Any], host: str, baseline_ref: str
|
|
@@ -645,7 +658,7 @@ class CorpusStore:
|
|
|
645
658
|
raise ValidationError("selection must be a list of qualified refs/domains")
|
|
646
659
|
packages = {p.get("package_id") for p in inventory.get("packages", []) if isinstance(p, dict)}
|
|
647
660
|
for value in selection:
|
|
648
|
-
if value == "all":
|
|
661
|
+
if value == "all" or value in packages:
|
|
649
662
|
continue
|
|
650
663
|
if ":" in value:
|
|
651
664
|
continue
|
|
@@ -704,6 +717,18 @@ class CorpusStore:
|
|
|
704
717
|
if package not in packages or domain not in packages[package]:
|
|
705
718
|
raise ValidationError(f"unknown corpus selection domain: {value}")
|
|
706
719
|
|
|
720
|
+
def _selection_subjects(self, runtime: dict[str, Any], user: dict[str, Any],
|
|
721
|
+
items: list[dict[str, Any]], selection: list[str] | None) -> list[dict[str, Any]]:
|
|
722
|
+
"""Validate stored host-qualified selections without delivering another host's items."""
|
|
723
|
+
subjects = list(items)
|
|
724
|
+
refs = {item["ref"] for item in subjects}
|
|
725
|
+
for host in ("claude", "codex"):
|
|
726
|
+
prefix = f"@local/learnings-{host}:"
|
|
727
|
+
if any(value.startswith(prefix) and value not in refs for value in selection or []):
|
|
728
|
+
subjects.extend(item for item in self._effective_items(runtime, user, host=host)[0]
|
|
729
|
+
if item.get("active", True))
|
|
730
|
+
return subjects
|
|
731
|
+
|
|
707
732
|
def _validate_candidate_projection(self, runtime: dict[str, Any], user: dict[str, Any]) -> None:
|
|
708
733
|
"""Use the real compiler as a plan validator without publishing output."""
|
|
709
734
|
catalog = self._catalog_module()
|
|
@@ -713,8 +738,10 @@ class CorpusStore:
|
|
|
713
738
|
items, _inventory, defaults, _baseline = self._effective_items(runtime, user, host=host)
|
|
714
739
|
active = [item for item in items if item.get("active", True) is not False]
|
|
715
740
|
selection = self._effective_selection(user, defaults, None)
|
|
716
|
-
self._validate_snapshot_selection(selection, _inventory,
|
|
717
|
-
|
|
741
|
+
self._validate_snapshot_selection(selection, _inventory,
|
|
742
|
+
self._selection_subjects(runtime, user, active, selection))
|
|
743
|
+
selected = self._selected_items(active, selection, self._enabled_overrides(user),
|
|
744
|
+
mode=self._selection_mode(user, defaults), host=host)
|
|
718
745
|
self._require_resolved(selected)
|
|
719
746
|
catalog.compile_items(_copy_json(selected), Path(temp) / host, host)
|
|
720
747
|
except Exception as exc:
|
|
@@ -802,12 +829,53 @@ class CorpusStore:
|
|
|
802
829
|
|
|
803
830
|
# ---- public read API ----------------------------------------------------
|
|
804
831
|
|
|
805
|
-
def
|
|
832
|
+
def local_item_counts(self) -> dict[str, int]:
|
|
833
|
+
"""Count retained local content without changing state or session selection."""
|
|
834
|
+
reject_symlink_ancestors(self.state_root)
|
|
835
|
+
reject_symlink_ancestors(self.user_root)
|
|
836
|
+
for directory in (self.state_root, self.runtime, self.user_root):
|
|
837
|
+
if directory.exists() and not directory.is_dir():
|
|
838
|
+
raise CorpusStoreError(f"retained local corpus root is not a directory: {directory}")
|
|
839
|
+
sources = [self._user_state_path, *(self.user_root / "learnings" / host / "events.jsonl"
|
|
840
|
+
for host in ("claude", "codex"))]
|
|
841
|
+
for path in sources:
|
|
842
|
+
reject_symlink_ancestors(path)
|
|
843
|
+
for parent in path.parents:
|
|
844
|
+
if parent == self.user_root.parent:
|
|
845
|
+
break
|
|
846
|
+
if parent.exists() and not parent.is_dir():
|
|
847
|
+
raise CorpusStoreError(f"retained local corpus directory is not a directory: {parent}")
|
|
848
|
+
if path.exists() and not path.is_file():
|
|
849
|
+
raise CorpusStoreError(f"retained local corpus source is not a file: {path}")
|
|
850
|
+
if pending_operations(self.state_root):
|
|
851
|
+
raise CorpusStoreError("retained local corpus has a pending transaction; finish recovery before inspection")
|
|
852
|
+
try:
|
|
853
|
+
user = self._user_state()
|
|
854
|
+
for host in ("claude", "codex"):
|
|
855
|
+
suppressed = user["learning_suppressions"].get(host, [])
|
|
856
|
+
if not isinstance(suppressed, list) or not all(isinstance(value, str) for value in suppressed):
|
|
857
|
+
raise CorpusStoreError(f"personal state has invalid learning suppressions for {host}")
|
|
858
|
+
items: dict[str, dict[str, Any]] = {}
|
|
859
|
+
for host in ("claude", "codex"):
|
|
860
|
+
items.update(self._overlay_user_items({}, user, host))
|
|
861
|
+
counts: dict[str, int] = {}
|
|
862
|
+
for item in items.values():
|
|
863
|
+
if item.get("active", True) is not False:
|
|
864
|
+
package = item["package_id"]
|
|
865
|
+
counts[package] = counts.get(package, 0) + 1
|
|
866
|
+
return counts
|
|
867
|
+
except (AttributeError, KeyError, TypeError, ValueError) as exc:
|
|
868
|
+
raise CorpusStoreError("invalid retained local corpus state") from exc
|
|
869
|
+
|
|
870
|
+
def install(self, domains: list[str] | None = None, *, selection_mode: str | None = None,
|
|
871
|
+
replace_selection: bool = False) -> dict[str, Any]:
|
|
806
872
|
"""Install one immutable validated baseline tuple without touching user data."""
|
|
807
873
|
with self._lock():
|
|
808
|
-
return self.commit_install(self.prepare_install(domains
|
|
874
|
+
return self.commit_install(self.prepare_install(domains, selection_mode=selection_mode,
|
|
875
|
+
replace_selection=replace_selection))
|
|
809
876
|
|
|
810
|
-
def prepare_install(self, domains: list[str] | None = None
|
|
877
|
+
def prepare_install(self, domains: list[str] | None = None, *, selection_mode: str | None = None,
|
|
878
|
+
replace_selection: bool = False) -> dict[str, Any]:
|
|
811
879
|
"""Stage a validated baseline and source plan without advancing any pointer."""
|
|
812
880
|
with self._lock():
|
|
813
881
|
self._recover_locked()
|
|
@@ -820,9 +888,15 @@ class CorpusStore:
|
|
|
820
888
|
items = [self._validate_item(item, allow_origin=True) for item in raw_items]
|
|
821
889
|
if len({item["ref"] for item in items}) != len(items):
|
|
822
890
|
raise ValidationError("catalog has duplicate corpus refs")
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
891
|
+
mode = self._selection_mode({}, {}, selection_mode)
|
|
892
|
+
selected = self._normalized_install_selection(domains, catalog)
|
|
893
|
+
if mode == "none" and selected:
|
|
894
|
+
raise ValidationError("no-corpus installation cannot include selection targets")
|
|
895
|
+
defaults = {"schema_version": SCHEMA_VERSION, "selection": selected}
|
|
896
|
+
if mode == "default":
|
|
897
|
+
defaults["selection"] += [LOCAL_PACKAGE, "@local/learnings-claude", "@local/learnings-codex"]
|
|
898
|
+
if selection_mode is not None:
|
|
899
|
+
defaults["selection_mode"] = mode
|
|
826
900
|
promotion_path = self.repo / "learn" / "promotions.json"
|
|
827
901
|
promotions = _json_read(promotion_path, {"version": 0, "promotions": []})
|
|
828
902
|
if not isinstance(promotions, dict) or not isinstance(promotions.get("promotions"), list):
|
|
@@ -849,6 +923,11 @@ class CorpusStore:
|
|
|
849
923
|
# successful-install record changes.
|
|
850
924
|
next_user = self._rebase_overlays(old_inventory, catalog, user)
|
|
851
925
|
before = {"runtime": _copy_json(runtime), "user": _copy_json(user)}
|
|
926
|
+
if replace_selection:
|
|
927
|
+
next_user = _copy_json(next_user)
|
|
928
|
+
next_user["selection"] = _copy_json(defaults["selection"])
|
|
929
|
+
next_user["selection_mode"] = mode
|
|
930
|
+
next_user.pop("enabled_overrides", None)
|
|
852
931
|
_atomic_write(root / "inventory.json", catalog)
|
|
853
932
|
_atomic_write(root / "defaults.json", defaults)
|
|
854
933
|
_atomic_write(root / "promotions.json", promotions)
|
|
@@ -909,6 +988,7 @@ class CorpusStore:
|
|
|
909
988
|
"revision": revision, "baseline_count": len(refs),
|
|
910
989
|
"personal_items": len(user["items"]), "overrides": len(user["overrides"]),
|
|
911
990
|
"tombstones": len(user["tombstones"]), "selection": self._effective_selection(user, defaults, None),
|
|
991
|
+
"selection_mode": self._selection_mode(user, defaults),
|
|
912
992
|
"enabled_overrides": _copy_json(self._enabled_overrides(user)),
|
|
913
993
|
}
|
|
914
994
|
|
|
@@ -925,7 +1005,8 @@ class CorpusStore:
|
|
|
925
1005
|
overrides = self._enabled_overrides(user)
|
|
926
1006
|
selection = self._effective_selection(user, defaults, None)
|
|
927
1007
|
enabled = {item["ref"] for item in self._selected_items(
|
|
928
|
-
[item for item in effective.values() if item.get("active", True) is not False], selection, overrides
|
|
1008
|
+
[item for item in effective.values() if item.get("active", True) is not False], selection, overrides,
|
|
1009
|
+
mode=self._selection_mode(user, defaults))}
|
|
929
1010
|
revision = self._authoring_revision(runtime, user)
|
|
930
1011
|
rows: list[dict[str, Any]] = []
|
|
931
1012
|
for ref in sorted(set(source) | set(user["items"]) | set(effective)):
|
|
@@ -993,11 +1074,12 @@ class CorpusStore:
|
|
|
993
1074
|
raise CorpusStoreError("could not allocate an unused personal identity")
|
|
994
1075
|
|
|
995
1076
|
def _prepare_operation(self, payload: dict[str, Any], runtime: dict[str, Any], user: dict[str, Any],
|
|
996
|
-
*, allocated_item_id: str | None = None
|
|
1077
|
+
*, allocated_item_id: str | None = None,
|
|
1078
|
+
allocated_item_ids: list[str] | None = None) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
|
|
997
1079
|
if not isinstance(payload, dict):
|
|
998
1080
|
raise ValidationError("plan payload must be an object")
|
|
999
1081
|
op = payload.get("operation", payload.get("op"))
|
|
1000
|
-
if op not in {"create", "update", "remove", "restore", "recover", "reset", "rollback", "select", "enable"}:
|
|
1082
|
+
if not isinstance(op, str) or op not in {"create", "update", "remove", "restore", "recover", "reset", "rollback", "select", "enable", "import"}:
|
|
1001
1083
|
raise ValidationError("unknown corpus operation")
|
|
1002
1084
|
allowed = {
|
|
1003
1085
|
"create": {"operation", "op", "item", "package_id", "expected_revision"},
|
|
@@ -1007,8 +1089,9 @@ class CorpusStore:
|
|
|
1007
1089
|
"recover": {"operation", "op", "ref", "expected_revision"},
|
|
1008
1090
|
"reset": {"operation", "op", "expected_revision"},
|
|
1009
1091
|
"rollback": {"operation", "op", "baseline_ref", "history_id", "expected_revision"},
|
|
1010
|
-
"select": {"operation", "op", "selection", "expected_revision"},
|
|
1092
|
+
"select": {"operation", "op", "selection", "selection_mode", "expected_revision"},
|
|
1011
1093
|
"enable": {"operation", "op", "items", "expected_revision"},
|
|
1094
|
+
"import": {"operation", "op", "capture_id", "candidates", "excluded", "expected_revision"},
|
|
1012
1095
|
}[op]
|
|
1013
1096
|
unknown = set(payload) - allowed
|
|
1014
1097
|
if unknown:
|
|
@@ -1021,7 +1104,31 @@ class CorpusStore:
|
|
|
1021
1104
|
items, inventory, defaults, _baseline_ref = self._effective_items(runtime, user)
|
|
1022
1105
|
effective = {item["ref"]: item for item in items}
|
|
1023
1106
|
details: dict[str, Any] = {"operation": op}
|
|
1024
|
-
if op == "
|
|
1107
|
+
if op == "import":
|
|
1108
|
+
import importlib
|
|
1109
|
+
name = "compose.corpus_import" if __package__ else "corpus_import"
|
|
1110
|
+
prepared_import = importlib.import_module(name).prepare_items(self, payload, user)
|
|
1111
|
+
refs = prepared_import["existing_refs"]
|
|
1112
|
+
receipt = prepared_import["receipt"]
|
|
1113
|
+
rows = prepared_import["items"]
|
|
1114
|
+
if rows:
|
|
1115
|
+
if allocated_item_ids is None or len(allocated_item_ids) != len(rows):
|
|
1116
|
+
raise ValidationError("import needs recorded runtime-owned identities")
|
|
1117
|
+
refs = []
|
|
1118
|
+
for raw, item_id in zip(rows, allocated_item_ids):
|
|
1119
|
+
_safe_part(item_id, "personal item id")
|
|
1120
|
+
raw = _copy_json(raw)
|
|
1121
|
+
raw.update(package_id=LOCAL_PACKAGE, item_id=item_id, ref=f"{LOCAL_PACKAGE}:{item_id}")
|
|
1122
|
+
item = self._validate_item(self._normalize_content(raw), allow_origin=True)
|
|
1123
|
+
if item["ref"] in effective or item["ref"] in next_user["items"]:
|
|
1124
|
+
raise ValidationError("import identity is already in use")
|
|
1125
|
+
next_user["items"][item["ref"]] = item
|
|
1126
|
+
refs.append(item["ref"])
|
|
1127
|
+
receipt["refs"] = refs
|
|
1128
|
+
receipt["item_digests"] = {ref: _digest(next_user["items"][ref]) for ref in refs}
|
|
1129
|
+
next_user.setdefault("imports", {})[receipt["request_digest"]] = receipt
|
|
1130
|
+
details.update(refs=refs, import_receipt=receipt, already_imported=not bool(rows))
|
|
1131
|
+
elif op == "create":
|
|
1025
1132
|
raw = _copy_json(payload.get("item"))
|
|
1026
1133
|
if not isinstance(raw, dict):
|
|
1027
1134
|
raise ValidationError("create needs item")
|
|
@@ -1168,6 +1275,14 @@ class CorpusStore:
|
|
|
1168
1275
|
details["items"] = _copy_json(choices)
|
|
1169
1276
|
elif op == "select":
|
|
1170
1277
|
selection = self._validate_selection(payload.get("selection"), inventory)
|
|
1278
|
+
if "selection_mode" in payload:
|
|
1279
|
+
if payload["selection_mode"] is None:
|
|
1280
|
+
raise ValidationError("selection_mode cannot be null")
|
|
1281
|
+
mode = self._selection_mode({}, {}, payload["selection_mode"])
|
|
1282
|
+
if mode == "none" and selection:
|
|
1283
|
+
raise ValidationError("no-corpus selection cannot include targets")
|
|
1284
|
+
next_user["selection_mode"] = mode
|
|
1285
|
+
details["selection_mode"] = mode
|
|
1171
1286
|
next_user["selection"] = selection
|
|
1172
1287
|
details["selection"] = selection
|
|
1173
1288
|
elif op == "reset":
|
|
@@ -1181,6 +1296,8 @@ class CorpusStore:
|
|
|
1181
1296
|
next_runtime["selected_baseline_ref"] = latest
|
|
1182
1297
|
_inv, latest_defaults = self._read_baseline(latest)
|
|
1183
1298
|
next_user["selection"] = latest_defaults.get("selection")
|
|
1299
|
+
if "selection_mode" in latest_defaults:
|
|
1300
|
+
next_user["selection_mode"] = latest_defaults["selection_mode"]
|
|
1184
1301
|
details["baseline_ref"] = latest
|
|
1185
1302
|
elif op == "rollback":
|
|
1186
1303
|
history_id = payload.get("history_id")
|
|
@@ -1209,8 +1326,22 @@ class CorpusStore:
|
|
|
1209
1326
|
self._recover_locked()
|
|
1210
1327
|
runtime, user = self._runtime_state(), self._user_state()
|
|
1211
1328
|
before = self._authoring_revision(runtime, user)
|
|
1212
|
-
|
|
1213
|
-
|
|
1329
|
+
op = payload.get("operation", payload.get("op")) if isinstance(payload, dict) else None
|
|
1330
|
+
allocated = self._next_personal_id(user) if op == "create" else None
|
|
1331
|
+
allocated_many = None
|
|
1332
|
+
if op == "import":
|
|
1333
|
+
import importlib
|
|
1334
|
+
name = "compose.corpus_import" if __package__ else "corpus_import"
|
|
1335
|
+
importer = importlib.import_module(name)
|
|
1336
|
+
payload = importer.sanitize_import_payload(payload)
|
|
1337
|
+
prepared_import = importer.prepare_items(self, payload, user)
|
|
1338
|
+
allocated_many, reserved = [], _copy_json(user)
|
|
1339
|
+
for _item in prepared_import["items"]:
|
|
1340
|
+
item_id = self._next_personal_id(reserved)
|
|
1341
|
+
allocated_many.append(item_id)
|
|
1342
|
+
reserved["items"][f"{LOCAL_PACKAGE}:{item_id}"] = {}
|
|
1343
|
+
next_runtime, next_user, details = self._prepare_operation(payload, runtime, user, allocated_item_id=allocated,
|
|
1344
|
+
allocated_item_ids=allocated_many)
|
|
1214
1345
|
self._validate_candidate_projection(next_runtime, next_user)
|
|
1215
1346
|
after = self._authoring_revision(next_runtime, next_user)
|
|
1216
1347
|
plan_id = uuid.uuid4().hex
|
|
@@ -1222,6 +1353,8 @@ class CorpusStore:
|
|
|
1222
1353
|
}
|
|
1223
1354
|
if allocated is not None:
|
|
1224
1355
|
plan["allocated_item_id"] = allocated
|
|
1356
|
+
if allocated_many is not None:
|
|
1357
|
+
plan["allocated_item_ids"] = allocated_many
|
|
1225
1358
|
self._write_transaction(plan_id, {"state": "PLANNED", "plan": plan})
|
|
1226
1359
|
return {key: plan[key] for key in ("schema_version", "plan_id", "expected_revision", "result_revision", "details")}
|
|
1227
1360
|
|
|
@@ -1251,11 +1384,16 @@ class CorpusStore:
|
|
|
1251
1384
|
# Persisted pre-allocation plans already resolved their identity in after-state.
|
|
1252
1385
|
ref = plan.get("details", {}).get("ref")
|
|
1253
1386
|
allocated = ref.split(":", 1)[1] if isinstance(ref, str) and ref.startswith(LOCAL_PACKAGE + ":") else None
|
|
1254
|
-
next_runtime, next_user, details = self._prepare_operation(plan["payload"], runtime, user, allocated_item_id=allocated
|
|
1387
|
+
next_runtime, next_user, details = self._prepare_operation(plan["payload"], runtime, user, allocated_item_id=allocated,
|
|
1388
|
+
allocated_item_ids=plan.get("allocated_item_ids"))
|
|
1255
1389
|
self._validate_candidate_projection(next_runtime, next_user)
|
|
1256
1390
|
result = self._authoring_revision(next_runtime, next_user)
|
|
1257
1391
|
if result != plan.get("result_revision"):
|
|
1258
1392
|
raise CorpusStoreError("plan result changed during apply")
|
|
1393
|
+
if details.get("operation") == "import":
|
|
1394
|
+
import importlib
|
|
1395
|
+
name = "compose.corpus_import" if __package__ else "corpus_import"
|
|
1396
|
+
importlib.import_module(name).verify_import_sources(self, details)
|
|
1259
1397
|
prepared = {"state": "PREPARED", "plan": plan, "prior_revision": current, "prepared_at": _utcnow()}
|
|
1260
1398
|
history_id = f"{prepared['prepared_at'].replace(':', '').replace('+00:00', 'Z')}-{plan_id[:12]}"
|
|
1261
1399
|
prepared["history_id"] = history_id
|
|
@@ -1279,18 +1417,42 @@ class CorpusStore:
|
|
|
1279
1417
|
# ---- immutable snapshots and history -----------------------------------
|
|
1280
1418
|
|
|
1281
1419
|
def _selected_items(self, items: list[dict[str, Any]], selection: list[str] | None,
|
|
1282
|
-
overrides: dict[str, bool] | None = None
|
|
1420
|
+
overrides: dict[str, bool] | None = None, *, mode: str = "default",
|
|
1421
|
+
host: str | None = None, cwd: str | Path | None = None) -> list[dict[str, Any]]:
|
|
1422
|
+
self._selection_mode({}, {}, mode)
|
|
1423
|
+
if mode == "none":
|
|
1424
|
+
return []
|
|
1283
1425
|
overrides = overrides or {}
|
|
1426
|
+
working = Path(cwd or Path.cwd()).resolve()
|
|
1284
1427
|
selected: list[dict[str, Any]] = []
|
|
1285
1428
|
for item in items:
|
|
1286
|
-
|
|
1429
|
+
origin = item.get("origin", {})
|
|
1430
|
+
if origin.get("type") == "instruction_import":
|
|
1431
|
+
scope = origin.get("scope", {})
|
|
1432
|
+
if not isinstance(scope, dict) or scope.get("kind") not in {"global", "project"}:
|
|
1433
|
+
raise ValidationError("imported item has invalid source scope")
|
|
1434
|
+
hosts = origin.get("hosts", ["claude", "codex"])
|
|
1435
|
+
if not isinstance(hosts, list) or not hosts or any(value not in {"claude", "codex"} for value in hosts):
|
|
1436
|
+
raise ValidationError("imported item has invalid host scope")
|
|
1437
|
+
if host is not None and host not in hosts:
|
|
1438
|
+
continue
|
|
1439
|
+
if scope["kind"] == "project":
|
|
1440
|
+
root = scope.get("root")
|
|
1441
|
+
if not isinstance(root, str) or not Path(root).is_absolute():
|
|
1442
|
+
raise ValidationError("imported project root must be absolute")
|
|
1443
|
+
project_root = Path(root)
|
|
1444
|
+
if project_root.resolve() != project_root:
|
|
1445
|
+
raise ValidationError("imported project root is no longer canonical; review its scope")
|
|
1446
|
+
if not working.is_relative_to(project_root):
|
|
1447
|
+
continue
|
|
1448
|
+
if item["ref"] in overrides and (mode == "default" or overrides[item["ref"]] is False):
|
|
1287
1449
|
if overrides[item["ref"]]:
|
|
1288
1450
|
selected.append(item)
|
|
1289
1451
|
continue
|
|
1290
1452
|
if selection and "all" in selection:
|
|
1291
1453
|
selected.append(item)
|
|
1292
1454
|
continue
|
|
1293
|
-
if item.get("tier") in {"core", "infra"}:
|
|
1455
|
+
if mode == "default" and item.get("tier") in {"core", "infra"}:
|
|
1294
1456
|
selected.append(item)
|
|
1295
1457
|
continue
|
|
1296
1458
|
if not selection:
|
|
@@ -1303,7 +1465,8 @@ class CorpusStore:
|
|
|
1303
1465
|
return selected
|
|
1304
1466
|
|
|
1305
1467
|
def snapshot(self, host: str, selection: list[str] | None = None, dry_run: bool = False,
|
|
1306
|
-
native: bool = False
|
|
1468
|
+
native: bool = False, *, selection_mode: str | None = None,
|
|
1469
|
+
cwd: str | Path | None = None) -> dict[str, Any]:
|
|
1307
1470
|
"""Compose an immutable activated-session snapshot.
|
|
1308
1471
|
|
|
1309
1472
|
A dry run has no durable write path: it compiles in an OS temporary
|
|
@@ -1327,8 +1490,16 @@ class CorpusStore:
|
|
|
1327
1490
|
items, inventory, defaults, baseline_ref = self._effective_items(runtime, user, host=host)
|
|
1328
1491
|
active = [item for item in items if item.get("active", True) is not False]
|
|
1329
1492
|
effective_selection = self._effective_selection(user, defaults, selection)
|
|
1330
|
-
self.
|
|
1331
|
-
|
|
1493
|
+
mode = self._selection_mode(user, defaults, selection_mode)
|
|
1494
|
+
if selection is not None and selection_mode is None and mode == "none":
|
|
1495
|
+
mode = "default"
|
|
1496
|
+
if mode == "none":
|
|
1497
|
+
effective_selection = []
|
|
1498
|
+
working = Path(cwd or Path.cwd()).resolve()
|
|
1499
|
+
self._validate_snapshot_selection(effective_selection, inventory,
|
|
1500
|
+
self._selection_subjects(runtime, user, active, effective_selection))
|
|
1501
|
+
selected = self._selected_items(active, effective_selection, self._enabled_overrides(user),
|
|
1502
|
+
mode=mode, host=host, cwd=working)
|
|
1332
1503
|
self._require_resolved(selected)
|
|
1333
1504
|
selected, promotion_warnings = self._resolve_promotions(selected, user, host, baseline_ref)
|
|
1334
1505
|
bootstrap_path = self.repo / "compose" / "bootstrap" / "SKILL.md"
|
|
@@ -1338,6 +1509,7 @@ class CorpusStore:
|
|
|
1338
1509
|
inputs = {
|
|
1339
1510
|
"schema_version": SCHEMA_VERSION, "host": host, "baseline_ref": baseline_ref,
|
|
1340
1511
|
"selection": effective_selection,
|
|
1512
|
+
"selection_mode": mode, "cwd": str(working),
|
|
1341
1513
|
"selection_digest": _digest(effective_selection),
|
|
1342
1514
|
"authoring_revision": self._authoring_revision(runtime, user),
|
|
1343
1515
|
"item_digests": {item["ref"]: _digest(item) for item in sorted(selected, key=lambda x: x["ref"])},
|
|
@@ -1379,14 +1551,21 @@ class CorpusStore:
|
|
|
1379
1551
|
files = _snapshot_relative_paths(compiled.get("files"))
|
|
1380
1552
|
output = {
|
|
1381
1553
|
"instruction_text": compiled.get("instruction_text", ""),
|
|
1382
|
-
"files": sorted(set(files) | {"bootstrap/SKILL.md"}), "item_refs": compiled.get("item_refs", []),
|
|
1554
|
+
"files": sorted(set(files) | ({"bootstrap/SKILL.md"} if mode != "none" else set())), "item_refs": compiled.get("item_refs", []),
|
|
1383
1555
|
"unavailable": compiled.get("unavailable", []) + promotion_warnings,
|
|
1384
1556
|
}
|
|
1385
1557
|
assets = _snapshot_assets(compiled.get("assets", {}), files)
|
|
1386
1558
|
if assets:
|
|
1387
1559
|
output["assets"] = assets
|
|
1388
|
-
invocation = f"Corpus management: invoke $
|
|
1389
|
-
|
|
1560
|
+
invocation = (f"Corpus management: invoke $agent-bios using {root / 'bootstrap' / 'SKILL.md'}."
|
|
1561
|
+
if mode != "none" else "")
|
|
1562
|
+
if mode == "none":
|
|
1563
|
+
output["instruction_text"] = ""
|
|
1564
|
+
for relative in files:
|
|
1565
|
+
if relative == "launch-content/instructions.md":
|
|
1566
|
+
(staging / relative).write_text("", encoding="utf-8")
|
|
1567
|
+
else:
|
|
1568
|
+
output["instruction_text"] = output["instruction_text"].rstrip() + "\n\n" + invocation + "\n"
|
|
1390
1569
|
for plugin in assets.get("claude_plugins", []):
|
|
1391
1570
|
for agent_path in (staging / plugin / "agents").glob("*.md"):
|
|
1392
1571
|
with agent_path.open("a", encoding="utf-8") as agent_file:
|
|
@@ -1394,9 +1573,10 @@ class CorpusStore:
|
|
|
1394
1573
|
if dry_run:
|
|
1395
1574
|
output["instruction_text"] = output["instruction_text"].replace(str(staging), str(root))
|
|
1396
1575
|
else:
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1576
|
+
if mode != "none":
|
|
1577
|
+
bootstrap_target = staging / "bootstrap" / "SKILL.md"
|
|
1578
|
+
bootstrap_target.parent.mkdir(parents=True, exist_ok=True)
|
|
1579
|
+
bootstrap_target.write_bytes(bootstrap_path.read_bytes())
|
|
1400
1580
|
_rewrite_staged_paths(staging, root)
|
|
1401
1581
|
output["instruction_text"] = output["instruction_text"].replace(str(staging), str(root))
|
|
1402
1582
|
_atomic_write(staging / "inventory.json", {"inputs": inputs, "items": selected})
|
|
@@ -1419,6 +1599,7 @@ class CorpusStore:
|
|
|
1419
1599
|
shutil.rmtree(staging)
|
|
1420
1600
|
return {"content_ref": content_ref, "path": str(root), "instruction_text": output["instruction_text"],
|
|
1421
1601
|
"revision": inputs["authoring_revision"], "unavailable": output.get("unavailable", []),
|
|
1602
|
+
"item_refs": output.get("item_refs", []), "selection_mode": mode,
|
|
1422
1603
|
"assets": output.get("assets", {})}
|
|
1423
1604
|
|
|
1424
1605
|
def history(self, ref: str | None = None) -> list[dict[str, Any]]:
|
|
@@ -14,6 +14,7 @@ import hashlib
|
|
|
14
14
|
import json
|
|
15
15
|
import os
|
|
16
16
|
from pathlib import Path
|
|
17
|
+
import stat
|
|
17
18
|
import sys
|
|
18
19
|
import threading
|
|
19
20
|
from typing import Any, Iterator
|
|
@@ -63,34 +64,66 @@ def _depths(name: str) -> dict[str, int]:
|
|
|
63
64
|
|
|
64
65
|
|
|
65
66
|
@contextlib.contextmanager
|
|
66
|
-
def
|
|
67
|
-
"""The single re-entrant cross-process lock for store and installer work."""
|
|
67
|
+
def _transaction_lock(state_root: Path, *, readonly: bool) -> Iterator[bool]:
|
|
68
68
|
root = Path(state_root).expanduser()
|
|
69
69
|
if root.is_symlink():
|
|
70
70
|
raise TransactionError(f"unsafe corpus state root: {root}")
|
|
71
|
+
if readonly:
|
|
72
|
+
reject_symlink_ancestors(root)
|
|
71
73
|
key = _key(root)
|
|
72
74
|
depths = _depths("lock_depths")
|
|
73
75
|
if depths.get(key, 0):
|
|
74
76
|
depths[key] += 1
|
|
75
77
|
try:
|
|
76
|
-
yield
|
|
78
|
+
yield True
|
|
77
79
|
finally:
|
|
78
80
|
depths[key] -= 1
|
|
79
81
|
return
|
|
80
|
-
|
|
82
|
+
if not readonly:
|
|
83
|
+
root.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
81
84
|
lock = root / ".corpus-store.lock"
|
|
82
85
|
if lock.is_symlink():
|
|
83
86
|
raise TransactionError(f"unsafe corpus transaction lock: {lock}")
|
|
84
|
-
flags = os.O_CREAT | os.O_RDWR | getattr(os, "O_NOFOLLOW", 0)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
flags = (os.O_RDONLY | os.O_NONBLOCK if readonly else os.O_CREAT | os.O_RDWR) | getattr(os, "O_NOFOLLOW", 0)
|
|
88
|
+
try:
|
|
89
|
+
descriptor = os.open(lock, flags, 0o600)
|
|
90
|
+
except FileNotFoundError:
|
|
91
|
+
if not readonly:
|
|
92
|
+
raise
|
|
93
|
+
yield False
|
|
94
|
+
return
|
|
95
|
+
try:
|
|
96
|
+
if readonly and not stat.S_ISREG(os.fstat(descriptor).st_mode):
|
|
97
|
+
raise TransactionError(f"unsafe corpus transaction lock: {lock}")
|
|
98
|
+
try:
|
|
99
|
+
fcntl.flock(descriptor, fcntl.LOCK_EX | (fcntl.LOCK_NB if readonly else 0))
|
|
100
|
+
except BlockingIOError:
|
|
101
|
+
if not readonly:
|
|
102
|
+
raise
|
|
103
|
+
yield False
|
|
104
|
+
return
|
|
88
105
|
depths[key] = 1
|
|
89
106
|
try:
|
|
90
|
-
yield
|
|
107
|
+
yield True
|
|
91
108
|
finally:
|
|
92
109
|
depths.pop(key, None)
|
|
93
|
-
fcntl.flock(
|
|
110
|
+
fcntl.flock(descriptor, fcntl.LOCK_UN)
|
|
111
|
+
finally:
|
|
112
|
+
os.close(descriptor)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@contextlib.contextmanager
|
|
116
|
+
def transaction_lock(state_root: Path) -> Iterator[None]:
|
|
117
|
+
"""The single re-entrant cross-process lock for store and installer work."""
|
|
118
|
+
with _transaction_lock(state_root, readonly=False):
|
|
119
|
+
yield
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@contextlib.contextmanager
|
|
123
|
+
def try_transaction_lock(state_root: Path) -> Iterator[bool]:
|
|
124
|
+
"""Observe under the existing lock, or defer without blocking or creating state."""
|
|
125
|
+
with _transaction_lock(state_root, readonly=True) as acquired:
|
|
126
|
+
yield acquired
|
|
94
127
|
|
|
95
128
|
|
|
96
129
|
@contextlib.contextmanager
|