---
slug: p-branches-not-pushed-origin
status: root-cause-found
trigger: "p-<project-name> branches of spt-agent-storage are not getting pushed. there is no content at origin"
created: 2026-05-28
updated: 2026-05-28
---

# Debug Session: p-branches-not-pushed-origin

## Symptoms

- **Expected:** `p-<project-name>` branches of `spt-agent-storage` repo should hold project-context commits (commune/signoff `<agent>.md` files) and be pushed to origin on every project commit. Cross-machine sync depends on origin having that content.
- **Actual (refined):** Origin DOES have `refs/heads/p-claude_skill_owl`, but it is stuck at the initial seed commit (`07f6367 init: tracked seed`) — identical SHA to `main`. No project-context commits have ever landed on it. The user perceived this as "no content" because the branch carries no project-specific commits.
- **Errors:** Silent failure path. A `commit_project_payload` error is swallowed by `route_two_slice` with a warning containing `(payload on disk)`. The .md file persists; the git worktree is never created; no commit fires; no push fires.
- **Timeline:** Never worked since project worktrees were introduced.
- **Reproduction:** Spawn an agent in a project, send a commune that includes `<project-context>...</project-context>`. Inspect `psyches/tracked/projects/<project>/` — files appear, but `.git` is absent. `git -C psyches/tracked/seed log p-<project>` shows only the init commit.

## Context

- Sync mechanism: `accept_flow` in `src/common/sync.rs` (lines 482-600). Single `git push --all origin` from seed at Step 5. Pushes every branch that exists in seed at that moment.
- Project worktree creation: `ensure_project_worktree` → `ensure_worktree(EnsureScope::Project, name)` in `src/common/tracked.rs:335-475`.
- Project commit/sync entry: `commit_project_payload` in `src/common/tracked.rs:1262-1282`. Calls `ensure_project_worktree` then `commit_payload` then `sync::sync_after_commit`.
- Project content write path: `route_two_slice` in `src/owl/echo_commune.rs:780-822`.
- On-disk state probed (`$LOCALAPPDATA/spt`):
  - `settings.json` shows `sync.state=enabled`, `remote_url=https://github.com/SaberMage/spt-agent-storage.git`.
  - `psyches/tracked/seed/` is a real bare repo with branches `main`, `a-doyle`, ..., `p-claude_skill_owl` (all of them present on origin too).
  - `psyches/tracked/projects/claude_skill_owl/` exists and contains `doyle.md`, `ling.md`, `probetest.md`, `todlando.md` — but has NO `.git` entry (not a git worktree).
  - `seed/worktrees/` admin dir has subdirs for every agent but ZERO subdirs for projects.
  - `p-claude_skill_owl` is at the same SHA as `main` (`07f6367`); its reflog is empty.

## Current Focus

- **hypothesis:** ROOT CAUSE — `route_two_slice` writes the project .md file BEFORE calling `commit_project_payload` (and therefore before `ensure_project_worktree`). The write pre-populates the project directory. When `ensure_worktree` then runs `git worktree add ../projects/{name} -b p-{name} main`, git refuses because the target path is non-empty. The fallback `git worktree add ../projects/{name} p-{name}` ALSO refuses for the same reason. `ensure_worktree` returns `Err(WorktreeFailed)`. `commit_project_payload` returns the error. `route_two_slice` swallows it with a `(payload on disk)` warning. Result: the directory is permanently in a non-worktree state — every subsequent commune repeats the same failure, and the project branch never advances past the seed-init commit.
- **test:** Reproduced in a scratch repo. Sequence `worktree add ../projects/foo -b p-foo main` → fatal: branch exists. Fallback `worktree add ../projects/foo p-foo` → fatal: path exists. Both error stderrs trigger the existing fallback gate (`s.contains("already exists")`) but the fallback ALSO fails, and the final error propagates.
- **expecting:** —
- **next_action:** Apply fix (see Resolution).
- **reasoning_checkpoint:** Confirmed independently of the user's premise. User said "no content at origin" — actually origin DOES carry the branch ref, but it points to the init commit, so semantically there is no project content. Bug remains real and severe.

## Evidence

- timestamp: 2026-05-28 (inline investigation)
  - file: `src/owl/echo_commune.rs:792-820`
  - finding: route_two_slice runs `std::fs::create_dir_all(&proj_dir)` then `std::fs::write(&proj_path, project_body)` BEFORE `tracked::commit_project_payload(...)`. The error from `commit_project_payload` is logged but swallowed (`(payload on disk)`).
- timestamp: 2026-05-28
  - file: `src/common/tracked.rs:335-474`
  - finding: `ensure_worktree` fast-path returns Ok when `.git` exists; otherwise runs `worktree add` which requires the target path to be empty. Fallback also runs `worktree add` against the same path. Both fail with `'... already exists'` stderr when the dir is non-empty.
- timestamp: 2026-05-28
  - command: `git -C psyches/tracked/seed worktree list --porcelain`
  - finding: 11 agent worktrees listed; ZERO project worktrees. Confirms no `projects/claude_skill_owl/` worktree was ever materialized.
- timestamp: 2026-05-28
  - command: `git -C psyches/tracked/seed log --oneline p-claude_skill_owl`
  - finding: `07f6367 init: tracked seed` — only the seed init commit. No project commits have ever landed.
