Skip to content

fix(telemetry): bind a branch only by the command's own declaration - #1045

Merged
blafourcade merged 13 commits into
nextfrom
fix/declaration-comes-only-from-the-command
Oct 16, 2026
Merged

blafourcade merged 13 commits into
nextfrom
fix/declaration-comes-only-from-the-command

Conversation

@blafourcade

Copy link
Copy Markdown
Contributor

🎯 What & why

A task and a ticket are meant to come only from aidd telemetry task. In practice a branch also got bound by a task key set by hand, or by a declaration that git branch -c/-m carried over from another branch. This happened in both the CLI and the hook. The usage contract also said the prompt gate "asks once per working branch", which is not what it does: it holds every prompt until the branch is declared.

🛠️ How it works

  • Stamp. The command now also writes branch.<name>.aiddDeclaredFor = <name>, just before aiddDeclaredAt, which is still written last (git-branch-binding-store-adapter.ts:26). It replaces every value of a key, because -C/-M onto an existing branch leaves keys listed twice.
  • One rule, two readers. A branch counts as declared only when all three hold: aiddDeclaredAt is set, the stamp (if present and non-empty) names the branch being read, and no declaration key is listed twice. The CLI applies this in branch-binding.ts:69, the hook in git.cjs:88. Both are pinned by the shared fixture scripts/__tests__/fixtures/telemetry-bindings/, which can now list keys in order.
  • Legacy. A declaration written by cli-v5.5.0 has no stamp. It still reads as before, so data already written keeps its meaning. Declaring again stamps it.
  • Rename. git branch -m A B leaves B undeclared, and the gate asks once on B. git moves the keys the same way for a copy and a rename, so the stamp cannot tell the two apart. Nothing is lost: B now starts when it took its name, read from the newest renamed/copied … to refs/heads/B reflog entry (git or libgit2) in branch-binding.ts:122. Without that, a copy onto a reused name kept the old declaration in the report.
  • Snapshots. A snapshot with no declared_at attributes nothing. Its generation is still chosen over every snapshot (attribution.ts:113).
  • M8: the doc was wrong, not the hook. The spec that introduced the gate requires asking "before any token of that work is spent". The rule and the reasons are in usage-contract.md:184 and usage-contract.md:200.

🧪 How to verify

  • cd cli && npx vitest run --project=e2e tests/e2e/telemetry-report-declaration.e2e.test.ts covers copy, hand-set, rename, -C onto a declared branch, reused names and legacy.
  • node --test scripts/__tests__/aidd-telemetry-hooks.test.js
  • Red runs, one mutation per guard and every gate are recorded in aidd_docs/tasks/2026_10/2026_10_16_declaration-comes-only-from-the-command/evidence.md. Telemetry mutation score is 96.4; every mutant on changed lines is killed.

⚠️ Heads-up

  • Limits are stated in the usage contract, not fixed:
    • someone editing the keys by hand can still declare a branch;
    • a copy or rename of an unstamped declaration is inherited;
    • snapshots recorded before this change keep attributing;
    • calls on a reused name made before it is declared again still go to the earlier generation;
    • an expired reflog entry falls back to the oldest one.
  • Bundle: 694.0 KB of the 695 KB budget.
  • One full pnpm test run had 2 failures that were not captured. They did not recur in three later runs.
  • An independent checker approved in round 6. The rounds are recorded in review.md.

🔗 Linked issue

None. Found by the R1 telemetry audit (B1, M8).

✅ I certify

  • I DO CERTIFY I READ EACH LINE OF THE PULL REQUEST BECAUSE I AM A SOFTWARE ENGINEER, NOT A AI PUPPY.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

blafourcade and others added 13 commits October 16, 2026 20:49
A task set by hand without aiddDeclaredAt, or a declaration carried to
another branch by git branch -c or -m, bound that branch in both the CLI
and the hook. The command now stamps aiddDeclaredFor with the branch it
declared on; both readers count a declaration only when aiddDeclaredAt is
set and the stamp, when present, names the branch read. Unstamped
declarations from earlier releases still read. A branch snapshot with no
declaration time attributes nothing. forget removes the new key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
…every prompt

The usage contract said the gate asks once per working branch; it holds
every prompt until the branch is declared, as the spec that introduced it
requires. The contract, the plugin README, the hook header and the shared
fixture now state the branch declaration rule, the rename and legacy
decisions, and the limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
…s nothing

Replaces a fallback to an empty list, whose only mutant was equivalent,
and records the mutation and gate runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
git branch -C or -M onto an existing branch leaves each declaration key
twice, and a plain git config set refuses such a key, so the command
could no longer declare that branch. The store now replaces or unsets
every value. Also from review round 1: the rename e2e checks a call made
on the renamed branch, the architecture doc no longer says the gate asks
once, the shared fixture keeps its formatting, and conditions made
always true by the new rule are dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
…ndeclared

git branch -C or -M onto a branch that already had declaration keys
keeps both sets. Reading the last value of each mixed the copied
declaration into the branch's own: a task or ticket the own declaration
never set came from the copy, and its stamp passed. Both readers now
treat any declaration key listed twice as no declaration; declaring
again replaces every value. The shared fixture can now list keys in
order, so both readers are pinned on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
…edeclaration counts

git branch -c, -m, -C and -M bring the source's reflog along, so the
oldest entry is the source's creation. On a name used before, that made
the earlier branch's declaration look newer than one made after the
copy, and the report kept it while status and the gate showed the new
one. The start is now the newest entry where the branch took its name,
else its oldest entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Kills the one mutant left on a changed line, and records round 3's gates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
libgit2 writes "branch: renamed" in lower case. The reflog subject is
now matched without regard to case, so such a rename starts the branch
too. Also from review round 4: the limit for calls on a reused name
before it is declared again, and stale comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
…back to the older

Picks the generation over every snapshot before keeping only declared
ones, so a reused name whose only snapshot was hand-set attributes
nothing instead of the earlier branch's task. Also from review round 5:
the rename of an unstamped declaration is stated as a limit, and the
task contract lists every limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
@blafourcade
blafourcade marked this pull request as ready for review October 16, 2026 21:45
@blafourcade
blafourcade requested a review from a team as a code owner October 16, 2026 21:45
@blafourcade
blafourcade merged commit 8f1b968 into next Oct 16, 2026
31 checks passed
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.

1 participant