Audit skill connectivity across 8 layers and surface gaps as actionable, machine-readable output. Supports audit (read-only) and ticketize (create Linear tickets for gaps) modes.
Install
npx skillscat add omninode-ai/omniclaude/feature-dashboard Install via the SkillsCat registry.
Feature Dashboard
Dispatch Requirement
When invoked, dispatch to a polymorphic-agent:
Agent(
subagent_type="onex:polymorphic-agent",
description="Feature dashboard audit",
prompt="Run the feature-dashboard skill. <full context>"
)CRITICAL: subagent_type MUST be "onex:polymorphic-agent" (with the onex: prefix).
Announce at start (audit mode): "Running feature-dashboard audit."
Announce at start (ticketize mode): "Running feature-dashboard ticketize."
Provides an automated audit of every skill's connectivity across 8 layers and surfaces gaps as
actionable, machine-readable output. Audit and mutation are strictly separated.
Overview
POST-MERGE EPIC CLOSED ON-DEMAND
git hook (gated) Linear relay any caller
| --mode=audit | --mode=ticketize |
+---------------------------+--------------------------+
|
onex.cmd.omniclaude.feature-dashboard.v1
|
+------------------------------------------+
| NodeSkillFeatureDashboardOrchestrator |
| handle_skill_requested -> claude -p |
+------------------------------------------+
|
+-------------------+----------------------------+
| --mode=audit (read-only) |
| Produces ModelFeatureDashboardResult |
| ALWAYS writes feature-dashboard.stable.json |
+-------------------+----------------------------+
|
+-------------------+----------------------------+
| --mode=ticketize (separate invocation) |
| Reads stable.json, checks freshness, |
| creates Linear tickets for gaps |
+------------------------------------------------+Trigger assignment:
- Post-merge hook:
--mode=audit(always writes stable.json; no Linear tools allowed) - Linear relay:
--mode=ticketize(reads existing stable.json; assumes recent audit ran on merge) - On-demand callers choose explicitly
Discovery Rule
Directories under plugins/onex/skills/ where:
- Name does NOT start with
_AND SKILL.mdexists in that directory
Sort discovered skills by name (alphabetical, deterministic).
audit mode instructions
- Announce: "Running feature-dashboard audit."
- Discover skills per discovery rule. Sort by name. If
--filter-skillis set, limit to that
one skill (kebab-case match). If--filter-statusis set (notall), run all skills but
only include matching statuses in rendered output (stable.json always contains all results). - For each skill:
a. Determinenode_type: readsrc/omniclaude/nodes/node_skill_{slug}_orchestrator/contract.yaml,
extractnode_typefield. If file absent or parse fails:node_type = "unknown".
b. Apply applicability matrix (see below) to determine which checks run.
c. Run applicable checks. Populateevidencelist for every check result (at least 1 entry
per PASS or FAIL).
d. Computestatusvia status rollup rules.
e. BuildModelSkillAuditwith all checks and gaps. - Build
ModelFeatureDashboardResultwith counts and sorted skill list. Computefailedandfail_reasonfrom--fail-onif set. - Always write
{output-dir}/feature-dashboard.stable.json(sorted keys, nogenerated_at),
regardless of--format. Create{output-dir}/if it does not exist. - Render additional formats per
--format:cli: Print summary table to stdout.markdown: Write{output-dir}/feature-dashboard-{date}.md.web: Write{output-dir}/feature-dashboard-{date}.html.all: All three formats.
- If
--fail-onthreshold exceeded: exit non-zero (return failure status).
Stable JSON format — always written, byte-stable across runs:
- Exclude
generated_atfield from the JSON output - Sort all keys (
sort_keys=True/orjson.OPT_SORT_KEYS) - Skills sorted alphabetically by
name - Idempotent: two consecutive runs on the same state produce byte-identical output
ticketize mode instructions
- Announce: "Running feature-dashboard ticketize."
- Load
{output-dir}/feature-dashboard.stable.json. Fail immediately with a clear error if:- File is absent, OR
- File fails to parse as
ModelFeatureDashboardResult
- For each skill where
statusispartialorbroken:
a. Announce: "Creating ticket for {skill} ({N} gaps)"
b. Callmcp__linear-server__save_issuewith:title:[Feature Dashboard] {skill}: {worst_severity} gaps ({N} total)
whereworst_severityis the highest severity ingaps(CRITICAL > HIGH > MEDIUM > LOW)description: Markdown list of all gaps, each withmessageandsuggested_fixteam: value from--teamarg (default:OmniNode)
c. Log returned ticket ID.
- No Linear tools may be called in
auditmode — ticketize is always a separate invocation.
Node Type Classifier
The audit determines which checks apply based on node_type from contract.yaml.
Known node_type allowlist:
ORCHESTRATOR_TYPES = {"ORCHESTRATOR_GENERIC"}
EFFECT_TYPES = {"EFFECT_GENERIC"}
UNKNOWN_TYPE = "unknown"If node_type is not in either set: classify as unknown. Apply only CRITICAL checks.
Downgrade topic checks to WARN instead of FAIL.
requires_event_bus(node_type, event_bus_block) -> bool
An event-driven node requires:
node_typeinORCHESTRATOR_TYPES, ANDevent_buskey present incontract.yaml(even if topic lists are empty)
Non-event-driven nodes (effects, helpers) are not required to declare topics.
Applicability Matrix
| Check | Applies to | Severity if Fail |
|---|---|---|
skill_md |
All skills | CRITICAL |
orchestrator_node |
All skills | CRITICAL |
contract_yaml |
All skills | CRITICAL (WARN if unknown node_type) |
event_bus_present |
Orchestrator nodes only | HIGH |
topics_nonempty |
Orchestrator nodes where requires_event_bus = True |
HIGH |
topics_namespaced |
Orchestrator nodes where requires_event_bus = True |
HIGH |
test_coverage |
All skills | MEDIUM |
linear_ticket |
All skills | LOW |
Connectivity Check Definitions
skill_md (CRITICAL — All)
Pass condition: SKILL.md exists and parses as YAML frontmatter with name and description
fields both present and non-empty.
Evidence (PASS): "plugins/onex/skills/{name}/SKILL.md: name='{name}', description present"
Evidence (FAIL): Reason for failure (missing file, parse error, missing field).
orchestrator_node (CRITICAL — All)
Pass condition: Directory src/omniclaude/nodes/node_skill_{slug}_orchestrator/ exists.{slug} is the kebab-to-snake conversion of the skill name.
Evidence (PASS): "src/omniclaude/nodes/node_skill_{slug}_orchestrator/ exists"
Evidence (FAIL): "src/omniclaude/nodes/node_skill_{slug}_orchestrator/ not found"
contract_yaml (CRITICAL — All; WARN for unknown node_type)
Pass condition: src/omniclaude/nodes/node_skill_{slug}_orchestrator/contract.yaml parses
without error as ModelContractYaml AND node_type is in the known allowlist.
WARN path: File parses but node_type is not in allowlist — check status is WARN.
Evidence (PASS/WARN): "contract.yaml: node_type='{node_type}'"
Evidence (FAIL): Parse error message.
event_bus_present (HIGH — Orchestrator nodes only)
Pass condition: event_bus key is present in contract.yaml (any value, including empty).
Evidence (PASS): "contract.yaml: event_bus block present"
Evidence (FAIL): "contract.yaml: event_bus key absent"
topics_nonempty (HIGH — Event-driven orchestrators only)
Pass condition: len(subscribe_topics) + len(publish_topics) >= 1
Evidence (PASS): "topics: {N} subscribe, {M} publish"
Evidence (FAIL): "topics: 0 subscribe, 0 publish"
topics_namespaced (HIGH — Event-driven orchestrators only)
Pass condition: Every topic in both subscribe_topics and publish_topics matches the
pattern: onex\.(cmd|evt)\.[a-z0-9_-]+\.[a-z0-9_.-]+\.v\d+
Evidence (PASS): "All {N} topics match namespace pattern"
Evidence (FAIL): "Invalid topic(s): {list of non-matching topics}"
test_coverage (MEDIUM — All)
Coverage truth hierarchy (in order):
PRIMARY (definitive PASS):
Read tests/unit/nodes/test_skill_node_coverage.py and extract the skill list. If the skill
name (kebab-case) appears in that list, result is PASS with evidence:"skill '{name}' in canonical coverage list in test_skill_node_coverage.py"
FALLBACK heuristics (only evaluated if skill is NOT in primary list):
- +2:
tests/**/test_*{slug}*file exists (Glob search,{slug}is snake_case) - +1: kebab name found in test file content in a
def test_line context (Grep) - +1: golden path fixture
plugins/onex/skills/_golden_path_validate/node_skill_{slug}_orchestrator.json
exists (Glob)
Scoring:
- Score >= 1: WARN (not PASS). Annotate: "covered by heuristic, not in canonical list"
- Score = 0: FAIL
Evidence (PASS): canonical list citation.
Evidence (WARN): heuristic signals found, with details.
Evidence (FAIL): "No coverage signal found for '{name}' (score=0)"
linear_ticket (LOW — All)
Pass condition: metadata.ticket in contract.yaml matches OMN-[1-9]\d+ (non-zero, not
the placeholder OMN-XXXX).
If --online true: verify the ticket ID exists via mcp__linear-server__get_issue. WARN
if ticket exists but is Done/Cancelled. FAIL if ticket ID not found.
Evidence (PASS): "metadata.ticket='{ticket_id}'"
Evidence (FAIL): "metadata.ticket absent or invalid: '{value}'"
Status Rollup
Applied per skill after all applicable checks complete:
- Any CRITICAL fail →
broken - Any HIGH fail →
broken - WARN on any HIGH check →
partial(notbroken) - All CRITICAL/HIGH pass (or WARN), any MEDIUM/LOW fail →
partial - All CRITICAL/HIGH pass (or WARN), any check WARN →
partial - All applicable checks PASS (no WARN, no FAIL) →
wired - Could not read key files (e.g., SKILL.md unreadable) →
unknown
Note on WARN: WARN in a HIGH check is not a FAIL — it downgrades to partial. WARN in
a MEDIUM check is also partial.
Contract YAML Schema (ModelContractYaml)
Used only as a parsing helper inside the skill — not a node or committed file.
class ModelEventBus(BaseModel, extra="allow"):
subscribe_topics: list[str] = Field(default_factory=list)
publish_topics: list[str] = Field(default_factory=list)
class ModelContractMetadata(BaseModel, extra="allow"):
ticket: str | None = None
class ModelContractYaml(BaseModel, extra="allow"):
name: str
node_type: str # validated against allowlist at check time
event_bus: ModelEventBus | None = None
metadata: ModelContractMetadata | None = NoneParse failure → contract_yaml check FAIL (CRITICAL). If node_type not in allowlist:contract_yaml check WARN, topic checks WARN instead of FAIL.
--fail-on Behavior
When --fail-on is set and audit mode is active:
broken: exit non-zero ifbroken > 0partial: exit non-zero ifbroken > 0orpartial > 0any: exit non-zero ifbroken > 0orpartial > 0orunknown > 0
Set failed = True and fail_reason = "N skill(s) at or above threshold" in the result model.
CLI Output Format
When --format=cli (default):
Feature Dashboard — {date}
--------------------------
Total: {total} Wired: {wired} Partial: {partial} Broken: {broken} Unknown: {unknown}
SKILL STATUS CHECKS
feature-dashboard wired 8/8 pass
gap partial 6/8 pass (2 warn)
pipeline-audit broken 5/8 pass (1 critical fail)Committed vs Gitignored Outputs
- Committed (stable, no timestamp):
docs/feature-dashboard/feature-dashboard.stable.json - Gitignored:
docs/feature-dashboard/*.html,docs/feature-dashboard/feature-dashboard-*.md
Reference Implementation
- Canonical result model:
src/omniclaude/nodes/node_skill_feature_dashboard_orchestrator/models/model_result.py - Node type classifier:
src/omniclaude/nodes/node_skill_feature_dashboard_orchestrator/classifier.py - SKILL.md frontmatter pattern:
plugins/onex/skills/gap/SKILL.md - Coverage test (canonical truth):
tests/unit/nodes/test_skill_node_coverage.py
See Also
pipeline-auditskill (comprehensive end-to-end pipeline verification)gapskill (cross-repo integration health audit -- detect/fix/cycle)NodeSkillFeatureDashboardOrchestrator(handlesonex.cmd.omniclaude.feature-dashboard.v1)ModelFeatureDashboardResult(canonical result model)docs/feature-dashboard/(output directory)