agent-bios 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEPENDENCIES.md +236 -80
- package/INSTALL.md +112 -0
- package/README.md +187 -524
- package/claude/CLAUDE.md +1 -1
- package/claude/guides/cli-multi-model-workflow.md +1 -1
- package/claude/guides/learning-flow.md +23 -12
- package/claude/guides/session-distill-workflow.md +22 -12
- package/claude/skills/understand/SKILL.md +52 -22
- package/codex/AGENTS.md +1 -1
- package/codex/guides/cli-multi-model-workflow.md +1 -1
- package/codex/guides/learning-flow.md +23 -12
- package/codex/guides/session-distill-workflow.md +22 -12
- package/compose/app_bridge/SKILL.md +75 -0
- package/compose/app_bridge/agents/openai.yaml +2 -0
- package/compose/app_bridge/scripts/bridge.py +76 -0
- package/compose/bootstrap/SKILL.md +12 -1
- package/compose/corpus.py +31 -9
- package/compose/corpus_app.py +456 -0
- package/compose/corpus_import.py +529 -0
- package/compose/corpus_install.py +202 -18
- package/compose/corpus_session.py +27 -0
- package/compose/corpus_setup.py +676 -0
- package/compose/corpus_setup_cli.py +585 -0
- package/compose/corpus_setup_i18n.py +324 -0
- package/compose/corpus_setup_ui.py +647 -0
- package/compose/corpus_store.py +213 -32
- package/compose/corpus_transaction.py +43 -10
- package/compose/corpus_ui_runtime.py +278 -0
- package/compose/corpus_understand.py +173 -22
- package/compose/setup/START.md +158 -0
- package/compose/ui_runtime/linkify_it_py-2.2.0-py3-none-any.whl +0 -0
- package/compose/ui_runtime/manifest.json +238 -0
- package/compose/ui_runtime/markdown_it_py-4.2.0-py3-none-any.whl +0 -0
- package/compose/ui_runtime/mdit_py_plugins-0.6.1-py3-none-any.whl +0 -0
- package/compose/ui_runtime/mdurl-0.1.2-py3-none-any.whl +0 -0
- package/compose/ui_runtime/platformdirs-4.11.8-py3-none-any.whl +0 -0
- package/compose/ui_runtime/pygments-2.21.0-py3-none-any.whl +0 -0
- package/compose/ui_runtime/rich-15.0.0-py3-none-any.whl +0 -0
- package/compose/ui_runtime/textual-8.2.8-py3-none-any.whl +0 -0
- package/compose/ui_runtime/typing_extensions-4.16.0-py3-none-any.whl +0 -0
- package/docs/advanced-launch.md +131 -0
- package/docs/assets/corpus-studio.svg +227 -0
- package/docs/corpus.md +117 -0
- package/docs/recovery.md +201 -0
- package/docs/session-model.md +120 -0
- package/docs/setup.md +206 -0
- package/docs/understand.md +88 -0
- package/install.sh +75 -46
- package/launch/agent-launch.py +99 -52
- package/launch/provision-venv.sh +44 -13
- package/learn/collect-learning.py +14 -5
- package/learn/learning.schema.json +2 -2
- package/package.json +14 -2
- package/provenance.json +1 -1
- package/wrappers/claude-run.sh +10 -13
|
@@ -26,6 +26,9 @@ import tomllib
|
|
|
26
26
|
import uuid
|
|
27
27
|
from typing import Any
|
|
28
28
|
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
sys.dont_write_bytecode = True
|
|
31
|
+
|
|
29
32
|
from corpus_transaction import (
|
|
30
33
|
_valid_release,
|
|
31
34
|
TransactionError,
|
|
@@ -345,7 +348,8 @@ class CorpusInstaller:
|
|
|
345
348
|
# CorpusStore delegates compile/load to a module name for normal package
|
|
346
349
|
# execution. Bind this instance to the release's exact catalog so a
|
|
347
350
|
# long-lived manager cannot retain an older release through sys.modules.
|
|
348
|
-
|
|
351
|
+
catalog = self._private_module(package_root, "corpus_catalog")
|
|
352
|
+
store._catalog_module = lambda: catalog
|
|
349
353
|
return store
|
|
350
354
|
|
|
351
355
|
def _catalog(self, package_root: Path) -> dict[str, Any]:
|
|
@@ -374,7 +378,9 @@ class CorpusInstaller:
|
|
|
374
378
|
"""Keep the legacy launcher panel readable without claiming activation."""
|
|
375
379
|
manifest = _read_json(package_root / "compose" / "domains.json")
|
|
376
380
|
available = sorted((manifest.get("domains") or {}).keys())
|
|
377
|
-
|
|
381
|
+
package_id = manifest.get("package_id", "@agent-bios/core")
|
|
382
|
+
applied = available if "all" in selection or package_id in selection else [
|
|
383
|
+
name for name in available if f"{package_id}/{name}" in selection]
|
|
378
384
|
_atomic_json(self.state_root / "corpus-status.json", {
|
|
379
385
|
"repo": str(package_root), "current_version": None, "latest_version": None,
|
|
380
386
|
"rolled_back_to": None, "versions": None, "summary": None,
|
|
@@ -387,7 +393,9 @@ class CorpusInstaller:
|
|
|
387
393
|
"""The status projection is part of the same publication as its record."""
|
|
388
394
|
manifest = _read_json(package_root / "compose" / "domains.json")
|
|
389
395
|
available = sorted((manifest.get("domains") or {}).keys())
|
|
390
|
-
|
|
396
|
+
package_id = manifest.get("package_id", "@agent-bios/core")
|
|
397
|
+
applied = available if "all" in selection or package_id in selection else [
|
|
398
|
+
name for name in available if f"{package_id}/{name}" in selection]
|
|
391
399
|
return _canonical({
|
|
392
400
|
"repo": str(package_root), "current_version": None, "latest_version": None,
|
|
393
401
|
"rolled_back_to": None, "versions": None, "summary": None,
|
|
@@ -544,6 +552,91 @@ class CorpusInstaller:
|
|
|
544
552
|
spec.loader.exec_module(module)
|
|
545
553
|
return module.ShellIntegration(self.env, self.repo)
|
|
546
554
|
|
|
555
|
+
def setup_catalog(self) -> dict[str, Any]:
|
|
556
|
+
return self._catalog(self.repo)
|
|
557
|
+
|
|
558
|
+
def setup_local_corpus(self) -> list[dict[str, Any]]:
|
|
559
|
+
"""Describe existing private content independently of installation choices."""
|
|
560
|
+
try:
|
|
561
|
+
counts = self._store(self.repo).local_item_counts()
|
|
562
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
563
|
+
raise InstallError(str(exc)) from exc
|
|
564
|
+
rows = []
|
|
565
|
+
for package_id, label in (("@local/personal", "Personal corpus"),
|
|
566
|
+
("@local/learnings-claude", "Claude learning records"),
|
|
567
|
+
("@local/learnings-codex", "Codex learning records")):
|
|
568
|
+
if counts.get(package_id):
|
|
569
|
+
rows.append({"target": package_id, "label": label, "item_count": counts[package_id]})
|
|
570
|
+
return rows
|
|
571
|
+
|
|
572
|
+
def setup_discover(self, project_roots=None) -> dict[str, Any]:
|
|
573
|
+
try:
|
|
574
|
+
from .corpus_import import discover
|
|
575
|
+
except ImportError:
|
|
576
|
+
from corpus_import import discover
|
|
577
|
+
return discover(environ=self.env, project_roots=project_roots)
|
|
578
|
+
|
|
579
|
+
def setup_revision(self) -> str:
|
|
580
|
+
"""Bind an installation review to the current private selection and authoring."""
|
|
581
|
+
paths = (self.record_path, self.runtime / "state.json", self.user_root / "state.json")
|
|
582
|
+
return hashlib.sha256(_canonical({str(path): self._file_version(path) for path in paths})).hexdigest()
|
|
583
|
+
|
|
584
|
+
def _app_manager(self):
|
|
585
|
+
try:
|
|
586
|
+
from .corpus_app import AppBridge
|
|
587
|
+
except ImportError:
|
|
588
|
+
from corpus_app import AppBridge
|
|
589
|
+
return AppBridge(self.repo, self.env)
|
|
590
|
+
|
|
591
|
+
def _app_lifecycle(self, action: str, dry_run: bool = False) -> dict[str, Any]:
|
|
592
|
+
try:
|
|
593
|
+
manager = self._app_manager()
|
|
594
|
+
if action == "remove":
|
|
595
|
+
return (manager.unregister(dry_run) if manager.has_owned_registration()
|
|
596
|
+
else manager.managed_status())
|
|
597
|
+
return manager.refresh_registration()
|
|
598
|
+
except (OSError, RuntimeError) as exc:
|
|
599
|
+
return {"changed": False, "needs_action": [str(exc)]}
|
|
600
|
+
|
|
601
|
+
def setup_extras(self, plan: dict[str, Any], dry_run: bool = False) -> dict[str, Any]:
|
|
602
|
+
try:
|
|
603
|
+
from .corpus_import import capture
|
|
604
|
+
except ImportError:
|
|
605
|
+
from corpus_import import capture
|
|
606
|
+
result: dict[str, Any] = {}
|
|
607
|
+
if plan.get("app_bridge"):
|
|
608
|
+
bridge = self._app_manager()
|
|
609
|
+
status = bridge.status()
|
|
610
|
+
if status.get("needs_action"):
|
|
611
|
+
raise InstallError("; ".join(status["needs_action"]))
|
|
612
|
+
result["app_bridge"] = ({"requested": True, "dry_run": True} if dry_run
|
|
613
|
+
else bridge.register())
|
|
614
|
+
paths = plan.get("import_paths") or []
|
|
615
|
+
if paths:
|
|
616
|
+
discovered = self.setup_discover(plan.get("project_roots"))
|
|
617
|
+
allowed = {row["path"] for row in discovered["sources"]}
|
|
618
|
+
if not set(paths) <= allowed:
|
|
619
|
+
raise InstallError("instruction import selection is not in the reviewed discovery set")
|
|
620
|
+
if dry_run:
|
|
621
|
+
result["import"] = {"paths": paths, "classification": "requires model review", "dry_run": True}
|
|
622
|
+
else:
|
|
623
|
+
record = self._old_record()
|
|
624
|
+
if record is None:
|
|
625
|
+
raise InstallError("install the private runtime before capturing instructions")
|
|
626
|
+
try:
|
|
627
|
+
captured = capture(self._store(Path(record["package_root"])), paths,
|
|
628
|
+
environ=self.env, project_roots=plan.get("project_roots"),
|
|
629
|
+
expected_source_digests=plan.get("_expected_source_digests"))
|
|
630
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
631
|
+
failure = InstallError(str(exc))
|
|
632
|
+
failure.completed_extras = result
|
|
633
|
+
raise failure from exc
|
|
634
|
+
result["import"] = {"capture_id": captured["capture_id"], "source_count": len(captured["sources"]),
|
|
635
|
+
"classification": "requires model review",
|
|
636
|
+
"next_command": "agent-bios import prompt " + captured["capture_id"],
|
|
637
|
+
"app_request": "Use $agent-bios to import capture " + captured["capture_id"]}
|
|
638
|
+
return result
|
|
639
|
+
|
|
547
640
|
def _shell_paths(self, action: str) -> list[dict[str, Any]]:
|
|
548
641
|
try:
|
|
549
642
|
manager = self._shell_manager()
|
|
@@ -561,14 +654,45 @@ class CorpusInstaller:
|
|
|
561
654
|
for row in changes]
|
|
562
655
|
|
|
563
656
|
@_serialized
|
|
564
|
-
def install(self, domains: str | None = None, dry_run: bool = False
|
|
657
|
+
def install(self, domains: str | None = None, dry_run: bool = False, *,
|
|
658
|
+
selection_mode: str | None = None, targets: list[str] | None = None) -> dict[str, Any]:
|
|
565
659
|
files = self._package_files()
|
|
566
|
-
|
|
660
|
+
explicit_selection = domains is not None or selection_mode is not None or targets is not None
|
|
661
|
+
if domains is not None and selection_mode is None:
|
|
662
|
+
selection_mode = "default"
|
|
663
|
+
if selection_mode not in {None, "default", "selected", "none"}:
|
|
664
|
+
raise InstallError("selection_mode must be default, selected, or none")
|
|
665
|
+
if targets is not None and (not isinstance(targets, list) or not all(isinstance(x, str) for x in targets)):
|
|
666
|
+
raise InstallError("corpus targets must be a list of qualified names")
|
|
667
|
+
if domains is not None and targets is not None:
|
|
668
|
+
raise InstallError("use domain flags or corpus targets, not both")
|
|
669
|
+
if domains is not None and selection_mode == "none":
|
|
670
|
+
raise InstallError("no-corpus installation cannot include domain flags")
|
|
671
|
+
requested = list(targets) if targets is not None else ([] if selection_mode == "none" else self._normalized_domains(domains))
|
|
672
|
+
if selection_mode == "none" and requested:
|
|
673
|
+
raise InstallError("no-corpus installation cannot include targets")
|
|
674
|
+
if selection_mode == "selected" and not requested:
|
|
675
|
+
raise InstallError("selected corpus installation needs at least one target")
|
|
567
676
|
prior = self._old_record()
|
|
568
677
|
# An update without an explicit selection must not broaden a saved
|
|
569
678
|
# core-only or domain-limited environment back to every domain.
|
|
570
|
-
if domains is None and isinstance((prior or {}).get("selection"), list):
|
|
679
|
+
if domains is None and not explicit_selection and isinstance((prior or {}).get("selection"), list):
|
|
571
680
|
requested = prior["selection"]
|
|
681
|
+
selection_mode = prior.get("selection_mode")
|
|
682
|
+
if selection_mode is not None:
|
|
683
|
+
preview_store = self._store(self.repo)
|
|
684
|
+
catalog = self.setup_catalog()
|
|
685
|
+
try:
|
|
686
|
+
available = list(catalog["items"])
|
|
687
|
+
runtime = preview_store._runtime_state()
|
|
688
|
+
if runtime.get("selected_baseline_ref"):
|
|
689
|
+
user = preview_store._user_state()
|
|
690
|
+
for host in ("claude", "codex"):
|
|
691
|
+
available.extend(item for item in preview_store._effective_items(runtime, user, host=host)[0]
|
|
692
|
+
if item["package_id"].startswith("@local/") and item.get("active", True))
|
|
693
|
+
preview_store._validate_snapshot_selection(requested, catalog, available)
|
|
694
|
+
except (ValueError, RuntimeError) as exc:
|
|
695
|
+
raise InstallError(str(exc)) from exc
|
|
572
696
|
# A preview is strictly read-only. A real install first refuses an
|
|
573
697
|
# unrelated reset, then finishes only its own pending install before
|
|
574
698
|
# creating an immutable release directory.
|
|
@@ -581,7 +705,8 @@ class CorpusInstaller:
|
|
|
581
705
|
release, entries, digest = self._copy_release(files, dry_run)
|
|
582
706
|
if dry_run:
|
|
583
707
|
return {"dry_run": True, "release": str(release), "release_digest": digest,
|
|
584
|
-
"files": len(entries), "selection": requested
|
|
708
|
+
"files": len(entries), "selection": requested, "selection_mode": selection_mode or "default",
|
|
709
|
+
"corpus_storage": "packaged library retained privately; selection controls delivery"}
|
|
585
710
|
shell_paths = self._shell_paths("restore")
|
|
586
711
|
# Stage the immutable baseline before publishing either source pointers
|
|
587
712
|
# or launcher projections. Store owns those source pointers; this
|
|
@@ -590,7 +715,8 @@ class CorpusInstaller:
|
|
|
590
715
|
prepare = getattr(store, "prepare_install", None)
|
|
591
716
|
if not callable(prepare):
|
|
592
717
|
raise InstallError("private corpus store does not support staged install recovery")
|
|
593
|
-
candidate = prepare(requested)
|
|
718
|
+
candidate = (prepare(requested, selection_mode=selection_mode, replace_selection=explicit_selection)
|
|
719
|
+
if selection_mode is not None else prepare(requested))
|
|
594
720
|
if not isinstance(candidate, dict) or not isinstance(candidate.get("details"), dict):
|
|
595
721
|
raise InstallError("private corpus store returned an invalid install candidate")
|
|
596
722
|
details = candidate["details"]
|
|
@@ -621,6 +747,8 @@ class CorpusInstaller:
|
|
|
621
747
|
"launcher": launcher, "config_files": config, "created_at": int(time.time()),
|
|
622
748
|
"mode": "private-session-scoped", "needs_action": [],
|
|
623
749
|
}
|
|
750
|
+
if selection_mode is not None:
|
|
751
|
+
record["selection_mode"] = selection_mode
|
|
624
752
|
paths = [{"path": str(path), "before": self._file_version(path),
|
|
625
753
|
"after": self._planned_version(content), "mode": mode}
|
|
626
754
|
for path, content, mode in owned]
|
|
@@ -649,7 +777,8 @@ class CorpusInstaller:
|
|
|
649
777
|
try:
|
|
650
778
|
with operation_scope(self.state_root):
|
|
651
779
|
self._finish_install_transaction(journal, journal_data, store, candidate)
|
|
652
|
-
return {"dry_run": False, "stored": True, "activation": "unverified", "record": record
|
|
780
|
+
return {"dry_run": False, "stored": True, "activation": "unverified", "record": record,
|
|
781
|
+
"app_bridge": self._app_lifecycle("refresh")}
|
|
653
782
|
except BaseException as exc:
|
|
654
783
|
journal_data["state"] = "NEEDS_RECOVERY"
|
|
655
784
|
journal_data["error"] = type(exc).__name__
|
|
@@ -813,7 +942,8 @@ class CorpusInstaller:
|
|
|
813
942
|
except (OSError, RuntimeError) as exc:
|
|
814
943
|
raise InstallError(str(exc)) from exc
|
|
815
944
|
if record is None:
|
|
816
|
-
|
|
945
|
+
app = self._app_lifecycle("remove", dry_run)
|
|
946
|
+
return {"removed": shell_removed, "preserved": ["no private install record"], "app_bridge": app}
|
|
817
947
|
removed: list[str] = list(shell_removed)
|
|
818
948
|
preserved: list[str] = []
|
|
819
949
|
for entry in [record.get("launcher"), *(record.get("config_files") or [])]:
|
|
@@ -840,7 +970,8 @@ class CorpusInstaller:
|
|
|
840
970
|
self.record_path.unlink(missing_ok=True)
|
|
841
971
|
# User package/overlays/learnings and session snapshots/pins are purposely
|
|
842
972
|
# not enumerated or deleted here. No whole state-root removal occurs.
|
|
843
|
-
|
|
973
|
+
app = self._app_lifecycle("remove", dry_run)
|
|
974
|
+
return {"removed": removed, "preserved": preserved, "app_bridge": app,
|
|
844
975
|
"retained": [str(self.user_root), str(self.state_root / "sessions")]}
|
|
845
976
|
|
|
846
977
|
def _reset_layout(self, record: dict[str, Any], release: Path) -> tuple[list[Path], Path, Path, list[tuple[Path, bytes, int]]]:
|
|
@@ -1617,7 +1748,7 @@ class CorpusInstaller:
|
|
|
1617
1748
|
|
|
1618
1749
|
|
|
1619
1750
|
def main(argv: list[str] | None = None) -> int:
|
|
1620
|
-
parser = argparse.ArgumentParser(prog="agent-bios
|
|
1751
|
+
parser = argparse.ArgumentParser(prog="agent-bios")
|
|
1621
1752
|
parser.add_argument("--repo", help=argparse.SUPPRESS)
|
|
1622
1753
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
1623
1754
|
install = subparsers.add_parser("install")
|
|
@@ -1628,6 +1759,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1628
1759
|
onboard.add_argument("--domains")
|
|
1629
1760
|
onboard.add_argument("--dry-run", action="store_true")
|
|
1630
1761
|
onboard.add_argument("--with", dest="with_capabilities")
|
|
1762
|
+
for command in (install, onboard):
|
|
1763
|
+
interaction = command.add_mutually_exclusive_group()
|
|
1764
|
+
interaction.add_argument("--interactive", action="store_true", help="open the Textual installation wizard (default)")
|
|
1765
|
+
interaction.add_argument("--non-interactive", action="store_true", help="run without the wizard and return JSON")
|
|
1766
|
+
command.add_argument("--corpus", choices=("all", "none", "selected"),
|
|
1767
|
+
help="active corpus policy; none retains library assets privately but delivers no corpus")
|
|
1768
|
+
command.add_argument("--select", action="append", default=[], metavar="TARGET",
|
|
1769
|
+
help="package/domain/item target; repeat for multiple targets")
|
|
1631
1770
|
subparsers.add_parser("verify")
|
|
1632
1771
|
subparsers.add_parser("status")
|
|
1633
1772
|
uninstall = subparsers.add_parser("uninstall")
|
|
@@ -1643,11 +1782,41 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1643
1782
|
reset.add_argument("--expected-revision", help="reset preview generation to accept")
|
|
1644
1783
|
args = parser.parse_args(argv)
|
|
1645
1784
|
installer = CorpusInstaller(Path(args.repo) if args.repo else Path(__file__).resolve().parent.parent)
|
|
1785
|
+
interactive = False
|
|
1646
1786
|
try:
|
|
1647
1787
|
if args.command in {"install", "onboard"}:
|
|
1648
1788
|
if args.with_capabilities:
|
|
1649
|
-
raise InstallError("
|
|
1650
|
-
|
|
1789
|
+
raise InstallError("use --interactive to select dependency installation; --with is legacy-only")
|
|
1790
|
+
interactive = not args.non_interactive
|
|
1791
|
+
if args.select and args.corpus != "selected":
|
|
1792
|
+
raise InstallError("--select requires --corpus selected")
|
|
1793
|
+
if interactive and args.domains is not None:
|
|
1794
|
+
raise InstallError("--domains is a compatibility selection flag; use --non-interactive or choose corpus in the wizard")
|
|
1795
|
+
if interactive:
|
|
1796
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
1797
|
+
raise InstallError("interactive installation needs an input/output terminal; use --non-interactive for automation or app tool calls")
|
|
1798
|
+
if os.environ.get("TERM") == "dumb":
|
|
1799
|
+
raise InstallError("this terminal cannot display the Textual wizard; use a capable terminal or --non-interactive")
|
|
1800
|
+
try:
|
|
1801
|
+
from .corpus_ui_runtime import activate_ui_runtime
|
|
1802
|
+
except ImportError:
|
|
1803
|
+
from corpus_ui_runtime import activate_ui_runtime
|
|
1804
|
+
activate_ui_runtime(installer.repo)
|
|
1805
|
+
try:
|
|
1806
|
+
from .corpus_setup_ui import run_setup_ui
|
|
1807
|
+
except ImportError:
|
|
1808
|
+
from corpus_setup_ui import run_setup_ui
|
|
1809
|
+
initial = None
|
|
1810
|
+
if args.corpus is not None:
|
|
1811
|
+
mode = "none" if args.corpus == "none" else "selected"
|
|
1812
|
+
targets = ["all"] if args.corpus == "all" else list(args.select) if args.corpus == "selected" else []
|
|
1813
|
+
initial = {"selection_mode": mode, "targets": targets, "dependencies": [],
|
|
1814
|
+
"app_bridge": False, "import_paths": [], "project_roots": []}
|
|
1815
|
+
result = run_setup_ui(installer, dry_run=args.dry_run, initial_plan=initial)
|
|
1816
|
+
else:
|
|
1817
|
+
mode = "none" if args.corpus == "none" else "selected" if args.corpus else None
|
|
1818
|
+
targets = ["all"] if args.corpus == "all" else args.select if args.corpus == "selected" else None
|
|
1819
|
+
result = installer.install(args.domains, args.dry_run, selection_mode=mode, targets=targets)
|
|
1651
1820
|
elif args.command == "verify":
|
|
1652
1821
|
result = installer.verify()
|
|
1653
1822
|
elif args.command == "status":
|
|
@@ -1659,10 +1828,25 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1659
1828
|
expected_revision=args.expected_revision)
|
|
1660
1829
|
else:
|
|
1661
1830
|
result = installer.migrate(apply=args.apply and not args.dry_run, yes=args.yes)
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1831
|
+
if interactive:
|
|
1832
|
+
try:
|
|
1833
|
+
from .corpus_setup import format_setup_result
|
|
1834
|
+
except ImportError:
|
|
1835
|
+
from corpus_setup import format_setup_result
|
|
1836
|
+
print(format_setup_result(result, language=result.get("ui_language", "en")))
|
|
1837
|
+
else:
|
|
1838
|
+
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
|
|
1839
|
+
return 1 if result.get("dependency_failed") or result.get("extras_error") or result.get("installation_error") or result.get("error") else 0
|
|
1840
|
+
except (InstallError, RuntimeError, ValueError, OSError) as exc:
|
|
1841
|
+
message = str(exc)
|
|
1842
|
+
language = getattr(exc, "ui_language", "en")
|
|
1843
|
+
if interactive and language != "en":
|
|
1844
|
+
try:
|
|
1845
|
+
from .corpus_setup_i18n import translate
|
|
1846
|
+
except ImportError:
|
|
1847
|
+
from corpus_setup_i18n import translate
|
|
1848
|
+
message = translate(language, "Setup could not finish. Details: {detail}", detail=message)
|
|
1849
|
+
print(f"corpus-install: {message}", file=sys.stderr)
|
|
1666
1850
|
return 1
|
|
1667
1851
|
|
|
1668
1852
|
|
|
@@ -23,6 +23,23 @@ class SessionError(RuntimeError):
|
|
|
23
23
|
pass
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def validate_working_directory_argv(host, argv):
|
|
27
|
+
"""Keep a private Codex snapshot and its native session in the same directory."""
|
|
28
|
+
if host != 'codex':
|
|
29
|
+
return
|
|
30
|
+
if not isinstance(argv, (list, tuple)) or not all(isinstance(token, str) for token in argv):
|
|
31
|
+
raise SessionError('private Codex activation requires a valid argument list')
|
|
32
|
+
for token in argv:
|
|
33
|
+
if token == '--':
|
|
34
|
+
break
|
|
35
|
+
if token == '--cd' or token.startswith('--cd=') or token.startswith('-C'):
|
|
36
|
+
raise SessionError(
|
|
37
|
+
'private Codex corpus activation cannot include --cd/-C: the selected corpus and '
|
|
38
|
+
'session pin use the launch working directory. cd into the target directory first, '
|
|
39
|
+
'then start a new activated session without a working-directory override'
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
26
43
|
def _global_instruction_choice(host, include_global_instructions):
|
|
27
44
|
if type(include_global_instructions) is not bool:
|
|
28
45
|
raise SessionError('include_global_instructions must be a boolean')
|
|
@@ -104,6 +121,7 @@ def _settings_values(argv):
|
|
|
104
121
|
|
|
105
122
|
|
|
106
123
|
def _record_instruction_choice(record, env, check_paths=True):
|
|
124
|
+
validate_working_directory_argv(record['host'], record.get('argv'))
|
|
107
125
|
include = _global_instruction_choice(record['host'], record.get('include_global_instructions', True))
|
|
108
126
|
if not include:
|
|
109
127
|
values = _settings_values(record['argv'])
|
|
@@ -455,6 +473,7 @@ def _verified_launch_snapshot(state_root, snapshot):
|
|
|
455
473
|
|
|
456
474
|
def compose_argv(command, argv, host, snapshot, cwd=None, env=None, include_global_instructions=True):
|
|
457
475
|
"""Preserve native instructions before the selected corpus and launch contract."""
|
|
476
|
+
validate_working_directory_argv(host, argv)
|
|
458
477
|
_global_instruction_choice(host, include_global_instructions)
|
|
459
478
|
native_env = dict(os.environ if env is None else env)
|
|
460
479
|
native_cwd = pathlib.Path(cwd or pathlib.Path.cwd()).resolve()
|
|
@@ -733,6 +752,7 @@ def recover_activations(state_root, command=None, host=None, env=None):
|
|
|
733
752
|
|
|
734
753
|
|
|
735
754
|
def create_codex_session(command, argv, state_root, record, cwd, env):
|
|
755
|
+
validate_working_directory_argv('codex', argv)
|
|
736
756
|
with CodexServer(command, config_flags(argv), cwd, env) as server:
|
|
737
757
|
params = {'cwd': str(cwd), 'developerInstructions': instruction_value(argv, 'codex'),
|
|
738
758
|
'ephemeral': False, 'experimentalRawEvents': False}
|
|
@@ -763,12 +783,19 @@ def create_codex_session(command, argv, state_root, record, cwd, env):
|
|
|
763
783
|
def launch(command, argv, state_root, host, snapshot, cwd=None, env=None, resume_id=None,
|
|
764
784
|
include_global_instructions=True):
|
|
765
785
|
"""Start a pinned native session, returning its exit status."""
|
|
786
|
+
validate_working_directory_argv(host, argv)
|
|
766
787
|
cwd = pathlib.Path(cwd or pathlib.Path.cwd()).resolve()
|
|
767
788
|
env = dict(os.environ if env is None else env)
|
|
768
789
|
if resume_id and include_global_instructions is not True:
|
|
769
790
|
raise SessionError('resume uses its recorded global instruction choice; omit the override or start a new session')
|
|
770
791
|
if not resume_id:
|
|
771
792
|
_global_instruction_choice(host, include_global_instructions)
|
|
793
|
+
elif host == 'codex':
|
|
794
|
+
_, pins = session_paths(state_root)
|
|
795
|
+
session_id = validate_session_id(resume_id)
|
|
796
|
+
pin_path = pins / host / f'{session_id}.json'
|
|
797
|
+
if pin_path.exists() or pin_path.is_symlink():
|
|
798
|
+
read_pin(state_root, host, session_id)
|
|
772
799
|
recovered = recover_activations(state_root, command, host, env)
|
|
773
800
|
if recovered['pending']:
|
|
774
801
|
print(f"agent-bios: {len(recovered['pending'])} activation(s) lack host evidence; their snapshots remain retained.", file=sys.stderr)
|