getcodexy 1.13.4__tar.gz → 2.0.0__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 (93) hide show
  1. {getcodexy-1.13.4 → getcodexy-2.0.0}/PKG-INFO +1 -1
  2. {getcodexy-1.13.4 → getcodexy-2.0.0}/pyproject.toml +1 -1
  3. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/_version_data/uv.lock +1 -1
  4. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component-manifest.json +8 -12
  5. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_capability_observation.py +14 -1
  6. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_capability_probe.py +21 -13
  7. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_capability_probe_process.py +9 -9
  8. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_cli.py +14 -1
  9. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_core_hooks.py +2 -11
  10. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_health.py +53 -91
  11. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_health_support.py +35 -7
  12. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_hook_registration.py +2 -24
  13. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_lifecycle_finish.py +13 -61
  14. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_lifecycle_recovery.py +2 -2
  15. getcodexy-2.0.0/src/codexy_runtime_tools/component_mcp_capability.py +204 -0
  16. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_mcp_materialization.py +59 -89
  17. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_registration_catalog.py +1 -1
  18. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_registration_files.py +3 -2
  19. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_registration_health.py +108 -11
  20. getcodexy-2.0.0/src/codexy_runtime_tools/component_registration_lifecycle.py +107 -0
  21. getcodexy-2.0.0/src/codexy_runtime_tools/component_registration_orphans.py +93 -0
  22. getcodexy-2.0.0/src/codexy_runtime_tools/component_registration_projection.py +77 -0
  23. getcodexy-2.0.0/src/codexy_runtime_tools/component_registration_snapshot.py +104 -0
  24. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_transaction_snapshot.py +8 -11
  25. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/github_pre_session.py +5 -4
  26. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/installer.py +0 -7
  27. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/mcp_bootstrap.py +3 -2
  28. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/package.py +2 -2
  29. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/release_watcher_contract.py +1 -1
  30. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/runtime.py +2 -2
  31. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/runtime_configuration.py +12 -9
  32. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/updater.py +1 -1
  33. {getcodexy-1.13.4 → getcodexy-2.0.0}/uv.lock +1 -1
  34. getcodexy-1.13.4/src/codexy_runtime_tools/component_watcher_probe.py +0 -152
  35. {getcodexy-1.13.4 → getcodexy-2.0.0}/README.md +0 -0
  36. {getcodexy-1.13.4 → getcodexy-2.0.0}/build_backend.py +0 -0
  37. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/__init__.py +0 -0
  38. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/activation_transaction.py +0 -0
  39. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/cache.py +0 -0
  40. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_hook_activation.py +0 -0
  41. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_hook_activation_expected.py +0 -0
  42. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_hook_activation_host.py +0 -0
  43. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_hook_activation_validation.py +0 -0
  44. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_inspection.py +0 -0
  45. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_inspection_host.py +0 -0
  46. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_inspection_types.py +0 -0
  47. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_integrity.py +0 -0
  48. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_interactive_probe.py +0 -0
  49. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_inventory_classification.py +0 -0
  50. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_inventory_records.py +0 -0
  51. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_lifecycle.py +0 -0
  52. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_lifecycle_admission.py +0 -0
  53. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_lifecycle_interlock.py +0 -0
  54. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_lifecycle_mcp.py +0 -0
  55. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_lifecycle_operation.py +0 -0
  56. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_lifecycle_preflight.py +0 -0
  57. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_lifecycle_support.py +0 -0
  58. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_lifecycle_terminal.py +0 -0
  59. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_manifest.py +0 -0
  60. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_manifest_dependencies.py +0 -0
  61. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_manifest_values.py +0 -0
  62. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_mcp_cache.py +0 -0
  63. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_mcp_probe.py +0 -0
  64. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_observed_inventory.py +0 -0
  65. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_resolver.py +0 -0
  66. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_transaction_durability.py +0 -0
  67. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_transaction_identity.py +0 -0
  68. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_transaction_receipts.py +0 -0
  69. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_transaction_state.py +0 -0
  70. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_transition_journal.py +0 -0
  71. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_transition_model.py +0 -0
  72. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_transition_receipt.py +0 -0
  73. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/component_transition_rejections.py +0 -0
  74. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/contract.py +0 -0
  75. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/github_checks.py +0 -0
  76. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/identity.py +0 -0
  77. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/local_marketplace.py +0 -0
  78. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/marketplace_identity.py +0 -0
  79. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/marketplace_repin.py +0 -0
  80. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/monolith_baseline.py +0 -0
  81. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/monolith_classifier.py +0 -0
  82. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/monolith_migration.py +0 -0
  83. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/monolith_migration_host.py +0 -0
  84. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/monolith_migration_plan.py +0 -0
  85. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/monolith_migration_state.py +0 -0
  86. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/package_archive.py +0 -0
  87. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/plugin_resolution.py +0 -0
  88. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/pre_session.py +0 -0
  89. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/release_contract_validation.py +0 -0
  90. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/release_provenance.py +0 -0
  91. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/source.py +0 -0
  92. {getcodexy-1.13.4 → getcodexy-2.0.0}/src/codexy_runtime_tools/title_policy.py +0 -0
  93. {getcodexy-1.13.4 → getcodexy-2.0.0}/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.4
