Problem
#617 stamps commits with the session that produced them. That covers the commits and nothing else.
A feature spans many sessions, and the expensive ones often produce no commit at all: planning, exploring, debugging, answering a review. Attribution built only on commits therefore undercounts, and it undercounts worst on the features that were hardest — exactly where a cost figure needs to be right.
Two more failures the commit trailer cannot fix:
- N sessions, one commit. Work accumulates across sessions and lands in a single commit. Only the last session's id survives; every earlier session detaches from the feature.
- One session, several pull requests. Summing session cost per PR counts that session twice, and the attributed total exceeds the invoice.
And the join itself is not guaranteed. Verified on the official docs of all five tools (probes/03-hooks-et-identifiants-session.md in the dashboard repo): none of them documents that the session id a hook sees is the one in its telemetry export. A spine built on a vendor identifier rests on nothing.
Scope
Own the identifier. The framework generates a run_id at session start. The vendor identifier is stored beside it with its kind, because the four supported tools use four different names (session_id on Claude Code and Codex, conversation_id on Cursor, sessionId on Copilot). Task-to-execution attribution then owes nothing to a vendor; only the cost join stays borrowed, and stays explicit.
One file per session, inside the task folder created by aidd-dev/01-plan:
aidd_docs/tasks/2026_08/2026_08_11_<slug>/
├── plan.md
├── phase-1.md
└── sessions/
└── <run_id>.json
One writer per file, so a merge conflict is structurally impossible — parallel worktrees, concurrent agents, and long-lived branches all converge without a union merge driver. The "list of sessions" is the directory listing.
Identifiers and intervals only.
{
"schema_version": 1,
"run_id": "01J9X4M2...",
"native_id": "0a3f9c2e-...",
"native_id_kind": "session.id",
"parent_run_id": null,
"tool": "claude-code",
"task_id": "2026_08_11_<slug>",
"intervals": [{ "phase": "phase-2", "from": "...", "to": "..." }],
"started_at": "...",
"ended_at": "..."
}
No tokens, no cost, no model. Those change mid-session, so a committed file carrying them is wrong from the first minute; they come from telemetry and join later. A session that switches task or phase records a second interval, never one overwritten value.
Written by hooks, never by the model. Session start creates the file; turn stop refreshes ended_at. Do not depend on a session-end event: Codex allows it 1 s (3 s max) and does not fire it for subagents, and OpenCode has no end event at all. "Last observed turn" is both more portable and more truthful than "clean exit declared".
Owned by the CLI. Enabling, installing and checking the pipeline is a CLI concern, per #617. Disabled by default on public repositories, with explicit per-repo opt-in: a contributor's identifier must never enter a permanent public history by default.
Read through GitHub. The dashboard's existing connector reads repository files, so no upload path is needed and no endpoint can fail at commit time. It also yields a second source, independent from the OTel flow, that must reconcile with it — a divergence becomes an integrity signal instead of a mystery.
Out of scope
Acceptance criteria
Prior art in this repo
Relations
Problem
#617 stamps commits with the session that produced them. That covers the commits and nothing else.
A feature spans many sessions, and the expensive ones often produce no commit at all: planning, exploring, debugging, answering a review. Attribution built only on commits therefore undercounts, and it undercounts worst on the features that were hardest — exactly where a cost figure needs to be right.
Two more failures the commit trailer cannot fix:
And the join itself is not guaranteed. Verified on the official docs of all five tools (
probes/03-hooks-et-identifiants-session.mdin the dashboard repo): none of them documents that the session id a hook sees is the one in its telemetry export. A spine built on a vendor identifier rests on nothing.Scope
Own the identifier. The framework generates a
run_idat session start. The vendor identifier is stored beside it with its kind, because the four supported tools use four different names (session_idon Claude Code and Codex,conversation_idon Cursor,sessionIdon Copilot). Task-to-execution attribution then owes nothing to a vendor; only the cost join stays borrowed, and stays explicit.One file per session, inside the task folder created by
aidd-dev/01-plan:One writer per file, so a merge conflict is structurally impossible — parallel worktrees, concurrent agents, and long-lived branches all converge without a union merge driver. The "list of sessions" is the directory listing.
Identifiers and intervals only.
{ "schema_version": 1, "run_id": "01J9X4M2...", "native_id": "0a3f9c2e-...", "native_id_kind": "session.id", "parent_run_id": null, "tool": "claude-code", "task_id": "2026_08_11_<slug>", "intervals": [{ "phase": "phase-2", "from": "...", "to": "..." }], "started_at": "...", "ended_at": "..." }No tokens, no cost, no model. Those change mid-session, so a committed file carrying them is wrong from the first minute; they come from telemetry and join later. A session that switches task or phase records a second interval, never one overwritten value.
Written by hooks, never by the model. Session start creates the file; turn stop refreshes
ended_at. Do not depend on a session-end event: Codex allows it 1 s (3 s max) and does not fire it for subagents, and OpenCode has no end event at all. "Last observed turn" is both more portable and more truthful than "clean exit declared".Owned by the CLI. Enabling, installing and checking the pipeline is a CLI concern, per #617. Disabled by default on public repositories, with explicit per-repo opt-in: a contributor's identifier must never enter a permanent public history by default.
Read through GitHub. The dashboard's existing connector reads repository files, so no upload path is needed and no endpoint can fail at commit time. It also yields a second source, independent from the OTel flow, that must reconcile with it — a divergence becomes an integrity signal instead of a mystery.
Out of scope
Acceptance criteria
parent_run_id, on every tool that exposes the relation.Prior art in this repo
plugins/aidd-dev/skills/01-plan/actions/04-plan.md— createsaidd_docs/tasks/<yyyy_mm>/<yyyy_mm_dd>_<feature-slug>/, which already is the feature identifier: dated, unique, greppable, and created before the first line of code.plugins/aidd-context/hooks/hooks.json— the proven plugin-hook pattern.plugins/aidd-context/skills/08-hook-generate/references/tool-paths.md— per-tool events, identifier field names, and failure semantics (corrected under fix(aidd-context): source-backed tool references, with dates and unverified cells #618)..aidd/config.yml, where the per-repo opt-in belongs.Relations