Skip to content

Latest commit

 

History

History
276 lines (196 loc) · 15.2 KB

File metadata and controls

276 lines (196 loc) · 15.2 KB

Skill Architecture — Author Guide

Tip: you can run any of these commands by asking Claude Code in your IDE — see Quick start in README.

This is the canonical reference for adding a Claude Code skill that wraps a YALC capability. Every new skill in .claude/skills/ starts here.


Overview — three skill systems, one of them is yours

YALC ships three different things called "skills". They look similar, run on different runtimes, and live in different places. Only the first is in scope for this guide.

System Location Runtime What it is
.claude/skills/ repo root Claude Code reads SKILL.md frontmatter and routes by trigger phrase Conversational entry points. A user says "qualify these leads" in a Claude Code session, the matching skill activates and drives the conversation + CLI calls. This guide is about these.
configs/skills/ repo root Framework runner reads YAML at scheduled run time Step definitions for framework:run — non-conversational, machine-driven, no human in the loop.
~/.gtm-os/skills/ user home skills:create wizard scaffolds + the runner reads at execute time User-authored skills generated by the skills:create wizard. Persisted per-user, never in the repo.

If you're adding a conversational entry point — keep reading. If you're adding a framework step or a user-wizard skill, this is the wrong document.


When to write a skill

Write a .claude/skills/<slug>/SKILL.md when:

  • A specific phrasing (or family of phrasings) should always route to a specific YALC flow. "Qualify these leads", "score these prospects", "run leads through the gates" → qualify-leads.
  • The user benefits from a guided conversation around the call (validating inputs, choosing between options, confirming destructive operations). The skill body is where that conversation lives.
  • The flow is high-frequency or high-value enough to justify the maintenance cost. Wrapping every CLI command would dilute trigger phrases and make the linter scream.

Don't write a skill when:

  • A user can call pnpm cli <command> directly with one line of input. Adding a skill for trivial commands clutters the namespace and creates trigger collisions.
  • The flow is exploratory and isn't repeated. Use a one-off /tmp/...mjs script instead.
  • The work belongs in a framework runner step (multi-step batch over time) — that's configs/skills/, not .claude/skills/.

Skill body patterns — Pattern A and Pattern B

YALC uses a hybrid runtime: shell-out for SIDE-EFFECTING commands, import-direct for PURE commands. The benchmark numbers below justify the split.

Pattern A — SIDE-EFFECTING (shell-out)

The default. Use this when the wrapped command writes to the database, calls an external API, modifies framework state, sends a message, or otherwise mutates something outside the read-only registry.

Reference: .claude/skills/provider-builder/SKILL.md. Read the whole thing before authoring your first shell-out skill.

Body skeleton:

Frontmatter:
  name: <kebab-case slug>
  description: |
    Use when the user says <trigger phrase 1>, <trigger phrase 2>, ...
  version: 1.0.0

