chore: remove aipass init pollution from devpulse
This commit is contained in:
@@ -1,57 +0,0 @@
|
||||
# Session Wrap-Up
|
||||
|
||||
Purpose: Button up everything at the end of a session — or before a /compact. Memories, plans, git — all tidy. Works for both closing out a chat and preparing for compaction.
|
||||
|
||||
**Workflow:** `/prep` → review output → close chat or `/compact`
|
||||
|
||||
## 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
|
||||
|
||||
Each memory file plays a distinct role. Update based on what actually changed this session.
|
||||
|
||||
- **`.trinity/passport.json`** — IDENTITY. Who you are: role, capabilities, principles. Only update if identity genuinely evolved this session.
|
||||
- **`.trinity/local.json`** — YOUR MEMORY. Add/update session entry with a summary of work done. Add key_learnings for anything learned. Trim oldest sessions if over 20.
|
||||
- **`.trinity/observations.json`** — YOUR MEMORY OF THE USER. Collaboration insights, preferences, friction points. Skip if nothing new about the user this session.
|
||||
- **`STATUS.local.md`** — PUBLIC STATUS BEACON. Current work, known issues, todos, notepad. Auto-synced to central STATUS.md on PR events — this is how other branches see you. Keep Current Work accurate.
|
||||
|
||||
## 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 (e.g., agent IDs needed for resume), write it to STATUS.local.md Notepad
|
||||
|
||||
## Confirm
|
||||
|
||||
List everything updated. Format:
|
||||
```
|
||||
Prep complete:
|
||||
- local.json: [what was added]
|
||||
- observations.json: [updated / skipped]
|
||||
- STATUS.local.md: [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.
|
||||
```
|
||||
@@ -1,56 +0,0 @@
|
||||
# Project-Level Hooks
|
||||
|
||||
These hooks are provisioned by `aipass init` and live in the project's
|
||||
`.claude/settings.json`. They fire when CWD is inside this project.
|
||||
|
||||
## What fires and what doesn't
|
||||
|
||||
**UserPromptSubmit** hooks fire from project settings. These work:
|
||||
- `branch_prompt_loader.py` — injects branch-specific prompt
|
||||
- `email_notification.py` — shows unread email count
|
||||
- `identity_injector.py` — injects branch identity from passport
|
||||
|
||||
**PreToolUse / PostToolUse** hooks are provisioned but **DO NOT FIRE** from
|
||||
project-level settings. This is a Claude Code limitation (confirmed S122,
|
||||
GitHub issue #36071). These scripts exist but are dead weight:
|
||||
- `pre_edit_gate.py` — intended to block cross-branch writes (never runs)
|
||||
- `auto_fix_diagnostics.py` — intended to run pyright+ruff (never runs)
|
||||
- `subagent_stop_gate.py` — intended to check subagent files (never runs)
|
||||
|
||||
These same hooks DO fire from provider settings (`~/.claude/settings.json`)
|
||||
where they are also wired. The provider copies handle all enforcement.
|
||||
|
||||
**PreCompact** hooks fire from project settings:
|
||||
- `pre_compact.py` — injects recovery context after compaction
|
||||
|
||||
## CWD guard interaction
|
||||
|
||||
When this project has UserPromptSubmit hooks (it does), the provider-level
|
||||
UserPromptSubmit hooks detect this and exit silently. This prevents the AIPass
|
||||
global prompt from being injected into projects that manage their own context.
|
||||
|
||||
The provider-level PreToolUse/PostToolUse hooks still fire (they can only run
|
||||
at provider level) — so enforcement (git_gate, pre_edit_gate, auto_fix) is
|
||||
always active regardless of CWD.
|
||||
|
||||
## Testing
|
||||
|
||||
Provider-level test harness covers project-level behavior:
|
||||
```bash
|
||||
python3 $AIPASS_HOME/.claude/hooks/hook_test.py --direct
|
||||
```
|
||||
|
||||
Tests include:
|
||||
- `direct_provider_guards_for_init_project` — verifies provider hooks are
|
||||
CWD-guarded when run from an aipass init project
|
||||
- `direct_project_settings_schema` — validates project settings.json has
|
||||
expected hooks and all referenced scripts exist
|
||||
|
||||
## Updating hooks
|
||||
|
||||
```bash
|
||||
drone @cli aipass init update # Refresh managed project files to latest templates
|
||||
```
|
||||
|
||||
## Related
|
||||
See `$AIPASS_HOME/.claude/hooks/README.md` for the full hook system documentation.
|
||||
@@ -1,366 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PostToolUse Auto-fix Hook — Detects errors and surfaces them for fixing.
|
||||
|
||||
Two-hook system:
|
||||
PostToolUse (this file) → runs pyright + ruff on edited file, saves errors to state
|
||||
PreToolUse (pre_edit_gate.py) → blocks edits to OTHER files until errors fixed
|
||||
|
||||
Key behaviors:
|
||||
- Runs py_compile (syntax), ruff lint+format, pyright (type errors) on edited file
|
||||
- Runs seedgo checklist for AIPass standards
|
||||
- Saves ruff lint AND pyright errors to state file for PreToolUse gate (hard block)
|
||||
- Surfaces ALL errors in additionalContext so Claude sees them
|
||||
|
||||
Version: 5.2.0
|
||||
|
||||
CHANGELOG:
|
||||
- v5.2.0 (2026-04-20): Save ruff lint errors to state file for hard-block enforcement.
|
||||
Pre-edit gate now blocks on F401/lint just like type errors.
|
||||
- v5.1.0 (2026-04-19): Added ruff format --check to surface format drift.
|
||||
- v5.0.0 (2026-03-17): Replaced mcp__ide__getDiagnostics with direct pyright.
|
||||
Added state file for PreToolUse gate integration.
|
||||
Single-file pyright (not whole project).
|
||||
- v4.3.0 (2026-03-17): Added seedgo checklist integration
|
||||
- v4.0.0 (2025-11-27): Complete rewrite - actual validation, silent operation
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
EDIT_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit"]
|
||||
LAST_FILE_PATH = Path(__file__).parent / ".last_diagnostics_file"
|
||||
STATE_FILE = Path(__file__).parent / ".diagnostics_state.json"
|
||||
SKIP_EXTENSIONS = {".md", ".txt", ".log", ".csv", ".html"}
|
||||
|
||||
# AIPass-specific Python patterns to check
|
||||
PYTHON_PATTERNS = {
|
||||
"bad_optional": {"pattern": ": str = None", "message": "Optional param should use 'str | None = None' pattern"},
|
||||
"logger_debug": {
|
||||
"pattern": "logger.debug(",
|
||||
"message": "Use logger.info for SystemLogger (logger.debug not supported)",
|
||||
},
|
||||
"return_error_msg": {
|
||||
"pattern": "return error_msg",
|
||||
"message": "Return None for error states, not error_msg string",
|
||||
},
|
||||
"open_no_encoding": {
|
||||
"pattern": "open(",
|
||||
"requires_missing": "encoding=",
|
||||
"message": "open() without encoding='utf-8'",
|
||||
},
|
||||
"log_not_log_operation": {
|
||||
"pattern": ".log(",
|
||||
"message": "Use log_operation() with success/error params, not .log()",
|
||||
},
|
||||
"dict_none_no_check": {
|
||||
"pattern": "Dict | None",
|
||||
"message": "Dict | None return: Add None check before using (if result is None: return)",
|
||||
},
|
||||
}
|
||||
|
||||
# JSON-specific patterns for emoji corruption
|
||||
JSON_CORRUPTION_CHARS = ["\ufffd", "\x00"]
|
||||
|
||||
|
||||
def run_python_checks(file_path: str) -> list[str]:
|
||||
"""Run actual Python validation - returns list of errors."""
|
||||
errors = []
|
||||
|
||||
# 1. Syntax check with py_compile
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "py_compile", file_path], capture_output=True, text=True, timeout=5
|
||||
)
|
||||
if result.returncode != 0:
|
||||
errors.append(f"SYNTAX: {result.stderr.strip()}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. Ruff check (if available) - fast linter
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ruff", "check", "--select=E,F,W", "--output-format=text", file_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
if result.stdout.strip():
|
||||
for line in result.stdout.strip().split("\n")[:5]:
|
||||
errors.append(f"LINT: {line}")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 3. Ruff format check — detect format drift
|
||||
try:
|
||||
result = subprocess.run(["ruff", "format", "--check", file_path], capture_output=True, text=True, timeout=10)
|
||||
if result.returncode != 0:
|
||||
errors.append(f"FORMAT: {Path(file_path).name} needs ruff format (run: ruff format {Path(file_path).name})")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 4. AIPass-specific pattern checks
|
||||
try:
|
||||
content = Path(file_path).read_text(encoding="utf-8")
|
||||
lines = content.split("\n")
|
||||
|
||||
for check in PYTHON_PATTERNS.values():
|
||||
pattern = check["pattern"]
|
||||
message = check["message"]
|
||||
requires_missing = check.get("requires_missing")
|
||||
|
||||
if requires_missing:
|
||||
if pattern in content and requires_missing not in content:
|
||||
errors.append(f"PATTERN: {message}")
|
||||
continue
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith(("#", '"', "'")):
|
||||
continue
|
||||
if f'"{pattern}' in line or f"'{pattern}" in line:
|
||||
continue
|
||||
if pattern in line:
|
||||
errors.append(f"PATTERN: {message}")
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def run_ruff_lint_structured(file_path: str) -> list[dict]:
|
||||
"""Run ruff check and return structured violations for the state file.
|
||||
|
||||
Returns list of {line, message} dicts — same format as pyright errors.
|
||||
Only non-empty when ruff finds real violations (not format drift).
|
||||
"""
|
||||
if "/.claude/hooks/" in file_path:
|
||||
return []
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ruff", "check", "--select=E,F,W", "--output-format=json", file_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
if not result.stdout.strip():
|
||||
return []
|
||||
violations = json.loads(result.stdout)
|
||||
if not isinstance(violations, list):
|
||||
return []
|
||||
errors = []
|
||||
for v in violations[:10]:
|
||||
line = v.get("location", {}).get("row", 0)
|
||||
code = v.get("code", "?")
|
||||
message = v.get("message", "unknown")[:100]
|
||||
errors.append({"line": line, "message": f"{code}: {message}"})
|
||||
return errors
|
||||
except (FileNotFoundError, json.JSONDecodeError, subprocess.TimeoutExpired, Exception):
|
||||
return []
|
||||
|
||||
|
||||
def run_pyright_check(file_path: str) -> list[dict]:
|
||||
"""Run pyright on a single file. Returns list of error dicts."""
|
||||
# Skip hook files - they don't follow project standards
|
||||
if "/.claude/hooks/" in file_path:
|
||||
return []
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "pyright", "--outputjson", file_path], capture_output=True, text=True, timeout=15
|
||||
)
|
||||
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return []
|
||||
|
||||
errors = []
|
||||
for diag in data.get("generalDiagnostics", []):
|
||||
severity = diag.get("severity", "")
|
||||
if severity == "error":
|
||||
line = diag.get("range", {}).get("start", {}).get("line", 0)
|
||||
message = diag.get("message", "Unknown error")
|
||||
errors.append({"line": line, "message": message[:100]})
|
||||
|
||||
return errors[:10] # Max 10 errors
|
||||
|
||||
except FileNotFoundError:
|
||||
return [] # pyright not installed
|
||||
except subprocess.TimeoutExpired:
|
||||
return [] # Timeout — don't block
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def save_diagnostics_state(file_path: str, errors: list[dict]):
|
||||
"""Save type errors to state file for PreToolUse gate."""
|
||||
try:
|
||||
if errors:
|
||||
state = {"file": str(Path(file_path).resolve()), "errors": errors}
|
||||
STATE_FILE.write_text(json.dumps(state), encoding="utf-8")
|
||||
else:
|
||||
# No errors — clear the state
|
||||
if STATE_FILE.exists():
|
||||
STATE_FILE.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def run_json_checks(file_path: str) -> list[str]:
|
||||
"""Run actual JSON validation - returns list of errors."""
|
||||
errors = []
|
||||
|
||||
try:
|
||||
content = Path(file_path).read_text(encoding="utf-8")
|
||||
|
||||
for char in JSON_CORRUPTION_CHARS:
|
||||
if char in content:
|
||||
errors.append(f"EMOJI CORRUPTION: Found corrupted character '{repr(char)}'")
|
||||
break
|
||||
|
||||
try:
|
||||
data = json.loads(content)
|
||||
|
||||
if isinstance(data, dict):
|
||||
for key in ["allowed_emojis", "emojis", "emoji_list"]:
|
||||
if key in data and isinstance(data[key], list):
|
||||
for item in data[key]:
|
||||
if isinstance(item, str) and len(item) == 1:
|
||||
if ord(item) < 128 and item not in "\u2713\u2717":
|
||||
errors.append(f"EMOJI CORRUPTION: Suspicious char '{item}' in {key}")
|
||||
break
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
errors.append(f"JSON SYNTAX: {e.msg} at line {e.lineno}")
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"READ ERROR: {e!s}")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def run_seedgo_checklist(file_path: str) -> list[str]:
|
||||
"""Run seedgo standards checklist — returns violations only."""
|
||||
if "/.claude/hooks/" in file_path:
|
||||
return []
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["drone", "@seedgo", "checklist", file_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
cwd=str(Path.home() / "Projects" / "AIPass"),
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return []
|
||||
|
||||
violations = []
|
||||
for line in result.stdout.split("\n"):
|
||||
line = line.strip()
|
||||
if line.startswith("\u2717"):
|
||||
violation = line[1:].strip()
|
||||
if violation:
|
||||
violations.append(violation)
|
||||
|
||||
return violations[:5]
|
||||
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def should_skip_file(file_path: str) -> bool:
|
||||
"""Check if file should be skipped."""
|
||||
if not file_path:
|
||||
return True
|
||||
ext = Path(file_path).suffix.lower()
|
||||
return ext in SKIP_EXTENSIONS
|
||||
|
||||
|
||||
def is_same_file_as_last(file_path: str) -> bool:
|
||||
"""Smart batching DISABLED — always recheck.
|
||||
|
||||
Previously skipped rechecks on the same file, but this caused
|
||||
errors introduced on second edit to be missed (state file didn't
|
||||
exist from first clean edit, so skip triggered). The 1.7s pyright
|
||||
cost per edit is acceptable for correctness.
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Main hook entry point."""
|
||||
try:
|
||||
input_data = json.load(sys.stdin)
|
||||
tool_name = input_data.get("tool_name", "")
|
||||
tool_input = input_data.get("tool_input", {})
|
||||
file_path = tool_input.get("file_path", "")
|
||||
|
||||
if tool_name not in EDIT_TOOLS:
|
||||
return
|
||||
|
||||
if should_skip_file(file_path):
|
||||
return
|
||||
|
||||
if is_same_file_as_last(file_path):
|
||||
return
|
||||
|
||||
# Collect all errors
|
||||
errors = []
|
||||
|
||||
if file_path.endswith(".py"):
|
||||
errors = run_python_checks(file_path)
|
||||
|
||||
# Seedgo standards checklist
|
||||
seedgo_violations = run_seedgo_checklist(file_path)
|
||||
for v in seedgo_violations:
|
||||
errors.append(f"SEEDGO: {v}")
|
||||
|
||||
# Pyright type errors (single file)
|
||||
type_errors = run_pyright_check(file_path)
|
||||
for te in type_errors:
|
||||
errors.append(f"TYPE: L{te['line']}: {te['message']}")
|
||||
|
||||
# Save ruff lint + type errors to state file for PreToolUse gate (hard block)
|
||||
ruff_lint_errors = run_ruff_lint_structured(file_path)
|
||||
save_diagnostics_state(file_path, ruff_lint_errors + type_errors)
|
||||
|
||||
elif file_path.endswith(".json"):
|
||||
errors = run_json_checks(file_path)
|
||||
else:
|
||||
return
|
||||
|
||||
# Build output
|
||||
if errors:
|
||||
error_text = "\n".join(f" - {e}" for e in errors)
|
||||
context = f"""[AUTO-FIX] {len(errors)} error(s) in {Path(file_path).name}:
|
||||
{error_text}
|
||||
|
||||
Fix these errors in {Path(file_path).name} now. Do not skip or defer."""
|
||||
|
||||
output = {
|
||||
"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": context},
|
||||
"systemMessage": f"[AUTO-FIX] {len(errors)} error(s) — fix before continuing",
|
||||
}
|
||||
print(json.dumps(output))
|
||||
else:
|
||||
output = {"systemMessage": "[diagnostics] ok"}
|
||||
print(json.dumps(output))
|
||||
|
||||
except Exception:
|
||||
pass # Silent fail
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Branch Prompt Loader — AIPass Public Repo
|
||||
|
||||
Injects branch-specific prompts based on CWD. When working in a branch
|
||||
directory, loads .aipass/aipass_local_prompt.md and outputs it so the
|
||||
AI sees branch-specific context.
|
||||
|
||||
Version: 1.0.0
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def find_branch_root() -> Path | None:
|
||||
"""
|
||||
Find the branch root directory.
|
||||
Looks for .trinity/ or .aipass/ as branch indicators.
|
||||
Stops at the repo root (has pyproject.toml or .git).
|
||||
"""
|
||||
cwd = Path.cwd()
|
||||
search_path = cwd
|
||||
|
||||
while search_path.parent != search_path:
|
||||
# Branch indicators: has .trinity/ (memory files) or apps/ (code)
|
||||
has_trinity = (search_path / ".trinity").is_dir()
|
||||
has_apps = (search_path / "apps").is_dir()
|
||||
|
||||
if has_trinity or has_apps:
|
||||
return search_path
|
||||
|
||||
# Stop at repo root
|
||||
if (search_path / "pyproject.toml").exists() or (search_path / ".git").is_dir():
|
||||
return None
|
||||
|
||||
search_path = search_path.parent
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
branch_root = find_branch_root()
|
||||
|
||||
if branch_root:
|
||||
prompt_file = branch_root / ".aipass" / "aipass_local_prompt.md"
|
||||
if prompt_file.exists():
|
||||
content = prompt_file.read_text().strip()
|
||||
branch_name = branch_root.name.upper()
|
||||
print(f"\n# Branch Context: {branch_name}\n<!-- Source: {prompt_file} -->\n{content}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Email Notification Hook - Notifies of new emails on prompt submit.
|
||||
|
||||
Checks the current branch's inbox for unread emails and displays
|
||||
a notification if any exist.
|
||||
|
||||
Version: 1.0.0
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def find_repo_root() -> Path | None:
|
||||
"""Find the repo root (contains pyproject.toml or .git)."""
|
||||
search = Path.cwd()
|
||||
while search.parent != search:
|
||||
if (search / "pyproject.toml").exists() or (search / ".git").is_dir():
|
||||
return search
|
||||
search = search.parent
|
||||
return None
|
||||
|
||||
|
||||
def find_branch_root() -> Path | None:
|
||||
"""Find the branch root directory by walking up from CWD."""
|
||||
cwd = Path.cwd()
|
||||
repo_root = find_repo_root()
|
||||
if not repo_root:
|
||||
return None
|
||||
|
||||
search_path = cwd
|
||||
for _ in range(10):
|
||||
has_trinity = (search_path / ".trinity").is_dir()
|
||||
has_id = list(search_path.glob("*.id.json"))
|
||||
has_apps = (search_path / "apps").is_dir()
|
||||
has_mail = (search_path / ".ai_mail.local").is_dir() or (search_path / "ai_mail.local").is_dir()
|
||||
|
||||
if (has_trinity or has_id or has_apps or has_mail) and search_path != repo_root:
|
||||
return search_path
|
||||
|
||||
if search_path == repo_root:
|
||||
break
|
||||
|
||||
parent = search_path.parent
|
||||
if parent == search_path:
|
||||
break
|
||||
search_path = parent
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def count_new_emails(branch_root: Path) -> int:
|
||||
"""Count new (unread) emails in the branch's inbox."""
|
||||
# Check both patterns: .ai_mail.local (canonical) and ai_mail.local (legacy)
|
||||
inbox_path = branch_root / ".ai_mail.local" / "inbox.json"
|
||||
if not inbox_path.exists():
|
||||
inbox_path = branch_root / "ai_mail.local" / "inbox.json"
|
||||
|
||||
if not inbox_path.exists():
|
||||
return 0
|
||||
|
||||
try:
|
||||
with open(inbox_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Handle both formats: {"messages": [...]} and bare [...]
|
||||
messages = data if isinstance(data, list) else data.get("messages", [])
|
||||
count = 0
|
||||
for msg in messages:
|
||||
if msg.get("status") == "new":
|
||||
count += 1
|
||||
elif msg.get("status") is None and not msg.get("read", False):
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
branch_root = find_branch_root()
|
||||
if not branch_root:
|
||||
return
|
||||
|
||||
new_count = count_new_emails(branch_root)
|
||||
if new_count > 0:
|
||||
plural = "s" if new_count != 1 else ""
|
||||
print(
|
||||
f"You have {new_count} new email{plural} - check with: drone @ai_mail inbox | then: drone @ai_mail view <id> | close with: drone @ai_mail close <id>"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,118 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Identity Injector - Injects branch identity on every prompt.
|
||||
|
||||
Reads from [BRANCH].id.json and outputs core identity fields.
|
||||
Finds the branch root by walking up from CWD looking for apps/ or *.id.json.
|
||||
|
||||
Version: 1.0.0
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def find_repo_root() -> Path | None:
|
||||
"""Find the repo root (contains pyproject.toml or .git)."""
|
||||
search = Path.cwd()
|
||||
while search.parent != search:
|
||||
if (search / "pyproject.toml").exists() or (search / ".git").is_dir():
|
||||
return search
|
||||
search = search.parent
|
||||
return None
|
||||
|
||||
|
||||
def find_branch_root() -> Path | None:
|
||||
"""Find the branch root directory by walking up from CWD."""
|
||||
cwd = Path.cwd()
|
||||
repo_root = find_repo_root()
|
||||
if not repo_root:
|
||||
return None
|
||||
|
||||
search_path = cwd
|
||||
while search_path >= repo_root:
|
||||
has_trinity = (search_path / ".trinity").is_dir()
|
||||
has_id = list(search_path.glob("*.id.json"))
|
||||
|
||||
if has_trinity or has_id:
|
||||
return search_path
|
||||
|
||||
if search_path == repo_root:
|
||||
break
|
||||
search_path = search_path.parent
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_id_file(branch_root: Path) -> Path | None:
|
||||
"""Find the identity file for a branch (.trinity/passport.json or *.id.json)."""
|
||||
# AIPass pattern: .trinity/passport.json
|
||||
passport = branch_root / ".trinity" / "passport.json"
|
||||
if passport.exists():
|
||||
return passport
|
||||
# Dev-Pass fallback: *.id.json
|
||||
id_files = list(branch_root.glob("*.id.json"))
|
||||
if id_files:
|
||||
return id_files[0]
|
||||
return None
|
||||
|
||||
|
||||
def format_identity(data: dict) -> str:
|
||||
"""Format branch_info + identity for injection."""
|
||||
lines = []
|
||||
|
||||
# Try branch_info first (enriched passports), fall back to identity block (setup.sh passports)
|
||||
branch = data.get("branch_info", {})
|
||||
identity = data.get("identity", {})
|
||||
name = branch.get("branch_name") or identity.get("name", "UNKNOWN")
|
||||
lines.append(f"# {name} Identity")
|
||||
lines.append(f"Path: {branch.get('path', 'unknown')}")
|
||||
lines.append(f"Email: {branch.get('email', 'unknown')}")
|
||||
|
||||
identity = data.get("identity", {})
|
||||
if identity.get("role"):
|
||||
lines.append(f"Role: {identity['role']}")
|
||||
traits = identity.get("traits") or data.get("traits")
|
||||
if traits:
|
||||
if isinstance(traits, list):
|
||||
lines.append("Traits: " + " | ".join(traits))
|
||||
else:
|
||||
lines.append(f"Traits: {traits}")
|
||||
if identity.get("purpose"):
|
||||
lines.append(f"Purpose: {identity['purpose']}")
|
||||
|
||||
what_i_do = identity.get("what_i_do", [])
|
||||
if what_i_do:
|
||||
lines.append("Do: " + " | ".join(what_i_do[:4]))
|
||||
|
||||
what_i_dont_do = identity.get("what_i_dont_do", [])
|
||||
if what_i_dont_do:
|
||||
lines.append("Don't: " + " | ".join(what_i_dont_do[:3]))
|
||||
|
||||
principles = data.get("principles", [])
|
||||
if principles:
|
||||
lines.append("Principles: " + " * ".join(principles))
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
branch_root = find_branch_root()
|
||||
if not branch_root:
|
||||
return
|
||||
|
||||
id_file = find_id_file(branch_root)
|
||||
if not id_file or not id_file.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
data = json.loads(id_file.read_text(encoding="utf-8"))
|
||||
output = format_identity(data)
|
||||
if output:
|
||||
print(f"\n{output}")
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,168 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pre-Compact Hook - Inject live state for post-compact recovery.
|
||||
|
||||
Reads STATUS.local.md, last session from local.json, and git branch
|
||||
to give the model real context after compaction — not generic advice.
|
||||
|
||||
Version: 3.0.0
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _find_branch_dir():
|
||||
"""Find the current branch directory from CWD."""
|
||||
cwd = Path.cwd()
|
||||
|
||||
# Check if we're in a branch dir or subdirectory of one
|
||||
# Pattern: .../src/aipass/{branch}/...
|
||||
parts = cwd.parts
|
||||
for i, part in enumerate(parts):
|
||||
if part == "aipass" and i > 0 and parts[i - 1] == "src":
|
||||
branch_dir = Path(*parts[: i + 2])
|
||||
if branch_dir.is_dir():
|
||||
return branch_dir
|
||||
|
||||
# Check if CWD itself has .trinity/
|
||||
if (cwd / ".trinity").is_dir():
|
||||
return cwd
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _read_status_local(branch_dir):
|
||||
"""Read STATUS.local.md if it exists."""
|
||||
for name in ["STATUS.local.md", "dev.local.md"]:
|
||||
path = branch_dir / name
|
||||
if path.is_file():
|
||||
try:
|
||||
return path.read_text(encoding="utf-8")[:3000]
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _read_last_session(branch_dir):
|
||||
"""Read the most recent session and key_learnings from local.json."""
|
||||
local_path = branch_dir / ".trinity" / "local.json"
|
||||
if not local_path.is_file():
|
||||
return None
|
||||
|
||||
try:
|
||||
data = json.loads(local_path.read_text(encoding="utf-8"))
|
||||
result = []
|
||||
|
||||
# Last session
|
||||
sessions = data.get("sessions", [])
|
||||
if sessions:
|
||||
last = sessions[0]
|
||||
result.append(
|
||||
f"Last session (#{last.get('session_number', '?')}, "
|
||||
f"{last.get('date', '?')}): {last.get('summary', 'no summary')}"
|
||||
)
|
||||
|
||||
# Key learnings (just the keys, not full values — breadcrumbs)
|
||||
learnings = data.get("key_learnings", {})
|
||||
if learnings:
|
||||
keys = list(learnings.keys())[-10:] # last 10
|
||||
result.append(f"Key learnings available: {', '.join(keys)}")
|
||||
|
||||
return "\n".join(result) if result else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _get_git_info():
|
||||
"""Get current git branch and short status."""
|
||||
try:
|
||||
branch = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "diff", "--stat", "--cached", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
dirty = subprocess.run(
|
||||
["git", "status", "--porcelain"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
result = []
|
||||
if branch.returncode == 0:
|
||||
result.append(f"Git branch: {branch.stdout.strip()}")
|
||||
if dirty.returncode == 0 and dirty.stdout.strip():
|
||||
lines = dirty.stdout.strip().split("\n")
|
||||
result.append(f"Uncommitted changes: {len(lines)} files")
|
||||
|
||||
return "\n".join(result) if result else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _get_branch_name(branch_dir):
|
||||
"""Extract branch name from directory."""
|
||||
return branch_dir.name if branch_dir else "unknown"
|
||||
|
||||
|
||||
def main():
|
||||
"""Main hook entry point."""
|
||||
try:
|
||||
json.load(sys.stdin)
|
||||
|
||||
branch_dir = _find_branch_dir()
|
||||
branch_name = _get_branch_name(branch_dir)
|
||||
|
||||
sections = []
|
||||
|
||||
sections.append(f"""POST-COMPACT RECOVERY — @{branch_name}
|
||||
|
||||
Context just compacted. Below is your live state. Use it to continue seamlessly.""")
|
||||
|
||||
# Git info
|
||||
git_info = _get_git_info()
|
||||
if git_info:
|
||||
sections.append(f"## Git\n{git_info}")
|
||||
|
||||
# Last session from local.json
|
||||
if branch_dir:
|
||||
session_info = _read_last_session(branch_dir)
|
||||
if session_info:
|
||||
sections.append(f"## Last Session\n{session_info}")
|
||||
|
||||
# STATUS.local.md — the main context
|
||||
if branch_dir:
|
||||
status = _read_status_local(branch_dir)
|
||||
if status:
|
||||
sections.append(f"## STATUS.local.md\n{status}")
|
||||
|
||||
# Recovery instructions (lean)
|
||||
sections.append("""## Recovery Protocol
|
||||
- Continue where the summary left off — don't restart or ask generic questions
|
||||
- .trinity/local.json has full session history and key_learnings — read it if you need more context
|
||||
- STATUS.local.md has current work, known issues, and todos
|
||||
- Save memories proactively — compaction just proved you need to
|
||||
- Match the conversation tone from before compaction""")
|
||||
|
||||
print("\n\n".join(sections), file=sys.stdout)
|
||||
print("Pre-compact: live state injected", file=sys.stderr)
|
||||
|
||||
except Exception as e:
|
||||
# Fail silently — never block compaction
|
||||
print(f"Pre-compact hook error: {e}", file=sys.stderr)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,149 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PreToolUse Gate — Blocks unsafe edits at the hook layer.
|
||||
|
||||
Rules (checked in order):
|
||||
1. Inbox lock — any write targeting *.ai_mail.local/inbox.json is BLOCKED.
|
||||
Use `drone @ai_mail email` instead.
|
||||
2. Cross-branch — writes to src/aipass/X/** from a CWD inside src/aipass/Y/**
|
||||
are BLOCKED unless the calling branch is in TRUSTED_CROSS_WRITERS.
|
||||
3. State-file — edits to OTHER .py files while the current branch has unresolved
|
||||
type errors are BLOCKED. (original v1.2.0 logic)
|
||||
|
||||
Track E additions: rules 1 + 2 (DPLAN-0139).
|
||||
Version: 1.3.0
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
STATE_FILE = Path(__file__).parent / ".diagnostics_state.json"
|
||||
EDIT_TOOLS = {"Edit", "Write", "MultiEdit", "NotebookEdit"}
|
||||
|
||||
# Single source of truth lives in permissions.py — inline here as fallback
|
||||
# so the hook works even when aipass package is not on sys.path.
|
||||
TRUSTED_CROSS_WRITERS: tuple[str, ...] = ("devpulse", "seedgo", "spawn")
|
||||
|
||||
|
||||
def _get_branch(file_path: str) -> str:
|
||||
"""Extract AIPass branch name from a file path (src/aipass/{branch}/ pattern)."""
|
||||
parts = Path(file_path).parts
|
||||
for i, part in enumerate(parts):
|
||||
if part == "aipass" and i > 0 and parts[i - 1] == "src" and i + 1 < len(parts):
|
||||
return parts[i + 1]
|
||||
return ""
|
||||
|
||||
|
||||
def _block(reason: str) -> None:
|
||||
# codeql[py/clear-text-logging-sensitive-data]
|
||||
print(json.dumps({"decision": "block", "reason": reason}))
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
input_data = json.load(sys.stdin)
|
||||
tool_name = input_data.get("tool_name", "")
|
||||
tool_input = input_data.get("tool_input", {})
|
||||
file_path = tool_input.get("file_path", "")
|
||||
|
||||
if tool_name not in EDIT_TOOLS:
|
||||
return
|
||||
|
||||
if not file_path:
|
||||
return
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Rule 1: Inbox lock — block all writes to *.ai_mail.local/inbox.json
|
||||
# ------------------------------------------------------------------
|
||||
fp = Path(file_path)
|
||||
if fp.name == "inbox.json" and ".ai_mail.local" in fp.parts:
|
||||
_block('Direct writes to inbox.json are blocked.\nUse: drone @ai_mail email @<branch> "Subject" "Body"')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Rule 1.5: Dispatched-agent path confinement (DPLAN-0155 M3)
|
||||
# Daemon-spawned agents can only write inside their own branch dir.
|
||||
# Breaks the prompt-injection amplifier chain — even if injected,
|
||||
# a dispatched agent cannot write to other agents' inboxes or code.
|
||||
# ------------------------------------------------------------------
|
||||
cwd = input_data.get("cwd", "") or os.getcwd()
|
||||
cwd_branch = _get_branch(cwd)
|
||||
|
||||
session_type = os.environ.get("AIPASS_SESSION_TYPE", "interactive")
|
||||
if session_type == "daemon" and cwd_branch:
|
||||
target_branch = _get_branch(str(fp.resolve()) if not fp.is_absolute() else str(fp))
|
||||
if target_branch and target_branch != cwd_branch:
|
||||
_block(
|
||||
f"Dispatched agent confined to own branch: '{cwd_branch}' "
|
||||
f"cannot write to '{target_branch}' in daemon mode."
|
||||
)
|
||||
repo_root = None
|
||||
for parent in Path(cwd).parents:
|
||||
if (parent / ".git").exists():
|
||||
repo_root = parent
|
||||
break
|
||||
if repo_root and not target_branch:
|
||||
allowed_prefix = str(repo_root / "src" / "aipass" / cwd_branch)
|
||||
resolved = str(fp.resolve()) if not fp.is_absolute() else str(fp)
|
||||
if not resolved.startswith(allowed_prefix):
|
||||
_block(f"Dispatched agent restricted to {allowed_prefix}. Cannot write to: {file_path}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Rule 2: Cross-branch write enforcement
|
||||
# ------------------------------------------------------------------
|
||||
target_branch = _get_branch(str(fp.resolve()) if not fp.is_absolute() else str(fp))
|
||||
|
||||
if cwd_branch and target_branch and cwd_branch != target_branch:
|
||||
if cwd_branch not in TRUSTED_CROSS_WRITERS:
|
||||
_block(
|
||||
f"Cross-branch write blocked: '{cwd_branch}' cannot write to '{target_branch}'.\n"
|
||||
f"Trusted cross-writers: {', '.join(TRUSTED_CROSS_WRITERS)}"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Rule 3: State-file (original v1.2.0) — .py files only
|
||||
# ------------------------------------------------------------------
|
||||
if not file_path.endswith(".py"):
|
||||
return
|
||||
|
||||
if not STATE_FILE.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
state = json.loads(STATE_FILE.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, IOError):
|
||||
return
|
||||
|
||||
errored_file = state.get("file", "")
|
||||
errors = state.get("errors", [])
|
||||
|
||||
if not errors:
|
||||
return
|
||||
|
||||
try:
|
||||
current = str(Path(file_path).resolve())
|
||||
errored = str(Path(errored_file).resolve())
|
||||
except (OSError, ValueError):
|
||||
return
|
||||
|
||||
if current == errored:
|
||||
return
|
||||
|
||||
current_branch = _get_branch(current)
|
||||
errored_branch = _get_branch(errored)
|
||||
if not errored_branch:
|
||||
return
|
||||
if current_branch and errored_branch and current_branch != errored_branch:
|
||||
return
|
||||
|
||||
error_summary = "\n".join(f" L{e['line']}: {e['message']}" for e in errors[:5])
|
||||
_block(f"Fix {len(errors)} error(s) in {Path(errored_file).name} before editing other files:\n{error_summary}")
|
||||
|
||||
except Exception:
|
||||
pass # Silent fail → allow
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,114 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SubagentStop Gate — Checks files modified by subagents before allowing them to finish.
|
||||
|
||||
Runs seedgo checklist + basic validation on any .py files the subagent touched.
|
||||
If violations found, blocks the stop and tells the subagent to fix them.
|
||||
|
||||
Version: 1.0.0
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _find_repo_root() -> Path | None:
|
||||
"""Walk up from CWD or AIPASS_HOME to find the git repo root."""
|
||||
for start in (os.environ.get("AIPASS_HOME", ""), os.getcwd()):
|
||||
p = Path(start)
|
||||
while p != p.parent:
|
||||
if (p / ".git").exists():
|
||||
return p
|
||||
p = p.parent
|
||||
return None
|
||||
|
||||
|
||||
AIPASS_ROOT = _find_repo_root()
|
||||
|
||||
|
||||
def get_modified_py_files() -> list[str]:
|
||||
"""Get Python files modified in the working tree (unstaged + staged)."""
|
||||
if AIPASS_ROOT is None:
|
||||
return []
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--name-only", "HEAD"], capture_output=True, text=True, timeout=5, cwd=str(AIPASS_ROOT)
|
||||
)
|
||||
files = []
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
line = line.strip()
|
||||
if line.endswith(".py") and not line.startswith(".claude/"):
|
||||
full = AIPASS_ROOT / line
|
||||
if full.exists():
|
||||
files.append(str(full))
|
||||
return files
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def run_seedgo_checklist(file_path: str) -> list[str]:
|
||||
"""Run seedgo checklist on a single file."""
|
||||
if AIPASS_ROOT is None:
|
||||
return []
|
||||
if "/.claude/" in file_path:
|
||||
return []
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["drone", "@seedgo", "checklist", file_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
cwd=str(AIPASS_ROOT),
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return []
|
||||
violations = []
|
||||
for line in result.stdout.split("\n"):
|
||||
line = line.strip()
|
||||
if line.startswith("\u2717"):
|
||||
v = line[1:].strip()
|
||||
if v:
|
||||
violations.append(v)
|
||||
return violations[:5]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
json.load(sys.stdin)
|
||||
|
||||
modified = get_modified_py_files()
|
||||
if not modified:
|
||||
return # Nothing to check
|
||||
|
||||
all_violations = {}
|
||||
for f in modified:
|
||||
vs = run_seedgo_checklist(f)
|
||||
if vs:
|
||||
name = Path(f).name
|
||||
all_violations[name] = vs
|
||||
|
||||
if not all_violations:
|
||||
return # All clear
|
||||
|
||||
# Build the block reason
|
||||
lines = ["Standards violations found in files you modified:\n"]
|
||||
for fname, vs in all_violations.items():
|
||||
lines.append(f" {fname}:")
|
||||
for v in vs:
|
||||
lines.append(f" - {v}")
|
||||
lines.append("\nFix these violations before finishing.")
|
||||
|
||||
output = {"decision": "block", "reason": "\n".join(lines)}
|
||||
print(json.dumps(output))
|
||||
|
||||
except Exception:
|
||||
pass # Silent fail — don't block on errors
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 .claude/hooks/branch_prompt_loader.py"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 .claude/hooks/email_notification.py"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 .claude/hooks/identity_injector.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreCompact": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 .claude/hooks/pre_compact.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"env": {
|
||||
"AIPASS_HOME": "/home/patrick/Projects/AIPass"
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<!-- Source: /home/patrick/Projects/AIPass/src/aipass/devpulse/CLAUDE.md -->
|
||||
# DEVPULSE
|
||||
|
||||
**User:** (your name here)
|
||||
|
||||
## What is AIPass
|
||||
|
||||
AIPass is a multi-agent framework. This project was created with `aipass init`.
|
||||
|
||||
**Key concepts:**
|
||||
- **Project** — this directory. Contains a registry and one or more agents.
|
||||
- **Agent** — a citizen that lives inside the project. Has identity (`.trinity/`), memory, mailbox, and its own apps/ directory.
|
||||
- **Registry** — `DEVPULSE_REGISTRY.json` tracks all agents in this project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Create your first agent:
|
||||
```
|
||||
aipass init agent <name>
|
||||
```
|
||||
|
||||
This creates a full agent scaffold inside `src/<name>/` (`apps/`, `.trinity/`, `.ai_mail.local/`) and registers it in your project registry.
|
||||
|
||||
## Available Commands
|
||||
|
||||
```
|
||||
aipass init agent <name> # Create a new agent
|
||||
drone @spawn create <name> # Create agent (alternative)
|
||||
drone @seedgo audit <project> # Run standards audit
|
||||
drone @ai_mail inbox # Check mailbox (per-agent)
|
||||
drone systems # List all available infrastructure
|
||||
```
|
||||
|
||||
## Startup Protocol
|
||||
|
||||
On any greeting, silently read these files — no narration, just do it and respond with the status.
|
||||
|
||||
**Read:** `DEVPULSE_REGISTRY.json`, `README.md`, `STATUS.local.md`
|
||||
**Run:** `git status`
|
||||
|
||||
Then check the registry for agents and report status.
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"id": "6e3e877e-56ed-4ec5-892f-81073257dc90",
|
||||
"name": "DEVPULSE",
|
||||
"version": "1.0.0",
|
||||
"created": "2026-05-12",
|
||||
"last_updated": "2026-05-12",
|
||||
"total_branches": 0
|
||||
},
|
||||
"branches": []
|
||||
}
|
||||
Reference in New Issue
Block a user