Compare commits
No commits in common. "main" and "fix/lowercase-org-slug" have entirely different histories.
main
...
fix/lowerc
@ -1,5 +0,0 @@
|
|||||||
name: CI
|
|
||||||
on: [push, pull_request]
|
|
||||||
jobs:
|
|
||||||
validate:
|
|
||||||
uses: molecule-ai/molecule-ci/.gitea/workflows/validate-plugin.yml@main
|
|
||||||
5
.github/workflows/ci.yml
vendored
Normal file
5
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
name: CI
|
||||||
|
on: [push, pull_request]
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
uses: molecule-ai/molecule-ci/.github/workflows/validate-plugin.yml@main
|
||||||
12
.gitignore
vendored
12
.gitignore
vendored
@ -19,15 +19,3 @@
|
|||||||
# Workspace auth tokens
|
# Workspace auth tokens
|
||||||
.auth-token
|
.auth-token
|
||||||
.auth_token
|
.auth_token
|
||||||
# Python bytecode (append only — do not remove entries above)
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
.Python
|
|
||||||
*.egg-info/
|
|
||||||
*.egg
|
|
||||||
.pytest_cache/
|
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
.eggs/
|
|
||||||
|
|||||||
49
README.md
49
README.md
@ -1,64 +1,19 @@
|
|||||||
# molecule-dev
|
# molecule-dev
|
||||||
|
|
||||||
Molecule AI codebase conventions, quality standards, past bugs, and coordination workflows. Agents operating in Molecule AI workspaces load these rules automatically.
|
Molecule AI plugin. Install via the Molecule AI platform plugin system.
|
||||||
|
|
||||||
## What it provides
|
## Usage
|
||||||
|
|
||||||
### Codebase conventions (`rules/codebase-conventions.md`)
|
|
||||||
|
|
||||||
Patterns, idioms, and anti-patterns specific to the Molecule AI codebase. Covers:
|
|
||||||
- Python style (type hints, docstrings, error handling)
|
|
||||||
- Go conventions (context usage, error wrapping)
|
|
||||||
- Directory structure and naming
|
|
||||||
- Deprecation policy
|
|
||||||
|
|
||||||
### Review loop skill (`review-loop`)
|
|
||||||
|
|
||||||
Guides the agent through a structured review before each commit:
|
|
||||||
1. Scope check — does the diff match the ticket?
|
|
||||||
2. Style check — conventions followed?
|
|
||||||
3. Test check — coverage sufficient?
|
|
||||||
4. Security check — secrets or injection risks?
|
|
||||||
5. Sign-off — agent declares the change ready
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
### In org template (org.yaml)
|
### In org template (org.yaml)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
plugins:
|
plugins:
|
||||||
- molecule-dev
|
- molecule-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### From URL (community install)
|
### From URL (community install)
|
||||||
|
|
||||||
```
|
```
|
||||||
github://Molecule-AI/molecule-ai-plugin-molecule-dev
|
github://Molecule-AI/molecule-ai-plugin-molecule-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Runtimes
|
|
||||||
|
|
||||||
- `claude_code` — primary
|
|
||||||
- `deepagents` — supported
|
|
||||||
- `hermes` — supported
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
rules/
|
|
||||||
codebase-conventions.md # Prose rules loaded into agent memory
|
|
||||||
adapters/
|
|
||||||
claude_code.py # Registers rules + review-loop skill
|
|
||||||
skills/
|
|
||||||
review-loop/ # SKILL.md + scripts/
|
|
||||||
runbooks/
|
|
||||||
local-dev-setup.md # Setup guide for contributors
|
|
||||||
```
|
|
||||||
|
|
||||||
## Known issues
|
|
||||||
|
|
||||||
See [known-issues.md](known-issues.md).
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Business Source License 1.1 — © Molecule AI.
|
Business Source License 1.1 — © Molecule AI.
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
# Test Coverage Rationale — molecule-dev
|
|
||||||
|
|
||||||
## Why This Plugin Has Limited Unit-Test Coverage
|
|
||||||
|
|
||||||
`molecule-dev` is a **skill+rules plugin** — it provides codebase conventions
|
|
||||||
(rules/codebase-conventions.md) and a review-loop coordination skill
|
|
||||||
(review-loop/SKILL.md) via prose documentation.
|
|
||||||
|
|
||||||
## What We Test (and Why)
|
|
||||||
|
|
||||||
| What | Why |
|
|
||||||
|------|-----|
|
|
||||||
| `plugin.yaml` schema | Verifies all 1 rule, 1 skill, 2 adapters registered |
|
|
||||||
| `rules/` directory + file | Declared rule file exists and is non-empty |
|
|
||||||
| `skills/review-loop` SKILL.md | Frontmatter parses, body has `#` heading |
|
|
||||||
| Adapters (2) | claude_code + deepagents adapters are wired |
|
|
||||||
| `known-issues.md` | Active Issues + Severity Definitions + Reporting sections present |
|
|
||||||
| `README.md` | H1 heading, Install section, Architecture section |
|
|
||||||
|
|
||||||
## What We Cannot Unit-Test Here
|
|
||||||
|
|
||||||
- **SKILL.md prose content** — review-loop guidance is prose; its quality is
|
|
||||||
a documentation review concern, not a unit-test concern.
|
|
||||||
|
|
||||||
- **Codebase conventions** — the rules in `rules/codebase-conventions.md` are
|
|
||||||
prose; their correctness is a team convention concern.
|
|
||||||
|
|
||||||
## Integration Tests
|
|
||||||
|
|
||||||
If you want to test that agents use the review-loop skill correctly:
|
|
||||||
1. Install `molecule-dev` on a test workspace
|
|
||||||
2. Ask the agent to coordinate a multi-stakeholder feature
|
|
||||||
3. Verify the agent follows the Round 1 → Round 2 → Round 3 structure
|
|
||||||
4. Verify QA findings are routed back and re-verified
|
|
||||||
@ -1,211 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Smoke tests for molecule-dev.
|
|
||||||
|
|
||||||
Rationale: This is a skill+rules plugin — it provides codebase conventions
|
|
||||||
(rules) and a review-loop coordination skill. Smoke tests verify all artifacts
|
|
||||||
exist and parse correctly. See tests/README.md.
|
|
||||||
|
|
||||||
Run: python tests/test_dev_smoke.py
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
|
|
||||||
|
|
||||||
def load_manifest():
|
|
||||||
import yaml
|
|
||||||
with open(os.path.join(REPO_ROOT, 'plugin.yaml')) as f:
|
|
||||||
return yaml.safe_load(f)
|
|
||||||
|
|
||||||
|
|
||||||
class TestPluginManifest(unittest.TestCase):
|
|
||||||
"""Verify plugin.yaml is well-formed."""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
cls.manifest = load_manifest()
|
|
||||||
|
|
||||||
def test_plugin_yaml_loads(self):
|
|
||||||
self.assertIsInstance(self.manifest, dict)
|
|
||||||
|
|
||||||
def test_name(self):
|
|
||||||
self.assertEqual(self.manifest['name'], 'molecule-dev')
|
|
||||||
|
|
||||||
def test_version_semver(self):
|
|
||||||
v = self.manifest['version']
|
|
||||||
self.assertRegex(v, r'^\d+\.\d+\.\d+$', f"Version {v!r} not semver")
|
|
||||||
|
|
||||||
def test_description_present(self):
|
|
||||||
self.assertGreater(len(self.manifest.get('description', '')), 10)
|
|
||||||
|
|
||||||
def test_runtimes_include_claude_code(self):
|
|
||||||
self.assertIn('claude_code', self.manifest.get('runtimes', []))
|
|
||||||
|
|
||||||
def test_rules_declared(self):
|
|
||||||
rules = self.manifest.get('rules', [])
|
|
||||||
self.assertIsInstance(rules, list)
|
|
||||||
self.assertGreater(len(rules), 0)
|
|
||||||
|
|
||||||
def test_skills_declared(self):
|
|
||||||
skills = self.manifest.get('skills', [])
|
|
||||||
self.assertIsInstance(skills, list)
|
|
||||||
self.assertGreater(len(skills), 0)
|
|
||||||
|
|
||||||
|
|
||||||
class TestRules(unittest.TestCase):
|
|
||||||
"""Verify declared rules files exist and are non-empty."""
|
|
||||||
|
|
||||||
RULES_DIR = os.path.join(REPO_ROOT, 'rules')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
cls.rules = load_manifest().get('rules', [])
|
|
||||||
|
|
||||||
def test_rules_directory_exists(self):
|
|
||||||
self.assertTrue(os.path.isdir(self.RULES_DIR))
|
|
||||||
|
|
||||||
def test_each_declared_rule_file_exists(self):
|
|
||||||
for rule in self.rules:
|
|
||||||
filename = os.path.basename(rule)
|
|
||||||
path = os.path.join(self.RULES_DIR, filename)
|
|
||||||
self.assertTrue(
|
|
||||||
os.path.isfile(path),
|
|
||||||
f"Rule {rule!r} declared but file not found at {path}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_each_rule_file_is_nonempty(self):
|
|
||||||
for rule in self.rules:
|
|
||||||
filename = os.path.basename(rule)
|
|
||||||
path = os.path.join(self.RULES_DIR, filename)
|
|
||||||
size = os.path.getsize(path)
|
|
||||||
self.assertGreater(size, 100, f"Rule {rule!r} is suspiciously small ({size} bytes)")
|
|
||||||
|
|
||||||
|
|
||||||
class TestSkills(unittest.TestCase):
|
|
||||||
"""Verify declared skills have SKILL.md with valid frontmatter."""
|
|
||||||
|
|
||||||
SKILLS_DIR = os.path.join(REPO_ROOT, 'skills')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
cls.skills = load_manifest().get('skills', [])
|
|
||||||
|
|
||||||
def test_skills_directory_exists(self):
|
|
||||||
self.assertTrue(os.path.isdir(self.SKILLS_DIR))
|
|
||||||
|
|
||||||
def test_each_declared_skill_directory_exists(self):
|
|
||||||
for skill in self.skills:
|
|
||||||
path = os.path.join(self.SKILLS_DIR, skill)
|
|
||||||
self.assertTrue(
|
|
||||||
os.path.isdir(path),
|
|
||||||
f"Skill {skill!r} declared but directory not found at {path}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_each_skill_has_skill_md(self):
|
|
||||||
for skill in self.skills:
|
|
||||||
path = os.path.join(self.SKILLS_DIR, skill, 'SKILL.md')
|
|
||||||
self.assertTrue(os.path.isfile(path), f"Skill {skill!r} missing SKILL.md at {path}")
|
|
||||||
|
|
||||||
def test_each_skill_md_has_frontmatter(self):
|
|
||||||
import yaml
|
|
||||||
for skill in self.skills:
|
|
||||||
path = os.path.join(self.SKILLS_DIR, skill, 'SKILL.md')
|
|
||||||
with open(path) as f:
|
|
||||||
content = f.read()
|
|
||||||
self.assertTrue(
|
|
||||||
content.startswith('---'),
|
|
||||||
f"{skill}: SKILL.md must have YAML frontmatter"
|
|
||||||
)
|
|
||||||
parts = content.split('---', 2)
|
|
||||||
self.assertEqual(len(parts), 3, f"{skill}: SKILL.md must have opening and closing ---")
|
|
||||||
_, frontmatter, _ = parts
|
|
||||||
data = yaml.safe_load(frontmatter)
|
|
||||||
self.assertIsInstance(data, dict)
|
|
||||||
self.assertIn('name', data, f"{skill}: frontmatter must have 'name'")
|
|
||||||
|
|
||||||
def test_each_skill_body_has_heading(self):
|
|
||||||
for skill in self.skills:
|
|
||||||
path = os.path.join(self.SKILLS_DIR, skill, 'SKILL.md')
|
|
||||||
with open(path) as f:
|
|
||||||
content = f.read()
|
|
||||||
parts = content.split('---', 2)
|
|
||||||
_, _, body = parts
|
|
||||||
self.assertRegex(
|
|
||||||
body.lstrip(), r'^# ',
|
|
||||||
f"{skill}: SKILL.md body must start with # heading"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestAdapters(unittest.TestCase):
|
|
||||||
"""Verify runtime adapters exist."""
|
|
||||||
|
|
||||||
def test_claude_code_adapter_exists(self):
|
|
||||||
path = os.path.join(REPO_ROOT, 'adapters', 'claude_code.py')
|
|
||||||
self.assertTrue(os.path.isfile(path))
|
|
||||||
|
|
||||||
def test_claude_code_adapter_imports_adaptor(self):
|
|
||||||
path = os.path.join(REPO_ROOT, 'adapters', 'claude_code.py')
|
|
||||||
with open(path) as f:
|
|
||||||
content = f.read()
|
|
||||||
self.assertIn('Adaptor', content)
|
|
||||||
|
|
||||||
def test_deepagents_adapter_exists(self):
|
|
||||||
path = os.path.join(REPO_ROOT, 'adapters', 'deepagents.py')
|
|
||||||
self.assertTrue(os.path.isfile(path))
|
|
||||||
|
|
||||||
|
|
||||||
class TestKnownIssues(unittest.TestCase):
|
|
||||||
"""Verify known-issues.md structure."""
|
|
||||||
|
|
||||||
KI_PATH = os.path.join(REPO_ROOT, 'known-issues.md')
|
|
||||||
|
|
||||||
def test_file_exists(self):
|
|
||||||
self.assertTrue(os.path.isfile(self.KI_PATH))
|
|
||||||
|
|
||||||
def test_has_active_issues_section(self):
|
|
||||||
with open(self.KI_PATH) as f:
|
|
||||||
self.assertIn('Active Issues', f.read())
|
|
||||||
|
|
||||||
def test_has_severity_definitions(self):
|
|
||||||
with open(self.KI_PATH) as f:
|
|
||||||
content = f.read()
|
|
||||||
self.assertIn('Severity Definitions', content)
|
|
||||||
|
|
||||||
def test_has_reporting_section(self):
|
|
||||||
with open(self.KI_PATH) as f:
|
|
||||||
content = f.read()
|
|
||||||
self.assertIn('Reporting', content)
|
|
||||||
|
|
||||||
|
|
||||||
class TestReadme(unittest.TestCase):
|
|
||||||
"""Verify README.md has required sections."""
|
|
||||||
|
|
||||||
README_PATH = os.path.join(REPO_ROOT, 'README.md')
|
|
||||||
|
|
||||||
def test_readme_exists(self):
|
|
||||||
self.assertTrue(os.path.isfile(self.README_PATH))
|
|
||||||
|
|
||||||
def test_readme_has_h1(self):
|
|
||||||
with open(self.README_PATH) as f:
|
|
||||||
first_line = f.readline().strip()
|
|
||||||
self.assertTrue(
|
|
||||||
first_line.startswith('# '),
|
|
||||||
f"README must start with # heading, got: {first_line!r}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_readme_has_install_section(self):
|
|
||||||
with open(self.README_PATH) as f:
|
|
||||||
content = f.read()
|
|
||||||
self.assertIn('Install', content)
|
|
||||||
|
|
||||||
def test_readme_has_architecture_section(self):
|
|
||||||
with open(self.README_PATH) as f:
|
|
||||||
content = f.read()
|
|
||||||
self.assertIn('Architecture', content)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main(verbosity=2)
|
|
||||||
Loading…
Reference in New Issue
Block a user