pactkit-opencode 2.27.0__tar.gz → 3.0.1__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 (24) hide show
  1. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/PKG-INFO +2 -2
  2. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/pyproject.toml +2 -2
  3. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/src/pactkit_opencode/deployer.py +42 -2
  4. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/src/pactkit_opencode.egg-info/PKG-INFO +2 -2
  5. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/src/pactkit_opencode.egg-info/SOURCES.txt +1 -0
  6. pactkit_opencode-3.0.1/src/pactkit_opencode.egg-info/requires.txt +1 -0
  7. pactkit_opencode-3.0.1/tests/test_gate_plugin_deploy.py +69 -0
  8. pactkit_opencode-2.27.0/src/pactkit_opencode.egg-info/requires.txt +0 -1
  9. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/README.md +0 -0
  10. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/setup.cfg +0 -0
  11. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/src/pactkit_opencode/__init__.py +0 -0
  12. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/src/pactkit_opencode.egg-info/dependency_links.txt +0 -0
  13. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/src/pactkit_opencode.egg-info/entry_points.txt +0 -0
  14. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/src/pactkit_opencode.egg-info/top_level.txt +0 -0
  15. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/tests/test_a1_config_recovery.py +0 -0
  16. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/tests/test_a2_mcp_retired.py +0 -0
  17. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/tests/test_bug035_opencode_dual_layer.py +0 -0
  18. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/tests/test_deploy_opencode_parity.py +0 -0
  19. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/tests/test_host_body_snapshot.py +0 -0
  20. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/tests/test_story069_opencode_format.py +0 -0
  21. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/tests/test_story070_opencode_compliance.py +0 -0
  22. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/tests/test_story071_opencode_config_parity.py +0 -0
  23. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/tests/test_story073_command_model_routing.py +0 -0
  24. {pactkit_opencode-2.27.0 → pactkit_opencode-3.0.1}/tests/test_story_work_units.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pactkit-opencode
3
- Version: 2.27.0
3
+ Version: 3.0.1
4
4
  Summary: PactKit adapter for OpenCode IDE
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
7
- Requires-Dist: pactkit<2.28.0,>=2.27.0
7
+ Requires-Dist: pactkit<3.1.0,>=3.0.1
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pactkit-opencode"
7
- version = "2.27.0"
7
+ version = "3.0.1"
8
8
  description = "PactKit adapter for OpenCode IDE"
9
9
  requires-python = ">=3.10"
10
10
  license = "MIT"
11
11
  dependencies = [
12
- "pactkit>=2.27.0,<2.28.0",
12
+ "pactkit>=3.0.1,<3.1.0",
13
13
  ]
14
14
 
15
15
  [project.entry-points."pactkit.deployers"]
@@ -226,6 +226,7 @@ class OpenCodeDeployer(DeployerBase):
226
226
 
227
227
  n_agents = _deploy_agents(agents_dir, enabled_agents, profile=self.profile)
228
228
 
229
+
229
230
  ci_config = cfg.get("ci", {})
230
231
  ci_provider = ci_config.get("provider", "none") if isinstance(ci_config, dict) else "none"
231
232
  project_root = Path.cwd()
@@ -236,9 +237,20 @@ class OpenCodeDeployer(DeployerBase):
236
237
 
237
238
  write_deploy_manifest(opencode_root, "opencode", cfg)
238
239
 
240
+ # STORY-slim-20260918dda35a04d01d: the pre-tool gate channel. Deployed
241
+ # GLOBALLY (like every other OpenCode asset here), so the plugin sees
242
+ # every project on the machine — it activates per project via the
243
+ # markers baked into its source, and is inert elsewhere.
244
+ #
245
+ # MUST run AFTER write_deploy_manifest: that function REBUILDS the
246
+ # manifest's `files` map from the declared components, and `plugins/`
247
+ # is not one — recording earlier gets silently overwritten, and then
248
+ # `pactkit uninstall` leaves the plugin behind as residue (R4).
249
+ n_plugins = self._deploy_gate_plugin(opencode_root)
250
+
239
251
  print(
240
252
  f"\n✅ OpenCode: {n_agents} Agents, {n_skills} Skills, "
241
- f"{n_commands} Commands → {opencode_root}"
253
+ f"{n_commands} Commands, {n_plugins} Gate plugin → {opencode_root}"
242
254
  )
243
255
 
244
256
  if target is None:
@@ -246,6 +258,27 @@ class OpenCodeDeployer(DeployerBase):
246
258
 
247
259
  # --- OpenCode-specific helpers ---
248
260
 
261
+ @staticmethod
262
+ def _deploy_gate_plugin(opencode_root: Path) -> int:
263
+ """Write the pre-tool gate plugin. Returns 1 when written, else 0.
264
+
265
+ The source comes from core's `opencode_plugin.render_plugin_source()` —
266
+ **never hand-copied**. A local copy of the field mapping would be a
267
+ second mapping, and a wrong mapping is a bypass (STORY R2).
268
+
269
+ Recorded in the deployment manifest, so `pactkit uninstall` removes it
270
+ with the rest of the owned files — no residue (R4).
271
+ """
272
+ from pactkit.deploy_manifest import atomic_write, record_deployed_file
273
+ from pactkit.generators.opencode_plugin import render_plugin_source
274
+
275
+ plugins_dir = opencode_root / "plugins"
276
+ plugins_dir.mkdir(parents=True, exist_ok=True)
277
+ plugin_path = plugins_dir / "pactkit-gate.js"
278
+ atomic_write(plugin_path, render_plugin_source())
279
+ record_deployed_file(opencode_root, plugin_path.relative_to(opencode_root).as_posix())
280
+ return 1
281
+
249
282
  @staticmethod
