agent-bios 0.17.1 → 0.19.0
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 +184 -486
- 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/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 +23 -2
- 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 +196 -18
- package/compose/corpus_session.py +27 -0
- package/compose/corpus_setup.py +674 -0
- package/compose/corpus_setup_cli.py +582 -0
- package/compose/corpus_setup_i18n.py +318 -0
- package/compose/corpus_setup_ui.py +633 -0
- package/compose/corpus_store.py +236 -34
- package/compose/corpus_transaction.py +43 -10
- package/compose/corpus_ui.py +279 -8
- package/compose/corpus_ui_runtime.py +278 -0
- package/compose/corpus_understand.py +6 -1
- package/compose/setup/START.md +147 -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 +190 -0
- package/docs/understand.md +40 -0
- package/install.sh +75 -46
- package/launch/agent-launch.py +91 -47
- 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
|
@@ -37,6 +37,7 @@ ITEM_FIELDS = {
|
|
|
37
37
|
RUNTIME_FIELDS = {
|
|
38
38
|
"digest", "revision", "content_ref", "path", "state", "created_at",
|
|
39
39
|
"updated_at", "baseline_ref", "plan_id", "history_id",
|
|
40
|
+
"enabled", "enabled_override",
|
|
40
41
|
}
|
|
41
42
|
LEARNING_FIELDS = {"schema_version", "learning_id", "lesson", "domain", "created", "supporting_sessions",
|
|
42
43
|
"criteria", "classification", "proposed_domain", "context"}
|
|
@@ -307,8 +308,28 @@ class CorpusStore:
|
|
|
307
308
|
for field, typ in (("items", dict), ("overrides", dict), ("tombstones", dict), ("learning_suppressions", dict)):
|
|
308
309
|
if not isinstance(state.get(field), typ):
|
|
309
310
|
raise CorpusStoreError(f"personal state has invalid {field}")
|
|
311
|
+
# Optional, so reading an older state does not change its revision or the
|
|
312
|
+
# exact before/after documents used by prepared-transaction recovery.
|
|
313
|
+
self._enabled_overrides(state)
|
|
314
|
+
self._selection_mode(state, {})
|
|
310
315
|
return state
|
|
311
316
|
|
|
317
|
+
@staticmethod
|
|
318
|
+
def _enabled_overrides(user: dict[str, Any]) -> dict[str, bool]:
|
|
319
|
+
values = user.get("enabled_overrides", {})
|
|
320
|
+
if (not isinstance(values, dict) or any(
|
|
321
|
+
not isinstance(ref, str) or ":" not in ref or not isinstance(value, bool)
|
|
322
|
+
for ref, value in values.items())):
|
|
323
|
+
raise CorpusStoreError("personal state has invalid enabled_overrides")
|
|
324
|
+
return values
|
|
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
|
+
|
|
312
333
|
def _write_transaction(self, tx_id: str, record: dict[str, Any]) -> None:
|
|
313
334
|
_atomic_write(self.runtime / "transactions" / tx_id / "journal.json", record)
|
|
314
335
|
|
|
@@ -351,7 +372,7 @@ class CorpusStore:
|
|
|
351
372
|
journal["history_id"] = history_id
|
|
352
373
|
_atomic_write(journal_path, journal)
|
|
353
374
|
history = {"runtime": prior["runtime"], "user": prior["user"],
|
|
354
|
-
"revision": plan.get("expected_revision")}
|
|
375
|
+
"revision": plan.get("expected_revision"), "details": plan.get("details", {})}
|
|
355
376
|
history_path = self.user_root / "history" / history_id / "state.json"
|
|
356
377
|
if not history_path.exists():
|
|
357
378
|
_atomic_write(history_path, history)
|
|
@@ -632,7 +653,7 @@ class CorpusStore:
|
|
|
632
653
|
raise ValidationError("selection must be a list of qualified refs/domains")
|
|
633
654
|
packages = {p.get("package_id") for p in inventory.get("packages", []) if isinstance(p, dict)}
|
|
634
655
|
for value in selection:
|
|
635
|
-
if value == "all":
|
|
656
|
+
if value == "all" or value in packages:
|
|
636
657
|
continue
|
|
637
658
|
if ":" in value:
|
|
638
659
|
continue
|
|
@@ -691,6 +712,18 @@ class CorpusStore:
|
|
|
691
712
|
if package not in packages or domain not in packages[package]:
|
|
692
713
|
raise ValidationError(f"unknown corpus selection domain: {value}")
|
|
693
714
|
|
|
715
|
+
def _selection_subjects(self, runtime: dict[str, Any], user: dict[str, Any],
|
|
716
|
+
items: list[dict[str, Any]], selection: list[str] | None) -> list[dict[str, Any]]:
|
|
717
|
+
"""Validate stored host-qualified selections without delivering another host's items."""
|
|
718
|
+
subjects = list(items)
|
|
719
|
+
refs = {item["ref"] for item in subjects}
|
|
720
|
+
for host in ("claude", "codex"):
|
|
721
|
+
prefix = f"@local/learnings-{host}:"
|
|
722
|
+
if any(value.startswith(prefix) and value not in refs for value in selection or []):
|
|
723
|
+
subjects.extend(item for item in self._effective_items(runtime, user, host=host)[0]
|
|
724
|
+
if item.get("active", True))
|
|
725
|
+
return subjects
|
|
726
|
+
|
|
694
727
|
def _validate_candidate_projection(self, runtime: dict[str, Any], user: dict[str, Any]) -> None:
|
|
695
728
|
"""Use the real compiler as a plan validator without publishing output."""
|
|
696
729
|
catalog = self._catalog_module()
|
|
@@ -700,8 +733,10 @@ class CorpusStore:
|
|
|
700
733
|
items, _inventory, defaults, _baseline = self._effective_items(runtime, user, host=host)
|
|
701
734
|
active = [item for item in items if item.get("active", True) is not False]
|
|
702
735
|
selection = self._effective_selection(user, defaults, None)
|
|
703
|
-
self._validate_snapshot_selection(selection, _inventory,
|
|
704
|
-
|
|
736
|
+
self._validate_snapshot_selection(selection, _inventory,
|
|
737
|
+
self._selection_subjects(runtime, user, active, selection))
|
|
738
|
+
selected = self._selected_items(active, selection, self._enabled_overrides(user),
|
|
739
|
+
mode=self._selection_mode(user, defaults), host=host)
|
|
705
740
|
self._require_resolved(selected)
|
|
706
741
|
catalog.compile_items(_copy_json(selected), Path(temp) / host, host)
|
|
707
742
|
except Exception as exc:
|
|
@@ -789,12 +824,15 @@ class CorpusStore:
|
|
|
789
824
|
|
|
790
825
|
# ---- public read API ----------------------------------------------------
|
|
791
826
|
|
|
792
|
-
def install(self, domains: list[str] | None = None
|
|
827
|
+
def install(self, domains: list[str] | None = None, *, selection_mode: str | None = None,
|
|
828
|
+
replace_selection: bool = False) -> dict[str, Any]:
|
|
793
829
|
"""Install one immutable validated baseline tuple without touching user data."""
|
|
794
830
|
with self._lock():
|
|
795
|
-
return self.commit_install(self.prepare_install(domains
|
|
831
|
+
return self.commit_install(self.prepare_install(domains, selection_mode=selection_mode,
|
|
832
|
+
replace_selection=replace_selection))
|
|
796
833
|
|
|
797
|
-
def prepare_install(self, domains: list[str] | None = None
|
|
834
|
+
def prepare_install(self, domains: list[str] | None = None, *, selection_mode: str | None = None,
|
|
835
|
+
replace_selection: bool = False) -> dict[str, Any]:
|
|
798
836
|
"""Stage a validated baseline and source plan without advancing any pointer."""
|
|
799
837
|
with self._lock():
|
|
800
838
|
self._recover_locked()
|
|
@@ -807,9 +845,15 @@ class CorpusStore:
|
|
|
807
845
|
items = [self._validate_item(item, allow_origin=True) for item in raw_items]
|
|
808
846
|
if len({item["ref"] for item in items}) != len(items):
|
|
809
847
|
raise ValidationError("catalog has duplicate corpus refs")
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
848
|
+
mode = self._selection_mode({}, {}, selection_mode)
|
|
849
|
+
selected = self._normalized_install_selection(domains, catalog)
|
|
850
|
+
if mode == "none" and selected:
|
|
851
|
+
raise ValidationError("no-corpus installation cannot include selection targets")
|
|
852
|
+
defaults = {"schema_version": SCHEMA_VERSION, "selection": selected}
|
|
853
|
+
if mode == "default":
|
|
854
|
+
defaults["selection"] += [LOCAL_PACKAGE, "@local/learnings-claude", "@local/learnings-codex"]
|
|
855
|
+
if selection_mode is not None:
|
|
856
|
+
defaults["selection_mode"] = mode
|
|
813
857
|
promotion_path = self.repo / "learn" / "promotions.json"
|
|
814
858
|
promotions = _json_read(promotion_path, {"version": 0, "promotions": []})
|
|
815
859
|
if not isinstance(promotions, dict) or not isinstance(promotions.get("promotions"), list):
|
|
@@ -836,6 +880,11 @@ class CorpusStore:
|
|
|
836
880
|
# successful-install record changes.
|
|
837
881
|
next_user = self._rebase_overlays(old_inventory, catalog, user)
|
|
838
882
|
before = {"runtime": _copy_json(runtime), "user": _copy_json(user)}
|
|
883
|
+
if replace_selection:
|
|
884
|
+
next_user = _copy_json(next_user)
|
|
885
|
+
next_user["selection"] = _copy_json(defaults["selection"])
|
|
886
|
+
next_user["selection_mode"] = mode
|
|
887
|
+
next_user.pop("enabled_overrides", None)
|
|
839
888
|
_atomic_write(root / "inventory.json", catalog)
|
|
840
889
|
_atomic_write(root / "defaults.json", defaults)
|
|
841
890
|
_atomic_write(root / "promotions.json", promotions)
|
|
@@ -896,18 +945,26 @@ class CorpusStore:
|
|
|
896
945
|
"revision": revision, "baseline_count": len(refs),
|
|
897
946
|
"personal_items": len(user["items"]), "overrides": len(user["overrides"]),
|
|
898
947
|
"tombstones": len(user["tombstones"]), "selection": self._effective_selection(user, defaults, None),
|
|
948
|
+
"selection_mode": self._selection_mode(user, defaults),
|
|
949
|
+
"enabled_overrides": _copy_json(self._enabled_overrides(user)),
|
|
899
950
|
}
|
|
900
951
|
|
|
901
952
|
def list_items(self, include_removed: bool = True) -> list[dict[str, Any]]:
|
|
902
953
|
with self._lock():
|
|
903
954
|
self._recover_locked()
|
|
904
955
|
runtime, user = self._runtime_state(), self._user_state()
|
|
905
|
-
selected_ref, inventory,
|
|
956
|
+
selected_ref, inventory, defaults = self._selected_baseline(runtime)
|
|
906
957
|
all_items, _, _, _ = self._effective_items(runtime, user)
|
|
907
958
|
effective = {item["ref"]: item for item in all_items}
|
|
908
959
|
for host in ("claude", "codex"):
|
|
909
960
|
effective.update({item["ref"]: item for item in self._effective_items(runtime, user, host=host, include_suppressed=True)[0]})
|
|
910
961
|
source = {item["ref"]: self._validate_item(item, allow_origin=True) for item in inventory["items"]}
|
|
962
|
+
overrides = self._enabled_overrides(user)
|
|
963
|
+
selection = self._effective_selection(user, defaults, None)
|
|
964
|
+
enabled = {item["ref"] for item in self._selected_items(
|
|
965
|
+
[item for item in effective.values() if item.get("active", True) is not False], selection, overrides,
|
|
966
|
+
mode=self._selection_mode(user, defaults))}
|
|
967
|
+
revision = self._authoring_revision(runtime, user)
|
|
911
968
|
rows: list[dict[str, Any]] = []
|
|
912
969
|
for ref in sorted(set(source) | set(user["items"]) | set(effective)):
|
|
913
970
|
base = source.get(ref)
|
|
@@ -919,6 +976,9 @@ class CorpusStore:
|
|
|
919
976
|
row["state"] = "removed" if removed else ("conflict" if item and (item.get("conflict") or item.get("content_conflict")) else "active")
|
|
920
977
|
row["digest"] = _digest(item or base or user["items"][ref])
|
|
921
978
|
row["baseline_ref"] = selected_ref if base else None
|
|
979
|
+
row["enabled"] = not removed and ref in enabled
|
|
980
|
+
row["enabled_override"] = overrides.get(ref)
|
|
981
|
+
row["revision"] = revision
|
|
922
982
|
rows.append(row)
|
|
923
983
|
return rows
|
|
924
984
|
|
|
@@ -944,7 +1004,11 @@ class CorpusStore:
|
|
|
944
1004
|
return {"ref": ref, "history": self.history(ref)}
|
|
945
1005
|
removed = ref in user["tombstones"] or (effective is not None and effective.get("active", True) is False)
|
|
946
1006
|
state = "removed" if removed else ("conflict" if effective and (effective.get("conflict") or effective.get("content_conflict")) else "active")
|
|
947
|
-
|
|
1007
|
+
overrides = self._enabled_overrides(user)
|
|
1008
|
+
enabled = bool(effective and not removed and self._selected_items(
|
|
1009
|
+
[effective], self._effective_selection(user, _defaults, None), overrides))
|
|
1010
|
+
return {"ref": ref, "item": effective, "digest": _digest(effective) if effective else None, "state": state,
|
|
1011
|
+
"enabled": enabled, "enabled_override": overrides.get(ref)}
|
|
948
1012
|
|
|
949
1013
|
# ---- plans --------------------------------------------------------------
|
|
950
1014
|
|
|
@@ -967,11 +1031,12 @@ class CorpusStore:
|
|
|
967
1031
|
raise CorpusStoreError("could not allocate an unused personal identity")
|
|
968
1032
|
|
|
969
1033
|
def _prepare_operation(self, payload: dict[str, Any], runtime: dict[str, Any], user: dict[str, Any],
|
|
970
|
-
*, allocated_item_id: str | None = None
|
|
1034
|
+
*, allocated_item_id: str | None = None,
|
|
1035
|
+
allocated_item_ids: list[str] | None = None) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
|
|
971
1036
|
if not isinstance(payload, dict):
|
|
972
1037
|
raise ValidationError("plan payload must be an object")
|
|
973
1038
|
op = payload.get("operation", payload.get("op"))
|
|
974
|
-
if op not in {"create", "update", "remove", "restore", "recover", "reset", "rollback", "select"}:
|
|
1039
|
+
if not isinstance(op, str) or op not in {"create", "update", "remove", "restore", "recover", "reset", "rollback", "select", "enable", "import"}:
|
|
975
1040
|
raise ValidationError("unknown corpus operation")
|
|
976
1041
|
allowed = {
|
|
977
1042
|
"create": {"operation", "op", "item", "package_id", "expected_revision"},
|
|
@@ -981,7 +1046,9 @@ class CorpusStore:
|
|
|
981
1046
|
"recover": {"operation", "op", "ref", "expected_revision"},
|
|
982
1047
|
"reset": {"operation", "op", "expected_revision"},
|
|
983
1048
|
"rollback": {"operation", "op", "baseline_ref", "history_id", "expected_revision"},
|
|
984
|
-
"select": {"operation", "op", "selection", "expected_revision"},
|
|
1049
|
+
"select": {"operation", "op", "selection", "selection_mode", "expected_revision"},
|
|
1050
|
+
"enable": {"operation", "op", "items", "expected_revision"},
|
|
1051
|
+
"import": {"operation", "op", "capture_id", "candidates", "excluded", "expected_revision"},
|
|
985
1052
|
}[op]
|
|
986
1053
|
unknown = set(payload) - allowed
|
|
987
1054
|
if unknown:
|
|
@@ -994,7 +1061,31 @@ class CorpusStore:
|
|
|
994
1061
|
items, inventory, defaults, _baseline_ref = self._effective_items(runtime, user)
|
|
995
1062
|
effective = {item["ref"]: item for item in items}
|
|
996
1063
|
details: dict[str, Any] = {"operation": op}
|
|
997
|
-
if op == "
|
|
1064
|
+
if op == "import":
|
|
1065
|
+
import importlib
|
|
1066
|
+
name = "compose.corpus_import" if __package__ else "corpus_import"
|
|
1067
|
+
prepared_import = importlib.import_module(name).prepare_items(self, payload, user)
|
|
1068
|
+
refs = prepared_import["existing_refs"]
|
|
1069
|
+
receipt = prepared_import["receipt"]
|
|
1070
|
+
rows = prepared_import["items"]
|
|
1071
|
+
if rows:
|
|
1072
|
+
if allocated_item_ids is None or len(allocated_item_ids) != len(rows):
|
|
1073
|
+
raise ValidationError("import needs recorded runtime-owned identities")
|
|
1074
|
+
refs = []
|
|
1075
|
+
for raw, item_id in zip(rows, allocated_item_ids):
|
|
1076
|
+
_safe_part(item_id, "personal item id")
|
|
1077
|
+
raw = _copy_json(raw)
|
|
1078
|
+
raw.update(package_id=LOCAL_PACKAGE, item_id=item_id, ref=f"{LOCAL_PACKAGE}:{item_id}")
|
|
1079
|
+
item = self._validate_item(self._normalize_content(raw), allow_origin=True)
|
|
1080
|
+
if item["ref"] in effective or item["ref"] in next_user["items"]:
|
|
1081
|
+
raise ValidationError("import identity is already in use")
|
|
1082
|
+
next_user["items"][item["ref"]] = item
|
|
1083
|
+
refs.append(item["ref"])
|
|
1084
|
+
receipt["refs"] = refs
|
|
1085
|
+
receipt["item_digests"] = {ref: _digest(next_user["items"][ref]) for ref in refs}
|
|
1086
|
+
next_user.setdefault("imports", {})[receipt["request_digest"]] = receipt
|
|
1087
|
+
details.update(refs=refs, import_receipt=receipt, already_imported=not bool(rows))
|
|
1088
|
+
elif op == "create":
|
|
998
1089
|
raw = _copy_json(payload.get("item"))
|
|
999
1090
|
if not isinstance(raw, dict):
|
|
1000
1091
|
raise ValidationError("create needs item")
|
|
@@ -1114,8 +1205,41 @@ class CorpusStore:
|
|
|
1114
1205
|
else:
|
|
1115
1206
|
next_user["items"][ref].pop("active", None)
|
|
1116
1207
|
details["ref"] = ref
|
|
1208
|
+
elif op == "enable":
|
|
1209
|
+
choices = payload.get("items")
|
|
1210
|
+
if not isinstance(choices, dict) or not choices:
|
|
1211
|
+
raise ValidationError("enable needs a non-empty mapping of refs to booleans or null")
|
|
1212
|
+
available = dict(effective)
|
|
1213
|
+
for host in ("claude", "codex"):
|
|
1214
|
+
available.update({item["ref"]: item for item in self._effective_items(runtime, user, host=host)[0]})
|
|
1215
|
+
overrides = dict(self._enabled_overrides(user))
|
|
1216
|
+
known = set(available) | set(overrides) | set(user["items"]) | {item["ref"] for item in inventory["items"]}
|
|
1217
|
+
for ref, value in choices.items():
|
|
1218
|
+
if not isinstance(ref, str) or ref not in known:
|
|
1219
|
+
raise ValidationError(f"unknown corpus enablement ref: {ref}")
|
|
1220
|
+
if value is None:
|
|
1221
|
+
overrides.pop(ref, None)
|
|
1222
|
+
elif not isinstance(value, bool):
|
|
1223
|
+
raise ValidationError("enable values must be booleans or null")
|
|
1224
|
+
elif ref not in available or available[ref].get("active", True) is False:
|
|
1225
|
+
raise ValidationError("recover or restore a removed item before enabling it")
|
|
1226
|
+
else:
|
|
1227
|
+
overrides[ref] = value
|
|
1228
|
+
if overrides:
|
|
1229
|
+
next_user["enabled_overrides"] = overrides
|
|
1230
|
+
else:
|
|
1231
|
+
next_user.pop("enabled_overrides", None)
|
|
1232
|
+
details["items"] = _copy_json(choices)
|
|
1117
1233
|
elif op == "select":
|
|
1118
1234
|
selection = self._validate_selection(payload.get("selection"), inventory)
|
|
1235
|
+
if "selection_mode" in payload:
|
|
1236
|
+
if payload["selection_mode"] is None:
|
|
1237
|
+
raise ValidationError("selection_mode cannot be null")
|
|
1238
|
+
mode = self._selection_mode({}, {}, payload["selection_mode"])
|
|
1239
|
+
if mode == "none" and selection:
|
|
1240
|
+
raise ValidationError("no-corpus selection cannot include targets")
|
|
1241
|
+
next_user["selection_mode"] = mode
|
|
1242
|
+
details["selection_mode"] = mode
|
|
1119
1243
|
next_user["selection"] = selection
|
|
1120
1244
|
details["selection"] = selection
|
|
1121
1245
|
elif op == "reset":
|
|
@@ -1129,6 +1253,8 @@ class CorpusStore:
|
|
|
1129
1253
|
next_runtime["selected_baseline_ref"] = latest
|
|
1130
1254
|
_inv, latest_defaults = self._read_baseline(latest)
|
|
1131
1255
|
next_user["selection"] = latest_defaults.get("selection")
|
|
1256
|
+
if "selection_mode" in latest_defaults:
|
|
1257
|
+
next_user["selection_mode"] = latest_defaults["selection_mode"]
|
|
1132
1258
|
details["baseline_ref"] = latest
|
|
1133
1259
|
elif op == "rollback":
|
|
1134
1260
|
history_id = payload.get("history_id")
|
|
@@ -1157,8 +1283,22 @@ class CorpusStore:
|
|
|
1157
1283
|
self._recover_locked()
|
|
1158
1284
|
runtime, user = self._runtime_state(), self._user_state()
|
|
1159
1285
|
before = self._authoring_revision(runtime, user)
|
|
1160
|
-
|
|
1161
|
-
|
|
1286
|
+
op = payload.get("operation", payload.get("op")) if isinstance(payload, dict) else None
|
|
1287
|
+
allocated = self._next_personal_id(user) if op == "create" else None
|
|
1288
|
+
allocated_many = None
|
|
1289
|
+
if op == "import":
|
|
1290
|
+
import importlib
|
|
1291
|
+
name = "compose.corpus_import" if __package__ else "corpus_import"
|
|
1292
|
+
importer = importlib.import_module(name)
|
|
1293
|
+
payload = importer.sanitize_import_payload(payload)
|
|
1294
|
+
prepared_import = importer.prepare_items(self, payload, user)
|
|
1295
|
+
allocated_many, reserved = [], _copy_json(user)
|
|
1296
|
+
for _item in prepared_import["items"]:
|
|
1297
|
+
item_id = self._next_personal_id(reserved)
|
|
1298
|
+
allocated_many.append(item_id)
|
|
1299
|
+
reserved["items"][f"{LOCAL_PACKAGE}:{item_id}"] = {}
|
|
1300
|
+
next_runtime, next_user, details = self._prepare_operation(payload, runtime, user, allocated_item_id=allocated,
|
|
1301
|
+
allocated_item_ids=allocated_many)
|
|
1162
1302
|
self._validate_candidate_projection(next_runtime, next_user)
|
|
1163
1303
|
after = self._authoring_revision(next_runtime, next_user)
|
|
1164
1304
|
plan_id = uuid.uuid4().hex
|
|
@@ -1170,6 +1310,8 @@ class CorpusStore:
|
|
|
1170
1310
|
}
|
|
1171
1311
|
if allocated is not None:
|
|
1172
1312
|
plan["allocated_item_id"] = allocated
|
|
1313
|
+
if allocated_many is not None:
|
|
1314
|
+
plan["allocated_item_ids"] = allocated_many
|
|
1173
1315
|
self._write_transaction(plan_id, {"state": "PLANNED", "plan": plan})
|
|
1174
1316
|
return {key: plan[key] for key in ("schema_version", "plan_id", "expected_revision", "result_revision", "details")}
|
|
1175
1317
|
|
|
@@ -1199,16 +1341,22 @@ class CorpusStore:
|
|
|
1199
1341
|
# Persisted pre-allocation plans already resolved their identity in after-state.
|
|
1200
1342
|
ref = plan.get("details", {}).get("ref")
|
|
1201
1343
|
allocated = ref.split(":", 1)[1] if isinstance(ref, str) and ref.startswith(LOCAL_PACKAGE + ":") else None
|
|
1202
|
-
next_runtime, next_user, details = self._prepare_operation(plan["payload"], runtime, user, allocated_item_id=allocated
|
|
1344
|
+
next_runtime, next_user, details = self._prepare_operation(plan["payload"], runtime, user, allocated_item_id=allocated,
|
|
1345
|
+
allocated_item_ids=plan.get("allocated_item_ids"))
|
|
1203
1346
|
self._validate_candidate_projection(next_runtime, next_user)
|
|
1204
1347
|
result = self._authoring_revision(next_runtime, next_user)
|
|
1205
1348
|
if result != plan.get("result_revision"):
|
|
1206
1349
|
raise CorpusStoreError("plan result changed during apply")
|
|
1350
|
+
if details.get("operation") == "import":
|
|
1351
|
+
import importlib
|
|
1352
|
+
name = "compose.corpus_import" if __package__ else "corpus_import"
|
|
1353
|
+
importlib.import_module(name).verify_import_sources(self, details)
|
|
1207
1354
|
prepared = {"state": "PREPARED", "plan": plan, "prior_revision": current, "prepared_at": _utcnow()}
|
|
1208
1355
|
history_id = f"{prepared['prepared_at'].replace(':', '').replace('+00:00', 'Z')}-{plan_id[:12]}"
|
|
1209
1356
|
prepared["history_id"] = history_id
|
|
1210
1357
|
self._write_transaction(plan_id, prepared)
|
|
1211
|
-
_atomic_write(self.user_root / "history" / history_id / "state.json",
|
|
1358
|
+
_atomic_write(self.user_root / "history" / history_id / "state.json",
|
|
1359
|
+
{"runtime": runtime, "user": user, "revision": current, "details": details})
|
|
1212
1360
|
if details.get("operation") == "reset":
|
|
1213
1361
|
_atomic_write(self.user_root / "trash" / history_id / "state.json", {"runtime": runtime, "user": user, "revision": current})
|
|
1214
1362
|
_atomic_write(self._user_state_path, next_user)
|
|
@@ -1225,12 +1373,43 @@ class CorpusStore:
|
|
|
1225
1373
|
|
|
1226
1374
|
# ---- immutable snapshots and history -----------------------------------
|
|
1227
1375
|
|
|
1228
|
-
def _selected_items(self, items: list[dict[str, Any]], selection: list[str] | None
|
|
1229
|
-
|
|
1230
|
-
|
|
1376
|
+
def _selected_items(self, items: list[dict[str, Any]], selection: list[str] | None,
|
|
1377
|
+
overrides: dict[str, bool] | None = None, *, mode: str = "default",
|
|
1378
|
+
host: str | None = None, cwd: str | Path | None = None) -> list[dict[str, Any]]:
|
|
1379
|
+
self._selection_mode({}, {}, mode)
|
|
1380
|
+
if mode == "none":
|
|
1381
|
+
return []
|
|
1382
|
+
overrides = overrides or {}
|
|
1383
|
+
working = Path(cwd or Path.cwd()).resolve()
|
|
1231
1384
|
selected: list[dict[str, Any]] = []
|
|
1232
1385
|
for item in items:
|
|
1233
|
-
|
|
1386
|
+
origin = item.get("origin", {})
|
|
1387
|
+
if origin.get("type") == "instruction_import":
|
|
1388
|
+
scope = origin.get("scope", {})
|
|
1389
|
+
if not isinstance(scope, dict) or scope.get("kind") not in {"global", "project"}:
|
|
1390
|
+
raise ValidationError("imported item has invalid source scope")
|
|
1391
|
+
hosts = origin.get("hosts", ["claude", "codex"])
|
|
1392
|
+
if not isinstance(hosts, list) or not hosts or any(value not in {"claude", "codex"} for value in hosts):
|
|
1393
|
+
raise ValidationError("imported item has invalid host scope")
|
|
1394
|
+
if host is not None and host not in hosts:
|
|
1395
|
+
continue
|
|
1396
|
+
if scope["kind"] == "project":
|
|
1397
|
+
root = scope.get("root")
|
|
1398
|
+
if not isinstance(root, str) or not Path(root).is_absolute():
|
|
1399
|
+
raise ValidationError("imported project root must be absolute")
|
|
1400
|
+
project_root = Path(root)
|
|
1401
|
+
if project_root.resolve() != project_root:
|
|
1402
|
+
raise ValidationError("imported project root is no longer canonical; review its scope")
|
|
1403
|
+
if not working.is_relative_to(project_root):
|
|
1404
|
+
continue
|
|
1405
|
+
if item["ref"] in overrides and (mode == "default" or overrides[item["ref"]] is False):
|
|
1406
|
+
if overrides[item["ref"]]:
|
|
1407
|
+
selected.append(item)
|
|
1408
|
+
continue
|
|
1409
|
+
if selection and "all" in selection:
|
|
1410
|
+
selected.append(item)
|
|
1411
|
+
continue
|
|
1412
|
+
if mode == "default" and item.get("tier") in {"core", "infra"}:
|
|
1234
1413
|
selected.append(item)
|
|
1235
1414
|
continue
|
|
1236
1415
|
if not selection:
|
|
@@ -1243,7 +1422,8 @@ class CorpusStore:
|
|
|
1243
1422
|
return selected
|
|
1244
1423
|
|
|
1245
1424
|
def snapshot(self, host: str, selection: list[str] | None = None, dry_run: bool = False,
|
|
1246
|
-
native: bool = False
|
|
1425
|
+
native: bool = False, *, selection_mode: str | None = None,
|
|
1426
|
+
cwd: str | Path | None = None) -> dict[str, Any]:
|
|
1247
1427
|
"""Compose an immutable activated-session snapshot.
|
|
1248
1428
|
|
|
1249
1429
|
A dry run has no durable write path: it compiles in an OS temporary
|
|
@@ -1267,8 +1447,16 @@ class CorpusStore:
|
|
|
1267
1447
|
items, inventory, defaults, baseline_ref = self._effective_items(runtime, user, host=host)
|
|
1268
1448
|
active = [item for item in items if item.get("active", True) is not False]
|
|
1269
1449
|
effective_selection = self._effective_selection(user, defaults, selection)
|
|
1270
|
-
self.
|
|
1271
|
-
|
|
1450
|
+
mode = self._selection_mode(user, defaults, selection_mode)
|
|
1451
|
+
if selection is not None and selection_mode is None and mode == "none":
|
|
1452
|
+
mode = "default"
|
|
1453
|
+
if mode == "none":
|
|
1454
|
+
effective_selection = []
|
|
1455
|
+
working = Path(cwd or Path.cwd()).resolve()
|
|
1456
|
+
self._validate_snapshot_selection(effective_selection, inventory,
|
|
1457
|
+
self._selection_subjects(runtime, user, active, effective_selection))
|
|
1458
|
+
selected = self._selected_items(active, effective_selection, self._enabled_overrides(user),
|
|
1459
|
+
mode=mode, host=host, cwd=working)
|
|
1272
1460
|
self._require_resolved(selected)
|
|
1273
1461
|
selected, promotion_warnings = self._resolve_promotions(selected, user, host, baseline_ref)
|
|
1274
1462
|
bootstrap_path = self.repo / "compose" / "bootstrap" / "SKILL.md"
|
|
@@ -1278,6 +1466,7 @@ class CorpusStore:
|
|
|
1278
1466
|
inputs = {
|
|
1279
1467
|
"schema_version": SCHEMA_VERSION, "host": host, "baseline_ref": baseline_ref,
|
|
1280
1468
|
"selection": effective_selection,
|
|
1469
|
+
"selection_mode": mode, "cwd": str(working),
|
|
1281
1470
|
"selection_digest": _digest(effective_selection),
|
|
1282
1471
|
"authoring_revision": self._authoring_revision(runtime, user),
|
|
1283
1472
|
"item_digests": {item["ref"]: _digest(item) for item in sorted(selected, key=lambda x: x["ref"])},
|
|
@@ -1319,14 +1508,21 @@ class CorpusStore:
|
|
|
1319
1508
|
files = _snapshot_relative_paths(compiled.get("files"))
|
|
1320
1509
|
output = {
|
|
1321
1510
|
"instruction_text": compiled.get("instruction_text", ""),
|
|
1322
|
-
"files": sorted(set(files) | {"bootstrap/SKILL.md"}), "item_refs": compiled.get("item_refs", []),
|
|
1511
|
+
"files": sorted(set(files) | ({"bootstrap/SKILL.md"} if mode != "none" else set())), "item_refs": compiled.get("item_refs", []),
|
|
1323
1512
|
"unavailable": compiled.get("unavailable", []) + promotion_warnings,
|
|
1324
1513
|
}
|
|
1325
1514
|
assets = _snapshot_assets(compiled.get("assets", {}), files)
|
|
1326
1515
|
if assets:
|
|
1327
1516
|
output["assets"] = assets
|
|
1328
|
-
invocation = f"Corpus management: invoke $
|
|
1329
|
-
|
|
1517
|
+
invocation = (f"Corpus management: invoke $agent-bios using {root / 'bootstrap' / 'SKILL.md'}."
|
|
1518
|
+
if mode != "none" else "")
|
|
1519
|
+
if mode == "none":
|
|
1520
|
+
output["instruction_text"] = ""
|
|
1521
|
+
for relative in files:
|
|
1522
|
+
if relative == "launch-content/instructions.md":
|
|
1523
|
+
(staging / relative).write_text("", encoding="utf-8")
|
|
1524
|
+
else:
|
|
1525
|
+
output["instruction_text"] = output["instruction_text"].rstrip() + "\n\n" + invocation + "\n"
|
|
1330
1526
|
for plugin in assets.get("claude_plugins", []):
|
|
1331
1527
|
for agent_path in (staging / plugin / "agents").glob("*.md"):
|
|
1332
1528
|
with agent_path.open("a", encoding="utf-8") as agent_file:
|
|
@@ -1334,9 +1530,10 @@ class CorpusStore:
|
|
|
1334
1530
|
if dry_run:
|
|
1335
1531
|
output["instruction_text"] = output["instruction_text"].replace(str(staging), str(root))
|
|
1336
1532
|
else:
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1533
|
+
if mode != "none":
|
|
1534
|
+
bootstrap_target = staging / "bootstrap" / "SKILL.md"
|
|
1535
|
+
bootstrap_target.parent.mkdir(parents=True, exist_ok=True)
|
|
1536
|
+
bootstrap_target.write_bytes(bootstrap_path.read_bytes())
|
|
1340
1537
|
_rewrite_staged_paths(staging, root)
|
|
1341
1538
|
output["instruction_text"] = output["instruction_text"].replace(str(staging), str(root))
|
|
1342
1539
|
_atomic_write(staging / "inventory.json", {"inputs": inputs, "items": selected})
|
|
@@ -1359,6 +1556,7 @@ class CorpusStore:
|
|
|
1359
1556
|
shutil.rmtree(staging)
|
|
1360
1557
|
return {"content_ref": content_ref, "path": str(root), "instruction_text": output["instruction_text"],
|
|
1361
1558
|
"revision": inputs["authoring_revision"], "unavailable": output.get("unavailable", []),
|
|
1559
|
+
"item_refs": output.get("item_refs", []), "selection_mode": mode,
|
|
1362
1560
|
"assets": output.get("assets", {})}
|
|
1363
1561
|
|
|
1364
1562
|
def history(self, ref: str | None = None) -> list[dict[str, Any]]:
|
|
@@ -1372,7 +1570,11 @@ class CorpusStore:
|
|
|
1372
1570
|
continue
|
|
1373
1571
|
if ref is not None:
|
|
1374
1572
|
user = record.get("user", {})
|
|
1375
|
-
|
|
1573
|
+
details = record.get("details", {})
|
|
1574
|
+
changed = details.get("items", {}) if details.get("operation") == "enable" else {}
|
|
1575
|
+
if (ref not in user.get("items", {}) and ref not in user.get("overrides", {})
|
|
1576
|
+
and ref not in user.get("tombstones", {}) and ref not in user.get("enabled_overrides", {})
|
|
1577
|
+
and ref not in changed):
|
|
1376
1578
|
continue
|
|
1377
1579
|
rows.append({"history_id": path.parent.name, "revision": record.get("revision"), "path": str(path.parent)})
|
|
1378
1580
|
return rows
|
|
@@ -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
|