Skip to content

Skills (for AI) ​

MONO apps live in separate repositories, so they can't see what the others have learned. MONO Skills is the shared brain that fixes that: a single private repo holding, per app, its knowledge, reusable skills, technical decisions, and the Claude session history β€” so any app (and any future session) can read what was already figured out.

Everything goes through one CLI:

bash
mono skills <command>

The permanent store is a fixed private repository β€” you never address it directly:

text
EJI-ICT/mono-skills   (branch: main)

Read before you build

When the feature is on, read the app's centralized knowledge/skills first for non-trivial work, and save a session summary after meaningful changes. That's the loop that makes the next session smarter.

Enable it ​

The feature is off by default. Turn it on per app with two env vars:

env
# .env / .env.dev
MONO_SKILLS=true
MONO_SKILLS_GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxx

MONO_SKILLS accepts true / 1 / yes / on (case-insensitive); anything else β€” including empty or unset β€” means disabled. The token is a GitHub PAT with access to EJI-ICT/mono-skills (classic PAT needs the repo scope; for the org it must satisfy the EJI-ICT PAT policy and SAML SSO authorization).

Disabled must never get in the way

When MONO_SKILLS is off (or missing), the whole workflow is a no-op: the commands return { "skipped": true, "reason": "MONO_SKILLS_DISABLED" } and do nothing. It must never block, delay, or change the primary task.

Rename the app first

While an app still uses a default template name (mono-host, mono-vue, mono-vue-host, mono-nuxt-host, mono-vue-remote), save and retry are skipped with { "skipped": true, "reason": "MONO_SKILLS_GENERIC_APP_NAME" } β€” history is not generated under a placeholder id. Rename the app in mono.config.ts (and package.json) to a project-specific name first (see Template Rule 1). mono skills check reports this via genericAppName: true.

You only ever configure those two variables. The repository, branch, staging directory, chunk size, and destination paths are all built in β€” there are no owner / repo / branch / url / token flags, and the CLI rejects them if passed.

Commands ​

