diff --git a/src/aipass/devpulse/.aipass/aipass_local_prompt.md b/src/aipass/devpulse/.aipass/aipass_local_prompt.md index af37ea27..5540117b 100644 --- a/src/aipass/devpulse/.aipass/aipass_local_prompt.md +++ b/src/aipass/devpulse/.aipass/aipass_local_prompt.md @@ -46,7 +46,7 @@ Only branch with git WRITE access. WRITE git (commit, push, checkout, merge, res - Chained read+write blocks the whole command (e.g. `git log && git push` → blocked). Keep read and write in separate invocations. Three rules: -1. Work on dev, merge to main when satisfied. `drone @git merge dev` squash-merges. +1. Work on dev, merge to main when satisfied. `drone @git merge ` makes a MERGE COMMIT (`gh pr merge --merge`) — dev stays a clean FF-able ancestor of main, never diverges. Post-merge "dev 1 behind main" is just the merge commit (cosmetic); realign with `drone @git sync` from dev (no checkout). Sync local main without checkout: `git fetch origin main:main`. 2. You commit, agents don't. Agents build+test, report results. You review, commit. 3. Local files = source of truth. diff --git a/src/aipass/drone/.seedgo/bypass.json b/src/aipass/drone/.seedgo/bypass.json index 92c590d0..45467787 100644 --- a/src/aipass/drone/.seedgo/bypass.json +++ b/src/aipass/drone/.seedgo/bypass.json @@ -264,7 +264,7 @@ { "file": "apps/modules/git_module.py", "standard": "unused_function", - "lines": [655], + "lines": [665], "reason": "get_introspective() called dynamically via getattr() by module_registry_handler.py:219 for internal module introspection. Also tested in test_git_module, test_system_pr, test_devpulse_plugins, test_git_access." }, { diff --git a/src/aipass/drone/apps/handlers/git/sync_handler.py b/src/aipass/drone/apps/handlers/git/sync_handler.py index 36b2dd80..0b369750 100644 --- a/src/aipass/drone/apps/handlers/git/sync_handler.py +++ b/src/aipass/drone/apps/handlers/git/sync_handler.py @@ -10,7 +10,8 @@ Branch synchronization — works on both main and dev. On main: pulls latest from origin/main. -On dev: pulls origin/main into dev (realigns after PR merge). +On dev: fast-forwards dev to origin/main (realigns after PR merge). + Refuses if dev has diverged — no silent rewrite. From other branch: checks out main first, then pulls. """ @@ -24,10 +25,10 @@ from aipass.drone.apps.handlers.git.lock_handler import find_repo_root def sync_main(autostash: bool = False) -> dict: - """Checkout main and pull latest changes. + """Sync current branch with origin/main. On dev: fast-forward (no checkout). On main/other: pull. Args: - autostash: If True, stash local changes before pull and restore after. + autostash: If True, stash local changes before sync and restore after. Use when sync fails with 'unstaged changes' error. Returns: @@ -174,7 +175,13 @@ def sync_main(autostash: bool = False) -> dict: def _sync_dev(repo_root, autostash: bool = False) -> dict: - """Pull origin/main into dev branch to realign after PR merge.""" + """Fast-forward dev to origin/main after PR merge. + + Uses ``git merge --ff-only origin/main`` instead of rebase so that + existing commits are never silently rewritten. Since merges to main + are always merge-commits (not squash), dev is always fast-forwardable + after a merge. If it is not, the command refuses loudly. + """ stashed = False if autostash: @@ -198,7 +205,7 @@ def _sync_dev(repo_root, autostash: bool = False) -> dict: return {"success": False, "message": f"Fetch failed: {fetch.stderr.strip()}", "stdout": ""} result = subprocess.run( - ["git", "pull", "origin", "main", "--rebase"], + ["git", "merge", "--ff-only", "origin/main"], capture_output=True, text=True, cwd=str(repo_root), @@ -209,13 +216,45 @@ def _sync_dev(repo_root, autostash: bool = False) -> dict: if result.returncode != 0: raw = result.stderr.strip() - msg = f"Failed to sync dev from main: {raw}" - if not autostash and ("unstaged changes" in raw or "uncommitted changes" in raw): - msg += "\n Tip: retry with 'drone @git sync --autostash'" + if "fast-forward" in raw.lower(): + msg = "Dev has diverged from main — cannot fast-forward. Use 'drone @git fix' to resolve." + else: + msg = f"Failed to sync dev from main: {raw}" + logger.error(msg) return {"success": False, "message": msg, "stdout": result.stdout} stdout = result.stdout.strip() - msg = f"Synced dev from origin/main: {stdout}" + msg = f"Synced dev from origin/main (fast-forward): {stdout}" json_handler.log_operation("sync_dev", {"result": stdout, "autostash": autostash}) logger.info(msg) return {"success": True, "message": msg, "stdout": stdout} + + +def sync_main_ref() -> dict: + """Update local main ref to match origin without checkout. + + Uses git fetch origin main:main — works from any branch. + Fails if local main has commits not on origin (non-fast-forward). + """ + repo_root = find_repo_root() + try: + result = subprocess.run( + ["git", "fetch", "origin", "main:main"], + capture_output=True, + text=True, + cwd=str(repo_root), + ) + if result.returncode != 0: + msg = f"Cannot update local main ref: {result.stderr.strip()}" + logger.error(msg) + return {"success": False, "message": msg, "stdout": ""} + + stdout = result.stdout.strip() + msg = f"Updated local main ref: {stdout or 'up to date'}" + json_handler.log_operation("sync_main_ref", {"result": stdout}) + logger.info(msg) + return {"success": True, "message": msg, "stdout": stdout} + except (OSError, subprocess.SubprocessError) as exc: + msg = f"sync_main_ref failed: {exc}" + logger.error(msg) + return {"success": False, "message": msg, "stdout": ""} diff --git a/src/aipass/drone/apps/modules/git_module.py b/src/aipass/drone/apps/modules/git_module.py index 4acb82c3..eefbc999 100644 --- a/src/aipass/drone/apps/modules/git_module.py +++ b/src/aipass/drone/apps/modules/git_module.py @@ -487,6 +487,12 @@ def _handle_checkout(args: list[str]) -> dict: def _handle_sync(args: list[str]) -> dict: """Handle the sync subcommand (owner tier).""" + if "--main-ref" in args: + result = sync_handler.sync_main_ref() + if result["success"]: + return {"stdout": result["message"], "stderr": "", "exit_code": 0} + return {"stdout": "", "stderr": result["message"], "exit_code": 1} + autostash = "--autostash" in args result = sync_handler.sync_main(autostash=autostash) @@ -598,9 +604,13 @@ def get_help(command: str | None = None) -> str: return "git checkout — Switch branches (main or dev only) [owner]\n" if command == "sync": return ( - "git sync [--autostash] — Checkout main and pull latest changes [owner]\n" + "git sync [--autostash] [--main-ref] — Sync with origin/main [owner]\n" + " On dev: fast-forward to origin/main (stays on dev, no checkout).\n" + " On main: pull latest. From other branch: checkout main first.\n" + " Refuses if dev has diverged — no silent rewrite.\n" " Options:\n" - " --autostash Stash local changes before pull and restore after.\n" + " --autostash Stash local changes before sync and restore after.\n" + " --main-ref Update local main ref without checkout (from any branch).\n" ) if command == "unlock": return "git unlock --force — Force-release the PR lock [owner]\n" @@ -645,7 +655,7 @@ def get_help(command: str | None = None) -> str: " delete-branch Delete a remote branch\n" " close-pr Close a PR\n" " merge Merge a PR\n" - " sync [--autostash] Checkout main and pull\n" + " sync [--autostash] Sync with origin/main (FF on dev)\n" " smart-sync Fetch + rebase if behind\n" " unlock --force Force-release the PR lock\n" " fix [--dry-run] Fix broken git states\n" diff --git a/src/aipass/drone/tests/test_git_module.py b/src/aipass/drone/tests/test_git_module.py index fff1eb6f..035d307b 100644 --- a/src/aipass/drone/tests/test_git_module.py +++ b/src/aipass/drone/tests/test_git_module.py @@ -365,6 +365,168 @@ class TestSyncHandler: assert "failed" in result["message"].lower() +class TestSyncDev: + """Sync from dev branch — fast-forward, not rebase.""" + + def test_sync_dev_ff_success(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Sync on dev fast-forwards cleanly when behind main.""" + registry = tmp_path / "AIPASS_REGISTRY.json" + registry.write_text("{}", encoding="utf-8") + monkeypatch.chdir(tmp_path) + + mock_head = MagicMock(returncode=0, stdout="dev", stderr="") + mock_fetch = MagicMock(returncode=0, stdout="", stderr="") + mock_merge = MagicMock(returncode=0, stdout="Updating abc..def\nFast-forward", stderr="") + + with patch( + "aipass.drone.apps.handlers.git.sync_handler.subprocess.run", + side_effect=[mock_head, mock_fetch, mock_merge], + ): + result = sync_main() + + assert result["success"] is True + assert "dev" in result["message"].lower() + + def test_sync_dev_stays_on_dev(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Sync on dev does NOT checkout main — no git checkout call.""" + registry = tmp_path / "AIPASS_REGISTRY.json" + registry.write_text("{}", encoding="utf-8") + monkeypatch.chdir(tmp_path) + + mock_head = MagicMock(returncode=0, stdout="dev", stderr="") + mock_fetch = MagicMock(returncode=0, stdout="", stderr="") + mock_merge = MagicMock(returncode=0, stdout="Already up to date.", stderr="") + + with patch( + "aipass.drone.apps.handlers.git.sync_handler.subprocess.run", + side_effect=[mock_head, mock_fetch, mock_merge], + ) as mock_run: + sync_main() + + cmds = [call[0][0] for call in mock_run.call_args_list] + assert not any("checkout" in cmd for cmd in cmds), "sync on dev must not checkout" + + def test_sync_dev_uses_ff_only(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Sync on dev uses git merge --ff-only, not git pull --rebase.""" + registry = tmp_path / "AIPASS_REGISTRY.json" + registry.write_text("{}", encoding="utf-8") + monkeypatch.chdir(tmp_path) + + mock_head = MagicMock(returncode=0, stdout="dev", stderr="") + mock_fetch = MagicMock(returncode=0, stdout="", stderr="") + mock_merge = MagicMock(returncode=0, stdout="Fast-forward", stderr="") + + with patch( + "aipass.drone.apps.handlers.git.sync_handler.subprocess.run", + side_effect=[mock_head, mock_fetch, mock_merge], + ) as mock_run: + sync_main() + + merge_call = mock_run.call_args_list[2][0][0] + assert "merge" in merge_call, "should use git merge" + assert "--ff-only" in merge_call, "should use --ff-only" + assert "--rebase" not in str(mock_run.call_args_list), "must NOT use rebase" + + def test_sync_dev_refuses_on_diverge(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Sync on dev fails loud when dev has diverged (not fast-forwardable).""" + registry = tmp_path / "AIPASS_REGISTRY.json" + registry.write_text("{}", encoding="utf-8") + monkeypatch.chdir(tmp_path) + + mock_head = MagicMock(returncode=0, stdout="dev", stderr="") + mock_fetch = MagicMock(returncode=0, stdout="", stderr="") + mock_merge = MagicMock(returncode=1, stdout="", stderr="fatal: Not possible to fast-forward, aborting.") + + with patch( + "aipass.drone.apps.handlers.git.sync_handler.subprocess.run", + side_effect=[mock_head, mock_fetch, mock_merge], + ): + result = sync_main() + + assert result["success"] is False + assert "fast-forward" in result["message"].lower() or "diverged" in result["message"].lower() + + def test_sync_dev_autostash(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Sync on dev with --autostash stashes and pops.""" + registry = tmp_path / "AIPASS_REGISTRY.json" + registry.write_text("{}", encoding="utf-8") + monkeypatch.chdir(tmp_path) + + mock_head = MagicMock(returncode=0, stdout="dev", stderr="") + mock_stash = MagicMock(returncode=0, stdout="Saved working directory", stderr="") + mock_fetch = MagicMock(returncode=0, stdout="", stderr="") + mock_merge = MagicMock(returncode=0, stdout="Fast-forward", stderr="") + mock_pop = MagicMock(returncode=0, stdout="Restored", stderr="") + + with patch( + "aipass.drone.apps.handlers.git.sync_handler.subprocess.run", + side_effect=[mock_head, mock_stash, mock_fetch, mock_merge, mock_pop], + ) as mock_run: + result = sync_main(autostash=True) + + assert result["success"] is True + cmds = [call[0][0] for call in mock_run.call_args_list] + assert any(cmd == ["git", "stash"] for cmd in cmds) + assert any(cmd == ["git", "stash", "pop"] for cmd in cmds) + + def test_sync_dev_fetch_failure(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Fetch failure in dev sync returns error.""" + registry = tmp_path / "AIPASS_REGISTRY.json" + registry.write_text("{}", encoding="utf-8") + monkeypatch.chdir(tmp_path) + + mock_head = MagicMock(returncode=0, stdout="dev", stderr="") + mock_fetch = MagicMock(returncode=1, stdout="", stderr="fatal: unable to access remote") + + with patch( + "aipass.drone.apps.handlers.git.sync_handler.subprocess.run", + side_effect=[mock_head, mock_fetch], + ): + result = sync_main() + + assert result["success"] is False + assert "fetch" in result["message"].lower() + + +class TestSyncMainRef: + """Sync local main ref without checkout.""" + + def test_sync_main_ref_success(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """sync_main_ref updates local main ref.""" + from aipass.drone.apps.handlers.git.sync_handler import sync_main_ref + + registry = tmp_path / "AIPASS_REGISTRY.json" + registry.write_text("{}", encoding="utf-8") + monkeypatch.chdir(tmp_path) + + mock_result = MagicMock(returncode=0, stdout="", stderr="") + with patch( + "aipass.drone.apps.handlers.git.sync_handler.subprocess.run", + return_value=mock_result, + ): + result = sync_main_ref() + + assert result["success"] is True + + def test_sync_main_ref_failure(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """sync_main_ref fails on non-fast-forward.""" + from aipass.drone.apps.handlers.git.sync_handler import sync_main_ref + + registry = tmp_path / "AIPASS_REGISTRY.json" + registry.write_text("{}", encoding="utf-8") + monkeypatch.chdir(tmp_path) + + mock_result = MagicMock(returncode=1, stdout="", stderr="! [rejected] main -> main (non-fast-forward)") + with patch( + "aipass.drone.apps.handlers.git.sync_handler.subprocess.run", + return_value=mock_result, + ): + result = sync_main_ref() + + assert result["success"] is False + assert "main" in result["message"].lower() + + # =========================================================================== # 4. pr_handler — error paths (no actual git) # =========================================================================== @@ -671,6 +833,22 @@ class TestGitModuleRouting: assert result["exit_code"] == 0 + @patch("aipass.drone.apps.plugins.devpulse_ops.auth.verify_git_access", return_value="devpulse") + def test_sync_main_ref_routes(self, _mock_auth: MagicMock, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """sync --main-ref routes to sync_main_ref.""" + registry = tmp_path / "AIPASS_REGISTRY.json" + registry.write_text("{}", encoding="utf-8") + monkeypatch.chdir(tmp_path) + + mock_result = MagicMock(returncode=0, stdout="", stderr="") + with patch( + "aipass.drone.apps.handlers.git.sync_handler.subprocess.run", + return_value=mock_result, + ): + result = handle_command("sync", ["--main-ref"]) + + assert result["exit_code"] == 0 + @patch("aipass.drone.apps.plugins.devpulse_ops.auth.verify_git_access", return_value="test_branch") def test_status_no_branch_dir(self, _mock_auth: MagicMock, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """status outside a branch directory returns error.""" diff --git a/src/aipass/flow/templates/playbook_plans/merge.md b/src/aipass/flow/templates/playbook_plans/merge.md index 53b2061c..dffa9921 100644 --- a/src/aipass/flow/templates/playbook_plans/merge.md +++ b/src/aipass/flow/templates/playbook_plans/merge.md @@ -47,18 +47,22 @@ Run by **devpulse** (only branch with git write). Tick each step as you go; fill ### Why `dev` shows "behind main" after a merge — and why it's fine -`drone @git merge` does a **squash** merge: GitHub bundles dev's commits into **one brand-new -commit** on main. Your `dev` branch keeps its **original** commits. Git compares by commit -*identity*, not content — so it sees "main has 1 commit dev doesn't" → the UI shows -**"dev is 1 behind main."** +`drone @git merge` runs `gh pr merge --merge` — a **merge commit**, not a squash. GitHub +adds a merge commit on main whose parent IS dev's tip. After merging, `dev` is a **clean +ancestor** of `main` (fast-forwardable), never diverged. The "dev is 1 behind main" is +just that one merge commit — **cosmetic and trivially resolved**. - **The files are identical. It is 100% cosmetic. You can always move forward** — the next `dev-pr` compares real file changes and works perfectly regardless of this graph quirk. -- Because it's a squash (not a fast-forward), **`dev` is NOT an ancestor of `main`**, so - `git merge --ff-only main` will **fail**. Do not use it after a squash merge. -- **Optional**, only if you want the graph to show dev even/ahead: back-merge with - `git merge origin/main` while on dev (via `drone @git`) — creates a merge commit, dev moves - *ahead*, push normally (NO force, NO reset). **Do NOT rebase** (rewrites dev, needs force-push). +- Because `dev` is a clean ancestor of `main`, **`git merge --ff-only origin/main` on dev + WORKS** — a clean fast-forward realign, no merge commit created, no history rewrite. +- **Realign dev to even** (recommended): `drone @git sync` from dev (clean FF), or + manually `git merge --ff-only origin/main` on dev. No force-push, no rebase needed. +- **Sync local `main` ref WITHOUT checkout**: `git fetch origin main:main` — updates the + local main ref to match origin with **zero working-tree touch, no checkout**. This is the + answer to the IDE "switch to main → your local changes would be overwritten by checkout" + dialog: that dialog is git SAFETY working — **Cancel, never Force Checkout**. You never + need to stand on main. --- @@ -97,15 +101,16 @@ The PR gate (verified against `.github/workflows/`): ## 5. Merge to main - [ ] **User's call to merge** — confirm GO -- [ ] `drone @git merge ` (squash-merge) +- [ ] `drone @git merge ` (merge commit via `gh pr merge --merge`) - [ ] ⚠️ The merge command **echoes the PR's ORIGINAL opening description** — often stale if the PR accumulated more work after it was opened. Don't trust it as the merge summary; the real contents are `git log main..dev` from before the merge. - [ ] ⚠️ **Verify `dev` SURVIVES the merge** (the #625 scar — empirical, every time): `drone @git branches` → `dev` still present; `git rev-parse dev` resolves ## 6. Post-merge realign -- [ ] **Expect `dev` to show "1 behind main" — that's the squash artifact, it's cosmetic, keep going.** See "Why dev shows behind main" up top. Do NOT reach for `--ff-only` (it fails after a squash) or a rebase/reset. -- [ ] **Stay on `dev`. Do not check out `main`.** Local main being behind is fine and expected — it's a push-target, not a thing to maintain. -- [ ] (Optional, cosmetic only) If you want the graph to show dev even/ahead: back-merge `git merge origin/main` on dev (via `drone @git`), then normal push. Never rebase, never reset, never checkout main. +- [ ] **Expect `dev` to show "1 behind main" — that's the merge commit, it's cosmetic + fast-forwardable.** See "Why dev shows behind main" up top. +- [ ] **Realign dev** (recommended): `drone @git sync` from dev, or `git merge --ff-only origin/main` on dev. Clean FF, no merge commit, no rewrite. +- [ ] **Stay on `dev`. Do not check out `main`.** Local main being behind is fine — sync it without checkout: `git fetch origin main:main` (zero working-tree touch). +- [ ] Never rebase, never reset, never checkout main. - [ ] Dependabot / other PRs targeting main: they go green once main has the fix + bots rebase — check after the push ## 7. Release tag (only if cutting a release) diff --git a/src/aipass/spawn/apps/handlers/regenerate_registry_ops.py b/src/aipass/spawn/apps/handlers/regenerate_registry_ops.py index 719c828a..f677fccd 100644 --- a/src/aipass/spawn/apps/handlers/regenerate_registry_ops.py +++ b/src/aipass/spawn/apps/handlers/regenerate_registry_ops.py @@ -87,7 +87,7 @@ def regenerate_template_registry(template_dir: Path) -> dict: try: tmp_path = registry_path.with_suffix(".tmp") tmp_path.write_text( - json.dumps(registry, indent=2, ensure_ascii=False) + "\n", + json.dumps(registry, indent=2, sort_keys=True, ensure_ascii=False) + "\n", encoding="utf-8", ) tmp_path.replace(registry_path) @@ -213,27 +213,27 @@ def _scan_template_directory( ) # Assign IDs to files with three-pass global matching. - # This prevents new files from stealing IDs that existing files should claim. + # Path first (stable), then hash (handles renames), then new IDs. files: dict[str, dict] = {} unmatched_files: list[dict] = [] - # Pass 1: hash matching for all files + # Pass 1: path matching (deterministic — same path keeps same ID) for entry in raw_files: - content_hash = entry.get("content_hash", "") - if content_hash and content_hash in hash_to_id: - candidate = hash_to_id[content_hash] + path = entry.get("path", "") + if path and path in path_to_file_id: + candidate = path_to_file_id[path] if candidate not in claimed_file_ids: claimed_file_ids.add(candidate) files[candidate] = entry continue unmatched_files.append(entry) - # Pass 2: path matching for remaining files + # Pass 2: hash matching for remaining files (catches renames) still_unmatched: list[dict] = [] for entry in unmatched_files: - path = entry.get("path", "") - if path and path in path_to_file_id: - candidate = path_to_file_id[path] + content_hash = entry.get("content_hash", "") + if content_hash and content_hash in hash_to_id: + candidate = hash_to_id[content_hash] if candidate not in claimed_file_ids: claimed_file_ids.add(candidate) files[candidate] = entry diff --git a/src/aipass/spawn/templates/builder/.spawn/.template_registry.json b/src/aipass/spawn/templates/builder/.spawn/.template_registry.json index 3ce736c9..7b52f18e 100644 --- a/src/aipass/spawn/templates/builder/.spawn/.template_registry.json +++ b/src/aipass/spawn/templates/builder/.spawn/.template_registry.json @@ -1,390 +1,390 @@ { - "metadata": { - "version": "1.0.0", - "last_updated": "2026-06-10", - "description": "Template file tracking registry for ID-based updates" + "directories": { + "d001": { + "has_branch_placeholder": false, + "name": ".ai_mail.local", + "path": ".ai_mail.local" + }, + "d003": { + "has_branch_placeholder": false, + "name": ".aipass", + "path": ".aipass" + }, + "d004": { + "has_branch_placeholder": false, + "name": ".archive", + "path": ".archive" + }, + "d005": { + "has_branch_placeholder": false, + "name": ".claude", + "path": ".claude" + }, + "d006": { + "has_branch_placeholder": false, + "name": ".seedgo", + "path": ".seedgo" + }, + "d007": { + "has_branch_placeholder": false, + "name": ".trinity", + "path": ".trinity" + }, + "d008": { + "has_branch_placeholder": false, + "name": "apps", + "path": "apps" + }, + "d009": { + "has_branch_placeholder": false, + "name": "handlers", + "path": "apps/handlers" + }, + "d010": { + "has_branch_placeholder": false, + "name": "modules", + "path": "apps/modules" + }, + "d011": { + "has_branch_placeholder": false, + "name": "plugins", + "path": "apps/plugins" + }, + "d012": { + "has_branch_placeholder": false, + "name": "artifacts", + "path": "artifacts" + }, + "d013": { + "has_branch_placeholder": false, + "name": "docs", + "path": "docs" + }, + "d014": { + "has_branch_placeholder": false, + "name": "docs.local", + "path": "docs.local" + }, + "d015": { + "has_branch_placeholder": false, + "name": "sub_agent_drops", + "path": "docs.local/sub_agent_drops" + }, + "d016": { + "has_branch_placeholder": false, + "name": "dropbox", + "path": "dropbox" + }, + "d017": { + "has_branch_placeholder": false, + "name": "logs", + "path": "logs" + }, + "d018": { + "has_branch_placeholder": false, + "name": "templates", + "path": "templates" + }, + "d019": { + "has_branch_placeholder": false, + "name": "tests", + "path": "tests" + }, + "d020": { + "has_branch_placeholder": false, + "name": "tools", + "path": "tools" + }, + "d021": { + "has_branch_placeholder": true, + "name": "{{BRANCH}}_json", + "path": "{{BRANCH}}_json" + }, + "d022": { + "has_branch_placeholder": false, + "name": "custom_config", + "path": "{{BRANCH}}_json/custom_config" + }, + "d023": { + "has_branch_placeholder": false, + "name": ".spawn", + "path": ".spawn" + }, + "d024": { + "has_branch_placeholder": false, + "name": "integrations", + "path": "apps/integrations" + } }, "files": { "f001": { - "path": ".ai_mail.local/README.md", - "name": "README.md", "content_hash": "49299c242a01", - "has_branch_placeholder": false + "has_branch_placeholder": false, + "name": "README.md", + "path": ".ai_mail.local/README.md" }, "f002": { - "path": ".ai_mail.local/inbox.json", - "name": "inbox.json", "content_hash": "c9702fe2cc21", - "has_branch_placeholder": false + "has_branch_placeholder": false, + "name": "inbox.json", + "path": ".ai_mail.local/inbox.json" }, "f003": { - "path": ".aipass/README.md", - "name": "README.md", "content_hash": "f42d87684fdf", - "has_branch_placeholder": false + "has_branch_placeholder": false, + "name": "README.md", + "path": ".aipass/README.md" }, "f004": { - "path": ".aipass/aipass_local_prompt.md", - "name": "aipass_local_prompt.md", "content_hash": "bf82b35fa7d5", - "has_branch_placeholder": false + "has_branch_placeholder": false, + "name": "aipass_local_prompt.md", + "path": ".aipass/aipass_local_prompt.md" }, "f005": { - "path": ".archive/README.md", - "name": "README.md", "content_hash": "93d3fcb74f23", - "has_branch_placeholder": false + "has_branch_placeholder": false, + "name": "README.md", + "path": ".archive/README.md" }, "f006": { - "path": ".claude/README.md", - "name": "README.md", "content_hash": "adb0ce8c53c1", - "has_branch_placeholder": false + "has_branch_placeholder": false, + "name": "README.md", + "path": ".claude/README.md" }, "f007": { - "path": ".claude/settings.local.json", - "name": "settings.local.json", "content_hash": "eacf065629cd", - "has_branch_placeholder": false + "has_branch_placeholder": false, + "name": "settings.local.json", + "path": ".claude/settings.local.json" }, "f008": { - "path": ".gitignore", - "name": ".gitignore", "content_hash": "2dd6758a96d5", - "has_branch_placeholder": false + "has_branch_placeholder": false, + "name": ".gitignore", + "path": ".gitignore" }, "f009": { - "path": ".seedgo/README.md", - "name": "README.md", "content_hash": "ea03468bbf16", - "has_branch_placeholder": false + "has_branch_placeholder": false, + "name": "README.md", + "path": ".seedgo/README.md" }, "f010": { - "path": ".seedgo/bypass.json", - "name": "bypass.json", "content_hash": "0ac90a35515b", - "has_branch_placeholder": false - }, - "f024": { - "path": ".spawn/.registry_ignore.json", - "name": ".registry_ignore.json", - "content_hash": "34f5e7ff7e01", - "has_branch_placeholder": false - }, - "f043": { - "path": ".spawn/README.md", - "name": "README.md", - "content_hash": "e22ad5337efd", - "has_branch_placeholder": false + "has_branch_placeholder": false, + "name": "bypass.json", + "path": ".seedgo/bypass.json" }, "f011": { - "path": ".trinity/README.md", - "name": "README.md", "content_hash": "f461c9b16fc5", - "has_branch_placeholder": false + "has_branch_placeholder": false, + "name": "README.md", + "path": ".trinity/README.md" }, "f012": { - "path": ".trinity/local.json", - "name": "local.json", "content_hash": "8f8a98e42d92", - "has_branch_placeholder": false + "has_branch_placeholder": false, + "name": "local.json", + "path": ".trinity/local.json" }, "f013": { - "path": ".trinity/observations.json", - "name": "observations.json", "content_hash": "62160dfa243c", - "has_branch_placeholder": false + "has_branch_placeholder": false, + "name": "observations.json", + "path": ".trinity/observations.json" }, "f014": { - "path": ".trinity/passport.json", - "name": "passport.json", "content_hash": "a05a4352e7a9", - "has_branch_placeholder": false - }, - "f016": { - "path": "DASHBOARD.local.json", - "name": "DASHBOARD.local.json", - "content_hash": "f4775daf1f75", - "has_branch_placeholder": false - }, - "f017": { - "path": "README.md", - "name": "README.md", - "content_hash": "ad99517a50f9", - "has_branch_placeholder": false - }, - "f019": { - "path": "apps/README.md", - "name": "README.md", - "content_hash": "92a956009e0e", - "has_branch_placeholder": false - }, - "f020": { - "path": "apps/__init__.py", - "name": "__init__.py", - "content_hash": "0ef6c27137dc", - "has_branch_placeholder": false - }, - "f021": { - "path": "apps/handlers/README.md", - "name": "README.md", - "content_hash": "2e4f4a0c1b47", - "has_branch_placeholder": false - }, - "f022": { - "path": "apps/handlers/__init__.py", - "name": "__init__.py", - "content_hash": "dbfc0e044461", - "has_branch_placeholder": false - }, - "f044": { - "path": "apps/integrations/README.md", - "name": "README.md", - "content_hash": "31c09afe1299", - "has_branch_placeholder": false - }, - "f023": { - "path": "apps/modules/README.md", - "name": "README.md", - "content_hash": "a4cf0a8e3b4f", - "has_branch_placeholder": false - }, - "f018": { - "path": "apps/modules/__init__.py", - "name": "__init__.py", - "content_hash": "e3b0c44298fc", - "has_branch_placeholder": false - }, - "f025": { - "path": "apps/plugins/README.md", - "name": "README.md", - "content_hash": "d1e4e2b98c38", - "has_branch_placeholder": false - }, - "f027": { - "path": "apps/{{BRANCH}}.py", - "name": "{{BRANCH}}.py", - "content_hash": "024209a8c889", - "has_branch_placeholder": true - }, - "f028": { - "path": "artifacts/README.md", - "name": "README.md", - "content_hash": "de20d11e5cfd", - "has_branch_placeholder": false - }, - "f029": { - "path": "artifacts/birth_certificate.json", - "name": "birth_certificate.json", - "content_hash": "0b6e4319781e", - "has_branch_placeholder": false - }, - "f030": { - "path": "docs/README.md", - "name": "README.md", - "content_hash": "2434da568727", - "has_branch_placeholder": false - }, - "f031": { - "path": "docs.local/README.md", - "name": "README.md", - "content_hash": "c19d8872ea2c", - "has_branch_placeholder": false - }, - "f032": { - "path": "docs.local/sub_agent_drops/README.md", - "name": "README.md", - "content_hash": "e3e5a6b9c9c5", - "has_branch_placeholder": false - }, - "f033": { - "path": "dropbox/README.md", - "name": "README.md", - "content_hash": "9e1e9b71f93b", - "has_branch_placeholder": false - }, - "f034": { - "path": "logs/README.md", - "name": "README.md", - "content_hash": "4ca207af6bd3", - "has_branch_placeholder": false - }, - "f035": { - "path": "pytest.ini", - "name": "pytest.ini", - "content_hash": "7b39ba7bca40", - "has_branch_placeholder": false - }, - "f045": { - "path": "requirements.project.txt", - "name": "requirements.project.txt", - "content_hash": "1facc521802b", - "has_branch_placeholder": false - }, - "f036": { - "path": "templates/README.md", - "name": "README.md", - "content_hash": "73b020b003f9", - "has_branch_placeholder": false - }, - "f037": { - "path": "tests/README.md", - "name": "README.md", - "content_hash": "c157895c9b27", - "has_branch_placeholder": false - }, - "f038": { - "path": "tests/__init__.py", - "name": "__init__.py", - "content_hash": "881f06bb6574", - "has_branch_placeholder": false - }, - "f039": { - "path": "tests/conftest.py", - "name": "conftest.py", - "content_hash": "97f220799d19", - "has_branch_placeholder": false - }, - "f040": { - "path": "tools/README.md", - "name": "README.md", - "content_hash": "3c7eaedb16ac", - "has_branch_placeholder": false - }, - "f041": { - "path": "{{BRANCH}}_json/README.md", - "name": "README.md", - "content_hash": "e64fa555e7b8", - "has_branch_placeholder": false - }, - "f042": { - "path": "{{BRANCH}}_json/custom_config/README.md", - "name": "README.md", - "content_hash": "28e9ae373563", - "has_branch_placeholder": false + "has_branch_placeholder": false, + "name": "passport.json", + "path": ".trinity/passport.json" }, "f015": { - "path": "apps/plugins/__init__.py", - "name": "__init__.py", "content_hash": "e3b0c44298fc", - "has_branch_placeholder": false + "has_branch_placeholder": false, + "name": "__init__.py", + "path": "apps/plugins/__init__.py" + }, + "f016": { + "content_hash": "f4775daf1f75", + "has_branch_placeholder": false, + "name": "DASHBOARD.local.json", + "path": "DASHBOARD.local.json" + }, + "f017": { + "content_hash": "ad99517a50f9", + "has_branch_placeholder": false, + "name": "README.md", + "path": "README.md" + }, + "f018": { + "content_hash": "e3b0c44298fc", + "has_branch_placeholder": false, + "name": "__init__.py", + "path": "apps/modules/__init__.py" + }, + "f019": { + "content_hash": "92a956009e0e", + "has_branch_placeholder": false, + "name": "README.md", + "path": "apps/README.md" + }, + "f020": { + "content_hash": "0ef6c27137dc", + "has_branch_placeholder": false, + "name": "__init__.py", + "path": "apps/__init__.py" + }, + "f021": { + "content_hash": "2e4f4a0c1b47", + "has_branch_placeholder": false, + "name": "README.md", + "path": "apps/handlers/README.md" + }, + "f022": { + "content_hash": "dbfc0e044461", + "has_branch_placeholder": false, + "name": "__init__.py", + "path": "apps/handlers/__init__.py" + }, + "f023": { + "content_hash": "a4cf0a8e3b4f", + "has_branch_placeholder": false, + "name": "README.md", + "path": "apps/modules/README.md" + }, + "f024": { + "content_hash": "34f5e7ff7e01", + "has_branch_placeholder": false, + "name": ".registry_ignore.json", + "path": ".spawn/.registry_ignore.json" + }, + "f025": { + "content_hash": "d1e4e2b98c38", + "has_branch_placeholder": false, + "name": "README.md", + "path": "apps/plugins/README.md" + }, + "f027": { + "content_hash": "024209a8c889", + "has_branch_placeholder": true, + "name": "{{BRANCH}}.py", + "path": "apps/{{BRANCH}}.py" + }, + "f028": { + "content_hash": "de20d11e5cfd", + "has_branch_placeholder": false, + "name": "README.md", + "path": "artifacts/README.md" + }, + "f029": { + "content_hash": "0b6e4319781e", + "has_branch_placeholder": false, + "name": "birth_certificate.json", + "path": "artifacts/birth_certificate.json" + }, + "f030": { + "content_hash": "2434da568727", + "has_branch_placeholder": false, + "name": "README.md", + "path": "docs/README.md" + }, + "f031": { + "content_hash": "c19d8872ea2c", + "has_branch_placeholder": false, + "name": "README.md", + "path": "docs.local/README.md" + }, + "f032": { + "content_hash": "e3e5a6b9c9c5", + "has_branch_placeholder": false, + "name": "README.md", + "path": "docs.local/sub_agent_drops/README.md" + }, + "f033": { + "content_hash": "9e1e9b71f93b", + "has_branch_placeholder": false, + "name": "README.md", + "path": "dropbox/README.md" + }, + "f034": { + "content_hash": "4ca207af6bd3", + "has_branch_placeholder": false, + "name": "README.md", + "path": "logs/README.md" + }, + "f035": { + "content_hash": "7b39ba7bca40", + "has_branch_placeholder": false, + "name": "pytest.ini", + "path": "pytest.ini" + }, + "f036": { + "content_hash": "73b020b003f9", + "has_branch_placeholder": false, + "name": "README.md", + "path": "templates/README.md" + }, + "f037": { + "content_hash": "c157895c9b27", + "has_branch_placeholder": false, + "name": "README.md", + "path": "tests/README.md" + }, + "f038": { + "content_hash": "881f06bb6574", + "has_branch_placeholder": false, + "name": "__init__.py", + "path": "tests/__init__.py" + }, + "f039": { + "content_hash": "97f220799d19", + "has_branch_placeholder": false, + "name": "conftest.py", + "path": "tests/conftest.py" + }, + "f040": { + "content_hash": "3c7eaedb16ac", + "has_branch_placeholder": false, + "name": "README.md", + "path": "tools/README.md" + }, + "f041": { + "content_hash": "e64fa555e7b8", + "has_branch_placeholder": false, + "name": "README.md", + "path": "{{BRANCH}}_json/README.md" + }, + "f042": { + "content_hash": "28e9ae373563", + "has_branch_placeholder": false, + "name": "README.md", + "path": "{{BRANCH}}_json/custom_config/README.md" + }, + "f043": { + "content_hash": "e22ad5337efd", + "has_branch_placeholder": false, + "name": "README.md", + "path": ".spawn/README.md" + }, + "f044": { + "content_hash": "31c09afe1299", + "has_branch_placeholder": false, + "name": "README.md", + "path": "apps/integrations/README.md" + }, + "f045": { + "content_hash": "1facc521802b", + "has_branch_placeholder": false, + "name": "requirements.project.txt", + "path": "requirements.project.txt" } }, - "directories": { - "d001": { - "path": ".ai_mail.local", - "name": ".ai_mail.local", - "has_branch_placeholder": false - }, - "d003": { - "path": ".aipass", - "name": ".aipass", - "has_branch_placeholder": false - }, - "d004": { - "path": ".archive", - "name": ".archive", - "has_branch_placeholder": false - }, - "d005": { - "path": ".claude", - "name": ".claude", - "has_branch_placeholder": false - }, - "d006": { - "path": ".seedgo", - "name": ".seedgo", - "has_branch_placeholder": false - }, - "d023": { - "path": ".spawn", - "name": ".spawn", - "has_branch_placeholder": false - }, - "d007": { - "path": ".trinity", - "name": ".trinity", - "has_branch_placeholder": false - }, - "d008": { - "path": "apps", - "name": "apps", - "has_branch_placeholder": false - }, - "d009": { - "path": "apps/handlers", - "name": "handlers", - "has_branch_placeholder": false - }, - "d024": { - "path": "apps/integrations", - "name": "integrations", - "has_branch_placeholder": false - }, - "d010": { - "path": "apps/modules", - "name": "modules", - "has_branch_placeholder": false - }, - "d011": { - "path": "apps/plugins", - "name": "plugins", - "has_branch_placeholder": false - }, - "d012": { - "path": "artifacts", - "name": "artifacts", - "has_branch_placeholder": false - }, - "d013": { - "path": "docs", - "name": "docs", - "has_branch_placeholder": false - }, - "d014": { - "path": "docs.local", - "name": "docs.local", - "has_branch_placeholder": false - }, - "d015": { - "path": "docs.local/sub_agent_drops", - "name": "sub_agent_drops", - "has_branch_placeholder": false - }, - "d016": { - "path": "dropbox", - "name": "dropbox", - "has_branch_placeholder": false - }, - "d017": { - "path": "logs", - "name": "logs", - "has_branch_placeholder": false - }, - "d018": { - "path": "templates", - "name": "templates", - "has_branch_placeholder": false - }, - "d019": { - "path": "tests", - "name": "tests", - "has_branch_placeholder": false - }, - "d020": { - "path": "tools", - "name": "tools", - "has_branch_placeholder": false - }, - "d021": { - "path": "{{BRANCH}}_json", - "name": "{{BRANCH}}_json", - "has_branch_placeholder": true - }, - "d022": { - "path": "{{BRANCH}}_json/custom_config", - "name": "custom_config", - "has_branch_placeholder": false - } + "metadata": { + "description": "Template file tracking registry for ID-based updates", + "last_updated": "2026-06-11", + "version": "1.0.0" } }