ds-identity · git:20260727.68dec2e · 2026-07-27 · sha256 5c8ab6d06a9180be
ds-identity git:20260727.68dec2eA
Immutable. This exact content never changes and is served at /api/v1/blob/5c8ab6d06a9180be.
---
name: ds-identity
description: "Manage the developer identity used for session telemetry attribution."
user-invocable: true
---
# /ds-identity
Manage the developer identity used for session telemetry attribution.
Supports manual, automatic (GitHub-derived), and provisional-to-confirmed
identity flows. Identity drives per-project `.agentic/session-log/<dev>.jsonl`
and the global operator mirror at `~/.agentic/session-log/<dev>.jsonl`.
Implementation: `bin/agentic-identity` (Python 3 stdlib + optional pyyaml).
## Usage
```
agentic-identity init <handle> [--display-name <name>] [--force] [--scope {global,project}]
agentic-identity show [--scope {global,project,effective}]
agentic-identity auto [--force] [--scope {global,project}]
agentic-identity confirm [--scope {global,project}]
```
## Subcommands
### init
```
agentic-identity init <handle> [--display-name <name>] [--force] [--scope {global,project}]
```
Set a developer identity manually. `<handle>` must match `^[a-z0-9._-]{1,64}$`.
- `--scope` defaults to `global`, preserving all existing behavior byte-for-byte.
- `--scope global` writes `~/.agentic/identity.yml` (atomic tmp+rename).
- `--scope project` writes `<cwd>/.agentic/identity.yml` (the current repo root;
exits `1` if `cwd` is not inside a git repo). The project file is gitignored
by the existing `.agentic/*` umbrella - it is per-developer only and never
lands in the repo by default.
- If a confirmed identity already exists at the target scope, `--force` is
required to overwrite.
- If the existing identity is provisional, overwrites silently (no `--force`
needed).
- After writing, flushes any pending buffer (see "Provisional model" below)
onto the new handle. For `--scope project`, only pending records whose
`repo_root` matches the current repo are flushed to the project handle;
other repos' buffered sessions remain in the buffer.
- `--display-name` sets an optional human-readable name stored as
`display_name` in the target identity file.
Exit codes: `0` success; `1` invalid handle, missing handle, flush error, or
not in a git repo (project scope); `2` confirmed identity exists without `--force`.
### show
```
agentic-identity show [--scope {global,project,effective}]
```
Print identity information. No writes, always exits `0`.
- `--scope global` (default): prints `~/.agentic/identity.yml`.
- `--scope project`: prints `<cwd>/.agentic/identity.yml`.
- `--scope effective`: resolves and prints the effective identity per the 4-tier
ordering (see "Scope / effective identity resolution" below). Also prints a
`scope:` field indicating which file won (`global` or `project`).
`--scope effective` is available on `show` only; it is rejected with exit `1`
on `init`, `auto`, and `confirm` (structural rejection; those subcommands write
to one explicit scope).
Example output (`--scope effective`, project identity active):
```
developer_id: repo-handle
display_name: Repo Handle
created_at: 2026-06-10T09:00:00Z
scope: project
```
Example output (provisional):
```
developer_id: jane.dev
display_name: Jane Dev
created_at: 2026-06-04T10:00:00Z
provisional: true
```
Example output (confirmed):
```
developer_id: jane.dev
display_name: Jane Dev
created_at: 2026-06-04T10:00:00Z
```
`provisional: true` appears only when the identity is provisional. Prints
"No identity set. Run: agentic-identity init <handle>" when no file exists
at the requested scope.
Exit codes: `0` always.
### auto
```
agentic-identity auto [--force] [--scope {global,project}]
```
Derive a handle automatically from the GitHub CLI and write it as provisional.
`--scope` defaults to `global`. `--scope project` writes to `<cwd>/.agentic/identity.yml`
(exits `1` if not in a git repo).
Steps:
1. Calls `gh api user --jq .login` with a 5-second timeout.
2. Lowercases the result and validates against `^[a-z0-9._-]{1,64}$`.
3. Writes `provisional: true` and `derived_from: gh` to
`~/.agentic/identity.yml` (atomic tmp+rename).
Behavior on edge cases:
- `gh` unavailable or unauthenticated: exits `1` with a hint to run
`gh auth login`.
- Login fails the regex after lowercasing: exits `1` with a hint to use
`agentic-identity init <handle>`.
- A confirmed (non-provisional) identity already exists: exits `2`
(no overwrite without `--force`).
- A provisional identity already exists: overwrites silently (no `--force`
needed).
`--force` bypasses the exit-`2` guard for confirmed identities.
A provisional identity does NOT activate telemetry writes. Session data is
instead buffered at `~/.agentic/session-log/.pending/` until confirmed (see
"Provisional model" below).
Exit codes: `0` success; `1` gh unavailable or invalid handle; `2` confirmed
identity exists without `--force`.
### confirm
```
agentic-identity confirm [--scope {global,project}]
```
Confirm a provisional identity and activate telemetry. `--scope` defaults to
`global`. `--scope project` confirms `<cwd>/.agentic/identity.yml` (exits `1`
if not in a git repo or if no project identity file exists).
Steps:
1. Strips `provisional:` and `derived_from:` from the target identity file
(atomic tmp+rename). The identity is now confirmed.
2. Calls `flushPendingBuffer` - moves buffered pending sessions into the
per-project and global session logs under the confirmed handle (see
"Pending buffer" below). For `--scope project`, only records whose
`repo_root` matches the current repo are attributed to the project handle.
3. Prints "Flushed N pending session(s)".
If the identity is already confirmed, `confirm` is a no-op (exits `0`).
If no identity file exists at the target scope, exits `1`.
Exit codes: `0` success or already confirmed; `1` no identity file or
flush error.
## Provisional model
When no confirmed identity exists, the Stop hook auto-derives a provisional
handle (via `auto`) and buffers session telemetry rather than writing it
to any log. This eliminates the one-session gap that existed before V1.
### Pending buffer
Location: `~/.agentic/session-log/.pending/<session-uuid>.json`
Each session produces one file, written atomically (tmp+rename) by the Stop
hook when the identity is provisional or absent. Format:
```json
{
"schema_version": 1,
"session_uuid": "<uuid>",
"ts": "<ISO8601>",
"project_slug": "<basename>",
"repo_root": "<abs cwd>",
"branch": "<branch>",
"data": {
"wall_seconds": 0,
"tokens": { "input": 0, "output": 0, "cache_creation": 0, "cache_read": 0 },
"spawn_count": 0,
"by_agent": {}
}
}
```
No `developer_id` field - sessions are unattributed until flushed.
`agentic-cost` does NOT read `.pending/` (the glob `*.jsonl` never reaches
the `.pending/` subdirectory).
Buffer cap: 100 files. When the cap is exceeded, the oldest file (by `ts`)
is dropped and a one-line notice is printed to stderr.
### Flush (`flushPendingBuffer`)
Called by both `confirm` and `init`. Race-safe: acquires an exclusive
`fcntl.flock` on `~/.agentic/session-log/.flush.lock` for the entire flush
loop (blocking acquire, 30-second timeout; on timeout prints a stderr warning
and exits cleanly with the buffer intact for the next run).
For each `.pending/*.json` record:
1. **Dedup** - scans the global `<dev>.jsonl` for a matching `session_uuid`.
If found, unlinks the pending file and skips.
2. **Attribution** - builds an attributed log line (all pending fields plus
`developer_id`; original `ts` preserved).
3. **Per-project write** - validates `repo_root` via
`git -C <repo_root> rev-parse --show-toplevel` (3-second timeout) and
checks that `basename(toplevel) == project_slug`. On success, appends to
`<repo_root>/.agentic/session-log/<dev>.jsonl` (mkdir -p). On mismatch or
failure, skips per-project with a one-line stderr warning.
4. **Global write** - always appends to
`~/.agentic/session-log/<dev>.jsonl` (mkdir -p).
5. **Cleanup** - unlinks the pending file only after all attempted appends
succeed. A global-write failure leaves the file for a future retry.
### First-session confirmation prompt
At the first user turn of a new session, the conductor surfaces a non-blocking
notice when a provisional identity is detected:
```
Tracking handle '<handle>' was auto-derived (provisional). Telemetry is
paused until you confirm. To confirm: agentic-identity confirm
To use a different handle: agentic-identity init <handle> --force
```
Telemetry continues to buffer (not lost). The prompt re-surfaces each session
until confirmed. CI/headless sessions never reach a user turn, so they stay
deferred and buffered automatically.
## Scope / effective identity resolution
A project-local identity file at `<repo>/.agentic/identity.yml` lets a developer
use a different handle for sessions in that repo without changing their global
default. The file is gitignored by the existing `.agentic/*` umbrella; it is
per-developer and never lands in the repo by default.
### 4-tier ordering
When the preflight, Stop hook, or `show --scope effective` resolves identity,
it applies this total ordering (higher tier wins):
| Tier | File | State |
|---|---|---|
| 1 (highest) | `<cwd>/.agentic/identity.yml` | confirmed (no `provisional: true`) |
| 2 | `~/.agentic/identity.yml` | confirmed |
| 3 | `<cwd>/.agentic/identity.yml` | provisional |
| 4 (lowest) | `~/.agentic/identity.yml` | provisional |
| none | neither file exists | - |
Key rules:
- A **confirmed global identity is not suppressed** by a provisional project
file. Tier 2 beats Tier 3.
- A confirmed project identity beats a confirmed global (Tier 1 > Tier 2).
- `--scope project` requires the `cwd` to be inside a git repo; exits `1` if not.
### `agentic-cost` and two-handle attribution
A developer who uses a `--scope project` handle in repo A and their global handle
everywhere else will appear as **two separate rows** in `agentic-cost team` and
`agentic-cost operator` output - one row per distinct `developer_id`. This is
intentional: each handle is an independent identity. Cross-handle rollup is not
provided automatically; aggregate manually if needed.
## Identity schema
Files: `~/.agentic/identity.yml` (global) and optionally `<cwd>/.agentic/identity.yml`
(project-local, gitignored). Both files use the same schema.
| Field | Required | Notes |
|---|---|---|
| `developer_id` | yes | Validated handle `^[a-z0-9._-]{1,64}$` |
| `display_name` | no | Optional human-readable name |
| `created_at` | yes | ISO8601 UTC timestamp |
| `provisional` | no | Present and `true` only when auto-derived; absent means confirmed |
| `derived_from` | no | Source of the auto-derived handle; `gh` when set by `auto` |
**Back-compat:** An identity written by `agentic-identity init` before V1
has no `provisional` key. Absent `provisional` is treated as confirmed
(`provisional === false`). Existing manually-created identities need zero
migration and continue to work without change.
## Relationship to tracking
| Identity state | Telemetry destination |
|---|---|
| Confirmed | Per-project `.agentic/session-log/<dev>.jsonl` + global `~/.agentic/session-log/<dev>.jsonl` |
| Provisional | `~/.agentic/session-log/.pending/<uuid>.json` (buffered; flushed on confirm/init) |
| None | Same as provisional; Stop hook also appends an identity nudge to this session's `.agentic/context.d/` shard, which the derived `.agentic/context.md` rollup then carries |
- `agentic-cost team` reads `.agentic/session-log/` (project-local) - aggregates
all confirmed developer files for the current repo.
- `agentic-cost operator` reads `~/.agentic/session-log/*.jsonl` (global) -
cross-repo rollup for the operator across all projects.
- Pending buffer is invisible to both commands until flushed.
## Exit codes (summary)
| Code | Meaning |
|---|---|
| `0` | Success |
| `1` | Error: invalid/missing handle, `gh` unavailable, no identity file, flush error |
| `2` | A confirmed identity already exists; re-run with `--force` to overwrite |