---
title: AI Coaching via CLI (manage coaching artifacts and start interviews from an AI agent)
---

# AI Coaching via CLI (manage coaching artifacts and start interviews from an AI agent)

## What it is

Omniscio's **AI Coaching** virtual project — the structured-interview surface where you sit down with Claude, answer a sequence of guided questions, and walk away with a saved **artifact** (a written summary, plan, or reflection that Claude composed from the conversation) — is reachable from outside Omniscio over the same `127.0.0.1:19519` localhost HTTP server that already exposes cron jobs, automations, recipes, settings, bookmarks, and tags. An external AI agent (Claude Code in another project, ChatGPT with the [omniscio-control skill](/.claude/skills/omniscio-control/SKILL.md), a curl script) holding the Omniscio bearer token can browse the user's interview-prompt library, list saved artifacts, fetch any specific version of any artifact, create brand-new artifacts, propose edits, propose deletions, and start a brand-new interview — all from a shell.

The in-app surface is described in [ai-coaching.md](ai-coaching.md): the AI Coaching sub-sidebar (Dashboard / Sessions / Artifacts / Bookmarks / Archived), the artifact editor with version history, and the way saved artifacts inject as a "Things to know about the user" profile block on every regular session spawn. This page covers the **CLI routes** that expose those primitives over HTTP.

The **gating model is split across the routes by blast radius**, identical in shape to the bookmarks and tags CLI surfaces:

- **A kill switch covers every route.** The `aiCoachingCliEnabled` setting (default `true`, toggle at Settings → CLI Control → "Enable AI Coaching CLI") short-circuits every route to 403 with `{ ok: false, error: 'AI Coaching CLI disabled in Settings → CLI Control', disabled: true }` before any auth, parse, or rate-limit work. The kill switch is checked first so the disabled-feature 403 wins over rate-limit / auth errors — when the user has explicitly turned the surface off, a more specific error would be confusing.
- **Mutations are approval-gated.** `POST /ai-coaching/artifacts` (create a new artifact), `PATCH /ai-coaching/artifacts/:id` (update), `DELETE /ai-coaching/artifacts/:id` (delete with cascade preview), and `POST /ai-coaching/interviews` (start a new interview) all enqueue `ai_coaching.artifact_create` / `ai_coaching.artifact_update` / `ai_coaching.artifact_delete` / `ai_coaching.start_interview` rows in the same `cli_pending_actions` queue used by cron jobs, automations, settings PATCH, session lifecycle, and tag library mutations. The route returns HTTP 202 immediately; the change does not apply until the user clicks **Approve** in the Omniscio inbox. The user-in-the-loop guarantee is identical to the other approval-gated CLI capabilities documented in [cli-pending-actions.md](cli-pending-actions.md), and shares the same open-pending cap.
- **The session-scoped artifact save applies immediately.** `POST /ai-coaching/sessions/:sessionId/artifact` is the in-app coach's OWN save path — it is session-bound (an agent/in-app token may write only its own session) and title-locked (the title comes from the session, not the body), so unlike the general artifact CRUD it applies at once with no inbox approval. It's documented here for completeness; an external agent managing artifacts uses the create/update/delete routes above.
- **Reads are bearer-gated and read-budgeted.** All six GETs require the bearer token and share the 60-reads-per-minute-per-token-hash budget with all other auth-gated reads on this server.

The split mirrors the in-app safety model: artifacts represent durable user-authored thinking, so any change to them (or any spawn of a brand-new interview that costs Claude time) lands in the inbox first. Reads are free because the user can already see this content in the in-app sidebar.

## Where to find it

The in-app half of AI Coaching is the sub-sidebar with Dashboard, Sessions, Artifacts, Bookmarks and Archived, plus the artifact editor and its version history — that is where you sit an interview and read what came out of it. The agent-facing half has no screen at all: an outside AI reaches the same prompts, artifacts and interviews over Omniscio's local control server, with the same access token the rest of the automation surfaces use. Saved artifacts are the part that reaches beyond this feature — they are injected as a profile block when an ordinary session starts, so what is written here shapes how your agents behave later.

## How it behaves

Whether a change happens at once or waits for you depends on what it touches. Reading is always safe and free, and an interview session saving its own artifact is locked to that one session and one title so it can simply write; everything else — creating, editing or deleting an artifact, and starting a new interview that spends real Claude time — waits in your inbox until you approve it. When a queued change has stopped making sense by the time you look at it, it is rejected with a reason rather than applied to the wrong thing.

## For agents

### How to use it

The thirteen routes this page documents in full:

| Method   | Path                                           | Approval-gated? | Status         | Purpose                                                                                                                                                                                                                                                                |
| -------- | ---------------------------------------------- | --------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET`    | `/ai-coaching/prompts`                         | n/a (read)      | `200 OK`       | List every available interview prompt (builtin + user-saved). Same source of truth as the in-app prompt picker on the Dashboard.                                                                                                                                       |
| `GET`    | `/ai-coaching/artifacts`                       | n/a (read)      | `200 OK`       | List the latest non-deleted version of every artifact, newest first. Mirrors the **Artifacts** section of the AI Coaching sub-sidebar. Pass `?fields=slim` for a lightweight index (no `content`, adds `estimatedTokens`).                                             |
| `GET`    | `/ai-coaching/artifacts/search`                | n/a (read)      | `200 OK`       | Semantic search across coaching artifacts. Query: `?q=<text>&topK=5`. Returns top-K results above similarity threshold with title, score, micro-summary. Empty array when embedding model is not ready (graceful degradation).                                         |
| `GET`    | `/ai-coaching/artifacts/:id`                   | n/a (read)      | `200 OK`       | Fetch one artifact's latest non-deleted version (full row including `content`). 404 when the id is unknown OR every version is soft-deleted.                                                                                                                           |
| `GET`    | `/ai-coaching/artifacts/:id/versions`          | n/a (read)      | `200 OK`       | List the version history for one artifact, descending by version number. List entries OMIT `content` to keep payloads slim. 404 when no non-deleted versions exist.                                                                                                    |
| `GET`    | `/ai-coaching/artifacts/:id/versions/:version` | n/a (read)      | `200 OK`       | Fetch the FULL row for one specific (id, version) pair, INCLUDING `content`. INCLUDES soft-deleted rows (asymmetric with the list endpoint) so the history viewer can show a rejected version's body.                                                                  |
| `GET`    | `/ai-coaching/sessions/:sessionId/info`        | n/a (read)      | `200 OK`       | Coaching-specific session metadata in one round-trip — `producesArtifactTitle`, `status`, `messageCount`. 404 for unknown / soft-deleted / non-coaching sessions.                                                                                                      |
| `POST`   | `/ai-coaching/artifacts`                       | **Yes**         | `202 Accepted` | Queue creation of a NEW artifact. Body: `{ title, content }`. **409** if the title already exists (use `PATCH` to add a version); a create never silently appends.                                                                                                     |
| `PATCH`  | `/ai-coaching/artifacts/:id`                   | **Yes**         | `202 Accepted` | Queue an artifact update. Body: `{ content, expectedLatestVersion }`. Optimistic concurrency — the dispatcher rejects with `STALE_ARTIFACT` at approve-time if the artifact has moved past `expectedLatestVersion`.                                                    |
| `DELETE` | `/ai-coaching/artifacts/:id`                   | **Yes**         | `202 Accepted` | Queue an artifact deletion (soft-deletes ALL versions). No body. Preview text embeds the cascade size: `Delete artifact "<title>" (N version(s))`.                                                                                                                     |
| `POST`   | `/ai-coaching/interviews`                      | **Yes**         | `202 Accepted` | Queue start of a new interview. Body: `{ promptId }`. The dispatcher spawns a real Claude session at approve-time, which is why this is gated rather than immediate.                                                                                                   |
| `POST`   | `/ai-coaching/sessions/:sessionId/artifact`    | No (immediate)  | `200 OK`       | The in-app coach's OWN save path. Body: `{ content }` only — the title is derived from the session, so it can only write that session's one artifact. Session-bound (foreign session → 403); non-coaching → 404; empty / >20K → 400. Applies immediately, no approval. |
| `POST`   | `/ai-coaching/export`                          | n/a (read)      | `200 OK`       | Export all coaching data (artifacts, bookmarks, core profile, memories, goals + goal check-ins) as JSON or Markdown. Body: `{ format: 'json'                                                                                                                           | 'md', includeBookmarks?: true, includeCoreProfile?: true, includeMemories?: true }`. Goals + check-ins are always included (a completeness surface, matching the full DSAR backup). Returns the serialized content inline. 100 MB size guard. |

All routes require the bearer token in `Authorization: Bearer <token>` (or `?token=<token>`). The mutating five share the global **10-mutations-per-minute** rate limit; `POST /ai-coaching/interviews` has an additional **dedicated 2/min coaching interview bucket** (mirrors the recipe-run bucket) because it spawns a paid Claude session (the session-scoped save is one of them — apply-immediately, not approval-gated); the six GETs share the **60-reads-per-minute** read budget per token hash. The kill switch precedes all of them.

### The rest of the namespace

The /ai-coaching namespace registers **41 routes over 29 paths** across nine registrar modules, and the table above is the artifact-and-interview thirteen. An agent that needs any of the following will not find it here, and should read the route modules — [src/main/services/cli/ai-coaching/](/src/main/services/cli/ai-coaching/) — or the machine-checked master table in [endpoint-index.md](/.claude/skills/omniscio-control/endpoint-index.md), which is lint-verified against the served route set. The thirteen tabled plus the twenty-eight below account for all 41:

- **Memories** — `GET /ai-coaching/memories`, `POST /ai-coaching/memories`, `GET /ai-coaching/memories/:sessionId`, `DELETE /ai-coaching/memories/:sessionId`. The per-session memory rows coaching writes for a user.
- **Core profile** — `GET /ai-coaching/core-profile`, `PUT /ai-coaching/core-profile`, `POST /ai-coaching/core-profile/regenerate`. The profile block injected into regular sessions.
- **Pinned context** — `GET /ai-coaching/pinned-context`, `PUT /ai-coaching/pinned-context`.
- **Goals** — `PATCH /ai-coaching/goals/:id`, `DELETE /ai-coaching/goals/:id`, `POST /ai-coaching/goals/:id/checkins` (the goal-memory module).
- **Bookmarks** — `GET /ai-coaching/bookmarks`, `POST /ai-coaching/bookmarks`, `PATCH /ai-coaching/bookmarks/:id`, `DELETE /ai-coaching/bookmarks/:id`.
- **Stats and feedback** — `GET /ai-coaching/stats`, `GET /ai-coaching/feedback`, `POST /ai-coaching/feedback`.
- **Insights** — `GET /ai-coaching/insights/:id`, `DELETE /ai-coaching/insights/:id`.
- **Search** — `GET /ai-coaching/search`.
- **Snack templates** — `GET /ai-coaching/snack-templates`.
- **Custom interviews** — `POST /ai-coaching/custom-interviews`, `POST /ai-coaching/interviews/:sessionId/reset`.
- **Compaction summaries** — `GET /ai-coaching/compaction-summaries`, `GET /ai-coaching/compaction-summaries/latest`.
- **Artifact levels** — `PATCH /ai-coaching/artifacts/:id/level`, alongside the artifact routes tabled above.

All of them are behind the same `aiCoachingCliEnabled` kill switch and the same bearer token as
the routes above; the approval and read-budget tiers differ per route, so read each module rather
than assuming the thirteen's split applies.

### Workflow

1. **Discover available interview prompts** with `GET /ai-coaching/prompts` before proposing a new interview. The response gives every prompt's `id`, `title`, `description`, and the `producesArtifactTitle` it'll save into. You need the `id` to start an interview in step 6.
2. **List existing artifacts** with `GET /ai-coaching/artifacts`. Each row is the latest non-deleted version per artifact title (so an artifact saved four times appears once with `version: 4`). Use this when the agent needs to know "what does the user already have written down?" Pass `?fields=slim` for a lightweight index — the response omits the (potentially large) `content` field and adds `estimatedTokens` (token count estimated from content length), plus any available `summaryText`, `keyPointsText`, and `microText`. Use the slim index to decide WHICH artifacts to pull in full via step 4.
3. **Search artifacts by meaning** with `GET /ai-coaching/artifacts/search?q=<query>&topK=5`. Uses on-device semantic embeddings to find the most relevant artifacts. Returns `{ results: [{ artifactId, title, score, microText, updatedAt }] }`. Empty array when the embedding model is not ready (graceful degradation). Use this to find relevant context before pulling full content, or to reference artifacts naturally in conversation.
4. **Inspect an artifact's content** with `GET /ai-coaching/artifacts/:id` (latest version only). Pass an artifact id from step 2's list or step 3's search results. Returns 404 if every version of that artifact is soft-deleted.
5. **Inspect history** with `GET /ai-coaching/artifacts/:id/versions` to see all the version numbers (without their bodies — content is omitted to keep the response small), then `GET /ai-coaching/artifacts/:id/versions/:version` to fetch any specific version's full body. Note the asymmetry: the per-version GET returns soft-deleted rows by design (so the history viewer can show a rejected version), the list endpoint hides them.
6. **Inspect coaching session metadata** with `GET /ai-coaching/sessions/:sessionId/info` when you have a session id and need to know whether it's an AI Coaching session, what artifact title it'll produce, its current `status`, and `messageCount`. 404 for non-coaching sessions (sessions whose `produces_artifact_title` is NULL).
7. **Propose an artifact edit** with `PATCH /ai-coaching/artifacts/:id` body `{ content, expectedLatestVersion }`. Send `X-Client-Request-Id` (≤64 chars) so retries are idempotent in a 30-day window. The route returns HTTP 202 with the queued row's `id`. Tell the user "open Omniscio's inbox and approve to apply" and stop. The dispatcher re-resolves the latest version at approve-time and rejects with `STALE_ARTIFACT` if the artifact has moved past `expectedLatestVersion` between enqueue and approve — this is the optimistic-concurrency guard. Read the latest version (step 4) immediately before submitting so `expectedLatestVersion` matches the live row.
8. **Propose an artifact deletion** with `DELETE /ai-coaching/artifacts/:id`. No body. Preview text embeds the cascade size — `Delete artifact "Weekly Review" (4 versions)` — so the user sees the blast radius before approving. On approval, every non-deleted version of the artifact is soft-deleted at once.
9. **Propose starting a new interview** with `POST /ai-coaching/interviews` body `{ promptId }`. The route validates the `promptId` against the live prompt library (returns 400 — not 404 — if the id is unknown, since a missing prompt is bad input rather than a missing resource). On approval, the dispatcher invokes the same start-interview flow as the in-app **Start interview** card on the Dashboard: it writes the prompt body into `~/Claude/ai-coaching/CLAUDE.md` and spawns a fresh Claude session that's pre-seeded with the prompt.
10. **Propose creating a NEW artifact** with `POST /ai-coaching/artifacts` body `{ title, content }` — the way to seed a profile artifact from outside an interview (e.g. importing the user's own notes from another source). The route returns **409** if an artifact with that `title` already exists (the error names the existing id — `PATCH` it to add a version instead); otherwise HTTP 202 with the queued row. On approval the dispatcher persists it as version 1. A create NEVER silently appends a version to an existing title — that distinction is the whole reason it's a separate route from `PATCH`.

### Approval flow recap

For the four approval-gated routes (`POST /ai-coaching/artifacts`, `PATCH /ai-coaching/artifacts/:id`, `DELETE /ai-coaching/artifacts/:id`, `POST /ai-coaching/interviews`):

1. Route returns HTTP 202 with `{ ok: true, data: { id, actionKind, status: 'pending', previewText, ... } }`.
2. The pending row appears in Omniscio's inbox as a yellow card titled with the preview text — for example `Update artifact "Weekly Review" to v5`, `Delete artifact "Project Retro" (3 versions)`, or `Start coaching interview: "Weekly Review"`.
3. The user clicks the card — the standard CLI Pending approval pane opens showing the action kind, target, and the payload JSON for review.
4. **Approve** → the dispatcher executes the side effect (insert a new artifact version / soft-delete all versions / spawn a new interview session), emits `AI_COACHING_ARTIFACTS_CHANGED` (for update + delete) or `SESSIONS_CHANGED` (for start-interview), and stamps `dispatched_at`. **Reject** → row flips to `rejected` with the reason, no side effect.
5. The renderer's open AI Coaching sub-sidebar receives the push and refreshes the artifacts list automatically.

The dispatcher distinguishes **permanent** from **transient** dispatch failures. Permanent — `STALE_ARTIFACT` for an update that lost the optimistic-concurrency race, target artifact missing at approve-time (deleted out from under the queued action), prompt id no longer in the library — flips the pending row to `rejected` with `PermanentDispatchError` so the user doesn't loop forever clicking Approve. Transient — DB lock, network blip — reverts the row to `pending` for retry.

### Idempotency

All four approval-gated routes accept an `X-Client-Request-Id` header (≤64 chars). When set, the same id within a 30-day window returns the existing pending row (HTTP 200, body includes `idempotent: true`) instead of queuing a duplicate. The lookup is **scoped per `action_kind`**, so the same id can map to an `ai_coaching.artifact_create`, an `ai_coaching.artifact_update`, an `ai_coaching.artifact_delete`, and an `ai_coaching.start_interview` independently — same id, different action kinds, four different pending rows.

Header-based idempotency is also used by `DELETE` (since DELETE bodies are spec-discouraged), matching the pattern used by `DELETE /tags/:id` and `DELETE /project/:id`.

The six read endpoints are naturally idempotent — they're stateless GETs.

### Examples

```bash
# Read the bearer token from Omniscio's auto-delivered file
TOKEN=$(<~/.amc/cli-token)

# 1. List every available interview prompt
curl -s -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19519/ai-coaching/prompts

# 2. List every saved artifact (latest version per title)
curl -s -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19519/ai-coaching/artifacts

# 2b. Slim artifact index (no content, adds estimatedTokens + summary fields)
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://127.0.0.1:19519/ai-coaching/artifacts?fields=slim"

# 3. Semantic search across artifacts (find relevant context by meaning)
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://127.0.0.1:19519/ai-coaching/artifacts/search?q=career+values&topK=5"

# 4. Fetch one artifact's latest content
curl -s -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19519/ai-coaching/artifacts/<artifact-uuid>

# 5. List every version of one artifact (no content in the list rows)
curl -s -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19519/ai-coaching/artifacts/<artifact-uuid>/versions

# 6. Fetch one specific version's full body (INCLUDES soft-deleted rows)
curl -s -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19519/ai-coaching/artifacts/<artifact-uuid>/versions/2

# 7. Get coaching-specific session metadata in one round-trip
curl -s -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19519/ai-coaching/sessions/<session-uuid>/info

# 7b. Propose creating a NEW artifact (409 if the title already exists — PATCH it instead)
curl -X POST http://127.0.0.1:19519/ai-coaching/artifacts \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Client-Request-Id: artifact-create-2026-06-17" \
  -d '{
    "title": "Business Operating Notes",
    "content": "# Business Operating Notes\n\nHow JLS actually runs..."
  }'
# Returns 202 with the pending row. The artifact does not exist until the user
# approves in Omniscio's inbox. Returns 409 if an artifact with that title already
# exists (the body names the existing id so you can PATCH it instead).

# 7. Propose updating an artifact (read latest version first to get expectedLatestVersion)
curl -X PATCH http://127.0.0.1:19519/ai-coaching/artifacts/<artifact-uuid> \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Client-Request-Id: artifact-update-2026-05-09" \
  -d '{
    "content": "# Weekly Review\n\nThis week I shipped...",
    "expectedLatestVersion": 4
  }'
# Returns 202 with the pending row. The artifact does not change until the
# user approves in Omniscio's inbox. If another writer bumped the version between
# your read and the approval, the dispatcher rejects with STALE_ARTIFACT.

# 8. Propose deleting an artifact (cascade size shown in approval preview)
curl -X DELETE http://127.0.0.1:19519/ai-coaching/artifacts/<artifact-uuid> \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-Client-Request-Id: artifact-delete-2026-05-09"

# 9. Propose starting a new interview
curl -X POST http://127.0.0.1:19519/ai-coaching/interviews \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Client-Request-Id: interview-start-weekly-2026-05-09" \
  -d '{"promptId": "<prompt-uuid-from-step-1>"}'
# Returns 202. On approval, Omniscio writes the prompt to ~/Claude/ai-coaching/CLAUDE.md
# and spawns a fresh Claude session with the kickoff message.

# 10. Export all coaching data as JSON (artifacts + bookmarks + core profile + memories + goals + check-ins)
curl -X POST http://127.0.0.1:19519/ai-coaching/export \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"format": "json"}'
# Returns 200 with { ok: true, data: { format, content, artifactCount, bookmarkCount, memoryCount, goalCount, goalCheckinCount, hasCoreProfile } }.
# content is the full JSON string. Also supports format: "md" for Markdown.
# Optional: includeBookmarks: false, includeCoreProfile: false to exclude sections.
```

### How it works

The routes live in [src/main/services/cli/cli-server-ai-coaching-routes.ts](/src/main/services/cli/cli-server-ai-coaching-routes.ts) (the session-artifact save is registered by `registerAiCoachingSessionInfoRoutes` in the `ai-coaching/session-routes.ts` sub-module), registered once at Omniscio startup from the CLI-route registration array. Tests call `registerAiCoachingRoutes()` explicitly inside `beforeAll` after `startCliServer()` to keep the canonical real-HTTP + real-SQLite test pattern.

**Schemas** (Zod), defined in [src/shared/ipc-schemas.ts](/src/shared/ipc-schemas.ts) — only the delete schema is strict-mode (no body fields), the update and start-interview schemas are plain `z.object`:

- `aiCoachingCliCreateArtifactSchema` — `title: z.string().min(1).max(200)`, `content: z.string().min(1).max(20_000)` (the 20K cap matches everywhere else). No version field — a create's natural key is the title, and the route 409s an existing one.
- `aiCoachingCliUpdateArtifactSchema` — `content: z.string().min(1).max(20_000)` (matches the in-app 20K cap), `expectedLatestVersion: z.number().int().min(1)`.
- `aiCoachingCliDeleteArtifactSchema` — `z.object({}).strict()` (no body fields; strict-mode rejects stray keys at submit time).
- `aiCoachingCliStartInterviewSchema` — `promptId: z.string().min(1).max(100)`.

The schemas are deliberately a **subset** of the in-app `aiCoachingUpdateArtifactSchema` accepted by the IPC handlers — every field a CLI caller can send is a valid value for in-app dispatch, so there's no approval-time-rejection footgun where a CLI-built payload passes the route's Zod check but fails the dispatcher's revalidate.

**Approval-gated route pipeline** (`POST /ai-coaching/artifacts`, `PATCH /ai-coaching/artifacts/:id`, `DELETE /ai-coaching/artifacts/:id`, `POST /ai-coaching/interviews`):

1. **Kill-switch check** — `aiCoachingCliReady(res)` reads the `aiCoachingCliEnabled` setting and writes the 403 + early-return if the flag is `false`. Default-on: a missing flag (older config snapshots that predate the field) is treated as enabled.
2. **Rate-limit check** — `checkRateLimit()` from [cli-server.ts](/src/main/services/cli/cli-server.ts) (10 mutations / minute global). 429 on bust. For `POST /ai-coaching/interviews` only: a **dedicated 2/min coaching interview bucket** (`checkCoachingInterviewRateLimit()`) runs after the global cap and before body parsing — mirrors the recipe-run pattern.
3. **Body parse** — `safeParse` against the per-route schema. 400 on validation failure with the issue messages joined.
4. **Target pre-check** — PATCH / DELETE call `getArtifactById(id)` from [queries-ai-coaching/index.ts](/src/main/db/queries-ai-coaching/index.ts) → 404 if the artifact is missing or every version is soft-deleted (saves the user from approving a card whose target is already gone). The create route **inverts** this: `POST /ai-coaching/artifacts` calls `getArtifactByTitle(title)` → **409** if the title is already taken (the error names the existing id so the caller can `PATCH` instead), because a create must never silently append a version to an interview-produced artifact. Either way the dispatcher's `validate` re-checks at approve-time to catch a target that changes between submit and approval. POST /interviews has no target row, so it skips this step.
5. **Prompt-id existence check** (POST /interviews only) — looks up the `promptId` in `listInterviewPrompts()`. Returns **400** (not 404) on miss because there is no resource at `/interviews/:id` we were looking for — an unknown promptId is malformed input.
6. **Idempotency lookup** — `findIdempotent(clientRequestId, actionKind)` from [queries-cli-pending.ts](/src/main/db/queries-cli-pending.ts) returns the existing row if the same `(clientRequestId, action_kind)` was inserted within the last 30 days. The lookup uses the unique partial index `idx_cli_pending_request ON (client_request_id, action_kind) WHERE client_request_id IS NOT NULL`. Returns HTTP 200 with `idempotent: true` on hit.
7. **Open-cap check** — `getOpenCount() >= CLI_PENDING_MAX_OPEN`. 409 on bust with `Maximum <cap> pending CLI actions reached. Approve or reject some first.` (the cap is a constant, not a literal to memorise — it went 20 -> 500 on 2026-09-14).
8. **Insert pending row** — `insertPending({ actionKind, targetId, payloadJson, previewText, clientRequestId })`. The `previewText` is built per-kind:
   - POST /artifacts: `Create artifact "<title>"` — sliced to 80 chars. `targetId` is `null` because there's no pre-existing target row.
   - PATCH: `Update artifact "<current-title>" to v<expectedLatestVersion + 1>` — sliced to 80 chars.
   - DELETE: `Delete artifact "<current-title>" (N version)` for N=1, `Delete artifact "<current-title>" (N versions)` for N≠1 — sliced to 80 chars.
   - POST /interviews: `Start coaching interview: "<prompt-title>"` — sliced to 80 chars. `targetId` is `null` because there's no pre-existing target row.
9. **Emit push** — `emitPush(IPC.CLI_PENDING_CHANGED, { id: row.id })` so the renderer's inbox refreshes.
10. **Track event** — `trackEvent('cli_control', 'hit', { path: '/ai-coaching/<verb>' })`.
11. **Respond** — `202 { ok: true, data: row }` on insert, `200 { ok: true, data: row, idempotent: true }` on idempotent replay.

**Read route pipeline** (the six GETs):

1. Kill-switch check (same `aiCoachingCliReady(res)` helper).
2. `authenticateRequest(req)` — bearer-token check. 401 on miss.
3. Read-budget check — `checkReadBudget(hashBearer(rawBearer))` shares the 60/min budget with every other auth-gated GET on this server. 429 on bust.
4. Direct call into the underlying query function:
   - `/prompts` → `listInterviewPrompts(db)`
   - `/artifacts` → `listLatestArtifacts(db)` (default) or `listLatestArtifactIndex(db)` when `?fields=slim` — the slim variant omits `content` and adds `estimatedTokens`
   - `/artifacts/:id` → `getArtifactById(db, id)` — 404 on miss
   - `/artifacts/:id/versions` → `listVersionsForId(db, id)` — 404 when the result is empty (no non-deleted versions)
   - `/artifacts/:id/versions/:version` → strict integer parse on `:version` (rejects `'abc'`, `'1.5'`, `'1e2'`, `'01'`, `'0'`, negatives) — 400 on parse failure, then `getVersionByIdAndNumber(db, id, n)` — 404 on miss; **note this query INCLUDES soft-deleted rows asymmetrically with the other reads** so the history viewer can show rejected versions.
   - `/sessions/:sessionId/info` → `getCoachingSessionInfo(db, sessionId)` — 404 for unknown / soft-deleted / non-coaching sessions (the query gates on `produces_artifact_title IS NOT NULL`).
5. `trackEvent('cli_control', 'hit', { path: '<route>' })` — only when the response is 200, never on 404 (telemetry would be noisy).
6. Respond — `200 { ok: true, data: { ... } }` on success, `404 { ok: false, error: '...' }` on miss, `400` on bad input where applicable.

**Handler modules** for the four approval-gated kinds live under [src/main/services/cli-action-handlers/](/src/main/services/cli-action-handlers/) (the dispatcher routes registry-first — see [cli-pending-handler-registry-contract.md](/.claude/memory/contracts/cli-pending-handler-registry-contract.md)):

- **`ai_coaching.artifact_create`** — `validate` re-checks the title is still free via `getArtifactByTitle` (an existing title → permanent reject, so a create never appends to an interview-produced artifact); `dispatch` calls `saveArtifactVersion(db, { title, content, sourceSessionId: null })` which mints version 1. `dispatch` does NOT re-check existence — `saveArtifactVersion` content-dedups, so a crash-recovery re-dispatch is idempotent without an extra key. Emits `AI_COACHING_ARTIFACTS_CHANGED`, then fires the shared `triggerPostArtifactSavePipeline` (Key Points / Summary / Micro condensed-level generation + Core Profile refresh) — identical to an interview-produced artifact, and wrapped so a condensation hiccup can never fail a create whose artifact already persisted.
- **`ai_coaching.artifact_update`** — re-resolves the artifact's latest version, compares against `payload.expectedLatestVersion`. If the live latest **does not match** what the payload expected, throws `PermanentDispatchError` with `STALE_ARTIFACT` so the row flips to `rejected` rather than silently overwriting fresher content. On match, calls `saveArtifactVersion(db, { title, content, sourceSessionId: null })` (which inserts a new version row), emits `AI_COACHING_ARTIFACTS_CHANGED`, then fires the same `triggerPostArtifactSavePipeline` (condensed levels + Core Profile refresh, isolated).
- **`ai_coaching.artifact_delete`** — re-resolves the target artifact (404 → `PermanentDispatchError`), calls `softDeleteAllVersions(db, targetId)` which sets `is_deleted = 1` on every version row at once, emits `AI_COACHING_ARTIFACTS_CHANGED`.
- **`ai_coaching.start_interview`** — re-resolves the `promptId` (404 → `PermanentDispatchError` if the prompt was deleted between enqueue and approve), invokes `startInterviewFromPromptId({ promptId })` which writes the prompt body into `~/Claude/ai-coaching/CLAUDE.md` and calls `createSessionWithPrompt` to spawn a real Claude session. Emits `SESSIONS_CHANGED`.

The standard CLI Pending approval pane (no dedicated AI-Coaching pane in v1) renders the action kind, the preview text, and the payload JSON. Approve / reject go through the standard `resolveInboxApproval('cli-pending', row.id, decision)` consumer in [stores/inbox-actions.ts](/src/renderer/src/stores/inbox-actions.ts), so post-resolution navigation is identical to every other CLI pending kind.

### Out of scope (v1)

- **No dedicated approval pane.** Update / delete / start-interview rows render in the generic JSON-payload pane that every other CLI pending uses. A purpose-built pane (showing a content-diff for update, a version-list for delete, a prompt preview for start-interview) was deferred until usage signals demand it.
- **The in-app artifact EDITOR save still skips background level generation.** `POST` / `PATCH` over the CLI now fire `triggerPostArtifactSavePipeline` (condensed levels + Core Profile refresh), same as the interview auto-extractor — but the in-app editor's `AI_COACHING_UPDATE_ARTIFACT` handler still calls `saveArtifactVersion` directly without the pipeline, so a manual editor edit injects at **Full** until something regenerates its levels. The one-shot deferred **levels backfill** (kicked off ~8s after app-ready, gated on AI Coaching being enabled) heals any artifact missing condensed levels — including CLI-imported ones created before this wiring existed and manually-edited versions — by calling the idempotent + cost-capped `generateArtifactLevels` for each, then refreshing the Core Profile once.
- **No version-restore route.** There is no CLI route that mirrors the in-app **Restore** button (open an old version's content into the editor pre-loaded). Workaround: read the old version's content via `GET /ai-coaching/artifacts/:id/versions/:version`, then submit it as a new update via `PATCH` with the current `expectedLatestVersion`.
- **No artifact reject route.** The in-app version-history pane has a per-version "reject" action (soft-deletes a single version, rather than the whole artifact). The CLI exposes only the whole-artifact delete; per-version rejection was deferred.
- **No `applied_by: 'cli'` distinction.** The pending row's `created_by` field is hard-coded to `'cli'` in `insertPending`, but artifact rows don't track which surface (in-app / CLI) originally proposed the version once approved. A CLI-applied edit is indistinguishable from an in-app edit in the artifact viewer.
- **No CLI bulk operations.** There is no `POST /ai-coaching/artifacts/bulk-delete` or `PATCH /ai-coaching/artifacts/bulk` over the CLI. Submit per-artifact if the agent needs to fan out across multiple; the 10-mutations-per-minute rate limit caps the burst at 10 per minute. (The desktop app now has a **Bulk Import** dialog for importing many files at once with auto-matching — see [ai-coaching.md § Bulk import](ai-coaching.md#bulk-import) — but that feature uses direct IPC, not the CLI.)

## Related

Which kinds of coaching session exist and what each one is for is an internal developer reference, not a customer page, and how an interview actually runs — the prompts, the extraction, and the profile block it leaves behind — is covered by the [Coaching engine](coaching-engine.md). For the questions people ask most often about the feature, see the [AI Coaching FAQ](ai-coaching-faq.md).

- [ai-coaching.md](ai-coaching.md) — the in-app AI Coaching virtual project (sub-sidebar, prompt library, artifact editor with version history, restore / delete UX, profile-block injection on regular session spawns).
- [cli-control.md](cli-control.md) — the CLI server itself, token handling, the full route catalog.
- [cli-pending-actions.md](cli-pending-actions.md) — the shared approval queue every approval-gated CLI mutation lands in (cron, automation, settings PATCH, session lifecycle, recipes, tags, bookmarks, AI Coaching).
- [cli-tags.md](cli-tags.md) — the tag-library CLI surface that this page's split-gating pattern most closely mirrors.
- [bookmarks.md](bookmarks.md) — the bookmarks CLI surface that introduced the per-feature kill-switch pattern this page uses.
- [omniscio-control skill](/.claude/skills/omniscio-control/SKILL.md) — the external-AI side of the contract: how an AI queues these actions in practice.
