Skip to content

[bug] stats, dashboard, session save and wiki slugs split one repo into one project per worktree #809

Closed

Description

@SaulMoro

Description

teamai stats --by-repo, teamai dashboard, session save and the codebase wiki commands treat every git worktree as its own project. Sessions from two worktrees of teamai-cli appear as two extra rows:

By Repo (local event log):

  teamai-cli        8 sess, 148 turns, 773 tools, 228.3M tok, 4 intv
  fix-760-lock      1 sess, 27 turns, 375 tools, 79.6M tok, 1 intv
  fix-748-followup  2 sess, 12 turns, 89 tools, 4.8M tok

Both extra rows are worktrees under .claude/worktrees/. Worktrees outside the checkout, such as Orca workspaces or git worktree add ../x, split the same way. The expected output has a single teamai-cli row.

Cause. Each event records its cwd but no project identity, and the wiki commands only look at the directory they run in. Every consumer derives the project from a per-worktree path:

Where How it derives the project Result
attributeRepo (src/utils/repo-attribution.ts:47), used by stats --by-repo takes the basename one row per worktree
dashboardWorkspaces and workspaceEvents (src/dashboard/workspaces.ts:57,76) calls resolveAnchors(cwd), then matches the path against [root, ...listWorktrees(root)] a deleted worktree matches neither, and its sessions move to "Unassigned sessions"
"Repository" filter (src/dashboard/client.ts:37-39) lists each distinct cwd one option per worktree
renderSessionMarkdown (src/session-collector.ts:80,172) prints the last cwd as Project: one repo appears as several projects in the team session log
import --dir (src/import.ts:325) and codebase --extract (src/codebase-extract.ts:539) take the basename as the project slug from a worktree, import --dir pushes teamwiki/evidence/code/<worktree>/ to the team repo next to the repo's own entry

Resolving the path at read time can't fix this. Worktrees are short-lived, and after one is removed git can no longer map its path to the main checkout. fix-760-lock above is no longer a registered worktree, and git worktree list omits it.

The hook already knows the answer when it writes the event, and discards it:

dashboardReport                       src/dashboard-collector.ts
  resolveConfigForDir(cwd)            src/config.ts
    detectProjectConfig(cwd)
      resolveAnchors(cwd)             -> { workspaceRoot, projectAnchor }, used for the config, then dropped
  parseHookEvent
  event.dataHome = getDataHome(config)
  appendEvent(event)

Proposed fix

All eight changes fit in one PR.

1. Record the project when the event is written. dashboardReport stores the main checkout, which all worktrees of a repo share, on every event:

 dashboardReport
   resolveConfigForDir(cwd)
     detectProjectConfig(cwd)
       resolveAnchors(cwd)
   parseHookEvent
   event.dataHome = getDataHome(config)
+  event.projectAnchor = (await resolveAnchors(cwd))?.projectAnchor
   appendEvent(event)

The hook writes it on every event, not only on session_start, because EnterWorktree or a cd changes the cwd in the middle of a session. The hook omits the field when cwd is outside a git repo. User-scope sessions get the field too, since a worktree splits the same way there.

2. Keep the hook's git calls at their current count. resolveAnchors memoizes each directory's result for the lifetime of the process, so the second call in one hook invocation runs no git command. It does not memoize null, so a directory that becomes a repo later in the same process still resolves.

3. Add one attribution helper to src/utils/repo-attribution.ts, and use it in both stats and the dashboard:

repoKey(session)
  return last projectAnchor seen in the session's events
      ?? last cwd                        # events from before this change, non-git dirs
                                         # events after the worktree is deleted have no anchor,
                                         # so the session keeps the one it had

repoLabel(key, allKeys)
  name = basename(key)
  if another key in allKeys has the same basename
    return basename(dirname(key)) + "/" + name     # work/api, personal/api
  return name

4. stats --by-repo. attributeByRepo groups sessions by repoKey and prints repoLabel.

5. Dashboard ownership. workspaceEvents matches on the anchor first:

 owner(event)
+  if event.projectAnchor
+    return the project whose root === event.projectAnchor
   return the project whose roots contain event.cwd (longest match)

Discovery (workspaces.ts:57) adds the project from event.projectAnchor and calls resolveAnchors only for events that lack it.

6. Dashboard "Repository" filter. Options and filtering use repoKey and repoLabel instead of the raw cwd. Session rows keep showing the cwd.

7. session save. Project: shows repoLabel of the session's repoKey. The cwd moves to its own Directory: line, so the log keeps the path.

8. Wiki slug. When --project is not given, import --dir and codebase --extract use basename(resolveAnchors(dir).projectAnchor), falling back to basename(dir) outside git. The main checkout keeps the slug it has today.

Not in scope

  • Events written before this change still split by worktree until they age out of events.jsonl. Nothing backfills them.
  • The legacy branch of filterEventsByScope (src/team-push.ts:415) still drops old events from worktrees outside projectRoot. New events carry dataHome and reach the right scope, so this gap also closes as old events age out.
  • Wiki entries already pushed under a worktree slug stay in the team repo. Remove them by hand.
  • Events whose cwd was deleted before the hook fired are still resolved to the user scope. That is [bug] hook events after a worktree is deleted fall back to the user scope #810. This fix only keeps them attributed to the session's repo in --by-repo.
  • There is no per-worktree breakdown. If someone needs one later, it belongs in a separate filter or flag.

Docs

  • DashboardEvent gains an optional projectAnchor. Document it next to dataHome in docs/designs/data-directory-layout.md, around line 323. That file has no zh-CN copy.
  • The --project help on codebase (src/index.ts:1119) says "defaults to directory name". Change it to the main checkout's directory name, then regenerate skill-data/core/references/commands.md with npx vitest run commands-reference -u.
  • The data-directory-layout.md paragraph also says stats --by-repo reads events from every scope. Since fix(report): each scope keeps its own reported dashboard snapshots (#786) #795, showStats passes only the current scope's events (src/stats.ts:266). Fix that sentence in the same PR.

Reproduction

  1. In a teamai-enabled repo, run git worktree add ../wt-demo -b wt-demo.
  2. Start an agent session in ../wt-demo and run a few tool calls.
  3. Run teamai stats --by-repo. It shows a wt-demo row next to the repo's row.
  4. Run git worktree remove ../wt-demo, then teamai dashboard. The session appears under "Unassigned sessions". This step comes from reading workspaces.ts and has not been run yet.

Acceptance

After step 3:

 By Repo (local event log):

-  my-repo  3 sess, ...
-  wt-demo  1 sess, ...
+  my-repo  4 sess, ...
  • After step 4, the dashboard lists the session under the repo's workspace, and the "Repository" filter shows the repo once.
  • Two unrelated repos that share a directory name appear as two rows with distinct labels.
  • A session that starts in ~ and then runs cd into the repo counts toward the repo.
  • session save from a worktree session prints the repo as Project:.
  • teamai import --dir . --dry-run in wt-demo prints (project: my-repo).

Environment

  • OS: macOS (Darwin 25.5.0)
  • teamai: 0.22.0
  • Provider: GitHub
  • AI tool(s): Claude Code, with worktrees from Orca and EnterWorktree

Related: #752, which covers the cross-project gaps left after #748. Its item 1 proposed the dataHome field on events, which #785 implemented.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions