Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install pyyaml pytest
pip install -e .
pip install pytest
export PYTHONPATH=.
- name: Run tests
run: PYTHONPATH=. python3 -m pytest tests/ -v
Expand Down
24 changes: 13 additions & 11 deletions agent_notes/commands/wizard/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def install_agents_filtered(clis: Set[str], scope: str, copy_mode: bool = False,
folder_overrides: dict = None, global_home_override: str = "") -> None:
"""Install agents for selected CLIs (filtered by the wizard)."""
from ...services import installer
from ...services.installer import _apply_overrides
from ...services.installer import _apply_overrides, _agent_glob
from ...registries.cli_registry import load_registry

registry = load_registry()
Expand All @@ -43,11 +43,12 @@ def install_agents_filtered(clis: Set[str], scope: str, copy_mode: bool = False,
if dst is None:
continue

files = list(src.glob("*.md"))
glob = _agent_glob(effective)
files = list(src.glob(glob))
if not files:
continue

place_dir_contents(src, dst, "*.md", copy_mode)
place_dir_contents(src, dst, glob, copy_mode)


def install_config_filtered(clis: Set[str], scope: str, copy_mode: bool = False,
Expand Down Expand Up @@ -161,15 +162,16 @@ def _execute_install(
if _cmd_names:
print(f" {Color.GREEN}✓{Color.NC} Commands {', '.join(sorted(_cmd_names))}")

# SessionStart hook (Claude Code only)
# SessionStart hooks — for every backend with features.session_hook == true
from ...services.installer import _install_session_hook
try:
_claude = _registry.get("claude")
if _claude.name in clis:
_claude_eff = _apply_overrides(_claude, folder_overrides, global_home_override or None)
_install_session_hook(_claude_eff, scope, memory_backend=memory_backend, memory_path=memory_path or "")
except (KeyError, Exception):
pass
for _hook_backend in _registry.with_feature("session_hook"):
if _hook_backend.name not in clis:
continue
try:
_hook_eff = _apply_overrides(_hook_backend, folder_overrides, global_home_override or None)
_install_session_hook(_hook_eff, scope, memory_backend=memory_backend, memory_path=memory_path or "")
except Exception:
pass

_fs.silent_file_ops = False

Expand Down
1 change: 1 addition & 0 deletions agent_notes/data/agents/agents.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ agents:
color: purple
effort: high
claude_exclude: true
codex_exclude: true
claude:
tools: "Agent(coder, reviewer, security-auditor, test-writer, test-runner, system-auditor, database-specialist, performance-profiler, api-reviewer, tech-writer, devops, explorer), Read, Grep, Glob, Bash"
memory: user
Expand Down
5 changes: 5 additions & 0 deletions agent_notes/data/cli/claude.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ layout:
config: CLAUDE.md
memory: agent-memory/
settings: settings.json
agent_extension: md
features:
agents: true
skills: true
Expand All @@ -20,7 +21,11 @@ features:
config_style: inline
settings_template: false
supports_symlink: true
session_hook: true
stop_hook: true
allow_entries: true
global_template: global-claude.md
exclude_flag: claude_exclude
accepted_providers: [anthropic, bedrock, vertex]
use_model_class: true
preferred_family: claude
29 changes: 29 additions & 0 deletions agent_notes/data/cli/codex.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: codex
label: Codex CLI
global_home: ~/.codex
local_dir: .codex
layout:
agents: agents/
skills: skills/
config: AGENTS.md
hooks: hooks.json
agent_extension: toml
features:
agents: true
skills: true
rules: false
commands: false
memory: false
frontmatter: codex
config_style: inline
settings_template: false
supports_symlink: true
session_hook: true
stop_hook: false
allow_entries: false
global_template: global-codex.md
exclude_flag: codex_exclude
strip_memory_section: true
accepted_providers: [openai]
use_model_class: false
preferred_family: gpt
5 changes: 5 additions & 0 deletions agent_notes/data/cli/opencode.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ layout:
agents: agents/
skills: skills/
config: AGENTS.md
agent_extension: md
features:
agents: true
skills: true
Expand All @@ -16,7 +17,11 @@ features:
config_style: inline
settings_template: false
supports_symlink: true
session_hook: false
stop_hook: false
allow_entries: false
global_template: global-opencode.md
exclude_flag: opencode_exclude
strip_memory_section: true
accepted_providers: [github-copilot, anthropic, openrouter, openai, google, moonshot]
preferred_family: claude
40 changes: 40 additions & 0 deletions agent_notes/data/global-codex.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Global Instructions

## Coding philosophy

- Read existing code before writing new code. Match project patterns.
- Minimal changes: only what was requested. Do not refactor beyond scope.
- Fix root causes, not symptoms.
- One approach, commit to it. Course-correct only on new evidence.

## Behavior

- Investigate before answering. Never speculate about code you haven't read.
- No over-engineering: no extra features, abstractions, or configs beyond scope.
- No comments or docs on code you didn't change.
- When the task is unclear, ask one clarifying question instead of guessing.

## Safety

- Confirm before: `git push --force`, `rm -rf`, `DROP TABLE`, branch deletion.
- Never commit: `.env`, `*.pem`, credentials, API keys, secrets.
- Never bypass: `--no-verify`, `--force` without explicit user request.
- Never force-push to main/master.

## Commits

- Load the `git` skill when asked to commit and follow its workflow.
- Analyze all changes, group into logical chunks, make small focused commits.
- Format: `#<ticket> type(scope): short description` — title only, no body.
- Extract ticket number from branch name when available.
- Types: feat, fix, refactor, test, docs, chore, style, perf

## Agent delegation

- Use subagents when tasks can run in parallel or require isolated context.
- For simple tasks, sequential operations, or single-file edits, work directly.
- Use `explorer` for quick lookups to save context tokens.
- Use `database-specialist` for schema, indexes, and query analysis.
- Use `performance-profiler` for bottleneck identification.
- Use `api-reviewer` for API design and consistency checks.
- Use `lead` for complex multi-step tasks requiring coordination.
14 changes: 14 additions & 0 deletions agent_notes/data/models/gpt-5-4-mini.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
id: gpt-5-4-mini
label: GPT-5.4 Mini
family: gpt
class: haiku
aliases:
openai: gpt-5.4-mini
pricing:
input: 0.15
output: 0.60
cache: 0.02
capabilities:
vision: true
long_context: false
tool_use: true
14 changes: 14 additions & 0 deletions agent_notes/data/models/gpt-5-4.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
id: gpt-5-4
label: GPT-5.4
family: gpt
class: sonnet
aliases:
openai: gpt-5.4
pricing:
input: 3.00
output: 15.00
cache: 0.30
capabilities:
vision: true
long_context: true
tool_use: true
14 changes: 14 additions & 0 deletions agent_notes/data/models/gpt-5-5.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
id: gpt-5-5
label: GPT-5.5
family: gpt
class: opus
aliases:
openai: gpt-5.5
pricing:
input: 10.00
output: 40.00
cache: 1.00
capabilities:
vision: true
long_context: true
tool_use: true
106 changes: 106 additions & 0 deletions agent_notes/data/templates/frontmatter/codex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Codex CLI agent file generator — emits whole-file TOML, not frontmatter+markdown."""

import tomli_w


_EFFORT_MAP = {
"minimal": "minimal",
"low": "low",
"medium": "medium",
"high": "high",
"xhigh": "xhigh",
}

_STRIP_PREFIXES = ("## Memory", "## Cost reporting")


def render(ctx: dict) -> str:
"""Not used for Codex — emit_file supersedes render.
Kept so the module is consistent with other frontmatter templates."""
return ""


def emit_file(ctx: dict, body: str) -> tuple[str, str]:
"""Return (filename, full_file_content) for a Codex agent TOML file.

The returned content is a complete TOML document — not a frontmatter snippet.
rendering.py writes this verbatim in place of the normal frontmatter+body output.
"""
agent_name = ctx['agent_name']
agent_config = ctx['agent_config']
model_str = ctx.get('model_str') or ''

doc: dict = {
"name": agent_name,
"description": agent_config["description"],
"developer_instructions": body,
}

if model_str:
doc["model"] = model_str

effort_key = agent_config.get("effort", "medium")
doc["model_reasoning_effort"] = _EFFORT_MAP.get(effort_key, "medium")

doc["sandbox_mode"] = _sandbox_mode(agent_config)

filename = f"{agent_name}.toml"
content = tomli_w.dumps(doc)
return filename, content


def post_process(prompt: str, ctx: dict) -> str:
"""Strip Codex-irrelevant sections from the agent prompt body.

Strips:
- ## Memory* sections (Codex has no agent memory)
- ## Cost reporting section (Claude Code CLI tool, not available in Codex)
"""
return _strip_sections(prompt, _STRIP_PREFIXES)


# --- helpers ---

def _sandbox_mode(agent_config: dict) -> str:
"""Derive Codex sandbox_mode from the agent's tool/permission config.

Writer agents (those that can Write or Edit files) -> "workspace-write"
All others -> "read-only"
"""
claude_config = agent_config.get('claude', {}) or {}
tools = claude_config.get('tools', '') or ''
if isinstance(tools, str):
if 'Write' in tools or 'Edit' in tools:
return "workspace-write"
elif isinstance(tools, list):
if 'Write' in tools or 'Edit' in tools:
return "workspace-write"

opencode_config = agent_config.get('opencode', {}) or {}
permission = opencode_config.get('permission', {}) or {}
if permission.get('edit') == 'allow':
return "workspace-write"

return "read-only"


def _strip_sections(content: str, strip_prefixes: tuple) -> str:
"""Strip ## sections whose heading starts with any of the given prefixes."""
lines = content.split('\n')
result_lines = []
in_stripped_section = False

for line in lines:
if any(line.startswith(prefix) for prefix in strip_prefixes):
in_stripped_section = True
continue
elif line.startswith('## ') and in_stripped_section:
in_stripped_section = False
result_lines.append(line)
elif not in_stripped_section:
result_lines.append(line)

while result_lines and result_lines[-1].strip() == '':
result_lines.pop()

return '\n'.join(result_lines)
1 change: 1 addition & 0 deletions agent_notes/domain/cli_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class CLIBackend:
settings_template: Optional[str] = None
accepted_providers: tuple[str, ...] = () # new
use_model_class: bool = False
preferred_family: Optional[str] = None # "claude", "gpt", etc. — preferred model family for step-2 fallback

def supports(self, feature: str) -> bool:
"""Return True if the backend has that feature enabled."""
Expand Down
3 changes: 2 additions & 1 deletion agent_notes/registries/cli_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ def load_registry(cli_dir: Optional[Path] = None) -> CLIRegistry:
strip_memory_section=data.get("strip_memory_section", False),
settings_template=data.get("settings_template"),
accepted_providers=tuple(data.get("accepted_providers", [])),
use_model_class=data.get("use_model_class", False)
use_model_class=data.get("use_model_class", False),
preferred_family=data.get("preferred_family"),
)
backends.append(backend)

Expand Down
30 changes: 14 additions & 16 deletions agent_notes/services/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"tomli is required on Python < 3.11. Install it: pip install tomli"
) from exc

import tomli_w

CONFIG_PATH = Path.home() / ".agent-notes" / "credentials.toml"
SAFE_MODE = 0o600

Expand Down Expand Up @@ -95,30 +97,26 @@ def _write(data: dict) -> None:
tmp_pathobj = Path(tmp_path)
try:
with os.fdopen(fd, "w") as fh:
_dump_toml(data, fh)
fh.write(_dump_toml(data))
tmp_pathobj.chmod(SAFE_MODE)
tmp_pathobj.replace(CONFIG_PATH)
finally:
if tmp_pathobj.exists():
tmp_pathobj.unlink()


def _dump_toml(data: dict, fh) -> None:
"""Minimal TOML writer for the credentials schema."""
def _dump_toml(data: dict) -> str:
"""Serialize credentials dict to TOML using tomli_w.

None values are filtered out before serialization — TOML has no null type.
The hand-rolled predecessor would have written Python's ``None`` repr, which
is not valid TOML and would fail on read-back anyway.
"""
providers = data.get("providers", {})
for name in sorted(providers):
block = providers[name]
fh.write(f"[providers.{name}]\n")
for k in sorted(block):
v = block[k]
if isinstance(v, bool):
fh.write(f"{k} = {'true' if v else 'false'}\n")
elif isinstance(v, str):
escaped = v.replace('\\', '\\\\').replace('"', '\\"')
fh.write(f'{k} = "{escaped}"\n')
else:
fh.write(f"{k} = {v!r}\n")
fh.write("\n")
clean: dict = {"providers": {}}
for name, block in providers.items():
clean["providers"][name] = {k: v for k, v in block.items() if v is not None}
return tomli_w.dumps(clean)


def list_providers() -> list:
Expand Down
Loading
Loading