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.
Files changed (55) hide show
  1. package/DEPENDENCIES.md +236 -80
  2. package/INSTALL.md +112 -0
  3. package/README.md +184 -486
  4. package/claude/CLAUDE.md +1 -1
  5. package/claude/guides/cli-multi-model-workflow.md +1 -1
  6. package/claude/guides/learning-flow.md +23 -12
  7. package/claude/guides/session-distill-workflow.md +22 -12
  8. package/codex/AGENTS.md +1 -1
  9. package/codex/guides/cli-multi-model-workflow.md +1 -1
  10. package/codex/guides/learning-flow.md +23 -12
  11. package/codex/guides/session-distill-workflow.md +22 -12
  12. package/compose/app_bridge/SKILL.md +75 -0
  13. package/compose/app_bridge/agents/openai.yaml +2 -0
  14. package/compose/app_bridge/scripts/bridge.py +76 -0
  15. package/compose/bootstrap/SKILL.md +23 -2
  16. package/compose/corpus.py +31 -9
  17. package/compose/corpus_app.py +456 -0
  18. package/compose/corpus_import.py +529 -0
  19. package/compose/corpus_install.py +196 -18
  20. package/compose/corpus_session.py +27 -0
  21. package/compose/corpus_setup.py +674 -0
  22. package/compose/corpus_setup_cli.py +582 -0
  23. package/compose/corpus_setup_i18n.py +318 -0
  24. package/compose/corpus_setup_ui.py +633 -0
  25. package/compose/corpus_store.py +236 -34
  26. package/compose/corpus_transaction.py +43 -10
  27. package/compose/corpus_ui.py +279 -8
  28. package/compose/corpus_ui_runtime.py +278 -0
  29. package/compose/corpus_understand.py +6 -1
  30. package/compose/setup/START.md +147 -0
  31. package/compose/ui_runtime/linkify_it_py-2.2.0-py3-none-any.whl +0 -0
  32. package/compose/ui_runtime/manifest.json +238 -0
  33. package/compose/ui_runtime/markdown_it_py-4.2.0-py3-none-any.whl +0 -0
  34. package/compose/ui_runtime/mdit_py_plugins-0.6.1-py3-none-any.whl +0 -0
  35. package/compose/ui_runtime/mdurl-0.1.2-py3-none-any.whl +0 -0
  36. package/compose/ui_runtime/platformdirs-4.11.8-py3-none-any.whl +0 -0
  37. package/compose/ui_runtime/pygments-2.21.0-py3-none-any.whl +0 -0
  38. package/compose/ui_runtime/rich-15.0.0-py3-none-any.whl +0 -0
  39. package/compose/ui_runtime/textual-8.2.8-py3-none-any.whl +0 -0
  40. package/compose/ui_runtime/typing_extensions-4.16.0-py3-none-any.whl +0 -0
  41. package/docs/advanced-launch.md +131 -0
  42. package/docs/assets/corpus-studio.svg +227 -0
  43. package/docs/corpus.md +117 -0
  44. package/docs/recovery.md +201 -0
  45. package/docs/session-model.md +120 -0
  46. package/docs/setup.md +190 -0
  47. package/docs/understand.md +40 -0
  48. package/install.sh +75 -46
  49. package/launch/agent-launch.py +91 -47
  50. package/launch/provision-venv.sh +44 -13
  51. package/learn/collect-learning.py +14 -5
  52. package/learn/learning.schema.json +2 -2
  53. package/package.json +14 -2
  54. package/provenance.json +1 -1
  55. package/wrappers/claude-run.sh +10 -13
@@ -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, active)
704
- selected = self._selected_items(active, selection)
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) -> dict[str, Any]:
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) -> dict[str, Any]:
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
- defaults = {"schema_version": SCHEMA_VERSION,
811
- "selection": self._normalized_install_selection(domains, catalog) +
812
- [LOCAL_PACKAGE, "@local/learnings-claude", "@local/learnings-codex"]}
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, _defaults = self._selected_baseline(runtime)
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
- return {"ref": ref, "item": effective, "digest": _digest(effective) if effective else None, "state": state}
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) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
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 == "create":
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
- allocated = self._next_personal_id(user) if isinstance(payload, dict) and payload.get("operation", payload.get("op")) == "create" else None
1161
- next_runtime, next_user, details = self._prepare_operation(payload, runtime, user, allocated_item_id=allocated)
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", {"runtime": runtime, "user": user, "revision": current})
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) -> list[dict[str, Any]]:
1229
- if selection and "all" in selection:
1230
- return items
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
- if item.get("tier") in {"core", "infra"}:
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) -> dict[str, Any]:
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._validate_snapshot_selection(effective_selection, inventory, active)
1271
- selected = self._selected_items(active, effective_selection)
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 $corpus using {root / 'bootstrap' / 'SKILL.md'}."
1329
- output["instruction_text"] = output["instruction_text"].rstrip() + "\n\n" + invocation + "\n"
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
- bootstrap_target = staging / "bootstrap" / "SKILL.md"
1338
- bootstrap_target.parent.mkdir(parents=True, exist_ok=True)
1339
- bootstrap_target.write_bytes(bootstrap_path.read_bytes())
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
- if ref not in user.get("items", {}) and ref not in user.get("overrides", {}) and ref not in user.get("tombstones", {}):
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 transaction_lock(state_root: Path) -> Iterator[None]:
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
- root.mkdir(parents=True, exist_ok=True, mode=0o700)
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
- descriptor = os.open(lock, flags, 0o600)
86
- with os.fdopen(descriptor, "a+", encoding="utf-8") as handle:
87
- fcntl.flock(handle, fcntl.LOCK_EX)
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(handle, fcntl.LOCK_UN)
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