Skip to content

feat(aidd-telemetry): a task declares its backlog once, and every session on it is found #975

Closed as not planned

Description

@blafourcade

Problem

The report answers "what did this task cost". It cannot answer "what did issue #661 cost" with anything a reader should trust, because the only upward link — backlog-link.json — is written by the model itself, from prose, as a step inside three skills (plugins/aidd-pm/skills/04-spec/actions/01-build.md:22-34, plugins/aidd-dev/skills/01-plan/actions/04-plan.md:16-26, plugins/aidd-orchestrator/skills/01-sdlc/references/01-frame.md:7). Nothing obliges a model to follow a written instruction, and nothing measures whether it did.

The obvious fixes all fail for reasons that are measurable in this repository today, not hypothetical. This issue records what was measured, what was therefore dropped, and the one shape that survives.

What was measured

Fact Command Result
The session trailer is not parseable as a trailer git log next -60 --format='%(trailers:key=AIDD-Session-Id,valueonly)' 0 parsed, while --grep finds the literal string in 44 of the same 60 commits, and Co-authored-by parses from 45
Why git log -1 --format=%B <commit> the squash appends --- and a trailing Co-authored-by paragraph after ours; git parses only the last paragraph
A branch rarely maps to one task on the expensive changes distinct task folders touched per squashed PR, last 300 commits of next 45 PRs touch 1 folder, 4 touch 2, one touches 5, two touch 8, one touches 20
The hook does not know the branch today grep -rn "abbrev-ref|symbolic-ref|show-current|rev-parse HEAD" plugins/aidd-telemetry no match
The task-path scrape records non-paths as task identities the run journals in this repository aidd_docs/tasks/2026_08/*/ is journalled as task_declared; taskIdentityFromWrittenPath turns it into the identity 2026_08/*. Also plan.md. and spec.md; (shell punctuation). 119 task_declared lines for 52 distinct paths in two sessions
git config branch.<name>.* behaves the way a task link must scratch repository follows git branch -m (value intact under the new name, gone from the old), and a deleted-then-recreated branch name reads empty

What this rules out

A branch is not a key. It is a reading, not an identity: cli/src/domain/models/task-attribution.ts already refuses an unbounded task interval — "no tool exposes when a flow leaves a ticket, so a boundless interval would attribute everything a long-running session goes on to do to the first ticket it ever named". A branch is that same unbounded interval with the branch's lifetime as its span. It is right about 85% of the time and silently wrong on the largest changes, which is the worst distribution: an abstention is cheaper than a confident wrong number. Gerrit reaches the same conclusion from the other side — a Change-Id is explicitly "not necessarily unique within a Gerrit instance; it can be reused among different repositories or branches" — and OpenTelemetry's vcs.ref.head.type is documented as "where you are right now; the current reference at a given time". Neither treats a ref as an identity.

A second commit trailer buys nothing. The trailer we already write is invisible to git interpret-trailers, %(trailers) and gh after this repository's own squash merges (0/60 above). Renaming it to AIDD-Task inherits that exactly. Resolving a task from a committed trailer would also have to happen inside the prepare-commit-msg delegate, which is POSIX sh by deliberate decision — "no Node, no dependency on this CLI still being installed" (cli/src/domain/formats/commit-session-trailer.ts, doc on sessionTrailerDelegateScript) — so it would mean scanning and parsing JSON in sh, or a hard Node dependency in a script whose point is not having one.

The hook must not write a task card. plugins/aidd-telemetry/hooks/lib/repo.cjs:288-313 already refuses to write into a checkout it does not own, because that "would dirty a checkout on a different branch, possibly with uncommitted work of its own". Writing an unasked file into the user's own tree is the same intrusion. It also feeds itself: lib/file-writes.cjs:117-166 journals every file under a task folder whose mtime is newer than the run file, so a card the hook writes is journalled on the next turn as a file written by the session — attribution evidence for a file nobody authored.

The attribution enum must not be re-sorted. declared is today the strongest value of by_task.attribution (cli/src/domain/models/task-attribution.ts:21, label at cli/src/application/display/cost-report-display.ts:36-39). A four-rung ladder that splits today's declared population into a new strongest rung and a new weakest rung changes what a shipped string means, in the one field a destination aggregates on.

Proposed shape

Two questions that the previous design conflated, each on the support that fits it:

  • Which backlog item does this task deliver? Shared, durable, belongs in the repository: backlog-link.json stays, with its name, its three states (declared / none / unreadable) and its existing provenance pair written_by / written_at. What changes is who writes it: a command, from its arguments, never prose.
  • Which task is this working tree on right now? Local, mutable, per-worktree: git config branch.<name>.aiddTask. It follows a rename, dies with the branch, does not haunt a reused name, and reads in one line from the sh delegate.

Scope

  1. aidd task start <slug> [--backlog <ref>]… — creates or reuses the task folder, writes backlog-link.json from its arguments, and sets branch.<current>.aiddTask to the task identity. Refuses, naming the reason, on a detached HEAD (git rev-parse --abbrev-ref HEAD prints the literal HEAD during rebase, cherry-pick, bisect and in a detached worktree) and on an integration branch.
  2. aidd task attach <task> — sets the same key for work already under way, and for a second machine or a second person on the same branch.
  3. The hook reads git config --get branch.<branch>.aiddTask at session_start and when the branch changes, and journals it as a timestamped observation. It writes no card, derives no identity: "a written path is recorded, never a derived task_id: that derivation belongs to the reader" (plugins/aidd-telemetry/hooks/lib/file-writes.cjs:1-3). Cost is one more option on the git rev-parse call the hook already makes per event (lib/repo.cjs:95-121).
  4. The reader builds intervals from that observation the way it already does for task_declared, bounded by the session's own witnessed span. If a new attribution value is needed, it is added; no existing value changes meaning or rank.
  5. aidd task show [<task>] — sessions, steps, files, commits and cost for one task, derived at read time, nothing frozen.
  6. check states what it cannot resolve rather than guessing: no key on this branch, a key naming a folder that does not exist, an unreadable backlog-link.json, a task folder outside the root aidd_docs/tasks/ tree.

Acceptance criteria

  • The same arguments produce the same bytes in backlog-link.json, modulo the timestamp.
  • A session started on a branch carrying the key is attributed to that task from its first request, with no model instruction involved, on every tool whose hook runs.
  • A session on a branch with no key is attributed by the routes that exist today, and never by a guess at the branch.
  • Renaming the branch keeps the attribution; deleting and recreating the branch name yields no attribution rather than the old one.
  • Running report twice over the same past window returns the same per-task totals, before and after a new task is started — a link created today never rewrites yesterday.
  • check names every unresolvable case above, with the path or branch it is talking about.
  • An unreadable backlog-link.json disables the link visibly; it never falls through to a weaker route that would produce a plausible number from a file the user broke.

Prior art in this repository

  • cli/src/domain/models/task-backlog-link.ts — why this file carries one fact and not a fuller card, and the three-state result.
  • cli/src/domain/models/task-attribution.ts — why a task interval is never left open-ended, with the measured cost of getting it wrong.
  • cli/src/domain/models/task-identity.ts:1-6 — the journal stores observations, the reader derives; "a conclusion frozen at write time cannot be revised".
  • cli/src/domain/formats/commit-session-trailer.ts — the delegate's constraints, and its refusal of a second identity for one thing.
  • plugins/aidd-telemetry/hooks/lib/repo.cjs — the git facts the hook already collects, and what it refuses to write.

Out of scope

  • Any hosted destination: sending, identity through a remote, prices, team and epic aggregation.
  • Changing by_task.attribution's existing values or their order.
  • A metadata.json carrying id, title, base, created_at or parent: each is derivable from the folder path, from git, or from the forge, and vcs.md already makes the branch prefix the authority for the PR base.
  • Rewriting history to repair unparseable trailers in merged commits.

Defects found while measuring this

Each stands on its own and can be split out when picked up.

  • The session trailer is not parseable after a squash merge. 0 of 60 recent next commits expose AIDD-Session-Id through %(trailers), though 44 contain the string: our block stops being the last paragraph once the squash appends its own. Nothing reads it today, so nothing broke loudly.
  • The task-path scrape produces phantom task identities. A glob (aidd_docs/tasks/2026_08/*/) and punctuation-suffixed paths (plan.md., spec.md;) are journalled as task_declared and become identities such as 2026_08/*, at the strongest attribution strength. Validate a scraped path against the filesystem before it becomes an identity.
  • mtime-based observed writes are polluted by plain git operations. checkout, pull, rebase and stash apply rewrite the mtime of every differing task document; the next turn journals them as written by the current session (lib/file-writes.cjs:60-104 records on mtime alone). Cross-check against the working tree's actual changes before journalling an observed write.
  • Task folders outside the root tree are invisible to every route. cli/aidd_docs/tasks/ and kanban/aidd_docs/tasks/ exist here, but both anchors require ^aidd_docs/tasks/ relative to the repository root (lib/file-writes.cjs:22, cli/src/domain/models/task-identity.ts:14). That work reports as "no declaration" — a claim about the work, when it is a limit of the pattern.
  • On a fork, the project's remote resolves to the wrong owner/repo. lib/repo.cjs:172 reads origin only, so a backlog shorthand expanded from the remote names a repository that does not hold the issue.

Activity

  1. VincentMenard76 commented on Oct 9, 2026

    @VincentMenard76
    Contributor

    One more defect of the same family as "task folders outside the root tree", surfaced by #932.

    • A task folder absent from the checkout running the report reads as declaration: "none". Since fix(aidd-telemetry): a session run in a git worktree is missing from every report outside that worktree, and lost when the worktree is removed #932, sessions from every worktree of a clone are counted from any checkout. A task created on another worktree's unmerged branch therefore reaches the backlog axis, which looks for <task>/backlog-link.json in the current checkout only (cli/src/contexts/telemetry/infrastructure/task-backlog-adapter.ts, repositoryRootAbove(projectRoot) then ENOENT → { kind: "none" }). The contract defines none as "its folder declares no backlog item" (aidd_docs/product/cost-report-contract.md, by_backlog); a folder that is not there at all is a different fact — an unknown printed as a nothing. Proposed: when the task folder itself is absent, answer a distinct declaration value (e.g. "absent"), so none keeps exactly its documented meaning and no existing value changes rank. It resolves on its own once the branch is merged and the checkout is up to date; a correction made by hand in backlog-link.json keeps applying, since nothing is frozen in the journal. Adding a value a consumer must tell apart likely bumps cost_report_version.
  2. blafourcade commented on Oct 15, 2026

    @blafourcade
    ContributorAuthor

    Superseded by #993: the task and ticket now come only from aidd telemetry task <name> [--ticket <ref>]; no backlog file is read.

  3. moved this from Ideation to Done in AIDD Roadmapon Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Fields

    Priority

    None yet

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions