Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
968f682
fix(moonshot): say consensus changes were auto-applied (not "review t…
readwrightexecute Jun 24, 2026
31cb14b
fix(cold-start): show a loading-model graphic during model switch / c…
readwrightexecute Jun 24, 2026
2af54ee
chore: rename INCIPIT_ env-var prefix (was PROMPTGEN_)
readwrightexecute Jun 25, 2026
1e9de67
test: add mocked-HTTP end-to-end tests for repo fetch
readwrightexecute Jun 25, 2026
5f9a564
feat: add GitHub OAuth login with server-side token store
readwrightexecute Jun 25, 2026
d37736e
feat: GitHub private-repo multi-select and grounding
readwrightexecute Jun 25, 2026
43a6d3f
feat: add Atlassian (Jira) OAuth 2.0 / 3LO "Sign in with Atlassian"
readwrightexecute Jun 25, 2026
d157bf3
feat: export the mega-prompt to Jira over REST (ADF + .md attachment)
readwrightexecute Jun 25, 2026
054c810
chore: add CI workflow + scoped coverage gate + env-var reference
readwrightexecute Jun 25, 2026
2f9395e
merge: fix/repo-fetch-e2e-tests into deploy/dev-integration
readwrightexecute Jun 25, 2026
8881f41
merge: feat/github-oauth-login into deploy/dev-integration
readwrightexecute Jun 25, 2026
8215ef6
merge: feat/github-private-repos-select into deploy/dev-integration
readwrightexecute Jun 25, 2026
3716b0e
merge: feat/jira-oauth-login into deploy/dev-integration
readwrightexecute Jun 25, 2026
7092209
merge: feat/jira-oauth-export into deploy/dev-integration
readwrightexecute Jun 25, 2026
a932c63
merge: chore/ci-coverage into deploy/dev-integration
readwrightexecute Jun 25, 2026
6053d3c
fix(auth): use SameSite=Lax for session cookie so OAuth callbacks work
readwrightexecute Jun 25, 2026
5e563a3
Merge branch 'fix/cold-start-loading' into deploy/dev-integration
readwrightexecute Jul 14, 2026
1528f22
Merge branch 'fix/moonshot-auto-applied-message' into deploy/dev-inte…
readwrightexecute Jul 14, 2026
468df8e
Merge remote-tracking branch 'origin/master' into deploy/dev-integration
readwrightexecute Jul 14, 2026
2e852d2
Merge origin/master into deploy/dev-integration
readwrightexecute Jul 14, 2026
80ba763
fix: harden OAuth and Jira integrations
readwrightexecute Jul 15, 2026
2822dac
fix: preserve wizard when loading GitHub repos
readwrightexecute Jul 29, 2026
3fe8a51
Merge pull request #13 from readwrightexecute/fix/github-repo-picker-…
readwrightexecute Jul 29, 2026
c6847aa
fix: recover Atlassian OAuth callback failures
readwrightexecute Jul 29, 2026
bc79b5e
Merge pull request #14 from readwrightexecute/fix/atlassian-toolbar-c…
readwrightexecute Jul 29, 2026
04a222c
Merge remote-tracking branch 'origin/deploy/dev-integration' into dep…
readwrightexecute Aug 3, 2026
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
53 changes: 37 additions & 16 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,55 @@
# Everything here is optional; defaults target a local Ollama install.

# Backend: openai (default) | diffusion-cnv | diffusion-oneshot
PROMPTGEN_BACKEND=openai
INCIPIT_BACKEND=openai

# --- OpenAI-compatible endpoint (the default backend) ---
# Ollama: http://localhost:11434/v1
# LM Studio: http://localhost:1234/v1
# llama-server: http://localhost:8080/v1
# OpenAI: https://api.openai.com/v1
PROMPTGEN_OPENAI_BASE_URL=http://localhost:11434/v1
PROMPTGEN_OPENAI_MODEL=
PROMPTGEN_OPENAI_API_KEY=
INCIPIT_OPENAI_BASE_URL=http://localhost:11434/v1
INCIPIT_OPENAI_MODEL=
INCIPIT_OPENAI_API_KEY=
# Shared Doppler/Open WebUI aliases are used only when the corresponding
# INCIPIT_OPENAI_* value is absent. A WebUI origin is normalized to /api.
# WEBUI_API_URL=https://webui.example.com
# WEBUI_MODEL=
# WEBUI_API_KEY=
# Send chat_template_kwargs.enable_thinking=false (Qwen/llama.cpp reasoning
# models only; OpenAI proper rejects it). Leave unset/false for portability.
PROMPTGEN_DISABLE_THINKING=
INCIPIT_DISABLE_THINKING=

# Where the in-UI settings are persisted (overrides the above once saved).
# PROMPTGEN_SETTINGS_FILE=.promptgen.json
# INCIPIT_SETTINGS_FILE=.promptgen.json
# Additional hosts allowed in the runtime settings base URL. Defaults allow
# localhost, api.openai.com, and the host from PROMPTGEN_OPENAI_BASE_URL.
# PROMPTGEN_ALLOWED_BASE_URL_HOSTS=llm.internal.example.com
# localhost, api.openai.com, and the host from INCIPIT_OPENAI_BASE_URL.
# INCIPIT_ALLOWED_BASE_URL_HOSTS=llm.internal.example.com

# Timeouts (seconds)
# PROMPTGEN_GEN_TIMEOUT=300
# PROMPTGEN_SESSION_TTL=86400
# INCIPIT_GEN_TIMEOUT=300
# INCIPIT_SESSION_TTL=86400

# --- Optional: Login with GitHub (private-repo grounding) ---
# Per-user OAuth grant so the wizard can read your private repos. The access
# token is stored server-side only; the browser cookie carries just a signed,
# opaque session id. Leave the client id/secret blank to hide the login button.
# The CLIENT_ID below is the public, registered OAuth-app id (not a secret).
# INCIPIT_GITHUB_OAUTH_CLIENT_ID=Ov23liQTAZncU8NnfMS4
# INCIPIT_GITHUB_OAUTH_CLIENT_SECRET= # SECRET — set via env/Doppler, never commit
# INCIPIT_GITHUB_OAUTH_REDIRECT_URL=https://incipit.nexus.inmotionhosting.com/auth/github/callback
# INCIPIT_GITHUB_OAUTH_SCOPES=repo
# Secret used to sign the session-id cookie. Set it so auth cookies survive a
# restart; if unset an ephemeral per-process secret is generated.
# INCIPIT_SESSION_COOKIE_SECRET=
# Set the Secure flag on auth cookies (HTTPS only). Default true; set false for
# local plain-HTTP development.
# INCIPIT_COOKIE_SECURE=true

# --- Advanced: diffusion backend (homelab / GPU only — see README) ---
# PROMPTGEN_CLI_BIN=/usr/local/bin/llama-diffusion-cli
# PROMPTGEN_MODEL=/models/diffusiongemma-26B-A4B-it-GGUF/diffusiongemma-26B-A4B-it-Q4_K_M.gguf
# PROMPTGEN_NGL=99
# PROMPTGEN_N_CPU_MOE=18
# PROMPTGEN_THREADS=8
# PROMPTGEN_IDLE_TIMEOUT=600
# INCIPIT_CLI_BIN=/usr/local/bin/llama-diffusion-cli
# INCIPIT_MODEL=/models/diffusiongemma-26B-A4B-it-GGUF/diffusiongemma-26B-A4B-it-Q4_K_M.gguf
# INCIPIT_NGL=99
# INCIPIT_N_CPU_MOE=18
# INCIPIT_THREADS=8
# INCIPIT_IDLE_TIMEOUT=600
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI

on:
push:
branches: ["**"]
pull_request:

jobs:
test:
name: pytest + coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
cache-dependency-path: |
requirements.txt
requirements-dev.txt

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt

- name: Run tests with coverage
# Coverage scope + 90% gate are configured in pytest.ini addopts; the
# suite is fully offline (no network, no model/subprocess).
run: pytest -q
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
__pycache__/
*.pyc
.promptgen.json
.incipit.json
.env
.venv/
.pytest_cache/
.coverage
.coverage.*
htmlcov/
.opencodereview/last-review.txt
# Claude Code authoring tooling — general skill, not part of the app. Kept on
# disk (so it still works locally) but not tracked; it dominated every diff.
Expand Down
2 changes: 1 addition & 1 deletion .opencodereview/rule.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
},
{
"path": "app/llm/**/*.py",
"rule": "Backends sit behind the LLMBackend Protocol (base.py); get_backend() selects by PROMPTGEN_BACKEND and imports lazily. The openai path must pull in NO diffusion/GPU code. enable_thinking=false is only sent when the disable_thinking setting is on. Flag cross-backend imports or eager diffusion imports on the openai path."
"rule": "Backends sit behind the LLMBackend Protocol (base.py); get_backend() selects by INCIPIT_BACKEND and imports lazily. The openai path must pull in NO diffusion/GPU code. enable_thinking=false is only sent when the disable_thinking setting is on. Flag cross-backend imports or eager diffusion imports on the openai path."
}
]
}
12 changes: 6 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ There is no test suite, linter, or build step for the Python app. The fast dev
loop avoids spawning the GPU model by pointing at an OpenAI-compatible endpoint:

```bash
PROMPTGEN_BACKEND=openai \
PROMPTGEN_OPENAI_BASE_URL=http://<llm-server>:<port>/v1 \
PROMPTGEN_OPENAI_API_KEY=<key> \
INCIPIT_BACKEND=openai \
INCIPIT_OPENAI_BASE_URL=http://<llm-server>:<port>/v1 \
INCIPIT_OPENAI_API_KEY=<key> \
python3 -m uvicorn app.main:app --port 8911
```

All configuration is environment variables (`PROMPTGEN_*`) read in
All configuration is environment variables (`INCIPIT_*`) read in
`app/config.py` — there is no config file. Container CMD runs uvicorn on
`:8000`.

Expand Down Expand Up @@ -66,7 +66,7 @@ Request/orchestration flow is fully async and event-driven:
open connection** (`subscribers` list) — a shared queue would split events
between stale and live tabs.
- **`app/llm/`** — backend abstraction behind the `LLMBackend` Protocol
(`base.py`). `get_backend()` selects by `PROMPTGEN_BACKEND`. Everything above
(`base.py`). `get_backend()` selects by `INCIPIT_BACKEND`. Everything above
this boundary is backend-agnostic.

### Wizard phases
Expand All @@ -89,7 +89,7 @@ To change what the spec contains, edit the YAML — not the code. Prompt wording
lives in `app/wizard/prompts/*.md.j2`; the system prompt is loaded once at
import (`flow.SYSTEM`).

### Backends (`PROMPTGEN_BACKEND`)
### Backends (`INCIPIT_BACKEND`)

- **`openai`** (default, `app/llm/openai_compat.py`) — any OpenAI-compatible
endpoint. Reads endpoint/model/key from `app/settings.py` (the runtime
Expand Down
127 changes: 119 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ Example endpoints (set the base URL in the settings panel):
| OpenAI | `https://api.openai.com/v1` | required |

Runtime endpoint changes are restricted to localhost, `api.openai.com`, and the
host from `PROMPTGEN_OPENAI_BASE_URL` by default. For another trusted host, set
`PROMPTGEN_ALLOWED_BASE_URL_HOSTS=host.example.com` before starting the app.
host from `INCIPIT_OPENAI_BASE_URL` by default. For another trusted host, set
`INCIPIT_ALLOWED_BASE_URL_HOSTS=host.example.com` before starting the app.

> **"Disable thinking" toggle:** local reasoning models (Qwen, etc.) can burn the
> whole token budget on a hidden think channel and return empty content. Turning
Expand Down Expand Up @@ -90,15 +90,126 @@ wording lives in `app/wizard/prompts/*.md.j2`.

## Configuration

All config is environment variables (`PROMPTGEN_*`) — see [`.env.example`](.env.example).
All config is environment variables (`INCIPIT_*`) — see [`.env.example`](.env.example).
A local `.env` is auto-loaded if present. Anything you save in the **⚙ Model
settings** panel is written to `.promptgen.json` (gitignored) and takes precedence
on the next run, so you configure your endpoint once.

There is **no authentication** — run it on localhost or a trusted network only.
The app has no login of its own — run it on localhost or a trusted network. The
optional **"Login with GitHub"** flow (see below) is a per-user OAuth grant used
only to read your private repos for grounding; it does not gate the app.

There's no test suite or build step for the app itself. For a fast dev loop,
point it at any running endpoint and run `uvicorn` as above.
### Optional: Login with GitHub (private-repo grounding)

For existing-codebase specs you can sign in with GitHub so the wizard can read
your **private** repos. The user's access token is stored **server-side only**
(in-memory, `app/auth.py`); the browser cookie carries just a signed, opaque
session id (`HttpOnly` + `Secure` + `SameSite=Lax`). Configure the OAuth app:

| Env var | What |
|---|---|
| `INCIPIT_GITHUB_OAUTH_CLIENT_ID` | OAuth app client id (public; a registered default is built in) |
| `INCIPIT_GITHUB_OAUTH_CLIENT_SECRET` | OAuth app client secret — **secret**, set via env/Doppler, never commit |
| `INCIPIT_GITHUB_OAUTH_REDIRECT_URL` | Callback URL registered on the OAuth app (`…/auth/github/callback`) |
| `INCIPIT_GITHUB_OAUTH_SCOPES` | Requested scopes (default `repo`) |
| `INCIPIT_SESSION_COOKIE_SECRET` | Secret used to sign the session cookie (set it so cookies survive restarts) |
| `INCIPIT_COOKIE_SECURE` | Set the cookie `Secure` flag (default `true`; set `false` for local plain HTTP) |

Token issuance/revocation is recorded on the `promptgen.audit` logger (no
tokens are ever logged). Leave the client id/secret blank to disable the button.

### Optional: Sign in with Atlassian (Jira export)

On the final step you can **"Sign in with Atlassian"** (OAuth 2.0 / 3LO) and
push the assembled mega-prompt straight into a Jira issue: pick a **Project** +
**Issue type**, hit **Export to Jira**, and you get back the issue key and a
clickable link. The brief is sent as a pretty **ADF** description and the raw
`.md` is also attached. Each user authorizes their **own** Jira site — there is
no shared/admin token. Access **and** refresh tokens plus the resolved
`cloudId`/site live **server-side only**; the cookie still carries just the
opaque signed session id, and the token is auto-refreshed before it lapses.

| Env var | What |
|---|---|
| `INCIPIT_ATLASSIAN_OAUTH_CLIENT_ID` | Atlassian OAuth app client id (public; a registered default is built in) |
| `INCIPIT_ATLASSIAN_OAUTH_CLIENT_SECRET` | Atlassian OAuth app client secret — **secret**, set via env/Doppler, never commit |
| `INCIPIT_ATLASSIAN_OAUTH_REDIRECT_URL` | Callback URL registered on the app (`…/auth/atlassian/callback`) |
| `INCIPIT_ATLASSIAN_OAUTH_SCOPES` | Console scopes (default `read:jira-work write:jira-work read:jira-user`); `offline_access` is appended at request time so a refresh token is issued |
| `INCIPIT_JIRA_ISSUE_TYPES` | Comma-separated issue types for the dropdown (default `Task,Story,Bug`) |
| `INCIPIT_JIRA_DEFAULT_PROJECT_KEY` | Optional project key to pre-select |
| `INCIPIT_JIRA_EXPORT_TIMEOUT` | End-to-end export budget in ms (default `4000`) |

Export events are recorded on the `promptgen.audit` logger. Leave the Atlassian
client id/secret blank to hide the button. **`INCIPIT_ATLASSIAN_OAUTH_CLIENT_SECRET`
must be supplied via env/Doppler** for the export flow to work.

### All environment variables

One table so a Doppler (or `.env`) config can be populated end-to-end. Secrets
are flagged — never commit them.

| Env var | Purpose | Default |
|---|---|---|
| `INCIPIT_BACKEND` | LLM backend: `openai` \| `diffusion-cnv` \| `diffusion-oneshot` | `openai` |
| `INCIPIT_OPENAI_BASE_URL` | OpenAI-compatible endpoint base URL | `http://localhost:11434/v1` |
| `INCIPIT_OPENAI_MODEL` | Default model id (overridable in the UI) | _(empty)_ |
| `INCIPIT_OPENAI_API_KEY` | API key for the endpoint (**secret**) | _(empty)_ |
| `WEBUI_API_URL` | Doppler/Open WebUI endpoint alias; an origin is normalized to `/api` | _(empty)_ |
| `WEBUI_MODEL` | Model alias used when `INCIPIT_OPENAI_MODEL` is unset | _(empty)_ |
| `WEBUI_API_KEY` | API-key alias used when `INCIPIT_OPENAI_API_KEY` is unset (**secret**) | _(empty)_ |
| `INCIPIT_REASONING_EFFORT` | `default` \| `none` \| `low` \| `medium` \| `high` | `default` |
| `INCIPIT_DISABLE_THINKING` | Back-compat: truthy → `reasoning_effort=none` | _(unset)_ |
| `INCIPIT_ALLOWED_BASE_URL_HOSTS` | Extra hosts allowed for the model endpoint (SSRF allow-list) | _(empty)_ |
| `INCIPIT_SETTINGS_FILE` | Path for persisted UI settings | `.promptgen.json` |
| `INCIPIT_MAX_TOKENS` | Max generated tokens | `2048` |
| `INCIPIT_GEN_TIMEOUT` | Generation timeout (s) | `300` |
| `INCIPIT_LOAD_TIMEOUT` | Model load timeout (s) | `600` |
| `INCIPIT_IDLE_TIMEOUT` | Idle-kill timeout for the diffusion subprocess (s) | `600` |
| `INCIPIT_CLI_BIN` | Path to `llama-diffusion-cli` (diffusion backends) | `/usr/local/bin/llama-diffusion-cli` |
| `INCIPIT_MODEL` | GGUF model path (diffusion backends) | _(see config)_ |
| `INCIPIT_NGL` / `INCIPIT_N_CPU_MOE` / `INCIPIT_THREADS` | Diffusion CLI GPU/CPU/thread knobs | `99` / `18` / `8` |
| `INCIPIT_PROMPT_MARKER` | Diffusion `-cnv` turn marker | `"\n> "` |
| `INCIPIT_DIFFUSION_ARGS` | Extra diffusion CLI args | _(see config)_ |
| `INCIPIT_SESSION_TTL` | Session + auth-record TTL (s) | `86400` |
| `INCIPIT_GITHUB_TOKEN` | Anonymous-rate-limit token for public repo grounding | _(empty)_ |
| `INCIPIT_FIRECRAWL_URL` | Firecrawl base URL for non-GitHub repo scraping | _(empty)_ |
| `INCIPIT_REPO_TIMEOUT` | Repo-fetch HTTP timeout (s) | `25` |
| `INCIPIT_REPO_CONTEXT_MAX` | Max chars of repo context injected into prompts | `6000` |
| `INCIPIT_GITHUB_OAUTH_CLIENT_ID` | GitHub OAuth app client id (public) | _(built-in default)_ |
| `INCIPIT_GITHUB_OAUTH_CLIENT_SECRET` | GitHub OAuth app client secret (**secret**) | _(empty)_ |
| `INCIPIT_GITHUB_OAUTH_REDIRECT_URL` | GitHub OAuth callback URL | `https://incipit.nexus.inmotionhosting.com/auth/github/callback` |
| `INCIPIT_GITHUB_OAUTH_SCOPES` | GitHub OAuth scopes | `repo` |
| `INCIPIT_ATLASSIAN_OAUTH_CLIENT_ID` | Atlassian OAuth app client id (public) | _(built-in default)_ |
| `INCIPIT_ATLASSIAN_OAUTH_CLIENT_SECRET` | Atlassian OAuth app client secret (**secret**) | _(empty)_ |
| `INCIPIT_ATLASSIAN_OAUTH_REDIRECT_URL` | Atlassian OAuth callback URL | `https://incipit.nexus.inmotionhosting.com/auth/atlassian/callback` |
| `INCIPIT_ATLASSIAN_OAUTH_SCOPES` | Atlassian console scopes (`offline_access` appended at request time) | `read:jira-work write:jira-work read:jira-user` |
| `INCIPIT_JIRA_ISSUE_TYPES` | Issue-type dropdown options | `Task,Story,Bug` |
| `INCIPIT_JIRA_DEFAULT_PROJECT_KEY` | Pre-selected project key | _(empty)_ |
| `INCIPIT_JIRA_EXPORT_TIMEOUT` | Export time budget (ms) | `4000` |
| `INCIPIT_SESSION_COOKIE_SECRET` | Secret for signing the session cookie (**secret**; set so cookies survive restarts) | _(ephemeral per-process)_ |
| `INCIPIT_COOKIE_SECURE` | Set the cookie `Secure` flag | `true` |

## Testing & coverage

The repo ships an offline `pytest` suite (no network, no model/subprocess) —
OAuth flows and the Jira REST client are exercised against a mocked httpx
transport (`respx`):

```bash
pip install -r requirements-dev.txt
pytest # runs with coverage (see pytest.ini)
```

CI ([`.github/workflows/ci.yml`](.github/workflows/ci.yml)) runs the same suite
on Python 3.11. Coverage is gated at **90%** but **scoped** (in `pytest.ini`) to
the security-critical, fully-offline-testable modules — `app/auth.py`,
`app/audit.py`, `app/jira.py`, `app/markdown_adf.py` — rather than the whole
`app` package: the LLM/diffusion backends and the wizard orchestration call out
to a model/subprocess and aren't covered by the offline suite, so a 90% gate
over all of `app` is impractical. The auth + export **routes** live in
`app/main.py` alongside every wizard route (so they can't be isolated per-file
by coverage), but they are covered by `tests/test_auth.py`,
`tests/test_jira_auth.py`, and `tests/test_jira_export.py`.

---

Expand All @@ -108,7 +219,7 @@ Incipit was originally built around **DiffusionGemma 26B-A4B-it** run through
`llama-diffusion-cli` (llama.cpp PR #24423, which has no HTTP server yet — the
app drives a persistent `-cnv` subprocess over stdin/stdout). This path requires
building llama.cpp from a pinned PR and a GPU, and is selected with
`PROMPTGEN_BACKEND=diffusion-cnv` (or `diffusion-oneshot`). It is **not** needed
`INCIPIT_BACKEND=diffusion-cnv` (or `diffusion-oneshot`). It is **not** needed
for the OpenAI-compatible path above.

```bash
Expand All @@ -120,7 +231,7 @@ hf download unsloth/diffusiongemma-26B-A4B-it-GGUF diffusiongemma-26B-A4B-it-Q4_
podman build -t localhost/promptgen:v3 .
```

Backends (`PROMPTGEN_BACKEND`):
Backends (`INCIPIT_BACKEND`):

| Value | What |
|---|---|
Expand Down
52 changes: 52 additions & 0 deletions app/audit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Append-only audit log for OAuth token lifecycle events.

There's no database (single-replica, in-memory app), so "audit table" is an
append-only log line on the dedicated `promptgen.audit` logger (lowercase
namespace, matching the rest of the app's loggers). Each record carries a
timestamp, the action, the provider, and the provider account id so token
issuance / revocation can be traced (AC8 / NFR3). No tokens or secrets are ever
logged.
"""

import logging
import time

log = logging.getLogger("promptgen.audit")


def _emit(action: str, provider: str, user_id: str = "", user_login: str = "",
**extra) -> None:
fields = {
"ts": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
"action": action,
"provider": provider,
"user_id": user_id or "-",
"user_login": user_login or "-",
**extra,
}
log.info("audit %s", " ".join(f"{k}={v}" for k, v in fields.items()))


def token_issued(provider: str, user_id: str = "", user_login: str = "",
**extra) -> None:
"""Record that a provider access token was issued/stored for a user."""
_emit("token_issued", provider, user_id, user_login, **extra)


def token_refreshed(provider: str, user_id: str = "", user_login: str = "",
**extra) -> None:
"""Record that a provider access token was refreshed (Atlassian)."""
_emit("token_refreshed", provider, user_id, user_login, **extra)


def token_revoked(provider: str, user_id: str = "", user_login: str = "",
**extra) -> None:
"""Record that a provider token was revoked / the user logged out."""
_emit("token_revoked", provider, user_id, user_login, **extra)


def jira_export(project_key: str, issue_key: str, user_id: str = "",
user_login: str = "", **extra) -> None:
"""Record that a mega-prompt was exported to a Jira issue."""
_emit("jira_export", "atlassian", user_id, user_login,
project=project_key or "-", issue=issue_key or "-", **extra)
Loading
Loading