- timestamp: 2026-05-28
  - command: `gh api repos/SaberMage/spt-agent-storage/branches --jq '.[].name'`
  - finding: origin lists `p-claude_skill_owl` (refuting the user's stated premise that p-* branches are absent), but at the seed-init SHA (matching local).
- timestamp: 2026-05-28
  - command: scratch-repo reproduction of `worktree add` against non-empty dir
  - finding: `fatal: '../projects/foo' already exists`. Exit 128. No `.git` file written.

## Eliminated

- `accept_flow` push filter — Not the bug. `git push --all origin` from seed DOES push every local branch including `p-*`. Origin received the ref at seed-init time.
- `enumerate_existing_worktrees` skipping project dirs — Tangential. It only matters during `accept_flow` Step 4 (`remote add origin` per worktree). Because no project worktree exists, that step skips them; but origin is already wired at the seed level, and that's the level that ultimately matters for pushing. The real bug prevents the project worktree from existing in the first place.
- The recent `accept_flow` idempotency fixes (b7668dd / e4ceca4 / d49ae9d) — orthogonal to this bug. They cover repo-create idempotency and stale-origin correction.

## Resolution

- root_cause: `route_two_slice` (`src/owl/echo_commune.rs:780-822`) writes the project `.md` file into `psyches/tracked/projects/<project>/` BEFORE calling `commit_project_payload`. This pre-population causes the subsequent `git worktree add` (in `ensure_worktree`, `src/common/tracked.rs:418-468`) to fail with `'...' already exists` on BOTH the primary and fallback attempts. The worktree never materializes, the commit never fires, and the project branch stays pinned at the seed-init SHA. The error is swallowed by route_two_slice with a `(payload on disk)` warning so the failure is silent in normal operation.

- fix (recommended): Reverse the order in `route_two_slice` — call `ensure_project_worktree(project_name)` FIRST so the worktree exists and `.git` is set up, then write the .md file INTO the now-real worktree, then call `commit_project_payload`. Concretely, change `src/owl/echo_commune.rs:790-820` to:
  ```rust
  // Ensure the worktree exists FIRST so its .git is in place; only then
  // write the file and request a commit. Writing before ensure leaves the
  // dir non-empty and breaks `git worktree add` permanently.
  let proj_dir = match tracked::ensure_project_worktree(project_name) {
      Ok(p) => p,
      Err(e) => {
          eprintln!(
              "route_two_slice: ensure_project_worktree({}) failed: {} (skipping project slot)",
              project_name, e
          );
          return Ok(());
      }
  };
  let proj_file_name = format!("{}.md", self_id);
  let proj_path = proj_dir.join(&proj_file_name);
  if let Err(e) = std::fs::write(&proj_path, project_body) {
      eprintln!(
          "route_two_slice: write project file {} failed: {} (continuing)",
          proj_path.display(), e
      );
      return Ok(());
  }
  let subject = tracked::compose_commit_subject(kind, self_id, "project_context", short_for_subject);
  if let Err(e) = tracked::commit_project_payload(project_name, &[proj_file_name.as_str()], &subject) {
      eprintln!(
          "route_two_slice: commit_project_payload for {}/{} failed: {} (payload on disk)",
          project_name, self_id, e
      );
  }
  Ok(())
  ```
  Apply the SAME reorder at the second project-write site in `src/owl/echo_commune.rs:501` and `:588`, and the analogous sites in `src/live/signoff.rs:789/824/863/904` and `src/live/context.rs:470/2338/2401/2517` (the grep found nine call sites — every one that does `let proj_dir = owlery::project_worktree_path(...)` followed by `fs::write` followed by a commit needs the reorder).

- fix (additional hardening — defense in depth): in `tracked.rs::ensure_worktree`, when both the primary and fallback `worktree add` attempts fail with `already exists` AND the target dir lacks a `.git` marker, perform a controlled recovery: move the existing content aside to `<wt>.salvage-<ts>/`, retry `worktree add`, then restore the salvaged files. This recovers existing broken installations without operator action. Lower priority — the reorder above prevents new occurrences.

- fix (one-time repair for already-broken installs): document a manual recovery step in the CHANGELOG / sync setup skill: `mv psyches/tracked/projects/<name> psyches/tracked/projects/<name>.bak`, run any commune (which now succeeds and creates a fresh worktree), then `cp` the saved .md files from the `.bak` directory into the real worktree and trigger another commune/signoff so they get committed and pushed. (Or have a `doctor --repair` flag do this.)

- verification:
  1. cargo test → all sync.rs + tracked.rs + echo_commune.rs tests still pass.
  2. Add a regression test in `route_two_slice`'s test module that asserts the project worktree's `.git` file exists AFTER the call (proves the order is correct).
  3. Add a regression test in `tracked.rs` that pre-populates `projects/foo/` with a stray file and proves `ensure_project_worktree("foo")` either succeeds (via salvage) or returns an error that does NOT leave the directory in the broken state.
  4. Manual: delete `psyches/tracked/projects/claude_skill_owl/` locally, run a commune with `<project-context>` content, then `git -C psyches/tracked/seed log p-claude_skill_owl` must show the new commit, and `git -C psyches/tracked/seed push origin p-claude_skill_owl` (or the post-commit sync) must succeed.

- files_changed: (proposed)
  - `src/owl/echo_commune.rs` — reorder all three project-write call sites
  - `src/live/signoff.rs` — reorder analogous project-write sites
  - `src/live/context.rs` — reorder analogous project-write sites (if any actually write before ensure)
  - `src/common/tracked.rs` — (optional defense-in-depth) add salvage path in `ensure_worktree`
  - tests in the same files + possibly a top-level integration test