Body:
  1. Greeting + scope statement ("I'll wrap leads:qualify. I'll ask
     three questions then call the CLI.").
  2. Ask for inputs via natural conversation, one question at a time.
  3. Local validation (URL format, file exists, required env var set
     via `process.env.X ? 'set' : 'unset'` — never read the value).
  4. Run the CLI:
       Bash(cd ~/Desktop/gtm-os && set -a && source .env.local && set +a && \
            npx tsx src/cli/index.ts <command> <args> --json)
  5. Parse the CLI's JSON output. On non-zero exit, surface the CLI's
     stderr verbatim — see "Failure surfacing" below.
  6. Render success cleanly with a follow-up offer ("Want me to also
     run campaign:track?").

Why shell-out is mandatory for side-effecting commands:

  • The CLI command is the source of truth for environment loading, tenant resolution, diagnostics wrapping, and exit-code semantics. Re-implementing those in the skill body drifts.
  • A user running pnpm cli leads:qualify ... directly should get bit-for-bit the same outcome as the skill. Shell-out guarantees that.
  • DB writes, API calls, and Notion writes are all wired through withDiagnostics() at the CLI layer. That wrapper reports failures to ops; bypassing it via direct import means broken alarms.

Pattern B — PURE (import-direct)

Use this only when the wrapped command:

  • Reads from the registry / cache / local YAML / config file.
  • Runs deterministic rules over those inputs.
  • Returns the same answer for the same inputs every time.
  • Never writes to disk, never calls the network, never calls Anthropic.

Tier 4 skills (list-adapters, show-routine, run-doctor) fit this pattern. The proposal generator at src/lib/routine/generator.ts is a worked example — pure rules over a few lookup values.

Body skeleton:

Frontmatter: same shape as Pattern A.

Body:
  1. Greeting (often no input is needed — these are read-only).
  2. Ask for inputs only if the function needs any.
  3. Generate a tiny TS snippet at /tmp/yalc-skill-<slug>.mjs that:
       - imports from the actual lib path (e.g.
         file:///abs/path/to/src/cli/commands/adapters-list.ts)
       - calls the function with the user's inputs
       - prints JSON to stdout
  4. Bash(cd ~/Desktop/gtm-os && npx tsx /tmp/yalc-skill-<slug>.mjs)
  5. Parse + render.
  6. **Fall back to shell-out if the inline runner errors.** This gives
     import-direct the safety of shell-out without committing to it
     forever. If a "pure" function later grows side effects, the
     fallback catches it instead of silently producing stale data.

The fallback rule is non-negotiable. Pure functions sometimes grow side effects under refactor (a cache write, a telemetry call). The fallback means the skill keeps working through the regression while a maintainer fixes the lib.


The hybrid runtime rule (with real numbers)

The split exists because chaining three CLI commands in a row is slow. Five trials each on this machine (M-series macOS, Node 20, tsx 4.x, dependencies symlinked from the repo's node_modules/):

Scenario min (ms) median (ms) max (ms)
A: shell-out, single (adapters:list --json) 736 764 1552
B: shell-out, chained ×3 (adapters:list + routine:propose + framework:list) 2387 2550 3070
C: import-direct, single (runAdaptersList via inline tsx) 699 723 738
D: import-direct, chained ×3 (single tsx, three imports) 1329 1343 1441

Numbers from node scripts/bench-skill-runtime.mjs against ~/Desktop/gtm-os. Re-run after any meaningful change to src/cli/index.ts or package.json dependency tree.

Read the table this way:

  • Single-command skills (A vs C). Shell-out adds ~40ms over import-direct (764 vs 723ms median). That's a rounding error on a flow the user is already waiting for. For single-command skills, shell out unconditionally — the engineering simplicity is worth more than 40ms.
  • Chained skills (B vs D). Three shell-outs in series cost 2550ms vs one tsx with three imports at 1343ms — a ~47% reduction. That's the win. The Tier 4 chained reads (e.g. an "everything I need to know about my YALC" composite read) get import-direct.
  • The Commander program loads a lot of stuff. Scenario A's 764ms is mostly Node + tsx + the full src/cli/index.ts lazy-import graph (98 commands' wires + dotenv + diagnostics). Scenario C's 723ms is Node + tsx + only the lib chain runAdaptersList actually pulls. The 40ms gap underestimates the true Commander tax — under load (or on a colder machine), expect more.

Rule:

  • Side-effecting? Always Pattern A (shell-out). No exceptions.
  • Pure, single-command? Pattern A by default. The 40ms doesn't justify the maintenance cost of two patterns.
  • Pure, three-or-more chained reads? Pattern B (import-direct). The chained subprocess tax compounds; the import-direct script lets you load the lib graph once.

The benchmark script writes its raw numbers to scripts/.bench-skill-runtime.last.json. The schema is documented and tested — docs/ and downstream consumers can rely on schemaVersion: 1.


Trigger-phrase rules

Every skill activates on phrases extracted from its frontmatter description. The trigger-phrase linter at scripts/lint-skill-triggers.mjs runs in CI and rejects any merge that introduces a substring overlap.

Hard rules:

  1. Quote your trigger phrases inside the description. Use the same bulleted-list-of-quoted-phrases shape provider-builder/SKILL.md uses. The linter expects this format.
  2. Run the linter locally before submitting. node scripts/lint-skill-triggers.mjs from the repo root. Exit 0 means clean.
  3. Avoid bare verbs. "Send", "run", "check" — those collide with every other skill. Anchor the trigger to a noun + verb pair: "send cold email", "run leads qualification", "check campaign status".
  4. Avoid common collision patterns:
    • Bare analyze / report — overlaps with monthly-report, sentiment-analysis, campaign-intelligence at minimum.
    • for [client] / for [campaign] — every client-scoped skill ends up matching.
    • First-person my X — "my campaigns", "my leads" — picked up by every skill that touches a user-owned object.
  5. Include both the formal name and the colloquial. "Qualify these leads" and "score these prospects" should both fire qualify-leads. The linter checks for collisions, not coverage; coverage is on you.

Failure surfacing — verbatim, never summarised

When the wrapped CLI exits non-zero, surface its stderr verbatim in the chat. Do not summarise. Do not rewrite. Do not suppress.

Wrong:

The CLI failed because the API key is missing. Please set it.

Right:

leads:qualify failed (exit 2):

Error: ANTHROPIC_API_KEY is not set.
At src/lib/anthropic/client.ts:14
  throw new Error('ANTHROPIC_API_KEY is not set.')

To fix: edit ~/.gtm-os/.env and re-run.

Why verbatim:

  • The CLI's error messages are tested and stable. They include line numbers and remediation hints. Skills that paraphrase strip the actionable parts.
  • Users debug by pasting the error into search. A summary breaks that.
  • If the CLI message is bad, the fix is in the CLI, not in every skill that wraps it.

In Pattern A bodies, this is one line: pipe stderr to the chat. In Pattern B bodies, the inline tsx script's stderr already lands in the bash output — same result. If your fallback fires, surface both errors (the import-direct one AND the shell-out one) so the user can see whether the regression is in the lib or in the CLI.


Slug naming — three places, one source of truth

Surface Format Example
CLI command area:command (colons) leads:qualify
Skill folder verb-noun (kebab) qualify-leads
yalc.ai page matches skill folder yalc.ai/skills/qualify-leads/

The CLI keeps colon-namespacing because Commander's --help groups commands by area prefix. The skill folder uses natural-language order because trigger phrases match natural language. The yalc.ai slug matches the skill folder so users can hop from a LinkedIn post to the skill landing page to the GitHub source without translating between formats.

The 16 wrappers in the 0.13.0 plan map this way:

CLI Skill folder Tier
leads:qualify qualify-leads 1
personalize personalize-message 1
leads:scrape-post scrape-post-engagers 1
campaign:create (+ create-sequence) launch-linkedin-campaign 1
routine:propose (+ routine:install) build-routine 1
signals:similar find-lookalikes 2
signals:enrich enrich-with-signals 2
research research-prospect 2
competitive-intel run-competitive-intel 2
leads:import import-leads 3
email:send (+ email:create-sequence) send-cold-email 3
campaign:track track-campaigns 3
linkedin:answer-comments answer-linkedin-comments 3
adapters:list list-adapters 4
routine:propose (read-only) show-routine 4
doctor run-doctor 4

Tier 4 is the only tier that gets Pattern B. Everything else is Pattern A.


Onboarding interruption guard

A user partway through yalc-gtm start (the Setup wizard) should not have a long-running skill kick off mid-Setup and fight for the same resources. The recommended check:

Pre-flight (do this before any other step):
  Bash(test -f ~/.gtm-os/.in-flight-setup && echo "BLOCKED" || echo "OK")
  If output is BLOCKED:
    Stop. Tell the user:
      "It looks like Setup is mid-flight. Finish `yalc-gtm start` first,
       then re-invoke me."
    Exit cleanly.

The ~/.gtm-os/.in-flight-setup flag is the convention going forward. As of 0.12.0 the Setup wizard does not yet write this flag — adding the flag write is a future change to bin/yalc-gtm.mjs (or wherever the wizard lives). Skills should still include the check today; it's a no-op until the flag exists, and it lights up automatically when the wizard ships the write.

If your skill is fast (Tier 4 read-only) and the user explicitly invokes it during Setup, you can soften this to a warning instead of a hard block. Anything Tier 1–3 should hard-block.


Reference checklist — pre-submit

Run through this before opening a PR:

  • Skill folder is at .claude/skills/<verb-noun>/.
  • SKILL.md frontmatter has name, description, version: 1.0.0.
  • Description has a bulleted list of quoted trigger phrases.
  • Trigger phrases are anchored to noun + verb (no bare run / check / analyze).
  • node scripts/lint-skill-triggers.mjs exits 0.
  • Body picks Pattern A or Pattern B per the hybrid rule.
  • Pattern B skill includes a fall-back to Pattern A on import-direct error.
  • CLI command(s) wrapped are listed in ## Tools used at the bottom.
  • Failure-surfacing prints CLI stderr verbatim, no summarisation.
  • Onboarding interruption guard is the very first step (~/.gtm-os/.in-flight-setup check).
  • References (references/*.md) are listed and exist if cited.
  • Three random trigger phrases activate the skill from a fresh Claude Code session.
  • Killing the relevant API key surfaces the actual CLI error to the user.
  • pnpm typecheck clean, pnpm test ≥ baseline, pnpm build:web clean.

If every box is checked, your skill is ready for review.


Out of scope for this guide

  • Migrating the 8 pre-0.13.0 skills. Audit-only — see CONTRIBUTING.md.
  • configs/skills/ framework runner steps. Different runtime, different review path.
  • ~/.gtm-os/skills/ wizard output. Created by skills:create, not authored by hand.
  • yalc.ai landing-page generation for the skill. That happens after the skill ships, via the skill-launch-page skill against the published .claude/skills/ path.