Skip to content

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.

  1. 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_id set and attribution_method = 'path'. When it can’t, the session is uploaded with project_id = NULL and left for phase 2.
  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. No claude -p subprocess, 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.

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.

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.

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.

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.

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:

CategoryWhen to setValues
clientAlways (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
platformInherited from the project row — generally don’t override unless the session genuinely crossed boundaries.app | cloud-run | infra | libs | template | marketplace
growthPick based on what the session actually did.content | strategy | outreach
knowledgecompany-os if touching the agents-org plugin; general otherwise.company-os | general
product / tool / privateNever — these categories have no subcategories.

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.

  • Re-running the skill for an already-attributed month is a no-op — the unattributed query filters on project_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_id for sessions where it was NULL). You can’t accidentally overwrite a manual or path-based attribution.

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:

  1. Add to seeds — manually edit codika-os-app/seeds/projects.yaml (or just create the folder for direct-child rules), run npm run db:sync:projects (dev) and again with DB_TARGET=prod, then codika-os sync --full to back-fill. The skill does NOT edit seeds/projects.yaml on its own.
  2. Skip — no action; the suggestion lives in the audit table and can be revisited later.

See Project taxonomy operations for the full workflow.

  • GET /api/tracking/unattributed?month=YYYY-MM[&workerEmail=…] — returns sessions with project_id IS NULL for 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 by codika-os sync to load the path-classifier index).

All three are Bearer-authenticated and respect the ?mode= query parameter for the dev/prod split.

Old (topic model)New (project model)
Attribution leaftopics (single kind field doing rollup + discovery)projects (category + optional subcategory, declarative discovery)
LLM runsclaude -p subprocess per sessionOperator’s current agent context
Audit method valuespath / llm-local / llm-failed / llm-usecasepath / agent-inline / subagent-heuristic / manual
Suggested-new payload{ suggestedSlug, suggestedKind }{ suggestedSlug, suggestedCategory, suggestedSubcategory }
Path prefixestopics.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.