IV (4)

janus-plugin-build

janus

Use when building a new Claude Code plugin from scratch, scaffolding a plugin structure, or porting an existing skill into a plugin. Covers manifest, hooks, sub-skills, validator, tests, and the retrospective loop.

Janus Plugin Build

IPurpose

Building a Claude Code plugin is a small set of moving parts that must line up exactly. This skill is the working record of how janus was built so the next plugin (pantheon, or any sibling) ships clean on the first pass.

IIFile Structure That Works

<plugin-root>/
├── README.md                       short, public, no PII
├── CHEAT_SHEET.md                  optional but useful
├── .gitignore
├── .claude-plugin/plugin.json      manifest
├── skills/<skill-name>/SKILL.md    one folder per skill; auto-loaded by router pattern
├── hooks/hooks.json                UserPromptSubmit + Stop
├── scripts/                        gate hook, validator, project bootstrap
├── references/                     demand-loaded reference docs
├── templates/                      project bootstrap templates
└── tests/                          pytest

IIIManifest Rules

  • name, version, description, skills, hooks are required.
  • skills is a folder path (./skills/). hooks is a file path (./hooks/hooks.json).
  • Description and interface strings are public — no operator name, no handle, no project codenames.

IVHook Rules

  • Only wire UserPromptSubmit and Stop unless the plugin genuinely needs more.
  • Command must use ${CLAUDE_PLUGIN_ROOT} for portability — never absolute user paths.
  • The gate hook script must be silent for trivial chat; emit context only for project-shape prompts; block completion claims missing a Verified: line.

VValidator Pattern

The validator is the contract. It must:

  • Confirm every required file exists.
  • Parse plugin.json and hooks.json as JSON.
  • Walk every SKILL.md and check frontmatter name matches the folder name and description is non-empty.
  • Block legacy path tokens. The bug that broke this plugin's predecessor was an underscore-prefixed folder reference that never matched its real path. Keep an explicit blocklist for whatever the predecessor used.
  • Block PII tokens in any non-private file.
  • Resolve every internal markdown link.
  • Print one of two messages and exit cleanly: PASS - <plugin> is valid. or FAIL - <plugin> is invalid. with itemized reasons.

VISkill Rules

  • Exactly one router skill at the top. All other sub-skills are routed to.
  • Every SKILL.md ≤ ~160 lines. If a skill grows past that, split it or move detail to references/.
  • Frontmatter description must trigger reliably for its scenario without naming the operator. The description is public — treat it like a search query the user might type.
  • One skill, one purpose. If two skills overlap, pick the narrower.

VIITests

Three test files cover the surface:

  • test_<plugin>_plugin_structure.py — manifest, hooks JSON, required folders, no legacy refs.
  • test_<plugin>_gate_hook.py — empty stdin, malformed JSON, recognized events, unrecognized events, completion gating.
  • test_skill_frontmatter.py — every SKILL.md has valid frontmatter, no PII.

Run pytest and the validator together. Both must pass before declaring completion.

VIIIPitfalls Hit Building Janus

  • A legacy scaffold subfolder (C:\Janus\janus\) was present from a prior attempt and would have poisoned the rglob-based validator with stale legacy-path references. Lesson: before scaffolding, scan the target root for unexpected state and quarantine it (Move-Item to a sibling archive folder, do not delete).
  • The argus reference validator used MAX_SKILL_LINES = 160. Treat that as the working ceiling. Several janus sub-skill drafts wanted to balloon past it — push detail into references/ instead.
  • The argus gate hook used hardcoded absolute paths in its validation reminder string. Janus uses ${CLAUDE_PLUGIN_ROOT} everywhere — including in the reminder — so the message stays portable.
  • PII enforcement is brittle if the validator walks the whole tree and tests the public skill files for the operator's name. Allow exactly one whitelist: templates/PROJECT_CONTEXT.md. Everything else, including README and SKILL bodies, is public.
  • The description field on the router SKILL.md is the most important string in the plugin. If it does not match what the user actually types, the router never fires.

IXRetrospective Loop

After building a plugin, before closing the thread:

  1. Update or write the meta-skill (this file, in janus's case).
  2. Write a HANDOFF_FROM_<plugin>.md back to the planning thread covering: built / verified / surprises / planner-mistakes / next phase / open questions / one-paragraph final shape.
  3. Final message to the operator: built, verified, learned, next. Five lines. No congratulations.