Skip to main content
HUMΛN
Developer Guide
Developer Guide

Building discoverable AI capabilities: the command registration model

rick··7 min·Technical

You shipped a bundle. The muscle works, the extension installs cleanly, the proof gates register. Then you watch a real user session and nobody invokes any of it. Not because it's broken — because nobody knows it's there.

This is the dark spot problem, and it is the default failure mode of every conversational platform. In a traditional SaaS app, a feature you ship gets a nav item, a button, a tab — pixels that advertise its existence. In a Companion-first product there is no nav. The interface is an input field. Anything the user doesn't think to ask for might as well not exist.

HUMΛN's answer is structural: a bundle that registers no commands is invalid. validateBundleManifest() throws a BundleValidationError if your commands[] array is missing or empty, and CI fails the build. Discoverability isn't a docs page or a launch email — it's a typed contract enforced before your bundle ever reaches an org. This post walks through that contract, shows how the intent classifier actually matches your triggers (so you can stop writing triggers that lose), and uses the real HITL bundle as the worked example.

The BundleCommand contract

Every capability your bundle contributes to the Companion is declared as a BundleCommand. This is the real interface from @human/platform-extensions:

export interface BundleCommand {
  /** Unique within the bundle, e.g. 'show_approvals' */
  id: string;
  /** Display name shown in the palette, e.g. 'Show approvals' */
  name: string;
  /** One-line description shown in the palette */
  description: string;
  /** 'lucide:<IconName>' scheme or absolute URL to icon image */
  icon: string;
  /** Palette slash shortcut, e.g. '/approvals' */
  shortcut?: string;
  /** Grouping label in the palette UI */
  category?: string;
  /** If set, this command opens this canvas kind */
  canvas_kind?: string;
  /** Natural-language phrases the intent classifier can match to invoke this command */
  intent_triggers: string[];
  /** When true, the Companion may invoke this command without explicit user confirm */
  autonomous_ok?: boolean;
}

Two fields do almost all of the discoverability work, and they target different user behaviors:

  • shortcut serves the user who already knows your command exists. Typing /hitl-sessions is deterministic — exact match, no classifier involved.
  • intent_triggers serves everyone else. These are the natural-language phrases that let a user invoke your capability without knowing it exists. This is where dark spots are created or destroyed, and it's where most bundle authors get it wrong.

How the classifier actually scores your triggers

Before writing triggers, understand what they're matched against. CommandIndexService.resolveBundleCommand() runs a two-stage match over every installed bundle's commands:

  1. Exact phrase containment. If the user's normalized input contains a trigger verbatim, that command scores 0.88.
  2. Word overlap. Otherwise, each trigger is tokenized and scored as (overlapping words / trigger words) × 0.72.

The invocation threshold is 0.6. Run the arithmetic and a hard truth falls out: a single-word trigger can never win cleanly. A one-word trigger that matches scores at most 0.72 — but so does every other bundle's one-word trigger containing that same word, and the resolver keeps only the single best score. Multi-word natural phrasings, by contrast, get the 0.88 exact-phrase score when users say what they actually say, and they degrade gracefully through partial word overlap when users paraphrase.

This is why the rule is: write triggers as sentences users would type, not as keywords you would index.

GOOD vs BAD: three trigger rewrites

Example 1 — the keyword collision

// BAD
intent_triggers: ['approvals', 'hitl', 'review']

Single keywords collide across bundles. The word "review" appears in triggers for code-review bundles, document workflows, and performance tooling. When three bundles all score 0.72 on "review", the resolver picks one winner by iteration order — and two bundles go dark. Worse, "approvals" alone gives the classifier zero signal about which approvals: expense approvals? deploy gates? sign-offs?

// GOOD
intent_triggers: ['what needs my sign-off', 'show pending approvals', 'anything waiting on me']

These are phrases a human actually types into a Companion at 9 a.m. Each is multi-word, so exact containment fires at 0.88, and partial matches ("anything waiting?") still clear the 0.6 bar through word overlap. No other bundle plausibly claims "what needs my sign-off".

Example 2 — the internal jargon trap

// BAD
intent_triggers: ['escalation queue', 'hitl ops', 'operator desk']

These are your team's names for the feature, not the user's. A support lead who has never read your manifest will not type "operator desk" — they'll type "is anyone stuck waiting for a human?" Jargon triggers only match users who already know the feature exists, which is precisely the population that didn't need discovery. You've written triggers for the 5% and left the 95% in the dark spot.

// GOOD
intent_triggers: ['active escalations', 'pending human review', 'awaiting approval', 'who is waiting on a person']

Each phrase describes the outcome the user wants, in the user's own vocabulary. Notice these include the real triggers shipped on hitl.sessions (more on that below).

Example 3 — the over-broad catch-all

// BAD
intent_triggers: ['help', 'show me', 'status', 'check']

