> For the complete documentation index, see [llms.txt](https://ai-os-and-trend-finder.gitbook.io/ai-os-and-trend-finder-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ai-os-and-trend-finder.gitbook.io/ai-os-and-trend-finder-docs/docs/skill-system.md).

# AI OS Skill System

Last audited: 2026-05-26.

This document maps the current AI OS skill system across aggregation, browser storage, the home dashboard, and the `/skills` route. It describes implemented behavior only. Trend Finder scores, sources, evidence, and creator angles are a separate extension system.

## Quick UI Answers

If every skill shows `0m`, that is expected when no per-skill time-saved estimate has been configured. AI OS intentionally defaults every skill to `0` minutes saved per run. A skill can have usage telemetry and an observed usage score while still showing `0m` saved.

The dollars-per-hour field is the conversion rate for time saved. It does not create time saved by itself. The dollar formula is:

```ts
dollarsSaved = (minutesSaved / 60) * hourlyRateUsd;
```

At `$120/hour`, a skill still shows `$0` when its saved-time estimate is `0`. After a skill is configured, the same rate affects the home "Skills saved" summary, the home "Skills saved" expansion rows, the home skill cards, and the `/skills` time-saved totals.

The "Ask AI" button in the home "Skills saved" expansion only reveals a terminal command to copy. It does not run automatically, parse the response, or write estimates back into AI OS. The user still enters the resulting minutes manually in the UI or places sanitized JSON in the optional local override file.

Skill signal scores and skill saved-time estimates are separate. Signal scores come from aggregation. Saved-time estimates come from browser `localStorage`, optionally overlaid by repo-local `data/` JSON files in dev.

## Main Concepts

Skills have three distinct data streams:

| Stream                                 | Source                                    | Used For                                           | Writes To                                                 |
| -------------------------------------- | ----------------------------------------- | -------------------------------------------------- | --------------------------------------------------------- |
| Installed and observed skill telemetry | Local Claude Code and Codex scans         | Skill list, uses, last used, observed signal score | `src/data/live-data.json` under `skills.active`           |
| Saved-time estimates                   | User input or optional `data/` JSON files | `0m`, minutes saved, dollars saved, ROI displays   | Browser `localStorage`; file overrides hydrate state only |
| Recommended skills                     | Data contract only today                  | Recommendation cards if populated                  | `skills.recommended`, currently empty from aggregation    |

Do not treat one stream as another. In particular:

* A high signal score does not mean a skill saved time.
* A configured saved-time estimate does not change the usage signal score.
* `skills.recommended` is not populated by the current aggregator.

## End-To-End Pipeline

1. `scripts/aggregate.ts` runs local scanners.
2. `scripts/lib/skill-scanner.ts` scans supported Claude Code and Codex skill roots and logs.
3. Aggregation merges installed and observed rows.
4. `scripts/lib/skill-score.ts` adds an observed usage signal score only when `totalUses > 0`.
5. Aggregation writes `skills.active` into generated private runtime data at `src/data/live-data.json`.
6. Vite dev middleware serves the generated file through `/__live-data`.
7. `useLiveData()` fetches `/__live-data` and validates it with `validateLiveData()`.
8. The home route derives display skills through `deriveSkillsFromLive()`.
9. The `/skills` route derives display rows through `buildSkillsFromLive()`.
10. `useTimeSaved()` loads user-configured minutes and hourly rate from browser `localStorage`, then overlays valid optional file values from `/__time-saved-config` in local dev.

The committed fallback `src/data/live-data.example.json` is safe sample data. The generated `src/data/live-data.json` is private local state and must stay out of git.

## Scanner Coverage

Current skill telemetry coverage:

| Agent/runtime | Installed skill roots                                                  | Usage roots                                                 |
| ------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------- |
| Claude Code   | `~/.claude/skills`, `~/.claude/plugins/**/skills`                      | `~/.claude/projects/**/*.jsonl`                             |
| Codex         | `CODEX_HOME/skills`, discovered Codex plugin/cache `skills` containers | `CODEX_HOME/sessions/**/*.jsonl` when rows match the parser |

`CODEX_HOME` falls back to `~/.codex` when it is unset or invalid.

The scanner counts two signal types:

* User messages that start with a slash command matching `/^\s*\/([a-zA-Z][a-zA-Z0-9_-]{1,40})(?:\s|$)/`.
* Log lines containing skill file paths matching `/\/skills\/([a-zA-Z][a-zA-Z0-9_-]{1,60})\/SKILL\.md/g`.

Scanner output uses this shape:

```ts
{
  name: "/review",
  uses7d: 4,
  totalUses: 18,
  lastUsed: "2h ago",
  lastUsedMs: 1778846400000,
  source: "Claude Code, Codex"
}
```

Important limits:

* Any prompt beginning with `/name` can count, even if it is a built-in command.
* The Codex parser accepts known user-message row shapes, not every possible Codex telemetry format.
* Gemini, OpenClaw, Hermes, and Pi-agent skill histories are not first-class skill telemetry sources today.
* The scanner does not read skill frontmatter, inputs, outputs, quality, success/failure, retries, or ratings.

## Aggregated Skill Rows

`scripts/aggregate.ts` emits:

```ts
skills: {
  active: buildActiveSkillRows(skillStats, skillScoreNow),
  recommended: [] as never[],
}
```

Observed rows with `totalUses > 0` receive:

```ts
{
  score: 68,
  scoreBasis: "observed-usage-v1",
  scoreBreakdown: {
    recentUse: 31,
    totalUse: 16,
    recency: 16,
    sourceCoverage: 5
  }
}
```

Installed-only rows stay unscored. The UI renders those as `No signal`; this is not an error state.

The current real aggregation output does not emit `category`, `workspace`, `inputs`, `outputs`, or configured saved-time minutes. Those are route-level display concerns or browser-local settings.

## Observed Signal Score

The observed signal score is deterministic and usage-derived. It is not a skill quality score.

Formula:

```ts
score = round(recentUse + totalUse + recency + sourceCoverage)

recentUse = 45 * min(log1p(uses7d) / log1p(20), 1)
totalUse = 25 * min(log1p(totalUses) / log1p(50), 1)
recency = 20 * freshness(lastUsedMs, now, 30 days)
sourceCoverage = source contains multiple roots ? 10 : source exists ? 5 : 0
```

`freshness()` is `1` for current or future timestamps, decays linearly to `0` over 30 days, and is `0` when `lastUsedMs` is missing.

Route display tiers:

| Score   | Label       |
| ------- | ----------- |
| `80+`   | High signal |
| `60-79` | Active      |
| `40-59` | Emerging    |
| `<40`   | Dormant     |
| Missing | No signal   |

## Saved-Time Storage

Saved-time values are browser-local and shared by the home dashboard and `/skills`. In local dev, optional repo-local files can override the initial hydrated values without changing `skills.active` or generated live data.

`src/lib/time-saved.ts` owns the hook:

```ts
useTimeSaved();
```

Storage keys:

| Value                           | Current key            | Legacy keys read for migration                         |
| ------------------------------- | ---------------------- | ------------------------------------------------------ |
| Per-skill minutes saved per run | `ai-os.time-saved.v1`  | `findtrend.time-saved.v1`, `claude-os.time-saved.v1`   |
| Hourly rate                     | `ai-os.hourly-rate.v1` | `findtrend.hourly-rate.v1`, `claude-os.hourly-rate.v1` |

Defaults:

* Minutes per skill: `0`
* Hourly rate: `120`

Optional dev file overrides:

| File                          | Shape                                  | Notes                                   |
| ----------------------------- | -------------------------------------- | --------------------------------------- |
| `data/ai-os.time-saved.json`  | `{ "skill name": minutes }` or wrapped | Supplies minutes saved per run by skill |
| `data/ai-os.hourly-rate.json` | `120` or `{ "hourlyRateUsd": 120 }`    | Supplies the calculator hourly rate     |

Wrapped files are also accepted:

```json
{
  "version": 1,
  "minutes": {
    "/recall": 12,
    "/wrap-up": 8
  }
}
```

```json
{
  "version": 1,
  "hourlyRateUsd": 120
}
```

Precedence during hydration is:

```
valid data/ file value -> localStorage migrated value -> default
```

The files are private operator config and are gitignored. Sanitized examples are committed at `data/ai-os.time-saved.example.json` and `data/ai-os.hourly-rate.example.json`. Invalid entries are dropped, malformed files are ignored with endpoint warnings, and missing files are normal.

Write behavior:

* `setMinutesFor(skillName, value)` rounds to the nearest integer and clamps at `0`.
* `setRate(value)` rounds to the nearest integer and clamps at `0`.
* `resetAll()` clears current and legacy saved-time/rate keys and restores the default hourly rate.

The browser does not write `data/ai-os.time-saved.json` or `data/ai-os.hourly-rate.json`. UI edits update the current browser state and `localStorage`; a hard reload reapplies valid file values when those files exist.

The setup wizard has a separate valuation config stored under `ai-os-config`. `VITE_CLAUDE_OS_HOURLY_RATE_USD` can seed or lock that setup value, but the current saved-time calculator reads `ai-os.hourly-rate.v1`.

## Saved-Time Math

The shared period helper in `src/lib/time-saved.ts` derives runs from the seven-day usage counter:

```ts
runsIn(uses7d, "day") = uses7d / 7;
runsIn(uses7d, "week") = uses7d;
runsIn(uses7d, "month") = (uses7d / 7) * 30;
```

The `/skills` page uses this helper directly for day, week, and month.

The home KPI strip labels its period selector as Today, 7 days, and 28 days. For the home "Skills saved" KPI and expansion, the internal month option uses a 28-day factor because the home spend strip is built around a four-week view.

Every saved-time display eventually follows the same core calculation:

```ts
minutesSaved = minutesPerRun * projectedRuns;
dollarsSaved = (minutesSaved / 60) * hourlyRateUsd;
```

## Home Dashboard Mapping

Source route: `src/routes/index.tsx`

Primary components:

* `src/components/home/kpi-strip.tsx`
* `src/components/home/skills-saved-expansion.tsx`
* `src/components/home/skill-card.tsx`
* `src/lib/home-transforms.ts`

### Top KPI Cards

The top three cards are separate systems:

| Card         | Click behavior           | Data source                             | Notes                                   |
| ------------ | ------------------------ | --------------------------------------- | --------------------------------------- |
| AI Spend     | Expands spend details    | Subscriptions, daily usage, model split | Not part of the skill saved-time system |
| Skills Saved | Expands ROI tuning table | `skills.active` plus `useTimeSaved()`   | Uses configured minutes and hourly rate |
| Activity     | Static KPI card          | Summary messages, daily activity        | Not part of the skill saved-time system |

The "Skills Saved" collapsed KPI uses:

* skill names and `uses7d` from `deriveSkillsFromLive()`
* per-skill minutes from `minutesFor(skill.name)`
* hourly rate from `useTimeSaved().rate`
* the home KPI period selector, defaulting to the 28-day view

If no skill has `minutesFor(skill.name) > 0`, the collapsed card shows `$0` and the subtitle prompts configuration.

### Skills Saved Expansion

The expansion table is opened by clicking the "Skills Saved" KPI card.

It renders:

* "Ask AI" helper command
* shared hourly-rate input
* one row per detected skill
* `uses` projected for the selected home KPI period
* editable "Time saved per run"
* dollars saved for each row

The "Ask AI" command is:

```sh
claude "Look at my skills in ~/.claude/ and estimate how many minutes each one saves per run compared to doing it manually. Output JSON like {\"skillName\": minutes}. Be conservative -- only count time the skill genuinely saves."
```

That command is advisory. AI OS does not execute it or write estimates from the response. The user can enter the resulting minutes in the UI or place sanitized values in `data/ai-os.time-saved.json` for local dev hydration.

### Home "Your Skills" Cards

The home skill cards below the KPI strip use the same `minutesFor(skill.name)` values and the same hourly rate. They are shown as a weekly view from the home route's fixed `period: "week"`.

Clicking a skill card navigates to `/skills`, where the per-run estimate can be edited inline.

## `/skills` Route Mapping

Source route: `src/routes/skills.tsx`

Primary transforms:

* `buildSkillsFromLive()` in `src/lib/route-transforms.ts`
* `liveTotals()` in `src/lib/route-transforms.ts`
* `useTimeSaved()` in `src/lib/time-saved.ts`

The route renders:

* hero image and capability count
* total seven-day invocations
* empty state when no skill rows exist
* time-saved summary for day, week, or month
* hourly-rate input and reset button
* top savers list
* usage-by-category chart
* category filter chips
* grouped skill cards
* observed score breakdown when score metadata is present
* per-skill "Saves N min/run" inputs

The `0m` values come from:

```ts
getDefaultMinutes(_name) {
  return 0;
}
```

Each skill card's "Saves" input writes to the same `ai-os.time-saved.v1` map used by the home dashboard. Once a non-zero value is entered, the card's saved time and the route's total time/dollar summary update.

The route's hourly-rate input writes to `ai-os.hourly-rate.v1`. The reset button clears both saved-time estimates and the rate.

### `/skills` View Model

`buildSkillsFromLive()` maps `skills.active` into `SkillRow`:

| Field            | Source                                          |
| ---------------- | ----------------------------------------------- |
| `name`           | `SkillEntry.name`                               |
| `category`       | inferred by `inferCategory(name)`               |
| `scope`          | currently hardcoded to `global`                 |
| `workspace`      | currently `null`                                |
| `lastUsed`       | `SkillEntry.lastUsed`                           |
| `status`         | derived from `uses7d` and `totalUses`           |
| `inputs`         | currently empty array                           |
| `outputs`        | currently empty array                           |
| `uses`           | `uses7d`                                        |
| `totalUses`      | `totalUses`                                     |
| `score`          | validated `SkillEntry.score`                    |
| `scoreBasis`     | `SkillEntry.scoreBasis` when score is valid     |
| `scoreBreakdown` | `SkillEntry.scoreBreakdown` when score is valid |

Statuses:

| Status   | Condition                                               |
| -------- | ------------------------------------------------------- |
| `active` | `uses7d > 0`                                            |
| `stale`  | `uses7d == 0 && totalUses > 0`                          |
| `unused` | `uses7d == 0 && totalUses == 0`                         |
| `broken` | display branch exists, but no current producer emits it |

Categories are inferred by string matching. One caveat: `recall` appears in both Research and Memory rules, but Research is checked first, so `/recall` maps to Research today.

## Recommended Skills

`skills.recommended` exists in the data contract and the home route can render recommendation cards when rows are present. Current aggregation emits an empty array.

The UI model can display:

* command
* basis
* confidence
* predicted hours/dollars saved
* evidence count
* inspiration tags

There is no current producer for these recommendation rows.

## Bundled Skill Relationship

The repository ships imported local skill assets at `skills/dream/SKILL.md` and `skills/personas/SKILL.md`.

`bun run setup` copies Dream to `~/.claude/skills/dream` and copies personas to `~/.hermes/skills/personas` only when that Hermes skill is absent. It does not currently install Dream or personas into `CODEX_HOME/skills`.

Dream reads generated dashboard data separately from `skills.active`. Dream can produce SKILLS-category prescriptions, but those are not `skills.recommended` and do not feed the `/skills` observed score or saved-time estimates.

## Other Skill Touchpoints

* Activity runs store a `skill` string derived from the slash command that started a Claude Code user message. If no slash command is detected, the activity skill is `"Manual session"`.
* Activity outputs inherit the run's `skill`.
* Memory graph event extraction special-cases `/recall` and `/wrap-up` as memory-related events.
* Settings can render `settings.skillFolders`, but current aggregation does not emit a settings branch with skill folder config.
* `skills/README_skills.md` documents the bundled local skill assets; they are not Trend Finder-specific yet.

## Current Limits

The skill system can answer:

* Which supported slash commands or skill files were observed.
* How often each skill was observed in the last seven days.
* How many total observed uses exist.
* When each skill was last observed.
* Which installed Claude Code and Codex skills have no observed uses.
* Whether a skill signal came from Claude Code, Codex, or both.
* A stable `0..100` observed usage signal for observed skills.
* Why the usage signal exists, via score breakdown fields.
* How much time and money the user says each skill saves per run.

It cannot currently answer:

* Whether a skill is good.
* Whether a skill completed successfully.
* Whether a skill saved time automatically.
* Whether a skill deserves a quality rating.
* Which skills should be recommended, except through currently unproduced data.
* Complete multi-agent skill history across Gemini, Pi agent, OpenClaw, Hermes, or Codex log formats that do not match the parser.

## Tests And Coverage

Relevant coverage:

* `scripts/lib/__tests__/skill-scanner.test.ts`
* `scripts/lib/__tests__/skill-score.test.ts`
* `scripts/lib/__tests__/aggregate-skill-output.test.ts`
* `src/lib/__tests__/route-transforms.test.ts`
* `src/lib/__tests__/home-transforms.test.ts`
* `src/lib/__tests__/time-saved.test.ts`
* `src/lib/__tests__/time-saved-hook.test.tsx`
* `src/routes/__tests__/skills.test.tsx`
* `src/routes/__tests__/home.test.tsx`
* `tests/e2e/skills-page.spec.ts`
* `tests/e2e/home-dashboard.spec.ts`

Known weak spots:

* No test decides whether built-in slash commands such as `/compact` should be excluded from skill telemetry.
* No test covers settings `skillFolders` because aggregation does not emit them.
* No tests cover Gemini, OpenClaw, Hermes, or Pi-agent skill roots because no source contract is implemented for those agents.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://ai-os-and-trend-finder.gitbook.io/ai-os-and-trend-finder-docs/docs/skill-system.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
