Skip to content

fix(votes): keep votes with the scope they were cast in (#787) - #793

Merged
jeff-r2026 merged 8 commits into
Tencent:mainfrom
SaulMoro:fix/787-votes-per-scope
Sep 24, 2026
Merged

jeff-r2026 merged 8 commits into
Tencent:mainfrom
SaulMoro:fix/787-votes-per-scope

Conversation

@SaulMoro

@SaulMoro SaulMoro commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #787
Part of #752

Summary

 vote recorded (Stop hook, adoption judge, recall search, recall feedback)
-  → ~/.teamai/votes/<user>.yaml                every scope
-  → pushed by whichever scope syncs next
+  → getVotesDir(config)
+       project: <dataHome>/votes/<user>.yaml
+       user:    ~/.teamai/user-votes/<user>.yaml
+  → pushed only by that scope (Stop hook, pull report)
 votes-sync (Stop)        getUserVotesDir()        → getVotesDir(dispatcher's config)
+votes-judge (Stop, #723) getUserVotesDir()        → getVotesDir(dispatcher's config)
 reportUsageToTeam        getUserVotesDir()        → getVotesDir(selfConfig)
 autoUpvote (recall)      ~/.teamai/votes          → getVotesDir(scope searched)
 recall feedback          autoDetectInit()         → resolveConfigForDir(); broken config → exit 1
+recall feedback --negative  local file only       → local file, else this scope's team file + pending deltas
+recall search            (broken project config)  → no recalled count recorded (which scope it searches: #796)
 viz (vote view)          detect ?? loadLocal      → resolveConfigForDir(); broken config → error

getVotesDir follows the same rule as getUsagePath after #758. A historical project-scoped ~/.teamai/config.yaml with no projectRoot lives in ~/.teamai, as recall and viz already treat it, so its votes go to user-votes/.

Shared directory: never read. In V2, ~/.teamai/votes holds the last merged snapshot of whichever team synced. --negative instead takes its baseline from the same scope's own team file (<reports checkout>/votes/<user>.yaml), because a scope's local file starts empty on upgrade. It takes max(local count, team count + pending delta), so the first downvote after the upgrade lowers the team's count.

Evidence

Real CLI on head 6e0c92c (dist/index.js) against a build of origin/main a84bf6f: 4 agents × 3 providers, 24 runs, all green. The agent axis goes through hook-dispatch stop --tool <agent> payloads in a sandbox HOME, the provider axis through each team's teamai.yaml provider. One member (tester) is in the user scope and in project A. Team A's reports branch already holds doc-old with 2 upvotes from before the upgrade.

Steps: a pending vote left in ~/.teamai/votes by an earlier release → recall feedback in A → Stop in the user scope (doc-u) → Stop in A (doc-a; the transcript recalls the doc and Reads its file, as #723 requires) → recall feedback in the user scope → pull in each scope → recall feedback --negative doc-old in A → pull in A.

agent provider build user-team receives team-a receives --negative doc-old in A
claude, codex, codebuddy git, gitlab, github origin/main doc-legacy doc-a-feedback doc-u doc-old=2 doc-a doc-u-feedback Document not found, stays 2
claude, codex, codebuddy git, gitlab, github 6e0c92c doc-u doc-u-feedback doc-old=2 doc-a-feedback doc-a Negative signal recorded, 2 → 1
opencode git, gitlab, github origin/main (none) doc-old=2 doc-legacy doc-a-feedback doc-u-feedback Document not found, stays 2
opencode git, gitlab, github 6e0c92c doc-u-feedback doc-old=2 doc-a-feedback Negative signal recorded, 2 → 1

OpenCode's plugin sends only { cwd } on session.idle (opencode-hooks.ts), so its Stop records no vote on either build. The adoption judge needs a local agent CLI, so it is covered by a test (below), not by the real-CLI run.

Earlier heads, unchanged by later commits (none touches the Stop sync, the report push or config resolution):

  • Rollback leg (597a84f, Claude × 3 providers): after rolling back to the earlier release with feedback in both scopes and re-upgrading, each team gains only its own new vote. Pending entries in the shared file are not pushed by this release.
  • Broken project config (7a1ca7a): recall feedback exits 1 with No feedback recorded: <config path>: <parse error>. Fix the file, … and writes nothing; recall records no count; an empty or invalid ~/.teamai/config.yaml is named (parse error printed once); a recall from a deleted cwd records in user-votes/.

Tests, src/__tests__/votes-scope.test.ts (real dispatcher, report, recall feedback, viz). New on 6e0c92c, both red on its parent:

--negative lowers upvotes the scope's team held before the upgrade   parent: team-a { doc-old: 2, doc-recalled: 2 }
--negative ignores upvotes only another scope / the shared dir hold   guard
adoption judge upvote in A reaches only A's team                      parent: judge pushed user-votes' doc-u to team-a

Test plan

On head 6e0c92c (branch = merge of origin/main 8cee7ab + fix, merged rather than rebased to avoid a force-push):

  • npm run build
  • Real CLI, Claude / Codex / CodeBuddy / OpenCode × git / gitlab / github (table above; 12 after-runs on 6e0c92c, 12 baseline runs)
  • npx vitest run: 4646 passed; 11 failed, all in stats-scope.test.ts
  • npx tsc --noEmit: fails only in src/stats.ts:250/251/347/370
  • npm run test:e2e: 237 passed, 26 skipped

Red checks come from main, not this PR. origin/main has been broken on its own since 352cfc4 (#795 × #771: loadAllReportedEvents now takes a config and returns a Promise). A clean archive of 8cee7ab gives the same tsc errors and the same 11 stats-scope failures. A separate hotfix PR fixes it. This PR does not touch stats.ts. On a84bf6f, before that break, the same fix passed all four checks: tsc ok, vitest 4637 passed, test:e2e 237 passed, build ok.

Merge Danger

Door: two-way

Rolling back is safe: the earlier release reads ~/.teamai/votes again. While it runs, its first Stop or pull pushes whatever is pending there, including votes left from before this upgrade, to the team of that scope. This release cannot prevent that. What the earlier release wrote is never pushed after re-upgrading.

Blast Radius: local-votes

  • Votes pending in ~/.teamai/votes at upgrade are never pushed (the intent).
  • recall feedback --negative reads the scope's reports checkout. If that checkout has not been created yet (no pull or push since setup), a doc that only the team holds still warns "not found".
  • In a project with an unreadable config, recall feedback now exits 1, a recall search records no recalled count (it still searches the fallback scope; recall searches nothing in a project whose config cannot be read #796 changes that), and the KB report route returns its error message instead of showing the user scope's data.
  • With no scope set up, the local vote view reads ~/.teamai/user-votes/, so it no longer shows history left in ~/.teamai/votes/.
  • A single-repo install still on the legacy <repo>/.teamai/config.yaml records into <repo>/.teamai/votes/, which self migration does not move (as with usage.jsonl after fix(usage): discard pre-upgrade usage and stop falling back past an unreadable config (#748) #758). Deltas still pending at migration stay there and are not pushed. They are gitignored and not leaked.
  • The remote votes/<user>.yaml format is unchanged.

Every scope recorded into one ~/.teamai/votes/<user>.yaml, so a vote cast in
one project (recall feedback, a recall search, a Stop whose push failed) was
pushed to the team of whichever scope synced next: the leak usage.jsonl had
before Tencent#758.

Votes now live in the data home of the scope that resolves for the session:
<dataHome>/votes/ for a project, ~/.teamai/user-votes/ for the user scope.
The Stop hook uses the config the dispatcher resolved; the pull report, recall
search, `recall feedback` and the vote view read only that scope's votes, and
the CLI readers resolve it with resolveConfigForDir, so an unreadable project
config falls back to no other scope: `recall feedback` exits 1 and the vote
view names the broken file.

The shared ~/.teamai/votes/ is never read. Its V2 `votes` map is the last
merged remote snapshot of whichever team synced, not this scope's history, and
seeding a scope from it would let `recall feedback --negative` push a
decrement and merged timestamps derived from another team. The remote
votes/<user>.yaml format is unchanged.
@jeff-r2026 jeff-r2026 self-assigned this Sep 24, 2026
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] PR description — Test plan: Required real-CLI coverage is incomplete. The repository requires validation with Claude, Codex, CodeBuddy, and OpenCode across git, gitlab, and github; the PR explicitly reports only Claude. Agent-independent implementation is not an exemption from the mandated test matrix. Add actual passing records for the three omitted agents before merge.

No additional code defects found in the reviewed diff. No earlier findings were supplied to mark resolved.

- recall search: in a project whose config cannot be read, detection falls
  back to another scope; record no recalled count there, so the vote cannot
  reach that scope's team. Which scope the search itself uses stays Tencent#796's.
- recall feedback: with no project config and an empty or invalid user
  config, name the file and the fix (requireInit's error) instead of
  "not set up here".
- CHANGELOG: note the recall search case; the shared directory is never
  read or pushed "by this release" (an earlier release still pushes it).
- Design doc: getUserVotesDir() is the exception to "getters unchanged".
- recall feedback: name an unusable user config through
  throwMissingOrInvalid (now exported) instead of re-running requireInit,
  which loaded the config twice and printed its parse error twice.
- recall search: a project detection that throws is treated like an
  unreadable config, so no recalled count lands in the fallback scope.
- The recall-search test now asserts the search ran and that neither the
  shared directory nor the broken project's votes/ was written; it fails on
  origin/main too.
- Design doc: re-wrap the edited paragraph.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/types.ts:1683 — Preserve supported configs without projectRoot. getVotesDir() unconditionally calls getDataHome(), which throws for a schema-valid historical scope: project config lacking projectRoot. Recall searches, feedback, and Stop hooks then fail to record votes; handle this shape before calling getDataHome, as recall.ts and viz.ts already do.
  • [P1 blocking] docs/designs/data-directory-layout.md:316 — Update every affected design document. docs/designs/git-native-memory.md:42 still documents local votes at ~/.teamai/votes/<user>.yaml. The trusted repository rules explicitly require updating all affected docs, including docs/designs/, and grepping old wording.
  • [P1 blocking] PR description — Complete the required real-CLI test matrix. The earlier finding remains unresolved: the test plan explicitly covers only Claude and omits Codex, CodeBuddy, and OpenCode. Repository policy requires real-CLI verification with all four agents across git, gitlab, and github.

…encent#787)

The previous commit treated any throw from recall's project detection as an
unreadable project, including a cwd that no longer exists. Such a cwd holds
no project and resolves to the user scope everywhere else
(resolveConfigForDir, detectTeam), so its recalled counts belong there.

Tests pin that case and the single parse-error line for an invalid user
config in recall feedback.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/types.ts:1683 — Preserve supported configs without projectRoot. LocalConfigSchema still accepts historical scope: project user configs without projectRoot, but getVotesDir() unconditionally calls getDataHome(), which throws. Recall voting, feedback, Stop hooks, and pull reporting therefore fail for this supported configuration. The earlier finding remains unresolved.
  • [P1 blocking] src/votes.ts:233 — Existing votes cannot receive negative feedback after upgrade. The new per-scope file starts empty, while remote counts are copied locally only when another delta is successfully pushed. Consequently, recall feedback --negative reports “not found” or “no upvotes” for previously upvoted documents—potentially indefinitely. Hydrate the scope-local baseline from that scope’s remote report before validating the decrement.
  • [P1 blocking] docs/designs/git-native-memory.md:42 — Update the affected design document. It still documents local votes at ~/.teamai/votes/<user>.yaml, contradicting the new project-specific and ~/.teamai/user-votes/ paths. The earlier documentation finding remains unresolved.
  • [P1 blocking] PR description — Complete the required real-CLI test matrix. The test plan still covers only Claude and explicitly omits Codex, CodeBuddy, and OpenCode. Repository policy requires real-CLI verification with all four agents across git, gitlab, and github; agent-independent implementation is not an exemption. The earlier testing finding remains unresolved.

No earlier findings were resolved in the current diff.

- getVotesDir: a historical project-scoped ~/.teamai/config.yaml with no
  projectRoot (schema-valid, not backfilled) made getDataHome throw, so
  recall, feedback and the Stop hook recorded no vote. It lives in
  ~/.teamai, as recall and viz already treat it, so its votes go to the
  user scope's user-votes/.
- git-native-memory design doc: the local votes path is user-votes/.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/votes.ts:233 — Existing votes still cannot receive negative feedback after upgrade. Each new scope-local file starts empty, so recall feedback --negative <doc> rejects documents already upvoted in the scope’s remote report until an unrelated local delta happens to trigger synchronization. Load the same scope’s remote baseline before validating/decrementing.
  • [P1 blocking] PR description — Complete the required real-CLI test matrix. The evidence covers only Claude and explicitly omits Codex, CodeBuddy, and OpenCode. Repository policy requires real-CLI verification for all four agents across git, gitlab, and github.

Resolved

  • The historical scope: project configuration without projectRoot is now handled by getVotesDir().
  • docs/designs/git-native-memory.md now documents user-votes/.

Note

  • The reported tests target commits 70e4599 and 597a84f, not current head 7a1ca7a; per review policy, this is noted but is not independently blocking.

@jeff-r2026 jeff-r2026 assigned jeff-r2026 and unassigned jeff-r2026 Sep 24, 2026
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/votes.ts:233 — Existing upvotes cannot be negatively rated after upgrade. If the scope’s remote report already contains an upvote but the new scope-local file does not yet exist, recall feedback --negative <doc> reports “Document not found,” writes no delta, and leaves the remote count unchanged. Accept a negative delta against the scope’s remote baseline or otherwise hydrate that baseline before validation.

Resolved

  • Historical project configs without projectRoot now use user-votes/.
  • docs/designs/git-native-memory.md now reflects the new path.
  • The PR description now provides real-CLI evidence for all four agents and three providers at head 7a1ca7a; testing documentation is sufficient.

# Conflicts:
#	docs/designs/data-directory-layout.md
#	src/team-push.ts
#	src/votes.ts
- recall feedback --negative counts the upvotes the scope's own team
  already holds (its reports checkout's votes/<user>.yaml plus the
  deltas not yet pushed). A scope's file starts empty on upgrade, so a
  doc upvoted before it was rejected as not found, or as having no
  upvotes once a later recall counted it. The shared ~/.teamai/votes and
  other scopes' teams are never read.
- The adoption judge (Tencent#723, merged meanwhile) recorded into and synced
  from the user scope's votes in every scope, which pushed the user
  scope's pending votes to the project's team. It uses the scope's votes
  like the Stop handler.
- votes-scope tests: Stop transcripts prove adoption with a Read of the
  recalled file (Tencent#723), and the update.js mock keeps the real lock.
@github-actions

Copy link
Copy Markdown

Review Result

  • No findings in the current diff.
  • The previously reported scope-path, legacy-config, documentation, and pre-upgrade negative-feedback issues are resolved.
  • The PR description now documents sufficient real-CLI testing across all four agents and three providers.

@github-actions

Copy link
Copy Markdown

Review Result

  • No findings in the current diff.
  • Previously reported scope-path, legacy-config, documentation, negative-feedback, and testing issues are resolved.
  • The PR description provides sufficient real-CLI verification. The evidence references 6e0c92c rather than merge head d61df43; per policy, this is non-blocking.
  • Review was static only; no PR code was executed.

@jeff-r2026
jeff-r2026 merged commit dc233e4 into Tencent:main Sep 24, 2026
11 checks passed
@SaulMoro
SaulMoro deleted the fix/787-votes-per-scope branch September 24, 2026 12:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Votes stay with the scope they were cast in

2 participants