Outcome
Every session of a supported tool leaves a durable record tying it to a task, and that record never contains a measurement.
Scope
- Includes: a new aidd-telemetry plugin, which carries the hooks. Installing the plugin installs the mechanism; the CLI wires nothing per tool.
- Includes: one file per session at aidd_docs/runs/<run_id>__<vendor_id>.json, written on session start and refreshed on the last observed turn. AIDD_RUNS_DIR overrides the directory.
- Includes: an explicit current-task pointer at .aidd/current-task, written by the planning and implementation skills. The hook reads it and never guesses.
- Includes: a run_id minted by the framework, stored beside the vendor identifier and the name of the field it came from.
- Includes: a stable project_id in every record, and the same value pushed into OTEL_RESOURCE_ATTRIBUTES by feat(cli): one command that turns the provider export on #646. Without it a sink mixes every repository on a machine with nothing to separate them, and it cannot be recovered after the fact.
- Includes: Claude Code only. The hook identifies its host from the payload it receives and exits 0 on any tool it does not support.
- Excludes: task identity and backlog links, which belong to the next milestone.
- Excludes: any token, cost, model or duration. Those change mid-session and come from telemetry.
- Excludes: backfilling sessions that predate the feature.
Why a dedicated plugin
aidd framework build copies every plugin's hooks/ into all five tool targets, and no exclusion mechanism exists (cli/src/application/use-cases/framework/strategies/tool-contracts.ts, hooksBundle). Putting telemetry hooks in an existing plugin would ship them to everyone with no way out. A separate plugin makes the opt-out trivial: do not install it.
It also matches docs/ARCHITECTURE.md — "Every capability lives in exactly one plugin, chosen by concern". Measurement is neither knowledge production, nor code transformation, nor version control.
The file
{
"schema_version": 1,
"run_id": "01J9X4M2K7QRVB",
"project_id": "ai-driven-dev/framework",
"tool": "claude-code",
"vendor_id": "79041f53-35b0-4924-8855-e43e9de72431",
"vendor_field": "session.id",
"parent_run_id": null,
"started_at": "2026-08-14T10:08:44Z",
"ended_at": "2026-08-14T11:05:20Z",
"tasks": [
{ "task_id": "2026_08_14_telemetry-v1", "from": "2026-08-14T10:08:44Z", "to": null }
]
}
A session can cover several tasks
task_id is a list of intervals, not a scalar. A session that plans task A then implements task B is otherwise attributed wholesale to one of them, and that is the common case rather than the edge.
"tasks": [
{ "task_id": "2026_08_15_alpha", "from": "...", "to": "..." },
{ "task_id": null, "from": "...", "to": "..." }
]
An interval with a null task_id is out-of-flow work inside an otherwise attached session.
Three decisions, measured
runs/ is global, not inside the task folder. A per-task journal has nowhere to put out-of-flow work — the ten-minute debug with no folder, the exploration. One location handles both identically: attachment becomes a field, not a path. task_id null is a normal state, and reattachable afterwards since the file is ours. This is what makes "61% attached, 39% out of task" sayable instead of ignoring the second half.
vendor_field names the field, not just its kind. Cursor exposes three identifiers in one payload — session_id, conversation_id, generation_id — equal on a single-turn session with no guarantee beyond. The journal records which one it took.
ended_at refreshes on the last observed turn, never on a session-end event: Codex allows it one second, three at most, and does not fire it for subagents.
Open, to decide during implementation
Closed. Records live in aidd_docs/runs/ inside the repository, with .gitignore carrying aidd_docs/runs/* and the two marker files negated. The directory enters git so opt-in survives a clone; the records never do. Verified in a scratch repository: git add -A mid-session stages nothing and the tree stays clean.
They were briefly written outside the repository, at ${XDG_STATE_HOME:-~/.local/state}/aidd/runs/<owner__repo>/, and moved back. Ignoring the contents answers the dirty-tree problem completely, while a home directory is nobody’s to control: invisible, per-machine, unbacked-up, three platform branches, and an owner/repo → owner__repo flattening with its own collision. In-project, the opt-in marker is the store, so the repository root is the key and the platform stops mattering.
The pointer has three states nobody has handled
- Two concurrent sessions in the same checkout share one .aidd/current-task. A background agent on another task silently re-points the foreground session.
- .aidd/ is currently neither tracked nor gitignored in this repository. On a fresh clone or in CI the pointer is absent, so every session journals as out-of-flow and reads as a coverage failure.
- Nothing validates the pointer against an existing task folder, so a stale value produces a confidently wrong chain.
The tracked-versus-ignored decision belongs here, with its consequence stated: committing runs/ puts who-worked-on-what-and-for-how-long into permanent git history, which is exactly what #652 says cannot ship without an organisational decision. Deciding it after the data exists is deciding it too late.
Done When
- Two agents working the same task in two parallel worktrees produce two files and zero merge conflicts, verified by merging both branches.
- A session producing no commit still appears in the journal.
- A session with no current-task pointer produces a record with task_id null, never nothing.
- The written JSON keys are exactly the ten documented ones, asserted as a whitelist.
- The hook exits 0 in every failure mode and returns in under 200 ms at p95 over 100 invocations.
- Installed on Codex, Cursor, Copilot or OpenCode, the hook writes nothing and exits 0.
- On a public repository nothing is written until the repository has opted in.
- A session that switches task mid-way produces two intervals, never one overwritten value.
- Two concurrent sessions in the same checkout do not corrupt each other's attachment.
- With no pointer present, the session journals out-of-flow and status reports it as such rather than as an error.
- A record carries project_id, and two repositories on one machine are separable at read time.
- scripts/__tests__/ carries a node:test file for the hook, and a runner is wired into lefthook.
Completion Evidence
A week of real work on this repository journaled, with the attached and out-of-flow shares, and the demonstration that no session was lost.
Relations
Design: aidd_docs/specs/2026_08/2026_08_13-work-tracking-linkage.md
Outcome
Every session of a supported tool leaves a durable record tying it to a task, and that record never contains a measurement.
Scope
Why a dedicated plugin
aidd framework build copies every plugin's hooks/ into all five tool targets, and no exclusion mechanism exists (cli/src/application/use-cases/framework/strategies/tool-contracts.ts, hooksBundle). Putting telemetry hooks in an existing plugin would ship them to everyone with no way out. A separate plugin makes the opt-out trivial: do not install it.
It also matches docs/ARCHITECTURE.md — "Every capability lives in exactly one plugin, chosen by concern". Measurement is neither knowledge production, nor code transformation, nor version control.
The file
{ "schema_version": 1, "run_id": "01J9X4M2K7QRVB", "project_id": "ai-driven-dev/framework", "tool": "claude-code", "vendor_id": "79041f53-35b0-4924-8855-e43e9de72431", "vendor_field": "session.id", "parent_run_id": null, "started_at": "2026-08-14T10:08:44Z", "ended_at": "2026-08-14T11:05:20Z", "tasks": [ { "task_id": "2026_08_14_telemetry-v1", "from": "2026-08-14T10:08:44Z", "to": null } ] }A session can cover several tasks
task_id is a list of intervals, not a scalar. A session that plans task A then implements task B is otherwise attributed wholesale to one of them, and that is the common case rather than the edge.
An interval with a null task_id is out-of-flow work inside an otherwise attached session.
Three decisions, measured
runs/ is global, not inside the task folder. A per-task journal has nowhere to put out-of-flow work — the ten-minute debug with no folder, the exploration. One location handles both identically: attachment becomes a field, not a path. task_id null is a normal state, and reattachable afterwards since the file is ours. This is what makes "61% attached, 39% out of task" sayable instead of ignoring the second half.
vendor_field names the field, not just its kind. Cursor exposes three identifiers in one payload — session_id, conversation_id, generation_id — equal on a single-turn session with no guarantee beyond. The journal records which one it took.
ended_at refreshes on the last observed turn, never on a session-end event: Codex allows it one second, three at most, and does not fire it for subagents.
Open, to decide during implementation
Closed. Records live in aidd_docs/runs/ inside the repository, with .gitignore carrying aidd_docs/runs/* and the two marker files negated. The directory enters git so opt-in survives a clone; the records never do. Verified in a scratch repository: git add -A mid-session stages nothing and the tree stays clean.
They were briefly written outside the repository, at ${XDG_STATE_HOME:-~/.local/state}/aidd/runs/<owner__repo>/, and moved back. Ignoring the contents answers the dirty-tree problem completely, while a home directory is nobody’s to control: invisible, per-machine, unbacked-up, three platform branches, and an owner/repo → owner__repo flattening with its own collision. In-project, the opt-in marker is the store, so the repository root is the key and the platform stops mattering.
The pointer has three states nobody has handled
The tracked-versus-ignored decision belongs here, with its consequence stated: committing runs/ puts who-worked-on-what-and-for-how-long into permanent git history, which is exactly what #652 says cannot ship without an organisational decision. Deciding it after the data exists is deciding it too late.
Done When
Completion Evidence
A week of real work on this repository journaled, with the attached and out-of-flow shares, and the demonstration that no session was lost.
Relations
Design: aidd_docs/specs/2026_08/2026_08_13-work-tracking-linkage.md