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:
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user