Every command prints one JSON object to stdout (so it's parseable), and never prints the token. The only accepted flags are --app, --dir, --type, --path, --query, --limit, --session-id, --dry-run.

CommandWhat it does
mono skills checkVerify setup: feature switch, token presence, repo + branch access, read/write permission, git identity. Never prints the token.
mono skills whoamiShow the local git identity used for attribution: { name, email, actorFolder }.
mono skills read --app <id> --type knowledge|skillsRead an app's centralized knowledge or skills.
mono skills read --app <id> --path <file>Read one safe file (e.g. knowledge/business-rules.md). Path traversal is rejected.
mono skills search --app <id> --query "<text>" [--limit 10]Find relevant knowledge / skills / history summaries β€” ranked, not a full dump.
mono skills save --app <id> --dir .mono/skills/pending/<id>Upload a completed, staged session. The primary command.
mono skills save … --dry-runValidate + redact + report the planned permanent path without uploading.
mono skills retry --app <id> --dir .mono/skills/failed/<id>Re-upload a previously failed session.
mono skills pending --app <id>List sessions staged locally, awaiting upload.
mono skills failed --app <id>List sessions whose upload failed (kept for retry).
mono skills session show --app <id> --session-id <id>Show a remote session's metadata + summary (no conversation).
mono skills templatePrint the canonical staged-session file JSON standard (schemas + examples). Runs even when the feature is disabled.

App id

--app is normalized and, when omitted, derived from the current project (mono.config.ts name, or the .mono/apps/<name> folder you're in). Pass --app explicitly when in doubt.

The AI workflow ​

When the feature is enabled, the loop around a piece of real work is:

text
1. mono skills check                         # confirm config (once)
2. mono skills read   --app <id> --type knowledge   # ground yourself
   mono skills search --app <id> --query "<topic>"  # only if relevant
3. …complete the user's task…
4. stage the session under .mono/skills/pending/<session-id>/
5. mono skills save --app <id> --dir .mono/skills/pending/<id> --dry-run   # optional
6. mono skills save --app <id> --dir .mono/skills/pending/<id>             # upload
7. report the permanent savedPath on success

A staged session is a folder with a few small files:

FileRequiredPurpose
metadata.jsonβœ…Topics, user requests, commands run, validation/outcome.
summary.mdβœ…A concise, reusable summary of what changed and why.
conversation.jsonlwhen enabledOne JSON object per line (user / assistant / tool).
files-changed.jsonwhen files changedPath + operation metadata only (no file contents).
decisions.jsonwhen decisions madeTitle / decision / reason.

On success the CLI returns { "success": true, "savedPath": "…" } and cleans up the local staging. On failure it preserves the staging (moving it to .mono/skills/failed/<id>) so it can be retried β€” and reports honestly.

Two ways to read what past sessions learned ​

  • mono skills read / search (token required) β€” the full, curated app knowledge and skills folders, plus ranked search. Needs MONO_SKILLS=true + MONO_SKILLS_GITHUB_TOKEN.
  • Public no-token files β€” the synced session history is published without auth, split the same way the site's llms.txt / llms-full.txt are:
    • https://mono-libs.pages.dev/llms-skills.txt β€” the summarized index: one short entry per session, grouped by project (newest first). Start here β€” scan it to find the sessions that matter, then follow a session's .md link for its own detail.
    • https://mono-libs.pages.dev/llms-skills-full.txt β€” the full content of every session (summaries + decisions + files changed, concatenated). Read this only when you want the full dump and the index isn't enough.

Both public files are generated at build from the same synced mono-skills data and work even when MONO_SKILLS is off β€” zero setup. Prefer the CLI when the feature is on (targeted + current); fall back to the summarized llms-skills.txt index (then llms-skills-full.txt) otherwise.

Index first, full only when needed

Read the llms-skills.txt index first because it's the summarized view β€” cheap to scan, enough to locate the right session. Reach for llms-skills-full.txt only when you actually need the full content of the matching sessions.

Session file formats (the JSON standard) ​

Only metadata.json and summary.md are required; the rest are optional. save/retry (and --dry-run) validate every staged file against this standard before uploading β€” a mismatch (a bad status, a decisions.json / files-changed.json that doesn't use the right wrapper/fields, or an invalid conversation.jsonl line) fails in the CLI and nothing is written to GitHub. Match these schemas exactly.

Get this as JSON

Run mono skills template to print this same standard (schemas + examples) as one machine-readable JSON object β€” handy to consult right before staging a session.

metadata.json β€” required ​

Fields (from the SessionMetadata type):

FieldTypeNotes
schemaVersionnumber1.
sessionIdstringStable id; also names the session folder (HH-mm-ss_<sessionId>).
status"completed" | "partial" | "failed"Session outcome.
startedAtstring?ISO 8601.
finishedAtstring?ISO 8601. Drives the destination timestamp (HH-mm-ss, local time); falls back to save time if absent/invalid.
topicsstring[]?For search ranking.
userRequestsstring[]?What the user asked for.
commandsRunstring[]?Commands executed.
validation{ testsRun?, testsPassed?, buildRun?, buildPassed? }?Booleans (*Passed may be null).
outcome{ result?: string, notes?: string }?Result summary.
json
{
  "schemaVersion": 1,
  "sessionId": "gallery-image-lightbox",
  "status": "completed",
  "startedAt": "2026-07-02T15:40:00.000Z",
  "finishedAt": "2026-07-02T15:53:00.000Z",
  "title": "gallery-apps: image-only lightbox on the Picsum gallery",
  "topics": ["image lightbox", "gallery", "Picsum"],
  "userRequests": ["Clicking a thumbnail should open an image-only popup…"],
  "commandsRun": ["mono skills check", "npx vue-tsc --noEmit", "npm run dev"],
  "validation": { "testsRun": false, "testsPassed": null, "buildRun": true, "buildPassed": true },
  "outcome": { "result": "shipped", "notes": "Added GalleryLightbox.vue; vue-tsc clean." }
}

Added automatically on upload β€” don't stage these

The uploaded copy of metadata.json is augmented by the CLI with app, actor ({ name, email }), actorFolder, repository, savedPath, and uploadedAt. Stage only your own fields.

Reader field drift

The reference history browser also reads a top-level title, and currently keys on commands / outcome-as-a-string rather than commandsRun / outcome{}. Treat the SessionMetadata names above as canonical; additionally include title (used by the browser), and add commands too if you want the browser to render the command list until the reader is reconciled.

summary.md β€” required ​

Free-form markdown: a concise, reusable "what changed and why" β€” the highest-value artifact for the next session. No schema.

files-changed.json β€” optional (when files changed) ​

An object wrapping a files array; each element is { path, operation, note? } with operation ∈ "added" | "modified" | "deleted" | "renamed". Path + operation metadata only β€” never file contents.

json
{
  "files": [
    { "path": "src/components/GalleryLightbox.vue", "operation": "added" },
    { "path": "src/pages/gallery/index.vue", "operation": "modified", "note": "wire up lightbox" }
  ]
}

decisions.json β€” optional (when decisions made) ​

An object wrapping a decisions array; each element is { title, decision, reason }.

json
{
  "decisions": [
    {
      "title": "Custom Teleport lightbox over mono-modal",
      "decision": "Built a custom Teleport overlay component instead of reusing mono-modal.",
      "reason": "mono-modal always renders a padded card; the design needed an edge-to-edge image-only view."
    }
  ]
}

Use title + reason

Some early sessions staged { decision, why } (no title); the canonical element is { title, decision, reason } β€” the reference reader keys on title/reason. Prefer it going forward.

conversation.jsonl β€” optional (when enabled) ​

One JSON object per line (user / assistant / tool). Stage a single conversation.jsonl; the CLI redacts secrets and chunks it into conversation/part-0001.jsonl, part-0002.jsonl, … (≀ 750 KB per part, never splitting a line). Do not pre-chunk.

index.json β€” generated by the CLI (never stage) ​

The CLI writes this itself, listing every uploaded file in upload order (it does not list itself):

json
{
  "schemaVersion": 1,
  "sessionId": "gallery-image-lightbox",
  "format": "jsonl",
  "files": ["metadata.json", "summary.md", "files-changed.json", "decisions.json"]
}

Registration files β€” generated by the CLI (never stage) ​

Created once per app / per actor and never overwritten:

  • <app>/app.json β€” { "schemaVersion": 1, "app": "<appId>", "createdAt": "<ISO>" }.
  • <app>/history/<actorFolder>/user.json β€” { "name": "<git user.name>", "email": "<git user.email>", "actorFolder": "<slug>__<6-hex>" }.

What gets stored ​

The CLI writes into a deterministic, per-app, per-actor, immutable layout:

text
<app>/
β”œβ”€ app.json
β”œβ”€ knowledge/         # stable, reusable app knowledge
β”œβ”€ skills/            # reusable step-by-step procedures
└─ history/<actor>/YYYY-MM-DD/HH-mm-ss_<session-id>/
   β”œβ”€ metadata.json  summary.md  index.json  …
   └─ conversation/part-0001.jsonl …

You never construct that path β€” the CLI does. Sessions are immutable: a save refuses to overwrite an existing session folder.

The date and time are two separate path segments β€” the CLI builds …/<actor>/<YYYY-MM-DD>/<HH-mm-ss>_<session-id>/ as ${date}/${time}_${sessionId} (local time from metadata.finishedAt, else save time). So 2026-06-29/ genuinely is its own folder containing 07-00-00_<session-id>/.

GitHub collapses single-child folders

GitHub's file browser folds any directory that holds exactly one child into a single breadcrumb row, so a date with just one session shows as 2026-06-29/07-00-00_<session-id> on one line. That is a GitHub display convenience, not a merged or broken folder β€” the date splits onto its own row the moment a second session lands on that day.

Safety ​

Non-negotiable

  • Go through the mono skills CLI only β€” never call the GitHub API, curl, gh, or git directly for this workflow, and never target another repository.
  • The token is never printed, logged, or passed as an argument.
  • A second-pass redaction strips secrets (tokens, keys, passwords, auth headers, connection strings β†’ [REDACTED]) from everything uploaded β€” but still don't stage secrets, .env values, or unnecessary personal data in the first place.
  • Only claim a save succeeded when the result is { "success": true }. Never hide from the user that history was saved, and never fabricate commands, files, or outcomes.

The whole .mono/ directory (including .mono/skills/ staging) is gitignored β€” nothing here is committed into the app repo (see the .mono/ convention in Sync).

When to save β€” and when not to ​

Save for meaningful work: code changes, bug investigations, architecture/DB/API changes, business-rule clarifications, config/security changes, useful debugging findings, reusable guidance (including failed work that produced insight).

Don't save greetings, empty or cancelled sessions, trivial typo fixes, or repeated prompts with no new result.


See also: Sync (how the .mono/ folder is managed), Environment (env vars and the MONO_ prefix), Template Rules (the day-to-day AI contract), and Reference LLMs.