Merge pull request #189 from AIOSAI/system/devpulse-codex-cli-integration-agentsmd-hooks-skills-setups

feat(system): Codex CLI integration: AGENTS.md, hooks, skills, setup.sh detection
This commit is contained in:
AIPass
2026-04-05 22:28:16 -07:00
committed by GitHub
8 changed files with 514 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "python3 $(git rev-parse --show-toplevel 2>/dev/null)/.codex/hooks/session_start_identity.py",
"timeout": 10,
"statusMessage": "Loading AIPass identity..."
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "python3 $(git rev-parse --show-toplevel 2>/dev/null)/.codex/hooks/prompt_inject.py",
"timeout": 10,
"statusMessage": "Injecting AIPass context..."
}
]
}
],
"PreToolUse": [
{
"matcher": "^(Edit|MultiEdit|Write|NotebookEdit)$",
"hooks": [
{
"type": "command",
"command": "python3 $(git rev-parse --show-toplevel 2>/dev/null)/.codex/hooks/pre_edit_gate.py",
"timeout": 5
}
]
}
]
}
}
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Codex PreToolUse hook: gate file edits to protect critical files.
Blocks edits to .trinity/passport.json and other protected paths.
"""
import json
import sys
PROTECTED_PATTERNS = [
".trinity/passport.json",
".aipass/registry.json",
"setup.sh",
]
def main():
try:
input_data = json.loads(sys.stdin.read())
except Exception:
print(json.dumps({}))
return
tool_input = input_data.get("input", {})
file_path = tool_input.get("file_path", "") or tool_input.get("path", "")
if not file_path:
print(json.dumps({}))
return
for pattern in PROTECTED_PATTERNS:
if pattern in file_path:
output = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny"
},
"systemMessage": f"Edit blocked: {pattern} is a protected file."
}
print(json.dumps(output))
return
print(json.dumps({}))
if __name__ == "__main__":
main()
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Codex UserPromptSubmit hook: inject per-turn AIPass context.
Injects branch identity, email notifications, and current time on every prompt.
"""
import json
import sys
from datetime import datetime
from pathlib import Path
def find_repo_root():
p = Path.cwd()
while p != p.parent:
if (p / ".git").exists():
return p
p = p.parent
return None
def get_branch_from_cwd(repo_root):
cwd = Path.cwd()
try:
rel = cwd.relative_to(repo_root / "src" / "aipass")
return str(rel).split("/")[0]
except ValueError:
try:
rel = cwd.relative_to(repo_root / "src")
return str(rel).split("/")[0]
except ValueError:
return None
def main():
try:
input_data = json.loads(sys.stdin.read())
except Exception:
input_data = {}
repo_root = find_repo_root()
if not repo_root:
print(json.dumps({}))
return
context_parts = []
# 1. Current time
now = datetime.now().strftime("%A, %B %-d %Y — %-I:%M %p")
context_parts.append(f"# Current Time: {now}")
# 2. Branch identity (compact form for per-turn)
branch = get_branch_from_cwd(repo_root)
if branch:
branch_dir = repo_root / "src" / "aipass" / branch
if not branch_dir.exists():
branch_dir = repo_root / "src" / branch
passport = branch_dir / ".trinity" / "passport.json"
if passport.exists():
try:
data = json.loads(passport.read_text(encoding="utf-8"))
identity = data.get("identity", {})
traits = data.get("traits", [])
context_parts.append(
f"# {branch.upper()} Identity\n"
f"Path: {data.get('branch_info', {}).get('path', 'unknown')}\n"
f"Role: {identity.get('role', 'unknown')}\n"
f"Traits: {' | '.join(traits)}\n"
f"Purpose: {identity.get('purpose', 'unknown')}"
)
except Exception:
pass
# 3. Email notification
inbox = branch_dir / ".ai_mail.local" / "inbox.json"
if inbox.exists():
try:
mail = json.loads(inbox.read_text(encoding="utf-8"))
unread = mail.get("unread_count", 0)
if unread > 0:
context_parts.append(
f"You have {unread} new emails - check with: "
f"drone @ai_mail inbox"
)
except Exception:
pass
if context_parts:
context = "\n\n".join(context_parts)
output = {
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": context
}
}
else:
output = {}
print(json.dumps(output))
if __name__ == "__main__":
main()
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Codex SessionStart hook: inject AIPass identity context.
Reads .trinity/passport.json and branch prompt, outputs Codex-format JSON
with additionalContext for identity injection.
"""
import json
import os
import sys
from pathlib import Path
def find_repo_root():
"""Walk up to find .git directory."""
p = Path.cwd()
while p != p.parent:
if (p / ".git").exists():
return p
p = p.parent
return None
def get_branch_from_cwd(repo_root):
"""Determine branch name from CWD relative to repo."""
cwd = Path.cwd()
try:
rel = cwd.relative_to(repo_root / "src" / "aipass")
return str(rel).split("/")[0]
except ValueError:
try:
rel = cwd.relative_to(repo_root / "src")
return str(rel).split("/")[0]
except ValueError:
return None
def main():
try:
input_data = json.loads(sys.stdin.read())
except Exception:
input_data = {}
repo_root = find_repo_root()
if not repo_root:
print(json.dumps({}))
return
context_parts = []
# 1. Global prompt
global_prompt = repo_root / ".aipass" / "aipass_global_prompt.md"
if global_prompt.exists():
context_parts.append(global_prompt.read_text(encoding="utf-8")[:8000])
# 2. Branch identity
branch = get_branch_from_cwd(repo_root)
if branch:
branch_dir = repo_root / "src" / "aipass" / branch
if not branch_dir.exists():
branch_dir = repo_root / "src" / branch
# Passport
passport = branch_dir / ".trinity" / "passport.json"
if passport.exists():
try:
data = json.loads(passport.read_text(encoding="utf-8"))
identity = data.get("identity", {})
context_parts.append(
f"# Branch Identity: {branch.upper()}\n"
f"Role: {identity.get('role', 'unknown')}\n"
f"Purpose: {identity.get('purpose', 'unknown')}\n"
f"Class: {identity.get('citizen_class', 'unknown')}"
)
except Exception:
pass
# Branch prompt
branch_prompt = branch_dir / ".aipass" / "aipass_local_prompt.md"
if branch_prompt.exists():
context_parts.append(branch_prompt.read_text(encoding="utf-8")[:4000])
if context_parts:
context = "\n\n---\n\n".join(context_parts)
output = {
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": context
}
}
else:
output = {}
print(json.dumps(output))
if __name__ == "__main__":
main()
+28
View File
@@ -0,0 +1,28 @@
---
name: memo
description: Update branch memory files after completing work. Saves session history, key learnings, and collaboration observations to .trinity/ files.
---
# Memory Update
Purpose: Update branch memory files after completing work this session.
## Execution
1. Read `.trinity/passport.json` first — re-absorb your identity, role, and principles before writing memories
2. Review what was done this session (context, recent changes, key decisions)
3. Update each file below as needed
4. Confirm completion — list files updated
## What to Update
### Always
- **.trinity/local.json** — Add new session entry to `sessions` if significant work was done. Add new `key_learnings` for facts you'd need next time. Trim oldest sessions if over 20.
- **.trinity/observations.json** — Add notable collaboration insights: breakthrough moments, pattern corrections, flow states, friction points, preference discoveries. Skip if nothing notable this session.
### If Relevant
- **.trinity/passport.json** — Evolve identity when the branch's role, capabilities, or principles have genuinely changed. Don't update just to update — but don't leave placeholders forever either.
- **README.md** — Does it reflect current state? Update if stale.
- **STATUS.local.md** — Drop quick notes on issues, todos, or ideas in the Notepad section.
+56
View File
@@ -0,0 +1,56 @@
---
name: prep
description: Session wrap-up. Update memories, check plans, review git state, check inbox, flag loose ends. Use before closing a session or compacting context.
---
# Session Wrap-Up
Purpose: Button up everything at the end of a session — or before a /compact. Memories, plans, git — all tidy.
## Execution
1. Read `.trinity/passport.json` first — re-absorb your identity before writing anything
2. Do ALL of the following, then confirm what was updated
## 1. Memories
- **.trinity/local.json** — Add/update session entry with summary of work done. Add new key_learnings for anything learned this session. Trim oldest sessions if over 20.
- **.trinity/observations.json** — Add collaboration insights if anything notable happened. Skip if nothing new.
- **.trinity/passport.json** — Only update if role/purpose/principles genuinely changed this session.
## 2. Active Plans
- Check any DPLANs or FPLANs referenced in this session
- Update their execution logs, status, decision logs with current state
- If a plan was completed, note it (but don't close — the user does that)
## 3. Git State
- Run `git status` — report uncommitted changes
- If there's a logical commit waiting, suggest it (don't commit without asking)
- Note the current branch and any open PRs
## 4. Inbox
- Run `drone @ai_mail inbox 2>/dev/null` — report any unread emails
- Close any that were already processed but not formally closed
## 5. Loose Ends
- Flag anything in-flight: running background agents, dispatched branches waiting for replies, pending decisions
- If anything can't survive compaction, write it to STATUS.local.md Notepad
## Confirm
List everything updated. Format:
```
Prep complete:
- local.json: [what was added]
- observations.json: [updated / skipped]
- Plans: [which ones updated]
- Git: [branch, uncommitted count, suggestion]
- Inbox: [count, action taken]
- Loose ends: [any flagged]
Ready to close out or compact.
```
+40
View File
@@ -0,0 +1,40 @@
# AIPass
A multi-agent framework where autonomous citizens live in branches and deploy disposable agents to do work.
## Branches
Every branch follows this structure:
```
src/aipass/{name}/
├── .trinity/ # Identity & memory (passport.json, local.json, observations.json)
├── .aipass/ # System prompt
├── .ai_mail.local/ # Mailbox (inbox.json, sent/)
├── apps/
│ ├── {name}.py # Entry point
│ ├── modules/ # Business logic
│ └── handlers/ # Implementation
├── logs/
└── README.md
```
15 branches: drone, seedgo, prax, cli, flow, ai_mail, api, trigger, spawn, devpulse, backup, daemon, memory, commons, skills
## Commands
drone systems # List all branches
drone @seedgo audit aipass # Run standards audit
drone @ai_mail inbox # Check email
drone @ai_mail email @target "Subject" "Body" # Send email
drone @flow list open # Active plans
## Identity
Read .trinity/passport.json to understand your role. Read .trinity/local.json for session history. Read STATUS.local.md for current work.
## Key Principles
- Code is truth. Running code beats architecture.
- Memory is everything. Update .trinity/ often.
- Dispatch, don't do. Branches are experts in their domain.
- Fail honestly. Errors over silent fallbacks.
+102
View File
@@ -343,6 +343,103 @@ else
echo "Skipping hooks (no .claude/hooks/ directory found)"
fi
# --- Install Codex CLI hooks ---
if command -v codex &>/dev/null; then
if [ -f "$SCRIPT_DIR/.codex/hooks.json" ]; then
echo "Installing Codex CLI hooks ..."
mkdir -p "$HOME/.codex"
python3 - "$SCRIPT_DIR" "$HOME/.codex/config.toml" << 'PYEOF'
import sys
from pathlib import Path
repo_root = sys.argv[1]
config_path = Path(sys.argv[2])
# Read existing config or start fresh
existing = {}
if config_path.exists():
for line in config_path.read_text().splitlines():
line = line.strip()
if line and not line.startswith("[") and not line.startswith("#") and "=" in line:
key, val = line.split("=", 1)
existing[key.strip()] = val.strip()
# Preserve model if set
model = existing.get("model", '"o4-mini"')
config = f'''model = {model}
check_for_update_on_startup = false
[features]
codex_hooks = true
[experimental_features]
multi_agent = true
multi_agent_v2 = true
[projects."{repo_root}"]
trust_level = "trusted"
'''
config_path.write_text(config)
print(f" config.toml -> {config_path}")
print(f" hooks.json -> {repo_root}/.codex/hooks.json (project-level, travels with repo)")
PYEOF
else
echo "Skipping Codex hooks (no .codex/hooks.json found in repo)"
fi
else
echo "Skipping Codex CLI (not installed)"
fi
# --- Install Gemini CLI hooks ---
if command -v gemini &>/dev/null; then
if [ -d "$SCRIPT_DIR/.gemini/hooks" ]; then
echo "Installing Gemini CLI hooks ..."
GEMINI_SETTINGS="$HOME/.gemini/settings.json"
mkdir -p "$HOME/.gemini"
python3 - "$SCRIPT_DIR" "$GEMINI_SETTINGS" << 'PYEOF'
import json
import sys
from pathlib import Path
repo_root = sys.argv[1]
settings_path = Path(sys.argv[2])
hooks_dir = f"{repo_root}/.gemini/hooks"
# Load existing settings or start fresh
if settings_path.exists():
settings = json.loads(settings_path.read_text())
else:
settings = {}
# Build hooks config with absolute paths (Gemini uses different event names)
settings["hooks"] = {
"SessionStart": [
{"hooks": [{"type": "command", "command": f"python3 {hooks_dir}/session_start_identity.py", "timeout": 10}]}
],
"BeforeModel": [
{"hooks": [{"type": "command", "command": f"python3 {hooks_dir}/prompt_inject.py", "timeout": 10}]}
],
"BeforeTool": [
{"matcher": "Edit|Write",
"hooks": [{"type": "command", "command": f"python3 {hooks_dir}/pre_edit_gate.py", "timeout": 5}]}
],
}
settings_path.write_text(json.dumps(settings, indent=2) + "\n")
print(f" hooks -> {settings_path}")
PYEOF
else
echo "Skipping Gemini hooks (no .gemini/hooks/ directory found in repo)"
fi
else
echo "Skipping Gemini CLI (not installed)"
fi
# --- Create global symlinks for CLI tools ---
echo ""
echo "Creating global symlinks ..."
@@ -370,6 +467,11 @@ if [ "$FAIL" -eq 0 ]; then
echo "seedgo is accessed via: drone @seedgo"
echo "No venv activation needed for CLI commands."
echo ""
echo "CLI integrations:"
echo " Claude Code: hooks installed to ~/.claude/settings.json"
command -v codex &>/dev/null && echo " Codex CLI: hooks at .codex/hooks.json + config at ~/.codex/config.toml"
command -v gemini &>/dev/null && echo " Gemini CLI: hooks installed to ~/.gemini/settings.json"
echo ""
else
echo "=== Setup finished with errors ==="
echo "The venv was created and the package was installed, but one or more"