3
+ Version: 2.0.0
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
@@ -5,7 +5,7 @@ backend-path = ["."]
5
5
 
6
6
  [project]
7
7
  name = "getcodexy"
8
- version = "1.13.4"
8
+ version = "2.0.0"
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.4"
7
+ version = "2.0.0"
8
8
  source = { editable = "." }
@@ -29,7 +29,7 @@
29
29
  {
30
30
  "id": "core",
31
31
  "plugin": "codexy",
32
- "version": "1.13.4",
32
+ "version": "2.0.0",
33
33
  "dependencies": [],
34
34
  "asset": {
35
35
  "pluginId": "codexy@codexy",
@@ -39,10 +39,6 @@
39
39
  "assets/codexy-icon.png",
40
40
  "skills/self-improvement/SKILL.md",
41
41
  "skills/self-improvement/agents/openai.yaml",
42
- ".mcp.json",
43
- "mcp/codexy_mcp_bootstrap.py",
44
- "mcp/codexy-mcp-watcher.sh",
45
- "mcp/codexy-mcp-watcher.cmd",
46
42
  "skills/dreaming/scripts/resumable-context-capsule.sh",
47
43
  "skills/dreaming/scripts/resumable-context-capsule.cmd",
48
44
  "skills/dreaming/scripts/resumable_context_capsule.py",
@@ -63,7 +59,7 @@
63
59
  {
64
60
  "id": "github",
65
61
  "plugin": "codexy-github",
66
- "version": "1.13.4",
62
+ "version": "2.0.0",
67
63
  "dependencies": [
68
64
  "core"
69
65
  ],
@@ -79,7 +75,7 @@
79
75
  {
80
76
  "id": "devtools",
81
77
  "plugin": "codexy-devtools",
82
- "version": "1.13.4",
78
+ "version": "2.0.0",
83
79
  "dependencies": [
84
80
  "core"
85
81
  ],
@@ -97,27 +93,27 @@
97
93
  "compatibleCombinations": [
98
94
  {
99
95
  "components": [],
100
- "version": "1.13.4"
96
+ "version": "2.0.0"
101
97
  },
102
98
  {
103
99
  "components": [
104
100
  "core"
105
101
  ],
106
- "version": "1.13.4"
102
+ "version": "2.0.0"
107
103
  },
108
104
  {
109
105
  "components": [
110
106
  "core",
111
107
  "github"
112
108
  ],
113
- "version": "1.13.4"
109
+ "version": "2.0.0"
114
110
  },
115
111
  {
116
112
  "components": [
117
113
  "core",
118
114
  "devtools"
119
115
  ],
120
- "version": "1.13.4"
116
+ "version": "2.0.0"
121
117
  },
122
118
  {
123
119
  "components": [
@@ -125,7 +121,7 @@
125
121
  "github",
126
122
  "devtools"
127
123
  ],
128
- "version": "1.13.4"
124
+ "version": "2.0.0"
129
125
  }
130
126
  ]
131
127
  }
@@ -11,17 +11,19 @@ _DIRECT_SCOPE = "plugin-subprocess"
11
11
  _REGISTRATION_SOURCE = "getcodexy-registration-check"
12
12
  _REGISTRATION_SCOPE = "current-doctor-invocation"
13
13
 
14
+ # Registration records configuration only; native specialist invocation remains unverified.
14
15
  CAPABILITIES = {
15
16
  "core": (
16
17
  "hook:codexy-thread-delivery",
17
18
  "specialist:codexy-architect",
19
+ "specialist:codexy-advisor",
18
20
  "specialist:codexy-auditor",
19
21
  "specialist:codexy-cartographer",
20
22
  "specialist:codexy-inspector",
21
23
  "specialist:codexy-sentinel",
22
24
  "specialist:codexy-shipwright",
23
25
  "specialist:codexy-warden",
24
- "specialist:codexy-watcher",
26
+ # Legacy registration remains visible without implying a current Watcher process.
25
27
  "mcp:watcher",
26
28
  ),
27
29
  "github": ("hook:codexy-github-workflow-context", "specialist:codexy-weaver"),
@@ -45,7 +47,18 @@ def component_observations(
45
47
  direct = probe.get("_capability_probes")
46
48
  if not isinstance(direct, dict):
47
49
  direct = {}
50
+ contract = probe.get("_capability_contract")
51
+ if (
52
+ component == "core"
53
+ and isinstance(contract, dict)
54
+ and contract.get("mcp:watcher") is False
55
+ ):
56
+ # A validated no-MCP core has no configured Watcher capability to report.
57
+ observations.pop("mcp:watcher", None)
48
58
  for name in observations:
59
+ if name == "mcp:watcher":
60
+ # Current getcodexy never launches legacy Watcher MCP, so execution stays unknown.
61
+ continue
49
62
  details = direct.get(name)
50
63
  if isinstance(details, dict):
51
64
  observations[name] = _entry(
@@ -13,7 +13,7 @@ from .component_capability_probe_process import (
13
13
  _probe_diagnostics,
14
14
  _run,
15
15
  )
16
- from .component_watcher_probe import probe_watcher as _probe_watcher
16
+ from .component_mcp_materialization import selected_mcp_spec, valid_component_mcp
17
17
  from .version_lock import default_package_version
18
18
 
19
19
 
@@ -57,7 +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
+ """Probe one installed component without starting legacy core MCP servers."""
61
61
  base = _base(record)
62
62
  if plugin is None:
63
63
  return base
@@ -68,17 +68,25 @@ def probe_component(component, plugin, record):
68
68
 
69
69
 
70
70
  def _probe_core(plugin, base):
71
- # The watcher probe depends on a callable core hook; merge only observed results.
72
- hook = _probe_hook("core", plugin, base)
73
- if not hook.get("started") or not hook.get("callable"):
74
- return hook
75
- watcher = _probe_watcher(plugin, base)
76
- probes = dict(hook.get("_capability_probes", {}))
77
- probes.update(watcher.get("_capability_probes", {}))
78
- watcher["_capability_probes"] = probes
79
- if not watcher.get("started") or not watcher.get("callable"):
80
- return watcher
81
- return _outcome(base, _capability_probes=probes)
71
+ # Preserve legacy registration visibility while the current doctor stays read-only.
72
+ watcher_declared = _core_declares_watcher(plugin)
73
+ if watcher_declared is not None:
74
+ base["_capability_contract"] = {"mcp:watcher": watcher_declared}
75
+ return _probe_hook("core", plugin, base)
76
+
77
+
78
+ def _core_declares_watcher(plugin):
79
+ """Read legacy Watcher registration without launching its configured runner."""
80
+ try:
81
+ manifest = json.loads(
82
+ (plugin / ".codex-plugin/plugin.json").read_text(encoding="utf-8")
83
+ )
84
+ except (OSError, ValueError, json.JSONDecodeError):
85
+ return None
86
+ version = manifest.get("version") if isinstance(manifest, dict) else None
87
+ if not isinstance(version, str) or not valid_component_mcp(plugin, "core", version):
88
+ return None
89
+ return "watcher" in selected_mcp_spec(plugin, "core").servers
82
90
 
83
91
 
84
92
  def _probe_hook(component, plugin, base):
@@ -97,6 +97,7 @@ def _run_windows(argv, cwd, input_text, env, deadline):
97
97
  )
98
98
  cleanup_deadline = deadline
99
99
  abort_pipes = False
100
+ threads_stopped = True
100
101
  try:
101
102
  remaining = max(0.0, deadline - perf_counter())
102
103
  if not remaining:
@@ -107,16 +108,17 @@ def _run_windows(argv, cwd, input_text, env, deadline):
107
108
  abort_pipes = True
108
109
  cleanup_deadline = perf_counter() + _WINDOWS_CLEANUP_TIMEOUT
109
110
  _terminate_process_tree(process, cleanup_deadline)
110
- _cancel_windows_pipe_threads(process, cleanup_deadline)
111
+ threads_stopped = _cancel_windows_pipe_threads(process, cleanup_deadline)
111
112
  remaining = max(0.0, cleanup_deadline - perf_counter())
112
113
  if remaining:
113
114
  try:
114
- process.communicate(timeout=remaining)
115
+ _ = process.communicate(timeout=remaining)
116
+ threads_stopped = True
115
117
  except (OSError, ValueError, subprocess.TimeoutExpired):
116
118
  pass
117
119
  raise
118
120
  finally:
119
- _close_process_pipes(process, abort=abort_pipes, deadline=cleanup_deadline)
121
+ _close_process_pipes(process, abort_pipes, threads_stopped)
120
122
  _wait_for_exit(process, cleanup_deadline)
121
123
 
122
124
 
@@ -145,10 +147,8 @@ def _terminate_process_tree(process, deadline):
145
147
  pass
146
148
 
147
149
 
148
- def _close_process_pipes(process, abort=False, deadline=None):
149
- threads_stopped = True
150
- if abort:
151
- threads_stopped = _cancel_windows_pipe_threads(process, deadline)
150
+ def _close_process_pipes(process, abort=False, threads_stopped: bool = False):
151
+ # Timeout handling owns cancellation; repeating it here can outlive the deadline.
152
152
  for attribute in ("stdin", "stdout", "stderr"):
153
153
  stream = getattr(process, attribute, None)
154
154
  if stream is None:
@@ -177,7 +177,7 @@ def _abort_process_pipe(process, attribute, stream):
177
177
  setattr(process, attribute, None)
178
178
 
179
179
 
180
- def _cancel_windows_pipe_threads(process, deadline):
180
+ def _cancel_windows_pipe_threads(process, deadline) -> bool:
181
181
  threads = [
182
182
  getattr(process, attribute, None)
183
183
  for attribute in ("stdin_thread", "stdout_thread", "stderr_thread")
@@ -186,7 +186,7 @@ def _cancel_windows_pipe_threads(process, deadline):
186
186
  if not threads:
187
187
  return False
188
188
  for thread in threads:
189
- if thread.is_alive():
189
+ if thread.is_alive() and deadline > perf_counter():
190
190
  _cancel_synchronous_io(thread)
191
191
  for thread in threads:
192
192
  if not thread.is_alive():
@@ -90,7 +90,20 @@ def main(argv: list[str] | None = None) -> int:
90
90
  unhealthy = arguments.command in {"status", "doctor"} and bool(
91
91
  receipt.get("errors")
92
92
  )
93
- return 0 if receipt["outcome"] == "completed" and not unhealthy else 2
93
+ # Doctor health findings stay outside the closed lifecycle-domain error codes.
94
+ orphaned_registration = arguments.command == "doctor" and any(
95
+ isinstance(entry, dict)
96
+ and entry.get("reason_code")
97
+ in {"orphaned-managed-registration", "registration-inspection-unavailable"}
98
+ for entry in receipt.get("component_health", [])
99
+ )
100
+ return (
101
+ 0
102
+ if receipt["outcome"] == "completed"
103
+ and not unhealthy
104
+ and not orphaned_registration
105
+ else 2
106
+ )
94
107
 
95
108
 
96
109
  def _record_capability_failures(receipt: dict[str, object]) -> None:
@@ -7,15 +7,10 @@ COMMAND_HOOKS = (
7
7
  (_TOOL_PREFIX + "create_thread$", "codexy-child-thread-creation"),
8
8
  (r"^(?:(?:agents|multi_agent_v1)__)?spawn_agent$", "codexy-subagent-ownership"),
9
9
  )
10
- WATCHER_WAIT_HOOK = (r"^(?:mcp__[^ ]+__)?watcher_wait$", "codexy-watcher-interrupt")
11
- LIFECYCLE_HOOKS = (WATCHER_WAIT_HOOK,)
12
- # Observe only completed sender calls; failed sends cannot release recipient waits.
13
- POST_TOOL_HOOKS = (
14
- (r"^mcp__codex_app__send_message_to_thread$", "codexy-watcher-interrupt"),
15
- )
10
+ # The installed core package carries only its retained routing and ownership launchers.
16
11
  LAUNCHERS = tuple(
17
12
  f"hooks/{stem}.{extension}"
18
- for _, stem in (*COMMAND_HOOKS, *LIFECYCLE_HOOKS)
13
+ for _, stem in COMMAND_HOOKS
19
14
  for extension in ("sh", "cmd")
20
15
  )
21
16
  # Windows invokes the event script with `py.exe -3 -I -B`, bypassing the POSIX
@@ -33,8 +28,4 @@ DEPENDENCIES = (
33
28
  "hooks/codexy_policy/envelope.py",
34
29
  "hooks/codexy_policy/timing.py",
35
30
  "hooks/codexy_policy/windows_timing.py",
36
- "hooks/codexy-watcher-interrupt.sh",
37
- "hooks/codexy-watcher-interrupt.cmd",
38
- "hooks/codexy_watcher_interrupt.py",
39
- "hooks/codexy_watcher_interrupt_events.py",
40
31
  )
@@ -2,27 +2,27 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import os
6
- from pathlib import Path
7
-
8
5
  from . import component_capability_probe as _probe
9
6
  from .component_capability_observation import component_observations
10
7
  from .component_hook_activation import ACTIVATION_STATES
11
- from .component_mcp_materialization import MCP_COMPONENTS, valid_component_mcp_cache
8
+ from .component_mcp_materialization import (
9
+ MCP_COMPONENTS,
10
+ mcp_spec_for_plugin,
11
+ valid_component_mcp_cache,
12
+ )
12
13
  from .component_health_support import (
13
14
  _authority_valid,
14
15
  _health_plugin,
15
16
  _legacy_state,
16
17
  _observed,
18
+ _plugin_root,
19
+ _selected_registration_valid,
17
20
  manifest_is_valid,
18
21
  record_version,
19
22
  version_relation,
20
23
  )
21
24
  from .component_manifest import ComponentManifest
22
- from .component_registration_health import (
23
- registration_role as _registration_role,
24
- valid_registration,
25
- )
25
+ from .component_registration_orphans import include_orphaned_registrations
26
26
  from .updater import compare_managed_files
27
27
 
28
28
  FAILURES = _probe.FAILURES
@@ -51,11 +51,12 @@ def health(
51
51
  activation,
52
52
  codex_home,
53
53
  )
54
- return [
54
+ entries = [
55
55
  _component_health(context, component)
56
56
  for component in manifest.component_ids
57
57
  if component in expected
58
58
  ]
59
+ return include_orphaned_registrations(entries, manifest, actual, codex_home)
59
60
 
60
61
 
61
62
  def _component_health(context, component):
@@ -64,6 +65,17 @@ def _component_health(context, component):
64
65
  )
65
66
  record = records.get(component)
66
67
  installed = component in actual
68
+ expected_mcp_spec = None
69
+ if component in MCP_COMPONENTS:
70
+ source_plugin = _plugin_root(record)
71
+ if source_plugin is not None:
72
+ try:
73
+ # Check package shape separately; authority remains a later health check.
74
+ expected_mcp_spec = mcp_spec_for_plugin(
75
+ source_plugin, component, record_version(record)
76
+ )
77
+ except (OSError, RuntimeError, ValueError):
78
+ expected_mcp_spec = None
67
79
  plugin = _health_plugin(manifest, component, record, codex_home)
68
80
  registration = (
69
81
  compare_managed_files(plugin, codex_home, component)
@@ -76,7 +88,9 @@ def _component_health(context, component):
76
88
  and manifest_is_valid(
77
89
  plugin, manifest.component(component).plugin, record_version(record)
78
90
  )
79
- and valid_registration(plugin, component)
91
+ and _selected_registration_valid(
92
+ plugin, component, record_version(record), expected_mcp_spec
93
+ )
80
94
  and (
81
95
  not registration
82
96
  or not registration["observed"]
@@ -84,11 +98,26 @@ def _component_health(context, component):
84
98
  )
85
99
  and (
86
100
  component not in MCP_COMPONENTS
87
- or valid_component_mcp_cache(codex_home, component, manifest.version)
101
+ or (
102
+ expected_mcp_spec is not None
103
+ and valid_component_mcp_cache(
104
+ codex_home,
105
+ component,
106
+ manifest.version,
107
+ expected_spec=expected_mcp_spec,
108
+ )
109
+ )
88
110
  )
89
111
  )
90
112
  state = _legacy_state(
91
- manifest, component, actual, records, admission_error, host_error, codex_home
113
+ manifest,
114
+ component,
115
+ actual,
116
+ records,
117
+ admission_error,
118
+ host_error,
119
+ codex_home,
120
+ expected_mcp_spec,
92
121
  )
93
122
  registration_state = registration["state"] if registration else "exact"
94
123
  if state in {"healthy", "stale"} and registration_state != "exact":
@@ -109,7 +138,18 @@ def _component_health(context, component):
109
138
  restart_required=False,
110
139
  observed={
111
140
  **_observed(record),
112
- "capabilities": component_observations(component, configured),
141
+ # Keep a no-MCP core accurate when activation prevents its live probe.
142
+ "capabilities": component_observations(
143
+ component,
144
+ configured,
145
+ {
146
+ "_capability_contract": {
147
+ "mcp:watcher": "watcher" in expected_mcp_spec.servers
148
+ }
149
+ }
150
+ if component == "core" and expected_mcp_spec is not None
151
+ else None,
152
+ ),
113
153
  "registration": registration,
114
154
  },
115
155
  )
@@ -170,81 +210,3 @@ def _mark(result: dict[str, object], stage: str, reason: str) -> dict[str, objec
170
210
  ):
171
211
  result["state"] = "incompatible"
172
212
  return result
173
-
174
-
175
- def _registration_roles(home, root, marker, expected, read_regular, max_bytes):
176
- roles, managed, unmanaged = [], [], [] # Track expected and marker-owned roles.
177
- for name, value in expected.items():
178
- roles.append(
179
- _registered_role(home, root, name, value, marker, read_regular, max_bytes)
180
- )
181
- for entry in os.scandir(root):
182
- if entry.name in expected or not entry.name.endswith(".toml"):
183
- continue
184
- try:
185
- current = (
186
- read_regular(home, Path(entry.path).relative_to(home), max_bytes)
187
- .decode()
188
- .replace("\r\n", "\n")
189
- .replace("\r", "\n")
190
- )
191
- except (OSError, UnicodeDecodeError, ValueError):
192
- unmanaged.append(entry.name)
193
- continue
194
- (managed if current.startswith(marker) else unmanaged).append(entry.name)
195
- roles.extend(
196
- _registration_role(
197
- name, "stale", "managed role is not declared by the current catalog"
198
- )
199
- for name in managed
200
- )
201
- states = {item["state"] for item in roles}
202
- state = next(
203
- (
204
- candidate
205
- for candidate in ("unmanaged-conflict", "stale", "missing")
206
- if candidate in states or candidate == "unmanaged-conflict" and unmanaged
207
- ),
208
- "exact",
209
- )
210
- return roles, tuple(sorted(managed)), tuple(sorted(unmanaged)), state
211
-
212
-
213
- def _registered_role(
214
- home: Path,
215
- root: Path,
216
- name: str,
217
- expected: bytes,
218
- marker: str,
219
- read_regular,
220
- max_bytes: int,
221
- ) -> dict[str, object]:
222
- try: # Inspect the marker-owned role file as a bounded regular input.
223
- current = (
224
- read_regular(home, root.relative_to(home) / name, max_bytes)
225
- .decode()
226
- .replace("\r\n", "\n")
227
- .replace("\r", "\n")
228
- )
229
- except FileNotFoundError:
230
- return _registration_role(name, "missing", "managed role file is missing")
231
- except (OSError, UnicodeDecodeError, ValueError):
232
- return _registration_role(
233
- name,
234
- "unmanaged-conflict",
235
- "managed role file is unreadable or not a regular file",
236
- True,
237
- )
238
- if not current.startswith(marker):
239
- return _registration_role(
240
- name,
241
- "unmanaged-conflict",
242
- "existing role file is not marker-owned by Codexy",
243
- True,
244
- )
245
- exact = current.encode() == expected
246
- return _registration_role(
247
- name,
248
- "exact" if exact else "stale",
249
- None if exact else "registered role differs from the package",
250
- )
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  from .component_mcp_materialization import (
8
8
  MCP_COMPONENTS,
9
9
  component_cache_plugin,
10
- mcp_spec,
10
+ McpSpec,
11
11
  )
12
12
  from .component_manifest import ComponentManifest
13
13
  from .component_registration_health import valid_registration
@@ -19,14 +19,25 @@ SURFACE_PATHS = {
19
19
  "agents/catalog.toml",
20
20
  "hooks/hooks.json",
21
21
  "skills/wiki/SKILL.md",
22
- *(path.as_posix() for path in mcp_spec("core").surface_paths),
23
22
  ),
24
23
  "github": ("agents/catalog.toml", "hooks/hooks.json"),
25
- "devtools": tuple(path.as_posix() for path in mcp_spec("devtools").surface_paths),
24
+ "devtools": (),
26
25
  }
27
26
  AUTHORITY_KEYS = ("authority", "artifact_authority", "artifactAuthority")
28
27
 
29
28
 
29
+ def _selected_registration_valid(plugin, component, version, expected_mcp_spec):
30
+ """Require a verified source MCP contract before accepting cached registration."""
31
+ if component in MCP_COMPONENTS and expected_mcp_spec is None:
32
+ return False
33
+ return valid_registration(
34
+ plugin,
35
+ component,
36
+ version,
37
+ expected_mcp_spec=expected_mcp_spec,
38
+ )
39
+
40
+
30
41
  def _legacy_state(
31
42
  manifest,
32
43
  component,
@@ -35,6 +46,7 @@ def _legacy_state(
35
46
  admission_error,
36
47
  host_error,
37
48
  codex_home=None,
49
+ expected_mcp_spec: McpSpec | None = None,
38
50
  ):
39
51
  """Preserve the coarse state contract while checking installed package integrity."""
40
52
  record = records.get(component)
@@ -46,11 +58,15 @@ def _legacy_state(
46
58
  if relation != 0:
47
59
  return "stale" if relation < 0 else "incompatible"
48
60
  plugin = _health_plugin(manifest, component, record, codex_home)
49
- if plugin is None or not _required_files(manifest, component, plugin, codex_home):
61
+ if plugin is None or not _required_files(
62
+ manifest, component, plugin, codex_home, expected_mcp_spec
63
+ ):
50
64
  return "stale"
51
65
  if not manifest_is_valid(
52
66
  plugin, manifest.component(component).plugin, record_version(record)
53
- ) or not valid_registration(plugin, component):
67
+ ) or not _selected_registration_valid(
68
+ plugin, component, record_version(record), expected_mcp_spec
69
+ ):
54
70
  return "incompatible"
55
71
  if component == "core" and any(
56
72
  os.path.lexists(plugin / path)
@@ -62,9 +78,21 @@ def _legacy_state(
62
78
  return "healthy"
63
79
 
64
80
 
65
- def _required_files(manifest, component, plugin, codex_home=None):
81
+ def _required_files(
82
+ manifest, component, plugin, codex_home=None, expected_mcp_spec=None
83
+ ):
84
+ # The selected source contract controls whether doctor expects registration files.
85
+ if component in MCP_COMPONENTS and expected_mcp_spec is None:
86
+ return False
87
+ mcp_paths = (
88
+ tuple(path.as_posix() for path in expected_mcp_spec.surface_paths)
89
+ if component in MCP_COMPONENTS
90
+ else ()
91
+ )
66
92
  paths = (
67
- manifest.component(component).asset.required_paths + SURFACE_PATHS[component]
93
+ manifest.component(component).asset.required_paths
94
+ + SURFACE_PATHS[component]
95
+ + mcp_paths
68
96
  )
69
97
  return all(
70
98
  (plugin / path).is_file() and not (plugin / path).is_symlink() for path in paths
@@ -2,8 +2,6 @@
2
2
 
3
3
  from .component_core_hooks import (
4
4
  COMMAND_HOOKS as _CORE_COMMAND_HOOKS,
5
- LIFECYCLE_HOOKS as _CORE_LIFECYCLE_HOOKS,
6
- POST_TOOL_HOOKS as _CORE_POST_TOOL_HOOKS,
7
5
  LAUNCHERS as CORE_HOOK_LAUNCHERS,
8
6
  )
9
7
 
@@ -27,11 +25,6 @@ def _command_hook(
27
25
  }
28
26
 
29
27
 
30
- def _lifecycle_hook(launcher_stem: str, event: str) -> dict[str, object]:
31
- command = f'"${{PLUGIN_ROOT}}/hooks/{launcher_stem}'
32
- return {"hooks": [_hook(f'{command}.sh" {event}', f'{command}.cmd" {event}', 3)]}
33
-
34
-
35
28
  def _bash_hooks(event: str) -> list[dict[str, object]]:
36
29
  return [_command_hook("^Bash$", "codexy-destructive-command", event)]
37
30
 
@@ -72,29 +65,14 @@ _TITLE_MATCHERS = (
72
65
 
73
66
 
74
67
  def _core_hooks() -> dict[str, object]:
75
- """Build core event groups with a separate completed-send cancellation hook."""
68
+ """Build the retained core routing and ownership hook event groups."""
76
69
  command = lambda event: [
77
70
  _command_hook(matcher, stem, event) for matcher, stem in _CORE_COMMAND_HOOKS
78
71
  ]
79
- lifecycle = lambda event: [
80
- _command_hook(matcher, stem, event) for matcher, stem in _CORE_LIFECYCLE_HOOKS
81
- ]
82
- post_tool = [
83
- _command_hook(matcher, stem, "PostToolUse", 3)
84
- for matcher, stem in _CORE_POST_TOOL_HOOKS
85
- ]
86
72
  return {
87
73
  "hooks": {
88
74
  "PermissionRequest": command("PermissionRequest"),
89
- "PreToolUse": command("PreToolUse") + lifecycle("PreToolUse"),
90
- "PostToolUse": post_tool,
91
- "Interrupt": [
92
- _lifecycle_hook(stem, "Interrupt") for _, stem in _CORE_LIFECYCLE_HOOKS
93
- ],
94
- "UserPromptSubmit": [
95
- _lifecycle_hook(stem, "UserPromptSubmit")
96
- for _, stem in _CORE_LIFECYCLE_HOOKS
97
- ],
75
+ "PreToolUse": command("PreToolUse"),
98
76
  }
99
77
  }
100
78