import from local vendored copy (2026-05-06)
Some checks failed
CI / validate (push) Failing after 0s
Some checks failed
CI / validate (push) Failing after 0s
This commit is contained in:
commit
a3cb0a7aba
8
.gitattributes
vendored
Normal file
8
.gitattributes
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Shell scripts and Python hooks are executed by Linux containers.
|
||||
# Force LF so Windows checkouts (core.autocrlf=true) don't break the
|
||||
# hook dispatch path — see Molecule-AI/molecule-core#507 where CRLF
|
||||
# line endings made claude-code try to exec `session-start-context.py\r`
|
||||
# and the SessionStart hook failed silently, producing
|
||||
# "(no response generated)" on every agent A2A call.
|
||||
*.sh text eol=lf
|
||||
*.py text eol=lf
|
||||
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
|
||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
# Credentials — never commit. Use .env.example as the template.
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.sample
|
||||
|
||||
# Private keys + certs
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
*.p12
|
||||
*.pfx
|
||||
|
||||
# Secret directories
|
||||
.secrets/
|
||||
|
||||
# Workspace auth tokens
|
||||
.auth-token
|
||||
.auth_token
|
||||
|
||||
# Python bytecode
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
1
.molecule-ci/scripts/requirements.txt
Normal file
1
.molecule-ci/scripts/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
pyyaml>=6.0
|
||||
50
.molecule-ci/scripts/validate-plugin.py
Normal file
50
.molecule-ci/scripts/validate-plugin.py
Normal file
@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate a Molecule AI plugin repo."""
|
||||
import os, sys, yaml
|
||||
|
||||
errors = []
|
||||
|
||||
if not os.path.isfile("plugin.yaml"):
|
||||
print("::error::plugin.yaml not found at repo root")
|
||||
sys.exit(1)
|
||||
|
||||
with open("plugin.yaml") as f:
|
||||
plugin = yaml.safe_load(f)
|
||||
|
||||
for field in ["name", "version", "description"]:
|
||||
if not plugin.get(field):
|
||||
errors.append(f"Missing required field: {field}")
|
||||
|
||||
v = str(plugin.get("version", ""))
|
||||
if v and not all(c in "0123456789." for c in v):
|
||||
errors.append(f"Invalid version format: {v}")
|
||||
|
||||
runtimes = plugin.get("runtimes")
|
||||
if runtimes is not None and not isinstance(runtimes, list):
|
||||
errors.append(f"runtimes must be a list, got {type(runtimes).__name__}")
|
||||
|
||||
content_paths = ["SKILL.md", "hooks", "skills", "rules"]
|
||||
found = [p for p in content_paths if os.path.exists(p)]
|
||||
if not found:
|
||||
errors.append("Plugin must contain at least one of: SKILL.md, hooks/, skills/, rules/")
|
||||
|
||||
if os.path.isfile("SKILL.md"):
|
||||
with open("SKILL.md") as f:
|
||||
first_line = f.readline().strip()
|
||||
if first_line and not first_line.startswith("#"):
|
||||
print("::warning::SKILL.md should start with a markdown heading (e.g., # Plugin Name)")
|
||||
|
||||
if errors:
|
||||
for e in errors:
|
||||
print(f"::error::{e}")
|
||||
sys.exit(1)
|
||||
|
||||
pn = plugin["name"]; pv = plugin["version"]
|
||||
print(f"\u2713 plugin.yaml valid: {pn} v{pv}")
|
||||
if found:
|
||||
print(f" Content: {', '.join(found)}")
|
||||
if runtimes:
|
||||
print(f" Runtimes: {', '.join(runtimes)}")
|
||||
|
||||
|
||||
|
||||
106
CLAUDE.md
Normal file
106
CLAUDE.md
Normal file
@ -0,0 +1,106 @@
|
||||
# molecule-session-context — Session Start Context Loader
|
||||
|
||||
`molecule-session-context` is a **session-initialisation hook plugin** that
|
||||
auto-loads recent cron-learnings and repo PR/issue counts at `SessionStart`.
|
||||
Pairs with `molecule-cron-learnings`.
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Runtime:** `claude_code`
|
||||
|
||||
---
|
||||
|
||||
## Repository Layout
|
||||
|
||||
```
|
||||
molecule-session-context/
|
||||
├── plugin.yaml — Plugin manifest
|
||||
├── hooks/
|
||||
│ └── session-start-context/
|
||||
│ └── hook.json — SessionStart hook definition
|
||||
└── adapters/ — Harness adaptors
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What It Does
|
||||
|
||||
At the start of every session, this hook:
|
||||
1. Reads the last N lines of `~/.claude/projects/<project>/cron-learnings.jsonl`
|
||||
2. Loads current PR/issue counts for the workspace repo
|
||||
3. Surfaces this context to the agent in the first response
|
||||
|
||||
This means the agent enters every session already knowing:
|
||||
- What went wrong last time (from cron-learnings)
|
||||
- How many open PRs and issues exist (context before acting)
|
||||
|
||||
---
|
||||
|
||||
## SessionStart Hook
|
||||
|
||||
The hook fires on every new session for a workspace. Configure how many
|
||||
learnings to load via workspace settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"session_context": {
|
||||
"learnings_lines": 20,
|
||||
"include_pr_counts": true,
|
||||
"include_issue_counts": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.11+
|
||||
- `gh` CLI authenticated
|
||||
- Write access to `Molecule-AI/molecule-ai-plugin-molecule-session-context`
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Molecule-AI/molecule-ai-plugin-molecule-session-context.git
|
||||
cd molecule-ai-plugin-molecule-session-context
|
||||
|
||||
# YAML validation
|
||||
python3 -c "import yaml; yaml.safe_load(open('plugin.yaml'))"
|
||||
```
|
||||
|
||||
### Pre-Commit Checklist
|
||||
|
||||
```bash
|
||||
# YAML structure
|
||||
python3 -c "import yaml; yaml.safe_load(open('plugin.yaml'))"
|
||||
|
||||
# Credential scan
|
||||
python3 -c "
|
||||
import re, sys
|
||||
with open('plugin.yaml') as f:
|
||||
content = f.read()
|
||||
patterns = [r'sk.ant', r'ghp.', r'AKIA[A-Z0-9]']
|
||||
if any(re.search(p, content) for p in patterns):
|
||||
print('FAIL: possible credentials found')
|
||||
sys.exit(1)
|
||||
print('No credentials: OK')
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Release Process
|
||||
|
||||
1. Review changes: `git log origin/main..HEAD --oneline`
|
||||
2. Bump `version` in `plugin.yaml` (semver)
|
||||
3. Commit: `chore: bump version to X.Y.Z`
|
||||
4. Tag and push: `git tag vX.Y.Z && git push origin main --tags`
|
||||
5. Create GitHub Release with changelog
|
||||
|
||||
---
|
||||
|
||||
## Known Issues
|
||||
|
||||
See `known-issues.md` at the repo root.
|
||||
19
README.md
Normal file
19
README.md
Normal file
@ -0,0 +1,19 @@
|
||||
# molecule-session-context
|
||||
|
||||
Molecule AI plugin. Install via the Molecule AI platform plugin system.
|
||||
|
||||
## Usage
|
||||
|
||||
### In org template (org.yaml)
|
||||
```yaml
|
||||
plugins:
|
||||
- molecule-session-context
|
||||
```
|
||||
|
||||
### From URL (community install)
|
||||
```
|
||||
github://Molecule-AI/molecule-ai-plugin-molecule-session-context
|
||||
```
|
||||
|
||||
## License
|
||||
Business Source License 1.1 — © Molecule AI.
|
||||
0
adapters/__init__.py
Normal file
0
adapters/__init__.py
Normal file
2
adapters/claude_code.py
Normal file
2
adapters/claude_code.py
Normal file
@ -0,0 +1,2 @@
|
||||
"""Claude Code adaptor — uses the generic rule+skill+hooks installer."""
|
||||
from plugins_registry.builtins import AgentskillsAdaptor as Adaptor # noqa: F401
|
||||
46
hooks/_lib.py
Executable file
46
hooks/_lib.py
Executable file
@ -0,0 +1,46 @@
|
||||
"""Common helpers for Claude Code hooks. Imported by the .py hook scripts.
|
||||
|
||||
Hooks receive JSON on stdin per the Claude Code hook spec, and may emit
|
||||
JSON on stdout or exit with code 2 to block. This module wraps both.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def read_input() -> dict:
|
||||
"""Parse stdin JSON. Empty input → empty dict."""
|
||||
raw = sys.stdin.read().strip()
|
||||
if not raw:
|
||||
return {}
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
|
||||
|
||||
def emit(payload: dict) -> None:
|
||||
"""Print JSON payload to stdout for the harness to interpret."""
|
||||
print(json.dumps(payload))
|
||||
|
||||
|
||||
def deny_pretooluse(reason: str) -> None:
|
||||
"""Emit a PreToolUse denial with reason and exit 0."""
|
||||
emit({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": reason,
|
||||
}
|
||||
})
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def add_context(text: str) -> None:
|
||||
"""Emit additionalContext for SessionStart / UserPromptSubmit hooks."""
|
||||
if text and text.strip():
|
||||
emit({"additionalContext": text})
|
||||
|
||||
|
||||
def warn_to_stderr(msg: str) -> None:
|
||||
"""Non-blocking warning visible to the next agent turn via stderr."""
|
||||
print(msg, file=sys.stderr)
|
||||
71
hooks/session-start-context.py
Executable file
71
hooks/session-start-context.py
Executable file
@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""SessionStart hook — auto-load recent cron-learnings, freeze status,
|
||||
and a one-line repo snapshot into Claude's context.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _lib import add_context, warn_to_stderr # noqa
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
LEARNINGS = os.path.expanduser(
|
||||
"~/.claude/projects/-Users-hongming-Documents-GitHub-molecule-monorepo/memory/cron-learnings.jsonl"
|
||||
)
|
||||
FREEZE = os.path.join(REPO, ".claude", "freeze")
|
||||
|
||||
|
||||
def tail(path: str, n: int) -> str:
|
||||
if not os.path.isfile(path):
|
||||
return ""
|
||||
try:
|
||||
with open(path) as f:
|
||||
lines = f.readlines()
|
||||
return "".join(lines[-n:]).rstrip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def gh_count(args: list) -> str:
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["gh"] + args + ["--json", "number"],
|
||||
capture_output=True, text=True, timeout=4,
|
||||
)
|
||||
if out.returncode != 0:
|
||||
return "?"
|
||||
import json
|
||||
return str(len(json.loads(out.stdout or "[]")))
|
||||
except Exception:
|
||||
return "?"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parts = []
|
||||
|
||||
learnings = tail(LEARNINGS, 20)
|
||||
if learnings:
|
||||
parts.append(f"## Recent cron learnings (last 20)\n{learnings}")
|
||||
|
||||
if os.path.isfile(FREEZE):
|
||||
try:
|
||||
with open(FREEZE) as f:
|
||||
frozen = f.readline().strip()
|
||||
parts.append(f"## ⚠ FREEZE ACTIVE\nEdits restricted to: {frozen}\nRemove .claude/freeze to unlock.")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
pr = gh_count(["pr", "list", "--repo", "Molecule-AI/molecule-monorepo", "--state", "open"])
|
||||
iss = gh_count(["issue", "list", "--repo", "Molecule-AI/molecule-monorepo", "--state", "open"])
|
||||
parts.append(f"## Repo state\nOpen PRs: {pr} · Open issues: {iss}")
|
||||
|
||||
if parts:
|
||||
add_context("\n\n".join(parts))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
warn_to_stderr(f"[session-start hook error] {e}")
|
||||
sys.exit(0)
|
||||
2
hooks/session-start-context.sh
Executable file
2
hooks/session-start-context.sh
Executable file
@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
exec python3 "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/session-start-context.py"
|
||||
54
known-issues.md
Normal file
54
known-issues.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Known Issues — molecule-session-context
|
||||
|
||||
---
|
||||
|
||||
## Active Issues
|
||||
|
||||
*(None currently open. This section is updated when issues are filed.)*
|
||||
|
||||
---
|
||||
|
||||
## Recently Resolved
|
||||
|
||||
*(No recently resolved issues.)*
|
||||
|
||||
---
|
||||
|
||||
## How to Update This File
|
||||
|
||||
When a new issue is identified:
|
||||
1. Add it under **Active Issues** using the template below
|
||||
2. Include: symptom, cause (if known), workaround
|
||||
3. When fixed, move to **Recently Resolved** and note the fix version
|
||||
|
||||
### Issue Template
|
||||
|
||||
```markdown
|
||||
## [TICKET-NUMBER] <Short Title>
|
||||
|
||||
**Severity:** P0 / P1 / P2 / P3
|
||||
**Status:** Workaround / Fix in progress / Fix available
|
||||
**Affected versions:** All / vX.Y.Z+
|
||||
|
||||
**Symptoms:**
|
||||
**Cause:**
|
||||
**Workaround:**
|
||||
**Fix (if available):**
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Severity Definitions
|
||||
|
||||
| Level | Description |
|
||||
|---|---|
|
||||
| P0 | Session start crashes; no context loaded |
|
||||
| P1 | Context loaded but wrong workspace |
|
||||
| P2 | Context stale or missing learnings |
|
||||
| P3 | Cosmetic or documentation issue |
|
||||
|
||||
---
|
||||
|
||||
## Reporting
|
||||
|
||||
Use the Molecule-AI/internal issue tracker. Tag with `plugin-molecule-session-context`.
|
||||
11
plugin.yaml
Normal file
11
plugin.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
name: molecule-session-context
|
||||
version: 1.0.0
|
||||
description: Auto-load recent cron-learnings + repo PR/issue counts at SessionStart. Pairs well with molecule-cron-learnings.
|
||||
author: Molecule AI
|
||||
tags: [molecule, guardrails]
|
||||
|
||||
runtimes:
|
||||
- claude_code
|
||||
|
||||
hooks:
|
||||
- session-start-context
|
||||
92
runbooks/local-dev-setup.md
Normal file
92
runbooks/local-dev-setup.md
Normal file
@ -0,0 +1,92 @@
|
||||
# Local Development Setup
|
||||
|
||||
This runbook covers setting up a local development environment for
|
||||
`molecule-session-context`.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.11+
|
||||
- `gh` CLI authenticated
|
||||
- Write access to `Molecule-AI/molecule-ai-plugin-molecule-session-context`
|
||||
|
||||
---
|
||||
|
||||
## Clone & Bootstrap
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Molecule-AI/molecule-ai-plugin-molecule-session-context.git
|
||||
cd molecule-ai-plugin-molecule-session-context
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validating Plugin Structure
|
||||
|
||||
```bash
|
||||
# YAML structure
|
||||
python3 -c "import yaml; yaml.safe_load(open('plugin.yaml'))"
|
||||
echo "plugin.yaml OK"
|
||||
|
||||
# Check all hook paths exist
|
||||
python3 -c "
|
||||
import yaml, os
|
||||
with open('plugin.yaml') as f:
|
||||
data = yaml.safe_load(f)
|
||||
for hook in data.get('hooks', []):
|
||||
path = f'hooks/{hook}/hook.json'
|
||||
exists = os.path.exists(path)
|
||||
print(f'[{\"OK\" if exists else \"MISSING\"}] {path}')
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing the SessionStart Hook
|
||||
|
||||
The harness wrapper is provided by the Molecule AI platform at runtime. To test:
|
||||
|
||||
1. Install the plugin in a test workspace
|
||||
2. Start a new session
|
||||
3. Verify the first agent response references recent learnings
|
||||
4. Check the cron-learnings JSONL file has entries
|
||||
|
||||
---
|
||||
|
||||
## Simulating Session Context
|
||||
|
||||
To test without a live workspace:
|
||||
|
||||
```bash
|
||||
# Create a mock learnings file
|
||||
mkdir -p ~/.claude/projects/test-project
|
||||
cat > ~/.claude/projects/test-project/cron-learnings.jsonl << 'EOF'
|
||||
{"tick": "2026-04-21T00:00Z", "role": "test-lead", "learnings": ["GH_TOKEN expired — refresh needed", "PR template missing Testing section"]}
|
||||
EOF
|
||||
|
||||
# Simulate session start (hook reads this file)
|
||||
cat ~/.claude/projects/test-project/cron-learnings.jsonl
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No context loaded at session start
|
||||
|
||||
- Verify `hooks/session-start-context/hook.json` is correctly named and placed
|
||||
- Check the hook is registered in `plugin.yaml`
|
||||
- Verify the workspace has read access to `~/.claude/projects/`
|
||||
|
||||
### Stale learnings
|
||||
|
||||
- The hook reads the JSONL file on every session start — check file permissions
|
||||
- If learnings are not being appended, check `molecule-cron-learnings` is installed
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- `molecule-cron-learnings` — appends learnings at end of each tick
|
||||
- `molecule-workflow-retro` — generates weekly retrospectives from learnings
|
||||
1
settings-fragment.json
Normal file
1
settings-fragment.json
Normal file
@ -0,0 +1 @@
|
||||
{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"bash ${CLAUDE_DIR}/hooks/session-start-context.sh"}]}]}}
|
||||
Loading…
Reference in New Issue
Block a user