Merge pull request #205 from AIOSAI/system/devpulse-s81-aipass-init-v2-real-templates-spawn-routing-re

feat(system): S81: aipass init v2 — real templates, spawn routing, README Quick Start
This commit is contained in:
AIPass
2026-04-09 00:22:53 -07:00
committed by GitHub
12 changed files with 1062 additions and 501 deletions
+29 -35
View File
@@ -48,20 +48,23 @@ AIPass is a local CLI framework that gives your AI agents **identity, memory, an
Your AI reads `.trinity/` on startup and writes back what it learned before the session ends. That's the whole memory model — JSON files your AI can read and write. Next session, it picks up where it left off. No database, no API, no setup beyond one command.
```bash
aipass init ~/Projects/my-saas-app
mkdir my-project && cd my-project
aipass init
```
Your project gets its own registry, its own identity, and persistent memory. Each project is isolated — its own agents, its own rules. No cross-contamination between projects.
**Add teammates when you need them:**
**Add agents when you need them:**
When one agent isn't enough, use `spawn` to create specialists with the same infrastructure AIPass runs on — communication, monitoring, standards, the whole scaffold.
```bash
aipass init agent my-agent # Full agent: apps, mail, memory, identity
```
| Start here | What to use | What you get |
|------------|-------------|-------------|
| One persistent agent | `aipass init` | Registry, passport, memory files, local prompt |
| A lightweight specialist | `spawn passport` | Identity + rich memory (no apps scaffold) |
| A full specialist | `spawn create` | All of the above + apps scaffold, mail, dashboard, tests |
| What you need | Command | What you get |
|---------------|---------|-------------|
| A new project | `aipass init` | Registry, project identity, prompts, hooks, docs |
| A full agent | `aipass init agent <name>` | Apps scaffold, mailbox, memory, identity — registered in project |
| A lightweight agent | `drone @spawn create <name> --template birthright` | Identity + memory only (no apps scaffold) |
**What makes this different:**
@@ -77,45 +80,36 @@ When one agent isn't enough, use `spawn` to create specialists with the same inf
## Quick Start
### Start your own project
```bash
pip install aipass # Install the package
pip install aipass
mkdir my-project && cd my-project
aipass init # Creates project: registry, prompts, hooks, docs
aipass init agent my-agent # Creates your first agent inside the project
cd my-agent
claude # Or: codex, gemini — your agent reads its memory and is ready
```
Or clone the full framework with all 15 agents:
That's it. Your agent has identity, memory, a mailbox, and knows what AIPass is. Say "hi" — it picks up where it left off. Come back tomorrow, it remembers.
### Explore the full framework
Clone the repo to see all 15 agents working together — the reference implementation:
```bash
git clone https://github.com/AIOSAI/AIPass.git
cd AIPass
./setup.sh # Creates venv, installs, bootstraps 15 agents
drone systems # See all agents
```
./setup.sh # Creates venv, installs, bootstraps 15 agents
drone systems # See all agents
Then start working:
```bash
cd src/aipass/devpulse
claude # devpulse reads its memory, knows who it is, picks up where it left off
claude # Talk to the orchestrator
```
Say "hi." Here's what that looks like:
```
You: hi
devpulse: Hey. Picking up where we left off.
Status:
- Branch: main, up to date
- Inbox: 1 email from drone — routing fix applied
- Git: 3 files modified, not committed
- Dropbox: 1 item from @api
Ready when you are.
```
Come back tomorrow — it remembers.
```bash
# More things you can do:
# Things you can do:
drone @seedgo audit aipass # Run 33 quality checks across all agents
drone @flow create . "Add user auth" # Create a work plan
drone systems # List every agent and what it does
-17
View File
@@ -1,17 +0,0 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
<title id="title">AIPass logo</title>
<desc id="desc">A passport-inspired pass badge with a routed A monogram.</desc>
<path d="M42 18H100L142 60V118C142 131.255 131.255 142 118 142H42C28.745 142 18 131.255 18 118V42C18 28.745 28.745 18 42 18Z" fill="#F8FAFC" stroke="#0F172A" stroke-width="6"/>
<path d="M100 18V47C100 54.18 105.82 60 113 60H142" fill="#E2E8F0"/>
<path d="M100 18V47C100 54.18 105.82 60 113 60H142" stroke="#0F172A" stroke-width="6" stroke-linejoin="round"/>
<rect x="30" y="30" width="10" height="100" rx="5" fill="#0F172A"/>
<path d="M54 40H86" stroke="#14B8A6" stroke-width="6" stroke-linecap="round"/>
<circle cx="104" cy="40" r="7" fill="#14B8A6"/>
<path d="M80 50L52 112H68L74 99H86L92 112H108L80 50Z" fill="#0F172A"/>
<path d="M80 51V78" stroke="#14B8A6" stroke-width="8" stroke-linecap="round"/>
<path d="M67 88H93" stroke="#14B8A6" stroke-width="10" stroke-linecap="round"/>
<circle cx="67" cy="88" r="5" fill="#14B8A6"/>
<circle cx="93" cy="88" r="5" fill="#14B8A6"/>
<path d="M54 122H94" stroke="#94A3B8" stroke-width="5" stroke-linecap="round"/>
<path d="M54 133H82" stroke="#CBD5E1" stroke-width="5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

+405 -90
View File
@@ -1,21 +1,29 @@
# =================== AIPass ====================
# Name: bootstrap.py
# Description: Init handler — bootstrap an AIPass project in any directory
# Version: 1.0.0
# Version: 2.0.0
# Created: 2026-03-14
# Modified: 2026-03-14
# Modified: 2026-04-08
# =============================================
"""
Init Bootstrap Handler - PRIVATE implementation
Business logic for `aipass init`. Creates the project scaffold:
1. {NAME}_REGISTRY.json — project registry with UUID
2. .trinity/passport.json — project identity
3. .trinity/local.json — local context (empty)
4. .trinity/observations.json — observations (empty)
5. .aipass/aipass_local_prompt.md — local prompt
6. AIPASS.md — project prompt
1. {NAME}_REGISTRY.json — project registry with UUID
2. .aipass/aipass_global_prompt.md — global prompt (injected every turn)
3. .aipass/aipass_local_prompt.md — local prompt skeleton
4. CLAUDE.md — project prompt (Claude Code reads this)
5. AGENTS.md — Codex equivalent of CLAUDE.md
6. GEMINI.md — Gemini equivalent of CLAUDE.md
7. README.md — getting started guide
8. STATUS.local.md — project status
9. .gitignore — standard AIPass ignores
10. .claude/settings.json — Claude Code hooks configuration
11. hooks/ — directory for user hooks
Projects are NOT citizens — no .trinity/ directory. Identity lives in the
registry JSON. Init is re-runnable: existing files are skipped, not errors.
RULES:
- Pure Python only (no module/prax/cli imports)
@@ -39,6 +47,311 @@ def _sanitize_name(raw: str) -> str:
return re.sub(r"[^A-Z0-9_-]", "_", raw.upper()).strip("_")
# ---------------------------------------------------------------------------
# Template content generators
# ---------------------------------------------------------------------------
def _claude_md(name: str) -> str:
"""Generate CLAUDE.md content — Claude Code reads this on startup."""
return (
f"# {name}\n"
"\n"
"**User:** (your name here)\n"
"\n"
"## What is AIPass\n"
"\n"
"AIPass is a multi-agent framework. This project was created with `aipass init`.\n"
"\n"
"**Key concepts:**\n"
"- **Project** — this directory. Contains a registry and one or more agents.\n"
"- **Agent** — a citizen that lives inside the project. Has identity (`.trinity/`), "
"memory, mailbox, and its own apps/ directory.\n"
f"- **Registry** — `{name}_REGISTRY.json` tracks all agents in this project.\n"
"\n"
"## Getting Started\n"
"\n"
"Create your first agent:\n"
"```\n"
"aipass init agent <name>\n"
"```\n"
"\n"
"This creates a full agent scaffold (`apps/`, `.trinity/`, `.ai_mail.local/`) "
"and registers it in your project registry.\n"
"\n"
"## Available Commands\n"
"\n"
"```\n"
"aipass init agent <name> # Create a new agent\n"
"drone @spawn create <name> # Create agent (alternative)\n"
"drone @seedgo audit <project> # Run standards audit\n"
"drone @ai_mail inbox # Check mailbox (per-agent)\n"
"drone systems # List all available infrastructure\n"
"```\n"
"\n"
"## Startup Protocol\n"
"\n"
"On any greeting, silently read these files — no narration, just do it "
"and respond with the status.\n"
"\n"
f"**Read:** `{name}_REGISTRY.json`, `README.md`, `STATUS.local.md`\n"
"**Run:** `git status`\n"
"\n"
"Then check the registry for agents and report status.\n"
)
def _agents_md(name: str) -> str:
"""Generate AGENTS.md content — Codex equivalent of CLAUDE.md."""
return (
f"# {name} — Agent Instructions\n"
"\n"
"This project uses AIPass, a multi-agent framework.\n"
"\n"
"## Key Concepts\n"
"\n"
"- **Project** — this directory. Contains a registry and one or more agents.\n"
"- **Agent** — a citizen that lives inside the project with its own identity, "
"memory, and code.\n"
f"- **Registry** — `{name}_REGISTRY.json` tracks all agents.\n"
"\n"
"## Getting Started\n"
"\n"
"Create your first agent:\n"
"```\n"
"aipass init agent <name>\n"
"```\n"
"\n"
"## Available Commands\n"
"\n"
"```\n"
"aipass init agent <name> # Create a new agent\n"
"drone @spawn create <name> # Create agent (alternative)\n"
"drone @seedgo audit <project> # Run standards audit\n"
"drone systems # List all infrastructure\n"
"```\n"
"\n"
"## Startup\n"
"\n"
f"On startup, read: `{name}_REGISTRY.json`, `README.md`, `STATUS.local.md`\n"
)
def _gemini_md(name: str) -> str:
"""Generate GEMINI.md content — Gemini equivalent of CLAUDE.md."""
return (
f"# {name} — Project Instructions\n"
"\n"
"This project uses AIPass, a multi-agent framework.\n"
"\n"
"## Key Concepts\n"
"\n"
"- **Project** — this directory. Contains a registry and one or more agents.\n"
"- **Agent** — a citizen that lives inside the project with its own identity, "
"memory, and code.\n"
f"- **Registry** — `{name}_REGISTRY.json` tracks all agents.\n"
"\n"
"## Getting Started\n"
"\n"
"Create your first agent: `aipass init agent <name>`\n"
"\n"
"## Available Commands\n"
"\n"
"```\n"
"aipass init agent <name> # Create a new agent\n"
"drone @spawn create <name> # Create agent (alternative)\n"
"drone @seedgo audit <project> # Run standards audit\n"
"drone systems # List all infrastructure\n"
"```\n"
"\n"
"## Startup\n"
"\n"
f"On startup, read: `{name}_REGISTRY.json`, `README.md`, `STATUS.local.md`\n"
)
def _readme_md(name: str) -> str:
"""Generate README.md content — real getting started guide."""
return (
f"# {name}\n"
"\n"
"An AIPass project.\n"
"\n"
"## Quick Start\n"
"\n"
"```bash\n"
"# 1. Create your first agent\n"
"aipass init agent my_agent\n"
"\n"
"# 2. Start a session\n"
"cd my_agent/\n"
"claude # or your preferred AI CLI\n"
"\n"
"# 3. Check project status\n"
"cat STATUS.local.md\n"
"```\n"
"\n"
"## Project Structure\n"
"\n"
"```\n"
f"{name.lower()}/\n"
f" {name}_REGISTRY.json # Agent registry\n"
" .aipass/ # Prompts (injected per-turn)\n"
" CLAUDE.md # Claude Code instructions\n"
" AGENTS.md # Codex instructions\n"
" GEMINI.md # Gemini instructions\n"
" STATUS.local.md # Project status\n"
" <agent_name>/ # Agent directories (created via aipass init agent)\n"
"```\n"
"\n"
"## What is AIPass?\n"
"\n"
"AIPass is a multi-agent framework where autonomous agents (citizens) "
"live in directories with persistent identity, memory, and communication.\n"
"\n"
"Each agent has:\n"
"- **Identity** — `.trinity/passport.json`\n"
"- **Memory** — `.trinity/local.json`, `observations.json`\n"
"- **Mailbox** — `.ai_mail.local/`\n"
"- **Code** — `apps/` with modules and handlers\n"
"\n"
"## Commands\n"
"\n"
"| Command | Description |\n"
"|---------|-------------|\n"
"| `aipass init agent <name>` | Create a new agent |\n"
"| `drone @spawn create <name>` | Create agent (alternative) |\n"
"| `drone @seedgo audit <project>` | Run standards audit |\n"
"| `drone @ai_mail inbox` | Check agent mailbox |\n"
"| `drone systems` | List infrastructure |\n"
"\n"
f"*Initialized with [AIPass](https://github.com/AIOSAI/AIPass) on "
f"{{date}}*\n"
)
def _global_prompt_md(name: str) -> str:
"""Generate .aipass/aipass_global_prompt.md — injected every turn."""
return (
f"# {name} — Project Context\n"
"<!-- Injected every turn via hook. -->\n"
"\n"
"## Terminology\n"
"\n"
"- **Project** — this directory. Contains a registry and agents.\n"
"- **Agent** — a citizen with identity (`.trinity/`), memory, mailbox, "
"and code (`apps/`).\n"
f"- **Registry** — `{name}_REGISTRY.json` tracks all agents.\n"
"\n"
"## Commands\n"
"\n"
"```\n"
"aipass init agent <name> # Create a new agent\n"
"drone @spawn create <name> # Create agent (alternative)\n"
"drone @seedgo audit <project> # Standards audit\n"
"drone @ai_mail inbox # Check mailbox\n"
"drone @flow create . \"Subject\" # Create a plan\n"
"drone systems # List infrastructure\n"
"```\n"
"\n"
"## Patterns\n"
"\n"
"- **Communication** — agents communicate via `.ai_mail.local/`.\n"
"- **Standards** — run `drone @seedgo audit` to check compliance.\n"
"- **Identity** — agents have `.trinity/passport.json`. "
"Projects use the registry.\n"
)
def _local_prompt_md(name: str) -> str:
"""Generate .aipass/aipass_local_prompt.md — starter skeleton."""
return (
f"# {name} — Local Prompt\n"
"<!-- Injected every turn. Customize for your project. -->\n"
"\n"
"## Project Identity\n"
"\n"
f"- **Name:** {name}\n"
"- **Purpose:** (describe your project)\n"
"- **Status:** New\n"
"\n"
"## How You Work\n"
"\n"
"- Read the registry and STATUS.local.md on startup for context\n"
"- Check the registry for active agents\n"
"\n"
"## Key Files\n"
"\n"
f"- `{name}_REGISTRY.json` — agent registry\n"
"- `STATUS.local.md` — current status\n"
)
def _gitignore() -> str:
"""Generate .gitignore content — standard AIPass ignores."""
return (
"# AIPass local state\n"
".trinity/\n"
".ai_mail.local/\n"
"*.local.*\n"
"\n"
"# Plans (local working docs)\n"
"DPLAN-*\n"
"FPLAN-*\n"
"APLAN-*\n"
"TDPLAN-*\n"
"\n"
"# Logs\n"
"logs/\n"
"\n"
"# Python\n"
"__pycache__/\n"
"*.py[cod]\n"
"*.egg-info/\n"
"dist/\n"
"build/\n"
".venv/\n"
"venv/\n"
"\n"
"# IDE\n"
".vscode/\n"
".idea/\n"
"\n"
"# OS\n"
".DS_Store\n"
"Thumbs.db\n"
"\n"
"# Archives\n"
".archive/\n"
"\n"
"# Disabled files\n"
"*(disabled)*\n"
)
def _claude_settings() -> str:
"""Generate .claude/settings.json — minimal hooks for prompt injection."""
return json.dumps(
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "cat .aipass/aipass_global_prompt.md 2>/dev/null || true",
}
],
}
]
}
},
indent=2,
ensure_ascii=False,
) + "\n"
def init_project(target: Path, project_name: str | None = None) -> dict:
"""Initialize an AIPass project in the target directory.
@@ -51,7 +364,6 @@ def init_project(target: Path, project_name: str | None = None) -> dict:
Raises:
ValueError: If project name is empty after sanitization
FileExistsError: If passport or registry already exists
"""
target = target.resolve()
if not target.exists():
@@ -69,101 +381,104 @@ def init_project(target: Path, project_name: str | None = None) -> dict:
today = date.today().isoformat()
created = []
# 1. Registry
# 1. Registry (skip if exists — init is re-runnable)
registry_filename = f"{name}_REGISTRY.json"
registry_path = target / registry_filename
if registry_path.exists():
raise FileExistsError(f"Registry already exists: {registry_path}")
registry_data = {
"metadata": {
"id": registry_id,
"name": name,
"version": "1.0.0",
"created": today,
"last_updated": today,
"total_branches": 0,
},
"branches": [],
}
registry_path.write_text(
json.dumps(registry_data, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
created.append(str(registry_path))
# 2. .trinity/
trinity_dir = target / ".trinity"
trinity_dir.mkdir(exist_ok=True)
passport = {
"document_metadata": {
"document_type": "project_identity",
"document_name": f"{name}.PASSPORT",
"version": "1.0.0",
"created": today,
"last_updated": today,
},
"identity": {
"project_name": name,
"role": "project_root",
"purpose": "",
},
"citizenship": {
"registered": True,
"registry_id": registry_id,
"registry_name": name,
},
}
passport_path = trinity_dir / "passport.json"
if passport_path.exists():
raise FileExistsError(
f"Passport already exists: {passport_path}. "
"Remove .trinity/passport.json to re-initialize."
# Read existing registry to get its ID
existing = json.loads(registry_path.read_text(encoding="utf-8"))
registry_id = existing["metadata"]["id"]
else:
registry_data = {
"metadata": {
"id": registry_id,
"name": name,
"version": "1.0.0",
"created": today,
"last_updated": today,
"total_branches": 0,
},
"branches": [],
}
registry_path.write_text(
json.dumps(registry_data, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
passport_path.write_text(
json.dumps(passport, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
created.append(str(passport_path))
created.append(str(registry_path))
# local.json + observations.json
for filename in ("local.json", "observations.json"):
p = trinity_dir / filename
if not p.exists():
p.write_text("{}\n", encoding="utf-8")
created.append(str(p))
# 3. .aipass/
# 2. .aipass/
aipass_dir = target / ".aipass"
aipass_dir.mkdir(exist_ok=True)
global_prompt_path = aipass_dir / "aipass_global_prompt.md"
if not global_prompt_path.exists():
global_prompt_path.write_text(_global_prompt_md(name), encoding="utf-8")
created.append(str(global_prompt_path))
prompt_path = aipass_dir / "aipass_local_prompt.md"
if not prompt_path.exists():
prompt_path.write_text(
f"# {name} — Local Prompt\n\nInjected every turn. Add project-specific context here.\n",
encoding="utf-8",
)
prompt_path.write_text(_local_prompt_md(name), encoding="utf-8")
created.append(str(prompt_path))
# 4. AIPASS.md
aipass_md = target / "AIPASS.md"
if not aipass_md.exists():
aipass_md.write_text(
"# AIPass — Project Prompt\n\n"
"Project-level instructions. Persists in context for the entire conversation.\n\n"
"## Startup\n\n"
"On any greeting, silently read these files from CWD and run the commands "
"— no narration, no announcing steps. Just do it and respond with the status.\n\n"
"**Read:** `.trinity/passport.json`, `.trinity/local.json`, "
"`.trinity/observations.json`, `README.md`\n"
"**Run:** `git status`\n\n"
"## Memories\n\n"
"Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`. "
"If compaction hits before you save, it's gone.\n",
# 3. CLAUDE.md
claude_md_path = target / "CLAUDE.md"
if not claude_md_path.exists():
claude_md_path.write_text(_claude_md(name), encoding="utf-8")
created.append(str(claude_md_path))
# 4. AGENTS.md (Codex)
agents_md_path = target / "AGENTS.md"
if not agents_md_path.exists():
agents_md_path.write_text(_agents_md(name), encoding="utf-8")
created.append(str(agents_md_path))
# 5. GEMINI.md
gemini_md_path = target / "GEMINI.md"
if not gemini_md_path.exists():
gemini_md_path.write_text(_gemini_md(name), encoding="utf-8")
created.append(str(gemini_md_path))
# 6. README.md
readme_md_path = target / "README.md"
if not readme_md_path.exists():
readme_content = _readme_md(name).replace("{date}", today)
readme_md_path.write_text(readme_content, encoding="utf-8")
created.append(str(readme_md_path))
# 7. STATUS.local.md
status_md_path = target / "STATUS.local.md"
if not status_md_path.exists():
status_md_path.write_text(
f"# {name}\n\n"
"**State:** New\n"
f"**Last update:** {today}\n\n"
"## Current Work\n\n"
"## Known Issues\n"
"- None\n",
encoding="utf-8",
)
created.append(str(aipass_md))
created.append(str(status_md_path))
# 8. .gitignore
gitignore_path = target / ".gitignore"
if not gitignore_path.exists():
gitignore_path.write_text(_gitignore(), encoding="utf-8")
created.append(str(gitignore_path))
# 9. .claude/settings.json
claude_dir = target / ".claude"
claude_dir.mkdir(exist_ok=True)
settings_path = claude_dir / "settings.json"
if not settings_path.exists():
settings_path.write_text(_claude_settings(), encoding="utf-8")
created.append(str(settings_path))
# 10. hooks/ directory
hooks_dir = target / "hooks"
if not hooks_dir.exists():
hooks_dir.mkdir()
created.append(str(hooks_dir))
return {
"registry_id": registry_id,
+89 -15
View File
@@ -16,6 +16,7 @@ Run: drone @cli aipass
"""
import os
import subprocess
import sys
from pathlib import Path
from typing import List
@@ -50,6 +51,11 @@ def print_introspection():
"Bootstrap a new AIPass project",
"drone @cli aipass init /path MyProject",
)
table.add_row(
"init agent",
"Create an agent in current project",
"drone @cli aipass init agent my_agent",
)
console.print(table)
console.print()
@@ -86,6 +92,7 @@ def print_help():
console.print(" [green]drone @cli aipass init[/green] [dim]Bootstrap in current directory[/dim]")
console.print(" [green]drone @cli aipass init /path[/green] [dim]Bootstrap in target directory[/dim]")
console.print(" [green]drone @cli aipass init /path MyProj[/green] [dim]Bootstrap with custom name[/dim]")
console.print(" [green]drone @cli aipass init agent <name>[/green] [dim]Create an agent in current project[/dim]")
console.print(" [green]drone @cli aipass --help[/green] [dim]This help message[/dim]")
console.print()
console.print("─" * 70)
@@ -95,14 +102,19 @@ def print_help():
console.print("[bold cyan]WHAT INIT CREATES:[/bold cyan]")
console.print()
files_text = """[bold]Project scaffold (6 files):[/bold]
files_text = """[bold]Project scaffold (11 items):[/bold]
[green]1.[/green] [yellow]{NAME}_REGISTRY.json[/yellow] Project registry with UUID
[green]2.[/green] [yellow].trinity/passport.json[/yellow] Project identity (with registry_id)
[green]3.[/green] [yellow].trinity/local.json[/yellow] Session history & learnings
[green]4.[/green] [yellow].trinity/observations.json[/yellow] Collaboration patterns
[green]5.[/green] [yellow].aipass/aipass_local_prompt.md[/yellow] Local prompt (injected every turn)
[green]6.[/green] [yellow]AIPASS.md[/yellow] Project prompt (persists in context)"""
[green]1.[/green] [yellow]{NAME}_REGISTRY.json[/yellow] Project registry with UUID
[green]2.[/green] [yellow].aipass/aipass_global_prompt.md[/yellow] Global prompt (injected every turn)
[green]3.[/green] [yellow].aipass/aipass_local_prompt.md[/yellow] Local prompt skeleton
[green]4.[/green] [yellow]CLAUDE.md[/yellow] Project prompt (Claude Code)
[green]5.[/green] [yellow]AGENTS.md[/yellow] Codex instructions
[green]6.[/green] [yellow]GEMINI.md[/yellow] Gemini instructions
[green]7.[/green] [yellow]README.md[/yellow] Getting started guide
[green]8.[/green] [yellow]STATUS.local.md[/yellow] Project status
[green]9.[/green] [yellow].gitignore[/yellow] Standard AIPass ignores
[green]10.[/green] [yellow].claude/settings.json[/yellow] Claude Code hooks
[green]11.[/green] [yellow]hooks/[/yellow] User hooks directory"""
console.print(Panel(files_text, border_style="green", padding=(1, 2), box=box.ROUNDED))
console.print()
@@ -143,6 +155,11 @@ def handle_command(command: str, args: List[str]) -> bool:
Returns:
True if handled, False if not this module's command
"""
# Direct subcommand: 'aipass init /path' on PATH → command="init"
if command == "init":
return _handle_init(args)
# Prefixed command: 'drone @cli aipass init' → command="aipass"
if command != "aipass":
return False
@@ -178,6 +195,10 @@ def _handle_init(args: List[str]) -> bool:
_print_init_help()
return True
# Route 'init agent <name>' to spawn
if args and args[0] == "agent":
return _handle_init_agent(args[1:])
# Parse positional args: [target_dir] [project_name]
caller_cwd = os.environ.get("AIPASS_CALLER_CWD", os.getcwd())
target = Path(args[0]) if args else Path(caller_cwd)
@@ -191,7 +212,7 @@ def _handle_init(args: List[str]) -> bool:
sys.exit(1)
except FileExistsError as exc:
logger.warning("Init target already exists: %s", exc)
error(str(exc), suggestion="Remove the existing file to re-initialize")
error(str(exc))
sys.exit(1)
except OSError as exc:
logger.error("Init filesystem error: %s", exc)
@@ -229,11 +250,59 @@ def _handle_init(args: List[str]) -> bool:
"files_created": len(result["created_files"]),
})
# Next steps
console.print()
console.print("[bold cyan]Next steps:[/bold cyan]")
console.print(f" [green]1.[/green] Create your first agent: [yellow]aipass init agent <name>[/yellow]")
console.print(f" [green]2.[/green] Start a session: [dim]cd <name>/ && claude[/dim]")
console.print(f" [green]3.[/green] Read the docs: [dim]cat README.md[/dim]")
console.print()
return True
def _handle_init_agent(args: List[str]) -> bool:
"""Handle 'aipass init agent <name>' — routes to drone @spawn create."""
if not args or args[0] in ("--help", "-h", "help"):
console.print()
header("aipass init agent — Create an Agent")
console.print("[dim]Creates a new agent inside the current project[/dim]")
console.print()
console.print("[bold cyan]Usage:[/bold cyan]")
console.print(" [green]aipass init agent <name>[/green]")
console.print(" [green]drone @cli aipass init agent <name>[/green]")
console.print()
console.print("[bold cyan]What it does:[/bold cyan]")
console.print(" Routes to [yellow]drone @spawn create <name>[/yellow]")
console.print(" Creates full agent scaffold (apps/, .trinity/, .ai_mail.local/)")
console.print(" Registers agent in the project registry")
console.print()
console.print("[dim]Commands: init agent, init agent --help[/dim]")
console.print()
return True
agent_name = args[0]
logger.info("Routing 'init agent %s' to drone @spawn create", agent_name)
try:
result = subprocess.run(
["drone", "@spawn", "create", agent_name],
check=False,
)
if result.returncode != 0:
error(
f"Agent creation failed (exit {result.returncode})",
suggestion="Check drone @spawn create --help",
)
return True
except FileNotFoundError:
error(
"drone command not found",
suggestion="Ensure AIPass is installed and drone is in PATH",
)
return True
def _print_init_help():
"""Display detailed help for the init subcommand."""
from rich.panel import Panel
@@ -266,14 +335,19 @@ def _print_init_help():
console.print("[bold cyan]WHAT IT CREATES:[/bold cyan]")
console.print()
files_text = """[bold]Project scaffold (6 files):[/bold]
files_text = """[bold]Project scaffold (11 items):[/bold]
[green]1.[/green] [yellow]{NAME}_REGISTRY.json[/yellow] Project registry with UUID
[green]2.[/green] [yellow].trinity/passport.json[/yellow] Project identity (with registry_id)
[green]3.[/green] [yellow].trinity/local.json[/yellow] Session history & learnings
[green]4.[/green] [yellow].trinity/observations.json[/yellow] Collaboration patterns
[green]5.[/green] [yellow].aipass/aipass_local_prompt.md[/yellow] Local prompt (injected every turn)
[green]6.[/green] [yellow]AIPASS.md[/yellow] Project prompt (persists in context)"""
[green]1.[/green] [yellow]{NAME}_REGISTRY.json[/yellow] Project registry with UUID
[green]2.[/green] [yellow].aipass/aipass_global_prompt.md[/yellow] Global prompt (injected every turn)
[green]3.[/green] [yellow].aipass/aipass_local_prompt.md[/yellow] Local prompt skeleton
[green]4.[/green] [yellow]CLAUDE.md[/yellow] Project prompt (Claude Code)
[green]5.[/green] [yellow]AGENTS.md[/yellow] Codex instructions
[green]6.[/green] [yellow]GEMINI.md[/yellow] Gemini instructions
[green]7.[/green] [yellow]README.md[/yellow] Getting started guide
[green]8.[/green] [yellow]STATUS.local.md[/yellow] Project status
[green]9.[/green] [yellow].gitignore[/yellow] Standard AIPass ignores
[green]10.[/green] [yellow].claude/settings.json[/yellow] Claude Code hooks
[green]11.[/green] [yellow]hooks/[/yellow] User hooks directory"""
console.print(Panel(files_text, border_style="green", padding=(1, 2), box=box.ROUNDED))
console.print()
+151 -89
View File
@@ -5,7 +5,6 @@ use tmp_path to stay fully isolated from the live filesystem.
"""
import json
import re
import uuid
from datetime import date
from pathlib import Path
@@ -75,8 +74,8 @@ def test_sanitize_name_only_underscores():
# ---------------------------------------------------------------------------
def test_init_project_creates_all_six_files(tmp_path):
"""init_project produces exactly the six expected files."""
def test_init_project_creates_all_expected_files(tmp_path):
"""init_project produces all expected files and directories."""
target = tmp_path / "proj"
target.mkdir()
@@ -84,16 +83,27 @@ def test_init_project_creates_all_six_files(tmp_path):
expected_files = [
target / "DEMO_REGISTRY.json",
target / ".trinity" / "passport.json",
target / ".trinity" / "local.json",
target / ".trinity" / "observations.json",
target / ".aipass" / "aipass_global_prompt.md",
target / ".aipass" / "aipass_local_prompt.md",
target / "AIPASS.md",
target / "CLAUDE.md",
target / "AGENTS.md",
target / "GEMINI.md",
target / "README.md",
target / "STATUS.local.md",
target / ".gitignore",
target / ".claude" / "settings.json",
]
for f in expected_files:
assert f.exists(), f"Expected file not created: {f}"
assert len(result["created_files"]) == 6
# hooks/ is a directory, not a file
assert (target / "hooks").is_dir(), "Expected hooks/ directory"
# No .trinity/ should be created (projects are not citizens)
assert not (target / ".trinity").exists(), ".trinity/ should NOT be created"
# 10 files + 1 directory = 11 created_files entries
assert len(result["created_files"]) == 11
def test_init_project_return_dict_structure(tmp_path):
@@ -146,38 +156,14 @@ def test_init_project_registry_json_contents(tmp_path):
assert data["branches"] == []
def test_init_project_passport_json_contents(tmp_path):
"""passport.json has correct identity and citizenship fields."""
def test_init_project_no_trinity_created(tmp_path):
"""Projects are not citizens — no .trinity/ directory created."""
target = tmp_path / "proj"
target.mkdir()
result = init_project(target, project_name="gamma")
init_project(target, project_name="gamma")
passport_path = target / ".trinity" / "passport.json"
data = json.loads(passport_path.read_text(encoding="utf-8"))
assert data["document_metadata"]["document_type"] == "project_identity"
assert data["document_metadata"]["document_name"] == "GAMMA.PASSPORT"
assert data["document_metadata"]["version"] == "1.0.0"
assert re.match(r"^\d{4}-\d{2}-\d{2}$", data["document_metadata"]["created"])
assert data["identity"]["project_name"] == "GAMMA"
assert data["identity"]["role"] == "project_root"
assert data["citizenship"]["registered"] is True
assert data["citizenship"]["registry_id"] == result["registry_id"]
assert data["citizenship"]["registry_name"] == "GAMMA"
def test_init_project_local_and_observations_are_empty_objects(tmp_path):
"""local.json and observations.json are written as empty JSON objects."""
target = tmp_path / "proj"
target.mkdir()
init_project(target, project_name="delta")
for filename in ("local.json", "observations.json"):
path = target / ".trinity" / filename
content = path.read_text(encoding="utf-8")
assert content == "{}\n"
assert not (target / ".trinity").exists()
def test_init_project_local_prompt_content(tmp_path):
@@ -193,41 +179,40 @@ def test_init_project_local_prompt_content(tmp_path):
assert "Local Prompt" in content
def test_init_project_aipass_md_content(tmp_path):
"""AIPASS.md contains the standard project prompt boilerplate."""
def test_init_project_claude_md_content(tmp_path):
"""CLAUDE.md contains real AIPass project content."""
target = tmp_path / "proj"
target.mkdir()
init_project(target, project_name="zeta")
md_path = target / "AIPASS.md"
md_path = target / "CLAUDE.md"
content = md_path.read_text(encoding="utf-8")
assert "# AIPass" in content
assert "## Startup" in content
assert "## Memories" in content
assert ".trinity/passport.json" in content
assert "# ZETA" in content
assert "## What is AIPass" in content
assert "## Getting Started" in content
assert "## Available Commands" in content
assert "## Startup Protocol" in content
# Startup protocol reads registry, not .trinity/
startup_idx = content.index("## Startup Protocol")
startup_section = content[startup_idx:]
assert ".trinity/" not in startup_section
assert "aipass init agent" in content
assert "ZETA_REGISTRY.json" in content
def test_init_project_raises_on_existing_passport(tmp_path):
"""FileExistsError when .trinity/passport.json already exists."""
def test_init_project_rerunnable_skips_existing_registry(tmp_path):
"""Running init twice skips the existing registry and reuses its ID."""
target = tmp_path / "proj"
target.mkdir()
trinity = target / ".trinity"
trinity.mkdir()
(trinity / "passport.json").write_text("{}", encoding="utf-8")
with pytest.raises(FileExistsError, match="Passport already exists"):
init_project(target, project_name="dup")
result1 = init_project(target, project_name="dup")
result2 = init_project(target, project_name="dup")
def test_init_project_raises_on_existing_registry(tmp_path):
"""FileExistsError when the REGISTRY.json file already exists."""
target = tmp_path / "proj"
target.mkdir()
(target / "DUP_REGISTRY.json").write_text("{}", encoding="utf-8")
with pytest.raises(FileExistsError, match="Registry already exists"):
init_project(target, project_name="dup")
# Same registry ID reused
assert result1["registry_id"] == result2["registry_id"]
# Second run creates no new files (all already exist)
assert len(result2["created_files"]) == 0
def test_init_project_raises_on_empty_name(tmp_path):
@@ -239,6 +224,84 @@ def test_init_project_raises_on_empty_name(tmp_path):
init_project(target, project_name="!!!")
def test_init_project_agents_md_content(tmp_path):
"""AGENTS.md contains Codex-equivalent content."""
target = tmp_path / "proj"
target.mkdir()
init_project(target, project_name="alpha")
content = (target / "AGENTS.md").read_text(encoding="utf-8")
assert "# ALPHA" in content
assert "ALPHA_REGISTRY.json" in content
assert "aipass init agent" in content
def test_init_project_gemini_md_content(tmp_path):
"""GEMINI.md contains Gemini-equivalent content."""
target = tmp_path / "proj"
target.mkdir()
init_project(target, project_name="alpha")
content = (target / "GEMINI.md").read_text(encoding="utf-8")
assert "# ALPHA" in content
assert "ALPHA_REGISTRY.json" in content
def test_init_project_gitignore_content(tmp_path):
""".gitignore contains standard AIPass patterns."""
target = tmp_path / "proj"
target.mkdir()
init_project(target, project_name="alpha")
content = (target / ".gitignore").read_text(encoding="utf-8")
assert ".trinity/" in content
assert ".ai_mail.local/" in content
assert "__pycache__/" in content
assert "logs/" in content
def test_init_project_claude_settings_content(tmp_path):
""".claude/settings.json has valid hook configuration."""
target = tmp_path / "proj"
target.mkdir()
init_project(target, project_name="alpha")
settings_path = target / ".claude" / "settings.json"
data = json.loads(settings_path.read_text(encoding="utf-8"))
assert "hooks" in data
assert "UserPromptSubmit" in data["hooks"]
def test_init_project_global_prompt_content(tmp_path):
"""Global prompt contains project name and AIPass terminology."""
target = tmp_path / "proj"
target.mkdir()
init_project(target, project_name="alpha")
content = (target / ".aipass" / "aipass_global_prompt.md").read_text(encoding="utf-8")
assert "# ALPHA" in content
assert "ALPHA_REGISTRY.json" in content
assert "## Commands" in content
def test_init_project_readme_md_content(tmp_path):
"""README.md contains getting started guide with project name."""
target = tmp_path / "proj"
target.mkdir()
init_project(target, project_name="alpha")
content = (target / "README.md").read_text(encoding="utf-8")
assert "# ALPHA" in content
assert "## Quick Start" in content
assert "aipass init agent" in content
def test_init_project_auto_creates_target_dir(tmp_path):
"""Target directory is created (including parents) if it doesn't exist."""
target = tmp_path / "deep" / "nested" / "proj"
@@ -248,7 +311,7 @@ def test_init_project_auto_creates_target_dir(tmp_path):
assert target.is_dir()
assert result["project_name"] == "NESTED"
assert len(result["created_files"]) == 6
assert len(result["created_files"]) == 11
def test_init_project_defaults_name_from_directory(tmp_path):
@@ -275,47 +338,48 @@ def test_init_project_custom_name_overrides_directory(tmp_path):
def test_init_project_skips_existing_optional_files(tmp_path):
"""local.json, observations.json, prompt, and AIPASS.md are not
overwritten if they already exist (only passport and registry guard
with errors)."""
"""Optional files are not overwritten if they already exist.
Init is re-runnable — existing files are skipped."""
target = tmp_path / "proj"
target.mkdir()
# Pre-create the optional files
trinity = target / ".trinity"
trinity.mkdir()
(trinity / "local.json").write_text('{"existing": true}\n', encoding="utf-8")
(trinity / "observations.json").write_text(
'{"existing": true}\n', encoding="utf-8"
)
# Pre-create optional files
aipass_dir = target / ".aipass"
aipass_dir.mkdir()
(aipass_dir / "aipass_global_prompt.md").write_text(
"# Custom global\n", encoding="utf-8"
)
(aipass_dir / "aipass_local_prompt.md").write_text(
"# Custom prompt\n", encoding="utf-8"
)
(target / "AIPASS.md").write_text("# Custom AIPASS\n", encoding="utf-8")
(target / "CLAUDE.md").write_text("# Custom CLAUDE\n", encoding="utf-8")
(target / "AGENTS.md").write_text("# Custom AGENTS\n", encoding="utf-8")
(target / "GEMINI.md").write_text("# Custom GEMINI\n", encoding="utf-8")
(target / "README.md").write_text("# Custom README\n", encoding="utf-8")
(target / "STATUS.local.md").write_text("# Custom status\n", encoding="utf-8")
(target / ".gitignore").write_text("# Custom\n", encoding="utf-8")
claude_dir = target / ".claude"
claude_dir.mkdir()
(claude_dir / "settings.json").write_text("{}\n", encoding="utf-8")
hooks_dir = target / "hooks"
hooks_dir.mkdir()
result = init_project(target, project_name="eta")
# Only registry and passport should be in created_files
assert len(result["created_files"]) == 2
# Only registry should be in created_files (everything else pre-existed)
assert len(result["created_files"]) == 1
# Verify pre-existing files were NOT overwritten
local_content = (trinity / "local.json").read_text(encoding="utf-8")
assert '"existing": true' in local_content
obs_content = (trinity / "observations.json").read_text(encoding="utf-8")
assert '"existing": true' in obs_content
prompt_content = (aipass_dir / "aipass_local_prompt.md").read_text(
encoding="utf-8"
)
assert prompt_content == "# Custom prompt\n"
md_content = (target / "AIPASS.md").read_text(encoding="utf-8")
assert md_content == "# Custom AIPASS\n"
md_content = (target / "CLAUDE.md").read_text(encoding="utf-8")
assert md_content == "# Custom CLAUDE\n"
def test_init_project_returns_dict(tmp_path):
@@ -328,15 +392,13 @@ def test_init_project_returns_dict(tmp_path):
assert isinstance(result, dict)
def test_init_project_no_overwrite_local_json(tmp_path):
"""Existing local.json content is preserved — no_overwrite contract."""
def test_init_project_agents_md_no_trinity(tmp_path):
"""AGENTS.md startup protocol references registry, not .trinity/."""
target = tmp_path / "proj"
target.mkdir()
trinity = target / ".trinity"
trinity.mkdir()
(trinity / "local.json").write_text('{"custom": true}\n', encoding="utf-8")
init_project(target, project_name="keep")
content = (trinity / "local.json").read_text(encoding="utf-8")
assert '"custom": true' in content
content = (target / "AGENTS.md").read_text(encoding="utf-8")
assert ".trinity/" not in content
assert "KEEP_REGISTRY.json" in content
@@ -13,6 +13,7 @@ You are DEVPULSE — orchestration hub. Manager, not builder. Coordinate, plan,
- Never block waiting on agents. Never burn context reading code across branches.
- Use `drone @branch --help` for command syntax. Use `drone systems` for branch list.
- **ALWAYS WAKE after sending dispatch emails.** Send email → wake. Every time. No asking. If the user wants something different, they will say so.
- **START WATCHDOG after any dispatch.** Run `python3 src/aipass/prax/tools/inbox_watchdog.py src/aipass/devpulse/.ai_mail.local/inbox.json --interval 30 &` after dispatching. Don't wait for Patrick to ask.
## Dispatch, Don't Do
@@ -9,6 +9,11 @@
"entry_point": "aipass.cli.apps.cli",
"description": "Universal Display & Output Service Provider",
"version": "2.0.0"
},
"spawn": {
"entry_point": "aipass.spawn.apps.spawn",
"description": "Branch lifecycle — create, register, scaffold citizens",
"version": "1.0.0"
}
}
}
+5 -1
View File
@@ -103,7 +103,11 @@ def handle_command(command: str, args: List[str]) -> bool:
kwargs["purpose"] = args[i + 1]
i += 2
elif args[i] == "--template" and i + 1 < len(args):
kwargs["template_dir"] = args[i + 1]
template_val = args[i + 1]
if validate_class(template_val):
kwargs["citizen_class"] = template_val
else:
kwargs["template_dir"] = template_val
i += 2
elif args[i] == "--registry" and i + 1 < len(args):
kwargs["registry_path"] = args[i + 1]
+8 -2
View File
@@ -55,7 +55,7 @@ def print_help():
warning("--role", details="Agent role description")
warning("--traits", details="Agent personality traits")
warning("--purpose", details="Agent purpose (brief)")
warning("--template", details="Custom template directory")
warning("--template", details="Template class name (builder, birthright) or custom directory path")
warning("--registry", details="Path to AIPASS_REGISTRY.json")
warning("--dry-run", details="Preview changes without modifying files")
warning("--trace", details="Enable verbose logging")
@@ -100,6 +100,12 @@ def handle_create(args):
parsed = parser.parse_args(remaining_args)
# --template can be a class name (e.g. "builder") or a raw path
template_dir = parsed.template
if parsed.template and validate_class(parsed.template):
citizen_class = parsed.template
template_dir = None
if dry_run:
return _dry_run_create(parsed.target_path, citizen_class, parsed)
@@ -108,7 +114,7 @@ def handle_create(args):
role=parsed.role,
traits=parsed.traits,
purpose=parsed.purpose,
template_dir=parsed.template,
template_dir=template_dir,
registry_path=parsed.registry,
citizen_class=citizen_class,
)
@@ -1,384 +1,350 @@
{
"metadata": {
"version": "1.0.0",
"last_updated": "2026-04-07",
"description": "Template file tracking registry for ID-based updates"
"description": "Template registry for tracking files",
"generated": true
},
"files": {
"f002": {
"f001": {
"path": ".ai_mail.local/README.md",
"name": "README.md",
"content_hash": "49299c242a01",
"has_branch_placeholder": false
"content_hash": "49299c242a018d73",
"has_branch_placeholder": true
},
"f001": {
"f002": {
"path": ".ai_mail.local/inbox.json",
"name": "inbox.json",
"content_hash": "c9702fe2cc21",
"has_branch_placeholder": false
},
"f015": {
"path": ".aipass/README.md",
"name": "README.md",
"content_hash": "f42d87684fdf",
"content_hash": "c9702fe2cc21b748",
"has_branch_placeholder": false
},
"f003": {
"path": ".aipass/aipass_local_prompt.md",
"name": "aipass_local_prompt.md",
"content_hash": "5f8e2a3859a9",
"has_branch_placeholder": false
"path": ".aipass/README.md",
"name": "README.md",
"content_hash": "f42d87684fdfb1af",
"has_branch_placeholder": true
},
"f004": {
"path": ".archive/README.md",
"name": "README.md",
"content_hash": "93d3fcb74f23",
"has_branch_placeholder": false
},
"f025": {
"path": ".claude/README.md",
"name": "README.md",
"content_hash": "adb0ce8c53c1",
"has_branch_placeholder": false
"path": ".aipass/aipass_local_prompt.md",
"name": "aipass_local_prompt.md",
"content_hash": "5f8e2a3859a99906",
"has_branch_placeholder": true
},
"f005": {
"path": ".claude/settings.local.json",
"name": "settings.local.json",
"content_hash": "eacf065629cd",
"has_branch_placeholder": false
"path": ".archive/README.md",
"name": "README.md",
"content_hash": "93d3fcb74f234b1d",
"has_branch_placeholder": true
},
"f006": {
"path": ".gitignore",
"name": ".gitignore",
"content_hash": "841dedb922da",
"has_branch_placeholder": false
},
"f027": {
"path": ".seedgo/README.md",
"path": ".claude/README.md",
"name": "README.md",
"content_hash": "ea03468bbf16",
"has_branch_placeholder": false
"content_hash": "adb0ce8c53c1da2d",
"has_branch_placeholder": true
},
"f007": {
"path": ".seedgo/bypass.json",
"name": "bypass.json",
"content_hash": "0ac90a35515b",
"has_branch_placeholder": false
},
"f016": {
"path": ".spawn/.registry_ignore.json",
"name": ".registry_ignore.json",
"content_hash": "34f5e7ff7e01",
"has_branch_placeholder": false
},
"f042": {
"path": ".spawn/README.md",
"name": "README.md",
"content_hash": "e22ad5337efd",
"has_branch_placeholder": false
},
"f028": {
"path": ".trinity/README.md",
"name": "README.md",
"content_hash": "f461c9b16fc5",
"path": ".claude/settings.local.json",
"name": "settings.local.json",
"content_hash": "eacf065629cd9c03",
"has_branch_placeholder": false
},
"f008": {
"path": ".trinity/local.json",
"name": "local.json",
"content_hash": "f4e62d2d12ba",
"path": ".gitignore",
"name": ".gitignore",
"content_hash": "841dedb922da7ddb",
"has_branch_placeholder": false
},
"f009": {
"path": ".trinity/observations.json",
"name": "observations.json",
"content_hash": "25b957960a59",
"has_branch_placeholder": false
"path": ".seedgo/README.md",
"name": "README.md",
"content_hash": "ea03468bbf16f248",
"has_branch_placeholder": true
},
"f010": {
"path": ".trinity/passport.json",
"name": "passport.json",
"content_hash": "04c2ead2a428",
"has_branch_placeholder": false
"path": ".seedgo/bypass.json",
"name": "bypass.json",
"content_hash": "0ac90a35515b35c2",
"has_branch_placeholder": true
},
"f011": {
"path": "DASHBOARD.local.json",
"name": "DASHBOARD.local.json",
"content_hash": "88360d943d4b",
"has_branch_placeholder": false
"path": ".trinity/README.md",
"name": "README.md",
"content_hash": "f461c9b16fc566a0",
"has_branch_placeholder": true
},
"f012": {
"path": "README.md",
"name": "README.md",
"content_hash": "ad99517a50f9",
"has_branch_placeholder": false
"path": ".trinity/local.json",
"name": "local.json",
"content_hash": "f4e62d2d12bada2e",
"has_branch_placeholder": true
},
"f013": {
"path": "STATUS.local.md",
"name": "STATUS.local.md",
"content_hash": "059295ad4d4e",
"has_branch_placeholder": false
},
"f029": {
"path": "apps/README.md",
"name": "README.md",
"content_hash": "92a956009e0e",
"has_branch_placeholder": false
"path": ".trinity/observations.json",
"name": "observations.json",
"content_hash": "25b957960a5939a3",
"has_branch_placeholder": true
},
"f014": {
"path": "apps/__init__.py",
"name": "__init__.py",
"content_hash": "41b011f487af",
"has_branch_placeholder": false
"path": ".trinity/passport.json",
"name": "passport.json",
"content_hash": "04c2ead2a4284027",
"has_branch_placeholder": true
},
"f030": {
"path": "apps/handlers/README.md",
"name": "README.md",
"content_hash": "2e4f4a0c1b47",
"has_branch_placeholder": false
"f015": {
"path": "CLAUDE.md",
"name": "CLAUDE.md",
"content_hash": "e24c75348c4faa57",
"has_branch_placeholder": true
},
"f016": {
"path": "DASHBOARD.local.json",
"name": "DASHBOARD.local.json",
"content_hash": "88360d943d4bacb6",
"has_branch_placeholder": true
},
"f017": {
"path": "apps/handlers/__init__.py",
"name": "__init__.py",
"content_hash": "55afc5eddbe0",
"has_branch_placeholder": false
},
"f031": {
"path": "apps/modules/README.md",
"path": "README.md",
"name": "README.md",
"content_hash": "a4cf0a8e3b4f",
"has_branch_placeholder": false
},
"f043": {
"path": "apps/modules/__init__.py",
"name": "__init__.py",
"content_hash": "e3b0c44298fc",
"has_branch_placeholder": false
},
"f032": {
"path": "apps/plugins/README.md",
"name": "README.md",
"content_hash": "d1e4e2b98c38",
"has_branch_placeholder": false
"content_hash": "ad99517a50f96ba7",
"has_branch_placeholder": true
},
"f018": {
"path": "STATUS.local.md",
"name": "STATUS.local.md",
"content_hash": "059295ad4d4e62eb",
"has_branch_placeholder": true
},
"f019": {
"path": "apps/README.md",
"name": "README.md",
"content_hash": "92a956009e0e22dc",
"has_branch_placeholder": true
},
"f020": {
"path": "apps/__init__.py",
"name": "__init__.py",
"content_hash": "41b011f487af14dc",
"has_branch_placeholder": true
},
"f021": {
"path": "apps/handlers/README.md",
"name": "README.md",
"content_hash": "2e4f4a0c1b474ffa",
"has_branch_placeholder": true
},
"f022": {
"path": "apps/handlers/__init__.py",
"name": "__init__.py",
"content_hash": "55afc5eddbe04134",
"has_branch_placeholder": true
},
"f023": {
"path": "apps/modules/README.md",
"name": "README.md",
"content_hash": "a4cf0a8e3b4ff993",
"has_branch_placeholder": true
},
"f024": {
"path": "apps/modules/__init__.py",
"name": "__init__.py",
"content_hash": "e3b0c44298fc1c14",
"has_branch_placeholder": false
},
"f025": {
"path": "apps/plugins/README.md",
"name": "README.md",
"content_hash": "d1e4e2b98c384ca2",
"has_branch_placeholder": true
},
"f026": {
"path": "apps/plugins/__init__.py",
"name": "__init__.py",
"content_hash": "e3b0c44298fc1c14",
"has_branch_placeholder": false
},
"f027": {
"path": "apps/{{BRANCH}}.py",
"name": "{{BRANCH}}.py",
"content_hash": "fc71b424c10b",
"content_hash": "fc71b424c10bb172",
"has_branch_placeholder": true
},
"f028": {
"path": "artifacts/README.md",
"name": "README.md",
"content_hash": "de20d11e5cfd3586",
"has_branch_placeholder": true
},
"f029": {
"path": "artifacts/birth_certificate.json",
"name": "birth_certificate.json",
"content_hash": "1ec401f4e397eec7",
"has_branch_placeholder": true
},
"f030": {
"path": "docs/README.md",
"name": "README.md",
"content_hash": "2434da568727499d",
"has_branch_placeholder": true
},
"f031": {
"path": "docs.local/README.md",
"name": "README.md",
"content_hash": "c19d8872ea2c2c25",
"has_branch_placeholder": true
},
"f032": {
"path": "docs.local/sub_agent_drops/README.md",
"name": "README.md",
"content_hash": "e3e5a6b9c9c51817",
"has_branch_placeholder": false
},
"f033": {
"path": "dropbox/README.md",
"name": "README.md",
"content_hash": "9e1e9b71f93b4cde",
"has_branch_placeholder": true
},
"f034": {
"path": "artifacts/README.md",
"path": "logs/README.md",
"name": "README.md",
"content_hash": "de20d11e5cfd",
"has_branch_placeholder": false
},
"f019": {
"path": "artifacts/birth_certificate.json",
"name": "birth_certificate.json",
"content_hash": "1ec401f4e397",
"has_branch_placeholder": false
},
"f020": {
"path": "docs/README.md",
"name": "README.md",
"content_hash": "2434da568727",
"has_branch_placeholder": false
"content_hash": "4ca207af6bd3a648",
"has_branch_placeholder": true
},
"f035": {
"path": "docs.local/README.md",
"name": "README.md",
"content_hash": "c19d8872ea2c",
"path": "pytest.ini",
"name": "pytest.ini",
"content_hash": "7b39ba7bca4025c5",
"has_branch_placeholder": false
},
"f036": {
"path": "docs.local/sub_agent_drops/README.md",
"name": "README.md",
"content_hash": "e3e5a6b9c9c5",
"has_branch_placeholder": false
},
"f021": {
"path": "dropbox/README.md",
"name": "README.md",
"content_hash": "9e1e9b71f93b",
"has_branch_placeholder": false
},
"f037": {
"path": "logs/README.md",
"name": "README.md",
"content_hash": "4ca207af6bd3",
"has_branch_placeholder": false
},
"f022": {
"path": "pytest.ini",
"name": "pytest.ini",
"content_hash": "7b39ba7bca40",
"has_branch_placeholder": false
},
"f038": {
"path": "templates/README.md",
"name": "README.md",
"content_hash": "73b020b003f9",
"has_branch_placeholder": false
"content_hash": "73b020b003f900e5",
"has_branch_placeholder": true
},
"f039": {
"f037": {
"path": "tests/README.md",
"name": "README.md",
"content_hash": "c157895c9b27",
"has_branch_placeholder": false
"content_hash": "c157895c9b27d3ba",
"has_branch_placeholder": true
},
"f023": {
"f038": {
"path": "tests/__init__.py",
"name": "__init__.py",
"content_hash": "881f06bb6574",
"has_branch_placeholder": false
"content_hash": "881f06bb6574d2f7",
"has_branch_placeholder": true
},
"f024": {
"f039": {
"path": "tests/conftest.py",
"name": "conftest.py",
"content_hash": "5d98b049957b",
"content_hash": "5d98b049957b24a4",
"has_branch_placeholder": false
},
"f040": {
"path": "tools/README.md",
"name": "README.md",
"content_hash": "3c7eaedb16ac",
"has_branch_placeholder": false
},
"f026": {
"path": "{{BRANCH}}_json/README.md",
"name": "README.md",
"content_hash": "e64fa555e7b8",
"has_branch_placeholder": false
"content_hash": "3c7eaedb16ac2695",
"has_branch_placeholder": true
},
"f041": {
"path": "{{BRANCH}}_json/README.md",
"name": "README.md",
"content_hash": "e64fa555e7b8e20f",
"has_branch_placeholder": true
},
"f042": {
"path": "{{BRANCH}}_json/custom_config/README.md",
"name": "README.md",
"content_hash": "28e9ae373563",
"has_branch_placeholder": false
},
"f033": {
"path": "apps/plugins/__init__.py",
"name": "__init__.py",
"content_hash": "e3b0c44298fc",
"has_branch_placeholder": false
"content_hash": "28e9ae3735636611",
"has_branch_placeholder": true
}
},
"directories": {
"d001": {
"path": ".ai_mail.local",
"name": ".ai_mail.local",
"has_branch_placeholder": false
"name": ".ai_mail.local"
},
"d002": {
"path": ".ai_mail.local/sent",
"name": "sent",
"has_branch_placeholder": false
"name": "sent"
},
"d003": {
"path": ".aipass",
"name": ".aipass",
"has_branch_placeholder": false
"name": ".aipass"
},
"d004": {
"path": ".archive",
"name": ".archive",
"has_branch_placeholder": false
"name": ".archive"
},
"d005": {
"path": ".claude",
"name": ".claude",
"has_branch_placeholder": false
"name": ".claude"
},
"d006": {
"path": ".seedgo",
"name": ".seedgo",
"has_branch_placeholder": false
},
"d023": {
"path": ".spawn",
"name": ".spawn",
"has_branch_placeholder": false
"name": ".seedgo"
},
"d007": {
"path": ".trinity",
"name": ".trinity",
"has_branch_placeholder": false
"name": ".trinity"
},
"d008": {
"path": "apps",
"name": "apps",
"has_branch_placeholder": false
"name": "apps"
},
"d009": {
"path": "apps/handlers",
"name": "handlers",
"has_branch_placeholder": false
"name": "handlers"
},
"d010": {
"path": "apps/modules",
"name": "modules",
"has_branch_placeholder": false
"name": "modules"
},
"d011": {
"path": "apps/plugins",
"name": "plugins",
"has_branch_placeholder": false
"name": "plugins"
},
"d012": {
"path": "artifacts",
"name": "artifacts",
"has_branch_placeholder": false
"name": "artifacts"
},
"d013": {
"path": "docs",
"name": "docs",
"has_branch_placeholder": false
},
"d019": {
"path": "docs.local",
"name": "docs.local",
"has_branch_placeholder": false
},
"d020": {
"path": "docs.local/sub_agent_drops",
"name": "sub_agent_drops",
"has_branch_placeholder": false
"name": "docs"
},
"d014": {
"path": "dropbox",
"name": "dropbox",
"has_branch_placeholder": false
},
"d021": {
"path": "logs",
"name": "logs",
"has_branch_placeholder": false
"path": "docs.local",
"name": "docs.local"
},
"d015": {
"path": "templates",
"name": "templates",
"has_branch_placeholder": false
"path": "docs.local/sub_agent_drops",
"name": "sub_agent_drops"
},
"d016": {
"path": "tests",
"name": "tests",
"has_branch_placeholder": false
"path": "dropbox",
"name": "dropbox"
},
"d017": {
"path": "tools",
"name": "tools",
"has_branch_placeholder": false
"path": "logs",
"name": "logs"
},
"d018": {
"path": "templates",
"name": "templates"
},
"d019": {
"path": "tests",
"name": "tests"
},
"d020": {
"path": "tools",
"name": "tools"
},
"d021": {
"path": "{{BRANCH}}_json",
"name": "{{BRANCH}}_json",
"has_branch_placeholder": true
"name": "{{BRANCH}}_json"
},
"d022": {
"path": "{{BRANCH}}_json/custom_config",
"name": "custom_config",
"has_branch_placeholder": false
"name": "custom_config"
}
}
}
@@ -274,3 +274,106 @@ class TestTemplateStructure:
"""Old agent.template directory should not exist."""
spawn_root = Path(__file__).parents[1]
assert not (spawn_root / "templates" / "agent.template").exists()
def test_builder_template_has_claude_md(self):
"""Builder template includes CLAUDE.md with startup protocol."""
from aipass.spawn.apps.handlers.class_registry import get_template_dir
builder = get_template_dir("builder")
claude_md = builder / "CLAUDE.md"
assert claude_md.exists(), "Builder template must include CLAUDE.md"
content = claude_md.read_text()
assert "{{BRANCHNAME}}" in content
assert "Startup" in content
assert ".trinity/passport.json" in content
def test_builder_template_has_local_prompt(self):
"""Builder template includes non-empty local prompt."""
from aipass.spawn.apps.handlers.class_registry import get_template_dir
builder = get_template_dir("builder")
prompt = builder / ".aipass" / "aipass_local_prompt.md"
assert prompt.exists()
content = prompt.read_text()
assert len(content) > 100, "Local prompt should have substantial content"
assert "{{BRANCHNAME}}" in content
# =============================================================================
# AGENT SCAFFOLD CONTENT TESTS
# =============================================================================
class TestAgentScaffoldContent:
"""Tests verifying created agents have useful content."""
def test_created_agent_has_claude_md(self, tmp_path):
"""Created agent should have CLAUDE.md with branch name substituted."""
from aipass.spawn.apps.modules.core import _spawn_agent
target = tmp_path / "content_test"
_spawn_agent(str(target), role="Tester", purpose="Testing scaffold")
claude_md = target / "CLAUDE.md"
assert claude_md.exists()
content = claude_md.read_text()
assert "CONTENT_TEST" in content
assert "{{BRANCHNAME}}" not in content
assert ".trinity/passport.json" in content
def test_created_agent_local_prompt_has_content(self, tmp_path):
"""Created agent's local prompt should reference branch identity."""
from aipass.spawn.apps.modules.core import _spawn_agent
target = tmp_path / "prompt_agent"
_spawn_agent(str(target), purpose="Testing prompt")
prompt = target / ".aipass" / "aipass_local_prompt.md"
assert prompt.exists()
content = prompt.read_text()
assert "PROMPT_AGENT" in content
assert len(content) > 100
def test_created_agent_claude_md_has_role(self, tmp_path):
"""CLAUDE.md should include the agent's role if provided."""
from aipass.spawn.apps.modules.core import _spawn_agent
target = tmp_path / "role_test"
_spawn_agent(str(target), role="Data Analyst", purpose="Reports")
content = (target / "CLAUDE.md").read_text()
assert "Data Analyst" in content
# =============================================================================
# MULTI-AGENT COEXISTENCE TESTS
# =============================================================================
class TestMultiAgentCoexistence:
"""Tests verifying multiple agents can coexist in the same registry."""
def test_two_agents_same_registry(self, tmp_path):
"""Two agents created with the same registry both register."""
from aipass.spawn.apps.modules.core import _spawn_agent
reg = tmp_path / "TEST_REGISTRY.json"
reg.write_text('{"metadata":{"version":"1.0.0","total_branches":0},"branches":[]}')
r1 = _spawn_agent(str(tmp_path / "agent_a"), registry_path=str(reg))
r2 = _spawn_agent(str(tmp_path / "agent_b"), registry_path=str(reg))
assert r1["success"] is True
assert r2["success"] is True
data = json.loads(reg.read_text())
names = [b["name"] for b in data["branches"]]
assert "AGENT_A" in names
assert "AGENT_B" in names
assert data["metadata"]["total_branches"] == 2
def test_three_agents_distinct_identities(self, tmp_path):
"""Three agents in same registry have distinct passports."""
from aipass.spawn.apps.modules.core import _spawn_agent
reg = tmp_path / "TEST_REGISTRY.json"
reg.write_text('{"metadata":{"version":"1.0.0","total_branches":0},"branches":[]}')
for name in ["alpha", "beta", "gamma"]:
_spawn_agent(str(tmp_path / name), registry_path=str(reg), purpose=f"{name} purpose")
for name in ["alpha", "beta", "gamma"]:
passport = json.loads((tmp_path / name / ".trinity" / "passport.json").read_text())
assert passport["branch_info"]["branch_name"] == name.upper()
assert passport["identity"]["citizen_class"] == "builder"
@@ -138,6 +138,54 @@ class TestCreateDryRun:
assert result == 1
class TestTemplateFlag:
"""Tests for --template flag as class selector."""
def test_template_flag_selects_class(self, tmp_path):
"""--template birthright creates birthright-class agent."""
from aipass.spawn.apps.spawn import handle_create
target = str(tmp_path / "tmpl_test")
with patch("aipass.spawn.apps.spawn.console"), \
patch("aipass.spawn.apps.spawn.header"):
result = handle_create([target, "--template", "birthright"])
assert result == 0
# Birthright class: has .trinity but no apps/
assert (tmp_path / "tmpl_test" / ".trinity").exists()
assert not (tmp_path / "tmpl_test" / "apps").exists()
def test_template_flag_overrides_positional_class(self, tmp_path):
"""--template overrides positional class arg."""
from aipass.spawn.apps.spawn import handle_create
target = str(tmp_path / "override_test")
# Positional says builder, flag says birthright
with patch("aipass.spawn.apps.spawn.console"), \
patch("aipass.spawn.apps.spawn.header"):
result = handle_create(["builder", target, "--template", "birthright"])
assert result == 0
# --template wins: birthright has no apps/
assert not (tmp_path / "override_test" / "apps").exists()
def test_template_flag_unknown_treated_as_path(self, tmp_path):
"""--template with unknown value is treated as path (backward compat)."""
from aipass.spawn.apps.spawn import handle_create
target = str(tmp_path / "path_test")
with patch("aipass.spawn.apps.spawn.console"), \
patch("aipass.spawn.apps.spawn.error") as mock_error:
result = handle_create([target, "--template", "/nonexistent/path"])
assert result == 1
mock_error.assert_called_once()
def test_template_flag_dry_run(self, tmp_path):
"""--template works with --dry-run."""
from aipass.spawn.apps.spawn import handle_create
target = str(tmp_path / "drytest")
with patch("aipass.spawn.apps.spawn.console"), \
patch("aipass.spawn.apps.spawn.header"):
result = handle_create([target, "--template", "birthright", "--dry-run"])
assert result == 0
assert not (tmp_path / "drytest").exists()
class TestPassportHelp:
"""Tests for passport --help interception."""