Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/modiqo/skillspec/llms.txt

Use this file to discover all available pages before exploring further.

Every SkillSpec contract contains a decision model — a deterministic fold over an ordered rule set that maps an input to a selected route, a forbidden set, a list of clarifying questions, and a set of post-task closures. The fold is evaluated without invoking a model: given rule order and a user input, the decision is reproducible and testable. This is intentionally boring. The goal is inspectable steering, not a hidden policy language.

Routes

Routes are the candidate ways to satisfy work. Each route has a stable machine id, a human label, and an optional rank. Lower rank means earlier default preference when no rule has explicitly selected a route.
routes:
  - id: one_shot_process
    label: Capture a one-shot process
    rank: 10
    description: Use `rote exec --` for finite commands whose stdout, stderr, exit, or file outputs should be remembered.

  - id: background_process
    label: Start and track a background process lease
    rank: 40
    description: Use for long finite non-interactive jobs that need status, wait, follow, or cleanup.

  - id: browser_handoff
    label: Hand off to rote-browse for browser state
    rank: 5
    tool_boundary:
      default: deny
      allow:
        - skillspec_cli
        - rote_exec
      permission_required_for:
        - any_unlisted_tool
Routes may also declare:
  • checks — a list of command ids that must pass before the route is used
  • tool_boundary — a phase-scoped permission contract (see Tool boundaries)
  • execution_plan — an ordered sequence of named phases, each with its own requires, forbid, and optional sub-tool_boundary
The route id is a stable API identifier. Human label and description can change without breaking tests and cross-references; the id must not churn.

Rules

Rules are the conditional steering layer. Each rule has an optional when predicate and a set of typed effects. Rules are evaluated in file order against the current input. When a rule matches, its effects are applied additively to the running decision state.
rules:
  - id: long_noninteractive_jobs_use_background
    when:
      command_likely_long_running: true
      interactive_prompt_likely: false
    prefer: background_process
    after_success:
      - query_lease_status
      - follow_process_stream
      - wait_for_process
    reason: Long finite non-interactive jobs should be leased, observable, and joined.
A rule without a when clause is unconditional — it matches every input. The reason field carries a human-readable rationale that is recorded in the trace and shown in alignment reports.

Predicate fields

The when predicate can combine any of these fields. Multiple fields compose with logical AND. Values inside user_says_any compose with logical OR.
FieldTypeMatches when
user_says_any[string]Input contains any listed phrase (word-boundary aware)
user_says_all_groups[[string]]Input contains at least one phrase from each group
task_recurrence_likelyboolInput signals daily/weekly/recurring work
domain_object_taskboolInput references a structured domain object (ticket, issue, email, repo, etc.)
interactive_prompt_likelyboolInput suggests login, auth, MFA, or a password prompt
command_likely_long_runningboolInput references test, build, deploy, watch, tail, or background process
Phrase matching is case-insensitive and word-boundary–aware — "browse" matches "browse the page" but not "unobserved".

Rule effect algebra

Rules apply in file order. The algebra is additive for most fields, with two exceptions:
decision = {
  input, route, route-order, forbid-set,
  allow-map, elicitation-list, after-success-list,
  matched-rule-list, reason
}

apply(rule):
  if rule.prefer     → route := rule.prefer            # sets (last write wins)
  if rule.route_order → route-order := rule.route_order # replaces current order
  forbid-set     := forbid-set union rule.forbid        # additive
  allow-map      := allow-map merge rule.allow          # additive
  elicitation-list   := append elicitation-list rule.elicit
  after-success-list := append after-success-list rule.after_success
  matched-rule-list  := append matched-rule-list rule.id
  reason         := rule.reason or reason               # last non-empty wins
The key properties:
  • prefer sets the currently selected route — last matching prefer wins in file order.
  • route_order replaces the current ordering — a later rule can completely reorder.
  • forbid is a union — every matched rule adds its forbidden set; nothing removes from it.
  • elicit appends — all matched rules contribute their elicitation requests.
  • after_success appends — all closures from matched rules accumulate.
Two individually valid rule sets can compose to different decisions under different concatenation order. This order-sensitivity is a property to test, not to hide — scenario tests pin the composed outcome for a given input.
After all rules are evaluated, if no rule has set a route via prefer, the first route in the current route_order is selected. This is the default_route_order basis, which appears in the decision trace.

