tga 10.2.0

Developer productivity analytics — git commit collection, classification, and reporting
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
# Configuration Reference

`tga` reads a YAML configuration file (`config.yaml` by default) that controls which
repositories to analyze, which external integrations to connect, and how classification
and reporting behave.

---

## Table of Contents

1. [File Location and Loading](#1-file-location-and-loading)
2. [Environment Variable Fallbacks](#2-environment-variable-fallbacks)
3. [Annotated Example Config](#3-annotated-example-config)
4. [Section Reference](#4-section-reference)
   - [database](#database)
   - [repositories](#repositories)
   - [output](#output)
   - [classification](#classification)
   - [github](#github)
   - [bitbucket](#bitbucket)
   - [jira](#jira)
   - [linear](#linear)
   - [pm.azure_devops](#pmazure_devops)
   - [team](#team)
   - [developer_aliases](#developer_aliases)
   - [cache](#cache)
5. [Special Notes](#5-special-notes)
6. [Validation Rules](#6-validation-rules)
7. [Multiple Repositories Example](#7-multiple-repositories-example)
8. [Full LLM Config Example](#8-full-llm-config-example)

---

## 1. File Location and Loading

By default `tga` looks for `config.yaml` in the current working directory. Use `--config`
to specify a different path:

```bash
tga analyze --config /etc/tga/production.yaml
```

Unknown YAML keys are silently ignored. This means a config file written for a newer
version of `tga` (or for the Python `gitflow-analytics` predecessor) loads without error
in older binaries.

### Database path resolution

`tga` uses a SQLite database (default: `tga.db`). Database path precedence:

1. `--database` CLI flag (highest priority).
2. `database:` key in the YAML config file.
3. Default `tga.db` (lowest priority).

**Important — relative path anchoring:** relative paths in options 2 and 3 are always
resolved relative to the **config file's directory**, not to the process working
directory. This is intentional: cron jobs and launchd services often run with cwd
set to `/` or some unrelated directory, so cwd-relative resolution would silently
open or create a ghost database at the wrong path.

Absolute paths (starting with `/`) and tilde-prefixed paths (`~/…`) are passed
through unchanged.

For production cron/launchd deployments, using an absolute path is the safest option:

```yaml
database: /var/data/tga.db
```

The `--database` CLI flag is resolved as-is (the shell expands `~` and relative paths
before passing them to `tga`).

---

## 2. Environment Variable Fallbacks

The following config fields can be omitted when the corresponding environment variable
is set. The environment variable takes precedence over the config file value.

| Config field | Environment variable |
|---|---|
| `github.token` | `GITHUB_TOKEN` |
| `bitbucket.app_password` | `BITBUCKET_APP_PASSWORD` |
| `bitbucket.token` | `BITBUCKET_TOKEN` |
| `classification.openrouter_api_key` | `OPENROUTER_API_KEY` |
| Logging verbosity | `RUST_LOG` (overrides `--log` and `-v`) |

Note: `tga` does not perform `${VAR}` interpolation inside YAML values. Either set the
field to the literal value, or rely on the environment variable fallback (where supported).

---

## 3. Annotated Example Config

```yaml
# ── Repositories (required) ─────────────────────────────────────────────────
repositories:
  - path: ~/code/backend-api      # Local git clone path; ~ is expanded
    name: backend-api             # Display name in reports (default: directory basename)
    branch: main                  # Branch to walk (default: auto-detect default branch)
    since_date: "2025-01-01"      # Ignore commits before this date (ISO 8601)
    until_date: "2025-12-31"      # Ignore commits after this date (ISO 8601)
    org: my-github-org            # GitHub org/owner for PR correlation

# ── Output ──────────────────────────────────────────────────────────────────
output:
  directory: ./reports            # Where report files are written; ~ is expanded
  formats:                        # Formats to produce
    - csv
    - json
    - markdown
  include_unclassified: false     # Include unclassified commits in output
  include_merges: false           # Include merge commits in output
  include_files: false            # Include file-level rows in output

# ── Classification ───────────────────────────────────────────────────────────
classification:
  rules_file: ./my-rules.yaml     # Custom rules YAML; extend or replace built-ins
  use_llm: false                  # Enable LLM fallback tier
  llm_model: "anthropic/claude-3.5-haiku"  # Model to use for LLM classification
  llm_provider: auto              # openrouter | openai | bedrock | auto
  openrouter_api_key: ""          # Or set OPENROUTER_API_KEY env var
  confidence_threshold: 0.7       # Minimum confidence to accept a result [0, 1]
  min_coverage_pct: 20.0          # Warn if fewer than this % of commits classified [0, 100]
  llm_fallback_threshold: 0.0     # Commits with confidence above this skip LLM entirely
  llm_fallback_concurrency: 8     # Max concurrent LLM requests during fallback pass

# ── GitHub ──────────────────────────────────────────────────────────────────
github:
  token: ""                       # Or set GITHUB_TOKEN env var
  org: my-github-org              # GitHub organization/owner
  repo: my-repo                   # Repository name (if single-repo focus)
  fetch_prs: false                # Fetch PR metadata from GitHub API
  ticket_regex: ""                # Custom regex for ticket detection; capture group 1 = ID

# ── Bitbucket ───────────────────────────────────────────────────────────────
bitbucket:
  username: ""                    # Bitbucket account username (Basic auth)
  app_password: ""                # Or set BITBUCKET_APP_PASSWORD env var
  token: ""                       # Or set BITBUCKET_TOKEN env var (Bearer; takes precedence)
  workspace: ""                   # Required when fetch_prs: true
  repo_slug: ""                   # Required when fetch_prs: true
  fetch_prs: false
  api_base_url: "https://api.bitbucket.org/2.0"

# ── JIRA ────────────────────────────────────────────────────────────────────
jira:
  url: https://company.atlassian.net
  username: user@company.com
  token: ""                       # JIRA API token
  project_key: ENG                # Filter JIRA issues by project key
  ticket_regex: ""                # Custom regex; capture group 1 = ticket ID

# ── Linear ──────────────────────────────────────────────────────────────────
linear:
  api_key: ""
  team_keys:                      # Linear team identifiers to fetch
    - ENG
    - FE
  fetch_on_reference: true        # Fetch issue details when ref appears in a commit
  ticket_regex: ""                # Custom regex; capture group 1 = ticket ID

# ── Azure DevOps ─────────────────────────────────────────────────────────────
pm:
  azure_devops:
    organization_url: https://dev.azure.com/myorg   # Required
    pat: ""                       # Azure DevOps Personal Access Token; required
    project: MyProject            # Default ADO project (or use `projects` list)
    projects:                     # List of ADO projects (alternative to single `project`)
      - MyProject
      - InfraTeam
    ticket_regex: '(?i)\bAB#(\d+)\b'   # Default; change capture group 1 = work item ID
    team_keys: []                 # ADO team keys to scope fetching
    fetch_on_reference: true      # Fetch work items when AB#N appears in commits
    fetch_prs: false              # Fetch ADO pull requests and reviewer data

# ── Team members ─────────────────────────────────────────────────────────────
team:
  members:
    - name: Alice Park
      email: alice.park@company.com
      aliases:
        - aparks@gmail.com
        - alice-park-github
  aliases:                        # Free-form alias → canonical name map
    "aparks": "Alice Park"

# ── Developer aliases (alternative format) ───────────────────────────────────
# Use developer_aliases OR team.members, not both.
developer_aliases:
  "John Doe":
    - "john.doe@company.com"
    - "jdoe@gmail.com"

# ── External aliases file ────────────────────────────────────────────────────
# Mutually exclusive with inline developer_aliases above.
# aliases_file: ~/config/tga-aliases.yaml

# ── Cache ────────────────────────────────────────────────────────────────────
cache:
  directory: ~/.tga-cache         # ~ is expanded
```

---

## 4. Section Reference

### database

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `database` | PathBuf | no | `tga.db` (config dir) | Path to the SQLite database. Supports `~`. Relative paths anchor to the config file's directory. |

See [Database path resolution](#database-path-resolution) above for the full
precedence rules and anchoring behaviour.

### repositories

Required. A list of one or more repository entries.

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `path` | PathBuf | yes | — | Filesystem path to the git repo. Supports `~` expansion. |
| `name` | String | no | Directory basename | Display name used in reports and DB records. |
| `branch` | String | no | Auto-detect | Branch to walk. If omitted, `tga` uses the repo's current HEAD branch. |
| `since_date` | String | no | — | Skip commits before this date (ISO 8601: `YYYY-MM-DD`). |
| `until_date` | String | no | — | Skip commits after this date (ISO 8601: `YYYY-MM-DD`). |
| `org` | String | no | — | GitHub org or owner for correlating commits to PRs. |

`since_date` / `until_date` in the repository config set a permanent floor/ceiling for
that repo. For per-run date windowing, use the CLI flags `--weeks`, `--from`, `--to`.

### output

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `directory` | PathBuf | no | `./reports` | Output directory. Alias: `output_path`. Supports `~`. |
| `formats` | Vec\<String\> | no | `[csv, json, markdown]` | Formats to produce. Valid values: `csv`, `json`, `markdown`. |
| `include_unclassified` | bool | no | `false` | When `true`, commits that were not classified appear in output. |
| `include_merges` | bool | no | `false` | When `true`, merge commits are included in output and metrics. |
| `include_files` | bool | no | `false` | When `true`, file-level change rows are included in output. |

### classification

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `rules_file` | PathBuf | no | — | Path to a custom rules YAML file. Supports `~`. |
| `use_llm` | bool | no | `false` | Enable the LLM fallback tier (Tier 3). |
| `llm_model` | String | no | Provider-specific default | LLM model identifier (e.g. `anthropic/claude-3.5-haiku` for OpenRouter). |
| `llm_provider` | String | no | `auto` | LLM provider: `openrouter`, `openai`, `bedrock`, or `auto`. `auto` prefers OpenRouter when `OPENROUTER_API_KEY` is set. |
| `openrouter_api_key` | String | no | `$OPENROUTER_API_KEY` | OpenRouter API key. Env var takes precedence. |
| `confidence_threshold` | f64 | no | `0.7` | Minimum confidence score [0, 1] to accept a classification result. |
| `custom_categories` | Vec | no | — | Additional subcategory definitions. |
| `min_coverage_pct` | f64 | no | `20.0` | Emit a warning in the report when fewer than this percentage of commits are classified [0, 100]. |
| `llm_fallback_threshold` | f64 | no | `0.0` | Commits with a rule-based confidence above this value skip the LLM tier entirely. Set to e.g. `0.5` to avoid sending already-confident results to the LLM. |
| `llm_fallback_concurrency` | usize | no | `8` | Maximum concurrent LLM requests during the fallback pass. |
| `llm_fallback_scope` | enum | no | `low_confidence` | Which verdicts reach the LLM: `low_confidence` (confidence at or below `llm_fallback_threshold`) or `unanswered` (only commits the rules left uncategorized; the threshold is ignored). Merge commits are never sent to the LLM, under either scope. #111 |

With `extend_defaults: false` in the last rules file, the LLM prompt offers only the rules' categories (plus any listed under a top-level `categories:` key with an optional `description`) and an `unclear` abstain option; a reply outside that set is dropped, never stored (#131). The `llm:` section also takes `effort: low|medium|high|xhigh|max` (Anthropic `output_config.effort`, `anthropic-api` only; leave unset for Haiku). Every LLM call is recorded in the `llm_usage` table with its input/output tokens, and `tga classify` prints the totals.

### github

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `token` | String | no | `$GITHUB_TOKEN` | GitHub Personal Access Token. Required for PR fetching. |
| `org` | String | no | — | GitHub organization or owner. |
| `repo` | String | no | — | Repository name for single-repo configurations. |
| `fetch_prs` | bool | no | `false` | When `true`, fetch pull request metadata from the GitHub API during `collect`. |
| `ticket_regex` | String | no | — | Custom regex for GitHub ticket detection in commit messages. Capture group 1 must contain the ticket ID. |

### bitbucket

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `username` | String | no | — | Bitbucket account username (required for Basic auth). |
| `app_password` | String | no | `$BITBUCKET_APP_PASSWORD` | Bitbucket App Password (Basic auth). |
| `token` | String | no | `$BITBUCKET_TOKEN` | Workspace or repository access token (Bearer auth). Takes precedence over `app_password`. |
| `workspace` | String | conditional | — | Workspace slug. Required when `fetch_prs: true` and `workspaces` is empty. |
| `workspaces` | Vec\<String\> | conditional | — | Workspace slugs whose repository list is discovered over `GET /2.0/repositories/{workspace}` (#5220). An alternative to the `workspace`/`repo_slug` pair; discovered repositories are unioned with it. |
| `repo_slug` | String | conditional | — | Repository slug. Required when `fetch_prs: true` and `workspaces` is empty. |
| `fetch_prs` | bool | no | `false` | Fetch PR metadata from Bitbucket API. |
| `api_base_url` | String | no | `https://api.bitbucket.org/2.0` | API base URL override. |

### jira

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `url` | String | conditional | — | JIRA instance base URL (e.g. `https://company.atlassian.net`). Required when using JIRA integration. |
| `username` | String | no | — | JIRA account email. |
| `token` | String | no | — | JIRA API token. |
| `project_key` | String | no | — | Filter JIRA issues to this project key. |
| `jira_project_mappings` | HashMap | no | — | Map of project key → work type for classification hints. |
| `ticket_regex` | String | no | — | Custom regex. Capture group 1 = ticket ID. |

### linear

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `api_key` | String | no | — | Linear API key. |
| `team_keys` | Vec\<String\> | no | — | Linear team identifiers to scope issue fetching. |
| `fetch_on_reference` | bool | no | `true` | When `true`, fetch issue details from Linear whenever a ticket reference is detected in a commit message. |
| `ticket_regex` | String | no | — | Custom regex. Capture group 1 = ticket ID. |

### pm.azure_devops

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `organization_url` | String | yes | — | ADO organization URL (e.g. `https://dev.azure.com/myorg`). |
| `pat` | String | yes | — | Azure DevOps Personal Access Token. |
| `project` | String | conditional | — | Default ADO project. Either `project` or `projects` must be provided. |
| `projects` | Vec\<String\> | conditional | — | List of ADO projects. Alternative to `project`. |
| `ticket_regex` | String | no | `(?i)\bAB#(\d+)\b` | Regex for detecting ADO work item references. Capture group 1 = work item ID. |
| `team_keys` | Vec\<String\> | no | — | ADO team keys for scoping. |
| `fetch_on_reference` | bool | no | `true` | Fetch work item details when an `AB#N` reference is detected. |
| `fetch_prs` | bool | no | `false` | Fetch ADO pull requests and reviewer votes. |

### team

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `members` | Vec | no | — | List of team members with explicit identity mappings. |
| `members[].name` | String | yes | — | Canonical display name. |
| `members[].email` | String | yes | — | Primary email address. |
| `members[].aliases` | Vec\<String\> | no | — | Additional emails, GitHub handles, or other identifiers. |
| `aliases` | HashMap | no | — | Free-form alias-to-canonical-name lookup. |

### developer_aliases

Alternative to `team.members`. Maps canonical name to a list of email aliases:

```yaml
developer_aliases:
  "John Doe":
    - "john.doe@company.com"
    - "jdoe@gmail.com"
```

The first email-shaped entry in each list is used as the canonical email.

Use either `developer_aliases` or `team.members`, not both. For teams with more than
20 developers, prefer the external file approach:

```yaml
aliases_file: ~/config/tga-aliases.yaml
```

### cache

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `directory` | PathBuf | no | `~/.tga-cache` | Cache directory. Supports `~` expansion. |

---

## 5. Special Notes

### Path expansion

`~` is expanded to the current user's home directory in the following fields:
`repositories[].path`, `aliases_file`, `cache.directory`, `output.directory`.

### ticket_regex capture group

When setting a custom `ticket_regex` for any integration (GitHub, JIRA, Linear, ADO),
the regex must contain a capture group 1 that matches the ticket identifier. For example:

```yaml
# Correct: capture group 1 captures "ENG-123"
ticket_regex: '([A-Z]+-\d+)'

# Correct: ADO default pattern
ticket_regex: '(?i)\bAB#(\d+)\b'

# Incorrect: no capture group
ticket_regex: '[A-Z]+-\d+'
```

### LLM provider auto-detection

When `llm_provider: auto` (the default), `tga` selects a provider at runtime:
- If `OPENROUTER_API_KEY` is set (or `openrouter_api_key` is in config): use OpenRouter
- Otherwise: use OpenAI with `OPENAI_API_KEY`

Set `llm_provider` explicitly to `openrouter`, `openai`, or `bedrock` to override this.

---

## 6. Validation Rules

`tga` validates the following at startup and will refuse to run if these are violated:

| Rule | Condition |
|---|---|
| `confidence_threshold` | Must be in the range [0, 1] |
| `min_coverage_pct` | Must be in the range [0, 100] |
| `pm.azure_devops` (if present) | Either `project` or `projects` must be non-empty |
| `bitbucket` (if `fetch_prs: true`) | Both `workspace` and `repo_slug` must be set |
| `repositories` | Must contain at least one entry |

---

## 7. Multiple Repositories Example

```yaml
repositories:
  - path: ~/code/backend-api
    name: backend-api
    branch: main
    org: acme-corp

  - path: ~/code/frontend-app
    name: frontend-app
    branch: main
    org: acme-corp

  - path: ~/code/infra
    name: infra
    branch: master
    org: acme-corp

github:
  token: ""          # Set GITHUB_TOKEN
  org: acme-corp
  fetch_prs: true

output:
  directory: ./reports
  formats: [csv, json, markdown]
```

Each repository produces its own per-repo metrics while also contributing to the
aggregated cross-repo summaries.

---

## 8. Full LLM Config Example with OpenRouter

```yaml
repositories:
  - path: ~/code/my-service
    name: my-service

classification:
  rules_file: ./rules.yaml
  use_llm: true
  llm_provider: openrouter
  llm_model: "anthropic/claude-3.5-haiku"
  openrouter_api_key: ""   # Or set OPENROUTER_API_KEY env var
  confidence_threshold: 0.7
  min_coverage_pct: 20.0
  # Commits already classified with confidence > 0.5 skip the LLM entirely.
  # This avoids unnecessary API calls for well-matched rule results.
  llm_fallback_threshold: 0.5
  # Allow up to 8 concurrent LLM requests to speed up the fallback pass.
  llm_fallback_concurrency: 8

output:
  directory: ./reports
  formats: [csv, json, markdown]
```

When `llm_provider: auto` and `OPENROUTER_API_KEY` is set in the environment, `tga`
will also use OpenRouter without requiring explicit `llm_provider` in the config.