Project taxonomy
The taxonomy lives in codika-os-app/seeds/projects.yaml. The seeder reads it, walks the workspace, mints projects + path prefixes, and lints the result. The YAML is the source of truth. Don’t edit the projects or project_path_prefixes tables by hand.
If you’re new to the model, read Concepts → Projects first.
Adding a project
Section titled “Adding a project”Most new projects don’t need any taxonomy change at all — they appear automatically.
The common case: a new direct-child folder
Section titled “The common case: a new direct-child folder”Most discovery rules use leaf: direct-child and walk a workspace folder. Adding a project in those categories is just create the folder and re-run the seeder:
| Action | What appears |
|---|---|
mkdir agency/projects/<new-client-slug> | New client:product project. Lints against existing client:knowledge entries. |
mkdir products/<new-product> | New product: project. |
mkdir agency/tools/<new-tool> | New tool: project. |
mkdir core/app/<new-app> | New platform:app project. |
Then, from codika-os-app:
npm run db:sync:projects # devDB_TARGET=prod npm run db:sync:projects # prodThat’s it. No YAML edit. The seeder picks the bare folder name as the slug, generates a display name, assigns a least-used color, and inserts the row plus its path prefixes (canonical + legacy).
The less common case: a new flat_path or prompt_only project
Section titled “The less common case: a new flat_path or prompt_only project”If the project doesn’t live under a direct-child root — it’s a fixed path, or it has no filesystem footprint at all — add a discovery entry to the right category in projects.yaml:
- id: growth display_name: Growth discovery: # … existing rules … - flat_path: workspace/content/podcasts slug: podcasts subcategory: content display_name: PodcastsRun npm run db:sync:projects after.
Verifying
Section titled “Verifying”After seeding, open /tracking. The new project shows up in the per-project table (zero values until a session attributes to it). Drill into a known matching session and confirm the Attribution panel shows method = 'path' with the new project id.
When to add a new subcategory
Section titled “When to add a new subcategory”Rare. Existing subcategories cover the work the team does today; the right defaults are:
- A new client → no taxonomy change. The two subcategories (
knowledge/product) cover every client. - A new internal product →
product(no subcategory). - A new internal tool →
tool(no subcategory). - A new platform repo → fits one of
app/cloud-run/infra/libs/template/marketplace.
Reach for a new subcategory only when the existing ones genuinely don’t fit AND the rollup distinction is worth keeping on every chart from that point on.
Trade-offs vs. a new top-level category
Section titled “Trade-offs vs. a new top-level category”A subcategory and a top-level category solve different problems:
| You want… | Add a… |
|---|---|
A finer breakdown within an existing rollup (e.g. split growth into content/strategy/outreach) | Subcategory |
A new rollup axis that should appear next to client / platform / tool on the headline donut | Top-level category |
Adding a top-level category is a heavier change: it shows up everywhere, every chart legend grows, every code path that enumerates categories needs an update (category-colors.ts, KPI tiles, the classifier schema check). Subcategories live inside an existing category and surface only on drill-down.
If you’re unsure, default to a subcategory. Promoting a subcategory to a top-level category later is just a YAML edit + reseed; the inverse (demoting a category) drops history.
Acting on agent-suggested new projects
Section titled “Acting on agent-suggested new projects”When the agent classifier (the /codika-os:classify-sessions skill) hits work that doesn’t fit any existing project, it returns projectSlug: null plus a suggestedSlug, suggestedCategory, and optional suggestedSubcategory. These rows are persisted in project_attribution_attempts for clustering.
At the end of the skill loop, suggestions are aggregated and surfaced to you. The workflow:
- Agent suggests — the skill collects per-cluster slugs with sample reasonings.
- Team triages — you review, decide whether each cluster is a real recurring project or a one-off. Reject one-offs.
- Seed change — for each accepted cluster, add a discovery rule to
seeds/projects.yaml(or create the folder, for direct-child cases). - Re-classify — run
npm run db:sync:projects(dev + prod), thencodika-os sync --fullto re-attribute existing sessions whose paths now match. Sessions still unattributed can be re-run through/codika-os:classify-sessions.
The skill does not edit seeds/projects.yaml itself. The taxonomy change is always a human decision committed to the repo.
Client slug parity rule
Section titled “Client slug parity rule”A client appears under two folders: agency/crm/codika-clients/clients/<slug>/ for the CRM side, agency/projects/<slug>/ for any product work. The seeder enforces that the slug matches on both sides — otherwise the same client would mint two project rows (creafid vs creafid-app) and rollups would split.
Detecting violations
Section titled “Detecting violations”The seeder fails loudly with a line like:
LINT: client slug parity violation agency/crm/codika-clients/clients/igs-syndic exists but no matching folder under agency/projects/igs-syndic. Either rename one side, or remove the orphan.Fixing
Section titled “Fixing”Pick the canonical slug (usually the public-facing name), then rename the off-side folder:
cd agency/crm/codika-clientsgit mv clients/igs-syndic clients/syndicablegit commit -m "rename: igs-syndic → syndicable"Then grep the workspace for any code or doc still referencing the old slug:
rg -l 'igs-syndic' --type-not lockUpdate those references, then re-run npm run db:sync:projects. The lint passes; existing sessions that touched the old path attribute via the legacy prefix (the seeder keeps clients/igs-syndic as is_legacy = true if you add it explicitly, otherwise sessions on the old path won’t match and are re-classified as unattributed — usually fine after a folder rename since the new path picks up subsequent activity).
The prompt_only mechanism
Section titled “The prompt_only mechanism”Two projects are reached only by the agent classifier:
| Project | When it lands |
|---|---|
growth:outreach | Prompt evidence describes outbound (Lemlist, Apollo, LinkedIn) and no path matches a more specific project. |
knowledge:general | Workspace-rooted reading/exploration that doesn’t fit anywhere more specific. (Also matched at low priority by path against workspace/.) |
When the path classifier runs at sync time, it cannot pick a prompt-only project — there are no path prefixes for it to match. The session is uploaded with project_id = NULL and surfaces in codika-os classifications pending. The agent classifier then picks it from prompt evidence.
If you find yourself wanting to add a third prompt-only project, ask whether the work really has no filesystem footprint. Most “feels like outreach” sessions actually touch workspace/openclaw/docs/campaigns/ or agency/crm/codika-clients/ and land via path matching. prompt_only is the exception, not the default.
- Don’t change a project
slugafter it has sessions attributed. URL routes (/tracking/projects/<slug>) and the CLI classifier output both depend on it. Usealiasto rename what users see. - Don’t add
.codika/as a path prefix. It’s the workspace root and would catch everything. - Don’t edit
projectsorproject_path_prefixesdirectly in SQL. Re-runningdb:sync:projectswill overwrite your change with the YAML’s view of the world. If you need a one-off attribution fix, updatesessions.project_id+attribution_method = 'manual'instead. - Don’t bypass the parity lint. If the seeder complains about client slugs, fix the folders. A drift between the two sides means broken rollups for that client until it’s resolved.