Elicitations

Elicitations are bounded questions. They are used when the skill should ask for a specific missing decision instead of guessing or asking open-ended questions. A rule requests an elicitation by adding its id to elicit. The elicitation itself lives in the elicitations mapping and defines the choices, an optional default, and an optional cap on how many choices may be selected.
elicitations:
  release_approval:
    question: Do you approve running the real mutating release/publish step?
    required_when:
      - missing: release_approval
    max_choices: 1
    choices:
      - id: approve_foreground
        label: Approve foreground run
        description: Run the final irreversible command visibly after preflight and dry-run evidence.
        sets:
          release_approval: foreground
        route: one_shot_process
        next: execute
        safety: destructive
      - id: do_not_run
        label: Do not run
        description: Stop after preflight and dry-run evidence.
        sets:
          release_approval: denied
        next: done
        safety: read_only
Each choice may:
  • sets — write named facts into the decision context
  • route — steer to a specific route if selected
  • next — advance to a named state in the state machine
  • safety — classify the consequence of this choice (read_only, local_write, destructive, etc.)
Choices do not execute commands. They surface a decision. required_when describes when the elicitation is mandatory — for example, when the selected route matches a given id, or when a named fact is absent from the context.
When authoring YAML by hand, quote question strings that contain : . An unquoted question: What are we reviewing: a dependency list? parses as a broken mapping before SkillSpec validation runs.

Tool boundaries

Tool boundaries are phase-scoped permission contracts. They can appear at three levels, inherited from outer to inner:
entry.tool_boundary  →  routes[].tool_boundary  →  routes[].execution_plan.phases[].tool_boundary
entry:
  tool_boundary:
    default: deny
    allow:
      - skillspec_cli
      - rote_exec
    forbid:
      - direct_cli_without_rote_exec
      - direct_shell_command_without_rote_exec
    permission_required_for:
      - any_unlisted_tool
      - any_new_data_source
      - any_external_side_effect
FieldEffect
default: denyEverything not listed requires explicit approval
default: allowEverything not listed is permitted
allowExplicitly permitted tools or actions
forbidExplicitly blocked tools or actions
permission_required_forNamed tools or action classes that require human approval before use
A boundary does not enforce by itself — it gives the harness a concrete pre-tool-call checklist. Any action outside the rendered boundary requires explicit user permission before use.

Complete example

The following contract is drawn from the before-after example. It shows routes, a steering rule, an elicitation, and scenario tests that prove the composed decision:
schema: skillspec/v0
id: example.browser_research
title: browser research
description: Route browser inspection tasks to automation and keep URL discovery separate from page evidence.

routes:
  - id: browser
    label: Browser automation
    rank: 10
  - id: native_search
    label: Native search for URL discovery
    rank: 20

rules:
  - id: browser_tasks_use_browser
    when:
      user_says_any:
        - browse
        - open
        - click
        - snapshot
        - inspect
        - extract from page
    prefer: browser
    forbid:
      - native_search_as_answer
    allow:
      native_search: url_discovery_only
    elicit:
      - target_url
    reason: Page evidence must come from observing the page, not from search-result summaries.

elicitations:
  target_url:
    question: Which URL should be inspected?
    choices:
      - id: user_provided_url
        label: Use the URL in the request
      - id: ask_for_url
        label: Ask for the URL
      - id: discover_url
        label: Search only to discover the URL
    default: ask_for_url
    max_choices: 1

tests:
  - name: browse task uses browser
    input: browse https://example.com and take a snapshot
    expect:
      route: browser
      forbid_exact:
        - native_search_as_answer
      elicit_exact:
        - target_url
      matched_rules_exact:
        - browser_tasks_use_browser

  - name: native search is not the answer
    input: inspect the latest docs page
    expect:
      route: browser
      not_forbid:
        - browser
      forbid:
        - native_search_as_answer
Run the decision engine against these tests with:
skillspec test skill.spec.yml
And run it against a specific input to see the decision trace:
skillspec decide skill.spec.yml --input='browse the dashboard' --trace-dir .skillspec/traces

Build docs developers (and LLMs) love