250
283
  def _generate_project_agents_md():
251
284
  """Generate project-level AGENTS.md if it does not exist (STORY-slim-008 R4)."""
@@ -256,7 +289,14 @@ class OpenCodeDeployer(DeployerBase):
256
289
  if agents_md_path.exists():
257
290
  return
258
291
  project_name = project_root.name
259
- content = f"# {project_name}\n\n@./docs/product/context.md\noutput MUST use Chinese\n"
292
+ # Never `@import` a file nothing generates (STORY-slim-148 R1): project
293
+ # agent instructions must point at the local context entry instead.
294
+ content = (
295
+ f"# {project_name}\n\n"
296
+ "Read `.pactkit/context.md` at session start for project state — "
297
+ "optional history, never a gate.\n"
298
+ "output MUST use Chinese\n"
299
+ )
260
300
  atomic_write(agents_md_path, content)
261
301
 
262
302
  @staticmethod
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pactkit-opencode
3
- Version: 2.27.0
3
+ Version: 3.0.1
4
4
  Summary: PactKit adapter for OpenCode IDE
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
7
- Requires-Dist: pactkit<2.28.0,>=2.27.0
7
+ Requires-Dist: pactkit<3.1.0,>=3.0.1
@@ -12,6 +12,7 @@ tests/test_a1_config_recovery.py
12
12
  tests/test_a2_mcp_retired.py
13
13
  tests/test_bug035_opencode_dual_layer.py
14
14
  tests/test_deploy_opencode_parity.py
15
+ tests/test_gate_plugin_deploy.py
15
16
  tests/test_host_body_snapshot.py
16
17
  tests/test_story069_opencode_format.py
17
18
  tests/test_story070_opencode_compliance.py
@@ -0,0 +1 @@
1
+ pactkit<3.1.0,>=3.0.1
@@ -0,0 +1,69 @@
1
+ """STORY-slim-20260918dda35a04d01d — pre-tool 门禁插件的部署与移除.
2
+
3
+ OpenCode 的插件面 `tool.execute.before` 抛异常即可否决调用(实测)。插件部署在
4
+ **全局** `~/.config/opencode/plugins/`,因此会经过这台机器上的每个 opencode
5
+ 项目——它靠源码里烘焙的**激活标记**按项目生效,在别处完全不动。
6
+
7
+ 本文件钉三件事(R4:可核验、可移除):
8
+
9
+ 1. 部署确实写出插件,且**与 core 的生成器逐字节一致**——adapter 侧手抄一份
10
+ 映射就是第二套映射,映射错了就是绕过。
11
+ 2. manifest 记录了它——否则 `pactkit uninstall` 会把它留成残骸。
12
+ 3. 移除确实删掉它。
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+
18
+ import pytest
19
+
20
+ from pactkit_opencode.deployer import OpenCodeDeployer
21
+
22
+
23
+ @pytest.fixture()
24
+ def deployed(tmp_path):
25
+ target = tmp_path / "opencode"
26
+ OpenCodeDeployer().deploy(config={}, target=str(target))
27
+ return target
28
+
29
+
30
+ class TestPluginIsDeployed:
31
+ def test_plugin_file_is_written(self, deployed):
32
+ assert (deployed / "plugins" / "pactkit-gate.js").is_file()
33
+
34
+ def test_plugin_matches_core_generator_byte_for_byte(self, deployed):
35
+ """adapter 侧**不得**有第二份映射——内容必须来自 core 的生成器。"""
36
+ from pactkit.generators.opencode_plugin import render_plugin_source
37
+
38
+ written = (deployed / "plugins" / "pactkit-gate.js").read_text(encoding="utf-8")
39
+ assert written == render_plugin_source(), (
40
+ "部署的插件与 core 生成器不一致——adapter 侧出现了第二份映射"
41
+ )
42
+
43
+ def test_plugin_carries_the_activation_markers(self, deployed):
44
+ """全局部署的关键:插件必须能按项目判断自己该不该生效。"""
45
+ from pactkit.profiles import PACTKIT_YAML_CANDIDATES
46
+
47
+ src = (deployed / "plugins" / "pactkit-gate.js").read_text(encoding="utf-8")
48
+ for marker in PACTKIT_YAML_CANDIDATES:
49
+ assert marker in src, f"激活标记 {marker!r} 缺失——该生效的项目不会生效"
50
+
51
+
52
+ class TestPluginIsRemovable:
53
+ def test_manifest_records_the_plugin(self, deployed):
54
+ """**必须在 write_deploy_manifest 之后记录**:那个函数会用声明的组件
55
+ 重建 `files`,`plugins/` 不是组件——记录早了会被静默覆盖,uninstall
56
+ 就会把它留下。"""
57
+ manifest = json.loads((deployed / ".pactkit-deployed.json").read_text(encoding="utf-8"))
58
+ assert "plugins/pactkit-gate.js" in manifest["files"], (
59
+ "manifest 未记录插件——pactkit uninstall 会把它留成残骸"
60
+ )
61
+
62
+ def test_uninstall_removes_the_plugin(self, deployed):
63
+ from pactkit.uninstall import run_uninstall
64
+
65
+ run_uninstall(deployed)
66
+
67
+ assert not (deployed / "plugins" / "pactkit-gate.js").exists(), (
68
+ "卸载后插件仍在——R4 要求可移除、不留残骸"
69
+ )
@@ -1 +0,0 @@
1
- pactkit<2.28.0,>=2.27.0