agent-bios 0.18.0 → 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 -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/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 +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 +167 -29
- package/compose/corpus_transaction.py +43 -10
- package/compose/corpus_ui_runtime.py +278 -0
- 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
|
@@ -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
|
|
|
@@ -645,7 +653,7 @@ class CorpusStore:
|
|
|
645
653
|
raise ValidationError("selection must be a list of qualified refs/domains")
|
|
646
654
|
packages = {p.get("package_id") for p in inventory.get("packages", []) if isinstance(p, dict)}
|
|
647
655
|
for value in selection:
|
|
648
|
-
if value == "all":
|
|
656
|
+
if value == "all" or value in packages:
|
|
649
657
|
continue
|
|
650
658
|
if ":" in value:
|
|
651
659
|
continue
|
|
@@ -704,6 +712,18 @@ class CorpusStore:
|
|
|
704
712
|
if package not in packages or domain not in packages[package]:
|
|
705
713
|
raise ValidationError(f"unknown corpus selection domain: {value}")
|
|
706
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
|
+
|
|
707
727
|
def _validate_candidate_projection(self, runtime: dict[str, Any], user: dict[str, Any]) -> None:
|
|
708
728
|
"""Use the real compiler as a plan validator without publishing output."""
|
|
709
729
|
catalog = self._catalog_module()
|
|
@@ -713,8 +733,10 @@ class CorpusStore:
|
|
|
713
733
|
items, _inventory, defaults, _baseline = self._effective_items(runtime, user, host=host)
|
|
714
734
|
active = [item for item in items if item.get("active", True) is not False]
|
|
715
735
|
selection = self._effective_selection(user, defaults, None)
|
|
716
|
-
self._validate_snapshot_selection(selection, _inventory,
|
|
717
|
-
|
|
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)
|
|
718
740
|
self._require_resolved(selected)
|
|
719
741
|
catalog.compile_items(_copy_json(selected), Path(temp) / host, host)
|
|
720
742
|
except Exception as exc:
|
|
@@ -802,12 +824,15 @@ class CorpusStore:
|
|
|
802
824
|
|
|
803
825
|
# ---- public read API ----------------------------------------------------
|
|
804
826
|
|
|
805
|
-
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]:
|
|
806
829
|
"""Install one immutable validated baseline tuple without touching user data."""
|
|
807
830
|
with self._lock():
|
|
808
|
-
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))
|
|
809
833
|
|
|
810
|
-
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]:
|
|
811
836
|
"""Stage a validated baseline and source plan without advancing any pointer."""
|
|
812
837
|
with self._lock():
|
|
813
838
|
self._recover_locked()
|
|
@@ -820,9 +845,15 @@ class CorpusStore:
|
|
|
820
845
|
items = [self._validate_item(item, allow_origin=True) for item in raw_items]
|
|
821
846
|
if len({item["ref"] for item in items}) != len(items):
|
|
822
847
|
raise ValidationError("catalog has duplicate corpus refs")
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
|
826
857
|
promotion_path = self.repo / "learn" / "promotions.json"
|
|
827
858
|
promotions = _json_read(promotion_path, {"version": 0, "promotions": []})
|
|
828
859
|
if not isinstance(promotions, dict) or not isinstance(promotions.get("promotions"), list):
|
|
@@ -849,6 +880,11 @@ class CorpusStore:
|
|
|
849
880
|
# successful-install record changes.
|
|
850
881
|
next_user = self._rebase_overlays(old_inventory, catalog, user)
|
|
851
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)
|
|
852
888
|
_atomic_write(root / "inventory.json", catalog)
|
|
853
889
|
_atomic_write(root / "defaults.json", defaults)
|
|
854
890
|
_atomic_write(root / "promotions.json", promotions)
|
|
@@ -909,6 +945,7 @@ class CorpusStore:
|
|
|
909
945
|
"revision": revision, "baseline_count": len(refs),
|
|
910
946
|
"personal_items": len(user["items"]), "overrides": len(user["overrides"]),
|
|
911
947
|
"tombstones": len(user["tombstones"]), "selection": self._effective_selection(user, defaults, None),
|
|
948
|
+
"selection_mode": self._selection_mode(user, defaults),
|
|
912
949
|
"enabled_overrides": _copy_json(self._enabled_overrides(user)),
|
|
913
950
|
}
|
|
914
951
|
|
|
@@ -925,7 +962,8 @@ class CorpusStore:
|
|
|
925
962
|
overrides = self._enabled_overrides(user)
|
|
926
963
|
selection = self._effective_selection(user, defaults, None)
|
|
927
964
|
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
|
|
965
|
+
[item for item in effective.values() if item.get("active", True) is not False], selection, overrides,
|
|
966
|
+
mode=self._selection_mode(user, defaults))}
|
|
929
967
|
revision = self._authoring_revision(runtime, user)
|
|
930
968
|
rows: list[dict[str, Any]] = []
|
|
931
969
|
for ref in sorted(set(source) | set(user["items"]) | set(effective)):
|
|
@@ -993,11 +1031,12 @@ class CorpusStore:
|
|
|
993
1031
|
raise CorpusStoreError("could not allocate an unused personal identity")
|
|
994
1032
|
|
|
995
1033
|
def _prepare_operation(self, payload: dict[str, Any], runtime: dict[str, Any], user: dict[str, Any],
|
|
996
|
-
*, 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]]:
|
|
997
1036
|
if not isinstance(payload, dict):
|
|
998
1037
|
raise ValidationError("plan payload must be an object")
|
|
999
1038
|
op = payload.get("operation", payload.get("op"))
|
|
1000
|
-
if op not in {"create", "update", "remove", "restore", "recover", "reset", "rollback", "select", "enable"}:
|
|
1039
|
+
if not isinstance(op, str) or op not in {"create", "update", "remove", "restore", "recover", "reset", "rollback", "select", "enable", "import"}:
|
|
1001
1040
|
raise ValidationError("unknown corpus operation")
|
|
1002
1041
|
allowed = {
|
|
1003
1042
|
"create": {"operation", "op", "item", "package_id", "expected_revision"},
|
|
@@ -1007,8 +1046,9 @@ class CorpusStore:
|
|
|
1007
1046
|
"recover": {"operation", "op", "ref", "expected_revision"},
|
|
1008
1047
|
"reset": {"operation", "op", "expected_revision"},
|
|
1009
1048
|
"rollback": {"operation", "op", "baseline_ref", "history_id", "expected_revision"},
|
|
1010
|
-
"select": {"operation", "op", "selection", "expected_revision"},
|
|
1049
|
+
"select": {"operation", "op", "selection", "selection_mode", "expected_revision"},
|
|
1011
1050
|
"enable": {"operation", "op", "items", "expected_revision"},
|
|
1051
|
+
"import": {"operation", "op", "capture_id", "candidates", "excluded", "expected_revision"},
|
|
1012
1052
|
}[op]
|
|
1013
1053
|
unknown = set(payload) - allowed
|
|
1014
1054
|
if unknown:
|
|
@@ -1021,7 +1061,31 @@ class CorpusStore:
|
|
|
1021
1061
|
items, inventory, defaults, _baseline_ref = self._effective_items(runtime, user)
|
|
1022
1062
|
effective = {item["ref"]: item for item in items}
|
|
1023
1063
|
details: dict[str, Any] = {"operation": op}
|
|
1024
|
-
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":
|
|
1025
1089
|
raw = _copy_json(payload.get("item"))
|
|
1026
1090
|
if not isinstance(raw, dict):
|
|
1027
1091
|
raise ValidationError("create needs item")
|
|
@@ -1168,6 +1232,14 @@ class CorpusStore:
|
|
|
1168
1232
|
details["items"] = _copy_json(choices)
|
|
1169
1233
|
elif op == "select":
|
|
1170
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
|
|
1171
1243
|
next_user["selection"] = selection
|
|
1172
1244
|
details["selection"] = selection
|
|
1173
1245
|
elif op == "reset":
|
|
@@ -1181,6 +1253,8 @@ class CorpusStore:
|
|
|
1181
1253
|
next_runtime["selected_baseline_ref"] = latest
|
|
1182
1254
|
_inv, latest_defaults = self._read_baseline(latest)
|
|
1183
1255
|
next_user["selection"] = latest_defaults.get("selection")
|
|
1256
|
+
if "selection_mode" in latest_defaults:
|
|
1257
|
+
next_user["selection_mode"] = latest_defaults["selection_mode"]
|
|
1184
1258
|
details["baseline_ref"] = latest
|
|
1185
1259
|
elif op == "rollback":
|
|
1186
1260
|
history_id = payload.get("history_id")
|
|
@@ -1209,8 +1283,22 @@ class CorpusStore:
|
|
|
1209
1283
|
self._recover_locked()
|
|
1210
1284
|
runtime, user = self._runtime_state(), self._user_state()
|
|
1211
1285
|
before = self._authoring_revision(runtime, user)
|
|
1212
|
-
|
|
1213
|
-
|
|
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)
|
|
1214
1302
|
self._validate_candidate_projection(next_runtime, next_user)
|
|
1215
1303
|
after = self._authoring_revision(next_runtime, next_user)
|
|
1216
1304
|
plan_id = uuid.uuid4().hex
|
|
@@ -1222,6 +1310,8 @@ class CorpusStore:
|
|
|
1222
1310
|
}
|
|
1223
1311
|
if allocated is not None:
|
|
1224
1312
|
plan["allocated_item_id"] = allocated
|
|
1313
|
+
if allocated_many is not None:
|
|
1314
|
+
plan["allocated_item_ids"] = allocated_many
|
|
1225
1315
|
self._write_transaction(plan_id, {"state": "PLANNED", "plan": plan})
|
|
1226
1316
|
return {key: plan[key] for key in ("schema_version", "plan_id", "expected_revision", "result_revision", "details")}
|
|
1227
1317
|
|
|
@@ -1251,11 +1341,16 @@ class CorpusStore:
|
|
|
1251
1341
|
# Persisted pre-allocation plans already resolved their identity in after-state.
|
|
1252
1342
|
ref = plan.get("details", {}).get("ref")
|
|
1253
1343
|
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
|
|
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"))
|
|
1255
1346
|
self._validate_candidate_projection(next_runtime, next_user)
|
|
1256
1347
|
result = self._authoring_revision(next_runtime, next_user)
|
|
1257
1348
|
if result != plan.get("result_revision"):
|
|
1258
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)
|
|
1259
1354
|
prepared = {"state": "PREPARED", "plan": plan, "prior_revision": current, "prepared_at": _utcnow()}
|
|
1260
1355
|
history_id = f"{prepared['prepared_at'].replace(':', '').replace('+00:00', 'Z')}-{plan_id[:12]}"
|
|
1261
1356
|
prepared["history_id"] = history_id
|
|
@@ -1279,18 +1374,42 @@ class CorpusStore:
|
|
|
1279
1374
|
# ---- immutable snapshots and history -----------------------------------
|
|
1280
1375
|
|
|
1281
1376
|
def _selected_items(self, items: list[dict[str, Any]], selection: list[str] | None,
|
|
1282
|
-
overrides: dict[str, bool] | None = 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 []
|
|
1283
1382
|
overrides = overrides or {}
|
|
1383
|
+
working = Path(cwd or Path.cwd()).resolve()
|
|
1284
1384
|
selected: list[dict[str, Any]] = []
|
|
1285
1385
|
for item in items:
|
|
1286
|
-
|
|
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):
|
|
1287
1406
|
if overrides[item["ref"]]:
|
|
1288
1407
|
selected.append(item)
|
|
1289
1408
|
continue
|
|
1290
1409
|
if selection and "all" in selection:
|
|
1291
1410
|
selected.append(item)
|
|
1292
1411
|
continue
|
|
1293
|
-
if item.get("tier") in {"core", "infra"}:
|
|
1412
|
+
if mode == "default" and item.get("tier") in {"core", "infra"}:
|
|
1294
1413
|
selected.append(item)
|
|
1295
1414
|
continue
|
|
1296
1415
|
if not selection:
|
|
@@ -1303,7 +1422,8 @@ class CorpusStore:
|
|
|
1303
1422
|
return selected
|
|
1304
1423
|
|
|
1305
1424
|
def snapshot(self, host: str, selection: list[str] | None = None, dry_run: bool = False,
|
|
1306
|
-
native: bool = False
|
|
1425
|
+
native: bool = False, *, selection_mode: str | None = None,
|
|
1426
|
+
cwd: str | Path | None = None) -> dict[str, Any]:
|
|
1307
1427
|
"""Compose an immutable activated-session snapshot.
|
|
1308
1428
|
|
|
1309
1429
|
A dry run has no durable write path: it compiles in an OS temporary
|
|
@@ -1327,8 +1447,16 @@ class CorpusStore:
|
|
|
1327
1447
|
items, inventory, defaults, baseline_ref = self._effective_items(runtime, user, host=host)
|
|
1328
1448
|
active = [item for item in items if item.get("active", True) is not False]
|
|
1329
1449
|
effective_selection = self._effective_selection(user, defaults, selection)
|
|
1330
|
-
self.
|
|
1331
|
-
|
|
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)
|
|
1332
1460
|
self._require_resolved(selected)
|
|
1333
1461
|
selected, promotion_warnings = self._resolve_promotions(selected, user, host, baseline_ref)
|
|
1334
1462
|
bootstrap_path = self.repo / "compose" / "bootstrap" / "SKILL.md"
|
|
@@ -1338,6 +1466,7 @@ class CorpusStore:
|
|
|
1338
1466
|
inputs = {
|
|
1339
1467
|
"schema_version": SCHEMA_VERSION, "host": host, "baseline_ref": baseline_ref,
|
|
1340
1468
|
"selection": effective_selection,
|
|
1469
|
+
"selection_mode": mode, "cwd": str(working),
|
|
1341
1470
|
"selection_digest": _digest(effective_selection),
|
|
1342
1471
|
"authoring_revision": self._authoring_revision(runtime, user),
|
|
1343
1472
|
"item_digests": {item["ref"]: _digest(item) for item in sorted(selected, key=lambda x: x["ref"])},
|
|
@@ -1379,14 +1508,21 @@ class CorpusStore:
|
|
|
1379
1508
|
files = _snapshot_relative_paths(compiled.get("files"))
|
|
1380
1509
|
output = {
|
|
1381
1510
|
"instruction_text": compiled.get("instruction_text", ""),
|
|
1382
|
-
"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", []),
|
|
1383
1512
|
"unavailable": compiled.get("unavailable", []) + promotion_warnings,
|
|
1384
1513
|
}
|
|
1385
1514
|
assets = _snapshot_assets(compiled.get("assets", {}), files)
|
|
1386
1515
|
if assets:
|
|
1387
1516
|
output["assets"] = assets
|
|
1388
|
-
invocation = f"Corpus management: invoke $
|
|
1389
|
-
|
|
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"
|
|
1390
1526
|
for plugin in assets.get("claude_plugins", []):
|
|
1391
1527
|
for agent_path in (staging / plugin / "agents").glob("*.md"):
|
|
1392
1528
|
with agent_path.open("a", encoding="utf-8") as agent_file:
|
|
@@ -1394,9 +1530,10 @@ class CorpusStore:
|
|
|
1394
1530
|
if dry_run:
|
|
1395
1531
|
output["instruction_text"] = output["instruction_text"].replace(str(staging), str(root))
|
|
1396
1532
|
else:
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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())
|
|
1400
1537
|
_rewrite_staged_paths(staging, root)
|
|
1401
1538
|
output["instruction_text"] = output["instruction_text"].replace(str(staging), str(root))
|
|
1402
1539
|
_atomic_write(staging / "inventory.json", {"inputs": inputs, "items": selected})
|
|
@@ -1419,6 +1556,7 @@ class CorpusStore:
|
|
|
1419
1556
|
shutil.rmtree(staging)
|
|
1420
1557
|
return {"content_ref": content_ref, "path": str(root), "instruction_text": output["instruction_text"],
|
|
1421
1558
|
"revision": inputs["authoring_revision"], "unavailable": output.get("unavailable", []),
|
|
1559
|
+
"item_refs": output.get("item_refs", []), "selection_mode": mode,
|
|
1422
1560
|
"assets": output.get("assets", {})}
|
|
1423
1561
|
|
|
1424
1562
|
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
|