Skip to content

feat(framework): per-session ledger in the task folder, keyed by an AIDD run id #620

Description

@alexsoyes

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

  • Two agents working the same task in parallel worktrees produce two files and zero merge conflicts.
  • A session that produces no commit still appears in the task folder.
  • A session that changes task or phase mid-way produces two intervals, not one overwritten value.
  • No token, cost, model or prompt content appears in any file.
  • On a public repository nothing is written unless the repo explicitly opted in.
  • A subagent records parent_run_id, on every tool that exposes the relation.
  • The record survives a squash-merge, since it is a file and not a commit message.
  • The dashboard can compute attributed vs unattributed cost from the folder plus telemetry, and the two sources reconcile.

Prior art in this repo

Relations

Field Value
blocked-by #585
related #617, #618

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Fields

    Priority

    None yet

    Projects

    Status
    Todo

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions