Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions aidd_docs/product/usage-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ Row and `totals`: `records`, `input`, `output`, `cache_read`, `cache_write` (eac

- Every axis partitions the same calls: each adds up to `totals`, counter by counter.
- `AIDD_TELEMETRY=0`: nothing read, object is `{"version": 1, "refused": "AIDD_TELEMETRY=0"}`.
- The text report labels an `absent` person `anonymous`; the envelope keeps `kind: absent`, `key: null`.
- A report ingests first. `files_read` and `unrecognised_shapes` describe that run; `records` the period.
- A report with `--days`, `--from` or `--to` reads only the days of its period and the day on each side, plus any monthly file one of them falls in, with every day of its month; with none it reads the whole ledger. The copies of one call have been observed less than a day apart, and under that bound both print the same figures. Outside it, only a report with no period folds every copy of a call. `skipped_ledger_lines` counts the lines of the partitions read.
- No path, working directory or branch. `session` keys are full session ids, `repository` keys are hashes.
Expand Down Expand Up @@ -151,7 +152,7 @@ Directory: `AIDD_TELEMETRY_DIR`, else `AIDD_USER_CONFIG_DIR/telemetry`, else `XD
| `bindings/branches.jsonl` | `task`, `ingest`, `report` | `report`, `forget` | branch declaration at a moment, with branch creation time from its reflog |
| `bindings/carries.jsonl` | `SessionStart` hook | hooks, `report` | `{session_id, from, at}` |
| `bindings/processes.jsonl` | `SessionStart` hook | `SessionStart` hook | `{pid, session_id, source, at}` |
| `identity.json` | `identity` | `report` | `{"person_id": "<id>"}`, owner-only |
| `identity.json` | `identity`, `on` | `report`, `status`, `on` | `{"person_id": "<id>"}`, owner-only |
| git config `--local` `branch.<name>.aiddTask`, `aiddTicket`, `aiddDeclaredAt` | `task` | hooks, `task`, `report` via snapshots | branch declaration; follows a rename, leaves with the branch |
| git config `--local` `aidd.telemetry` | `on`, `off`, `forget` | hooks, CLI | `2:<token>` or `off`. Half of consent: alone it grants nothing |

Expand Down Expand Up @@ -263,7 +264,7 @@ stateDiagram-v2

| Actor | Does |
| --- | --- |
| `on` | under the ledger lock: fresh token, key `2:<token>` first, then open line (a key with no interval measures nothing; an interval its key does not name would be closed by the first hook). Key naming an open interval of this clone: no change. Otherwise closes this clone's stale open intervals at its time. Refuses a clone with no identity |
| `on` | under the ledger lock: fresh token, key `2:<token>` first, then open line (a key with no interval measures nothing; an interval its key does not name would be closed by the first hook). Key naming an open interval of this clone: no change. Otherwise closes this clone's stale open intervals at its time. Refuses a clone with no identity. Then, only once on, writes the person's identity when one was chosen |
| `off` | under the same lock: close the clone's open token, then key `off`; key and interval change together, so an ingest never sees one without the other. Also closes the interval its key names at its path, whatever identity it reads there |
| ingest | before judging any call, for every open interval, closes at observation time when the identity at its path changed or the live key is not `2:<that token>`. Nothing at the path: not closed (unmounted volume, folder moved and back); measured again when back. Unreadable config or `stat`: stays open, stores nothing |
| hooks | after the Claude-only guard, even headless: key not naming an open interval recorded for this clone's real path => append a close at hook time. Ends a manual `git config aidd.telemetry off` or `--unset` before the next prompt's calls. `AIDD_TELEMETRY=0`: does nothing |
Expand Down Expand Up @@ -309,5 +310,18 @@ Release 1 opens no network connection, in the telemetry context or the hooks. `f

- Local only: ledger `cwd` and `git_branch`, roots, process facts, transcripts.
- Text report: no path, branch, working directory or whole session id (short keys); envelope per [Report envelope](#report-envelope). `task`, `status`, `forget` print what they act on (a branch: `Branch feat/x is bound.`; `forget` lists entries and repositories); the lock error names the lock file.
- Identity only if chosen with `aidd telemetry identity <id>`; labels the `person` axis only.
- Identity only if chosen: `aidd telemetry identity <id>`, `on --as <id>`, or the answer to `on`'s question. Labels the `person` axis only; `on` and `status` name it.
- No currency amount: the destination owns the price table.

**Who is measured, at `on`.**

| Case | `on` does |
| --- | --- |
| identity set, no flag | keeps it, asks nothing |
| none set, a terminal | after the opt-in is confirmed, asks: git's `user.name` in that directory, else `user.email` (offered only when `personIdOf` accepts it, never under `AIDD_TELEMETRY=0`), another identifier typed, or anonymous |
| `--as <id>` | sets it, replacing one set; refused before anything is written when not one line of 1 to 128 characters |
| `--anonymous` | sets nothing; refused while an identity is set (`identity --off` first) |
| `--yes`, none set, no flag | refused, exit 1, nothing written |
| `--as` with `--anonymous` | refused, exit 1 |

A refused `on` writes neither consent nor identity. Staying anonymous is not stored: the next `on`, in any clone, asks again.
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
status: done
issue: 1029
---

# Who is measured is chosen at opt-in

## Need

`aidd telemetry report --axis person` names a person only when they ran `aidd telemetry identity <id>`, a command almost nobody finds (#1029). Otherwise the row reads `not set`, and nothing says under which name a machine is measured. The person axis matters once a team's figures are gathered (#662), so each person must have chosen how they appear before that.

## Decisions (agreed with the user)

| # | Decision | Why |
| --- | --- | --- |
| 1 | With no identity, the text report labels the person row `anonymous`. The envelope is unchanged: kind `absent`, `key: null`. | Version 1 keeps its meaning, a real identity named `anonymous` cannot be confused with none, and `--absent person` (#1030) still selects it. |
| 2 | A non-interactive `on --yes` with no identity set and neither `--as` nor `--anonymous` is an error, exit 1, before anything is written. | Same rule as `--yes` today: nothing implicit. |
| 3 | Choosing to stay anonymous is not stored. The next `on`, in any clone, asks again while no identity is set. | `on` runs once per clone; the identity store keeps its one-field format. |

## Surface

| Command | Behaviour |
| --- | --- |
| `aidd telemetry on` (interactive), no identity set | After the opt-in is confirmed, one question: name me as the proposal, as another identifier (then typed), or stay anonymous. With no proposal, the first choice is not offered. |
| `aidd telemetry on`, identity set | No question. The output says under which name the clone is measured. |
| `aidd telemetry on --as <id>` | Sets `<id>` as the identity, replacing one already set; the output names the previous one when it changes. |
| `aidd telemetry on --anonymous` | Sets nothing. With an identity already set, it is an error, exit 1, pointing to `aidd telemetry identity --off`. |
| `aidd telemetry status` | Says under which name this machine is measured, or `anonymous` and how to be named. |
| `aidd telemetry report` | Text: the person row with no identity reads `anonymous`. JSON unchanged. |

## Rules

| # | Rule |
| --- | --- |
| 1 | The proposal is the git `user.name` of the directory the command runs in, else its `user.email`, as git resolves them (local, then global). Read through a port. It is shown, never stored without the person choosing it. |
| 2 | A proposal or a typed identifier goes through `personIdOf` (trimmed, one line, 1 to 128 characters). A proposal it rejects is not offered. A rejected `--as` is an error before anything is written. |
| 3 | `--as` and `--anonymous` together: error, exit 1. Either with `--yes` or interactive is accepted. |
| 4 | The identity is written only when `on` succeeds (status `on`). A refused `on` writes no identity. |
| 5 | `AIDD_TELEMETRY=0` does not refuse `on`, as before this change. Under it, the git name is neither read nor proposed: the question offers only another identifier or anonymous. |
| 6 | `identity`, `identity <id>` and `identity --off` behave as before; their wording says `anonymous` for nobody, as the report does (decision 1). |

```mermaid
flowchart TD
F["flags checked (rules 2, 3)"] --> C{"opt-in confirmed?"}
C -->|no| N["nothing changed"]
C -->|yes| I{"identity set?"}
I -->|"yes, no --as"| K["choice: keep it (--anonymous here: error)"]
I -->|"no, or --as"| Q["choice: --as, --anonymous, or the question<br/>(--yes with neither: error, decision 2)"]
K --> O["on use case, given the choice"]
Q --> O
O -->|refused| R["refused: no consent, no identity written"]
O -->|on| W["identity written when one was chosen; output names it"]
```

The question comes after the opt-in is confirmed and before the use case writes anything, so a refusal leaves both consent and identity untouched: the choice is passed to the use case, which writes the identity only on success.

## Unchanged

The identity store format, `identity.json` and where it lives; the envelope; consent; what is measured. The identity never leaves the machine.

## Docs

Usage contract (person axis, opt-in), `cli/aidd_docs/memory/telemetry.md`, `plugins/aidd-telemetry/README.md`, and the `00-init` skill's `start` action: it asks the person to type an identifier or stay anonymous, reads nothing from git so no name reaches the session before the person chooses, says that `aidd telemetry on` in a terminal offers the git name, and runs `aidd telemetry on --yes --as '<id>'` or `--anonymous`.

## Acceptance

| # | Criterion | Proof |
| --- | --- | --- |
| 1 | Every row of the Surface and Rules tables has a test, written first and seen failing | tests, red run in `evidence.md` |
| 2 | A refused `on` writes neither consent nor identity, whatever the flags | use-case test |
| 3 | No identity is ever written without `--as`, a typed id, or the proposal chosen | test per path, plus a mutation on the write guard |
| 4 | The envelope is byte-identical to before for every axis | existing envelope tests unchanged |
| 5 | Telemetry mutation at 96 or above, every mutant on a changed line killed or explained | stryker |
| 6 | `pnpm smoke`, `scripts/check-claude-tokens-end-to-end.cjs` both modes pass | `evidence.md` |
| 7 | Docs and the `00-init` skill say what `on` asks and how to answer it non-interactively | grep, plus a fresh checker |
Loading
Loading