This is the greedy failure mode — triggers so generic they match everything. "Status" overlaps with deploy status, billing status, connector health, and a dozen other commands. You aren't increasing your bundle's reach; you're injecting noise into every org's intent routing and making the whole palette worse. The classifier will sometimes route "check my invoices" to your HITL command, the user will hit a confirm prompt for something irrelevant, and they will trust the Companion a little less.

// GOOD
intent_triggers: ['check escalation status', 'status of my escalation', 'did a human pick up my request']

Same intent space, but scoped. Every trigger carries domain-distinguishing words ("escalation", "human") alongside the generic verb, so word-overlap scoring naturally disambiguates against neighboring bundles.

The worked example: the HITL bundle

The humanos.bundle.hitl.v1 manifest in @human/hitl-bundle ships human-in-the-loop escalation for the Companion and deployed agents. Here is its real command registration:

commands: [
  {
    id: 'hitl.sessions',
    name: 'HITL Sessions',
    description: 'View active human-in-the-loop escalation sessions requiring attention',
    icon: 'Users',
    shortcut: '/hitl-sessions',
    category: 'Human-in-the-Loop',
    intent_triggers: ['hitl sessions', 'active escalations', 'pending human review', 'awaiting approval'],
    autonomous_ok: false,
  },
  {
    id: 'hitl.escalate',
    name: 'Escalate to Human',
    description: 'Escalate the current task to a human operator for review and approval',
    icon: 'ArrowUpCircle',
    shortcut: '/escalate',
    category: 'Human-in-the-Loop',
    intent_triggers: ['escalate to human', 'request human review', 'needs human approval', 'escalate task'],
    autonomous_ok: false,
  },
]

Read the trigger lists with the scoring model in mind. hitl.sessions keeps one expert phrase ('hitl sessions') for users who know the term, then three natural phrasings ('active escalations', 'pending human review', 'awaiting approval') that map to how an operator actually thinks about the work. An operator who types "show me pending human review items" hits the exact-phrase score on 'pending human review' and lands on the right command — without ever having opened the palette.

autonomous_ok and the confirm-by-default posture

Both HITL commands set autonomous_ok: false, and that's not an accident — it's the default posture you should adopt. When autonomous_ok is unset or false, the Companion proposes the command and waits for explicit user confirmation before executing. Set it to true only when the command is read-only and reversible — listing sessions, opening a canvas — never for anything that mutates state, notifies humans, or spends money.

This is HUMΛN's human-in-the-loop principle expressed at the manifest level: intent matching is probabilistic, and a 0.62-confidence match on a destructive action should never auto-execute. The confirm step costs the user one tap; a wrong autonomous invocation costs you their trust in every future suggestion.

Where your commands surface: the three discovery surfaces

Registration buys you placement on all three Companion discovery surfaces — and per Canon, these three are exhaustive:

  1. The / command palette. Typing / as the first character (or tapping the / toolbar button on mobile) shows every registered bundle and connector command, grouped by your category label, filterable by name and description. Your shortcut works here as a direct invocation.
  2. Contextual suggestion chips. Up to 3 chips below the input field, drawn from a pool informed by installed bundles, recent actions, and active signals. Well-named commands with clear descriptions are eligible to surface when context matches.
  3. The home canvas. The tightly-gated 5-card briefing that opens on the first message of a session. Bundle-related suggestions are one of the card types — your commands become reachable from the org's morning briefing when they're the priority.

You write one declaration; the platform handles all three placements. That's the compound payoff of the contract.

Testing discovery locally

Before you publish, verify the index actually serves your commands. Install your bundle into a dev org, then:

curl -s http://localhost:3001/v1/companion/commands \
  -H "Authorization: Bearer $DELEGATION_TOKEN" | jq '.bundle_commands'

The response contains bundle_commands — the full list of BundleCommand entries from every installed bundle in the org, each annotated with bundle_id and bundle_name. The index is cached 60 seconds per org, so if you reinstall and don't see your changes, wait out the TTL (the install path calls invalidateCache(orgDid), but a stale local API process won't).

Then test trigger matching the way a user would: type your natural phrasings into the Companion and confirm the right command is proposed. If a paraphrase you expect to work doesn't clear the threshold, add it as another trigger — triggers are cheap; dark spots are expensive.

The checklist

  • Declare at least one BundleCommand per bundle — validation will reject you otherwise.
  • Write intent_triggers as multi-word natural phrasings in the user's vocabulary, never single keywords or internal jargon.
  • Include one expert-shorthand trigger for power users, three or more natural ones for everyone else.
  • Default autonomous_ok to false; opt in only for read-only, reversible commands.
  • Verify with GET /v1/companion/commands and live paraphrase testing before publishing.

The capability you build is only as real as a user's ability to summon it. Register your commands like the contract they are.


This is Part 2 of the Companion Platform Series. Part 1: The Companion is the new AI UX. Part 3: AI-native mobile UX.