diff --git a/.github/scripts/build_power.py b/.github/scripts/build_power.py new file mode 100644 index 0000000..7aa2360 --- /dev/null +++ b/.github/scripts/build_power.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""POWER.md generator for inline plugins (Kiro Power). + +Single source of truth is the plugin's skills//SKILL.md. This emits +/POWER.md so the inline skill installs as a Kiro Power. Pure, +deterministic, idempotent. POWER.md is a GENERATED, CI-owned artifact - same +contract as .codex-plugin/plugin.json: never hand-edit. The release pipeline +regenerates it; validate.yml runs --check to block drift. + +displayName + keywords are reused from the plugin's .codex-plugin/plugin.json +(one curated source). references/ files are NOT moved; only relative links are +rewritten so they resolve from the plugin (power) root. If /mcp.json +exists, an "## MCP Tools (Kiro)" trailer naming its servers is appended. + +Stdlib only (no PyYAML): the SKILL.md frontmatter is a fixed, simple shape +parsed line by line - same approach as .github/scripts/update_external_plugins.py. + +Usage: + python3 .github/scripts/build_power.py plugins/ # write + python3 .github/scripts/build_power.py plugins/ --check # verify +""" + +import json +import os +import re +import sys + + +def parse_frontmatter(skill_path): + src = open(skill_path, encoding="utf-8").read() + if not src.startswith("---"): + raise SystemExit(f"build_power: {skill_path} has no leading --- " + f"frontmatter") + parts = src.split("---") + fm, body = parts[1], "---".join(parts[2:]) + out = {} + for raw in fm.splitlines(): + line = raw.rstrip("\r") + m = re.match(r"^name:\s*(.+?)\s*$", line) + if m: + out["name"] = m.group(1) + continue + m = re.match(r"^description:\s*(.+?)\s*$", line) + if m: + out["description"] = m.group(1) + continue + m = re.match(r"^\s+author:\s*(.+?)\s*$", line) + if m: + out["author"] = m.group(1) + continue + m = re.match(r"^\s+version:\s*(.+?)\s*$", line) + if m: + out["version"] = m.group(1) + continue + for k in ("name", "description", "author", "version"): + if not out.get(k): + raise SystemExit(f"build_power: {skill_path} frontmatter " + f"missing {k}") + return out, body + + +def yaml_dq(s): + return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"' + + +def build_power(plugin_dir): + plugin_dir = plugin_dir.rstrip("/") + name = os.path.basename(plugin_dir) + skill_path = os.path.join(plugin_dir, "skills", name, "SKILL.md") + codex_path = os.path.join(plugin_dir, ".codex-plugin", "plugin.json") + mcp_path = os.path.join(plugin_dir, "mcp.json") + + meta, body = parse_frontmatter(skill_path) + + if not os.path.isfile(codex_path): + raise SystemExit(f"build_power: {codex_path} not found (needed for " + f"displayName/keywords)") + codex = json.load(open(codex_path, encoding="utf-8")) + display_name = (codex.get("interface") or {}).get("displayName") \ + or meta["name"] + keywords = codex.get("keywords") or [] + if not keywords: + raise SystemExit(f"build_power: {codex_path} has no keywords") + + rewritten = re.sub(r"\]\(references/", + f"](skills/{name}/references/", body) + rewritten = re.sub(r"^\n+", "", rewritten) + rewritten = re.sub(r"\s*$", "", rewritten) + + # Quote free-text scalars + every keyword so a future value containing a + # YAML-sensitive char (:, #, [, etc.) cannot break frontmatter parsing. + # version stays unquoted: a CI-controlled multi-dot semver is always a + # YAML string and validate.yml reads it directly. + front = "\n".join([ + "---", + f"name: {yaml_dq(meta['name'])}", + f"displayName: {yaml_dq(display_name)}", + f"description: {yaml_dq(meta['description'])}", + f"keywords: [{', '.join(yaml_dq(k) for k in keywords)}]", + f"author: {yaml_dq(meta['author'])}", + f"version: {meta['version']}", + "---", + ]) + + banner = ( + "" + ) + + doc = f"{front}\n\n{banner}\n\n{rewritten}\n" + + if os.path.isfile(mcp_path): + mcp = json.load(open(mcp_path, encoding="utf-8")) + servers = list((mcp.get("mcpServers") or {}).keys()) + if servers: + names = ", ".join(f"`{s}`" for s in servers) + doc += ( + "\n## MCP Tools (Kiro)\n\n" + f"This Power bundles {names} (see `mcp.json`). Kiro registers " + "it under the Powers section of `~/.kiro/settings/mcp.json` " + "on install. The guidance above works without it.\n" + ) + return doc + + +def main(argv): + args = [a for a in argv if a != "--check"] + check = "--check" in argv + if len(args) != 1: + raise SystemExit("usage: build_power.py [--check]") + plugin_dir = args[0] + generated = build_power(plugin_dir) + dest = os.path.join(plugin_dir.rstrip("/"), "POWER.md") + current = open(dest, encoding="utf-8").read() \ + if os.path.isfile(dest) else None + + if check: + if current != generated: + print(f"build_power: {dest} is out of sync with SKILL.md. " + f"Run: python3 .github/scripts/build_power.py {plugin_dir}") + sys.exit(1) + print(f"build_power: {dest} in sync") + return + if current == generated: + print(f"build_power: {dest} already up to date") + return + with open(dest, "w", encoding="utf-8") as f: + f.write(generated) + print(f"build_power: wrote {dest}") + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/.github/scripts/update_external_plugins.py b/.github/scripts/update_external_plugins.py index 375fd24..a3b2717 100644 --- a/.github/scripts/update_external_plugins.py +++ b/.github/scripts/update_external_plugins.py @@ -9,6 +9,9 @@ if newer than the pinned ref, rewrite atomically: - .claude-plugin: entry.source.ref = vX.Y.Z AND entry.version = X.Y.Z - .agents: entry.source.ref = vX.Y.Z (no version field) + - .kiro: entry.source.ref = vX.Y.Z (no version field; mirror of + .agents - Kiro does not consume this manifest for install + today, it is kept in sync for pattern parity / drift guard) No third-party deps (stdlib only). Real errors exit non-zero; "nothing to do" exits 0. `--dry-run` prints intended changes without writing. @@ -33,6 +36,7 @@ CLAUDE_MANIFEST = ".claude-plugin/marketplace.json" AGENTS_MANIFEST = ".agents/plugins/marketplace.json" +KIRO_MANIFEST = ".kiro/plugins/marketplace.json" POLICY_FILE = ".github/external-plugin-updates.json" DEFAULTS = { @@ -159,6 +163,8 @@ def main() -> int: claude = load_json(CLAUDE_MANIFEST) agents = load_json(AGENTS_MANIFEST) agents_by_name = {p.get("name"): p for p in agents.get("plugins", [])} + kiro = load_json(KIRO_MANIFEST) + kiro_by_name = {p.get("name"): p for p in kiro.get("plugins", [])} changed = [] for entry in claude.get("plugins", []): @@ -179,6 +185,15 @@ def main() -> int: fail(f"{name}: {AGENTS_MANIFEST} repo {a_repo!r} != " f"{repo!r} ({CLAUDE_MANIFEST})") + k = kiro_by_name.get(name) + if k is None: + fail(f"{name}: no matching entry in {KIRO_MANIFEST}") + k_src = k.get("source", {}) + k_repo = normalize_repo(k_src.get("url", "")) + if k_repo.lower() != repo.lower(): + fail(f"{name}: {KIRO_MANIFEST} repo {k_repo!r} != " + f"{repo!r} ({CLAUDE_MANIFEST})") + cfg = {**pdefaults, **(poverrides.get(name) or {})} ver, tag = latest_version(repo, cfg) cur_ref = src.get("ref", "") @@ -192,6 +207,7 @@ def main() -> int: src["ref"] = tag entry["version"] = ver # mirrored, no leading v a_src["ref"] = tag # .agents: ref only + k_src["ref"] = tag # .kiro: ref only (mirror) if not changed: print("Nothing to update.") @@ -207,7 +223,9 @@ def main() -> int: write_json(CLAUDE_MANIFEST, claude) write_json(AGENTS_MANIFEST, agents) - print(f"\nUpdated {CLAUDE_MANIFEST} and {AGENTS_MANIFEST}.") + write_json(KIRO_MANIFEST, kiro) + print(f"\nUpdated {CLAUDE_MANIFEST}, {AGENTS_MANIFEST}, " + f"and {KIRO_MANIFEST}.") return 0 diff --git a/.github/workflows/automated-release.yml b/.github/workflows/automated-release.yml index 8ac69b2..a502bc6 100644 --- a/.github/workflows/automated-release.yml +++ b/.github/workflows/automated-release.yml @@ -19,6 +19,7 @@ name: Automated Release # - bumps plugins[].version in marketplace.json # - syncs that plugin's SKILL.md metadata.version # - syncs that plugin's .codex-plugin/plugin.json version (if present) +# - regenerates that plugin's POWER.md (Kiro) from SKILL.md (if present) # - prepends an entry to plugins//CHANGELOG.md # - tags -vX.Y.Z and creates a GitHub Release @@ -194,6 +195,17 @@ jobs: open(codex_path, 'a').write('\n') print(f" synced {codex_path} -> {new}") + # 2c. Regenerate the Kiro POWER.md from the now-bumped + # SKILL.md (only if the plugin ships one - it needs a + # .codex-plugin/plugin.json for displayName/keywords). The + # commit step stages all of plugins/, so it lands in the + # release commit + tag, same as SKILL.md/codex. + if os.path.isfile(os.path.join(source, 'POWER.md')): + subprocess.run( + ['python3', '.github/scripts/build_power.py', source], + check=True) + print(f" regenerated {source}/POWER.md -> {new}") + # 3. plugins//CHANGELOG.md entry. # Markdownlint-clean and idempotent: a stable `# Changelog` # H1 + preamble stays on top, new version sections are diff --git a/.github/workflows/update-external-plugins.yml b/.github/workflows/update-external-plugins.yml index 7e4907a..29464e6 100644 --- a/.github/workflows/update-external-plugins.yml +++ b/.github/workflows/update-external-plugins.yml @@ -41,7 +41,9 @@ jobs: import json, re, sys claude = json.load(open('.claude-plugin/marketplace.json')) agents = json.load(open('.agents/plugins/marketplace.json')) + kiro = json.load(open('.kiro/plugins/marketplace.json')) a_by_name = {p.get('name'): p for p in agents.get('plugins', [])} + k_by_name = {p.get('name'): p for p in kiro.get('plugins', [])} def norm(url): m = re.match(r'^git@github\.com:(.+?)(?:\.git)?$', (url or '').strip()) @@ -71,6 +73,17 @@ jobs: errors.append( f"{name}: ref mismatch claude {ref!r} vs " f".agents {a_src.get('ref','')!r}") + k = k_by_name.get(name) + if k is None: + errors.append(f"{name}: missing from .kiro manifest") + continue + k_src = k.get('source', {}) + if norm(k_src.get('url', '')) != repo.lower(): + errors.append(f"{name}: .kiro repo mismatch") + if k_src.get('ref', '') != ref: + errors.append( + f"{name}: ref mismatch claude {ref!r} vs " + f".kiro {k_src.get('ref','')!r}") if errors: print("\n".join(f"❌ {x}" for x in errors)) @@ -89,8 +102,9 @@ jobs: body: | Automated external-plugin pin update. - Bumps `source.ref` in both manifests (and the mirrored - `version` in `.claude-plugin/marketplace.json`) to the latest + Bumps `source.ref` in all three manifests (`.claude-plugin`, + `.agents`, `.kiro`) plus the mirrored `version` in + `.claude-plugin/marketplace.json` to the latest eligible upstream release. Manifest-only `chore:` change - no agent-plugins release is triggered. @@ -99,6 +113,7 @@ jobs: add-paths: | .claude-plugin/marketplace.json .agents/plugins/marketplace.json + .kiro/plugins/marketplace.json - name: Auto-merge # The inline "Validate manifest sync" step above is the gate: if it diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index f912d43..df475aa 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -6,12 +6,16 @@ on: - 'plugins/**' - '.claude-plugin/**' - '.agents/plugins/**' + - '.kiro/plugins/**' + - '.github/scripts/**' push: branches: [master, main] paths: - 'plugins/**' - '.claude-plugin/**' - '.agents/plugins/**' + - '.kiro/plugins/**' + - '.github/scripts/**' workflow_dispatch: jobs: @@ -126,6 +130,48 @@ jobs: print(f"\n✅ {len(skills)} inline plugin test suite(s) present") EOF + - name: Validate inline POWER.md (Kiro) sync + run: | + python3 << 'EOF' + import glob, os, subprocess, sys + import yaml + + powers = sorted(glob.glob('plugins/*/POWER.md')) + if not powers: + print("ℹ️ No inline POWER.md files - nothing to check.") + sys.exit(0) + + errors = [] + for power in powers: + plugin_dir = os.path.dirname(power) + r = subprocess.run( + ['python3', '.github/scripts/build_power.py', + plugin_dir, '--check'], + capture_output=True, text=True) + if r.returncode != 0: + errors.append(f"{power}: out of sync - " + f"{(r.stdout + r.stderr).strip()}") + continue + fm = yaml.safe_load( + open(power).read().split('---', 2)[1]) + need = {'name', 'displayName', 'description', 'keywords', + 'author', 'version'} - set(fm.keys()) + if need: + errors.append(f"{power}: frontmatter missing {need}") + elif not (isinstance(fm['keywords'], list) and fm['keywords']): + errors.append(f"{power}: keywords must be a non-empty list") + else: + print(f" ✅ {power} in sync " + f"(v{fm['version']}, {len(fm['keywords'])} keywords)") + + if errors: + print("\n".join(f"❌ {e}" for e in errors)) + print("\nPOWER.md is generated from SKILL.md. Regenerate: " + "python3 .github/scripts/build_power.py ") + sys.exit(1) + print(f"\n✅ {len(powers)} inline POWER.md file(s) in sync") + EOF + - name: Validate marketplace.json run: | python3 << 'EOF' @@ -243,10 +289,12 @@ jobs: python3 << 'EOF' import json, re, sys - print("🔍 Checking .claude-plugin <-> .agents external sync...") + print("🔍 Checking .claude-plugin <-> .agents <-> .kiro sync...") claude = json.load(open('.claude-plugin/marketplace.json')) agents = json.load(open('.agents/plugins/marketplace.json')) + kiro = json.load(open('.kiro/plugins/marketplace.json')) a_by_name = {p.get('name'): p for p in agents.get('plugins', [])} + k_by_name = {p.get('name'): p for p in kiro.get('plugins', [])} def norm(url): m = re.match(r'^git@github\.com:(.+?)(?:\.git)?$', (url or '').strip()) @@ -286,11 +334,27 @@ jobs: errors.append( f"{name}: ref mismatch - .claude-plugin {ref!r} vs " f".agents {a_src.get('ref','')!r}") + k = k_by_name.get(name) + if k is None: + errors.append( + f"{name}: external plugin missing from " + f".kiro/plugins/marketplace.json") + continue + k_src = k.get('source', {}) + if norm(k_src.get('url', '')) != repo.lower(): + errors.append( + f"{name}: .kiro repo " + f"{norm(k_src.get('url',''))!r} != {repo.lower()!r}") + if k_src.get('ref', '') != ref: + errors.append( + f"{name}: ref mismatch - .claude-plugin {ref!r} vs " + f".kiro {k_src.get('ref','')!r}") if errors: print("\n".join(f"❌ {x}" for x in errors)) sys.exit(1) - print(f"✅ {n} external plugin(s) in sync across both manifests") + print(f"✅ {n} external plugin(s) in sync across all three " + f"manifests") EOF - name: Check for Broken Links @@ -317,6 +381,7 @@ jobs: with: globs: | plugins/**/*.md + !plugins/**/POWER.md README.md CONTRIBUTING.md diff --git a/.kiro/plugins/marketplace.json b/.kiro/plugins/marketplace.json new file mode 100644 index 0000000..934f999 --- /dev/null +++ b/.kiro/plugins/marketplace.json @@ -0,0 +1,46 @@ +{ + "name": "antonbabenko", + "interface": { + "displayName": "Agent Plugins" + }, + "plugins": [ + { + "name": "code-intelligence", + "source": { + "source": "local", + "path": "./plugins/code-intelligence" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Development" + }, + { + "name": "terraform-skill", + "source": { + "source": "url", + "url": "git@github.com:antonbabenko/terraform-skill.git", + "ref": "v1.14.0" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Development" + }, + { + "name": "claude-delegator", + "source": { + "source": "url", + "url": "git@github.com:antonbabenko/claude-delegator.git", + "ref": "v1.6.0" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Development" + } + ] +} diff --git a/CLAUDE.md b/CLAUDE.md index f5a7ecb..8b31380 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,18 +24,25 @@ is either: ``` agent-plugins/ -├── .claude-plugin/marketplace.json # Marketplace + plugin entries -├── plugins/ # Inline plugins only (empty until added) +├── .claude-plugin/marketplace.json # Claude Code marketplace + plugin entries +├── .agents/plugins/marketplace.json # Codex/agents mirror (external: url+ref) +├── .kiro/plugins/marketplace.json # Kiro mirror (pin-sync parity; see note) +├── plugins/ # Inline plugins only (empty until added) │ └── .gitkeep -│ └── / # (when added) source dir for the manifest -│ ├── skills// # Autodiscovered: skills//SKILL.md -│ │ ├── SKILL.md # Core skill file -│ │ └── references/ # Reference files loaded on demand -│ ├── tests/ # Baseline scenarios, rationalization table -│ └── CHANGELOG.md # Per-plugin changelog (CI-managed) -└── .github/workflows/ - ├── validate.yml # PR validation (hybrid source-aware) - └── automated-release.yml # Per-plugin release (inline plugins only) +│ └── / # (when added) source dir for the manifest +│ ├── skills// # Autodiscovered: skills//SKILL.md +│ │ ├── SKILL.md # Core skill file — single source of truth +│ │ └── references/ # Reference files loaded on demand +│ ├── POWER.md # GENERATED Kiro Power (from SKILL.md), CI-owned +│ ├── tests/ # Baseline scenarios, rationalization table +│ └── CHANGELOG.md # Per-plugin changelog (CI-managed) +└── .github/ + ├── scripts/ + │ ├── update_external_plugins.py # Bumps external pins in all 3 manifests + │ └── build_power.py # POWER.md generator (`--check` for CI) + └── workflows/ + ├── validate.yml # PR validation (hybrid source-aware) + └── automated-release.yml # Per-plugin release (inline plugins only) ``` Claude Code autodiscovers `/skills//SKILL.md` and, for external @@ -58,9 +65,23 @@ bumped automatically: the scheduled `Update External Plugins` workflow (`.github/workflows/update-external-plugins.yml`) auto-discovers external entries from `.claude-plugin/marketplace.json`, resolves the latest upstream release, and opens a reviewable `chore(external-plugins): ...` PR updating -`source.ref` in both manifests plus the mirrored `version`. Per-plugin -overrides live in `.github/external-plugin-updates.json`; `validate.yml` -cross-checks the two manifests stay in sync. Do not hand-bump. +`source.ref` in **all three** manifests (`.claude-plugin`, `.agents`, +`.kiro`) plus the mirrored `version` in `.claude-plugin`. Per-plugin overrides +live in `.github/external-plugin-updates.json`; `validate.yml` cross-checks the +three manifests stay in sync. Do not hand-bump. + +**Kiro note.** `.kiro/plugins/marketplace.json` mirrors `.agents` so external +pins never drift, but Kiro installs a Power from a GitHub repo URL ("Add power +from GitHub") — it does **not** consume this manifest for install today. It +exists for pattern parity and the drift guard; inline plugins are made +installable as Kiro Powers via a generated `POWER.md` (see below). + +`POWER.md` is a **generated, CI-owned artifact** (like +`.codex-plugin/plugin.json`): produced from `SKILL.md` by +`.github/scripts/build_power.py`, regenerated by the release pipeline, and +checked in `validate.yml` (`build_power.py --check` fails the PR on +drift). Never hand-edit it — edit `SKILL.md`/`references/` and run +`python3 .github/scripts/build_power.py plugins/`. ## Adding a Plugin @@ -70,7 +91,10 @@ cross-checks the two manifests stay in sync. Do not hand-bump. `source: { "source": "github", "repo": "owner/repo", "ref": "vX.Y.Z" }`, `description`, optional `category` / `keywords`, optional `version` (mirror of the ref, manual). -2. No local content, CHANGELOG, or tests here. No scoped-commit release. +2. Mirror the entry into `.agents/plugins/marketplace.json` **and** + `.kiro/plugins/marketplace.json` (same `source.url`/`ref`; no `version` + field). `validate.yml` fails if the three manifests drift. +3. No local content, CHANGELOG, or tests here. No scoped-commit release. **Inline plugin** — content lives here: @@ -87,6 +111,13 @@ cross-checks the two manifests stay in sync. Do not hand-bump. enforces it: at least one `## Scenario`, a `## Running These Tests` protocol, and a `### Success Criteria` list. Copy the shape of `plugins/code-intelligence/tests/baseline-scenarios.md`. +6. To also ship it as a Kiro Power: ensure the plugin has a + `.codex-plugin/plugin.json` (supplies `displayName` + `keywords`), then + generate `plugins//POWER.md` with + `python3 .github/scripts/build_power.py plugins/` and commit it. + Never hand-edit POWER.md; the release pipeline regenerates it and + `validate.yml` `--check`s it. Optional: add `plugins//mcp.json` + (a `## MCP Tools (Kiro)` trailer is auto-appended naming its servers). ## Development Workflow @@ -165,6 +196,7 @@ type). Bot release commits (`chore(release): ...`) never bump - type `chore` - bumps `plugins[].version` in `marketplace.json`, - syncs that plugin's `SKILL.md` `metadata.version`, - syncs that plugin's `.codex-plugin/plugin.json` `version` (if present), +- regenerates that plugin's `POWER.md` from `SKILL.md` (if present), - prepends an entry to `plugins//CHANGELOG.md`, - tags `-vX.Y.Z` and creates a GitHub Release. @@ -243,6 +275,8 @@ a query**, not a human reading end to end. Mandatory for every addition to | Detailed guides, templates, examples | `.../references/*.md` | | Baseline test scenarios | `plugins//tests/*.md` | | Per-plugin changelog | `plugins//CHANGELOG.md` (CI-managed) | +| Kiro Power (generated) | `plugins//POWER.md` (CI-owned; from SKILL.md) | | Installation/usage docs | `README.md` | | Contributor process | `CONTRIBUTING.md` | | Marketplace + versions | `.claude-plugin/marketplace.json` (CI-managed versions) | +| External pin mirrors | `.agents/plugins/` + `.kiro/plugins/marketplace.json` (CI-synced) | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 230d2e9..0c581cc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,19 +33,21 @@ standards, and the per-plugin release model before contributing. **External** (content + releases in its own repo) — manifest entry only: -1. Add a `plugins[]` entry to BOTH `.claude-plugin/marketplace.json` +1. Add a `plugins[]` entry to ALL THREE manifests: + `.claude-plugin/marketplace.json` (`source: { "source": "github", "repo": "owner/repo", "ref": "vX.Y.Z" }` - plus a mirrored top-level `version: "X.Y.Z"`) and - `.agents/plugins/marketplace.json` (Codex; same `name`, `ref`, no - `version`). + plus a mirrored top-level `version: "X.Y.Z"`), + `.agents/plugins/marketplace.json` (Codex), and + `.kiro/plugins/marketplace.json` (Kiro mirror) — same `name`/`ref`, no + `version` in the latter two. 2. No local content, CHANGELOG, tests, or scoped-commit release. 3. **Do not hand-bump the pin.** The scheduled `Update External Plugins` workflow (`.github/workflows/update-external-plugins.yml`) auto-discovers every external entry, resolves the latest upstream release, and opens a reviewable `chore(external-plugins): ...` PR that updates `source.ref` in - both manifests and the mirrored `version`. Override defaults (prereleases, - tag pattern, tags-vs-releases) per plugin in - `.github/external-plugin-updates.json`. CI cross-checks the two manifests + all three manifests and the mirrored `version`. Override defaults + (prereleases, tag pattern, tags-vs-releases) per plugin in + `.github/external-plugin-updates.json`. CI cross-checks the three manifests stay in sync. **Inline** (content lives here): @@ -61,6 +63,10 @@ standards, and the per-plugin release model before contributing. 5. `plugins//tests/baseline-scenarios.md` is **required** and CI-enforced (see Testing). It must contain at least one `## Scenario ...`, a `## Running These Tests` protocol, and a `### Success Criteria` list. +6. Kiro Power (optional): with a `.codex-plugin/plugin.json` present, run + `python3 .github/scripts/build_power.py plugins/` and commit the + generated `plugins//POWER.md`. It is CI-owned — never hand-edit; + the release pipeline regenerates it and `validate.yml` `--check`s it. See CLAUDE.md "SKILL.md Architecture" and the "LLM Consumption Rules" for content shape and token discipline. @@ -130,11 +136,13 @@ example - copy its shape. ## CI -`validate.yml` runs on every PR touching `plugins/**` or `.claude-plugin/**`: +`validate.yml` runs on every PR touching `plugins/**`, `.claude-plugin/**`, +`.agents/plugins/**`, `.kiro/plugins/**`, or `.github/scripts/**`: frontmatter, size, **inline plugin tests present** (baseline-scenarios.md with scenarios + run protocol + success criteria), manifest validity, manifest <-> -SKILL.md <-> `.codex-plugin/plugin.json` version sync, broken links, and -markdown lint. +SKILL.md <-> `.codex-plugin/plugin.json` version sync, **POWER.md (Kiro) +regenerated and in sync**, **three-manifest external sync** +(`.claude-plugin` <-> `.agents` <-> `.kiro`), broken links, and markdown lint. Every step in **Validate Skill Files** is blocking - markdown lint included (no `continue-on-error`). One red step fails the whole check. diff --git a/README.md b/README.md index cc5a458..d11d674 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,34 @@ ln -s "$(pwd)/agent-plugins/plugins/code-intelligence" ~/.antigravity/skills/cod Update with `git pull` in each clone. +
+Kiro (Powers) + +These skills are also [Kiro Powers](https://kiro.dev/docs/powers/) (a +`POWER.md` generated from the same `SKILL.md` - shared content, not forked). +In Kiro: **Powers panel → "Add power from GitHub"**, then paste: + +```text +# terraform-skill (root POWER.md; bundles optional read-only terraform-mcp-server) +https://github.com/antonbabenko/terraform-skill + +# code-intelligence (POWER.md under the plugin subdir) +https://github.com/antonbabenko/agent-plugins/tree/master/plugins/code-intelligence +``` + +Kiro activates a power on keyword match (e.g. "terraform", "lsp", "rename"). + +> Notes. Kiro installs a power from a GitHub repo URL, not from a marketplace +> manifest - the `.kiro/plugins/marketplace.json` in this repo mirrors the +> `.agents`/`.claude-plugin` manifests for pin-sync parity only; Kiro does not +> consume it for install today. Kiro discovering a `POWER.md` under a +> subdirectory (the `code-intelligence` path above) is the documented install +> shape; if your Kiro build only accepts a repo-root `POWER.md`, clone the repo +> and install `code-intelligence` from the local `plugins/code-intelligence` +> path instead. + +
+
Manual (Claude Code - symlink a local clone) diff --git a/plugins/code-intelligence/POWER.md b/plugins/code-intelligence/POWER.md new file mode 100644 index 0000000..181726b --- /dev/null +++ b/plugins/code-intelligence/POWER.md @@ -0,0 +1,89 @@ +--- +name: "code-intelligence" +displayName: "Code Intelligence" +description: "Use when navigating or refactoring code with a language server - choosing between semantic (LSP), exact-text (rg), and fuzzy/semantic search; anchoring LSP calls by position; gating degraded results; and disclosing tool substitutions, in any language." +keywords: ["code-intelligence", "code-navigation", "language-server", "lsp", "refactoring", "search-precedence", "tool-disclosure"] +author: "Anton Babenko" +version: 0.4.1 +--- + + + +# Code Intelligence + +Pick the search tool by task, not by habit. Generic and language-agnostic; +domain skills extend it with server capability matrices and ecosystem +prerequisites - for example the `terraform-skill` plugin (same marketplace) +owns the terraform-ls capability matrix and Terraform setup. It is +model-triggered guidance, not enforcement. + +## Tool Precedence + +| Goal | Use | Tradeoff | +|------|-----|----------| +| Symbol relationships: definition, references, call sites, rename safety | Language server (LSP) at a position | Needs a running server + indexed workspace | +| Exact text, known name, exhaustive enumeration, config/value files | `rg` then Read | No semantic scope; matches strings in comments too | +| Conceptual / fuzzy / "where might this live" / cross-repo discovery | A semantic/neural search tool, if the host provides one | Not exact; never use for counts or completeness claims | + +Detail: [Precedence Table](skills/code-intelligence/references/tool-precedence.md#precedence-table), +[When LSP Is Wrong](skills/code-intelligence/references/tool-precedence.md#when-lsp-is-wrong). + +## Calling the LSP + +- DO call at a position (`file:line:character`). Anchor the position with a + text search for a known occurrence first. +- DON'T pass a bare symbol name and expect resolution. A name-only call that + returns empty is a usage defect, not server failure. +- DO Read the returned locations for source text; LSP returns locations and + symbols, not the lines. +- DO retry once on a cold start: the first call after launch may return empty + while the server indexes. +- DO prefer the server's own operation when it advertises it: use `rename` / + `prepareRename` for renames and call hierarchy for callers - they carry + language-specific semantics a manual pass misses. +- DON'T report an unsupported operation as a finding. When the server lacks + one, redirect: `findReferences` (then filter to call sites) instead of call + hierarchy; enumerate references then hand-edit instead of a rename provider. + +Detail: [Position Anchoring](skills/code-intelligence/references/lsp-calls.md#position-anchoring), +[Unsupported Operations](skills/code-intelligence/references/lsp-calls.md#unsupported-operations). + +## Degradation Gate + +Two distinct cases: + +- **No LSP at all** (host exposes no language-server tool, or the server fails + to start): that IS unavailability. Disclose it on the first line (see below) + and use text search. The gate does not apply - there is nothing to gate. +- **LSP callable but a position-anchored call returns empty:** do NOT conclude + "unavailable" yet. Pass ALL three: + 1. `documentSymbol` on an in-scope file returns symbols -> server responsive + (responsiveness only, NOT proof of complete reference coverage). + 2. The failing call was position-anchored (not symbol-name-only). + 3. That anchored call still returned empty after a cold-start retry. + +Only after the three-part case passes is a disclosed text fallback warranted. + +Detail: [Degradation Gate](skills/code-intelligence/references/degradation-and-disclosure.md#degradation-gate). + +## Disclose Substitutions + +State any tool substitution OR omission on the FIRST line of the response, not +in a later summary (post-hoc accounting is a rule violation): + +`Intended: . Actual: . Reason: . Impact: .` + +Detail: [Disclosure Format](skills/code-intelligence/references/degradation-and-disclosure.md#disclosure-format). + +## Do Not Invent a Missing Tool + +Before claiming a tool (e.g. `rg`) is shimmed, aliased, or absent, prove it: +`type -a `, `ls -l` the resolved path, ` --version` shows the +expected banner. An unproven "tool is missing" claim followed by a fallback is +a verification failure, not a sanctioned substitution. + +If genuinely absent or aliased: prefer the LSP for semantic tasks; for exact +text use the host-approved text search; `git grep` / `grep` only as an +explicitly disclosed last resort, never the default substitute. + +Detail: [Anti-Phantom-Shim Proof](skills/code-intelligence/references/degradation-and-disclosure.md#anti-phantom-shim-proof).