getcodexy 1.13.0__tar.gz → 1.13.2__tar.gz

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 (88) hide show
  1. {getcodexy-1.13.0 → getcodexy-1.13.2}/PKG-INFO +1 -1
  2. {getcodexy-1.13.0 → getcodexy-1.13.2}/build_backend.py +1 -0
  3. {getcodexy-1.13.0 → getcodexy-1.13.2}/pyproject.toml +1 -1
  4. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/_version_data/uv.lock +1 -1
  5. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/activation_transaction.py +5 -0
  6. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/cache.py +6 -0
  7. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component-manifest.json +8 -8
  8. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_capability_observation.py +2 -0
  9. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_capability_probe.py +6 -0
  10. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_cli.py +6 -0
  11. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_core_hooks.py +3 -0
  12. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_health.py +5 -5
  13. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_health_support.py +3 -0
  14. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_hook_activation_expected.py +5 -0
  15. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_hook_activation_host.py +4 -0
  16. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_hook_activation_validation.py +2 -0
  17. getcodexy-1.13.2/src/codexy_runtime_tools/component_hook_registration.py +120 -0
  18. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_inspection.py +3 -0
  19. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_inspection_host.py +1 -0
  20. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_inspection_types.py +2 -0
  21. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_integrity.py +5 -5
  22. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_interactive_probe.py +3 -0
  23. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_inventory_classification.py +7 -0
  24. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_inventory_records.py +2 -0
  25. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_lifecycle_admission.py +4 -0
  26. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_lifecycle_finish.py +5 -0
  27. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_lifecycle_interlock.py +2 -0
  28. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_lifecycle_mcp.py +2 -0
  29. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_lifecycle_operation.py +2 -2
  30. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_lifecycle_preflight.py +3 -0
  31. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_lifecycle_recovery.py +3 -2
  32. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_lifecycle_support.py +2 -0
  33. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_lifecycle_terminal.py +2 -0
  34. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_manifest.py +1 -0
  35. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_manifest_dependencies.py +2 -0
  36. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_manifest_values.py +2 -0
  37. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_mcp_cache.py +4 -0
  38. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_registration_files.py +1 -0
  39. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_registration_health.py +5 -108
  40. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_resolver.py +3 -1
  41. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_transaction_durability.py +1 -0
  42. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_transaction_receipts.py +1 -0
  43. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_transaction_snapshot.py +1 -0
  44. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_transaction_state.py +1 -0
  45. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_transition_journal.py +2 -0
  46. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_transition_model.py +1 -0
  47. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_transition_receipt.py +1 -0
  48. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_transition_rejections.py +1 -0
  49. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_watcher_probe.py +1 -0
  50. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/contract.py +1 -0
  51. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/github_checks.py +4 -0
  52. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/github_pre_session.py +1 -0
  53. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/identity.py +1 -0
  54. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/installer.py +3 -0
  55. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/local_marketplace.py +1 -0
  56. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/marketplace_identity.py +2 -0
  57. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/marketplace_repin.py +1 -0
  58. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/monolith_baseline.py +1 -0
  59. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/monolith_classifier.py +1 -0
  60. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/monolith_migration.py +2 -0
  61. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/monolith_migration_host.py +1 -0
  62. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/monolith_migration_plan.py +1 -0
  63. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/package.py +3 -0
  64. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/package_archive.py +2 -0
  65. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/plugin_resolution.py +3 -0
  66. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/pre_session.py +2 -0
  67. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/release_contract_validation.py +1 -0
  68. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/runtime.py +1 -0
  69. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/runtime_configuration.py +1 -0
  70. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/source.py +1 -0
  71. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/title_policy.py +1 -0
  72. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/updater.py +1 -1
  73. {getcodexy-1.13.0 → getcodexy-1.13.2}/uv.lock +1 -1
  74. {getcodexy-1.13.0 → getcodexy-1.13.2}/README.md +0 -0
  75. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/__init__.py +0 -0
  76. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_capability_probe_process.py +0 -0
  77. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_hook_activation.py +0 -0
  78. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_lifecycle.py +0 -0
  79. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_mcp_materialization.py +0 -0
  80. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_mcp_probe.py +0 -0
  81. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_observed_inventory.py +0 -0
  82. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_registration_catalog.py +0 -0
  83. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/component_transaction_identity.py +0 -0
  84. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/mcp_bootstrap.py +0 -0
  85. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/monolith_migration_state.py +0 -0
  86. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/release_provenance.py +0 -0
  87. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/release_watcher_contract.py +0 -0
  88. {getcodexy-1.13.0 → getcodexy-1.13.2}/src/codexy_runtime_tools/version_lock.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getcodexy
