diff --git a/.codex/hooks.json b/.codex/hooks.json new file mode 100644 index 00000000..96b6cd63 --- /dev/null +++ b/.codex/hooks.json @@ -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 + } + ] + } + ] + } +} diff --git a/.codex/hooks/pre_edit_gate.py b/.codex/hooks/pre_edit_gate.py new file mode 100755 index 00000000..bc790ee8 --- /dev/null +++ b/.codex/hooks/pre_edit_gate.py @@ -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() diff --git a/.codex/hooks/prompt_inject.py b/.codex/hooks/prompt_inject.py new file mode 100755 index 00000000..29173917 --- /dev/null +++ b/.codex/hooks/prompt_inject.py @@ -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() diff --git a/.codex/hooks/session_start_identity.py b/.codex/hooks/session_start_identity.py new file mode 100755 index 00000000..3a4fe6cf --- /dev/null +++ b/.codex/hooks/session_start_identity.py @@ -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() diff --git a/.codex/skills/memo/SKILL.md b/.codex/skills/memo/SKILL.md new file mode 100644 index 00000000..558fffaf --- /dev/null +++ b/.codex/skills/memo/SKILL.md @@ -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. diff --git a/.codex/skills/prep/SKILL.md b/.codex/skills/prep/SKILL.md new file mode 100644 index 00000000..ffd98e50 --- /dev/null +++ b/.codex/skills/prep/SKILL.md @@ -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. +``` diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..11f63545 --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/setup.sh b/setup.sh index 6063ad0b..c4f02fa9 100755 --- a/setup.sh +++ b/setup.sh @@ -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"