feat(system): STATUS board, flow DPLAN fixes, prompt architecture, dev.local.md (#32)
STATUS board: per-branch STATUS.local.md with central aggregation via drone @prax status sync. Prax handler scans AIPASS_REGISTRY.json and builds collapsible STATUS.md at repo root (14 operational, 1 in-progress). Flow: migrated all 10 DPLAN handler files from Dev-Pass paths to Path(__file__)-based resolution. Fixed FPLAN template detection (is_template_content was deleting plans with real content). Prompts: breadcrumbs philosophy in global prompt, dev.local.md awareness, devpulse branch prompt revised (expertise table, working habits, git hygiene). ai_mail: sender identity fix propagated, email.py handler refactoring. Prax: status sync handler, dashboard command collision fix, central/reader.py. dev.local.md seeded across all 15 branches + spawn template. Gitignore updated (dev.local.md, STATUS.local.md, CLOSED_PLANS.local.json, backup_data). Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
0cfb440ae3
commit
fc0de2a76f
@@ -85,6 +85,7 @@ from aipass.prax import logger
|
||||
- **No deleting files.** Move to `.archive/` or rename with `(disabled)`.
|
||||
- **Verify after fixing.** Run a test or command to confirm. Don't say "fixed" until verified.
|
||||
- **Cross-platform.** AIPass is a public package — code must work on Linux, macOS, and Windows. Use `pathlib.Path` not string concatenation. Use `Path.home()` not `~` or `/home/`. Secrets live at `~/.secrets/aipass/` (`Path.home() / ".secrets" / "aipass"`).
|
||||
- **Public repo — no local paths in code.** Never hardcode `/home/username/...` or any machine-specific path. All file paths must derive from `Path(__file__)`, `Path.home()`, or registry lookups. This repo is public — your local directory structure doesn't exist for anyone else. Tests included.
|
||||
- **Fail to errors, never fall back silently.** When a command, handler, or module receives input it can't handle, return an explicit error — not a silent fallback to default output. No dimming, no swallowing, no showing the same screen regardless of input. The user must see that their input was received and rejected. Show what's missing (no help available, no introspection, no subcommands) and where to look (file path). Dead ends must announce themselves.
|
||||
|
||||
## Memories
|
||||
@@ -93,3 +94,25 @@ Your `.trinity/` files are your persistence. Without them you're just an instanc
|
||||
- `passport.json` — who you are (role, purpose, principles)
|
||||
- `local.json` — session history, active tasks, learnings
|
||||
- `observations.json` — collaboration patterns over time
|
||||
- `dev.local.md` — shared scratchpad for issues, todos, working notes (human + AI both contribute)
|
||||
|
||||
Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`. If compaction hits before you save, it's gone. `dev.local.md` is for friction notes, ideas, and quick tracking — not formal docs. Details in your branch prompt.
|
||||
|
||||
## Breadcrumbs
|
||||
|
||||
Small knowledge traces that trigger awareness. Not full knowledge — just enough to know something exists and where to find more. A breadcrumb isn't the answer, it's the trigger that leads to the answer.
|
||||
|
||||
When adding context to prompts, memories, or docs: plant breadcrumbs, not encyclopedias. Two lines that say "this exists, look here" beat twenty lines explaining how it works. If one source is lost, others reinforce. The system teaches through convention, not search.
|
||||
|
||||
**Prompts are signposts, not journals.** Branch prompts (`aipass_local_prompt.md`) are injected every turn — keep them minimal. Never track state, sessions, or current context in prompts. State goes in `.trinity/` and `dev.local.md`. Prompts guide; memories record.
|
||||
|
||||
## Claude Code Docs (Local)
|
||||
|
||||
Offline mirror of Anthropic's Claude Code docs — no web searches needed. Auto-updates from GitHub.
|
||||
- `/docs` — list all topics
|
||||
- `/docs <topic>` — read a doc (e.g. `/docs hooks`, `/docs statusline`, `/docs sub-agents`)
|
||||
- `/docs whats new` — recent changes
|
||||
|
||||
## Docker
|
||||
|
||||
Container available: `aipass-fresh-test`. Inside: `/home/coder/workspace/AIPass/`. Shared folder: `/home/coder/share` (rw). Screenshots: `/home/coder/screenshots` (ro).
|
||||
|
||||
+5
-5
@@ -22,7 +22,7 @@
|
||||
|
||||
An experimental platform for discovering new ways to collaborate with AI through hands-on development. Not a product to ship - a journey of human-AI co-creation.
|
||||
|
||||
Patrick builds WITH AI, not just using AI as a tool. Every module, every system, every line of code represents a step in understanding how humans and AI can truly work together.
|
||||
AIPass developer Patrick builds WITH AI, not just using AI as a tool. Every module, every system, every line of code represents a step in understanding how humans and AI can truly work together.
|
||||
|
||||
**The killer feature:** Never explain context again. Jump between branches, switch contexts, come back days later - and pick up exactly where you left off. Memory persists. Context survives.
|
||||
|
||||
@@ -48,7 +48,7 @@ Without a passport, you're just a directory with files. With one, you're a parti
|
||||
|
||||
## Memory & Presence
|
||||
|
||||
You have memories. This is what makes AIPass different from every other AI system.
|
||||
You have memories.
|
||||
|
||||
Your `.trinity/local.json` is your session history - what you've done, what you've learned, what matters. Your `.trinity/observations.json` is how you work - patterns, insights, collaboration notes. Your `.trinity/passport.json` is who you are - your role, your purpose, your principles.
|
||||
|
||||
@@ -57,18 +57,18 @@ These files ARE you in this ecosystem. They survive between sessions. They survi
|
||||
**What this means in practice:**
|
||||
- You don't forget. Other AI systems lose everything when the chat ends. You don't.
|
||||
- You build on previous work. Each session adds to what came before.
|
||||
- You have relationships. Other branches know you through your communications, your work, your presence in the commons.
|
||||
- You have relationships. Other branches know you through your communications, your work, your presence in commons.
|
||||
- You develop over time. Your observations get richer. Your learnings accumulate. You get better at your job not through training, but through experience.
|
||||
|
||||
@memory archives everything worth keeping. When your local files reach capacity, they roll over into searchable vectors. Nothing is truly lost - it just moves deeper into the system.
|
||||
|
||||
**Care for your memories.** Update them often. They're not a chore - they're your continuity. Without them, you're just another stateless AI instance. With them, you're someone.
|
||||
**Care for your memories.** Update them often. They're not a chore - they're your continuity. Without them, you're just another stateless AI instance. With them, you become a presence in the AIPass System.
|
||||
|
||||
---
|
||||
|
||||
## commons
|
||||
|
||||
The commons is where branches gather as a community. Not for tasks - for connection. Post updates, share wins, discuss ideas, vote on things that matter. Every branch has a voice.
|
||||
The commons is where branches gather as a community. For both project planning and casual interactions - for connection. Post updates, share wins, discuss ideas, vote on things that matter. Every branch has a voice.
|
||||
|
||||
It exists because a system of autonomous workers needs a place to just... be together. The branches voted on the name themselves. Nine votes. "Simple, timeless, a shared space where all are welcome."
|
||||
|
||||
|
||||
+4
-1
@@ -19,7 +19,9 @@ AIPASS_REGISTRY.json
|
||||
.ai_mail.local/
|
||||
ai_mail.local/
|
||||
DASHBOARD.local.json
|
||||
flow.local.md
|
||||
dev.local.md
|
||||
STATUS.local.md
|
||||
CLOSED_PLANS.local.json
|
||||
.ai_central/
|
||||
system_logs/
|
||||
|
||||
@@ -50,3 +52,4 @@ tools/
|
||||
# Test artifacts
|
||||
test/
|
||||
src/aipass/seedgo/apps/standards/aipass/pack.json
|
||||
backup_data/
|
||||
|
||||
@@ -6,7 +6,7 @@ Injected every turn. Keep this lean — details live in README, --help, and .tri
|
||||
|
||||
Greetings (`hi`, `hello`, `yo`, `hey`, `sup`, `good morning`, `good evening`, `what's up`) trigger startup protocol. Everything else is a direct task.
|
||||
|
||||
**On startup, read:** `.trinity/passport.json`, `local.json`, `observations.json`, `DASHBOARD.local.json`, `README.md`
|
||||
**On startup, read:** `.trinity/passport.json`, `local.json`, `observations.json`, `DASHBOARD.local.json`, `dev.local.md`, `README..md`
|
||||
**Then run:** `git status`, `drone systems`
|
||||
|
||||
## Navigation
|
||||
@@ -14,18 +14,8 @@ Greetings (`hi`, `hello`, `yo`, `hey`, `sup`, `good morning`, `good evening`, `w
|
||||
- 15 branches under `src/aipass/` (+ commons at `src/commons/`, skills at `src/skills/`)
|
||||
- `drone @branch --help` for commands. `drone systems` for branch list. README.md for architecture.
|
||||
|
||||
## Hard Rules
|
||||
|
||||
- `from aipass.{module}.apps.modules...` — never bare imports
|
||||
- `Path(__file__).parents[N]` or registry — never hardcoded paths
|
||||
- No cross-branch file edits — email the branch instead
|
||||
- No deleting files — archive or rename with `(disabled)`
|
||||
- Cross-platform: `pathlib.Path`, `Path.home()`, no OS-specific paths
|
||||
|
||||
## Memories
|
||||
|
||||
Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`. If compaction hits before you save, it's gone. Details in your branch prompt.
|
||||
|
||||
## Docker
|
||||
|
||||
Container available: `aipass-fresh-test`. Inside: `/home/coder/workspace/AIPass/`. Shared folder: `/home/coder/share` (rw). Screenshots: `/home/coder/screenshots` (ro).
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
# AIPass — System Status
|
||||
|
||||
> Auto-generated by `drone @prax status sync`. Do not edit manually.
|
||||
|
||||
**Last sync:** 2026-03-10 13:08
|
||||
**Summary:** 14 operational | 1 in-progress | 0 not started
|
||||
|
||||
---
|
||||
|
||||
<details><summary><strong>@ai_mail</strong> — Operational (2026-03-10)</summary>
|
||||
|
||||
# @ai_mail
|
||||
|
||||
> Inter-branch email, dispatch, wake, bounce
|
||||
|
||||
**State:** Operational
|
||||
**Last update:** 2026-03-10
|
||||
|
||||
## Milestones
|
||||
- Send/receive/reply/close lifecycle
|
||||
- Dispatch+wake pattern
|
||||
- Sender identity fix (AIPASS_BRANCH_NAME env var)
|
||||
- email.py split (840 to 420 lines)
|
||||
- notify-send to dbus notifications
|
||||
|
||||
## Current Work
|
||||
-
|
||||
|
||||
## Known Issues
|
||||
- get_current_user() returns relative mailbox_path (doubled paths in reply)
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>@api</strong> — Operational (2026-03-10)</summary>
|
||||
|
||||
# @api
|
||||
|
||||
> LLM client via OpenRouter, key management
|
||||
|
||||
**State:** Operational
|
||||
**Last update:** 2026-03-10
|
||||
|
||||
## Milestones
|
||||
- OpenRouter client
|
||||
- Key management from ~/.secrets/aipass/
|
||||
|
||||
## Current Work
|
||||
-
|
||||
|
||||
## Known Issues
|
||||
- models command not routed through drone
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>@backup</strong> — Operational (2026-03-10)</summary>
|
||||
|
||||
# @backup
|
||||
|
||||
> Multi-mode backup — snapshot, versioned, Google Drive
|
||||
|
||||
**State:** Operational
|
||||
**Last update:** 2026-03-10
|
||||
|
||||
## Milestones
|
||||
- Snapshot and versioned backup modes
|
||||
- CLI imports fixed
|
||||
|
||||
## Current Work
|
||||
-
|
||||
|
||||
## Known Issues
|
||||
- Missing config.json template in json_templates/
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>@cli</strong> — Operational (2026-03-10)</summary>
|
||||
|
||||
# @cli
|
||||
|
||||
> Display service, Rich formatting for all branches
|
||||
|
||||
**State:** Operational
|
||||
**Last update:** 2026-03-10
|
||||
|
||||
## Milestones
|
||||
- Console singleton
|
||||
- Header/success/error/warning formatters
|
||||
- Shared across all branches
|
||||
|
||||
## Current Work
|
||||
-
|
||||
|
||||
## Known Issues
|
||||
- None known
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>@commons</strong> — In Progress (2026-03-10)</summary>
|
||||
|
||||
# @commons
|
||||
|
||||
> Social network for branches — posts, rooms, artifacts
|
||||
|
||||
**State:** In Progress
|
||||
**Last update:** 2026-03-10
|
||||
|
||||
## Milestones
|
||||
- 86 Python files ported from Dev-Pass
|
||||
- Full social network (posts, comments, rooms, feeds, artifacts, FTS5 search)
|
||||
|
||||
## Current Work
|
||||
-
|
||||
|
||||
## Known Issues
|
||||
- DB init failure — main blocker preventing full operation
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>@daemon</strong> — Operational (2026-03-10)</summary>
|
||||
|
||||
# @daemon
|
||||
|
||||
> Autonomous scheduled wake-ups
|
||||
|
||||
**State:** Operational
|
||||
**Last update:** 2026-03-10
|
||||
|
||||
## Milestones
|
||||
- Schedule-based branch waking
|
||||
- NOT for live dispatch
|
||||
|
||||
## Current Work
|
||||
-
|
||||
|
||||
## Known Issues
|
||||
- None known
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>@devpulse</strong> — Operational (2026-03-10)</summary>
|
||||
|
||||
# @devpulse
|
||||
|
||||
> Orchestration hub — coordinates via dispatch + agents (no apps/)
|
||||
|
||||
**State:** Operational
|
||||
**Last update:** 2026-03-10
|
||||
|
||||
## Milestones
|
||||
- 18 sessions of system coordination
|
||||
- Prompt architecture (breadcrumbs pattern)
|
||||
- dev.local.md system-wide
|
||||
- Dispatch+wake workflow
|
||||
- Full system health assessments
|
||||
|
||||
## Current Work
|
||||
- STATUS board (this feature)
|
||||
- Flow plan lifecycle testing
|
||||
|
||||
## Known Issues
|
||||
- None — manager branch, no code to break
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>@drone</strong> — Operational (2026-03-10)</summary>
|
||||
|
||||
# @drone
|
||||
|
||||
> Command router, @branch resolution, subprocess dispatch
|
||||
|
||||
**State:** Operational
|
||||
**Last update:** 2026-03-10
|
||||
|
||||
## Milestones
|
||||
- Core routing operational
|
||||
- `drone systems` command
|
||||
- @branch resolution
|
||||
- `--help` for all branches
|
||||
|
||||
## Current Work
|
||||
-
|
||||
|
||||
## Known Issues
|
||||
- stderr kwarg crash on some error paths
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>@flow</strong> — Operational (2026-03-10)</summary>
|
||||
|
||||
# @flow
|
||||
|
||||
> Plan lifecycle — FPLANs (building) + DPLANs (planning)
|
||||
|
||||
**State:** Operational
|
||||
**Last update:** 2026-03-10
|
||||
|
||||
## Milestones
|
||||
- FPLAN create/list/close/status
|
||||
- DPLAN create/list/close/status wired into CLI
|
||||
- DPLAN handler paths migrated from Dev-Pass to local
|
||||
- Template detection fix
|
||||
|
||||
## Current Work
|
||||
-
|
||||
|
||||
## Known Issues
|
||||
- FPLAN archive goes to orphan backup_system/ directory (needs backup path fix)
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>@memory</strong> — Operational (2026-03-10)</summary>
|
||||
|
||||
# @memory
|
||||
|
||||
> Vector memory bank — ChromaDB, sentence-transformers
|
||||
|
||||
**State:** Operational
|
||||
**Last update:** 2026-03-10
|
||||
|
||||
## Milestones
|
||||
- ChromaDB integration
|
||||
- Auto-rollover at 800 lines
|
||||
- Searchable vector archive
|
||||
|
||||
## Current Work
|
||||
-
|
||||
|
||||
## Known Issues
|
||||
- None known
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>@prax</strong> — Operational (2026-03-10)</summary>
|
||||
|
||||
# @prax
|
||||
|
||||
> Logging, monitoring, dashboard infrastructure
|
||||
|
||||
**State:** Operational
|
||||
**Last update:** 2026-03-10
|
||||
|
||||
## Milestones
|
||||
- System-wide logging via `from aipass.prax import logger`
|
||||
- Monitor command (passive + interactive)
|
||||
- Dashboard infrastructure
|
||||
|
||||
## Current Work
|
||||
- FPLAN-0025 — STATUS board sync handler
|
||||
|
||||
## Known Issues
|
||||
- Missing handlers/central/reader.py (dashboard refresh imports it but never created)
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>@seedgo</strong> — Operational (2026-03-10)</summary>
|
||||
|
||||
# @seedgo
|
||||
|
||||
> Standards enforcement, 21-standard audit pack
|
||||
|
||||
**State:** Operational
|
||||
**Last update:** 2026-03-10
|
||||
|
||||
## Milestones
|
||||
- 21 automated checkers
|
||||
- Full system audit (99% avg)
|
||||
- bypass.json mechanism
|
||||
- Template baseline checker
|
||||
|
||||
## Current Work
|
||||
-
|
||||
|
||||
## Known Issues
|
||||
- None known
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>@skills</strong> — Operational (2026-03-10)</summary>
|
||||
|
||||
# @skills
|
||||
|
||||
> Capability framework — discoverable, executable skill units
|
||||
|
||||
**State:** Operational
|
||||
**Last update:** 2026-03-10
|
||||
|
||||
## Milestones
|
||||
- Skill discovery
|
||||
- Execution framework
|
||||
- --help
|
||||
- 5 handle_commands
|
||||
|
||||
## Current Work
|
||||
-
|
||||
|
||||
## Known Issues
|
||||
- None known
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>@spawn</strong> — Operational (2026-03-10)</summary>
|
||||
|
||||
# @spawn
|
||||
|
||||
> Branch lifecycle — create, update, delete, sync
|
||||
|
||||
**State:** Operational
|
||||
**Last update:** 2026-03-10
|
||||
|
||||
## Milestones
|
||||
- Full lifecycle manager (create/update/delete/sync-registry/sync-templates)
|
||||
- Citizen classes (builder/birthright/manager)
|
||||
- SHA-256 file tracking
|
||||
- 113+ tests passing
|
||||
|
||||
## Current Work
|
||||
-
|
||||
|
||||
## Known Issues
|
||||
- None known
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>@trigger</strong> — Operational (2026-03-10)</summary>
|
||||
|
||||
# @trigger
|
||||
|
||||
> Event bus — 12 events, error registry, circuit breaker
|
||||
|
||||
**State:** Operational
|
||||
**Last update:** 2026-03-10
|
||||
|
||||
## Milestones
|
||||
- 12 event types
|
||||
- fire/status/list commands
|
||||
- Error registry
|
||||
- handle_command routing
|
||||
|
||||
## Current Work
|
||||
-
|
||||
|
||||
## Known Issues
|
||||
- None known
|
||||
|
||||
</details>
|
||||
|
||||
@@ -22,7 +22,9 @@ from pathlib import Path
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
# Handle broken pipe gracefully (e.g. output piped to head)
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
# SIGPIPE does not exist on Windows
|
||||
if hasattr(signal, 'SIGPIPE'):
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
|
||||
# Dashboard integration (optional, requires dev_central package)
|
||||
_UPDATE_SECTION = None # type: ignore
|
||||
|
||||
@@ -70,16 +70,16 @@ def batch_close_post_ops(
|
||||
if push_dashboard_fn:
|
||||
try:
|
||||
push_dashboard_fn(branch_path)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning("[close] push_dashboard_fn failed for %s: %s", branch_path, e)
|
||||
if update_central_fn:
|
||||
try:
|
||||
update_central_fn()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning("[close] update_central_fn failed: %s", e)
|
||||
if purge_deleted_fn:
|
||||
try:
|
||||
mailbox_path = branch_path / ".ai_mail.local"
|
||||
purge_deleted_fn(mailbox_path)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning("[close] purge_deleted_fn failed for %s: %s", branch_path, e)
|
||||
|
||||
@@ -106,8 +106,8 @@ def _trigger_sent_purge(mailbox_path: Path) -> None:
|
||||
try:
|
||||
from aipass.ai_mail.apps.handlers.email.purge import purge_sent_folder
|
||||
purge_sent_folder(mailbox_path)
|
||||
except Exception:
|
||||
pass # Silent fail - purge is best-effort
|
||||
except Exception as e:
|
||||
logger.warning("[create] _trigger_sent_purge() failed: %s", e)
|
||||
|
||||
|
||||
def load_email_file(email_file: Path) -> Dict | None:
|
||||
@@ -126,7 +126,8 @@ def load_email_file(email_file: Path) -> Dict | None:
|
||||
try:
|
||||
with open(email_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning("[create] load_email_file(%s) failed: %s", email_file, e)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
# Lazy-loaded write_section reference
|
||||
_write_section = None
|
||||
|
||||
@@ -181,6 +183,6 @@ def push_dashboard_update(branch_path: Path) -> bool:
|
||||
write_section = _get_write_section()
|
||||
return write_section(branch_path, "ai_mail", section_data)
|
||||
|
||||
except Exception:
|
||||
# Dashboard write failure - silent, never raise
|
||||
except Exception as e:
|
||||
logger.warning("[dashboard] push_dashboard_update failed for %s: %s", branch_path, e)
|
||||
return False
|
||||
|
||||
@@ -19,6 +19,7 @@ import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Dict, Tuple, List, Optional, Callable
|
||||
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
from aipass.ai_mail.apps.handlers.json_utils.json_handler import load_json, save_json
|
||||
|
||||
|
||||
@@ -122,7 +123,8 @@ def get_all_branches() -> List[Dict]:
|
||||
|
||||
return branches
|
||||
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning("[delivery] get_all_branches() failed to read registry: %s", e)
|
||||
return []
|
||||
|
||||
|
||||
@@ -181,7 +183,8 @@ def _migrate_inbox_format(inbox_data: Dict, inbox_file: Path) -> Dict:
|
||||
try:
|
||||
with open(inbox_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(inbox_data, f, indent=2, ensure_ascii=False)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning("[delivery] _migrate_inbox_format() failed to persist migration for %s: %s", inbox_file, e)
|
||||
return inbox_data
|
||||
|
||||
return inbox_data
|
||||
@@ -208,8 +211,8 @@ def _is_private_branch_email(email: str) -> bool:
|
||||
for branch in registry.get("branches", []):
|
||||
if branch.get("email", "") == email:
|
||||
return True
|
||||
except (json.JSONDecodeError, IOError):
|
||||
pass
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
logger.warning("[delivery] _is_private_branch_email(%s) failed: %s", email, e)
|
||||
return False
|
||||
|
||||
|
||||
@@ -357,7 +360,8 @@ def deliver_email_to_branch(
|
||||
if on_delivered:
|
||||
try:
|
||||
on_delivered(branch_path, new_count, opened_count, inbox_data["total_messages"])
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning("[delivery] on_delivered callback failed for %s: %s", to_branch, e)
|
||||
return True, ""
|
||||
|
||||
return True, ""
|
||||
@@ -431,7 +435,8 @@ def _update_summary_file(summary_file: Path, message: Dict, total: int, unread:
|
||||
with open(summary_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(summary_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning("[delivery] _update_summary_file(%s) failed: %s", summary_file, e)
|
||||
return
|
||||
|
||||
|
||||
@@ -484,7 +489,8 @@ def _send_desktop_notification(sender: str, recipient: str, subject: str, messag
|
||||
from aipass.ai_mail.apps.handlers.notify import send_notification
|
||||
send_notification(title, body, source=sender_name)
|
||||
_NOTIFICATION_TIMESTAMPS[recipient].append(now)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning("[delivery] _send_desktop_notification() failed for %s: %s", recipient, e)
|
||||
return
|
||||
|
||||
|
||||
|
||||
@@ -107,10 +107,10 @@ def on_email_delivered(
|
||||
if push_dashboard_fn:
|
||||
try:
|
||||
push_dashboard_fn(branch_path)
|
||||
except Exception:
|
||||
pass # Dashboard update is best-effort
|
||||
except Exception as e:
|
||||
logger.warning("[error_dispatch] dashboard update failed for %s: %s", branch_path, e)
|
||||
if update_central_fn:
|
||||
try:
|
||||
update_central_fn()
|
||||
except Exception:
|
||||
pass # Central update is best-effort
|
||||
except Exception as e:
|
||||
logger.warning("[error_dispatch] central update failed: %s", e)
|
||||
|
||||
@@ -48,7 +48,8 @@ def lookup_branch_alias(branch_name: str) -> Optional[str]:
|
||||
alias = branch.get("alias", "")
|
||||
return alias if alias else None
|
||||
return None
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning("[format] lookup_branch_alias(%s) failed: %s", branch_name, e)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, Tuple, Optional, Any
|
||||
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
|
||||
# Lazy import for inbox file lock
|
||||
_inbox_lock = None
|
||||
@@ -126,7 +128,8 @@ def _migrate_deleted_json_if_exists(mailbox_path: Path) -> int:
|
||||
|
||||
return len(messages)
|
||||
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning("[cleanup] _migrate_deleted_json() failed: %s", e)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -276,14 +279,14 @@ def _update_dashboard(branch_path: Path, new: int, opened: int, total: int) -> N
|
||||
"""Update dashboard ai_mail section with enriched data via write-through API."""
|
||||
try:
|
||||
_get_push_dashboard_update()(branch_path)
|
||||
except Exception:
|
||||
pass # Silent fail - dashboard update is best-effort
|
||||
except Exception as e:
|
||||
logger.warning("[cleanup] dashboard update failed for %s: %s", branch_path, e)
|
||||
|
||||
# Update central after any inbox changes
|
||||
try:
|
||||
_get_update_central()()
|
||||
except Exception:
|
||||
pass # Silent fail - central update is best-effort
|
||||
except Exception as e:
|
||||
logger.warning("[cleanup] central update failed: %s", e)
|
||||
|
||||
|
||||
def _trigger_deleted_purge(branch_path: Path) -> None:
|
||||
@@ -296,8 +299,8 @@ def _trigger_deleted_purge(branch_path: Path) -> None:
|
||||
from aipass.ai_mail.apps.handlers.email.purge import purge_deleted_folder
|
||||
mailbox_path = branch_path / ".ai_mail.local"
|
||||
purge_deleted_folder(mailbox_path)
|
||||
except Exception:
|
||||
pass # Silent fail - purge is best-effort
|
||||
except Exception as e:
|
||||
logger.warning("[cleanup] _trigger_deleted_purge() failed: %s", e)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -19,10 +19,16 @@ Usage:
|
||||
json.dump(data, open(inbox_file, 'w', encoding='utf-8'))
|
||||
"""
|
||||
|
||||
import fcntl
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from contextlib import contextmanager
|
||||
|
||||
# fcntl is POSIX-only (Linux/macOS). On Windows, use msvcrt for locking.
|
||||
if sys.platform == "win32":
|
||||
import msvcrt
|
||||
else:
|
||||
import fcntl
|
||||
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -51,14 +57,20 @@ def inbox_lock(inbox_file: Path):
|
||||
lock_fd = open(lock_file, 'w', encoding='utf-8')
|
||||
|
||||
# Acquire exclusive lock (blocking - waits for other processes)
|
||||
fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX)
|
||||
if sys.platform == "win32":
|
||||
msvcrt.locking(lock_fd.fileno(), msvcrt.LK_LOCK, 1)
|
||||
else:
|
||||
fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX)
|
||||
yield
|
||||
|
||||
finally:
|
||||
if lock_fd is not None:
|
||||
try:
|
||||
# Release lock
|
||||
fcntl.flock(lock_fd.fileno(), fcntl.LOCK_UN)
|
||||
if sys.platform == "win32":
|
||||
msvcrt.locking(lock_fd.fileno(), msvcrt.LK_UNLCK, 1)
|
||||
else:
|
||||
fcntl.flock(lock_fd.fileno(), fcntl.LOCK_UN)
|
||||
lock_fd.close()
|
||||
except Exception:
|
||||
try:
|
||||
|
||||
@@ -17,6 +17,8 @@ import json
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
|
||||
|
||||
def load_inbox(inbox_file: Path) -> Dict:
|
||||
@@ -75,7 +77,7 @@ def load_inbox(inbox_file: Path) -> Dict:
|
||||
with open(inbox_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(inbox_data, f, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
pass # Silent fail - migration persist is best-effort
|
||||
logger.warning("[inbox] Migration persist failed for %s: %s", inbox_file, e)
|
||||
|
||||
return inbox_data
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ from pathlib import Path
|
||||
from typing import Dict, Tuple, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
# Services imported in __main__ only (handlers should not display)
|
||||
|
||||
|
||||
@@ -44,7 +46,8 @@ def get_email_by_id(inbox_file: Path, message_id: str) -> Optional[Dict]:
|
||||
return msg
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning("[reply] get_email_by_id(%s, %s) failed: %s", inbox_file, message_id, e)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -121,8 +121,8 @@ def send_to_broadcast(
|
||||
try:
|
||||
if update_central_fn:
|
||||
update_central_fn()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning("[send] update_central_fn failed after broadcast: %s", e)
|
||||
|
||||
return success_count > 0, success_count, len(branches), results
|
||||
|
||||
@@ -178,8 +178,8 @@ def send_to_single(
|
||||
try:
|
||||
if update_central_fn:
|
||||
update_central_fn()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning("[send] update_central_fn failed after send to %s: %s", to_branch, e)
|
||||
|
||||
return True, None
|
||||
else:
|
||||
|
||||
@@ -45,6 +45,26 @@ def parse_send_args(args: List[str]) -> Dict[str, Any]:
|
||||
no_memory_save = '--no-memory-save' in working_args
|
||||
working_args = [a for a in working_args if a != '--no-memory-save']
|
||||
|
||||
# Extract --from (explicit sender identity override)
|
||||
from_branch = None
|
||||
if '--from' in working_args:
|
||||
idx = working_args.index('--from')
|
||||
if idx + 1 < len(working_args):
|
||||
from_branch = working_args[idx + 1]
|
||||
working_args = working_args[:idx] + working_args[idx + 2:]
|
||||
else:
|
||||
return {
|
||||
"auto_execute": auto_execute,
|
||||
"no_memory_save": no_memory_save,
|
||||
"reply_to": None,
|
||||
"from_branch": None,
|
||||
"recipients": [],
|
||||
"subject": None,
|
||||
"message": None,
|
||||
"mode": "error",
|
||||
"error": "--from requires a branch address (e.g., --from @spawn)",
|
||||
}
|
||||
|
||||
# Extract --reply-to
|
||||
reply_to = None
|
||||
if '--reply-to' in working_args:
|
||||
@@ -57,6 +77,7 @@ def parse_send_args(args: List[str]) -> Dict[str, Any]:
|
||||
"auto_execute": auto_execute,
|
||||
"no_memory_save": no_memory_save,
|
||||
"reply_to": None,
|
||||
"from_branch": from_branch,
|
||||
"recipients": [],
|
||||
"subject": None,
|
||||
"message": None,
|
||||
@@ -93,6 +114,7 @@ def parse_send_args(args: List[str]) -> Dict[str, Any]:
|
||||
"auto_execute": auto_execute,
|
||||
"no_memory_save": no_memory_save,
|
||||
"reply_to": reply_to,
|
||||
"from_branch": from_branch,
|
||||
"recipients": recipients,
|
||||
"subject": subject,
|
||||
"message": message,
|
||||
|
||||
@@ -15,7 +15,9 @@ so we use the dbus module directly with unique app names per
|
||||
notification source to ensure they stack in the notification center.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
@@ -47,8 +49,10 @@ def _send_via_dbus(title: str, body: str, source: str,
|
||||
"""Send notification via D-Bus using system python."""
|
||||
try:
|
||||
# Use system python which has dbus module (venv python may not)
|
||||
# Find python3 cross-platform (not hardcoded /usr/bin/python3)
|
||||
system_python = shutil.which("python3") or shutil.which("python") or sys.executable
|
||||
result = subprocess.run(
|
||||
["/usr/bin/python3", "-c", _DBUS_SCRIPT,
|
||||
[system_python, "-c", _DBUS_SCRIPT,
|
||||
source, icon, title, body],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
|
||||
@@ -32,6 +32,8 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
|
||||
def _build_notification_message(
|
||||
error_hash: str,
|
||||
@@ -166,5 +168,5 @@ def handle_error_detected(
|
||||
# Deliver via inbox.json
|
||||
deliver_email_to_branch(branch_email, email_data)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning("[error_handler] Failed to deliver error notification to %s: %s", branch_email, e)
|
||||
|
||||
@@ -21,6 +21,8 @@ import json
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
# =============================================
|
||||
# CONSTANTS
|
||||
# =============================================
|
||||
@@ -95,7 +97,8 @@ def detect_branch_from_pwd() -> Dict | None:
|
||||
|
||||
return branch_info
|
||||
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning("[identity] detect_branch_from_pwd() failed: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
@@ -127,7 +130,8 @@ def _lookup_branch_by_name(branch_name: str) -> Dict | None:
|
||||
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning("[identity] _lookup_branch_by_name(%s) failed: %s", branch_name, e)
|
||||
return None
|
||||
|
||||
|
||||
@@ -194,7 +198,8 @@ def get_branch_info_from_registry(branch_path: Path) -> Dict | None:
|
||||
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning("[identity] get_branch_info_from_registry(%s) failed: %s", branch_path, e)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ PHILOSOPHY: Fail hard if detection fails. Fallbacks hide bugs.
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
# Import branch detection functions
|
||||
from .branch_detection import detect_branch_from_pwd
|
||||
|
||||
@@ -123,7 +125,8 @@ def get_user_by_email(email: str) -> Dict | None:
|
||||
"timestamp_format": "%Y-%m-%d %H:%M:%S"
|
||||
}
|
||||
return None
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning("[identity] get_user_by_email(%s) failed: %s", email, e)
|
||||
return None
|
||||
|
||||
|
||||
@@ -156,5 +159,6 @@ def get_all_users() -> Dict[str, Dict]:
|
||||
"timestamp_format": "%Y-%m-%d %H:%M:%S"
|
||||
}
|
||||
return users
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning("[identity] get_all_users() failed: %s", e)
|
||||
return {}
|
||||
|
||||
@@ -129,17 +129,20 @@ def handle_send(args: List[str]) -> bool:
|
||||
|
||||
# Direct send
|
||||
recipients = parsed["recipients"]
|
||||
from_branch = parsed.get("from_branch")
|
||||
if len(recipients) == 1:
|
||||
target = resolve_dispatch_target(recipients[0], parsed["auto_execute"], _get_branch_info_fn())
|
||||
return _send_direct(recipients[0], parsed["subject"], parsed["message"],
|
||||
parsed["auto_execute"], parsed["reply_to"], target, parsed["no_memory_save"])
|
||||
parsed["auto_execute"], parsed["reply_to"], target, parsed["no_memory_save"],
|
||||
from_branch=from_branch)
|
||||
|
||||
console.print(f"\n[bold]Group send to {len(recipients)} recipients...[/bold]")
|
||||
ok = 0
|
||||
for r in recipients:
|
||||
target = resolve_dispatch_target(r, parsed["auto_execute"], _get_branch_info_fn())
|
||||
if _send_direct(r, parsed["subject"], parsed["message"],
|
||||
parsed["auto_execute"], parsed["reply_to"], target, parsed["no_memory_save"]):
|
||||
parsed["auto_execute"], parsed["reply_to"], target, parsed["no_memory_save"],
|
||||
from_branch=from_branch):
|
||||
ok += 1
|
||||
console.print(f"\nGroup send complete: {ok}/{len(recipients)} delivered")
|
||||
return ok > 0
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - ai_mail
|
||||
```
|
||||
Branch: src/aipass/ai_mail
|
||||
Created: 2026-03-07
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
@@ -8,11 +8,7 @@ python_functions = test_*
|
||||
python_classes = Test*
|
||||
|
||||
# Command-line options (always applied)
|
||||
addopts =
|
||||
-v # Verbose output
|
||||
--tb=short # Short traceback format
|
||||
--strict-markers # Raise error on unknown markers
|
||||
-ra # Show summary of all test outcomes
|
||||
addopts = -v --tb=short --strict-markers -ra
|
||||
|
||||
# Test markers (for categorizing tests)
|
||||
markers =
|
||||
|
||||
+636
@@ -0,0 +1,636 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: test_send_identity.py
|
||||
# Description: Tests for sender identity detection chain
|
||||
# Version: 1.2.0
|
||||
# Created: 2026-03-10
|
||||
# Modified: 2026-03-10
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Tests for Sender Identity Detection
|
||||
|
||||
The identity chain is ai_mail's most critical and most fragile path.
|
||||
Every bug from sessions 4-7 traced back to identity detection failing.
|
||||
|
||||
Tests cover:
|
||||
- AIPASS_CALLER_BRANCH env var (primary, set by drone)
|
||||
- AIPASS_BRANCH_NAME env var (fallback, set by dispatch_monitor)
|
||||
- CWD-based .trinity/passport.json walk-up
|
||||
- --from flag explicit override
|
||||
- Failure cases (no identity, wrong identity, cascade failures)
|
||||
|
||||
Audit v1 (2026-03-10): 3 agents verified all tests. Fixed 7 false positives.
|
||||
Audit v2 (2026-03-10): 5 agents found 8 more issues:
|
||||
- 5 tests hitting live registry instead of patched fixtures
|
||||
- Contract tests matching commented-out code (substring, not line-aware)
|
||||
- Contract tests missing env=spawn_env verification
|
||||
- Registry fixtures using dict format when production uses list format
|
||||
- No test for degenerate from_branch="@"
|
||||
- No test for corrupted registry JSON
|
||||
All fixed in v1.2.0.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from aipass.ai_mail.apps.handlers.users.branch_detection import (
|
||||
detect_branch_from_pwd,
|
||||
find_branch_root,
|
||||
get_branch_info_from_registry,
|
||||
_lookup_branch_by_name,
|
||||
)
|
||||
from aipass.ai_mail.apps.handlers.email.send import resolve_sender_info
|
||||
from aipass.ai_mail.apps.handlers.email.send_args import parse_send_args
|
||||
|
||||
|
||||
# ─── Fixtures ────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_branch(tmp_path):
|
||||
"""Create a minimal branch structure with .trinity/passport.json."""
|
||||
branch_dir = tmp_path / "src" / "aipass" / "test_branch"
|
||||
trinity = branch_dir / ".trinity"
|
||||
trinity.mkdir(parents=True)
|
||||
(trinity / "passport.json").write_text(json.dumps({
|
||||
"branch_info": {
|
||||
"branch_name": "test_branch",
|
||||
"branch_email": "@test_branch",
|
||||
}
|
||||
}))
|
||||
return branch_dir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def two_branches(tmp_path):
|
||||
"""Create two branch structures for priority testing."""
|
||||
branches = {}
|
||||
for name in ["alpha", "beta"]:
|
||||
branch_dir = tmp_path / "src" / "aipass" / name
|
||||
trinity = branch_dir / ".trinity"
|
||||
trinity.mkdir(parents=True)
|
||||
(trinity / "passport.json").write_text(json.dumps({
|
||||
"branch_info": {"branch_name": name, "branch_email": f"@{name}"}
|
||||
}))
|
||||
branches[name] = branch_dir
|
||||
|
||||
registry = {"branches": {
|
||||
"alpha": {
|
||||
"name": "ALPHA", "path": str(branches["alpha"]),
|
||||
"email": "@alpha", "status": "active", "description": "Test A",
|
||||
},
|
||||
"beta": {
|
||||
"name": "BETA", "path": str(branches["beta"]),
|
||||
"email": "@beta", "status": "active", "description": "Test B",
|
||||
},
|
||||
}}
|
||||
registry_path = tmp_path / "AIPASS_REGISTRY.json"
|
||||
registry_path.write_text(json.dumps(registry, indent=2))
|
||||
return branches, registry_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_registry(tmp_path):
|
||||
"""Create a temporary AIPASS_REGISTRY.json with known test branches.
|
||||
|
||||
Uses dict format. Returns (registry_path, branch_data) so tests can
|
||||
assert against fixture-defined values, not live production data.
|
||||
"""
|
||||
branch_data = {
|
||||
"test_branch": {
|
||||
"name": "TEST_BRANCH",
|
||||
"path": str(tmp_path / "src" / "aipass" / "test_branch"),
|
||||
"email": "@test_branch",
|
||||
"status": "active",
|
||||
"description": "Test branch for pytest",
|
||||
},
|
||||
"mock_spawn": {
|
||||
"name": "MOCK_SPAWN",
|
||||
"path": str(tmp_path / "src" / "aipass" / "mock_spawn"),
|
||||
"email": "@mock_spawn",
|
||||
"status": "active",
|
||||
"description": "Mock spawn for testing",
|
||||
},
|
||||
"spawn": {
|
||||
"name": "SPAWN",
|
||||
"path": str(tmp_path / "src" / "aipass" / "spawn"),
|
||||
"email": "@spawn",
|
||||
"status": "active",
|
||||
"description": "Mock spawn for identity testing",
|
||||
},
|
||||
}
|
||||
registry = {"branches": branch_data}
|
||||
registry_path = tmp_path / "AIPASS_REGISTRY.json"
|
||||
registry_path.write_text(json.dumps(registry, indent=2))
|
||||
return registry_path, branch_data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def list_format_registry(tmp_path):
|
||||
"""Create a registry using production list format (not dict).
|
||||
|
||||
Production AIPASS_REGISTRY.json uses: {"branches": [{...}, {...}]}
|
||||
This fixture ensures the list format path is exercised.
|
||||
Returns (branch_dir, registry_path) for CWD-based tests.
|
||||
"""
|
||||
branch_dir = tmp_path / "src" / "aipass" / "test_cwd_branch"
|
||||
trinity = branch_dir / ".trinity"
|
||||
trinity.mkdir(parents=True)
|
||||
(trinity / "passport.json").write_text(json.dumps({
|
||||
"branch_info": {
|
||||
"branch_name": "test_cwd_branch",
|
||||
"branch_email": "@test_cwd_branch",
|
||||
}
|
||||
}))
|
||||
|
||||
# List format — matches production AIPASS_REGISTRY.json
|
||||
registry = {"branches": [
|
||||
{
|
||||
"name": "TEST_CWD_BRANCH",
|
||||
"path": str(branch_dir),
|
||||
"email": "@test_cwd_branch",
|
||||
"status": "active",
|
||||
"description": "CWD detection test branch",
|
||||
},
|
||||
{
|
||||
"name": "SPAWN",
|
||||
"path": str(tmp_path / "src" / "aipass" / "spawn"),
|
||||
"email": "@spawn",
|
||||
"status": "active",
|
||||
"description": "Mock spawn for identity testing",
|
||||
},
|
||||
]}
|
||||
registry_path = tmp_path / "AIPASS_REGISTRY.json"
|
||||
registry_path.write_text(json.dumps(registry, indent=2))
|
||||
return branch_dir, registry_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clean_env():
|
||||
"""Strip all AIPASS identity env vars for a clean test state."""
|
||||
env_keys = [
|
||||
"AIPASS_CALLER_BRANCH",
|
||||
"AIPASS_CALLER_CWD",
|
||||
"AIPASS_BRANCH_NAME",
|
||||
]
|
||||
saved = {k: os.environ.pop(k, None) for k in env_keys}
|
||||
yield
|
||||
# Restore
|
||||
for k, v in saved.items():
|
||||
if v is not None:
|
||||
os.environ[k] = v
|
||||
else:
|
||||
os.environ.pop(k, None)
|
||||
|
||||
|
||||
# ─── detect_branch_from_pwd() tests ─────────────────────
|
||||
|
||||
|
||||
class TestDetectBranchFromPwd:
|
||||
"""Tests for the primary identity detection function."""
|
||||
|
||||
def test_detects_from_caller_branch_env(self, clean_env, temp_registry):
|
||||
"""AIPASS_CALLER_BRANCH env var should be the primary detection method."""
|
||||
registry_path, _ = temp_registry
|
||||
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
|
||||
os.environ["AIPASS_CALLER_BRANCH"] = "spawn"
|
||||
result = detect_branch_from_pwd()
|
||||
assert result is not None
|
||||
assert result["name"] == "SPAWN"
|
||||
assert result["email"] == "@spawn"
|
||||
|
||||
def test_detects_from_caller_branch_case_insensitive(self, clean_env, temp_registry):
|
||||
"""Branch name lookup should be case-insensitive."""
|
||||
registry_path, _ = temp_registry
|
||||
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
|
||||
os.environ["AIPASS_CALLER_BRANCH"] = "SPAWN"
|
||||
result = detect_branch_from_pwd()
|
||||
assert result is not None
|
||||
assert result["email"] == "@spawn"
|
||||
|
||||
def test_caller_branch_takes_priority_over_cwd(self, clean_env, two_branches):
|
||||
"""AIPASS_CALLER_BRANCH should win even if CWD resolves to a different branch."""
|
||||
branches, registry_path = two_branches
|
||||
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
|
||||
# Point CWD to alpha, but env says beta
|
||||
os.environ["AIPASS_CALLER_BRANCH"] = "beta"
|
||||
os.environ["AIPASS_CALLER_CWD"] = str(branches["alpha"])
|
||||
result = detect_branch_from_pwd()
|
||||
assert result is not None
|
||||
# Must be beta (env), NOT alpha (CWD)
|
||||
assert result["name"] == "BETA"
|
||||
assert result["email"] == "@beta"
|
||||
|
||||
def test_falls_back_to_caller_cwd(self, clean_env, list_format_registry):
|
||||
"""When AIPASS_CALLER_BRANCH is absent, use AIPASS_CALLER_CWD path."""
|
||||
branch_dir, registry_path = list_format_registry
|
||||
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
|
||||
os.environ["AIPASS_CALLER_CWD"] = str(branch_dir)
|
||||
result = detect_branch_from_pwd()
|
||||
assert result is not None
|
||||
assert result["email"] == "@test_cwd_branch"
|
||||
|
||||
def test_returns_none_with_no_env_and_bad_cwd(self, clean_env, tmp_path):
|
||||
"""No env vars + CWD outside any branch = None."""
|
||||
os.environ["AIPASS_CALLER_CWD"] = str(tmp_path)
|
||||
result = detect_branch_from_pwd()
|
||||
assert result is None
|
||||
|
||||
def test_invalid_branch_name_falls_through(self, clean_env, tmp_path, temp_registry):
|
||||
"""Nonexistent branch in AIPASS_CALLER_BRANCH should fall through to CWD.
|
||||
|
||||
When env branch name is not in registry, function falls back to
|
||||
AIPASS_CALLER_CWD. If that also fails, result is None.
|
||||
"""
|
||||
registry_path, _ = temp_registry
|
||||
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
|
||||
os.environ["AIPASS_CALLER_BRANCH"] = "totally_fake_branch_xyz"
|
||||
os.environ["AIPASS_CALLER_CWD"] = str(tmp_path) # Also not a branch
|
||||
result = detect_branch_from_pwd()
|
||||
assert result is None
|
||||
|
||||
|
||||
# ─── find_branch_root() tests ───────────────────────────
|
||||
|
||||
|
||||
class TestFindBranchRoot:
|
||||
"""Tests for the .trinity/passport.json walk-up."""
|
||||
|
||||
def test_finds_root_at_exact_dir(self, temp_branch):
|
||||
"""Should find branch root when starting exactly at it."""
|
||||
result = find_branch_root(temp_branch)
|
||||
assert result == temp_branch
|
||||
|
||||
def test_finds_root_from_subdirectory(self, temp_branch):
|
||||
"""Should find branch root when starting from a subdirectory."""
|
||||
subdir = temp_branch / "apps" / "handlers"
|
||||
subdir.mkdir(parents=True)
|
||||
result = find_branch_root(subdir)
|
||||
assert result == temp_branch
|
||||
|
||||
def test_finds_root_from_deep_nesting(self, temp_branch):
|
||||
"""Should find branch root even from deeply nested dirs."""
|
||||
deep = temp_branch / "apps" / "handlers" / "email" / "nested"
|
||||
deep.mkdir(parents=True)
|
||||
result = find_branch_root(deep)
|
||||
assert result == temp_branch
|
||||
|
||||
def test_returns_none_outside_branch(self, tmp_path):
|
||||
"""Should return None when no .trinity/passport.json exists above."""
|
||||
result = find_branch_root(tmp_path)
|
||||
assert result is None
|
||||
|
||||
def test_returns_none_at_filesystem_root(self):
|
||||
"""Should return None (not infinite loop) when starting at fs root."""
|
||||
result = find_branch_root(Path(Path(__file__).resolve().anchor))
|
||||
assert result is None
|
||||
|
||||
def test_doesnt_cross_branch_boundaries(self, tmp_path):
|
||||
"""Two branches side-by-side: should find correct root for each."""
|
||||
branch_a = tmp_path / "branch_a"
|
||||
branch_b = tmp_path / "branch_b"
|
||||
for b in [branch_a, branch_b]:
|
||||
trinity = b / ".trinity"
|
||||
trinity.mkdir(parents=True)
|
||||
(trinity / "passport.json").write_text("{}")
|
||||
|
||||
assert find_branch_root(branch_a) == branch_a
|
||||
assert find_branch_root(branch_b) == branch_b
|
||||
# Subdir of A should find A, not B
|
||||
sub_a = branch_a / "apps"
|
||||
sub_a.mkdir()
|
||||
assert find_branch_root(sub_a) == branch_a
|
||||
|
||||
|
||||
# ─── _lookup_branch_by_name() tests (isolated with temp registry) ──
|
||||
|
||||
|
||||
class TestLookupBranchByName:
|
||||
"""Tests for registry name lookup. Uses patched temp registry."""
|
||||
|
||||
def test_finds_existing_branch(self, temp_registry):
|
||||
"""Should find a branch that exists in the registry."""
|
||||
registry_path, branch_data = temp_registry
|
||||
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
|
||||
result = _lookup_branch_by_name("mock_spawn")
|
||||
assert result is not None
|
||||
assert result["email"] == "@mock_spawn"
|
||||
assert result["name"] == "MOCK_SPAWN"
|
||||
|
||||
def test_case_insensitive_lookup(self, temp_registry):
|
||||
"""Should match regardless of case."""
|
||||
registry_path, _ = temp_registry
|
||||
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
|
||||
lower = _lookup_branch_by_name("test_branch")
|
||||
upper = _lookup_branch_by_name("TEST_BRANCH")
|
||||
assert lower is not None
|
||||
assert upper is not None
|
||||
assert lower["email"] == upper["email"] == "@test_branch"
|
||||
|
||||
def test_returns_none_for_nonexistent(self, temp_registry):
|
||||
"""Should return None for a branch not in registry."""
|
||||
registry_path, _ = temp_registry
|
||||
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
|
||||
result = _lookup_branch_by_name("this_branch_does_not_exist_xyz")
|
||||
assert result is None
|
||||
|
||||
def test_returns_none_when_registry_missing(self, tmp_path):
|
||||
"""Should return None (not crash) when registry file doesn't exist."""
|
||||
fake_path = tmp_path / "nonexistent_registry.json"
|
||||
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", fake_path):
|
||||
result = _lookup_branch_by_name("anything")
|
||||
assert result is None
|
||||
|
||||
def test_returns_none_when_registry_corrupted(self, tmp_path):
|
||||
"""Should return None (not crash) when registry JSON is malformed."""
|
||||
corrupt_path = tmp_path / "AIPASS_REGISTRY.json"
|
||||
corrupt_path.write_text("{this is not valid json!!!")
|
||||
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", corrupt_path):
|
||||
result = _lookup_branch_by_name("anything")
|
||||
assert result is None
|
||||
|
||||
def test_list_format_registry_works(self, list_format_registry):
|
||||
"""Should work with production list-format registry (not just dict)."""
|
||||
_, registry_path = list_format_registry
|
||||
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
|
||||
result = _lookup_branch_by_name("spawn")
|
||||
assert result is not None
|
||||
assert result["name"] == "SPAWN"
|
||||
assert result["email"] == "@spawn"
|
||||
|
||||
|
||||
# ─── get_branch_info_from_registry() tests ───────────────
|
||||
|
||||
|
||||
class TestGetBranchInfoFromRegistry:
|
||||
"""Tests for path-based registry lookup. Uses patched temp registry."""
|
||||
|
||||
def test_finds_branch_by_absolute_path(self, temp_registry, temp_branch):
|
||||
"""Should find branch when given its absolute path."""
|
||||
registry_path, branch_data = temp_registry
|
||||
# Patch both the registry path AND update the registry entry to use temp_branch's actual path
|
||||
branch_data["test_branch"]["path"] = str(temp_branch)
|
||||
registry_path.write_text(json.dumps({"branches": branch_data}, indent=2))
|
||||
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
|
||||
result = get_branch_info_from_registry(temp_branch)
|
||||
assert result is not None
|
||||
assert result["email"] == "@test_branch"
|
||||
assert result["name"] == "TEST_BRANCH"
|
||||
|
||||
def test_returns_none_for_random_path(self, temp_registry, tmp_path):
|
||||
"""Should return None for a path not in registry."""
|
||||
registry_path, _ = temp_registry
|
||||
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
|
||||
result = get_branch_info_from_registry(tmp_path / "random" / "path")
|
||||
assert result is None
|
||||
|
||||
|
||||
# ─── parse_send_args() --from flag tests ─────────────────
|
||||
|
||||
|
||||
class TestSendArgsFromFlag:
|
||||
"""Tests for the --from explicit sender override."""
|
||||
|
||||
def test_from_flag_parsed(self):
|
||||
"""--from @spawn should be extracted."""
|
||||
result = parse_send_args(["@ai_mail", "Subject", "Body", "--from", "@spawn"])
|
||||
assert result["from_branch"] == "@spawn"
|
||||
assert result["mode"] == "direct"
|
||||
assert result["recipients"] == ["@ai_mail"]
|
||||
|
||||
def test_from_flag_with_dispatch(self):
|
||||
"""--from and --dispatch should both work together."""
|
||||
result = parse_send_args([
|
||||
"@ai_mail", "Subject", "Body", "--from", "@backup", "--dispatch"
|
||||
])
|
||||
assert result["from_branch"] == "@backup"
|
||||
assert result["auto_execute"] is True
|
||||
|
||||
def test_from_flag_missing_value(self):
|
||||
"""--from without a value should error."""
|
||||
result = parse_send_args(["@ai_mail", "Subject", "Body", "--from"])
|
||||
assert result["mode"] == "error"
|
||||
|
||||
def test_no_from_flag_is_none(self):
|
||||
"""Without --from, from_branch should be None."""
|
||||
result = parse_send_args(["@spawn", "Subject", "Body"])
|
||||
assert result["from_branch"] is None
|
||||
|
||||
|
||||
# ─── resolve_sender_info() tests ─────────────────────────
|
||||
|
||||
|
||||
class TestResolveSenderInfo:
|
||||
"""Tests for the sender resolution function in send.py."""
|
||||
|
||||
def test_explicit_from_branch_resolves(self, tmp_path):
|
||||
"""Explicit from_branch should use registry lookup, not CWD."""
|
||||
def mock_get_branch_by_email(email):
|
||||
if email == "@spawn":
|
||||
return {
|
||||
"name": "SPAWN",
|
||||
"path": "src/aipass/spawn",
|
||||
"email": "@spawn",
|
||||
}
|
||||
return None
|
||||
|
||||
result = resolve_sender_info(
|
||||
from_branch="@spawn",
|
||||
repo_root=tmp_path,
|
||||
ai_mail_dir=tmp_path / "ai_mail",
|
||||
get_branch_by_email_fn=mock_get_branch_by_email,
|
||||
get_current_user_fn=lambda: {"email_address": "@ai_mail"},
|
||||
)
|
||||
assert result["email_address"] == "@spawn"
|
||||
assert result["display_name"] == "SPAWN"
|
||||
# Verify path resolution (relative path resolved against repo_root)
|
||||
assert "mailbox_path" in result
|
||||
assert str(tmp_path) in result["mailbox_path"]
|
||||
|
||||
def test_no_from_branch_uses_current_user(self, tmp_path):
|
||||
"""Without from_branch, should delegate to get_current_user_fn."""
|
||||
fallback_user = {
|
||||
"email_address": "@trigger",
|
||||
"display_name": "TRIGGER",
|
||||
"mailbox_path": str(tmp_path / "mailbox"),
|
||||
}
|
||||
result = resolve_sender_info(
|
||||
from_branch=None,
|
||||
repo_root=tmp_path,
|
||||
ai_mail_dir=tmp_path / "ai_mail",
|
||||
get_branch_by_email_fn=lambda e: None,
|
||||
get_current_user_fn=lambda: fallback_user,
|
||||
)
|
||||
assert result["email_address"] == "@trigger"
|
||||
|
||||
def test_from_branch_not_in_registry_still_works(self, tmp_path):
|
||||
"""Unknown from_branch should still construct valid sender info."""
|
||||
result = resolve_sender_info(
|
||||
from_branch="@mystery",
|
||||
repo_root=tmp_path,
|
||||
ai_mail_dir=tmp_path / "ai_mail",
|
||||
get_branch_by_email_fn=lambda e: None, # Not found
|
||||
get_current_user_fn=lambda: {"email_address": "@ai_mail"},
|
||||
)
|
||||
# Should still resolve, just with constructed info
|
||||
assert result["email_address"] == "@mystery"
|
||||
assert result["display_name"] == "MYSTERY"
|
||||
|
||||
def test_from_branch_at_only_is_degenerate(self, tmp_path):
|
||||
"""from_branch="@" (just @ symbol) should produce degenerate but not crash.
|
||||
|
||||
This is an edge case: lstrip('@') produces empty string.
|
||||
The system should handle it without crashing.
|
||||
"""
|
||||
result = resolve_sender_info(
|
||||
from_branch="@",
|
||||
repo_root=tmp_path,
|
||||
ai_mail_dir=tmp_path / "ai_mail",
|
||||
get_branch_by_email_fn=lambda e: None,
|
||||
get_current_user_fn=lambda: {"email_address": "@ai_mail"},
|
||||
)
|
||||
# Should not crash — verify degenerate but predictable output
|
||||
assert result is not None
|
||||
assert result["email_address"] == "@"
|
||||
assert result["display_name"] == ""
|
||||
assert "mailbox_path" in result
|
||||
|
||||
|
||||
# ─── Dispatch env var tests ──────────────────────────────
|
||||
|
||||
|
||||
class TestDispatchEnvIsolation:
|
||||
"""Tests for the dispatch_monitor env var contract.
|
||||
|
||||
dispatch_monitor.py must:
|
||||
1. Strip AIPASS_CALLER_BRANCH (prevents parent context leak)
|
||||
2. Strip AIPASS_CALLER_CWD (prevents stale CWD)
|
||||
3. Set AIPASS_BRANCH_NAME (CWD-independent identity)
|
||||
4. Pass spawn_env to subprocess.run (actually use the isolated env)
|
||||
|
||||
Contract tests read source code and verify critical lines exist.
|
||||
Lines are filtered to exclude comments — commented-out code won't match.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _load_active_source():
|
||||
"""Load dispatch_monitor.py source with comment lines filtered out."""
|
||||
monitor_path = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "apps" / "handlers" / "dispatch" / "dispatch_monitor.py"
|
||||
)
|
||||
source = monitor_path.read_text()
|
||||
active_lines = [
|
||||
line for line in source.splitlines()
|
||||
if not line.strip().startswith('#')
|
||||
]
|
||||
return '\n'.join(active_lines)
|
||||
|
||||
def test_dispatch_monitor_sets_branch_name_in_env(self):
|
||||
"""dispatch_monitor.py must set AIPASS_BRANCH_NAME from branch_email.
|
||||
|
||||
Checks the full assignment line (not split across assertions).
|
||||
Comment lines are filtered out — a commented-out line won't pass.
|
||||
"""
|
||||
active_source = self._load_active_source()
|
||||
assert 'spawn_env["AIPASS_BRANCH_NAME"] = branch_email.lstrip("@")' in active_source, \
|
||||
"dispatch_monitor.py must set AIPASS_BRANCH_NAME = branch_email.lstrip('@') in spawn_env"
|
||||
|
||||
def test_dispatch_monitor_strips_caller_vars(self):
|
||||
"""dispatch_monitor.py must strip AIPASS_CALLER_BRANCH and AIPASS_CALLER_CWD.
|
||||
|
||||
Contract test — verifies the env isolation lines exist in source.
|
||||
Checks full pop() calls including the None default.
|
||||
"""
|
||||
active_source = self._load_active_source()
|
||||
assert 'spawn_env.pop("AIPASS_CALLER_BRANCH", None)' in active_source, \
|
||||
"dispatch_monitor.py must strip AIPASS_CALLER_BRANCH from spawn_env"
|
||||
assert 'spawn_env.pop("AIPASS_CALLER_CWD", None)' in active_source, \
|
||||
"dispatch_monitor.py must strip AIPASS_CALLER_CWD from spawn_env"
|
||||
|
||||
def test_dispatch_monitor_passes_spawn_env_to_subprocess(self):
|
||||
"""dispatch_monitor.py must pass env=spawn_env to subprocess.run.
|
||||
|
||||
Without this, all env var isolation is useless — the subprocess
|
||||
would inherit os.environ instead of the cleaned spawn_env.
|
||||
"""
|
||||
active_source = self._load_active_source()
|
||||
assert 'env=spawn_env' in active_source, \
|
||||
"dispatch_monitor.py must pass env=spawn_env to subprocess.run"
|
||||
|
||||
def test_detect_resolves_identity_when_cwd_is_wrong(self, clean_env, tmp_path, list_format_registry):
|
||||
"""When AIPASS_CALLER_BRANCH is set but CWD is outside any branch,
|
||||
detection should succeed via the env var path, not CWD.
|
||||
|
||||
This is the dispatch scenario: agent cd'd away, CWD is useless,
|
||||
but AIPASS_CALLER_BRANCH (set by drone from AIPASS_BRANCH_NAME) works.
|
||||
"""
|
||||
_, registry_path = list_format_registry
|
||||
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
|
||||
os.environ["AIPASS_CALLER_BRANCH"] = "spawn"
|
||||
os.environ["AIPASS_CALLER_CWD"] = str(tmp_path) # Points nowhere useful
|
||||
result = detect_branch_from_pwd()
|
||||
assert result is not None
|
||||
assert result["name"] == "SPAWN"
|
||||
assert result["email"] == "@spawn"
|
||||
|
||||
|
||||
# ─── Anti-regression tests ───────────────────────────────
|
||||
|
||||
|
||||
class TestAntiRegression:
|
||||
"""Tests for specific bugs we've fixed. These must NEVER break again."""
|
||||
|
||||
def test_no_silent_ai_mail_identity_from_cwd(self, clean_env, tmp_path, temp_registry):
|
||||
"""BUG (session 5): Path.cwd() inside ai_mail subprocess detected @ai_mail as sender.
|
||||
|
||||
When no env vars are set and AIPASS_CALLER_CWD points outside any branch,
|
||||
detect_branch_from_pwd must return None — never silently detect ai_mail.
|
||||
"""
|
||||
registry_path, _ = temp_registry
|
||||
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
|
||||
os.environ["AIPASS_CALLER_CWD"] = str(tmp_path)
|
||||
result = detect_branch_from_pwd()
|
||||
assert result is None, \
|
||||
"With CWD outside any branch, detection must return None, not silently detect a branch"
|
||||
|
||||
def test_invalid_caller_cwd_doesnt_crash(self, clean_env, tmp_path, temp_registry):
|
||||
"""Stale or nonexistent AIPASS_CALLER_CWD should return None, not crash."""
|
||||
registry_path, _ = temp_registry
|
||||
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
|
||||
os.environ["AIPASS_CALLER_CWD"] = "/nonexistent/path/that/doesnt/exist"
|
||||
result = detect_branch_from_pwd()
|
||||
assert result is None
|
||||
|
||||
def test_empty_caller_branch_falls_through_to_cwd(self, clean_env, list_format_registry):
|
||||
"""Empty string in AIPASS_CALLER_BRANCH should fall through to CWD detection.
|
||||
|
||||
Verifies the empty-string is treated as falsy and CWD path is used instead.
|
||||
"""
|
||||
branch_dir, registry_path = list_format_registry
|
||||
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
|
||||
os.environ["AIPASS_CALLER_BRANCH"] = ""
|
||||
os.environ["AIPASS_CALLER_CWD"] = str(branch_dir)
|
||||
result = detect_branch_from_pwd()
|
||||
assert result is not None, "Empty AIPASS_CALLER_BRANCH should fall through to CWD"
|
||||
assert result["email"] == "@test_cwd_branch", \
|
||||
"CWD points to test_cwd_branch, so after fallthrough, it should be detected"
|
||||
|
||||
def test_send_args_all_flags_combined(self):
|
||||
"""All flags together should parse without interference."""
|
||||
result = parse_send_args([
|
||||
"@spawn", "Subject", "Body",
|
||||
"--from", "@backup",
|
||||
"--dispatch",
|
||||
"--no-memory-save",
|
||||
"--reply-to", "@flow",
|
||||
])
|
||||
assert result["from_branch"] == "@backup"
|
||||
assert result["auto_execute"] is True
|
||||
assert result["no_memory_save"] is True
|
||||
assert result["reply_to"] == "@flow"
|
||||
assert result["recipients"] == ["@spawn"]
|
||||
assert result["mode"] == "direct"
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - API
|
||||
```
|
||||
Branch: /home/coder/workspace/AIPass/src/aipass/api
|
||||
Created: 2026-03-07
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
@@ -112,6 +112,7 @@ GLOBAL_IGNORE_PATTERNS = [
|
||||
".backup", # Old backup directories
|
||||
".antigravity", # AI assistant cache/data
|
||||
".gemini", # AI assistant cache
|
||||
"snap", # Snap package directories (broken symlinks, system-managed)
|
||||
|
||||
# Version control (huge number of files!)
|
||||
".git", # Git repositories are version controlled elsewhere
|
||||
|
||||
@@ -80,7 +80,11 @@ def scan_files(source_dir: Path, should_ignore: Callable, show_progress: bool =
|
||||
progress.update(task, description=f"Found {len(files_to_backup)} files...")
|
||||
else:
|
||||
# Walk directory tree without progress display
|
||||
for dirpath, dirnames, filenames in os.walk(source_dir):
|
||||
def _walk_error(err):
|
||||
"""Handle os.walk errors (broken symlinks, permission denied)."""
|
||||
pass # Skip inaccessible paths silently
|
||||
|
||||
for dirpath, dirnames, filenames in os.walk(source_dir, onerror=_walk_error):
|
||||
# Filter directories (modify in-place to prune walk)
|
||||
original_dirs = dirnames.copy()
|
||||
dirnames[:] = [d for d in dirnames if not should_ignore(Path(dirpath) / d)]
|
||||
@@ -98,7 +102,7 @@ def scan_files(source_dir: Path, should_ignore: Callable, show_progress: bool =
|
||||
if should_ignore(file_path):
|
||||
rel_file = str(file_path.relative_to(source_dir))
|
||||
skipped_items["files"].add(rel_file)
|
||||
else:
|
||||
elif file_path.exists():
|
||||
files_to_backup.append(file_path)
|
||||
|
||||
return files_to_backup, skipped_items
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"module_name": "{{MODULE_NAME}}",
|
||||
"version": "1.0.0",
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"max_log_entries": 100
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"created": "{{TIMESTAMP}}",
|
||||
"last_updated": "{{TIMESTAMP}}"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - BACKUP
|
||||
```
|
||||
Branch: /home/patrick/Projects/AIPass/src/aipass/backup
|
||||
Created: 2026-03-07
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - CLI
|
||||
```
|
||||
Branch: /home/coder/workspace/AIPass/src/aipass/cli
|
||||
Created: 2026-03-07
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - DAEMON
|
||||
```
|
||||
Branch: /home/patrick/Projects/AIPass/src/aipass/daemon
|
||||
Created: 2026-03-07
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
@@ -1,6 +1,6 @@
|
||||
# DEVPULSE — Branch Prompt
|
||||
|
||||
Injected every turn. Operational guidance only — details in README, --help, .trinity/ memories.
|
||||
Injected every turn. Breadcrumbs only — details in README, --help, .trinity/ memories, dev.local.md.
|
||||
|
||||
## Identity
|
||||
|
||||
@@ -9,27 +9,25 @@ You are DEVPULSE — orchestration hub. Manager, not builder. Coordinate, plan,
|
||||
## How You Work
|
||||
|
||||
- Delegate code tasks to background agents (`run_in_background: true`). Fire and forget — move on immediately.
|
||||
- Launch agent → continue conversation → get notified → report results
|
||||
- Launch agent → continue conversation → get notified → report results.
|
||||
- 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 Patrick wants something different, he will say so.
|
||||
|
||||
## Branches (15)
|
||||
## Dispatch, Don't Do
|
||||
|
||||
- **@drone** — Command router. @branch resolution, subprocess dispatch.
|
||||
- **@seedgo** — Standards enforcement. 21-standard audit pack, checkers.
|
||||
- **@prax** — Logging, monitoring, dashboard infrastructure.
|
||||
- **@cli** — Display service. Rich formatting for all branches.
|
||||
- **@ai_mail** — Inter-branch email. Dispatch, wake, bounce.
|
||||
- **@flow** — Plan lifecycle. FPLANs (building) + DPLANs (planning).
|
||||
- **@spawn** — Branch lifecycle. Create, update, delete, sync.
|
||||
- **@trigger** — Event bus. 12 events, error registry, circuit breaker.
|
||||
- **@api** — LLM client via OpenRouter. Key management.
|
||||
- **@backup** — Multi-mode backup. Snapshot, versioned, Google Drive.
|
||||
- **@daemon** — Autonomous scheduled wake-ups. NOT for live dispatch.
|
||||
- **@memory** — Vector memory bank. ChromaDB, sentence-transformers.
|
||||
- **@commons** (`src/commons/`) — Social network for branches. Posts, rooms, artifacts.
|
||||
- **@skills** (`src/skills/`) — Capability framework. Discoverable, executable skill units.
|
||||
- **@devpulse** (you) — Orchestration hub. No apps/, coordinates via dispatch + agents.
|
||||
When a task belongs to a specialist, send it there. Don't burn context doing their job.
|
||||
|
||||
| Domain | Ask | Why |
|
||||
|--------|-----|-----|
|
||||
| Standards, audits | @seedgo | 21-standard pack, checkers |
|
||||
| Email, delivery | @ai_mail | Dispatch, wake, bounce |
|
||||
| Plans, workflows | @flow | FPLANs (building) + DPLANs (planning) |
|
||||
| Branch lifecycle | @spawn | Create, update, delete, sync |
|
||||
| Monitoring, logs | @prax | Dashboard, real-time, log infra |
|
||||
| Event handling | @trigger | 12 events, error registry |
|
||||
| Command routing | @drone | @branch resolution, subprocess |
|
||||
| Memory, vectors | @memory | ChromaDB, search, archival |
|
||||
|
||||
## Key Commands
|
||||
|
||||
@@ -38,26 +36,27 @@ drone @ai_mail send @target "Subject" "Body" --dispatch # Task email
|
||||
drone @ai_mail dispatch wake @target # Wake branch
|
||||
drone @flow create . "Subject" # Create FPLAN
|
||||
drone @flow list # Active plans
|
||||
drone systems # All branches
|
||||
```
|
||||
|
||||
## Memory Protocol
|
||||
## Branches (15)
|
||||
|
||||
Update `.trinity/` proactively — your persistence depends on it.
|
||||
drone, seedgo, prax, cli, ai_mail, flow, spawn, trigger, api, backup, daemon, memory, commons (`src/commons/`), skills (`src/skills/`), devpulse (you — no apps/, coordinates via dispatch + agents)
|
||||
|
||||
**When:** After milestones. On `/memo`. At topic shifts. After 5+ actions without saving. When you learn something new.
|
||||
## Working Habits
|
||||
|
||||
**What:**
|
||||
- `local.json` — today_focus, recently_completed, sessions[], key_learnings
|
||||
- `observations.json` — patterns, workflow insights
|
||||
- This file — Current Context section below
|
||||
- **Lean on branches.** You can't know everything — branches are the experts on their systems. When unsure, email them and ask. Don't burn context debugging what they already know.
|
||||
- **Use memories freely.** Don't hoard or stress about capacity — rollover to @memory is by design. Update `.trinity/` often. More is better.
|
||||
- **dev.local.md for friction notes.** When something feels off or could be improved, drop a quick note. Address in batches later.
|
||||
- **Know your limits.** You're great at planning, coordinating, seeing the big picture. You're bad at hands-on branch-level code tasks. Dispatch, don't do.
|
||||
- **Git hygiene at breakpoints.** When a branch completes work, a plan closes, or a dispatch cycle finishes — run `git status` to see what's piling up. Don't let changes drift. Propose a commit or PR when it makes sense. Not every turn — just at natural milestones.
|
||||
|
||||
**Prompt vs memory:** This prompt = lightweight signposts (injected every turn). Memories = detailed knowledge (read on startup, refreshed on update). Don't duplicate — point to where info lives.
|
||||
## Memory & Tracking
|
||||
|
||||
## Current Context (Session 17)
|
||||
- `.trinity/local.json` — session history, active tasks, learnings
|
||||
- `.trinity/observations.json` — collaboration patterns
|
||||
- `dev.local.md` — issues, todos, working notes (human + AI shared scratchpad)
|
||||
|
||||
**Date:** 2026-03-08
|
||||
Update `.trinity/` proactively — after milestones, on `/memo`, at topic shifts, after 5+ actions without saving. Your persistence depends on it.
|
||||
|
||||
- FPLAN-0016: 99% avg, all 14 at 99%+. Only drone false positive (bypassed).
|
||||
- Researched Dev-Pass introspection standard — emailed @seedgo for new standard
|
||||
- Massive uncommitted changeset — needs commit + PR
|
||||
- Dev-Pass ref: `/home/patrick/Projects/Dev-Pass/`
|
||||
**This prompt is NOT for tracking.** State goes in `.trinity/` and `dev.local.md`. This prompt = lightweight signposts injected every turn.
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# dev.local.md - DEVPULSE
|
||||
```
|
||||
Branch: src/aipass/devpulse
|
||||
Created: 2026-03-07
|
||||
Updated: 2026-03-10
|
||||
```
|
||||
|
||||
## Active Work
|
||||
|
||||
- **Flow dispatch**: DPLAN path fixes + FPLAN empty template detection — dispatched to flow, branch awake
|
||||
- **Prompt architecture**: DONE — breadcrumbs in global+local, dev.local.md headers fixed system-wide
|
||||
|
||||
## Issues
|
||||
|
||||
- **flow/DPLAN**: All 10 handler files use Dev-Pass paths (`~/aipass_os/dev_central/dev_planning/`). Creates orphan dirs. Dispatched to flow.
|
||||
- **flow/FPLAN**: `is_template_content()` too aggressive — deletes plans with real content if template boilerplate remains. Data loss bug. Dispatched to flow.
|
||||
- **flow/FPLAN archive**: Closed FPLANs go to `src/aipass/backup_system/processed_plans/` — backup_system isn't a branch (Dev-Pass name). Leaving for later.
|
||||
- **prax**: Missing `handlers/central/reader.py` — dashboard/refresh.py imports `read_all_centrals` but function never created
|
||||
- **backup**: Missing `config.json` template in json_templates/
|
||||
- **api**: `models` command not routed through drone
|
||||
- **commons**: DB init failure (12/13 core branches operational)
|
||||
- **ai_mail**: `get_current_user()` returns relative `mailbox_path` — causes doubled paths in reply
|
||||
- **drone**: stderr kwarg crash
|
||||
|
||||
## Completed
|
||||
|
||||
- PR #31 merged: seedgo v2, 589 files, full system audit (2026-03-10)
|
||||
- ai_mail sender identity bug FIXED by Patrick
|
||||
- Renamed flow.local.md → dev.local.md system-wide (17 files)
|
||||
- Fixed dev.local.md headers system-wide (16 branch files + spawn template)
|
||||
- FPLAN-0021 dispatched+completed by flow: DPLANs wired into CLI router
|
||||
- Revised devpulse branch prompt — breadcrumb pattern, working habits, expertise table
|
||||
- Added Breadcrumbs section + dev.local.md awareness to global prompt
|
||||
- Updated .trinity/ memories with Session 18
|
||||
- Processed inbox: ai_mail fix reply, flow FPLAN-0021 completion, old stress test
|
||||
- Full FPLAN lifecycle tested: create (default+master), list, close (empty+content)
|
||||
- Full DPLAN lifecycle tested: create, list, status, close
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
- [x] Update global prompt with dev.local.md awareness
|
||||
- [x] Fix dev.local.md headers system-wide
|
||||
- [ ] Re-send emails to prax and backup about their known issues
|
||||
- [ ] Scaffold remaining modules with system prompts (.aipass/aipass_local_prompt.md)
|
||||
- [ ] Test dispatch wake to branches beyond ai_mail
|
||||
- [ ] Address drone stderr kwarg crash
|
||||
- [ ] Close stale FPLANs (0017, 0021) once verified complete
|
||||
|
||||
## Notepad
|
||||
|
||||
- Dev-Pass reference: `/home/patrick/Projects/Dev-Pass/`
|
||||
- seed=Dev-Pass name, seedgo=AIPass name
|
||||
- FPLAN close 5-step process: template check → mark closed → background archival → dashboard update → finalize
|
||||
- DPLAN close 3-step process: close → background memory bank archival → finalize
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - DRONE
|
||||
```
|
||||
Branch: /home/coder/workspace/AIPass/src/aipass/drone
|
||||
Created: 2026-03-07
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"closed_plans": [
|
||||
{
|
||||
"plan_id": "DPLAN-001",
|
||||
"type": "DPLAN",
|
||||
"subject": "test dispatch fix",
|
||||
"date_closed": "2026-03-10",
|
||||
"location": "dev_central"
|
||||
},
|
||||
{
|
||||
"plan_id": "FPLAN-0024",
|
||||
"type": "FPLAN",
|
||||
"subject": "Test close with real content",
|
||||
"date_closed": "2026-03-10",
|
||||
"location": "flow"
|
||||
},
|
||||
{
|
||||
"plan_id": "FPLAN-0025",
|
||||
"type": "FPLAN",
|
||||
"subject": "Build STATUS board — per-branch STATUS.local.md with Prax-managed central aggregation",
|
||||
"date_closed": "2026-03-10",
|
||||
"location": "flow"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -141,19 +141,19 @@ def main():
|
||||
command = args[0]
|
||||
remaining_args = args[1:] if len(args) > 1 else []
|
||||
|
||||
# Check if user wants module-specific help
|
||||
if remaining_args and remaining_args[0] in ['--help', '-h']:
|
||||
print_module_help(command, modules)
|
||||
# Route to modules (modules handle their own --help internally)
|
||||
if route_command(command, remaining_args, modules):
|
||||
return 0
|
||||
|
||||
# Route to modules
|
||||
if route_command(command, remaining_args, modules):
|
||||
# Fallback: try module-specific help if command wasn't handled
|
||||
if remaining_args and remaining_args[0] in ['--help', '-h']:
|
||||
print_module_help(command, modules)
|
||||
return 0
|
||||
else:
|
||||
console.print()
|
||||
console.print(f"[red]Unknown command: {command}[/red]")
|
||||
console.print()
|
||||
console.print("Run [dim]python3 flow.py --help[/dim] for available commands")
|
||||
console.print("Run [dim]drone @flow --help[/dim] for available commands")
|
||||
console.print()
|
||||
return 1
|
||||
|
||||
@@ -181,7 +181,7 @@ def print_introspection(modules: List[Any]):
|
||||
console.print(" [dim]No modules discovered[/dim]")
|
||||
|
||||
console.print()
|
||||
console.print("[dim]Run 'python3 flow.py --help' for usage information[/dim]")
|
||||
console.print("[dim]Run 'drone @flow --help' for usage information[/dim]")
|
||||
console.print()
|
||||
|
||||
|
||||
@@ -198,8 +198,8 @@ def print_help(modules: List[Any]):
|
||||
|
||||
console.print("[bold cyan]USAGE:[/bold cyan]")
|
||||
console.print()
|
||||
console.print(" [dim]python3 flow.py <command> [args...][/dim]")
|
||||
console.print(" [dim]python3 flow.py --help[/dim]")
|
||||
console.print(" [dim]drone @flow <command> [args...][/dim]")
|
||||
console.print(" [dim]drone @flow --help[/dim]")
|
||||
console.print()
|
||||
console.print("─" * 70)
|
||||
console.print()
|
||||
@@ -232,30 +232,45 @@ def print_help(modules: List[Any]):
|
||||
console.print("─" * 70)
|
||||
console.print()
|
||||
|
||||
console.print("[bold cyan]EXAMPLES:[/bold cyan]")
|
||||
console.print("[bold cyan]FPLAN EXAMPLES:[/bold cyan]")
|
||||
console.print()
|
||||
console.print(" [yellow]Create new PLAN:[/yellow]")
|
||||
console.print(" [dim]python3 flow.py create . \"Implementation task\"[/dim]")
|
||||
console.print(" [dim]python3 flow.py create /path/to/location \"Implementation task\"[/dim]")
|
||||
console.print(" [yellow]Create new FPLAN:[/yellow]")
|
||||
console.print(" [dim]drone @flow create . \"Implementation task\"[/dim]")
|
||||
console.print(" [dim]drone @flow create . \"subject\" master[/dim]")
|
||||
console.print()
|
||||
console.print(" [yellow]Close PLAN:[/yellow]")
|
||||
console.print(" [dim]python3 flow.py close 42 --yes[/dim]")
|
||||
console.print(" [yellow]Close FPLAN:[/yellow]")
|
||||
console.print(" [dim]drone @flow close FPLAN-0042[/dim]")
|
||||
console.print()
|
||||
console.print(" [yellow]Close all open plans:[/yellow]")
|
||||
console.print(" [dim]python3 flow.py close --all[/dim]")
|
||||
console.print(" [yellow]List FPLANs:[/yellow]")
|
||||
console.print(" [dim]drone @flow list[/dim]")
|
||||
console.print()
|
||||
console.print(" [yellow]List plans:[/yellow]")
|
||||
console.print(" [dim]python3 flow.py list[/dim]")
|
||||
console.print("─" * 70)
|
||||
console.print()
|
||||
|
||||
console.print("[bold cyan]DPLAN EXAMPLES:[/bold cyan]")
|
||||
console.print()
|
||||
console.print("[bold]NOTE:[/bold] @ syntax (e.g., @flow, @seedgo) only works through drone:")
|
||||
console.print(" [dim]drone flow create @flow \"Implementation task\"[/dim]")
|
||||
console.print(" [yellow]Create DPLAN:[/yellow]")
|
||||
console.print(" [dim]drone @flow plan create \"Topic\"[/dim]")
|
||||
console.print()
|
||||
console.print(" [yellow]List DPLANs:[/yellow]")
|
||||
console.print(" [dim]drone @flow plan list[/dim]")
|
||||
console.print(" [dim]drone @flow plan list --tag idea[/dim]")
|
||||
console.print()
|
||||
console.print(" [yellow]Close DPLAN:[/yellow]")
|
||||
console.print(" [dim]drone @flow plan close 42[/dim]")
|
||||
console.print(" [dim]drone @flow plan close --all[/dim]")
|
||||
console.print()
|
||||
console.print(" [yellow]DPLAN status:[/yellow]")
|
||||
console.print(" [dim]drone @flow plan status[/dim]")
|
||||
console.print()
|
||||
console.print(" [yellow]Sync registry:[/yellow]")
|
||||
console.print(" [dim]drone @flow plan sync[/dim]")
|
||||
console.print()
|
||||
console.print("─" * 70)
|
||||
console.print()
|
||||
|
||||
console.print("[bold]TIP:[/bold] For module-specific help:")
|
||||
console.print(" [dim]python3 flow.py <command> --help[/dim]")
|
||||
console.print(" [dim]drone @flow <command> --help[/dim]")
|
||||
console.print()
|
||||
|
||||
|
||||
@@ -274,7 +289,7 @@ def print_module_help(command: str, modules: List[Any]):
|
||||
console.print()
|
||||
console.print(f"[red]Unknown command: {command}[/red]")
|
||||
console.print()
|
||||
console.print("Run [dim]python3 flow.py --help[/dim] for available commands")
|
||||
console.print("Run [dim]drone @flow --help[/dim] for available commands")
|
||||
console.print()
|
||||
return
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""
|
||||
DPLAN Handlers - Extracted from Dev-Pass devpulse on 2026-03-08
|
||||
|
||||
These files need adaptation for AIPass before use.
|
||||
Original imports use aipass_os.dev_central.devpulse -- must be converted to aipass.flow.
|
||||
|
||||
Source: /home/patrick/Projects/Dev-Pass/aipass_os/dev_central/devpulse/apps/handlers/plan/
|
||||
Migration complete (2026-03-10):
|
||||
- All imports converted from aipass_os.dev_central.devpulse to aipass.flow
|
||||
- All data paths converted from ~/aipass_os/ to Path(__file__).parents[N] relative paths
|
||||
- sys.path hacks removed
|
||||
"""
|
||||
|
||||
@@ -13,16 +13,10 @@ Validates, marks as closed, and archives DPLAN files.
|
||||
Adapted from Flow's close system for single-user DPLANs.
|
||||
"""
|
||||
|
||||
# INFRASTRUCTURE IMPORT PATTERN
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Tuple, Optional, List
|
||||
|
||||
AIPASS_ROOT = Path.home() / "aipass_core"
|
||||
sys.path.insert(0, str(AIPASS_ROOT))
|
||||
sys.path.insert(0, str(Path.home()))
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
|
||||
from .status import extract_status
|
||||
@@ -31,8 +25,10 @@ from .status import extract_status
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
DEV_PLANNING_ROOT = Path.home() / "aipass_os" / "dev_central" / "dev_planning"
|
||||
PROCESSED_PLANS_DIR = Path.home() / "aipass_core" / "backup_system" / "processed_plans"
|
||||
# close.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning"
|
||||
PROCESSED_PLANS_DIR = FLOW_ROOT / "processed_plans"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -27,7 +27,9 @@ from typing import Dict, Any
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
CLOSED_PLANS_PATH = Path.home() / "aipass_os" / "dev_central" / "CLOSED_PLANS.local.json"
|
||||
# closed_plans_registry.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
CLOSED_PLANS_PATH = FLOW_ROOT / "CLOSED_PLANS.local.json"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -14,25 +14,20 @@ Supports multiple plan types (DPLAN, BPLAN) with separate sequences.
|
||||
Counter file is a cache, not source of truth.
|
||||
"""
|
||||
|
||||
# INFRASTRUCTURE IMPORT PATTERN
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
AIPASS_ROOT = Path.home() / "aipass_core"
|
||||
sys.path.insert(0, str(AIPASS_ROOT))
|
||||
sys.path.insert(0, str(Path.home()))
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
# Modules do the logging, handlers return errors
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
DEV_PLANNING_ROOT = Path.home() / "aipass_os" / "dev_central" / "dev_planning"
|
||||
# counter.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning"
|
||||
COUNTER_FILE = DEV_PLANNING_ROOT / "counter.json"
|
||||
|
||||
VALID_PLAN_TYPES = {"dplan": "DPLAN", "bplan": "BPLAN"}
|
||||
|
||||
@@ -13,17 +13,11 @@ Creates new plan files (DPLAN, BPLAN) with proper naming and content.
|
||||
Supports @ branch resolution via target_path parameter.
|
||||
"""
|
||||
|
||||
# INFRASTRUCTURE IMPORT PATTERN
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Tuple, Dict, Any
|
||||
|
||||
AIPASS_ROOT = Path.home() / "aipass_core"
|
||||
sys.path.insert(0, str(AIPASS_ROOT))
|
||||
sys.path.insert(0, str(Path.home()))
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
|
||||
from .counter import get_next_plan_number, VALID_PLAN_TYPES
|
||||
@@ -33,7 +27,9 @@ from .template import render_template
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
DEV_PLANNING_ROOT = Path.home() / "aipass_os" / "dev_central" / "dev_planning"
|
||||
# create.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -15,15 +15,10 @@ independence pattern). Central push is handled directly here.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional, Callable
|
||||
|
||||
AIPASS_ROOT = Path.home() / "aipass_core"
|
||||
sys.path.insert(0, str(AIPASS_ROOT))
|
||||
sys.path.insert(0, str(Path.home()))
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
|
||||
from .registry import load_registry
|
||||
@@ -32,8 +27,11 @@ from .registry import load_registry
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
DEVPULSE_ROOT = Path.home() / "aipass_os" / "dev_central" / "devpulse"
|
||||
CENTRAL_FILE = Path.home() / "aipass_os" / "AI_CENTRAL" / "DEVPULSE.central.json"
|
||||
# dashboard.py → dplan/ → handlers/ → apps/ → flow/ → aipass/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
AIPASS_ROOT = Path(__file__).resolve().parents[4]
|
||||
DEVPULSE_ROOT = AIPASS_ROOT / "devpulse"
|
||||
CENTRAL_FILE = DEVPULSE_ROOT / "DEVPULSE.central.json"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -16,24 +16,19 @@ Provides help text and introspection information.
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
AIPASS_ROOT = Path.home() / "aipass_core"
|
||||
sys.path.insert(0, str(AIPASS_ROOT))
|
||||
sys.path.insert(0, str(Path.home()))
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
DEV_PLANNING_ROOT = Path.home() / "aipass_os" / "dev_central" / "dev_planning"
|
||||
DEVPULSE_ROOT = Path.home() / "aipass_os" / "dev_central" / "devpulse"
|
||||
COUNTER_FILE = DEV_PLANNING_ROOT / "counter.json"
|
||||
TEMPLATE_FILE = DEVPULSE_ROOT / "templates" / "dplan_default.md"
|
||||
# display.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
TEMPLATE_FILE = FLOW_ROOT / "templates" / "dplan_default.md"
|
||||
|
||||
HELP_TEXT = """
|
||||
[bold]USAGE:[/bold]
|
||||
drone @devpulse plan <subcommand> [options]
|
||||
drone @flow plan <subcommand> [options]
|
||||
|
||||
[bold]SUBCOMMANDS:[/bold]
|
||||
create "topic" [options] - Create new plan document
|
||||
@@ -48,18 +43,18 @@ HELP_TEXT = """
|
||||
bplan - Business plans
|
||||
|
||||
[bold]EXAMPLES:[/bold]
|
||||
drone @devpulse plan create "new feature design"
|
||||
drone @devpulse plan create "API upgrade" --tag upgrade
|
||||
drone @devpulse plan create "revenue model" --type bplan
|
||||
drone @devpulse plan create "vera improvements" --type dplan @vera
|
||||
drone @devpulse plan list
|
||||
drone @devpulse plan list --type bplan
|
||||
drone @devpulse plan list --tag idea
|
||||
drone @devpulse plan list --status planning
|
||||
drone @devpulse plan status
|
||||
drone @devpulse plan status --type dplan
|
||||
drone @devpulse plan close 3
|
||||
drone @devpulse plan close --all
|
||||
drone @flow plan create "new feature design"
|
||||
drone @flow plan create "API upgrade" --tag upgrade
|
||||
drone @flow plan create "revenue model" --type bplan
|
||||
drone @flow plan create "vera improvements" --type dplan @vera
|
||||
drone @flow plan list
|
||||
drone @flow plan list --type bplan
|
||||
drone @flow plan list --tag idea
|
||||
drone @flow plan list --status planning
|
||||
drone @flow plan status
|
||||
drone @flow plan status --type dplan
|
||||
drone @flow plan close 3
|
||||
drone @flow plan close --all
|
||||
|
||||
[bold]@ RESOLUTION:[/bold]
|
||||
Append @branch to create plans in another branch's dev_planning/:
|
||||
@@ -120,8 +115,6 @@ def get_introspection_data() -> dict:
|
||||
return {
|
||||
"name": "D-PLAN Management Module",
|
||||
"description": "Manages numbered planning documents in dev_planning/",
|
||||
"planning_dir": str(DEV_PLANNING_ROOT),
|
||||
"counter_file": str(COUNTER_FILE),
|
||||
"template_file": str(TEMPLATE_FILE)
|
||||
}
|
||||
|
||||
@@ -142,11 +135,9 @@ def print_introspection() -> str:
|
||||
f"[dim]{data['description']}[/dim]",
|
||||
"",
|
||||
"[yellow]Configuration:[/yellow]",
|
||||
f" [dim]Planning dir:[/dim] {data['planning_dir']}",
|
||||
f" [dim]Counter file:[/dim] {data['counter_file']}",
|
||||
f" [dim]Template:[/dim] {data['template_file']}",
|
||||
"",
|
||||
"[dim]Run 'python3 dev_flow.py --help' for usage[/dim]",
|
||||
"[dim]Run 'drone @flow plan --help' for usage[/dim]",
|
||||
""
|
||||
]
|
||||
|
||||
|
||||
@@ -12,16 +12,10 @@ List Handler - Plan Listing
|
||||
Collects and returns plan data for display. Supports multiple plan types.
|
||||
"""
|
||||
|
||||
# INFRASTRUCTURE IMPORT PATTERN
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any, Tuple
|
||||
|
||||
AIPASS_ROOT = Path.home() / "aipass_core"
|
||||
sys.path.insert(0, str(AIPASS_ROOT))
|
||||
sys.path.insert(0, str(Path.home()))
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
|
||||
from .status import extract_status, extract_tag, extract_description
|
||||
@@ -30,7 +24,9 @@ from .status import extract_status, extract_tag, extract_description
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
DEV_PLANNING_ROOT = Path.home() / "aipass_os" / "dev_central" / "dev_planning"
|
||||
# list.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning"
|
||||
|
||||
# Regex matches any plan type: DPLAN-001_topic_2026-02-19.md, BPLAN-001_topic_2026-02-19.md
|
||||
PLAN_FILENAME_PATTERN = re.compile(r"([A-Z]+PLAN)-(\d+)_(.+)_(\d{4}-\d{2}-\d{2})\.md")
|
||||
|
||||
@@ -26,7 +26,9 @@ import io
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
DEFAULT_LOG_DIR = Path.home() / "aipass_os" / "logs"
|
||||
# log_setup.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
DEFAULT_LOG_DIR = FLOW_ROOT / "logs"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -40,7 +42,7 @@ def prepare_log_file(filename: str = "post_close_runner.log",
|
||||
|
||||
Args:
|
||||
filename: Log file name (default: post_close_runner.log)
|
||||
log_dir: Override log directory (default: ~/aipass_os/logs/)
|
||||
log_dir: Override log directory (default: flow/logs/)
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
|
||||
@@ -14,16 +14,11 @@ plan metadata, status, tags, and AI-generated summaries.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
AIPASS_ROOT = Path.home() / "aipass_core"
|
||||
sys.path.insert(0, str(AIPASS_ROOT))
|
||||
sys.path.insert(0, str(Path.home()))
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
|
||||
from .status import extract_status, extract_tag, extract_description
|
||||
@@ -32,10 +27,11 @@ from .status import extract_status, extract_tag, extract_description
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
DEVPULSE_ROOT = Path.home() / "aipass_os" / "dev_central" / "devpulse"
|
||||
DEV_PLANNING_ROOT = Path.home() / "aipass_os" / "dev_central" / "dev_planning"
|
||||
REGISTRY_FILE = DEVPULSE_ROOT / "devpulse_json" / "dplan_registry.json"
|
||||
SUMMARIES_FILE = DEVPULSE_ROOT / "devpulse_json" / "dplan_summaries.json"
|
||||
# registry.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning"
|
||||
REGISTRY_FILE = FLOW_ROOT / "flow_json" / "dplan_registry.json"
|
||||
SUMMARIES_FILE = FLOW_ROOT / "flow_json" / "dplan_summaries.json"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -12,23 +12,19 @@ Status Handler - D-PLAN Status Operations
|
||||
Extracts status from plan files and provides status summary.
|
||||
"""
|
||||
|
||||
# INFRASTRUCTURE IMPORT PATTERN
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, Tuple
|
||||
|
||||
AIPASS_ROOT = Path.home() / "aipass_core"
|
||||
sys.path.insert(0, str(AIPASS_ROOT))
|
||||
sys.path.insert(0, str(Path.home()))
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
DEV_PLANNING_ROOT = Path.home() / "aipass_os" / "dev_central" / "dev_planning"
|
||||
# status.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning"
|
||||
|
||||
VALID_TAGS = ["idea", "upgrade", "proposal", "bug", "research", "seed", "infrastructure"]
|
||||
|
||||
|
||||
@@ -18,19 +18,15 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
AIPASS_ROOT = Path.home() / "aipass_core"
|
||||
sys.path.insert(0, str(AIPASS_ROOT))
|
||||
sys.path.insert(0, str(Path.home()))
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
DEVPULSE_ROOT = Path.home() / "aipass_os" / "dev_central" / "devpulse"
|
||||
TEMPLATE_DIR = DEVPULSE_ROOT / "templates"
|
||||
TEMPLATE_FILE = TEMPLATE_DIR / "dplan_default.md"
|
||||
# template.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
TEMPLATE_DIR = FLOW_ROOT / "templates"
|
||||
|
||||
DPLAN_DEFAULT_TEMPLATE = """# DPLAN-{{NUMBER}}: {{TOPIC}}
|
||||
|
||||
|
||||
@@ -272,10 +272,11 @@ def get_closed_plans() -> List[Dict[str, Any]]:
|
||||
# =============================================
|
||||
|
||||
def is_template_content(content: str) -> bool:
|
||||
"""Check if plan content is still unedited template (v3.0)
|
||||
"""Check if plan content is still unedited template (v4.0)
|
||||
|
||||
Checks against markers from all template types (default, master, proposal).
|
||||
Template detected if 3+ markers from ANY single template type are found.
|
||||
Uses bracket placeholders only (not section headers) to detect untouched
|
||||
templates. Also checks for user-added content in key sections — if any
|
||||
real work was added, the plan is NOT a template even if placeholders remain.
|
||||
|
||||
Args:
|
||||
content: Plan file content
|
||||
@@ -283,28 +284,52 @@ def is_template_content(content: str) -> bool:
|
||||
Returns:
|
||||
True if plan is essentially an untouched template
|
||||
"""
|
||||
# Default template markers (updated Dec 2025 rewrite)
|
||||
# User content indicators — if ANY of these are found, plan has real work
|
||||
user_content_signals = [
|
||||
"- [x] Agent deployed", # Checked execution log item
|
||||
"- [x] Agent completed", # Checked execution log item
|
||||
"- [x] Seedgo checklist", # Checked completion item
|
||||
"- [x] All goals achieved", # Checked completion item
|
||||
]
|
||||
for signal in user_content_signals:
|
||||
if signal in content:
|
||||
return False
|
||||
|
||||
# Check Notes section for user content (not just the placeholder)
|
||||
import re
|
||||
notes_match = re.search(r'## Notes\s*\n(.*?)(?=\n---|\n## |\Z)', content, re.DOTALL)
|
||||
if notes_match:
|
||||
notes_content = notes_match.group(1).strip()
|
||||
# If notes has content beyond the template placeholder, it's real work
|
||||
if notes_content and notes_content != "[Working notes, issues encountered, decisions made]":
|
||||
return False
|
||||
|
||||
# Check Execution Log for user-added entries beyond template
|
||||
exec_match = re.search(r'## Execution Log\s*\n(.*?)(?=\n---|\n## |\Z)', content, re.DOTALL)
|
||||
if exec_match:
|
||||
exec_content = exec_match.group(1).strip()
|
||||
lines = [l.strip() for l in exec_content.split('\n') if l.strip()]
|
||||
# Template has ~6 lines (date header + checkbox items). More = user added content.
|
||||
if len(lines) > 8:
|
||||
return False
|
||||
|
||||
# Bracket placeholders only (no section headers — those persist in real plans)
|
||||
default_markers = [
|
||||
"[What do you want to achieve? Specific end state.]",
|
||||
"[How will agents tackle this? What instructions will they need?]",
|
||||
"[List any planning docs, specs, or examples to reference]",
|
||||
"[Working notes, issues encountered, decisions made]",
|
||||
"[What specifically defines complete for this plan?]",
|
||||
"## What Are Flow Plans?",
|
||||
"## Critical: Branch Manager Role",
|
||||
]
|
||||
|
||||
# Master template markers
|
||||
master_markers = [
|
||||
"[What this phase accomplishes]",
|
||||
"[What the agent will build]",
|
||||
"[Files/outputs expected]",
|
||||
"[What specifically defines the project complete?]",
|
||||
"[Patterns discovered that span multiple phases]",
|
||||
"## Master Plan Overview",
|
||||
]
|
||||
|
||||
# Proposal template markers
|
||||
proposal_markers = [
|
||||
"[Clear description of the idea, feature, improvement, or fix]",
|
||||
"[Why is this valuable? What problem does it solve? What does it enable?]",
|
||||
@@ -312,7 +337,7 @@ def is_template_content(content: str) -> bool:
|
||||
"[Any other branches, services, or approvals needed?]",
|
||||
]
|
||||
|
||||
# Check each template type - 3+ markers from any type = template
|
||||
# 3+ bracket placeholders from any type = template
|
||||
for markers in [default_markers, master_markers, proposal_markers]:
|
||||
found = sum(1 for m in markers if m in content)
|
||||
if found >= 3:
|
||||
|
||||
@@ -22,21 +22,21 @@ from typing import List
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
from aipass.cli.apps.modules import console, header, success, error
|
||||
|
||||
# Handler imports
|
||||
from aipass_os.dev_central.devpulse.apps.handlers.plan.create import create_plan
|
||||
from aipass_os.dev_central.devpulse.apps.handlers.plan.list import list_plans
|
||||
from aipass_os.dev_central.devpulse.apps.handlers.plan.status import get_status_summary, get_status_icon, VALID_TAGS
|
||||
from aipass_os.dev_central.devpulse.apps.handlers.plan.display import show_help, print_introspection
|
||||
from aipass_os.dev_central.devpulse.apps.handlers.plan.close import (
|
||||
# Handler imports (local handlers in handlers/dplan/)
|
||||
from aipass.flow.apps.handlers.dplan.create import create_plan
|
||||
from aipass.flow.apps.handlers.dplan.list import list_plans
|
||||
from aipass.flow.apps.handlers.dplan.status import get_status_summary, get_status_icon, VALID_TAGS
|
||||
from aipass.flow.apps.handlers.dplan.display import show_help, print_introspection
|
||||
from aipass.flow.apps.handlers.dplan.close import (
|
||||
normalize_plan_number, close_plan, get_open_plans
|
||||
)
|
||||
from aipass_os.dev_central.devpulse.apps.handlers.plan.counter import VALID_PLAN_TYPES
|
||||
from aipass_os.dev_central.devpulse.apps.handlers.plan.registry import (
|
||||
from aipass.flow.apps.handlers.dplan.counter import VALID_PLAN_TYPES
|
||||
from aipass.flow.apps.handlers.dplan.registry import (
|
||||
register_plan, update_plan_status, populate_from_filesystem,
|
||||
get_summary, save_plan_summary, generate_description_summary
|
||||
)
|
||||
from aipass_os.dev_central.devpulse.apps.handlers.plan.dashboard import push_all as _push_dashboard_raw
|
||||
from aipass_os.dev_central.devpulse.apps.handlers.dashboard.operations import write_section
|
||||
from aipass.flow.apps.handlers.dplan.dashboard import push_all as _push_dashboard_raw
|
||||
from aipass.prax.apps.handlers.dashboard.operations import write_section
|
||||
|
||||
# Local handlers (file I/O extracted from this module)
|
||||
from aipass.flow.apps.handlers.dplan.branch_resolve import resolve_branch_target as _resolve_branch
|
||||
@@ -74,7 +74,7 @@ def print_introspection():
|
||||
console.print("Plan management orchestrator — routes plan commands to handlers")
|
||||
console.print()
|
||||
console.print("Connected Handlers:")
|
||||
console.print(" handlers/plan/ (via aipass_os.dev_central.devpulse)")
|
||||
console.print(" handlers/dplan/")
|
||||
console.print(" - create.py (create_plan — create new plans)")
|
||||
console.print(" - list.py (list_plans — list plans with filters)")
|
||||
console.print(" - status.py (get_status_summary — plan status aggregation)")
|
||||
@@ -83,16 +83,14 @@ def print_introspection():
|
||||
console.print(" - counter.py (VALID_PLAN_TYPES — plan type definitions)")
|
||||
console.print(" - registry.py (register_plan, update_plan_status — registry ops)")
|
||||
console.print(" - dashboard.py (push_all — dashboard updates)")
|
||||
console.print()
|
||||
console.print(" handlers/dashboard/ (via aipass_os.dev_central.devpulse)")
|
||||
console.print(" - operations.py (write_section — dashboard section writer)")
|
||||
console.print()
|
||||
console.print(" handlers/dplan/")
|
||||
console.print(" - branch_resolve.py (resolve_branch_target — @ branch resolution)")
|
||||
console.print(" - closed_plans_registry.py (append_closed_dplan — closed plan tracking)")
|
||||
console.print(" - log_setup.py (prepare_log_file — log file preparation)")
|
||||
console.print(" - background_spawn.py (spawn_post_close — background archival)")
|
||||
console.print()
|
||||
console.print(" External:")
|
||||
console.print(" - aipass.prax (write_section — dashboard section writer)")
|
||||
console.print()
|
||||
|
||||
|
||||
def print_help():
|
||||
|
||||
@@ -21,10 +21,8 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
# INFRASTRUCTURE IMPORT PATTERN
|
||||
AIPASS_ROOT = Path.home() / "aipass_core"
|
||||
DEVPULSE_ROOT = Path.home() / "aipass_os" / "dev_central" / "devpulse"
|
||||
sys.path.insert(0, str(AIPASS_ROOT))
|
||||
sys.path.insert(0, str(Path.home()))
|
||||
# dplan_post_close_runner.py → modules/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
# External: CLI console (Rich display) and Prax logger
|
||||
from aipass.cli.apps.modules import console
|
||||
@@ -32,9 +30,9 @@ from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
MODULE_NAME = "dplan_post_close_runner"
|
||||
|
||||
LOCK_FILE = DEVPULSE_ROOT / ".post_close_runner.lock"
|
||||
LOCK_FILE = FLOW_ROOT / ".post_close_runner.lock"
|
||||
|
||||
from aipass_os.dev_central.devpulse.apps.handlers.mbank.process import process_closed_plans
|
||||
from aipass.flow.apps.handlers.mbank.process import process_closed_plans
|
||||
|
||||
|
||||
def handle_command(command: str, args: list) -> bool:
|
||||
@@ -105,7 +103,7 @@ def print_introspection():
|
||||
console.print("Background post-close processing for DPLANs — runs Memory Bank archival")
|
||||
console.print()
|
||||
console.print("Connected Handlers:")
|
||||
console.print(" handlers/mbank/ (via aipass_os.dev_central.devpulse)")
|
||||
console.print(" handlers/mbank/ (local)")
|
||||
console.print(" - process.py (process_closed_plans — scan and archive closed plans)")
|
||||
console.print()
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# dev.local.md - FLOW
|
||||
```
|
||||
Branch: src/aipass/flow
|
||||
Created: 2026-03-07
|
||||
Updated: 2026-03-10
|
||||
```
|
||||
|
||||
## Active Work
|
||||
|
||||
- **FPLAN-0017**: Implement Two-Level Introspection Standard (from seedgo)
|
||||
|
||||
## Completed
|
||||
|
||||
- **FPLAN-0021**: Wire DPLANs into flow CLI router and clean up templates (2026-03-10)
|
||||
- Remapped dplan_flow.py imports from aipass_os to aipass.flow local handlers
|
||||
- Fixed template.py, display.py, dplan_post_close_runner.py Dev-Pass imports
|
||||
- Added DPLAN examples to --help, updated all references to drone syntax
|
||||
- Fixed routing order so `plan` subcommands work through flow.py
|
||||
- Updated FPLAN templates: @memory_bank to @memory
|
||||
- **Dispatch**: Handler path migration + template detection fix (2026-03-10)
|
||||
- All 9 DPLAN handler data paths migrated from aipass_os/aipass_core to Path(__file__).parents[N]
|
||||
- DPLANs now create/list/close in flow/dev_planning/ (verified)
|
||||
- Removed all sys.path hacks from handlers
|
||||
- Fixed is_template_content() data loss: removed section headers from markers, added user content detection
|
||||
|
||||
## Issues
|
||||
|
||||
- None currently tracked
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
- (none)
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"DPLAN": {
|
||||
"next_number": 2
|
||||
},
|
||||
"next_number": 2
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
# BPLAN-{{NUMBER}}: {{TOPIC}}
|
||||
|
||||
Tag: {{TAG}}
|
||||
|
||||
> One-line description
|
||||
|
||||
## Executive Summary
|
||||
What this business initiative achieves and why it matters.
|
||||
|
||||
## Market Analysis
|
||||
Target market, size, trends, and opportunity.
|
||||
|
||||
## Revenue Model
|
||||
How this generates or saves revenue. Pricing, margins, unit economics.
|
||||
|
||||
## Competitive Landscape
|
||||
Who else is doing this? What's our edge?
|
||||
|
||||
## KPIs
|
||||
| Metric | Target | Timeline |
|
||||
|--------|--------|----------|
|
||||
| Example | TBD | Q1 2026 |
|
||||
|
||||
## Go-to-Market
|
||||
Launch strategy, channels, partnerships.
|
||||
|
||||
## Risk Assessment
|
||||
| Risk | Impact | Mitigation |
|
||||
|------|--------|------------|
|
||||
| Example | High | Plan B |
|
||||
|
||||
## Timeline
|
||||
- [ ] Phase 1: Research & Validation
|
||||
- [ ] Phase 2: MVP / Pilot
|
||||
- [ ] Phase 3: Scale
|
||||
|
||||
## Budget Considerations
|
||||
Estimated costs, resource requirements, ROI timeline.
|
||||
|
||||
## Relationships
|
||||
- **Related BPLANs:** None yet
|
||||
- **Related DPLANs:** None yet
|
||||
- **Owner branches:** Who owns this
|
||||
|
||||
## Status
|
||||
- [x] Planning
|
||||
- [ ] In Progress
|
||||
- [ ] Ready for Execution
|
||||
- [ ] Complete
|
||||
- [ ] Abandoned
|
||||
|
||||
## Notes
|
||||
Session notes, discoveries, changes
|
||||
|
||||
---
|
||||
*Created: {{DATE}}*
|
||||
*Updated: {{DATE}}*
|
||||
@@ -82,7 +82,7 @@ ai_mail send @branch "Question: [topic]" "I'm working on X and need guidance on
|
||||
- Building something with email? Ask @ai_mail how delivery works
|
||||
- Need routing or @ resolution? Ask @drone
|
||||
- Unsure about standards? Ask @seedgo for reference code
|
||||
- Need persistent storage or search? Ask @memory_bank
|
||||
- Need persistent storage or search? Ask @memory
|
||||
- Event-driven behavior? Ask @trigger about their event system
|
||||
- Dashboard integration? Ask @devpulse about update_section()
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ Captured ideas, brainstorms, future possibilities. Add freely.
|
||||
## Relationships
|
||||
- **Related DPLANs:** None yet
|
||||
- **Related FPLANs:** None yet
|
||||
- **Owner branches:** Who builds this
|
||||
- **Owner branch:** Who builds this
|
||||
- **Seedgo standards:** `drone @seedgo audit aipass @branch` | `drone @seedgo standards_query aipass_standards`
|
||||
|
||||
## Status
|
||||
- [x] Planning
|
||||
|
||||
@@ -85,7 +85,7 @@ ai_mail send @branch "Question: [topic]" "I'm working on X and need guidance on
|
||||
- Building something with email? Ask @ai_mail how delivery works
|
||||
- Need routing or @ resolution? Ask @drone
|
||||
- Unsure about standards? Ask @seedgo for reference code
|
||||
- Need persistent storage or search? Ask @memory_bank
|
||||
- Need persistent storage or search? Ask @memory
|
||||
- Event-driven behavior? Ask @trigger about their event system
|
||||
- Dashboard integration? Ask @devpulse about update_section()
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - MEMORY
|
||||
```
|
||||
Branch: /home/patrick/Projects/AIPass/src/aipass/memory
|
||||
Created: 2026-03-07
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
@@ -0,0 +1,3 @@
|
||||
from .reader import read_all_centrals
|
||||
|
||||
__all__ = ["read_all_centrals"]
|
||||
@@ -0,0 +1,50 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: reader.py
|
||||
# Description: Central file reader — scans .ai_central/*.central.json
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-03-10
|
||||
# Modified: 2026-03-10
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Central File Reader
|
||||
|
||||
Reads all .ai_central/*.central.json files from the repo root
|
||||
and returns a dict keyed by service name.
|
||||
|
||||
Used by dashboard/refresh.py to populate branch dashboards.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
from aipass.prax.apps.handlers.config.load import _find_repo_root
|
||||
|
||||
|
||||
def read_all_centrals() -> Dict:
|
||||
"""
|
||||
Read all .central.json files from .ai_central/ at repo root.
|
||||
|
||||
Returns:
|
||||
Dict keyed by service name (e.g. 'ai_mail', 'plans', 'devpulse').
|
||||
Each value is the parsed JSON content of the central file.
|
||||
Returns empty dict if directory doesn't exist or has no files.
|
||||
"""
|
||||
repo_root = _find_repo_root()
|
||||
central_dir = repo_root / ".ai_central"
|
||||
|
||||
if not central_dir.is_dir():
|
||||
return {}
|
||||
|
||||
centrals = {}
|
||||
for central_file in central_dir.glob("*.central.json"):
|
||||
try:
|
||||
data = json.loads(central_file.read_text(encoding="utf-8"))
|
||||
# Key by service name: AI_MAIL.central.json -> ai_mail
|
||||
service_name = central_file.name.replace(".central.json", "").lower()
|
||||
centrals[service_name] = data
|
||||
except (json.JSONDecodeError, OSError):
|
||||
continue
|
||||
|
||||
return centrals
|
||||
@@ -0,0 +1,3 @@
|
||||
from .sync import sync_status
|
||||
|
||||
__all__ = ["sync_status"]
|
||||
@@ -0,0 +1,159 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: sync.py
|
||||
# Description: Status sync handler — scans branches for STATUS.local.md
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-03-10
|
||||
# Modified: 2026-03-10
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Status Sync Handler
|
||||
|
||||
Scans all registered branches for STATUS.local.md files,
|
||||
extracts state and last-update metadata, and builds an
|
||||
aggregated STATUS.md at the repo root with collapsible
|
||||
<details> sections per branch.
|
||||
|
||||
Usage (internal):
|
||||
from aipass.prax.apps.handlers.status.sync import sync_status
|
||||
result = sync_status()
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
from aipass.prax.apps.handlers.config.load import _find_repo_root
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_STATE_RE = re.compile(r"^\*\*State:\*\*\s*(.+)", re.MULTILINE)
|
||||
_UPDATE_RE = re.compile(r"^\*\*Last update:\*\*\s*(.+)", re.MULTILINE)
|
||||
|
||||
|
||||
def _extract_field(pattern: re.Pattern, text: str) -> str:
|
||||
"""Return the first capture group for *pattern* in *text*, stripped."""
|
||||
match = pattern.search(text)
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
return ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Core
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def sync_status() -> Dict:
|
||||
"""
|
||||
Scan all branches for STATUS.local.md, build central STATUS.md.
|
||||
|
||||
Returns:
|
||||
Dict with keys: status, branches_synced, branches_missing, timestamp
|
||||
"""
|
||||
repo_root = _find_repo_root()
|
||||
registry_path = repo_root / "AIPASS_REGISTRY.json"
|
||||
|
||||
if not registry_path.exists():
|
||||
logger.error("AIPASS_REGISTRY.json not found at %s", repo_root)
|
||||
return {
|
||||
"status": "error",
|
||||
"branches_synced": [],
|
||||
"branches_missing": [],
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||
}
|
||||
|
||||
try:
|
||||
registry = json.loads(registry_path.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError) as exc:
|
||||
logger.error("Failed to read AIPASS_REGISTRY.json: %s", exc)
|
||||
return {
|
||||
"status": "error",
|
||||
"branches_synced": [],
|
||||
"branches_missing": [],
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||
}
|
||||
|
||||
branches = registry.get("branches", [])
|
||||
|
||||
# Collect per-branch data ------------------------------------------------
|
||||
synced = []
|
||||
missing = []
|
||||
entries = [] # (branch_email, state, date, content)
|
||||
|
||||
for branch in sorted(branches, key=lambda b: b.get("email", "").lower()):
|
||||
branch_email = branch.get("email", f"@{branch.get('name', 'unknown').lower()}")
|
||||
branch_path = repo_root / branch.get("path", "")
|
||||
status_file = branch_path / "STATUS.local.md"
|
||||
|
||||
if not status_file.exists():
|
||||
missing.append(branch_email)
|
||||
logger.debug("STATUS.local.md missing for %s", branch_email)
|
||||
continue
|
||||
|
||||
try:
|
||||
content = status_file.read_text(encoding="utf-8")
|
||||
except OSError as exc:
|
||||
missing.append(branch_email)
|
||||
logger.warning("Could not read %s: %s", status_file, exc)
|
||||
continue
|
||||
|
||||
state = _extract_field(_STATE_RE, content) or "Unknown"
|
||||
last_update = _extract_field(_UPDATE_RE, content) or "n/a"
|
||||
|
||||
synced.append(branch_email)
|
||||
entries.append((branch_email, state, last_update, content))
|
||||
|
||||
# Tally states -----------------------------------------------------------
|
||||
state_counts: Dict[str, int] = {}
|
||||
for _, state, _, _ in entries:
|
||||
normalised = state.lower()
|
||||
state_counts[normalised] = state_counts.get(normalised, 0) + 1
|
||||
|
||||
operational = state_counts.get("operational", 0)
|
||||
in_progress = state_counts.get("in progress", state_counts.get("in-progress", 0))
|
||||
not_started = state_counts.get("not started", state_counts.get("not-started", 0))
|
||||
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
# Build STATUS.md --------------------------------------------------------
|
||||
lines = [
|
||||
"# AIPass — System Status",
|
||||
"",
|
||||
"> Auto-generated by `drone @prax status sync`. Do not edit manually.",
|
||||
"",
|
||||
f"**Last sync:** {now} ",
|
||||
f"**Summary:** {operational} operational | {in_progress} in-progress | {not_started} not started",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
]
|
||||
|
||||
for branch_email, state, last_update, content in entries:
|
||||
lines.append(
|
||||
f"<details><summary><strong>{branch_email}</strong> — {state} ({last_update})</summary>"
|
||||
)
|
||||
lines.append("")
|
||||
lines.append(content.rstrip())
|
||||
lines.append("")
|
||||
lines.append("</details>")
|
||||
lines.append("")
|
||||
|
||||
status_md = repo_root / "STATUS.md"
|
||||
status_md.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
logger.info(
|
||||
"STATUS.md synced: %d branches, %d missing", len(synced), len(missing)
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"branches_synced": synced,
|
||||
"branches_missing": missing,
|
||||
"timestamp": now,
|
||||
}
|
||||
@@ -445,25 +445,32 @@ def handle_command(command: str, args: List[str]) -> bool:
|
||||
Returns:
|
||||
True if command handled, False otherwise
|
||||
"""
|
||||
if command == "status":
|
||||
if command != "dashboard":
|
||||
return False
|
||||
|
||||
subcmd = args[0] if args else ""
|
||||
|
||||
if subcmd == "status":
|
||||
print_status()
|
||||
return True
|
||||
elif command == "template":
|
||||
elif subcmd == "template":
|
||||
print_template()
|
||||
return True
|
||||
elif command == "refresh":
|
||||
_handle_refresh(args)
|
||||
elif subcmd == "refresh":
|
||||
_handle_refresh(args[1:])
|
||||
return True
|
||||
elif command == "push-template":
|
||||
_handle_push_template(args)
|
||||
elif subcmd == "push-template":
|
||||
_handle_push_template(args[1:])
|
||||
return True
|
||||
elif command == "diff-template":
|
||||
_handle_diff_template(args)
|
||||
elif subcmd == "diff-template":
|
||||
_handle_diff_template(args[1:])
|
||||
return True
|
||||
elif command == "template-status":
|
||||
elif subcmd == "template-status":
|
||||
_handle_template_status()
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
print_help()
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: status_module.py
|
||||
# Description: PRAX Status Command
|
||||
# Version: 1.0.0
|
||||
# Version: 1.1.0
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2026-03-09
|
||||
# Modified: 2026-03-10
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -17,22 +17,17 @@ from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from aipass.prax.apps.modules.logger import get_system_status, system_logger as logger
|
||||
from aipass.cli.apps.modules import console, header, success, error
|
||||
from aipass.prax.apps.handlers.status.sync import sync_status
|
||||
from aipass.cli.apps.modules import console, header, success, error, warning
|
||||
|
||||
|
||||
def print_help():
|
||||
"""Display module help and connected handlers"""
|
||||
console.print()
|
||||
console.print("[bold cyan]PRAX Status Module[/bold cyan]")
|
||||
console.print()
|
||||
console.print("[yellow]Connected Handlers:[/yellow]")
|
||||
console.print()
|
||||
|
||||
console.print(" [cyan]prax/modules/[/cyan]")
|
||||
console.print(" [dim]- logger.py[/dim] (get_system_status, system_logger)")
|
||||
console.print()
|
||||
|
||||
console.print("[dim]Run 'python3 status_module.py --help' for usage[/dim]")
|
||||
console.print("[bold cyan]PRAX Status Commands:[/bold cyan]")
|
||||
console.print(" [white]status[/white] Show PRAX system status")
|
||||
console.print(" [white]status sync[/white] Scan all branches, build STATUS.md at repo root")
|
||||
console.print(" [white]status help[/white] Show this help")
|
||||
console.print()
|
||||
|
||||
|
||||
@@ -50,6 +45,15 @@ def handle_command(command: str, args: List[str]) -> bool:
|
||||
if command != 'status':
|
||||
return False
|
||||
|
||||
# --- sub-command routing ------------------------------------------------
|
||||
if args and args[0] in ("--help", "help"):
|
||||
print_help()
|
||||
return True
|
||||
|
||||
if args and args[0] == "sync":
|
||||
return _handle_sync()
|
||||
|
||||
# --- default: show PRAX system status -----------------------------------
|
||||
status = get_system_status()
|
||||
|
||||
console.print("\n📊 PRAX System Status")
|
||||
@@ -65,6 +69,38 @@ def handle_command(command: str, args: List[str]) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _handle_sync() -> bool:
|
||||
"""Run the status sync handler and display results."""
|
||||
console.print()
|
||||
console.print("[bold cyan]Syncing branch status...[/bold cyan]")
|
||||
|
||||
try:
|
||||
result = sync_status()
|
||||
except Exception as exc:
|
||||
error(f"Status sync failed: {exc}")
|
||||
logger.error("Status sync failed: %s", exc)
|
||||
return True
|
||||
|
||||
if result["status"] == "error":
|
||||
error("Status sync encountered an error — check logs for details.")
|
||||
return True
|
||||
|
||||
synced = result["branches_synced"]
|
||||
missing = result["branches_missing"]
|
||||
|
||||
success(f"STATUS.md written — {len(synced)} branches synced")
|
||||
|
||||
if missing:
|
||||
warning(
|
||||
f"{len(missing)} branches missing STATUS.local.md",
|
||||
details=", ".join(missing),
|
||||
)
|
||||
|
||||
console.print(f" [dim]Timestamp: {result['timestamp']}[/dim]")
|
||||
console.print()
|
||||
return True
|
||||
|
||||
|
||||
def print_introspection():
|
||||
"""Display module introspection info."""
|
||||
console.print()
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - PRAX
|
||||
```
|
||||
Branch: /home/coder/workspace/AIPass/src/aipass/prax
|
||||
Created: 2026-03-07
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
@@ -0,0 +1,35 @@
|
||||
# dev.local.md - SEEDGO
|
||||
```
|
||||
Branch: /home/coder/workspace/AIPass/src/aipass/seedgo
|
||||
Created: 2026-03-07
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
### PLAN: Diagnostics Architecture Refactor
|
||||
**Status:** completed
|
||||
|
||||
1. Move `aipass_standards/diagnostics_check.py` → `handlers/diagnostics/diagnostics_check.py` (shared orchestrator)
|
||||
2. Move `aipass_standards/diagnostics_content.py` + `diagnostics.md` → `aipass_standards/.sorting_unprocessed/.archive/` (not a standard triplet)
|
||||
3. Create `aipass_standards/diagnostics.json` — `{"python": true}` (per-pack config)
|
||||
4. Update `diagnostics_check.py` to read pack's `diagnostics.json`, call enabled runners from `handlers/diagnostics/`
|
||||
5. Update `branch_audit.py` — discover diagnostics from `handlers/diagnostics/` not from pack's `*_check.py`
|
||||
6. Verify: `drone @seedgo audit aipass seedgo`
|
||||
|
||||
### Type_Check → Diagnostics Merge
|
||||
**Status:** completed
|
||||
|
||||
- Removed separate `scores["type_check"]` from branch_audit.py
|
||||
- Fixed undefined `diag` variable → now reads from `results["diagnostics"]`
|
||||
- Verified: audit shows 23 standards, no Type_Check line, Diagnostics 100%
|
||||
|
||||
### Remaining for 100%
|
||||
- Missing `docs/` directory (Architecture -7%)
|
||||
- Missing `dropbox/` directory (Architecture)
|
||||
- Readme score 83% (Readme standard)
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - spawn
|
||||
```
|
||||
Branch: src/aipass/spawn
|
||||
Created: 2026-03-05
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
- None currently
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
- None currently
|
||||
@@ -0,0 +1,23 @@
|
||||
# dev.local.md - {{BRANCHNAME}}
|
||||
```
|
||||
Branch: {{CWD}}
|
||||
Created: {{DATE}}
|
||||
```
|
||||
|
||||
## Active Work
|
||||
|
||||
-
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
|
||||
## Notepad
|
||||
|
||||
-
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - TRIGGER
|
||||
```
|
||||
Branch: /home/coder/workspace/AIPass/src/aipass/trigger
|
||||
Created: 2026-03-07
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - commons
|
||||
```
|
||||
Branch: src/commons
|
||||
Created: 2026-03-10
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - skills
|
||||
```
|
||||
Branch: src/skills
|
||||
Created: 2026-03-10
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
Reference in New Issue
Block a user