3
- Version: 1.13.0
3
+ Version: 1.13.2
4
4
  Summary: A Codex harness for coordinating agents through planning, implementation, verification, and review.
5
5
  Author: Eunsoo Lee
6
6
  License-Expression: MIT
@@ -14,6 +14,7 @@ _PACKAGED_LOCK = _ROOT / "src/codexy_runtime_tools/_version_data/uv.lock"
14
14
 
15
15
 
16
16
  def _materialize_version_lock() -> None:
17
+ """Copy the canonical project lock into the package payload before builds."""
17
18
  contents = _CANONICAL_LOCK.read_bytes()
18
19
  if not contents:
19
20
  raise ValueError("packages/getcodexy/uv.lock must not be empty")
@@ -5,7 +5,7 @@ backend-path = ["."]
5
5
 
6
6
  [project]
7
7
  name = "getcodexy"
8
- version = "1.13.0"
8
+ version = "1.13.2"
9
9
  description = "A Codex harness for coordinating agents through planning, implementation, verification, and review."
10
10
  readme = { file = "README.md", content-type = "text/markdown" }
11
11
  requires-python = ">=3.10"
@@ -4,5 +4,5 @@ requires-python = ">=3.10"
4
4
 
5
5
  [[package]]
6
6
  name = "getcodexy"
7
- version = "1.13.0"
7
+ version = "1.13.2"
8
8
  source = { editable = "." }
@@ -22,6 +22,7 @@ class ActivationSnapshot:
22
22
 
23
23
  @classmethod
24
24
  def capture(cls, home: Path) -> "ActivationSnapshot":
25
+ """Capture only Codexy activation paths and named config backups."""
25
26
  if not home.exists():
26
27
  return cls(home, ())
27
28
  files: list[Entry] = []
@@ -36,6 +37,7 @@ class ActivationSnapshot:
36
37
  return cls(home, tuple(files))
37
38
 
38
39
  def restore(self) -> None:
