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.
Files changed (55) hide show
  1. package/DEPENDENCIES.md +236 -80
  2. package/INSTALL.md +112 -0
  3. package/README.md +187 -524
  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/claude/skills/understand/SKILL.md +52 -22
  9. package/codex/AGENTS.md +1 -1
  10. package/codex/guides/cli-multi-model-workflow.md +1 -1
  11. package/codex/guides/learning-flow.md +23 -12
  12. package/codex/guides/session-distill-workflow.md +22 -12
  13. package/compose/app_bridge/SKILL.md +75 -0
  14. package/compose/app_bridge/agents/openai.yaml +2 -0
  15. package/compose/app_bridge/scripts/bridge.py +76 -0
  16. package/compose/bootstrap/SKILL.md +12 -1
  17. package/compose/corpus.py +31 -9
  18. package/compose/corpus_app.py +456 -0
  19. package/compose/corpus_import.py +529 -0
  20. package/compose/corpus_install.py +202 -18
  21. package/compose/corpus_session.py +27 -0
  22. package/compose/corpus_setup.py +676 -0
  23. package/compose/corpus_setup_cli.py +585 -0
  24. package/compose/corpus_setup_i18n.py +324 -0
  25. package/compose/corpus_setup_ui.py +647 -0
  26. package/compose/corpus_store.py +213 -32
  27. package/compose/corpus_transaction.py +43 -10
  28. package/compose/corpus_ui_runtime.py +278 -0
  29. package/compose/corpus_understand.py +173 -22
  30. package/compose/setup/START.md +158 -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 +206 -0
  47. package/docs/understand.md +88 -0
  48. package/install.sh +75 -46
  49. package/launch/agent-launch.py +99 -52
  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
@@ -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
- store._catalog_module = lambda: self._private_module(package_root, "corpus_catalog")
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
- applied = available if selection == ["all"] else sorted(value.rsplit("/", 1)[-1] for value in selection)
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
- applied = available if selection == ["all"] else sorted(value.rsplit("/", 1)[-1] for value in selection)
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) -> dict[str, Any]:
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
- requested = self._normalized_domains(domains)
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
- return {"removed": shell_removed, "preserved": ["no private install record"]}
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
- return {"removed": removed, "preserved": preserved,
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 corpus")
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("optional --with dependencies are not installed by private corpus mode")
1650
- result = installer.install(args.domains, args.dry_run)
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
- print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
1663
- return 0
1664
- except InstallError as exc:
1665
- print(f"corpus-install: {exc}", file=sys.stderr)
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)