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
1 change: 1 addition & 0 deletions aidd_docs/product/usage-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ stateDiagram-v2
- `.aidd/config.json` is never consent: not `telemetry: {enabled: true, version: 2}`, not the old bare `enabled: true`. Not read for consent.
- Unreadable git config: `unreadable`, grants nothing, never rewritten.
- A clone that never opted in: nothing stored, asked or blocked.
- Key `off`, from `off` or by hand: `status` and `task` say measurement was turned off in this clone and that `aidd telemetry on` measures it again; a damaged log or an unidentified clone reads unreadable instead, since `on` would refuse. Any other key that grants nothing reads as never opted in. Ingest still counts its calls under `no-consent`.

**`on` also:** removes the old `telemetry` block from `.aidd/config.json` byte-for-byte otherwise, deleting the file only if that block was all (says the change may need committing); removes an earlier measurement's leftovers in the repository; records its clone for `forget`; warns when Claude Code's transcript retention is short; ends with the next step, declaring a task, which the hooks ask for in Claude Code. Does not reset offsets.

Expand Down
57 changes: 57 additions & 0 deletions aidd_docs/tasks/2026_10/2026_10_16_status-after-off/contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
status: done
issue: 1035
---

# Status after `off` says measurement was turned off

## Need

After `aidd telemetry on` then `aidd telemetry off`, `aidd telemetry status` prints `Not measured: this clone has not run \`aidd telemetry on\`.` (#1035). That is false: the clone ran `on`. The person is told to do what they already did, and is not told that they turned measurement off themselves. `aidd telemetry task` in that clone says the same falsehood: `This clone has not opted in to measurement`.

`off` leaves a trace that tells the two apart: it writes `aidd.telemetry = off` (`CONSENT_WITHDRAWN`) in the clone's git config, and only when the clone's key granted consent. A clone that never ran `on` has no key, or a key that grants nothing and is not `off`.

## Decision

| # | Decision | Why |
| --- | --- | --- |
| 1 | A new refusal reason, `turned-off`, beside `no-consent`, read from the key `off` | The refusal reason is printed as text only: `status` and `task` have no JSON output, and the report's refused envelope carries no reason. No contract changes, and the exhaustive word tables force both commands to say something true |
| 2 | The domain says what the key `off` means, in a function of its own beside `CONSENT_WITHDRAWN`; `consentOf` keeps its three answers | `consentOf` is pinned by the fixture the hooks share (`scripts/__tests__/fixtures/telemetry-bindings`, `off` reads `absent`), and the hooks only ask whether a key grants. The application compares no strings |
| 3 | `NotStoredReason` and the report envelope are not touched | `not_stored.no-consent` is part of `version: 1`, and the issue is about status |

## Surface

| Command | After `on` then `off` | Clone that never ran `on` |
| --- | --- | --- |
| `aidd telemetry status` | `Not measured: measurement was turned off in this clone. Run \`aidd telemetry on\` to measure it again.` exit 0 | unchanged: `Not measured: this clone has not run \`aidd telemetry on\`.` |
| `aidd telemetry task <name>` | `Measurement was turned off in this clone, so no task was declared. Run \`aidd telemetry on\` here to measure it again.` exit 1 | unchanged |

## Rules

| # | Rule |
| --- | --- |
| 1 | A key of exactly `off` is `turned-off`, whether `off` wrote it or a person did by hand: either way measurement was turned off in this clone, and `on` turns it back on |
| 2 | Any other key that grants nothing (absent, `2`, `true`, `{}`) stays `no-consent` |
| 3 | Precedence is unchanged: `AIDD_TELEMETRY=0`, outside a repository, unidentified repository and an unreadable git config come first. `turned-off` is decided only once the consent log is read and the clone identified, where `on` would not refuse it: a key `off` with a damaged log, or a clone with no identity, reads `unreadable-consent`, so status never advises an `on` that would fail. An absent key still reads `no-consent` before the log, as today |
| 4 | `on` after `off` measures again and status says so, as today |

## Unchanged

The consent key format, the consent log, `off` itself, ingest and its `not_stored` reasons (`no-consent` still counts calls made while a clone was off), the report envelope, the hooks. A key `2:<token>` that names a closed interval, the status-side counterpart of `consent-closed`, still reads `no-consent`: the `on` then `off` flow never leaves one.

## Docs

The usage contract states what `status` says after `off`. No doc, skill or hook quotes the old message (grep in `evidence.md`).

## Acceptance

| # | Criterion | Proof |
| --- | --- | --- |
| 1 | Each Surface row and Rules 1 to 3 have a test, written first and seen failing | tests, red run in `evidence.md` |
| 2 | A never-on clone keeps today's status and task text, byte for byte | existing display tests unchanged |
| 3 | Each guard is proven by a mutation that turns it red | `evidence.md` |
| 4 | `cd cli && pnpm test`, lefthook pre-commit and pre-push green | `evidence.md` |
| 5 | `pnpm smoke` passes and the bundle stays within 695 KB | `evidence.md` |
| 6 | `scripts/check-claude-tokens-end-to-end.cjs` passes, plain and `--attended` | `evidence.md` |
| 7 | Telemetry mutation at 96 or above, every mutant on a changed line killed or explained | stryker |
| 8 | Sandboxed repro: after `off`, status is wrong before and right after; a never-on and an on clone unchanged | `evidence.md` |
198 changes: 198 additions & 0 deletions aidd_docs/tasks/2026_10/2026_10_16_status-after-off/evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# Evidence

## Red runs

`pnpm exec vitest run tests/contexts/telemetry/domain/telemetry-consent.unit.test.ts tests/contexts/telemetry/application/telemetry-status-use-case.unit.test.ts tests/contexts/telemetry/application/declare-task-use-case.unit.test.ts tests/presentation/display/telemetry-display.unit.test.ts`, before `turnedOff`, `turned-off` and its words exist:

```txt
× is one whose key is off, as `aidd telemetry off` writes it
× is not one whose key is null / "2" / "2:t" / "true" / "{}" / "OFF" / " off" / "off " / ""
× is not one whose git config cannot be read
TypeError: turnedOff is not a function
× explains each refusal
AssertionError: expected undefined to be 'Measurement was turned off in this cl…'
× explains a refusal
AssertionError: expected [ undefined ] to deeply equal [ Array(1) ]
× says a clone turned off was turned off, even with a log that cannot be read
AssertionError: expected { status: 'refused', …(1) } to deeply equal { status: 'refused', …(1) }
× refuses a clone turned off as turned off, and writes nothing
AssertionError: expected { status: 'refused', …(1) } to deeply equal { status: 'refused', …(1) }
Tests 15 failed | 57 passed (72)
```

Green before the change, by construction: "never says a clone with no key, or a key that grants nothing, was turned off" (today nothing is ever `turned-off`). Proven a guard by M1, M2 and M4 below.

`pnpm exec vitest run tests/e2e/telemetry-lifecycle.e2e.test.ts -t "turned off"`:

```txt
× says, after on then off, that measurement was turned off and how to turn it back on
AssertionError: expected 'Not measured: this clone has not run …' to contain 'Not measured: measurement was turned …'
Tests 1 failed | 11 skipped (12)
```

The never-on assertions before it passed: the clone that never ran `on` keeps today's words.

## Existing tests

No existing `no-consent` assertion seeds the key `off`, so none flipped. `consentOf` is unchanged: the fixture the hooks share (`scripts/__tests__/fixtures/telemetry-bindings/expected.json`, `"off": "absent"`) still holds. The `no-consent` declare refusal is now pinned to its exact words instead of `toContain`.

## Mutations

Each applied by hand to the source, the four unit files above run, then reverted:

```txt
## M1 turnedOff ignores the value: 14 red
× is not one whose key is null
× is not one whose key is "2"
× is not one whose key is "2:t"
× is not one whose key is "true"
× is not one whose key is "{}"
× is not one whose key is "OFF"
× is not one whose key is " off"
× is not one whose key is "off "
× is not one whose key is ""
× refuses like a declaration does
× never says a clone with no key, or a key that grants nothing, was turned off
× refuses a project that has not opted in, and writes nothing
× refuses a clone with no consent, and one whose git config cannot be read, differently
× refuses a key that names no interval open for the clone, and a damaged log
## M2 turnedOff ignores the kind: 15 red
× is not one whose key is null
× is not one whose key is "2"
× is not one whose key is "2:t"
× is not one whose key is "true"
× is not one whose key is "{}"
× is not one whose key is "OFF"
× is not one whose key is " off"
× is not one whose key is "off "
× is not one whose key is ""
× is not one whose git config cannot be read
× refuses like a declaration does
× never says a clone with no key, or a key that grants nothing, was turned off
× refuses a project that has not opted in, and writes nothing
× refuses a clone with no consent, and one whose git config cannot be read, differently
× refuses a key that names no interval open for the clone, and a damaged log
## M3 off key reads no-consent: 2 red
× says a clone turned off was turned off, even with a log that cannot be read
× refuses a clone turned off as turned off, and writes nothing
## M4 any absent key reads turned-off: 5 red
× refuses like a declaration does
× never says a clone with no key, or a key that grants nothing, was turned off
× refuses a project that has not opted in, and writes nothing
× refuses a clone with no consent, and one whose git config cannot be read, differently
× refuses a key that names no interval open for the clone, and a damaged log
## M5 turned-off decided after the log: 2 red
× says a clone turned off was turned off, even with a log that cannot be read
× refuses a key that names no interval open for the clone, and a damaged log
## M6 status words say has not run: 1 red
× explains a refusal
## M7 task words say not opted in: 1 red
× explains each refusal
```

## Gates

| Gate | Decisive line |
| --- | --- |
| `cd cli && pnpm test` | `Tests 6636 passed \| 1 skipped (6637)`, exit 0 |
| `pnpm exec lefthook run pre-commit` (on commit 519ec045) | `✔️ cli-architecture`, `cli-biome`, `cli-type-honesty`, `cli-typecheck`, `markdown-links`, `scripts-tests`, `commitlint`: all green |
| `pnpm exec lefthook run pre-push` | first run: `golden-baseline.e2e.test.ts > snapshot is deterministic` timed out at 60000 ms (machine shared with another agent; the same test passed in `pnpm test` above, and nothing here touches it). Second run: `Tests 6636 passed \| 1 skipped (6637)`, `✔️ cli-knip`, `✔️ cli-test`, exit 0 |
| `pnpm smoke` (sandboxed) | `PASS: 132 FAIL: 0 SKIP: 1 · coverage 100%`, exit 0 |
| Bundle budget | `Bundle size: 693.1 KB / budget: 695 KB` (692.8 KB before) |
| `node scripts/check-claude-tokens-end-to-end.cjs cli/dist/cli.js` (sandboxed) | `2 calls, 393 tokens, all on task demo, ticket T-1, and on task demo in this repository`, exit 0 |
| same, `--attended` | `…; asked, answered by the hook, kept across /clear`, exit 0 |
| `pnpm test:mutation:telemetry` | `score 96.3, floor 96` |

### Mutants on changed lines

`consented-repositories.ts` lines 45 to 46: 9 mutants, all killed. `telemetry-consent.ts` lines 49 to 50: 9 mutants, 8 killed. The survivor is equivalent: `reading.kind === "value"` replaced by `true` leaves `reading.value === CONSENT_WITHDRAWN`, and an unreadable reading has no `value`, so `undefined === "off"` is still `false`. The kind test is there for the type checker's narrowing only. `telemetry-display.ts` is outside the telemetry mutation scope; its two new strings are pinned by exact `toBe`/`toEqual` (M6, M7).

## Docs grep

`git grep -n "Not measured\|has not opted in\|has not run" -- ':!aidd_docs/tasks' ':!cli/aidd_docs/tasks'`: the old words appear only in `telemetry-display.ts` and its tests, and in test names. No skill, hook, README or memory quotes them. The usage contract's consent section gains one line on the key `off`.

## Sandboxed repro

Built `cli/dist/cli.js`, three fresh clones in a `mktemp -d` sandbox (HOME, XDG_CONFIG_HOME, AIDD_USER_CONFIG_DIR, AIDD_TELEMETRY_DIR, CLAUDE_CONFIG_DIR inside it, `GIT_CONFIG_NOSYSTEM=1`): `never` never runs `on`, `on` runs `on --yes --anonymous`, `off` runs `on --yes --anonymous` then `off`.

Before:

```txt
-- off: Measurement is off for this clone. What was measured stays until `aidd telemetry forget --yes`.
== never: status
Not measured: this clone has not run `aidd telemetry on`.
== on: status
This clone is measured since 2026-10-16.
Nothing is bound: the default branch master, which is never bound. Declare with `aidd telemetry task <name>`.
Measured anonymously. To be named, run `aidd telemetry identity <id>`.
== off: status
Not measured: this clone has not run `aidd telemetry on`.
== off: task
Error: This clone has not opted in to measurement, so no task was declared. Run `aidd telemetry on` here first.
exit=1
== off: git key = off
```

After:

```txt
-- off: Measurement is off for this clone. What was measured stays until `aidd telemetry forget --yes`.
== never: status
Not measured: this clone has not run `aidd telemetry on`.
== on: status
This clone is measured since 2026-10-16.
Nothing is bound: the default branch master, which is never bound. Declare with `aidd telemetry task <name>`.
Measured anonymously. To be named, run `aidd telemetry identity <id>`.
== off: status
Not measured: measurement was turned off in this clone. Run `aidd telemetry on` to measure it again.
== off: task
Error: Measurement was turned off in this clone, so no task was declared. Run `aidd telemetry on` here to measure it again.
exit=1
== off: git key = off
```

## Review round 1 fixes

Both minor findings of `review.md` round 1, fixed in `f0f30ac3`. Contract Rule 3, the plan and the usage contract now say a key `off` reads `turned-off` only once the consent log is read and the clone identified; otherwise `unreadable-consent`, because `on` would refuse there.

### Red run

`pnpm exec vitest run tests/contexts/telemetry/application/telemetry-status-use-case.unit.test.ts tests/contexts/telemetry/application/declare-task-use-case.unit.test.ts`, before the change:

```txt
× says a clone turned off cannot be read, not to run on, when on would refuse
AssertionError: expected { status: 'refused', …(1) } to deeply equal { status: 'refused', …(1) }
Tests 1 failed | 27 passed (28)
```

`"{}"` in the never-opted-in loop is green by construction: it was already `no-consent`. G4 below proves it a guard.

### Guard breaks

Each applied to the source, the two files above run, then reverted:

```txt
## G1 turned-off decided before the log again: 1 red
× says a clone turned off cannot be read, not to run on, when on would refuse
## G2 an off key reads no-consent: 3 red
× says a clone turned off was turned off
× says a clone turned off cannot be read, not to run on, when on would refuse
× refuses a clone turned off as turned off, and writes nothing
## G3 the turned-off answer is skipped: 2 red
× says a clone turned off was turned off
× refuses a clone turned off as turned off, and writes nothing
## G4 a key {} reads turned off: 2 red
× refuses like a declaration does
× never says a clone with no key, or a key that grants nothing, was turned off
```

### Gates

| Gate | Decisive line |
| --- | --- |
| `cd cli && pnpm test` | `Tests 6637 passed \| 1 skipped (6638)`, exit 0 |
| lefthook pre-commit (on `f0f30ac3`) | `cli-architecture`, `cli-biome`, `cli-type-honesty`, `cli-typecheck`, `markdown-links`, `scripts-tests`, `commitlint`: all `✔️` |
| lefthook pre-push | `Tests 6637 passed \| 1 skipped (6638)`, `✔️ cli-knip`, `✔️ cli-test`, exit 0 |
| `pnpm test:mutation:telemetry` | `score 96.3, floor 96` |

Mutants on changed lines: `consented-repositories.ts` 15 of 15 killed. `telemetry-consent.ts` lines 49 to 50: 8 of 9 killed. The survivor is the same equivalent mutant as before (`reading.kind === "value"` replaced by `true`).
32 changes: 32 additions & 0 deletions aidd_docs/tasks/2026_10/2026_10_16_status-after-off/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
objective: "After `off`, status and task say measurement was turned off in this clone and how to turn it back on; a clone that never ran `on` keeps today's words."
status: done
---

# Plan: Status after `off` says measurement was turned off

## Overview

| Field | Value |
| ---------- | ----------------------- |
| **Goal** | a `turned-off` refusal, read from the key `off`, worded for `status` and `task` |
| **Source** | [`contract.md`](./contract.md), issue #1035 |

One phase: the change is a domain variant, one branch, two words and a docs line.

## Steps

| # | Step | Files |
| --- | --- | --- |
| 1 | Tests first, seen red: the domain reads `off`, and only `off`, as turned off; status and declare refuse `turned-off` for `off` and keep `no-consent` for absent, `2`, `true`, `{}`; a damaged log or an unidentified clone beats `turned-off`; both words exact; the lifecycle e2e runs `on`, `off`, `status`, `task` | `telemetry-consent` unit test, `telemetry-status-use-case`, `declare-task-use-case`, `telemetry-display` unit tests, `telemetry-lifecycle.e2e.test.ts` |
| 2 | `telemetry-consent.ts` says whether a key was turned off; `RefusalReason` gains `turned-off`; `ConsentedRepositories` maps it; the two word tables say it | `domain/telemetry-consent.ts`, `application/consented-repositories.ts`, `presentation/display/telemetry-display.ts` |
| 3 | Each guard proven by a mutation, reverted | `evidence.md` |
| 4 | Usage contract: what status says after `off` | `aidd_docs/product/usage-contract.md` |
| 5 | Gates and the sandboxed repro | `evidence.md` |

## Decisions

| Decision | Why |
| --- | --- |
| A domain function beside `CONSENT_WITHDRAWN`, not a `Consent` variant | `consentOf` is pinned to `absent` for `off` by the fixture the hooks share; the grammar still stays in the domain |
| `turned-off` is decided after the consent log is read | status never advises an `on` that would refuse (contract rule 3, review round 1) |
Loading
Loading