40
+ """Rebuild the captured paths without following links or overwriting files."""
39
41
  for root in (
40
42
  Path("agents/codexy-github"),
41
43
  Path("agents/codexy"),
@@ -47,6 +49,7 @@ class ActivationSnapshot:
47
49
  _remove_tree(backup)
48
50
  if self.entries:
49
51
  self.home.mkdir(parents=True, exist_ok=True)
52
+ # Recreate parents before children; O_EXCL rejects unexpected replacements.
50
53
  for entry in sorted(self.entries, key=lambda item: len(item.relative.parts)):
51
54
  path = self.home / entry.relative
52
55
  if entry.data is None:
@@ -84,6 +87,7 @@ def _capture_directory(relative: Path, descriptor: int) -> list[Entry]:
84
87
  _require_safe(metadata, relative)
85
88
  entries = [Entry(relative, None, stat.S_IMODE(metadata.st_mode))]
86
89
  for name in sorted(os.listdir(descriptor)):
90
+ # Inspect children without following links, then confirm each opened inode.
87
91
  child = os.stat(name, dir_fd=descriptor, follow_symlinks=False)
88
92
  child_relative = relative / name
89
93
  _require_safe(child, child_relative)
@@ -174,6 +178,7 @@ def _open_directory(path: Path, expected: os.stat_result) -> int:
174
178
 
175
179
 
176
180
  def _require_safe(metadata: os.stat_result, path: Path) -> None:
181
+ """Reject links, multiply-linked files, and special files in activation state."""
177
182
  reparse = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
178
183
  if (
179
184
  stat.S_ISLNK(metadata.st_mode)
@@ -1,3 +1,5 @@
1
+ """Validate plugin release identities and derive runtime cache keys."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
5
  import hashlib
@@ -15,6 +17,7 @@ SEMVER = re.compile(
15
17
 
16
18
 
17
19
  def plugin_release(manifest_path: Path, package_override: bool = False) -> str:
20
+ """Return a valid manifest version or the explicit package-override identity."""
18
21
  if not manifest_path.is_file():
19
22
  if package_override:
20
23
  return "package-override"
@@ -29,6 +32,7 @@ def plugin_release(manifest_path: Path, package_override: bool = False) -> str:
29
32
  def releases_match(
30
33
  expected_manifest: Path, observed_manifest: Path
31
34
  ) -> tuple[bool, str]:
35
+ """Compare release versions while retaining safe mismatch text."""
32
36
  try:
33
37
  expected = plugin_release(expected_manifest)
34
38
  except (OSError, ValueError, json.JSONDecodeError):
@@ -54,7 +58,9 @@ def releases_match(
54
58
  def runtime_cache_key(
55
59
  *, manifest: Path, package_override: bool, identity: list[str]
56
60
  ) -> str:
61
+ """Bind cache identity to package release so an upgrade cannot reuse old state."""
57
62
  release = plugin_release(manifest, package_override)
63
+ # NUL delimiters prevent adjacent identity components from becoming ambiguous.
58
64
  digest_input = "\0".join(
59
65
  ("codexy.runtime-cache/v2", *identity[:-1], release, identity[-1])
60
66
  )
@@ -29,7 +29,7 @@
29
29
  {
30
30
  "id": "core",
31
31
  "plugin": "codexy",
32
- "version": "1.13.0",
32
+ "version": "1.13.2",
33
33
  "dependencies": [],
34
34
  "asset": {
35
35
  "pluginId": "codexy@codexy",
@@ -63,7 +63,7 @@
63
63
  {
64
64
  "id": "github",
65
65
  "plugin": "codexy-github",
66
- "version": "1.13.0",
66
+ "version": "1.13.2",
67
67
  "dependencies": [
68
68
  "core"
69
69
  ],
@@ -79,7 +79,7 @@
79
79
  {
80
80
  "id": "devtools",
81
81
  "plugin": "codexy-devtools",
82
- "version": "1.13.0",
82
+ "version": "1.13.2",
83
83
  "dependencies": [
84
84
  "core"
85
85
  ],
@@ -97,27 +97,27 @@
97
97
  "compatibleCombinations": [
98
98
  {
99
99
  "components": [],
100
- "version": "1.13.0"
100
+ "version": "1.13.2"
101
101
  },
102
102
  {
103
103
  "components": [
104
104
  "core"
105
105
  ],
106
- "version": "1.13.0"
106
+ "version": "1.13.2"
107
107
  },
108
108
  {
109
109
  "components": [
110
110
  "core",
111
111
  "github"
112
112
  ],
113
- "version": "1.13.0"
113
+ "version": "1.13.2"
114
114
  },
115
115
  {
116
116
  "components": [
117
117
  "core",
118
118
  "devtools"
119
119
  ],
120
- "version": "1.13.0"
120
+ "version": "1.13.2"
121
121
  },
122
122
  {
123
123
  "components": [
@@ -125,7 +125,7 @@
125
125
  "github",
126
126
  "devtools"
127
127
  ],
128
- "version": "1.13.0"
128
+ "version": "1.13.2"
129
129
  }
130
130
  ]
131
131
  }
@@ -39,6 +39,7 @@ def component_observations(
39
39
  name: _entry(configured, source=_REGISTRATION_SOURCE, scope=_REGISTRATION_SCOPE)
40
40
  for name in CAPABILITIES.get(component, ())
41
41
  }
42
+ # Registration establishes configuration only; absent direct probes stay unknown.
42
43
  if not probe:
43
44
  return observations
44
45
  direct = probe.get("_capability_probes")
@@ -94,6 +95,7 @@ def _entry(
94
95
  source: str,
95
96
  scope: str,
96
97
  ) -> dict[str, object]:
98
+ """Keep unobserved capability states unknown and never infer verification."""
97
99
  return {
98
100
  "states": {
99
101
  "configured": "configured" if configured else UNKNOWN,
@@ -57,6 +57,7 @@ def _request(method, identifier=None, params=None):
57
57
 
58
58
 
59
59
  def probe_component(component, plugin, record):
60
+ """Probe one installed component, keeping core hook and watcher checks linked."""
60
61
  base = _base(record)
61
62
  if plugin is None:
62
63
  return base
@@ -67,6 +68,7 @@ def probe_component(component, plugin, record):
67
68
 
68
69
 
69
70
  def _probe_core(plugin, base):
71
+ # The watcher probe depends on a callable core hook; merge only observed results.
70
72
  hook = _probe_hook("core", plugin, base)
71
73
  if not hook.get("started") or not hook.get("callable"):
72
74
  return hook
@@ -80,6 +82,7 @@ def _probe_core(plugin, base):
80
82
 
81
83
 
82
84
  def _probe_hook(component, plugin, base):
85
+ """Execute a registered hook and validate its event-specific response."""
83
86
  event, marker = HOOK_SPECS[component]
84
87
  capability = f"hook:{marker}"
85
88
  payload = {"prompt": "review GitHub issue 723"}
@@ -121,6 +124,7 @@ def _probe_hook(component, plugin, base):
121
124
 
122
125
 
123
126
  def _registered_hook(plugin, event, marker):
127
+ """Return the platform-specific command for the registered marker, if any."""
124
128
  try:
125
129
  hooks = json.loads((plugin / "hooks/hooks.json").read_text(encoding="utf-8"))
126
130
  except (OSError, ValueError, json.JSONDecodeError):
@@ -163,6 +167,7 @@ def _argv(command, plugin, args=()):
163
167
  and values[0].lower().endswith((".bat", ".cmd"))
164
168
  and os.path.isfile(values[0])
165
169
  ):
170
+ # Batch files need cmd.exe quoting; other launchers remain an argv sequence.
166
171
  shell = os.environ.get("COMSPEC", "cmd.exe")
167
172
  command = f'"{values[0]}" {subprocess.list2cmdline(values[1:])}'.rstrip()
168
173
  return f'{subprocess.list2cmdline([shell])} /d /s /c "{command}"'
@@ -178,6 +183,7 @@ def probe_reason(probe, default):
178
183
 
179
184
 
180
185
  def identity_matches(manifest, component, record, probe):
186
+ """Require the registered release to match each probed runtime."""
181
187
  names = probe.get("runtime_names")
182
188
  versions = probe.get("runtime_versions")
183
189
  return (
@@ -20,6 +20,7 @@ from .version_lock import default_package_version
20
20
 
21
21
 
22
22
  def main(argv: list[str] | None = None) -> int:
23
+ """Dispatch lifecycle and inspection commands to their stable receipt forms."""
23
24
  parser = argparse.ArgumentParser(prog="getcodexy", allow_abbrev=False)
24
25
  parser.add_argument(
25
26
  "--codex",
@@ -68,6 +69,7 @@ def main(argv: list[str] | None = None) -> int:
68
69
  arguments.codex,
69
70
  )
70
71
  except PreAdmissionError as error:
72
+ # Host admission failures use machine receipts only where JSON was requested.
71
73
  if arguments.command == "bootstrap" and arguments.json_output:
72
74
  print(json.dumps(_bootstrap_host_failure(), sort_keys=True))
73
75
  return 2
@@ -92,6 +94,7 @@ def main(argv: list[str] | None = None) -> int:
92
94
 
93
95
 
94
96
  def _record_capability_failures(receipt: dict[str, object]) -> None:
97
+ """Add each known live capability failure once to doctor/status errors."""
95
98
  errors = receipt.get("errors")
96
99
  health = receipt.get("component_health")
97
100
  if not isinstance(errors, list) or not isinstance(health, list):
@@ -140,6 +143,7 @@ def _human(command: str, receipt: dict[str, object]) -> str:
140
143
  if command in {"install", "update", "remove", "bootstrap"} and receipt.get(
141
144
  "outcome"
142
145
  ) in {"completed", "pending-action"}:
146
+ # A new Codex task is needed because the host loads installed tools at startup.
143
147
  return (
144
148
  f"getcodexy {command}: {receipt['outcome']}; "
145
149
  "start a fresh Codex task to refresh installed plugins and MCP tools"
@@ -165,6 +169,7 @@ def _human(command: str, receipt: dict[str, object]) -> str:
165
169
 
166
170
 
167
171
  def _bootstrap_host_failure() -> dict[str, object]:
172
+ """Encode trusted-host admission rejection using the bootstrap receipt schema."""
168
173
  rejection = Rejection.from_failure(
169
174
  RejectionStage.HOST, StateFailure.INCONSISTENT_INSTALLED_STATE
170
175
  )
@@ -174,6 +179,7 @@ def _bootstrap_host_failure() -> dict[str, object]:
174
179
 
175
180
 
176
181
  def _migration_host_failure() -> dict[str, object]:
182
+ """Return the equivalent machine-readable migration rejection without running it."""
177
183
  return {
178
184
  "schema": "getcodexy.monolith-migration-receipt.v1",
179
185
  "command": "migrate",
@@ -14,6 +14,8 @@ LAUNCHERS = tuple(
14
14
  for _, stem in (*COMMAND_HOOKS, *LIFECYCLE_HOOKS)
15
15
  for extension in ("sh", "cmd")
16
16
  )
17
+ # Windows invokes the event script with `py.exe -3 -I -B`, bypassing the POSIX
18
+ # resolver; keep the entrypoint and sibling event helper in this required set.
17
19
  DEPENDENCIES = (
18
20
  "hooks/codexy-hook-runtime.sh",
19
21
  "hooks/codexy-thread-delivery.py",
@@ -30,4 +32,5 @@ DEPENDENCIES = (
30
32
  "hooks/codexy-watcher-interrupt.sh",
31
33
  "hooks/codexy-watcher-interrupt.cmd",
32
34
  "hooks/codexy_watcher_interrupt.py",
35
+ "hooks/codexy_watcher_interrupt_events.py",
33
36
  )
@@ -41,7 +41,7 @@ def health(
41
41
  activation: dict[str, str] | None = None,
42
42
  codex_home=None,
43
43
  ) -> list[dict[str, object]]:
44
- expected = set(recorded or ()) | set(actual)
44
+ expected = set(recorded or ()) | set(actual) # Include known and live components.
45
45
  context = (
46
46
  manifest,
47
47
  actual,
@@ -118,7 +118,7 @@ def _component_health(context, component):
118
118
  (not installed, "installed", "component-not-installed"),
119
119
  (not configured, "configured", "component-not-configured"),
120
120
  )
121
- for failed, stage, reason in checks:
121
+ for failed, stage, reason in checks: # Report the earliest failure.
122
122
  if failed:
123
123
  return _mark(result, stage, reason)
124
124
  if activation is not None and component in activation:
@@ -155,7 +155,7 @@ def _component_health(context, component):
155
155
 
156
156
 
157
157
  def _mark(result: dict[str, object], stage: str, reason: str) -> dict[str, object]:
158
- fallback, restart = FAILURES[reason]
158
+ fallback, restart = FAILURES[reason] # Map the reason to recovery guidance.
159
159
  result.update(
160
160
  first_failure_stage=stage,
161
161
  reason_code=reason,
@@ -173,7 +173,7 @@ def _mark(result: dict[str, object], stage: str, reason: str) -> dict[str, objec
173
173
 
174
174
 
175
175
  def _registration_roles(home, root, marker, expected, read_regular, max_bytes):
176
- roles, managed, unmanaged = [], [], []
176
+ roles, managed, unmanaged = [], [], [] # Track expected and marker-owned roles.
177
177
  for name, value in expected.items():
178
178
  roles.append(
179
179
  _registered_role(home, root, name, value, marker, read_regular, max_bytes)
@@ -219,7 +219,7 @@ def _registered_role(
219
219
  read_regular,
220
220
  max_bytes: int,
221
221
  ) -> dict[str, object]:
222
- try:
222
+ try: # Inspect the marker-owned role file as a bounded regular input.
223
223
  current = (
224
224
  read_regular(home, root.relative_to(home) / name, max_bytes)
225
225
  .decode()
@@ -36,6 +36,7 @@ def _legacy_state(
36
36
  host_error,
37
37
  codex_home=None,
38
38
  ):
39
+ """Preserve the coarse state contract while checking installed package integrity."""
39
40
  record = records.get(component)
40
41
  if admission_error or host_error:
41
42
  return "incompatible"
@@ -94,6 +95,7 @@ def _observed(record):
94
95
 
95
96
 
96
97
  def _authority_valid(record):
98
+ """Accept explicit attestation or recognized local marketplace forms."""
97
99
  if not record:
98
100
  return False
99
101
  key = next((key for key in AUTHORITY_KEYS if key in record), None)
@@ -128,6 +130,7 @@ def _authority_valid(record):
128
130
 
129
131
 
130
132
  def version_relation(manifest, record):
133
+ """Treat unparseable installed versions as incompatible, never as a match."""
131
134
  try:
132
135
  return compare_versions(record_version(record), manifest.version)
133
136
  except ComponentResolutionError:
@@ -32,6 +32,8 @@ _EVENT_KEYS = {
32
32
 
33
33
  @dataclass(frozen=True)
34
34
  class ExpectedHook:
35
+ """Installed hook identity and settings used to verify the host's active row."""
36
+
35
37
  component: str
36
38
  plugin_id: str
37
39
  version: str
@@ -48,6 +50,7 @@ class ExpectedHook:
48
50
 
49
51
  @property
50
52
  def key(self) -> str:
53
+ """Keep duplicate registrations distinct by their declared array positions."""
51
54
  return (
52
55
  f"{self.plugin_id}:hooks/hooks.json:{self.event_key}:"
53
56
  f"{self.group_index}:{self.hook_index}"
@@ -59,6 +62,7 @@ def expected_hooks(
59
62
  components: tuple[str, ...],
60
63
  records: dict[str, dict[str, object]],
61
64
  ) -> dict[str, tuple[ExpectedHook, ...]]:
65
+ """Parse installed manifests into exact, platform-specific hook expectations."""
62
66
  expected: dict[str, tuple[ExpectedHook, ...]] = {}
63
67
  for component in components:
64
68
  record = records.get(component)
@@ -81,6 +85,7 @@ def expected_hooks(
81
85
  raise HookStateError("installed hook registration has an invalid shape")
82
86
  hooks_map = cast(dict[str, object], hooks)
83
87
  entries: list[ExpectedHook] = []
88
+ # Event, positions, and command variant bind each host row to its source.
84
89
  for event, groups_value in hooks_map.items():
85
90
  if event not in _EVENT_LABELS or event not in _EVENT_KEYS:
86
91
  raise HookStateError("installed hook registration has an unknown event")
@@ -43,6 +43,7 @@ def list_hooks(executable: Path, codex_home: Path) -> tuple[dict[str, object], .
43
43
  "unable to determine the inspected working directory"
44
44
  ) from error
45
45
  environment = os.environ.copy()
46
+ # Drop inherited variables that can redirect Git or Python during host startup.
46
47
  for name in _ENVIRONMENT_UNSAFE:
47
48
  _ = environment.pop(name, None)
48
49
  environment.update(
@@ -132,6 +133,7 @@ def list_hooks(executable: Path, codex_home: Path) -> tuple[dict[str, object], .
132
133
  pass
133
134
  cleanup_failed = False
134
135
  if process.poll() is None:
136
+ # Stop the Windows process tree; POSIX cleans up the direct child.
135
137
  if os.name == "nt":
136
138
  try:
137
139
  _ = process.wait(timeout=1)
@@ -194,6 +196,7 @@ def normalize_hook_rows(value: object) -> tuple[dict[str, object], ...]:
194
196
  if isinstance(hooks, list):
195
197
  value = cast(list[object], hooks)
196
198
  elif isinstance(data, list):
199
+ # Host versions may group hooks by CWD; reject any group carrying warnings.
197
200
  flattened: list[dict[str, object]] = []
198
201
  for item_value in cast(list[object], data):
199
202
  if not isinstance(item_value, dict):
@@ -223,6 +226,7 @@ def normalize_hook_rows(value: object) -> tuple[dict[str, object], ...]:
223
226
 
224
227
 
225
228
  def _response(values: queue.Queue[object], identifier: int) -> dict[str, object]:
229
+ """Wait for the requested JSON-RPC id within this call's timeout window."""
226
230
  deadline = time.monotonic() + HOOK_LIST_TIMEOUT_SECONDS
227
231
  while True:
228
232
  remaining = deadline - time.monotonic()
@@ -16,6 +16,7 @@ def classify_activation(
16
16
  *,
17
17
  codex_home: Path | None = None,
18
18
  ) -> dict[str, str]:
19
+ """Require one enabled, trusted host row matching each installed hook exactly."""
19
20
  observed = normalize_hook_rows(rows)
20
21
  by_key: dict[str, list[dict[str, object]]] = {}
21
22
  for row in observed:
@@ -38,6 +39,7 @@ def classify_activation(
38
39
  component_keys = {
39
40
  row.get("key") for row in component_rows if isinstance(row.get("key"), str)
40
41
  }
42
+ # Extra rows indicate stale host state, even when required rows match.
41
43
  if component_keys - required_keys:
42
44
  failures[component] = "required-hook-trust-stale"
43
45
  continue
@@ -0,0 +1,120 @@
1
+ """Canonical hook manifests for installed Codexy components."""
2
+
3
+ from .component_core_hooks import (
4
+ COMMAND_HOOKS as _CORE_COMMAND_HOOKS,
5
+ LIFECYCLE_HOOKS as _CORE_LIFECYCLE_HOOKS,
6
+ LAUNCHERS as CORE_HOOK_LAUNCHERS,
7
+ )
8
+
9
+
10
+ def _hook(command: str, windows: str, timeout: int) -> dict[str, object]:
11
+ return {
12
+ "type": "command",
13
+ "command": command,
14
+ "commandWindows": windows,
15
+ "timeout": timeout,
16
+ }
17
+
18
+
19
+ def _command_hook(matcher: str, launcher_stem: str, event: str) -> dict[str, object]:
20
+ command = f'"${{PLUGIN_ROOT}}/hooks/{launcher_stem}'
21
+ return {
22
+ "matcher": matcher,
23
+ "hooks": [_hook(f'{command}.sh" {event}', f'{command}.cmd" {event}', 5)],
24
+ }
25
+
26
+
27
+ def _lifecycle_hook(launcher_stem: str, event: str) -> dict[str, object]:
28
+ command = f'"${{PLUGIN_ROOT}}/hooks/{launcher_stem}'
29
+ return {"hooks": [_hook(f'{command}.sh" {event}', f'{command}.cmd" {event}', 3)]}
30
+
31
+
32
+ def _bash_hooks(event: str) -> list[dict[str, object]]:
33
+ return [_command_hook("^Bash$", "codexy-destructive-command", event)]
34
+
35
+
36
+ def _title_hooks(event: str) -> list[dict[str, object]]:
37
+ return [_title_hook(matcher, kind, event) for matcher, kind in _TITLE_MATCHERS]
38
+
39
+
40
+ def _title_hook(matcher: str, kind: str, event: str) -> dict[str, object]:
41
+ return {
42
+ "matcher": matcher,
43
+ "hooks": [
44
+ _hook(
45
+ f'"${{PLUGIN_ROOT}}/hooks/codexy-title-check.sh" {event} {kind}',
46
+ f'"${{PLUGIN_ROOT}}/hooks/codexy-title-check.cmd" {event} {kind}',
47
+ 5,
48
+ )
49
+ ],
50
+ }
51
+
52
+
53
+ _TITLE_MATCHERS = (
54
+ (
55
+ "^(?:mcp__codex_apps__github_(?:create|update)_issue|github\\.(?:create|update)_issue)$",
56
+ "issue",
57
+ ),
58
+ (
59
+ "^(?:mcp__codex_apps__github_(?:create|update)_pull_request|github\\.(?:create|update)_pull_request)$",
60
+ "pr",
61
+ ),
62
+ (
63
+ "^(?:mcp__codex_apps__github_(?:merge_pull_request|enable_auto_merge)|github\\.(?:merge_pull_request|enable_auto_merge))$",
64
+ "merge",
65
+ ),
66
+ ("^functions\\.exec$", "nested"),
67
+ ("^Bash$", "shell"),
68
+ )
69
+
70
+
71
+ def _core_hooks() -> dict[str, object]:
72
+ """Build core event groups, keeping interrupt and prompt callbacks matcher-free."""
73
+ command = lambda event: [
74
+ _command_hook(matcher, stem, event) for matcher, stem in _CORE_COMMAND_HOOKS
75
+ ]
76
+ lifecycle = lambda event: [
77
+ _command_hook(matcher, stem, event) for matcher, stem in _CORE_LIFECYCLE_HOOKS
78
+ ]
79
+ return {
80
+ "hooks": {
81
+ "PermissionRequest": command("PermissionRequest"),
82
+ "PreToolUse": command("PreToolUse") + lifecycle("PreToolUse"),
83
+ "Interrupt": [
84
+ _lifecycle_hook(stem, "Interrupt") for _, stem in _CORE_LIFECYCLE_HOOKS
85
+ ],
86
+ "UserPromptSubmit": [
87
+ _lifecycle_hook(stem, "UserPromptSubmit")
88
+ for _, stem in _CORE_LIFECYCLE_HOOKS
89
+ ],
90
+ }
91
+ }
92
+
93
+
94
+ def _github_hooks() -> dict[str, object]:
95
+ context = '"${PLUGIN_ROOT}/hooks/codexy-github-workflow-context'
96
+ return {
97
+ "hooks": {
98
+ "UserPromptSubmit": [
99
+ {"hooks": [_hook(f'{context}.sh"', f'{context}.cmd"', 5)]}
100
+ ],
101
+ "PermissionRequest": _title_hooks("PermissionRequest")
102
+ + _bash_hooks("PermissionRequest"),
103
+ "PreToolUse": _title_hooks("PreToolUse") + _bash_hooks("PreToolUse"),
104
+ }
105
+ }
106
+
107
+
108
+ HOOKS = {"core": _core_hooks(), "github": _github_hooks()}
109
+ LAUNCHERS = {
110
+ "core": CORE_HOOK_LAUNCHERS,
111
+ "github": (
112
+ "hooks/codexy-github-workflow-context.sh",
113
+ "hooks/codexy-github-workflow-context.cmd",
114
+ "hooks/codexy-title-check.sh",
115
+ "hooks/codexy-title-check.cmd",
116
+ "hooks/codexy-destructive-command.sh",
117
+ "hooks/codexy-destructive-command.cmd",
118
+ ),
119
+ "devtools": ("mcp/codexy-mcp-devtools",),
120
+ }
@@ -104,6 +104,7 @@ def _inspect(
104
104
  runner: Runner | None,
105
105
  hook_lister: HookLister | None = None,
106
106
  ) -> InspectionReport:
107
+ """Compare live host and hook state with recorded package data."""
107
108
  home, manifest = _absolute(codex_home), load_component_manifest()
108
109
  _validate_real_path(home, require_exists=False)
109
110
  recorded, inventory, inventory_error = _recorded(home)
@@ -115,6 +116,7 @@ def _inspect(
115
116
  host_error = probe.value if probe else None
116
117
  if probe is None:
117
118
  assert executable is not None and invoke is not None
119
+ # Installed-plugin and marketplace reads supply live authority for the report.
118
120
  try:
119
121
  installed = _json(
120
122
  invoke([str(executable), "plugin", "list", "--json"]), "plugin list"
@@ -210,6 +212,7 @@ def _actual(
210
212
  installed: object,
211
213
  root: MarketplaceBinding | None,
212
214
  ) -> tuple[tuple[str, ...], dict[str, dict[str, object]], str | None]:
215
+ """Reconcile parsed installed records with host-admitted canonical components."""
213
216
  actual: tuple[str, ...] = ()
214
217
  records: dict[str, dict[str, object]] = {}
215
218
  try:
@@ -23,6 +23,7 @@ class ProbeStage(str, Enum):
23
23
  def host(
24
24
  home: Path, codex: Path | None, runner: Runner | None
25
25
  ) -> tuple[Path, Runner, None] | tuple[None, None, ProbeStage]:
26
+ """Resolve Codex and return a stage-specific setup failure."""
26
27
  try:
27
28
  return (
28
29
  trusted_codex(codex or _find_codex()),
@@ -6,6 +6,8 @@ from .component_manifest import ComponentManifest
6
6
 
7
7
 
8
8
  class InspectionReport(TypedDict):
9
+ """Internal read-only snapshot shared by status and doctor projections."""
10
+
9
11
  manifest: ComponentManifest
10
12
  actual: tuple[str, ...]
11
13
  recorded: tuple[str, ...] | None
@@ -41,8 +41,8 @@ def _verify_component(
41
41
  def _read_component(
42
42
  root: Path, verified: dict[Path, bytes], modes: dict[Path, int]
43
43
  ) -> None:
44
- size = sum(map(len, verified.values()))
45
- pending = [Path()]
44
+ size = sum(map(len, verified.values())) # The manifest shares the same cap.
45
+ pending = [Path()] # Traverse from the component root.
46
46
  while pending:
47
47
  relative_directory = pending.pop()
48
48
  directory = root / relative_directory
@@ -94,7 +94,7 @@ def frozen_component(
94
94
  )
95
95
  with os.fdopen(descriptor, "wb") as output:
96
96
  output.write(data)
97
- os.chmod(destination, 0o600 | (modes.get(relative, 0) & 0o111))
97
+ os.chmod(destination, 0o600 | (modes.get(relative, 0) & 0o111)) # keep exec
98
98
  yield target
99
99
 
100
100
 
@@ -177,7 +177,7 @@ def _read_regular_windows(
177
177
  opened = os.fstat(descriptor)
178
178
  if not stat.S_ISREG(opened.st_mode):
179
179
  raise ValueError(f"component integrity requires regular files: {target}")
180
- _, final = _windows_safe_path(root, relative)
180
+ _, final = _windows_safe_path(root, relative) # Recheck path after opening it.
181
181
  if (opened.st_dev, opened.st_ino) != (final.st_dev, final.st_ino):
182
182
  raise ValueError(
183
183
  f"component integrity path changed while reading: {target}"
@@ -231,7 +231,7 @@ def _has_windows_reparse_point(metadata: os.stat_result) -> bool:
231
231
  def valid_agent_toml(text: str, path: Path) -> bool:
232
232
  try:
233
233
  import tomllib
234
- except ModuleNotFoundError:
234
+ except ModuleNotFoundError: # Keep validation dependency-free on older runtimes.
235
235
  import ast
236
236
 
237
237
  tree = ast.parse(text, filename=str(path))