Classification
codika-os attributes every Claude Code session to a project so the dashboard can roll up time, cost, and leverage by client, product, internal tool, platform area, growth motion, or knowledge work. Attribution happens in two phases.
The two-phase model
Section titled “The two-phase model”- At sync time — the CLI runs a deterministic path-cluster classifier. For each session it computes a project distribution over the catalog using the touched paths, the cwd, and longest-prefix-wins scoring. When it can confidently pick a project, the session is uploaded with
project_idset andattribution_method = 'path'. When it can’t, the session is uploaded withproject_id = NULLand left for phase 2. - On demand — the operator invokes
/codika-os:classify-sessions. The skill walks the backlog month by month, builds an evidence prompt per session, and classifies each one in the operator’s current Claude Code agent context. Noclaude -psubprocess, no parallel Task subagent, no direct Anthropic API call: the main agent does the work, sequentially, then submits results back via the CLI.
The two phases share the same project_attribution_attempts audit table. Phase 1 writes method = 'path'. Phase 2 writes method = 'agent-inline'. Both are append-only.
Why agent-inline
Section titled “Why agent-inline”The legacy classifier ran claude -p per session, which reloaded the full Claude Code session bootstrap (CLAUDE.md hierarchy, plugins, MCP tool descriptions) on every call — ~68k cache_write tokens per session. A backfill of ~1,200 sessions cost ~$124 on the operator’s Claude Max subscription with no batching or oversight.
Doing the work in-context means the operator’s agent prompt is already cached. Marginal cost per session is just the evidence (first prompts + paths) and a small JSON output. The same backfill drops from $124 to cents-to-dollars.
Monthly windowing
Section titled “Monthly windowing”The unit of work is one calendar month. The skill probes the current month, then asks the user where to start, then loops forward through today. Each month is fetched and classified independently. This keeps the per-iteration prompt small and bounds the worst case for any single batch.
The evidence contract
Section titled “The evidence contract”Per session, the agent sees:
- The first 4 user prompts (each truncated to 600 chars), redacted of API keys only.
- Up to 8
paths_touched. cwd.
Plus the active project catalog. The catalog is delivered in two views:
catalog— flat list, one entry per project:slug,category,subcategory,display_name,alias,prompt_only,path_prefixes.catalog_by_category— same projects grouped by category (client,product,tool,platform,growth,knowledge,private) for prompt-friendly rendering.
These four signals are the contract. If we ever extend them, we extend in one place — the unattributed endpoint — and update this document.
The result contract
Section titled “The result contract”Per session, the agent outputs one entry:
type CompleteClassification = { sessionId: string; // Pick exactly one branch: projectSlug?: string | null; // existing catalog slug, or null when proposing new subcategory?: string; // optional — set when project spans multiple subcategories suggestedSlug?: string; // only when projectSlug is null suggestedCategory?: string; // only when projectSlug is null; one of the 7 categories suggestedSubcategory?: string; // only when projectSlug is null and the category has subcategories // Always present: confidence: number; // 0..1 reasoning: string; // one sentence};projectSlug and the suggested* fields are mutually exclusive — set the slug for a hit, set the suggested fields for a miss.
Picking subcategory
Section titled “Picking subcategory”The optional subcategory field is the per-session refinement that lets multi-scope projects (clients) and category-level rollups (platform, growth, knowledge) report finer breakdowns:
| Category | When to set | Values |
|---|---|---|
client | Always (client projects span both). knowledge if the work was reading/writing CRM notes/emails/contracts; product if it was development under agency/projects/<slug>/. | knowledge | product |
platform | Inherited from the project row — generally don’t override unless the session genuinely crossed boundaries. | app | cloud-run | infra | libs | template | marketplace |
growth | Pick based on what the session actually did. | content | strategy | outreach |
knowledge | company-os if touching the agents-org plugin; general otherwise. | company-os | general |
product / tool / private | Never — these categories have no subcategories. | — |
Operational surface
Section titled “Operational surface”Two CLI subcommands:
codika-os classifications pending --month YYYY-MM --json
Section titled “codika-os classifications pending --month YYYY-MM --json”Returns { sessions, catalog, catalog_by_category } for the given month. Up to 500 sessions. Scoped to the bearer’s worker by default.
codika-os classifications submit <file> --json
Section titled “codika-os classifications submit <file> --json”Reads classifications from a JSON file ([…] or { classifications: [...] }) and POSTs them to /api/sync/classifications. Returns { applied, suggested_new, skipped, invalid_slug_demoted }.
See Commands for the full request/response shapes.
Idempotency
Section titled “Idempotency”- Re-running the skill for an already-attributed month is a no-op — the
unattributedquery filters onproject_id IS NULL, so sessions that already have a project don’t come back. - Submit is append-only on attempts (every call writes a new row) and NULL-only on sessions (it only sets
project_idfor sessions where it was NULL). You can’t accidentally overwrite a manual or path-based attribution.
New-project suggestions
Section titled “New-project suggestions”When the agent decides nothing in the catalog fits AND the work looks like a recurring category, it returns projectSlug: null plus suggestedSlug, suggestedCategory, and (optionally) suggestedSubcategory. These rows are persisted in project_attribution_attempts.suggested_new_project_slug / suggested_new_project_category / suggested_new_project_subcategory for clustering.
At the end of the skill loop, suggestions are aggregated by slug and surfaced to the user. The user then decides:
- Add to seeds — manually edit
codika-os-app/seeds/projects.yaml(or just create the folder for direct-child rules), runnpm run db:sync:projects(dev) and again withDB_TARGET=prod, thencodika-os sync --fullto back-fill. The skill does NOT editseeds/projects.yamlon its own. - Skip — no action; the suggestion lives in the audit table and can be revisited later.
See Project taxonomy operations for the full workflow.
Endpoints
Section titled “Endpoints”GET /api/tracking/unattributed?month=YYYY-MM[&workerEmail=…]— returns sessions withproject_id IS NULLfor the bearer’s worker in the given month, plus the active project catalog grouped by category.POST /api/sync/classifications— accepts{ classifications: [...], modelHint? }, applies them, and returns counts.GET /api/projects— full project catalog (used bycodika-os syncto load the path-classifier index).
All three are Bearer-authenticated and respect the ?mode= query parameter for the dev/prod split.
Where it differs from the old design
Section titled “Where it differs from the old design”Old (topic model) | New (project model) | |
|---|---|---|
| Attribution leaf | topics (single kind field doing rollup + discovery) | projects (category + optional subcategory, declarative discovery) |
| LLM runs | claude -p subprocess per session | Operator’s current agent context |
| Audit method values | path / llm-local / llm-failed / llm-usecase | path / agent-inline / subagent-heuristic / manual |
| Suggested-new payload | { suggestedSlug, suggestedKind } | { suggestedSlug, suggestedCategory, suggestedSubcategory } |
| Path prefixes | topics.path_prefixes text[] | project_path_prefixes table (one row per prefix, carries its own subcategory) |
Historical rows from the old design were dropped in the v3 wipe-and-reseed; there is no compat layer.