pactkit-opencode 2.7.0__tar.gz → 2.9.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.
- {pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/PKG-INFO +2 -2
- pactkit_opencode-2.9.0/README.md +99 -0
- {pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/pyproject.toml +2 -2
- {pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/src/pactkit_opencode/deployer.py +114 -17
- {pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/src/pactkit_opencode.egg-info/PKG-INFO +2 -2
- {pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/src/pactkit_opencode.egg-info/SOURCES.txt +1 -0
- pactkit_opencode-2.9.0/src/pactkit_opencode.egg-info/requires.txt +1 -0
- {pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/tests/test_bug035_opencode_dual_layer.py +1 -1
- {pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/tests/test_story069_opencode_format.py +14 -11
- {pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/tests/test_story070_opencode_compliance.py +7 -3
- {pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/tests/test_story073_command_model_routing.py +16 -5
- pactkit_opencode-2.7.0/src/pactkit_opencode.egg-info/requires.txt +0 -1
- {pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/setup.cfg +0 -0
- {pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/src/pactkit_opencode/__init__.py +0 -0
- {pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/src/pactkit_opencode.egg-info/dependency_links.txt +0 -0
- {pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/src/pactkit_opencode.egg-info/entry_points.txt +0 -0
- {pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/src/pactkit_opencode.egg-info/top_level.txt +0 -0
- {pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/tests/test_deploy_opencode_parity.py +0 -0
- {pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/tests/test_story071_opencode_config_parity.py +0 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# pactkit-opencode
|
|
2
|
+
|
|
3
|
+
> PactKit PDCA workflow framework adapted for [OpenCode](https://opencode.ai).
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/pactkit-opencode/)
|
|
6
|
+
[](https://www.python.org/downloads/)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
## What is this?
|
|
10
|
+
|
|
11
|
+
**pactkit-opencode** brings the [PactKit](https://pactkit.dev) spec-driven development workflow to OpenCode IDE. It deploys:
|
|
12
|
+
|
|
13
|
+
- **Commands** (`commands/*.md`) — 11 PDCA workflow commands, auto-discovered by OpenCode (invoked via `/project-plan`, `/project-act`, etc.)
|
|
14
|
+
- **Skills** (`skills/*/SKILL.md`) — 10 embedded skills loaded by AI agent on demand
|
|
15
|
+
- **Agents** (`agents/*.md`) — 9 specialized agent definitions
|
|
16
|
+
- **Rules** (`rules/*.md`) — Governance rules loaded per-command
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install pactkit
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
> `pactkit-opencode` is automatically installed as a dependency of `pactkit`.
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Deploy PactKit to OpenCode (~/.config/opencode/)
|
|
30
|
+
pactkit init --format opencode
|
|
31
|
+
|
|
32
|
+
# Update when pactkit is upgraded
|
|
33
|
+
pactkit upgrade --format opencode
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then in OpenCode, use commands like:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
/project-plan "Add user authentication"
|
|
40
|
+
/project-act STORY-001
|
|
41
|
+
/project-done
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## What Gets Deployed
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
~/.config/opencode/
|
|
48
|
+
├── AGENTS.md # Global constitution
|
|
49
|
+
├── rules/ # Governance rule modules
|
|
50
|
+
├── commands/ # 11 PDCA command playbooks (auto-discovered by OpenCode)
|
|
51
|
+
├── agents/ # 9 agent definitions (mode: subagent)
|
|
52
|
+
├── skills/ # 10 skill packages (AI agent loads on demand)
|
|
53
|
+
└── opencode.json # Global config (model routing, instructions)
|
|
54
|
+
|
|
55
|
+
./ # Project root
|
|
56
|
+
├── .opencode/
|
|
57
|
+
│ ├── pactkit.yaml # Project config
|
|
58
|
+
│ └── AGENTS.local.md # Your custom instructions (never overwritten)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## OpenCode Architecture
|
|
62
|
+
|
|
63
|
+
OpenCode has two separate mechanisms for extending the AI assistant:
|
|
64
|
+
|
|
65
|
+
| Mechanism | Location | Invocation | Purpose |
|
|
66
|
+
|-----------|----------|------------|---------|
|
|
67
|
+
| **Commands** | `commands/*.md` | User types `/command-name` | PDCA workflow entry points |
|
|
68
|
+
| **Skills** | `skills/*/SKILL.md` | AI agent loads via `skill()` | Embedded tools (visualize, board, scaffold) |
|
|
69
|
+
|
|
70
|
+
Commands are auto-discovered from `commands/*.md` — no `opencode.json` configuration needed.
|
|
71
|
+
|
|
72
|
+
## Key Differences from Claude Code
|
|
73
|
+
|
|
74
|
+
| Feature | Claude Code | OpenCode |
|
|
75
|
+
|---------|-------------|----------|
|
|
76
|
+
| Commands | Skills-only (`skills/project-*/SKILL.md`) | Dual: `commands/` + `skills/` |
|
|
77
|
+
| Config dir | `~/.claude/` | `~/.config/opencode/` |
|
|
78
|
+
| Agent format | Native multi-agent | `mode: subagent` |
|
|
79
|
+
| Model routing | Via skill frontmatter | Via `opencode.json` command entries |
|
|
80
|
+
| Rule loading | `@import` directives | Inline embedding per command |
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
git clone https://github.com/pactkit/pactkit-opencode.git
|
|
86
|
+
cd pactkit-opencode
|
|
87
|
+
pip install -e .
|
|
88
|
+
pytest tests/ -v
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
94
|
+
|
|
95
|
+
## Related Projects
|
|
96
|
+
|
|
97
|
+
- [PactKit](https://pactkit.dev) — Core framework
|
|
98
|
+
- [pactkit-codex](https://github.com/pactkit/pactkit-codex) — Adapter for OpenAI Codex CLI
|
|
99
|
+
- [OpenCode](https://opencode.ai) — AI-powered coding IDE
|
|
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pactkit-opencode"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.9.0"
|
|
8
8
|
description = "PactKit adapter for OpenCode IDE"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"pactkit>=2.
|
|
12
|
+
"pactkit>=2.9.0",
|
|
13
13
|
]
|
|
14
14
|
|
|
15
15
|
[project.entry-points."pactkit.deployers"]
|
|
@@ -4,6 +4,7 @@ Extracted from pactkit core (STORY-slim-058).
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
|
+
import re
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
from pactkit import __version__, prompts
|
|
@@ -25,8 +26,12 @@ from pactkit.generators.deployer import (
|
|
|
25
26
|
_deploy_skills,
|
|
26
27
|
)
|
|
27
28
|
from pactkit.profiles import get_profile
|
|
29
|
+
from pactkit.prompts.rules import CREDENTIAL_SAFETY_FILE
|
|
28
30
|
from pactkit.utils import atomic_write
|
|
29
31
|
|
|
32
|
+
# Commands excluded from OpenCode deployment (require multi-agent capabilities)
|
|
33
|
+
OPENCODE_EXCLUDED_COMMANDS = {"project-sprint"}
|
|
34
|
+
|
|
30
35
|
|
|
31
36
|
class OpenCodeDeployer(DeployerBase):
|
|
32
37
|
"""OpenCode deployment — generate OpenCode-native configuration (STORY-069).
|
|
@@ -36,7 +41,9 @@ class OpenCodeDeployer(DeployerBase):
|
|
|
36
41
|
- AGENTS.md (slim header — rules loaded via instructions)
|
|
37
42
|
- rules/ directory with modular rule files (STORY-071 R6)
|
|
38
43
|
- opencode.json (global config with instructions: ["rules/*.md"])
|
|
39
|
-
- agents
|
|
44
|
+
- agents/ directory
|
|
45
|
+
- commands/ directory (PDCA commands, invoked via /command-name)
|
|
46
|
+
- skills/ directory (embedded skills like pactkit-board, pactkit-visualize)
|
|
40
47
|
|
|
41
48
|
STORY-slim-008: Aligned with _deploy_classic() feature set:
|
|
42
49
|
- Reads pactkit.yaml for selective deployment (R1)
|
|
@@ -55,11 +62,11 @@ class OpenCodeDeployer(DeployerBase):
|
|
|
55
62
|
|
|
56
63
|
# Prepare directories
|
|
57
64
|
agents_dir = opencode_root / "agents"
|
|
58
|
-
commands_dir = opencode_root / "commands"
|
|
59
65
|
skills_dir = opencode_root / "skills"
|
|
66
|
+
commands_dir = opencode_root / "commands"
|
|
60
67
|
rules_dir = opencode_root / "rules"
|
|
61
68
|
|
|
62
|
-
for d in [opencode_root, agents_dir,
|
|
69
|
+
for d in [opencode_root, agents_dir, skills_dir, commands_dir, rules_dir]:
|
|
63
70
|
d.mkdir(parents=True, exist_ok=True)
|
|
64
71
|
|
|
65
72
|
# STORY-slim-008 R1+R2: Load config from project-level pactkit.yaml
|
|
@@ -76,13 +83,35 @@ class OpenCodeDeployer(DeployerBase):
|
|
|
76
83
|
cfg = {}
|
|
77
84
|
|
|
78
85
|
enabled_agents = cfg.get("agents", sorted(VALID_AGENTS))
|
|
79
|
-
enabled_commands = cfg.get("commands", sorted(VALID_COMMANDS))
|
|
80
86
|
enabled_skills = cfg.get("skills", sorted(VALID_SKILLS))
|
|
87
|
+
enabled_commands = cfg.get("commands", sorted(VALID_COMMANDS))
|
|
81
88
|
enabled_rules = cfg.get("rules", sorted(VALID_RULES))
|
|
82
89
|
|
|
83
|
-
#
|
|
90
|
+
# Filter out excluded commands (project-sprint requires multi-agent)
|
|
91
|
+
enabled_commands = [c for c in enabled_commands if c not in OPENCODE_EXCLUDED_COMMANDS]
|
|
92
|
+
|
|
93
|
+
# Deploy embedded skills (pactkit-board, pactkit-visualize, etc.)
|
|
84
94
|
n_skills = _deploy_skills(skills_dir, enabled_skills, profile=self.profile)
|
|
85
95
|
_cleanup_legacy(skills_dir)
|
|
96
|
+
# Clean up legacy command skills from skills/ directory (migrated to commands/)
|
|
97
|
+
self._cleanup_command_skills(skills_dir)
|
|
98
|
+
|
|
99
|
+
# Apply CLI-to-script replacement to deployed embedded skill SKILL.md files
|
|
100
|
+
for skill_md in skills_dir.glob("*/SKILL.md"):
|
|
101
|
+
content = skill_md.read_text(encoding="utf-8")
|
|
102
|
+
replaced = _replace_cli_with_scripts(content)
|
|
103
|
+
if replaced != content:
|
|
104
|
+
atomic_write(skill_md, replaced)
|
|
105
|
+
|
|
106
|
+
# Deploy commands to commands/ directory (OpenCode uses /command-name to invoke)
|
|
107
|
+
n_commands = _deploy_commands(commands_dir, enabled_commands, profile=self.profile, config=cfg)
|
|
108
|
+
|
|
109
|
+
# Apply CLI-to-script replacement to deployed command files
|
|
110
|
+
for cmd_md in commands_dir.glob("*.md"):
|
|
111
|
+
content = cmd_md.read_text(encoding="utf-8")
|
|
112
|
+
replaced = _replace_cli_with_scripts(content)
|
|
113
|
+
if replaced != content:
|
|
114
|
+
atomic_write(cmd_md, replaced)
|
|
86
115
|
|
|
87
116
|
n_rules = _deploy_rules(opencode_root, enabled_rules)
|
|
88
117
|
self._deploy_agents_md_inline(opencode_root)
|
|
@@ -94,13 +123,13 @@ class OpenCodeDeployer(DeployerBase):
|
|
|
94
123
|
)
|
|
95
124
|
command_models = cfg.get("command_models", {})
|
|
96
125
|
self._update_global_opencode_json(
|
|
97
|
-
opencode_root,
|
|
126
|
+
opencode_root,
|
|
127
|
+
command_models=command_models,
|
|
128
|
+
providers=providers,
|
|
129
|
+
enabled_commands=enabled_commands,
|
|
98
130
|
)
|
|
99
131
|
|
|
100
132
|
n_agents = _deploy_agents(agents_dir, enabled_agents, profile=self.profile)
|
|
101
|
-
n_commands = _deploy_commands(
|
|
102
|
-
commands_dir, enabled_commands, profile=self.profile, config=cfg
|
|
103
|
-
)
|
|
104
133
|
|
|
105
134
|
ci_config = cfg.get("ci", {})
|
|
106
135
|
ci_provider = ci_config.get("provider", "none") if isinstance(ci_config, dict) else "none"
|
|
@@ -108,8 +137,8 @@ class OpenCodeDeployer(DeployerBase):
|
|
|
108
137
|
_deploy_ci(ci_provider, project_root, cfg)
|
|
109
138
|
|
|
110
139
|
print(
|
|
111
|
-
f"\n✅ OpenCode: {n_agents} Agents, {
|
|
112
|
-
f"{
|
|
140
|
+
f"\n✅ OpenCode: {n_agents} Agents, {n_skills} Skills, "
|
|
141
|
+
f"{n_commands} Commands → {opencode_root}"
|
|
113
142
|
)
|
|
114
143
|
|
|
115
144
|
if target is None:
|
|
@@ -242,8 +271,10 @@ class OpenCodeDeployer(DeployerBase):
|
|
|
242
271
|
return {}
|
|
243
272
|
|
|
244
273
|
@staticmethod
|
|
245
|
-
def _update_global_opencode_json(
|
|
246
|
-
|
|
274
|
+
def _update_global_opencode_json(
|
|
275
|
+
opencode_root, command_models=None, providers=None, enabled_commands=None
|
|
276
|
+
):
|
|
277
|
+
"""Update global opencode.json with instructions, command templates, and model routing."""
|
|
247
278
|
json_path = opencode_root / "opencode.json"
|
|
248
279
|
config = {}
|
|
249
280
|
|
|
@@ -255,7 +286,7 @@ class OpenCodeDeployer(DeployerBase):
|
|
|
255
286
|
|
|
256
287
|
config.setdefault("$schema", "https://opencode.ai/config.json")
|
|
257
288
|
|
|
258
|
-
credential_path = f"rules/{
|
|
289
|
+
credential_path = f"rules/{CREDENTIAL_SAFETY_FILE}"
|
|
259
290
|
existing = [
|
|
260
291
|
i
|
|
261
292
|
for i in config.get("instructions", [])
|
|
@@ -265,16 +296,34 @@ class OpenCodeDeployer(DeployerBase):
|
|
|
265
296
|
existing.append(credential_path)
|
|
266
297
|
config["instructions"] = existing
|
|
267
298
|
|
|
299
|
+
# OpenCode auto-discovers commands from commands/*.md directory.
|
|
300
|
+
# opencode.json "command" entries are only needed for model routing overrides.
|
|
301
|
+
# NOTE: "template" in JSON is inline prompt text, NOT a file path — do not write it.
|
|
302
|
+
cmd_config = config.get("command", {})
|
|
303
|
+
if enabled_commands:
|
|
304
|
+
# Remove stale command entries not in enabled set
|
|
305
|
+
stale_keys = [k for k in cmd_config if k.startswith("project-") and k not in enabled_commands]
|
|
306
|
+
for k in stale_keys:
|
|
307
|
+
del cmd_config[k]
|
|
308
|
+
# Remove legacy "template" fields (was incorrectly treated as file path)
|
|
309
|
+
for cmd_name in list(cmd_config):
|
|
310
|
+
if cmd_name.startswith("project-") and "template" in cmd_config[cmd_name]:
|
|
311
|
+
del cmd_config[cmd_name]["template"]
|
|
312
|
+
# Remove entry entirely if only template was present
|
|
313
|
+
if not cmd_config[cmd_name]:
|
|
314
|
+
del cmd_config[cmd_name]
|
|
315
|
+
|
|
316
|
+
# Apply model routing
|
|
268
317
|
if command_models and providers:
|
|
269
|
-
cmd_config = config.get("command", {})
|
|
270
318
|
for cmd_name, model_short in command_models.items():
|
|
271
319
|
model_id = OpenCodeDeployer._resolve_opencode_model_id(model_short, providers)
|
|
272
320
|
if model_id:
|
|
273
321
|
if cmd_name not in cmd_config:
|
|
274
322
|
cmd_config[cmd_name] = {}
|
|
275
323
|
cmd_config[cmd_name]["model"] = model_id
|
|
276
|
-
|
|
277
|
-
|
|
324
|
+
|
|
325
|
+
if cmd_config:
|
|
326
|
+
config["command"] = cmd_config
|
|
278
327
|
|
|
279
328
|
content = json.dumps(config, indent=2, ensure_ascii=False) + "\n"
|
|
280
329
|
atomic_write(json_path, content)
|
|
@@ -301,6 +350,54 @@ class OpenCodeDeployer(DeployerBase):
|
|
|
301
350
|
'["npx", "-y", "@anthropic/mcp-puppeteer"] }'
|
|
302
351
|
)
|
|
303
352
|
|
|
353
|
+
@staticmethod
|
|
354
|
+
def _cleanup_command_skills(skills_dir):
|
|
355
|
+
"""Remove legacy command skills from skills/ directory.
|
|
356
|
+
|
|
357
|
+
Previously, PDCA commands were deployed as skills/{name}/SKILL.md.
|
|
358
|
+
Now they are in commands/{name}.md. Clean up the old entries.
|
|
359
|
+
"""
|
|
360
|
+
for d in skills_dir.iterdir():
|
|
361
|
+
if d.is_dir() and d.name.startswith("project-"):
|
|
362
|
+
import shutil
|
|
363
|
+
shutil.rmtree(d)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# --- Module-level helper functions ---
|
|
367
|
+
|
|
368
|
+
_VIZ_SCRIPT = "python3 ~/.config/opencode/skills/pactkit-visualize/scripts/visualize.py"
|
|
369
|
+
_BOARD_SCRIPT = "python3 ~/.config/opencode/skills/pactkit-board/scripts/board.py"
|
|
370
|
+
_SCAFFOLD_SCRIPT = "python3 ~/.config/opencode/skills/pactkit-scaffold/scripts/scaffold.py"
|
|
371
|
+
|
|
372
|
+
# CLI subcommands that map to skill scripts (order matters: longer patterns first)
|
|
373
|
+
_CLI_TO_SCRIPT = [
|
|
374
|
+
# visualize variants
|
|
375
|
+
("Run `pactkit visualize --lazy`", f"Run `{_VIZ_SCRIPT}` (file, `--mode class`, `--mode call` if source changed)"),
|
|
376
|
+
("Run `pactkit visualize", f"Run `{_VIZ_SCRIPT}"),
|
|
377
|
+
("Run `visualize --focus", f"Run `{_VIZ_SCRIPT} --focus"),
|
|
378
|
+
("Run `visualize --mode", f"Run `{_VIZ_SCRIPT} --mode"),
|
|
379
|
+
("Run `visualize`", f"Run `{_VIZ_SCRIPT}`"),
|
|
380
|
+
# board
|
|
381
|
+
("Run `python3 ~/.config/opencode/skills/pactkit-board/scripts/board.py", f"Run `{_BOARD_SCRIPT}"),
|
|
382
|
+
# scaffold
|
|
383
|
+
("Run `python3 ~/.config/opencode/skills/pactkit-scaffold/scripts/scaffold.py", f"Run `{_SCAFFOLD_SCRIPT}"),
|
|
384
|
+
# pactkit CLI → manual fallback hint
|
|
385
|
+
("Run `pactkit clean`", "Run language-specific cleanup (e.g., `find . -name '__pycache__' -exec rm -rf {} +` for Python)"),
|
|
386
|
+
]
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _replace_cli_with_scripts(content):
|
|
390
|
+
"""Replace pactkit CLI and bare visualize commands with direct OpenCode script paths."""
|
|
391
|
+
for old, new in _CLI_TO_SCRIPT:
|
|
392
|
+
content = content.replace(old, new)
|
|
393
|
+
# Handle backtick-wrapped bare `visualize` with flags
|
|
394
|
+
content = re.sub(
|
|
395
|
+
r'`visualize (--(?:focus|mode|entry))',
|
|
396
|
+
rf'`{_VIZ_SCRIPT} \1',
|
|
397
|
+
content,
|
|
398
|
+
)
|
|
399
|
+
return content
|
|
400
|
+
|
|
304
401
|
|
|
305
402
|
# Auto-register when this module is imported
|
|
306
403
|
register_deployer("opencode", OpenCodeDeployer, force=True)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pactkit>=2.9.0
|
|
@@ -39,7 +39,7 @@ class TestR1NoGlobalOpencodeJson:
|
|
|
39
39
|
assert (out / "agents").is_dir()
|
|
40
40
|
|
|
41
41
|
def test_global_deploy_has_commands_dir(self, tmp_path):
|
|
42
|
-
"""commands/ IS created
|
|
42
|
+
"""commands/ IS created — OpenCode uses /command-name to invoke."""
|
|
43
43
|
out = tmp_path / "opencode-global"
|
|
44
44
|
deploy(format="opencode", target=str(out))
|
|
45
45
|
assert (out / "commands").is_dir()
|
|
@@ -10,8 +10,11 @@ from pactkit.config import VALID_AGENTS, VALID_COMMANDS, VALID_FORMATS, VALID_SK
|
|
|
10
10
|
from pactkit.generators.deployer import deploy
|
|
11
11
|
from pactkit_opencode.deployer import OpenCodeDeployer
|
|
12
12
|
|
|
13
|
-
#
|
|
14
|
-
#
|
|
13
|
+
# OpenCode deploys commands to commands/{name}.md (invoked via /command-name)
|
|
14
|
+
# and embedded skills to skills/{name}/SKILL.md (auto-discovered by AI agent).
|
|
15
|
+
# project-sprint is excluded from OpenCode (requires multi-agent).
|
|
16
|
+
OPENCODE_EXCLUDED_COMMANDS = frozenset({"project-sprint"})
|
|
17
|
+
_DEPLOYABLE_COMMANDS = VALID_COMMANDS - OPENCODE_EXCLUDED_COMMANDS
|
|
15
18
|
_EMBEDDED_SKILLS = VALID_SKILLS - VALID_COMMANDS
|
|
16
19
|
|
|
17
20
|
# ===========================================================================
|
|
@@ -53,8 +56,8 @@ class TestR2GlobalDeployment:
|
|
|
53
56
|
deploy(format="opencode", target=str(out))
|
|
54
57
|
assert (out / "agents").is_dir()
|
|
55
58
|
|
|
56
|
-
def
|
|
57
|
-
"""commands/ directory
|
|
59
|
+
def test_commands_dir_created(self, tmp_path):
|
|
60
|
+
"""commands/ directory IS created — OpenCode uses /command-name to invoke."""
|
|
58
61
|
out = tmp_path / "opencode"
|
|
59
62
|
deploy(format="opencode", target=str(out))
|
|
60
63
|
assert (out / "commands").is_dir()
|
|
@@ -73,22 +76,22 @@ class TestR2GlobalDeployment:
|
|
|
73
76
|
deployed = {f.stem for f in agents_dir.glob("*.md")}
|
|
74
77
|
assert deployed == VALID_AGENTS
|
|
75
78
|
|
|
76
|
-
def
|
|
77
|
-
"""All commands are deployed."""
|
|
79
|
+
def test_all_commands_deployed_to_commands_dir(self, tmp_path):
|
|
80
|
+
"""All commands (except project-sprint) are deployed as commands/{name}.md."""
|
|
78
81
|
out = tmp_path / "opencode"
|
|
79
82
|
deploy(format="opencode", target=str(out))
|
|
80
83
|
commands_dir = out / "commands"
|
|
81
84
|
deployed = {f.stem for f in commands_dir.glob("*.md")}
|
|
82
|
-
|
|
85
|
+
for cmd in _DEPLOYABLE_COMMANDS:
|
|
86
|
+
assert cmd in deployed, f"{cmd}.md not found in commands/"
|
|
83
87
|
|
|
84
|
-
def
|
|
85
|
-
"""All embedded skills are
|
|
86
|
-
to commands/{name}.md in opencode format, not skills/.)"""
|
|
88
|
+
def test_all_embedded_skills_deployed(self, tmp_path):
|
|
89
|
+
"""All embedded skills are in skills/."""
|
|
87
90
|
out = tmp_path / "opencode"
|
|
88
91
|
deploy(format="opencode", target=str(out))
|
|
89
92
|
skills_dir = out / "skills"
|
|
90
93
|
deployed = {d.name for d in skills_dir.iterdir() if d.is_dir()}
|
|
91
|
-
assert deployed
|
|
94
|
+
assert _EMBEDDED_SKILLS.issubset(deployed)
|
|
92
95
|
|
|
93
96
|
|
|
94
97
|
# ===========================================================================
|
{pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/tests/test_story070_opencode_compliance.py
RENAMED
|
@@ -49,14 +49,18 @@ class TestAC1CommandFrontmatter:
|
|
|
49
49
|
|
|
50
50
|
def test_all_commands_converted(self, tmp_path):
|
|
51
51
|
"""All command files use OpenCode frontmatter."""
|
|
52
|
+
from pactkit.config import VALID_COMMANDS
|
|
53
|
+
EXCLUDED = frozenset({"project-sprint"})
|
|
52
54
|
out = tmp_path / "oc"
|
|
53
55
|
deploy(format="opencode", target=str(out))
|
|
54
56
|
commands_dir = out / "commands"
|
|
55
|
-
for
|
|
57
|
+
for cmd_name in VALID_COMMANDS - EXCLUDED:
|
|
58
|
+
cmd_file = commands_dir / f"{cmd_name}.md"
|
|
59
|
+
assert cmd_file.exists(), f"{cmd_name}.md missing in commands/"
|
|
56
60
|
content = cmd_file.read_text()
|
|
57
61
|
frontmatter = self._extract_frontmatter(content)
|
|
58
|
-
assert "allowed-tools" not in frontmatter, f"{
|
|
59
|
-
assert "agent: build" in frontmatter, f"{
|
|
62
|
+
assert "allowed-tools" not in frontmatter, f"{cmd_name} still has allowed-tools"
|
|
63
|
+
assert "agent: build" in frontmatter, f"{cmd_name} missing agent: build"
|
|
60
64
|
|
|
61
65
|
def test_command_body_preserved(self, tmp_path):
|
|
62
66
|
"""Command body content is preserved after frontmatter conversion."""
|
{pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/tests/test_story073_command_model_routing.py
RENAMED
|
@@ -65,8 +65,15 @@ class TestAC1CommandModel:
|
|
|
65
65
|
"""Command frontmatter should NOT contain model: (routing is in opencode.json)."""
|
|
66
66
|
out = self._deploy_with_providers(tmp_path)
|
|
67
67
|
content = (out / "commands" / "project-act.md").read_text()
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
import re
|
|
69
|
+
matches = list(re.finditer(r'---\n(.*?)\n---', content, re.DOTALL))
|
|
70
|
+
assert matches, "No frontmatter found"
|
|
71
|
+
for m in matches:
|
|
72
|
+
if "description:" in m.group(1):
|
|
73
|
+
frontmatter = m.group(1)
|
|
74
|
+
break
|
|
75
|
+
else:
|
|
76
|
+
frontmatter = matches[-1].group(1)
|
|
70
77
|
for line in frontmatter.strip().split("\n"):
|
|
71
78
|
assert not line.strip().startswith("model:"), f"Unexpected model in frontmatter: {line}"
|
|
72
79
|
|
|
@@ -77,10 +84,14 @@ class TestAC1CommandModel:
|
|
|
77
84
|
|
|
78
85
|
|
|
79
86
|
class TestAC2PlanNoModel:
|
|
80
|
-
"""AC2: Commands that inherit main model
|
|
87
|
+
"""AC2: Commands that inherit main model have no model override in opencode.json."""
|
|
81
88
|
|
|
82
89
|
def test_plan_not_in_command_routing(self, tmp_path):
|
|
83
|
-
"""project-plan
|
|
90
|
+
"""project-plan has no entry in opencode.json command section (no model override needed).
|
|
91
|
+
|
|
92
|
+
OpenCode auto-discovers commands from commands/*.md directory.
|
|
93
|
+
JSON command entries are only for model routing overrides.
|
|
94
|
+
"""
|
|
84
95
|
out = tmp_path / "oc"
|
|
85
96
|
out.mkdir(parents=True, exist_ok=True)
|
|
86
97
|
providers = {"$schema": "x", "provider": {"tp": {"models": {"claude-sonnet-4.6": {}}}}}
|
|
@@ -90,7 +101,7 @@ class TestAC2PlanNoModel:
|
|
|
90
101
|
assert "project-plan" not in data.get("command", {})
|
|
91
102
|
|
|
92
103
|
def test_clarify_not_in_command_routing(self, tmp_path):
|
|
93
|
-
"""project-clarify
|
|
104
|
+
"""project-clarify has no entry in opencode.json command section (no model override needed)."""
|
|
94
105
|
out = tmp_path / "oc"
|
|
95
106
|
out.mkdir(parents=True, exist_ok=True)
|
|
96
107
|
providers = {"$schema": "x", "provider": {"tp": {"models": {"claude-sonnet-4.6": {}}}}}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pactkit>=2.7.0
|
|
File without changes
|
|
File without changes
|
{pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/src/pactkit_opencode.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/src/pactkit_opencode.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/src/pactkit_opencode.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
{pactkit_opencode-2.7.0 → pactkit_opencode-2.9.0}/tests/test_story071_opencode_config_parity.py
RENAMED
|
File without changes
|