pip install <market-data-lib> when a connected MCP server already provides the same data, or from writing a workflow from scratch when a visible prompt skill already covers it. It does this by rendering a per-turn capability index into the agent’s dynamic preamble (after tool deferral), and by intercepting exec/process calls that would install a package the operator has marked as redundant.
Both behaviors read from a single operator-owned config block, tooling:, in ~/.comis/config.yaml. This page covers the operator workflow: how to materialize that block, fill it out, and keep it in sync as MCPs and skills come and go.
Concepts
Thetooling: block has four sub-trees, all operator-only:
| Sub-tree | Purpose | Renderer / consumer |
|---|---|---|
tooling.capabilityClusters.clusters | Operator-defined clusters with label, priority, preferOverInstalls. Three reserved IDs (external-integrations, prompt-skills, other-tools) ship with defaults. | Capability-index renderer (per turn) |
tooling.mcp.capabilityHints | Per-MCP hint: cluster, description, replacesPackages. Keyed by MCP server name. | Renderer + install-detour validator |
tooling.skills.capabilityHints | Per-skill hint: cluster, description?, replacesPackages. Keyed by skill name or skill key. | Renderer + install-detour validator |
tooling.installDetours.mode | "observe" / "advise" / "soft-stop" (default "advise") — controls how exec/process reacts to a redundant-install command. | Install-detour validator |
tooling.capabilityIndex.enabled | Boolean, default true. Toggles the per-turn ## Capabilities block in the dynamic preamble. | Prompt-assembly pipeline |
tooling tree is immutable at runtime — config.patch from agent-callable surfaces is rejected by IMMUTABLE_CONFIG_PREFIXES. Changes apply only after a daemon restart. This is why both CLI verbs below have a daemon-state requirement.
See config-yaml for the full schema.
The two CLI commands
Two sub-subcommands undercomis config:
comis config sync-tooling— discovers connected MCPs and installed skills, materializes thetooling:block (or appends/prunes against an existing block).comis config tooling-fill— populates thedescriptionandreplacesPackagesstub fields via the live agent.
tooling: block, and the install-detour subsystem will fire on package overlaps for every hint.
comis config sync-tooling
Three modes:
| Command | Effect | Daemon state |
|---|---|---|
comis config sync-tooling | Inspect — print the discovered/existing/diff/wouldWrite shape. No file mutation. | Up or down. |
comis config sync-tooling --write | Apply — atomic write with a config.pre-sync-tooling-<ISO>-<hex>.yaml backup. Append-only on existing keys (operator hand-edits preserved); prunes hints for removed MCPs/skills. | Down. |
comis config sync-tooling --write --overwrite | Regenerate — wipe and rebuild the managed sub-trees (mcp.capabilityHints, skills.capabilityHints, installDetours, capabilityIndex). Preserves operator-authored tooling.capabilityClusters.clusters byte-for-byte. | Down. |
Discovery rules
- MCP servers: walks
integrations.mcp.servers[].namefrom the active config. - Skills: walks every
agents.<id>.skills.discoveryPathsplus the daemon defaults~/.comis/skillsand~/.comis/workspace/skills. Each first-level subdirectory containing aSKILL.mdis a skill. Duplicates across paths dedupe by skill name (first-loaded wins).
Generated stub shape
Each newly-discovered MCP lands as:tooling.skills.capabilityHints with cluster prompt-skills by default (or comis.capability.cluster from the SKILL.md frontmatter if the skill author declared one). The description for a skill defaults to the SKILL.md frontmatter description.
Append-only contract
Subsequentsync-tooling --write runs never rewrite operator hand-edits to description, replacesPackages, or cluster on existing entries. Only:
- New hints (newly-discovered MCPs/skills) are added.
- Stale hints (MCPs/skills no longer in discovery) are removed.
- Unrecognized operator-authored keys under
tooling.*(a futuretooling.fooor operator-only sub-keys) are left untouched.
Daemon-active guard
--write and --write --overwrite exit 1 with daemon is running — stop it before running sync-tooling --write if the gateway responds to system.ping within 1 second. Inspect mode bypasses this check.
Output
Inspect mode (default) prints, in order:--format json emits a parseable document with discovered, existing, diff, and wouldWrite keys — useful for CI scripting.
--write prints a one-line summary on success: tooling: +N hints, -M hints (backup: <path>).
comis config tooling-fill
Fills description and replacesPackages on a tooling capability hint via the live Comis agent.
What the agent does
The CLI POSTs a strictly-bounded prompt to/api/chat. The prompt asks the agent to return exactly two lines:
command and args from integrations.mcp.servers. For a skill hint, it seeds context with the SKILL.md frontmatter description and asks the agent to refine it.
The response is parsed defensively:
- Anything outside the two contracted lines is stripped (the agent cannot inject
cluster, schema changes, or any other field). - Package names are validated against
/^@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)?$/i(npm scoped + pip name shape). Names that fail are silently dropped with a warning. - If all package names are dropped, the parser surfaces a hard failure and the orchestrator exits 1 without touching the file.
State machine
A successfultooling-fill --yes --restart runs through 17 ordered steps:
- Validate args, resolve hint kind (mcp vs skills,
--kindoverride on ambiguous bare names) - Parse
config.yamlAST viayaml@2.8.4’sparseDocument - Locate target hint, gate idempotency (stub-valued fills freely; non-stub refuses without
--force) - Verify daemon is up (LLM call needs
/api/chat) - POST to
/api/chat - Parse response, strip non-contracted fields
- Validate package names against the npm/pip regex
- Show diff to operator (skip with
--yes); honour--dry-runby exiting here - Get restart authorization (
--restart/ non-TTY requires it explicitly) - Stop daemon via auto-detected supervisor (systemd → pm2 → bare-process)
- Write backup →
config.pre-tooling-fill-<ISO>-<hex>.yaml - AST-mutate via
setHintFields(onlydescriptionandreplacesPackages—cluster, sibling hints, comments preserved) - Atomic write (temp + fsync + rename + parent-dir-fsync; ownership preserved)
comis config validate(with${VAR}env-substitution so${COMIS_GATEWAY_TOKEN}-style configs validate correctly)- Start daemon
- Wait for daemon to be alive — poll
/api/system.pingfor 15 s.systemctl startexits 0 once the unit is queued; this poll catches the case where the daemon then crashes during boot. - Delete recoverable temp file
Idempotency
A hint is “stub-valued” iff:description ∈ {missing, "", "TODO"}, ANDreplacesPackages ∈ {missing, []}
--force overwrites operator-edited values (the agent’s response replaces them entirely; replacesPackages is replace, not merge).
--all
Fills every stub-valued hint in one daemon-restart cycle. Sequential LLM calls per hint, single backup, single atomic write at end. If the agent call fails for hint K, hints 1..K−1 stay committed; K and K+1..N are skipped; the run exits 1 with stderr listing.
--all silently skips operator-filled hints unless --force is also set.
Supervisor auto-detection
The CLI probes in this order:systemctl is-active comis→kind: "systemd"pm2 jlist | jqfinds acomisentry →kind: "pm2"pgrep -f 'node.*daemon\.js'→kind: "bare-process"(refuses to auto-restart — set--restart-cmdexplicitly)
--restart-cmd override runs once, at the start phase (after the file is written). For a stop+start command like "systemctl stop comis && systemctl start comis", the orchestrator does not invoke it twice — the protected window weakens slightly under --restart-cmd (the daemon is up during the file write), but this is operator-explicit and the daemon does not watch config.yaml for changes, so the write doesn’t take effect until the operator’s restart command lands.
File ownership preservation
Runningtooling-fill as root (the only user with systemctl access on a typical VPS deployment) is safe: atomicWriteFile captures the original file’s uid:gid before the write and chownSyncs the new file back to that owner after the rename. Without this, the new file would be owned by root and the unprivileged daemon service user (comis under systemd) would fail to read it at the next restart.
This applies to both sync-tooling --write and tooling-fill since they share the same atomic-write primitive.
Backup retention
After every successful run,pruneOldBackups(homeDir, prefix, keep=5) trims older backups of the same prefix. The tooling-fill and sync-tooling prefixes prune independently — a tooling-fill backup is never deleted by a sync-tooling --write run, and vice versa.
Verifying the round-trip
After filling, confirm the operator-edit-preservation invariant by re-runningsync-tooling:
--write would be a no-op.
You can also send a turn through the gateway and watch for Dynamic preamble assembled in the daemon logs — the clusterCount, activeToolCount, and capabilityIndexTokens fields reflect the assembled capability index:
Troubleshooting
Daemon failed to read config file after sync-tooling --write
Daemon failed to read config file after sync-tooling --write
Likely a stale-permission edge case. Check ownership:If owned by root, restore from the most recent backup or
chown comis:comis ~/.comis/config.yaml.systemctl: 'Start request repeated too quickly'
systemctl: 'Start request repeated too quickly'
systemd’s Not a bug in
start-limit-hit safety mechanism — typically 5 starts in 10 s. Recover with:tooling-fill; it’s a systemd guard against rapid restart loops. Avoid invoking the CLI repeatedly without spacing in CI.tooling-fill: 'Cannot reach Comis daemon — gateway unreachable'
tooling-fill: 'Cannot reach Comis daemon — gateway unreachable'
The CLI’s LLM call goes through the local gateway (The CLI will then stop the daemon to do the file edit, and restart it on completion.
/api/chat). The daemon must be up at invocation time. Start it first:tooling-fill refuses an existing hint without --force
tooling-fill refuses an existing hint without --force
By design — Add
tooling-fill will not overwrite operator-edited values without explicit consent. The error message includes the current values for review:--force to refill, or hand-edit the YAML directly (which sync-tooling --write will then preserve append-only).Validation failed; rolled back to ...yaml. Daemon failed to restart.
Validation failed; rolled back to ...yaml. Daemon failed to restart.
The post-write Run
validateConfig rejected the new file. The orchestrator restored the backup and attempted a restart. If the daemon also failed to restart, the message will tell you so explicitly:systemctl restart comis (or the equivalent for your supervisor). If the rollback’s own write failed (rare — disk full / permissions), the message includes a cp <backup> <config> recovery command.Related
MCP Integration
Connect MCP servers — the upstream of
tooling.mcp.capabilityHintsCustom Skills
Author skills with
comis.capability metadataconfig.yaml reference
Full schema for the
tooling: blockCLI reference
config sync-tooling and config tooling-fill flag tables