fix(reports): keep teamai-reports reads fresh and read-only - #560
Conversation
Tencent#489 moved independent git clones onto the teamai-reports worktree but kept the single-repo reader/writer patterns, which regressed git-kind clones: read-only commands published a missing teamai-reports branch (Tencent#558), pull indexed votes and read stats from a stale reports checkout (Tencent#557), and writers merged per-member files into that stale copy, so a second machine of the same member stayed diverged and its reports never reached origin. - Read-only callers (members, digest, projects members, stats, viz, the vote read in contribute, pull's votes/stats reads) pass pushIfCreated: false. - A cold start reuses an unpublished local teamai-reports branch instead of failing with "a branch named 'teamai-reports' already exists". - refreshReportsWorktree syncs under the reports lock: offline keeps the local copy, unpushed report commits are rebased onto origin and dropped only when they conflict, and a busy lock reads the local copy. - pull refreshes the reports worktree once per scope before the search index and skill recommendations. - updateReports() replaces ensure + write + commitAndPushReports: it syncs, runs the merge/write, commits and pushes under one lock. Pull auto-report, Stop-hook votes, session save --push, init and bootstrap member registration use it; auto-report and the Stop hook skip the round-trip when nothing is pending. Closes Tencent#557 Closes Tencent#558 Claude-Session: https://claude.ai/code/session_01TFtw1GRR7pJNTGzNCikoKe
…iter changes to Tencent#561 Keep this PR to what the two issues ask for: read-only callers never publish teamai-reports, cold start reuses an unpublished local branch, and pull refreshes the reports worktree (under the reports lock) before reading votes and stats. Pull's auto-report runs after that refresh, so its stats merge also starts from fresh data. The writer-side refactor (updateReports replacing commitAndPushReports in init, bootstrap, session save, the Stop hook and auto-report) is reverted here and proposed separately in Tencent#561. Because writers still write before taking the lock, the refresh no longer discards uncommitted report files: it fast-forwards, or rebases unpushed commits with --autostash, and only resets a clean worktree whose unpushed commits conflict with origin. Claude-Session: https://claude.ai/code/session_01TFtw1GRR7pJNTGzNCikoKe
|
Found a locally reproducible issue that I recommend fixing before merging. [P1] Handle autostash conflicts after a successful rebaseIn Reproduced against PR head
The command exits successfully (
Suggestion: explicitly handle autostash restoration conflicts rather than treating a successful rebase as proof that the worktree is safe. Check the post-rebase index/worktree state and provide a recovery path that preserves the original uncommitted report content without leaving conflict markers. A conservative fallback is to skip rebasing a dirty checkout and keep its local copy until it can be refreshed safely. Please add a real-git regression covering the combination of an unpushed commit, a dirty tracked report, and an upstream change to that same report. Validation: |
git rebase --autostash can exit 0 when the rebase itself succeeds but reapplying the autostash conflicts. That left UU paths, conflict markers in report YAML, and a leftover autostash stash, so a read-only command could serve invalid stats. After rebase/merge, inspect the worktree. If the rebase has finished and stash-apply conflicts remain, restore the original uncommitted content (the stash --theirs side) and drop the autostash instead of treating exit 0 as proof that the checkout is clean.
|
Addressed the P1 autostash leftover in
Added a real-git regression in Validation:
|
|
Need to resolve conflicts |
Resolve the conflict with Tencent#562 in reports-branch-readonly tests by using path.join for the reports worktree path. Keep Tencent#560's read-only refresh and autostash recovery together with Tencent#562's pushIfUnchanged retry and pull report-timeout bookkeeping. The late-report isolation test now only asserts that partition sync-locks stay held; .reports-lock is released after the earlier read-only refresh.
|
Merged current The conflict was with #562 (
The late-report isolation test now only asserts that partition Validation after the merge:
|
|
Local review of [P1] Reports refresh deletes unrelated business-worktree stashesIn Git worktrees share Reproduced locally with the built CLI, using an isolated
The stash reference is silently removed; its objects may remain recoverable until Git garbage collection. Suggested fix: identify the specific stash object created by the current refresh and clean up only that object after restoring its contents successfully. Do not select stash entries by message text. Please add a real-Git self-mode regression test that preserves a pre-existing business-worktree stash. Validation on PR head: |
Git worktrees share refs/stash. dropRebaseAutostash scanned the whole stash list and dropped every entry whose message contained autostash, so a self-mode members refresh could delete a business-tree stash created with `git stash push -m autostash`. Snapshot stash SHAs before rebase --autostash and drop only objects created by that refresh. Do not select stashes by message text.
|
Addressed the P1 shared-stash deletion in
The refresh now snapshots stash SHAs before Added a real-git self-mode regression: unpushed report commit + dirty Validation:
|
|
Rechecked [P1] A concurrently created business stash is mistaken for the reports autostash
The reports lock does not protect ordinary Git commands in the business worktree. This no longer requires a stash message containing Deterministic reproduction with the built CLI: Use the previous self-mode setup: an independent unpushed report commit, a dirty tracked report, and a conflicting upstream update. In the isolated test repository, install a temporary
Reproduced twice on Suggested fix: identify the exact autostash belonging to this operation, rather than all SHAs added during a time window. A conservative alternative is to skip rebasing a dirty reports checkout or retain a stash when ownership cannot be proven. Please add a real-Git regression where another worktree creates a stash during refresh. Validation: |
stashShasCreatedSince treated every stash SHA that appeared after the pre-rebase snapshot as this refresh's autostash. Worktrees share refs/stash, and the reports lock does not serialize ordinary git stash in the business tree, so a stash created during rebase was deleted. Stop using rebase --autostash. Snapshot dirty tracked files with git stash create (a dangling commit, not stored in refs/stash), rebase on a clean tree, then stash apply that object. Never drop from the shared stash list.
|
Confirmed the remaining P1 on Addressed in the next commit (appended, no force-push):
Added a real-git self-mode regression with a Validation:
|
Summary
Fixes the two follow-ups to #489 for independent git clones:
teamai-reports.pullrefreshes the reports worktree from origin before it indexes votes and reads stats.A related writer-side problem (writers merge per-member files into a stale checkout) has been split out to #561 to keep this PR small.
Type of Change
What changed
Source: 8 files, +134/−54.
pushIfCreated: false. These aremembers,digest,projects members,stats,viz, the vote read incontribute, and pull's votes/stats reads.teamai-reportsbranch. Previously, removingreports-wtbefore the branch was published made the next cold start fail witha branch named 'teamai-reports' already exists.refreshReportsWorktreenow syncs under the reports lock. Before, it ran an unlockedgit reset --hard origin/teamai-reports.pullrefreshes the reports worktree once per scope, before the search index (votes → hotness) and skill recommendations. The pull auto-report runs after this, so its stats merge also starts from fresh data.docs/usage-guideupdated.Test Plan
npx tsc --noEmit— cleannpx vitest run— 225 files / 3128 tests passednpm run test:e2e— 27 files passed, 3 skipped (124 tests passed, 26 skipped)git-kind-reports.test.ts(real git), cold start:reports-wtis removed, and a writer then publishes itgit-kind-reports.test.ts(real git), refresh:0aheadreports-branch-readonly.test.ts:members.test.ts: listing passespushIfCreated: falsenpm run build, thennode dist/index.js)updatehook rejects pushes tomain; an isolatedHOMEper checkout. Members arealiceandbob, andalicehas two machines (A and B).mainand against this branch.main(c83b9b1)teamai memberson a cold start leaves origin withoutteamai-reports(#558)teamai digest— sameteamai stats— sameteamai projects members demo— sameteamai pullwith nothing to report — samesession save --pushpublishesteamai-reportsvotes/bob.yaml, alicepull --force→guide.mdhotness1(#557)votes/bob.yamlafterpullpullauto-report from A publishesstats/alice.yaml(1 session)pullauto-report from B merges onto A (1 + 2 = 3 sessions)teamai membersstill runssession save --pushB(b0) → A(a1) → B(b1), all three on originalice-a1lostteamai.yamlprovidergit/gitlab/github× config agentclaude/codex/codebuddy/opencode. All 12 combinations passed 12/12 in-scope checks. The out-of-scope writer check behaves as onmainin every combination.git fetch/push, so no hosted GitLab/GitHub API is involved.reports-branch-readonlyuses a self config;self-mode-no-business-reset,single-repo-mode), not by the CLI E2E above.Related Issues
Closes #557
Closes #558
Follow-up to #489. Writer-side follow-up: #561.
Notes for Reviewers
commitAndPushReports.commitAndPushReportsskips as it already does ("another reports write is in progress"). Its written file stays uncommitted in the worktree until that file's next write; the refresh carries it along instead of discarding it.pullon a non-HTTP scope now does one extra fetch ofteamai-reports.updateReportsreplacingcommitAndPushReports). The second commit reverts that part to keep this PR to fix(pull): refresh reports worktree before reading votes and stats #557/fix(members): avoid publishing reports branch from read-only queries #558, and the writer refactor is now proposed in fix(reports): report writers merge into a stale teamai-reports worktree #561. The net diff againstmainis the scope described above.https://claude.ai/code/session_01TFtw1GRR7pJNTGzNCikoKe