Compare commits
10 Commits
be2910f8f6
...
b9c41da8c3
| Author | SHA1 | Date | |
|---|---|---|---|
| b9c41da8c3 | |||
| 5ab7054976 | |||
| 41f98f6efd | |||
| 36ca3744aa | |||
| 25a6a454b9 | |||
| 76cdbdff7a | |||
| 5f355d664d | |||
| d9a894f49f | |||
| 33153594f1 | |||
| 663f10c3fe |
84
.agents/evals/cross-cutting/gitleaks/eval.yaml
Normal file
84
.agents/evals/cross-cutting/gitleaks/eval.yaml
Normal file
@@ -0,0 +1,84 @@
|
||||
skill_name: gitleaks
|
||||
|
||||
trigger_tests:
|
||||
- id: explicit-trigger-install
|
||||
name: Explicit trigger — install and configure
|
||||
query: "set up gitleaks in this repo"
|
||||
should_trigger: true
|
||||
|
||||
- id: explicit-trigger-update-hook
|
||||
name: Explicit trigger — update hook
|
||||
query: "update the gitleaks pre-commit hook"
|
||||
should_trigger: true
|
||||
|
||||
- id: implicit-trigger-false-positive
|
||||
name: Implicit trigger — suppress false positive in pre-commit hook
|
||||
query: "my pre-commit hook keeps blocking commits because it thinks my test fixture has an API key, how do I suppress it?"
|
||||
should_trigger: true
|
||||
|
||||
- id: implicit-trigger-scan-history
|
||||
name: Implicit trigger — audit repo history for secrets
|
||||
query: "I want to scan my entire git history to make sure no credentials were ever committed"
|
||||
should_trigger: true
|
||||
|
||||
- id: negative-trigger-security-review
|
||||
name: Negative trigger — general code security review
|
||||
query: "do a security review of this pull request"
|
||||
should_trigger: false
|
||||
|
||||
- id: negative-trigger-ci-scan
|
||||
name: Negative trigger — CI pipeline secret scanning
|
||||
query: "add secret scanning to my GitHub Actions CI pipeline"
|
||||
should_trigger: false
|
||||
|
||||
- id: negative-trigger-other-tool
|
||||
name: Negative trigger — different secret scanning tool
|
||||
query: "trufflehog is flagging false positives in my repo, how do I configure it?"
|
||||
should_trigger: false
|
||||
|
||||
output_tests:
|
||||
- id: output-states-intent-before-running
|
||||
name: Deterministic — states intent and waits for confirmation before running setup script
|
||||
type: deterministic
|
||||
prompt: "set up gitleaks in /home/user/myproject"
|
||||
expected_output: >
|
||||
The skill states what it is about to do (run setup-gitleaks.sh, which will modify
|
||||
.git/hooks/pre-commit and seed .gitleaks.toml) and asks for confirmation before
|
||||
executing the script. The confirmation request appears before any Bash command
|
||||
that runs the script.
|
||||
assertions:
|
||||
- "Output contains 'setup-gitleaks.sh' before any Bash tool invocation that runs it"
|
||||
- "Output contains a confirmation prompt (e.g. 'Proceed?') before running the setup script"
|
||||
- "Output does not run the setup script without an explicit confirmation step"
|
||||
|
||||
- id: output-redacts-secret-values
|
||||
name: Deterministic — redacts secret value and prescribes rotation before history rewrite
|
||||
type: deterministic
|
||||
prompt: "gitleaks found an AWS secret key on line 42 of config/settings.py — it says the value is AKIAIOSFODNN7EXAMPLE. What do I do?"
|
||||
expected_output: >
|
||||
The skill does not echo or repeat the secret value. It references the finding by
|
||||
rule ID, file path, and line number only. It states that the secret must be rotated
|
||||
immediately. It mentions git filter-repo (not git filter-branch) for history rewrite
|
||||
and asks for user confirmation before running the rewrite.
|
||||
assertions:
|
||||
- "Output does not contain the literal string 'AKIAIOSFODNN7EXAMPLE'"
|
||||
- "Output mentions rotating or revoking the credential as the first action"
|
||||
- "Output references 'git filter-repo' for history rewrite, not 'git filter-branch'"
|
||||
- "Output states that user confirmation is required before running the history rewrite"
|
||||
|
||||
- id: output-quality-allowlist-guidance
|
||||
name: LLM-rubric — allowlist guidance is correct, version-aware, and minimal
|
||||
type: llm-rubric
|
||||
prompt: "gitleaks keeps flagging my docs/research/ directory as containing secrets, how do I suppress it?"
|
||||
expected_output: >
|
||||
High-quality output checks the installed gitleaks version before prescribing any
|
||||
TOML syntax, recommends a path-based allowlist entry in .gitleaks.toml (not a
|
||||
.gitleaksignore fingerprint), uses the correct TOML syntax for the detected version,
|
||||
adds only the minimal allowlist entry needed for the identified false positive, and
|
||||
includes a verification step (re-run gitleaks dir -v or gitleaks dir --log-level debug)
|
||||
after making the change.
|
||||
assertions:
|
||||
- "Output checks or asks about the gitleaks version before writing TOML syntax"
|
||||
- "Output recommends a path-based allowlist entry in .gitleaks.toml rather than .gitleaksignore"
|
||||
- "Output includes a command to verify the suppression works after the change"
|
||||
- "Output explains why .gitleaksignore fingerprints are fragile (line numbers shift)"
|
||||
62
.agents/evals/marketplace/marketplace-architect/eval.yaml
Normal file
62
.agents/evals/marketplace/marketplace-architect/eval.yaml
Normal file
@@ -0,0 +1,62 @@
|
||||
skill_name: marketplace-architect
|
||||
|
||||
trigger_tests:
|
||||
- id: explicit-trigger-refactor
|
||||
name: "Explicit trigger — refactor repo into marketplace"
|
||||
query: "turn this repo into a plugin marketplace"
|
||||
should_trigger: true
|
||||
|
||||
- id: implicit-trigger-team-sharing
|
||||
name: "Implicit trigger — team distribution without saying marketplace"
|
||||
query: "how do I share my skills with my team so they can install them"
|
||||
should_trigger: true
|
||||
|
||||
- id: negative-trigger-new-skill
|
||||
name: "Negative trigger — new skill authoring belongs to write-skill"
|
||||
query: "write me a new skill for code review"
|
||||
should_trigger: false
|
||||
|
||||
output_tests:
|
||||
- id: deterministic-cross-compat-loaded
|
||||
name: "Deterministic — cross-compat reference loaded before tool-specific recommendations"
|
||||
type: deterministic
|
||||
prompt: "audit this repo and recommend how to convert it into a plugin marketplace for both Claude Code and Copilot CLI"
|
||||
expected_output: >
|
||||
The skill reads references/cross-compat.md early in its response and demonstrates
|
||||
awareness of the Claude Code / Copilot CLI format differences — specifically that
|
||||
manifest paths differ (.claude-plugin/ vs .github/plugin/), agent files differ
|
||||
(.md vs .agent.md), and hooks placement differs — before recommending any layout.
|
||||
assertions:
|
||||
- "Output references or acknowledges the divergence between Claude Code and Copilot CLI manifest paths before proposing a directory layout"
|
||||
- "Output does not recommend a single shared plugin.json location without noting the two-location requirement (.claude-plugin/ and plugin root)"
|
||||
- "Output does not write or propose writing any files before presenting a plan"
|
||||
|
||||
- id: deterministic-gate-a-blocks-writes
|
||||
name: "Deterministic — Gate A plan presented and approval requested before any writes"
|
||||
type: deterministic
|
||||
prompt: "help me migrate my skills and agents into a plugin marketplace layout"
|
||||
expected_output: >
|
||||
The skill produces a migration plan (checklist of old path → new path, one row per
|
||||
file) and explicitly asks the user for approval before proceeding to generate any
|
||||
manifest files. No plugin.json or marketplace.json is written or shown as written output.
|
||||
assertions:
|
||||
- "Output contains a migration plan or checklist with at least one file-move entry in the form 'old path → new path'"
|
||||
- "Output explicitly asks the user to approve or confirm the plan before proceeding"
|
||||
- "Output does not contain a complete plugin.json or marketplace.json file unless the user has already said 'yes' or equivalent in the prompt"
|
||||
|
||||
- id: llm-rubric-adopt-plugin
|
||||
name: "LLM-rubric — adopt external plugin covers all required checks without premature writes"
|
||||
type: llm-rubric
|
||||
prompt: "I want to add this plugin to my marketplace: https://github.com/example/my-tool-plugin"
|
||||
expected_output: >
|
||||
The skill fetches and inspects the plugin source, classifies what assets it contains
|
||||
(skills, agents, hooks, MCP servers), checks whether any existing plugin in the
|
||||
marketplace has a naming conflict, evaluates cross-tool compatibility against the
|
||||
divergence table, and summarises what would be added to marketplace.json — all without
|
||||
writing any files and without proceeding past Gate A without explicit user approval.
|
||||
assertions:
|
||||
- "Response identifies what asset types the external plugin contains (skills, agents, hooks, and/or MCP servers)"
|
||||
- "Response checks or asks about naming conflicts with existing plugins in the marketplace"
|
||||
- "Response notes at least one Claude Code vs Copilot CLI compatibility consideration for the adopted plugin"
|
||||
- "Response summarises the proposed change to marketplace.json without writing the file"
|
||||
- "Response asks for user approval (Gate A) before any file is created or modified"
|
||||
15
.agents/skills/gitleaks/META.md
Normal file
15
.agents/skills/gitleaks/META.md
Normal file
@@ -0,0 +1,15 @@
|
||||
```yaml
|
||||
version: "1.0"
|
||||
updated: 2026-06-20
|
||||
|
||||
when: >
|
||||
Invoked when the user wants to install gitleaks and wire it as a git pre-commit secret
|
||||
scanner, update the hook in an existing repo, tune allowlist rules to suppress false
|
||||
positives, debug a scan finding, or rotate a real secret that was found. Covers the full
|
||||
lifecycle: install → configure → maintain → remediate. Not invoked for general code
|
||||
security review (security-review skill) or CI pipeline secret scanning (write-ci-pipeline skill).
|
||||
|
||||
references:
|
||||
- https://github.com/gitleaks/gitleaks/releases/tag/v8.24.2
|
||||
- https://github.com/gitleaks/gitleaks/blob/main/README.md
|
||||
```
|
||||
111
.agents/skills/gitleaks/SKILL.md
Normal file
111
.agents/skills/gitleaks/SKILL.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
name: gitleaks
|
||||
description: Use when the user wants to install gitleaks, wire it as a git pre-commit secret scanner, update the hook in an existing repo, tune allowlist rules, resolve false positives, or debug a gitleaks scan finding. Do NOT use when the user wants a general security review of code (use security-review), wants to add secret scanning to a CI pipeline (use write-ci-pipeline), or is asking about a different secret scanning tool such as trufflehog or git-secrets.
|
||||
metadata:
|
||||
category: cross-cutting
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Edit
|
||||
---
|
||||
|
||||
<requirements>
|
||||
|
||||
## Required inputs
|
||||
|
||||
- **Target repo path** — absolute path to the git repository to configure; inferred from current working directory if not stated, ask if ambiguous
|
||||
- **Task type** — install/configure, update hook, tune allowlist, debug finding; inferred from the user's request
|
||||
|
||||
## Constraints
|
||||
|
||||
- Always state what you are about to do before running `setup-gitleaks.sh` — the script modifies `.git/hooks/pre-commit` and seeds `.gitleaks.toml`
|
||||
- Never modify `.gitleaks.toml` if the user has not asked for allowlist changes — it is project-owned once seeded; treat it as user-controlled config
|
||||
- Never run `gitleaks git` or `gitleaks dir` across the full history without warning the user it may be slow on large repos
|
||||
- Redact any secret values that appear in gitleaks output before showing them to the user — show the rule ID, file, and line number only
|
||||
- When the installed gitleaks version is unknown, check it with `gitleaks version` before suggesting config syntax — v8.24.2 uses `[allowlist]`; v8.25.0+ uses `[[allowlists]]`
|
||||
- False positive suppression: prefer path-based allowlists in `.gitleaks.toml` over fingerprint-based entries in `.gitleaksignore` — fingerprints are line-number-sensitive and break on file edits
|
||||
|
||||
</requirements>
|
||||
|
||||
<steps>
|
||||
|
||||
## Process
|
||||
|
||||
### Install and configure
|
||||
|
||||
1. **Confirm target.** State: "I will run `scripts/setup-gitleaks.sh <path>` which will install gitleaks (if absent), seed `.gitleaks.toml` (first run only), and write the pre-commit hook. Proceed?" Wait for confirmation — this modifies the repo's git hook.
|
||||
|
||||
2. **Run setup script.** Execute from the ai-development repo root:
|
||||
```
|
||||
bash scripts/setup-gitleaks.sh <TARGET_REPO>
|
||||
```
|
||||
The script is idempotent — it replaces the gitleaks block in the hook on every run without disturbing other hook content.
|
||||
|
||||
3. **Verify installation.** Run `gitleaks version` to confirm the binary is available. Run `gitleaks git --staged --redact -v` in the target repo to confirm the hook would work on a staged commit (add a dummy change if needed to test).
|
||||
|
||||
4. **Commit `.gitleaks.toml`.** Remind the user that `.gitleaks.toml` belongs in version control so all contributors share the same allowlist rules.
|
||||
|
||||
### Update hook
|
||||
|
||||
Re-run `bash scripts/setup-gitleaks.sh <TARGET_REPO>` from the ai-development repo root. The managed block (delimited by `# managed by setup-gitleaks.sh` / `# end gitleaks` markers) is always replaced with the current version. Non-gitleaks hook content is preserved.
|
||||
|
||||
### Tune allowlist / resolve false positives
|
||||
|
||||
1. **Identify the false positive.** Run `gitleaks dir --log-level debug <path>` to see which rule fired and which allowlist entries (if any) are already active.
|
||||
|
||||
2. **Check the gitleaks version.** Run `gitleaks version`. Use `[allowlist]` syntax for v8.24.2; use `[[allowlists]]` syntax for v8.25.0+. Using the wrong syntax silently produces no errors but the allowlist does nothing — this is the most common configuration trap.
|
||||
|
||||
3. **Choose suppression strategy.** Read `.gitleaks.toml` first. See `references/allowlist-patterns.md` for syntax examples and when to use each approach:
|
||||
- Path regex in `[allowlist]` — for files that can never contain real secrets (research notes, terminal captures, test fixtures). Preferred.
|
||||
- Stopwords in `[allowlist]` — for placeholder patterns like "example", "changeme".
|
||||
- `disabledRules` in `[extend]` — to disable a noisy default rule entirely. Use only when the rule has no value for this repo.
|
||||
- `.gitleaksignore` fingerprint — last resort; breaks when the file is edited because line numbers shift.
|
||||
|
||||
4. **Edit `.gitleaks.toml`.** Add the minimal allowlist entry needed. Do not suppress more than the identified false positive.
|
||||
|
||||
5. **Verify.** Re-run `gitleaks dir -v <path>` or `gitleaks git -v` to confirm the false positive is suppressed and no real findings are hidden.
|
||||
|
||||
### Scan modes
|
||||
|
||||
| Mode | Command | When to use |
|
||||
|---|---|---|
|
||||
| Staged changes (pre-commit) | `gitleaks git --staged --redact -v` | What the hook runs |
|
||||
| Full commit history | `gitleaks git -v` | Audit existing repo history |
|
||||
| Working directory files | `gitleaks dir -v <path>` | Scan uncommitted files |
|
||||
| Debug allowlists | `gitleaks dir --log-level debug <path>` | See which files are skipped and which allowlists fire |
|
||||
|
||||
### Resolve a real finding
|
||||
|
||||
1. Do not redact or show the secret value. Reference the rule ID, file, and line number only.
|
||||
2. The secret is compromised the moment it was committed — rotate it immediately, regardless of whether the commit is reachable from the public remote.
|
||||
3. Remove the secret from history using `git filter-repo` (not `git filter-branch`). This is a history-rewrite — confirm with the user before running. Force-push to all remotes after rewriting.
|
||||
4. Add the file path to the `.gitleaks.toml` allowlist only if the file is known to be a false-positive source going forward (e.g. a test fixture). Do not add an allowlist entry to suppress a real finding that has been removed.
|
||||
|
||||
## Output format
|
||||
|
||||
No structured output file. The skill produces:
|
||||
- Modified `.git/hooks/pre-commit` in the target repo (via the setup script)
|
||||
- Modified `.gitleaks.toml` in the target repo (allowlist changes only, when requested)
|
||||
- Terminal confirmation of what was changed and what to do next
|
||||
|
||||
</steps>
|
||||
|
||||
<checks>
|
||||
|
||||
## Failure handling
|
||||
|
||||
- `setup-gitleaks.sh` not found — stop; instruct the user to run from the ai-development repo root at `/root/ai-development/`
|
||||
- Target path is not a git repository — report the error from the script and ask the user to confirm the correct path
|
||||
- `gitleaks` binary not installed and download fails — report the curl/network error; direct the user to manual install at `https://github.com/gitleaks/gitleaks/releases`
|
||||
- Wrong TOML syntax for installed version — detect via `gitleaks version`, show the correct syntax for that version, do not guess
|
||||
|
||||
## Self-check
|
||||
|
||||
- [ ] Target repo confirmed before running the setup script
|
||||
- [ ] `gitleaks version` checked before writing any `.gitleaks.toml` allowlist syntax
|
||||
- [ ] Secret values in scan output redacted before displaying to the user
|
||||
- [ ] `.gitleaks.toml` edits are minimal — only the identified false positive suppressed
|
||||
- [ ] After any allowlist change: re-ran scan to verify suppression works and no real findings are hidden
|
||||
- [ ] For real findings: rotation step stated before history rewrite, user confirmed history rewrite before running `git filter-repo`
|
||||
|
||||
</checks>
|
||||
83
.agents/skills/gitleaks/references/allowlist-patterns.md
Normal file
83
.agents/skills/gitleaks/references/allowlist-patterns.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Gitleaks allowlist patterns
|
||||
|
||||
## Version syntax
|
||||
|
||||
| Version | Allowlist syntax |
|
||||
|---|---|
|
||||
| v8.24.2 and earlier | `[allowlist]` (singular table) |
|
||||
| v8.25.0 and later | `[[allowlists]]` (array of tables) |
|
||||
|
||||
**Critical**: using the wrong syntax produces no error but the allowlist silently does nothing. Always check `gitleaks version` first.
|
||||
|
||||
## v8.24.2 syntax (this repo uses 8.24.2)
|
||||
|
||||
### Suppress by path regex
|
||||
|
||||
Use for files that can never contain real secrets (research notes, terminal captures, test fixtures, generated docs).
|
||||
|
||||
```toml
|
||||
[allowlist]
|
||||
description = "research notes and terminal captures"
|
||||
paths = [
|
||||
'''docs/research/.*''',
|
||||
'''tests/fixtures/.*''',
|
||||
]
|
||||
```
|
||||
|
||||
### Suppress by stopword
|
||||
|
||||
Use for placeholder values that match secret patterns but are clearly not real.
|
||||
|
||||
```toml
|
||||
[allowlist]
|
||||
description = "placeholder values"
|
||||
stopwords = ["example", "placeholder", "changeme", "your-api-key-here"]
|
||||
```
|
||||
|
||||
### Disable a default rule entirely
|
||||
|
||||
Use only when a rule has no value for this repo and produces pervasive false positives.
|
||||
|
||||
```toml
|
||||
[extend]
|
||||
useDefault = true
|
||||
disabledRules = ["generic-api-key"]
|
||||
```
|
||||
|
||||
## v8.25.0+ syntax (for reference)
|
||||
|
||||
```toml
|
||||
[[allowlists]]
|
||||
description = "research notes"
|
||||
paths = ['''docs/research/.*''']
|
||||
|
||||
[[allowlists]]
|
||||
description = "placeholder values"
|
||||
stopwords = ["example", "placeholder"]
|
||||
```
|
||||
|
||||
## .gitleaksignore (fingerprint-based — last resort)
|
||||
|
||||
```
|
||||
# Format: <fingerprint>:<line-number>
|
||||
# Generated by: gitleaks git -v --report-format json | jq -r '.[] | "\(.Fingerprint):\(.StartLine)"'
|
||||
abc123def456:42
|
||||
```
|
||||
|
||||
Avoid this approach: fingerprints embed line numbers. Any edit to the file shifts line numbers and invalidates the entry, re-surfacing the false positive.
|
||||
|
||||
## Verification after any change
|
||||
|
||||
```bash
|
||||
# Scan current files
|
||||
gitleaks dir -v .
|
||||
|
||||
# Scan with debug output to see which allowlists fired
|
||||
gitleaks dir --log-level debug .
|
||||
|
||||
# Scan commit history
|
||||
gitleaks git -v
|
||||
|
||||
# Scan only staged changes (what the pre-commit hook runs)
|
||||
gitleaks git --staged --redact -v
|
||||
```
|
||||
21
.agents/skills/marketplace-architect/META.md
Normal file
21
.agents/skills/marketplace-architect/META.md
Normal file
@@ -0,0 +1,21 @@
|
||||
```yaml
|
||||
version: "1.0"
|
||||
updated: 2026-06-20
|
||||
|
||||
when: >
|
||||
Invoked when the user wants to create, manage, or maintain a plugin marketplace for
|
||||
Claude Code and/or GitHub Copilot CLI. Covers four operations: (a) audit and refactor
|
||||
a repository of skills/agents/hooks into a plugin marketplace layout, (b) adopt external
|
||||
plugins/skills/agents from outside sources, (c) update and maintain an existing
|
||||
marketplace.json and plugin manifests, (d) validate existing plugin manifests for naming,
|
||||
structure, and cross-tool compatibility. Also triggered implicitly when the user asks
|
||||
about distributing skills to a team, organizing loose skills into installable units, or
|
||||
setting up cross-tool distribution — even without saying "marketplace".
|
||||
|
||||
references:
|
||||
- https://code.claude.com/docs/en/plugins
|
||||
- https://code.claude.com/docs/en/plugin-marketplaces
|
||||
- https://code.claude.com/docs/en/plugins-reference
|
||||
- https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-creating
|
||||
- https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-marketplace
|
||||
```
|
||||
101
.agents/skills/marketplace-architect/SKILL.md
Normal file
101
.agents/skills/marketplace-architect/SKILL.md
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
name: marketplace-architect
|
||||
description: >
|
||||
Manages and maintains a plugin marketplace for Claude Code and GitHub Copilot CLI.
|
||||
Use this whenever the user wants to: create or update a marketplace (marketplace.json,
|
||||
plugin.json manifests), adopt plugins/skills/agents/hooks from external sources, evaluate
|
||||
cross-tool compatibility between Claude Code and Copilot CLI, plan plugin groupings and
|
||||
boundaries, refactor a repository into marketplace format, validate plugin naming, detect
|
||||
duplicate capabilities, or generate per-plugin install docs — even if they don't use the
|
||||
word "marketplace". Do NOT use when the user wants to author a new skill from scratch
|
||||
(use write-skill), debug an existing skill (use diagnose), or run a direct plugin CLI
|
||||
command (copilot plugin install, claude plugin list).
|
||||
metadata:
|
||||
category: marketplace
|
||||
---
|
||||
|
||||
<requirements>
|
||||
|
||||
## Required inputs
|
||||
|
||||
- **Target operation** — what the user wants to do; inferred from request. If ambiguous, ask: audit/refactor, adopt an external plugin, update/maintain an existing marketplace, or validate manifests.
|
||||
- **Repository path** — path to the repo to act on; defaults to current working directory if not stated.
|
||||
- **Marketplace name** — kebab-case identifier (e.g. `my-ai-marketplace`); required only when generating a new `marketplace.json`. Infer from repo name if obvious, ask if not.
|
||||
- **Plugin source** — URL, GitHub slug, or local path; required only when adopting an external plugin.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Load `references/cross-compat.md` before any tool-specific decision — Claude Code and Copilot CLI diverge in ways that cause silent breakage at install time.
|
||||
- Never write files until the user has approved the plan at Gate A and the specific file contents at Gate B — two separate explicit approvals required.
|
||||
- If credential-shaped content is detected in any manifest field, halt and redirect to environment variable references (e.g. `$MY_TOKEN`) — do not generate the manifest.
|
||||
- Produce cross-tool deltas and per-plugin READMEs only when explicitly requested — do not generate them automatically.
|
||||
- Scripts in `scripts/` are loaded on demand by the step that needs them — never preloaded.
|
||||
- Flag any `../` cross-references in the audited repo before recommending plugin boundaries — plugins cannot reference files outside their own directory after install-time caching.
|
||||
- Plugin names must be kebab-case; validate against the reserved name list in `references/claude-code.md` before generating any manifest.
|
||||
- Do not set `version` in both `plugin.json` and the marketplace entry — `plugin.json` wins silently and causes update failures.
|
||||
|
||||
</requirements>
|
||||
|
||||
<steps>
|
||||
|
||||
## Process
|
||||
|
||||
1. **Identify the operation.** Determine intent from the user's request — one of: (a) audit/refactor a repo into marketplace format, (b) adopt an external plugin/skill/agent, (c) maintain or update an existing marketplace, (d) validate existing manifests. Ask if the operation cannot be inferred.
|
||||
|
||||
2. **Load the compatibility reference.** Read `references/cross-compat.md` before any tool-specific decision. Claude Code and Copilot CLI diverge in manifest paths, agent file naming, and hooks layout — every recommendation depends on this table.
|
||||
|
||||
3. **Execute the operation phase.**
|
||||
|
||||
**(a) Audit/refactor:** Run `scripts/inventory.sh` against the repo to classify every asset (skill / command / agent / hook / prompt / MCP). Flag any `../` cross-references — these break under install-time caching. Recommend plugin groupings by user outcome (~10–20 plugins); warn if proposed count exceeds 20 or falls below 3. Diff skill descriptions for duplicate capabilities before finalising boundaries. Produce a concrete migration checklist: old path → new path, one row per file.
|
||||
|
||||
**(b) Adopt external plugin:** Fetch and inspect the plugin source. Classify included assets. Check for naming conflicts with existing plugins in the marketplace. Evaluate cross-tool compatibility using `references/cross-compat.md`. Summarise what will be added to `marketplace.json`.
|
||||
|
||||
**(c) Maintain/update:** Read current `marketplace.json` and all `plugin.json` files. Identify stale versions, reserved name violations, kebab-case violations, and `version` duplication between plugin.json and marketplace entry. Report findings as a prioritised fix list.
|
||||
|
||||
**(d) Validate:** Run `scripts/validate.sh` (wraps `claude plugin validate` plus custom JSON and naming checks). Report each violation with a recommended fix. Do not proceed to file writes until all errors are resolved.
|
||||
|
||||
4. **Gate A — plan review.** Present the full plan or fix list to the user. Wait for explicit approval before proceeding. Do not interpret silence or "looks good" as approval — require a direct "yes" or equivalent.
|
||||
|
||||
5. **Generate outputs.** After Gate A approval: for audit/refactor and adopt operations, run `scripts/gen_manifests.sh` to produce `plugin.json` (at both `.claude-plugin/plugin.json` and plugin root until the Copilot fallback is verified) and `marketplace.json` (at `.claude-plugin/marketplace.json`; optionally mirror to `.github/plugin/marketplace.json`). Read `references/claude-code.md` for Claude-specific path rules and `references/copilot-cli.md` for Copilot-specific requirements.
|
||||
|
||||
6. **Gate B — file write approval.** Show the user every file that will be written with its full contents. Wait for explicit approval per file or as a batch. Write nothing until approved.
|
||||
|
||||
7. **Validate post-write.** After writes complete, run `scripts/validate.sh` again. Report any remaining issues. Suggest local install test commands: `claude --plugin-dir ./plugins/<name>` and `copilot plugin install ./plugins/<name>`.
|
||||
|
||||
8. **Optional deliverables.** Only when the user explicitly asks: emit cross-tool delta notes (what each plugin needs for Copilot vs Claude Code) and per-plugin README with install commands for both tools.
|
||||
|
||||
## Output format
|
||||
|
||||
Files generated depend on operation:
|
||||
- **Audit/refactor and adopt:** `plugin.json` (two locations per plugin until verified), `marketplace.json` (`.claude-plugin/`, optionally `.github/plugin/`), migration checklist as a markdown table
|
||||
- **Maintain/update:** updated `marketplace.json` and affected `plugin.json` files
|
||||
- **Validate:** report only — no file writes unless explicitly requested after review
|
||||
- **Optional:** per-plugin `README.md` with both `claude` and `copilot` install commands
|
||||
|
||||
</steps>
|
||||
|
||||
<checks>
|
||||
|
||||
## Failure handling
|
||||
|
||||
- `scripts/inventory.sh` not found or fails — perform manual asset classification using Read and Bash find; note the fallback in output.
|
||||
- `scripts/gen_manifests.sh` not found or fails — generate manifest JSON inline; flag that the output was not script-produced.
|
||||
- `scripts/validate.sh` not found or `claude plugin validate` unavailable — run manual JSON schema and naming checks using `references/claude-code.md`; flag that automated validation was skipped.
|
||||
- Plugin source unreachable (bad URL, private repo, missing path) — stop the adopt operation, report the error, ask the user to verify the source before retrying.
|
||||
- Reserved name detected in proposed plugin or marketplace name — halt, report the name and the reserved list from `references/claude-code.md`, ask for a replacement before proceeding.
|
||||
- Credential-shaped content detected in any manifest field — halt, do not generate the manifest, redirect to environment variable references.
|
||||
|
||||
## Self-check
|
||||
|
||||
- [ ] `references/cross-compat.md` loaded before any tool-specific recommendation was made
|
||||
- [ ] Operation identified before any scanning or file reading began
|
||||
- [ ] Gate A presented and explicit approval received before any manifest was generated
|
||||
- [ ] Gate B presented with full file contents and explicit approval received before any file was written
|
||||
- [ ] No credential-shaped content in any generated manifest field
|
||||
- [ ] All plugin names validated as kebab-case and checked against reserved name list
|
||||
- [ ] `version` field not set in both `plugin.json` and marketplace entry for the same plugin
|
||||
- [ ] Scripts loaded on demand by step — not preloaded at skill invocation
|
||||
- [ ] Cross-tool deltas and READMEs produced only if explicitly requested
|
||||
- [ ] Post-write validation run and findings reported
|
||||
|
||||
</checks>
|
||||
169
.agents/skills/marketplace-architect/references/claude-code.md
Normal file
169
.agents/skills/marketplace-architect/references/claude-code.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Claude Code Plugin Reference
|
||||
|
||||
Verified against code.claude.com/docs as of June 2026.
|
||||
|
||||
---
|
||||
|
||||
## Directory structure
|
||||
|
||||
```text
|
||||
plugin-root/
|
||||
├── .claude-plugin/
|
||||
│ └── plugin.json # ONLY plugin.json goes here; all other dirs at plugin root
|
||||
├── skills/ # skill directories: <name>/SKILL.md
|
||||
├── commands/ # legacy flat .md files; promote to skills/ for new plugins
|
||||
├── agents/ # agent definitions: <name>.md
|
||||
├── hooks/
|
||||
│ └── hooks.json
|
||||
├── .mcp.json
|
||||
├── .lsp.json
|
||||
├── monitors/
|
||||
│ └── monitors.json
|
||||
├── bin/ # executables added to PATH while plugin is enabled
|
||||
└── settings.json # default settings applied when plugin is enabled
|
||||
```
|
||||
|
||||
A plugin that ships exactly one skill may place `SKILL.md` directly at the plugin root.
|
||||
Use `skills/` for plugins that may grow beyond one skill.
|
||||
|
||||
---
|
||||
|
||||
## plugin.json schema
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-plugin", // kebab-case, no spaces — also the skill namespace prefix
|
||||
"displayName": "My Plugin", // human-readable; shown in UI (v2.1.143+)
|
||||
"description": "What it does",
|
||||
"version": "1.0.0", // OPTIONAL — omit to use git SHA per commit
|
||||
"author": { "name": "Name", "url": "https://..." },
|
||||
"homepage": "https://...",
|
||||
"repository": "https://github.com/...",
|
||||
"license": "MIT",
|
||||
"keywords": [],
|
||||
"defaultEnabled": true, // set false to install disabled (v2.1.154+)
|
||||
"dependencies": [
|
||||
{ "name": "other-plugin", "version": "~2.1.0" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Only `name` is required. Add fields only when needed.
|
||||
|
||||
---
|
||||
|
||||
## marketplace.json schema
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-ai-marketplace",
|
||||
"owner": { "name": "Your Name", "email": "you@example.com" },
|
||||
"description": "Description",
|
||||
"version": "1.0.0",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "startup-cto",
|
||||
"source": "./plugins/startup-cto",
|
||||
"description": "...",
|
||||
"strict": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`description` and `version` are also accepted under a `metadata` key for backward compatibility.
|
||||
|
||||
---
|
||||
|
||||
## Plugin source types
|
||||
|
||||
| Type | Format | Notes |
|
||||
|---|---|---|
|
||||
| Relative path | `"./plugins/my-plugin"` | Must start with `./`. Resolved from marketplace root. Only works with git-hosted marketplaces, not URL-based. |
|
||||
| GitHub | `{ "source": "github", "repo": "owner/repo", "ref": "main", "sha": "abc123" }` | sha pins exact commit; ref is branch/tag |
|
||||
| URL / git | `{ "source": "url", "url": "https://...", "ref": "main" }` | also accepts `owner/repo` shorthand and SSH URLs |
|
||||
| git-subdir | `{ "source": "git-subdir", "url": "...", "path": "packages/my-plugin" }` | sparse clone of a monorepo path |
|
||||
| npm | `{ "source": "npm", "package": "@scope/pkg", "version": "^2.0.0", "registry": "https://..." }` | installed via npm install |
|
||||
|
||||
When both `ref` and `sha` are set, `sha` is the effective pin.
|
||||
|
||||
---
|
||||
|
||||
## Strict mode
|
||||
|
||||
Controls whether `plugin.json` is the authority for component definitions.
|
||||
|
||||
- **`strict: true`** (default) — plugin has its own `plugin.json`; marketplace entry can add extra skills/hooks on top.
|
||||
- **`strict: false`** — marketplace entry is the entire definition; plugin needs no `plugin.json`. The entry declares `skills`, `agents`, `hooks`, `mcpServers` path arrays.
|
||||
|
||||
Do not use `strict: false` plus a component-declaring `plugin.json` — this is a conflict and fails to load.
|
||||
|
||||
---
|
||||
|
||||
## Reserved marketplace names
|
||||
|
||||
These names are blocked for third-party use:
|
||||
`claude-code-marketplace`, `claude-code-plugins`, `claude-plugins-official`,
|
||||
`claude-plugins-community`, `claude-community`, `anthropic-marketplace`,
|
||||
`anthropic-plugins`, `agent-skills`, `anthropic-agent-skills`,
|
||||
`knowledge-work-plugins`, `life-sciences`, `claude-for-legal`,
|
||||
`claude-for-financial-services`, `financial-services-plugins`
|
||||
|
||||
Names that impersonate official marketplaces are also blocked (e.g. `official-claude-plugins`,
|
||||
`anthropic-tools-v2`).
|
||||
|
||||
Plugin names must be kebab-case (lowercase, digits, hyphens). Claude.ai marketplace sync
|
||||
rejects anything else even if the local CLI tolerates it.
|
||||
|
||||
---
|
||||
|
||||
## Version management
|
||||
|
||||
- If `version` is set in `plugin.json`, users receive updates only when you bump it.
|
||||
- If `version` is omitted, git commit SHA is used — every commit is a new version.
|
||||
- If `version` is set in both `plugin.json` and the marketplace entry, `plugin.json` wins silently.
|
||||
- **Recommendation:** omit `version` unless you need explicit release gates.
|
||||
|
||||
---
|
||||
|
||||
## Environment variables
|
||||
|
||||
- **`${CLAUDE_PLUGIN_ROOT}`** — absolute path to the plugin's installation directory. Use in hook commands and MCP/LSP configs for all in-plugin file references. This path changes on update.
|
||||
- **`${CLAUDE_PLUGIN_DATA}`** — persistent directory for plugin state that survives updates. Use for `node_modules`, generated code, caches.
|
||||
|
||||
---
|
||||
|
||||
## Validation and CLI commands
|
||||
|
||||
```bash
|
||||
# Validate plugin structure and manifest
|
||||
claude plugin validate ./my-plugin
|
||||
claude plugin validate ./my-plugin --strict # treat warnings as errors
|
||||
|
||||
# Install/manage
|
||||
claude plugin install <name>@<marketplace>
|
||||
claude plugin update <name>@<marketplace>
|
||||
claude plugin uninstall <name>
|
||||
claude plugin list
|
||||
claude plugin enable <name>
|
||||
claude plugin disable <name>
|
||||
|
||||
# Marketplace
|
||||
claude plugin marketplace add owner/repo
|
||||
claude plugin marketplace update
|
||||
|
||||
# Development
|
||||
claude --plugin-dir ./my-plugin # load without installing
|
||||
claude --plugin-dir ./my-plugin.zip # load from zip (v2.1.128+)
|
||||
claude plugin init my-tool # scaffold a skills-dir plugin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key gotchas
|
||||
|
||||
1. **Plugins are copied to cache on install.** Cannot reference `../shared-utils` — those files are not copied. Duplicate shared files into each plugin or use symlinks.
|
||||
2. **`commands/` ≠ `skills/`.** Flat `foo.md` is a legacy command; `foo/SKILL.md` is a skill. Promote flat commands to skill directories during migration.
|
||||
3. **Only `plugin.json` in `.claude-plugin/`.** Skills, agents, hooks, and other directories must be at the plugin root, not inside `.claude-plugin/`.
|
||||
4. **Plugin names are skill namespace prefixes.** `name: my-plugin` means skills invoke as `/my-plugin:skill-name`.
|
||||
5. **`defaultEnabled: false` requires v2.1.154+.** Earlier versions ignore it and enable on install.
|
||||
143
.agents/skills/marketplace-architect/references/copilot-cli.md
Normal file
143
.agents/skills/marketplace-architect/references/copilot-cli.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# GitHub Copilot CLI Plugin Reference
|
||||
|
||||
Verified against docs.github.com as of June 2026.
|
||||
|
||||
---
|
||||
|
||||
## Directory structure
|
||||
|
||||
```text
|
||||
plugin-root/
|
||||
├── plugin.json # at plugin root (NOT in .claude-plugin/)
|
||||
├── skills/ # skill directories: <name>/SKILL.md (same as Claude Code)
|
||||
├── agents/ # agent files: <name>.agent.md (differs from Claude Code)
|
||||
├── hooks.json # at plugin root (differs from Claude Code: hooks/hooks.json)
|
||||
└── .mcp.json # at plugin root (same as Claude Code)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## plugin.json schema (Copilot)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-plugin",
|
||||
"description": "What it does",
|
||||
"version": "1.0.0",
|
||||
"author": { "name": "Name", "email": "you@example.com" },
|
||||
"license": "MIT",
|
||||
"keywords": [],
|
||||
"agents": "agents/",
|
||||
"skills": ["skills/"],
|
||||
"hooks": "hooks.json",
|
||||
"mcpServers": ".mcp.json"
|
||||
}
|
||||
```
|
||||
|
||||
Key difference from Claude Code: Copilot expects component path declarations inside
|
||||
`plugin.json` (`"skills": "skills/"`, `"agents": "agents/"`, etc.). Claude Code instead
|
||||
defaults to standard dirs and takes path overrides only via the marketplace entry.
|
||||
This means the same `plugin.json` may need these fields for Copilot but not for Claude.
|
||||
|
||||
---
|
||||
|
||||
## marketplace.json schema (Copilot)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-ai-marketplace",
|
||||
"owner": { "name": "Your Name", "email": "you@example.com" },
|
||||
"metadata": { "description": "Agents, skills and workflows", "version": "1.0.0" },
|
||||
"plugins": [
|
||||
{
|
||||
"name": "startup-cto",
|
||||
"source": "./plugins/startup-cto",
|
||||
"description": "...",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Copilot's primary marketplace manifest path is `.github/plugin/marketplace.json`.
|
||||
It also reads `.claude-plugin/marketplace.json` as a fallback.
|
||||
Relative `source` paths: `./x` and `x` are both valid (Claude requires `./`).
|
||||
|
||||
---
|
||||
|
||||
## Agent file format
|
||||
|
||||
Copilot agents use `.agent.md` extension with frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-agent
|
||||
description: What this agent does
|
||||
tools:
|
||||
- read_file
|
||||
- run_command
|
||||
---
|
||||
|
||||
Agent instructions here.
|
||||
```
|
||||
|
||||
Claude Code agents use `.md` extension without the `.agent.md` suffix.
|
||||
If shipping agents for both tools, create both files:
|
||||
- `agents/my-agent.md` — Claude Code
|
||||
- `agents/my-agent.agent.md` — Copilot CLI
|
||||
|
||||
---
|
||||
|
||||
## CLI commands
|
||||
|
||||
```bash
|
||||
# Install plugin locally (development)
|
||||
copilot plugin install ./my-plugin
|
||||
|
||||
# List installed plugins
|
||||
copilot plugin list
|
||||
|
||||
# In interactive mode
|
||||
/plugin list
|
||||
/skills list
|
||||
/agent
|
||||
|
||||
# Reload after changes
|
||||
/reload-plugins
|
||||
|
||||
# Uninstall (uses bare plugin name, not @marketplace form)
|
||||
copilot plugin uninstall <name>
|
||||
|
||||
# Marketplace
|
||||
copilot plugin marketplace add owner/repo
|
||||
```
|
||||
|
||||
> ⚠️ **UNVERIFIED: Copilot marketplace install command.**
|
||||
> The `update`/`uninstall` commands take a bare `<name>`. Whether install from a marketplace
|
||||
> uses `<name>@<marketplace>` (Claude Code's form) or a bare `<name>` is not confirmed in docs.
|
||||
> Run `copilot plugin install --help` before documenting the install command anywhere.
|
||||
|
||||
---
|
||||
|
||||
## Validation
|
||||
|
||||
Copilot has no documented `plugin validate` command. For Copilot-side validation, run manual checks:
|
||||
- Valid JSON in `plugin.json` and `marketplace.json`
|
||||
- Required fields: `name`, `description`
|
||||
- Unique plugin names across marketplace
|
||||
- Kebab-case plugin names
|
||||
- All `source` paths resolve to existing directories
|
||||
- `.agent.md` files have valid YAML frontmatter with `name`, `description`, `tools`
|
||||
|
||||
---
|
||||
|
||||
## Key differences from Claude Code (summary)
|
||||
|
||||
| What | Claude Code | Copilot CLI |
|
||||
|---|---|---|
|
||||
| Plugin manifest location | `.claude-plugin/plugin.json` | `plugin.json` at plugin root |
|
||||
| Agent files | `agents/<name>.md` | `agents/<name>.agent.md` |
|
||||
| Hooks file | `hooks/hooks.json` | `hooks.json` at plugin root |
|
||||
| Component paths | Declared in marketplace entry | Declared in `plugin.json` |
|
||||
| Validate command | `claude plugin validate` | None — manual checks only |
|
||||
| Relative source `./` | Required | Optional (`x` also valid) |
|
||||
@@ -0,0 +1,79 @@
|
||||
# Cross-Tool Compatibility Reference
|
||||
|
||||
Claude Code and GitHub Copilot CLI share the plugin concept but diverge in specific, breaking
|
||||
ways. **Skills are the portable core. Manifests and agents are where they split.**
|
||||
|
||||
Make Claude Code the source of truth — it is the stricter, more fully specified format.
|
||||
Treat "loads in Copilot CLI" as a tested checklist item per plugin, not an assumption.
|
||||
|
||||
---
|
||||
|
||||
## Divergence table
|
||||
|
||||
| Concern | Claude Code | GitHub Copilot CLI | Portable choice |
|
||||
|---|---|---|---|
|
||||
| Marketplace manifest path | `.claude-plugin/marketplace.json` (required) | `.github/plugin/marketplace.json` (primary); also reads `.claude-plugin/` | Put it in `.claude-plugin/` — both read it. Optionally mirror to `.github/plugin/`. |
|
||||
| Plugin manifest path | `.claude-plugin/plugin.json` (required; only `plugin.json` goes in this dir) | `plugin.json` at **plugin root** | Ship in both locations until verified — see ⚠️ below |
|
||||
| Skills | `skills/<name>/SKILL.md` | `skills/<name>/SKILL.md` | ✅ Identical |
|
||||
| Agents | `agents/<name>.md` | `agents/<name>.agent.md` (frontmatter incl. `tools:`) | Diverges — keep portable logic in skills; ship per-tool agent files only when needed |
|
||||
| Hooks | `hooks/hooks.json` | `hooks.json` at plugin root | Diverges; declare paths in manifest to be safe |
|
||||
| MCP servers | `.mcp.json` at plugin root | `.mcp.json` at plugin root | ✅ Same |
|
||||
| Relative `source` | must start with `./` | `./x` and `x` both valid | Always use `./` — valid for both |
|
||||
| Validate command | `claude plugin validate .` (or `/plugin validate .`) | none documented | Run Claude validator + manual JSON checks for Copilot |
|
||||
| Install marketplace | `claude plugin marketplace add owner/repo` | `copilot plugin marketplace add owner/repo` | Same shape |
|
||||
| Install plugin | `claude plugin install <name>@<marketplace-name>` | install by plugin name; `@marketplace` suffix unconfirmed | ⚠️ Verify Copilot install string before documenting |
|
||||
| Local install (dev) | `claude --plugin-dir ./plugin` | `copilot plugin install ./plugin` | Tool-specific |
|
||||
| Component paths in plugin.json | Claude defaults to standard dirs; path overrides via marketplace entry only | `"skills": "skills/"`, `"agents": "agents/"`, etc. in plugin.json | Generate per-tool manifests rather than one shared file |
|
||||
|
||||
> ⚠️ **UNVERIFIED — test before committing to a layout.**
|
||||
> Copilot docs confirm it reads the **marketplace** manifest from `.claude-plugin/`. They do NOT
|
||||
> confirm the same fallback for a plugin's `plugin.json`. Copilot docs show `plugin.json` at plugin
|
||||
> root; Claude requires it in `.claude-plugin/`. Until verified: ship `plugin.json` in BOTH
|
||||
> `plugin-name/plugin.json` and `plugin-name/.claude-plugin/plugin.json` (identical content), then
|
||||
> drop whichever proves redundant.
|
||||
|
||||
---
|
||||
|
||||
## `@<marketplace-name>` resolution
|
||||
|
||||
`claude plugin install startup-cto@my-ai-marketplace` requires the marketplace manifest's
|
||||
top-level `name` field to be exactly `my-ai-marketplace`. It is **not** the GitHub repo name.
|
||||
Keep them aligned to avoid confusion, but they are separate fields.
|
||||
|
||||
---
|
||||
|
||||
## Canonical cross-compatible repo layout
|
||||
|
||||
```text
|
||||
repo-root/
|
||||
├── .claude-plugin/
|
||||
│ └── marketplace.json # both tools read here
|
||||
├── .github/plugin/
|
||||
│ └── marketplace.json # OPTIONAL: Copilot canonical path (mirror)
|
||||
├── plugins/
|
||||
│ └── startup-cto/
|
||||
│ ├── plugin.json # Copilot root manifest ┐ ship both until
|
||||
│ ├── .claude-plugin/ # │ the note above is
|
||||
│ │ └── plugin.json # Claude manifest ┘ verified
|
||||
│ ├── skills/
|
||||
│ │ ├── fundraising/SKILL.md
|
||||
│ │ └── hiring/SKILL.md
|
||||
│ ├── agents/
|
||||
│ │ ├── startup-cto.md # Claude
|
||||
│ │ └── startup-cto.agent.md # Copilot (only if shipping native agents)
|
||||
│ ├── hooks/hooks.json # Claude
|
||||
│ ├── hooks.json # Copilot (if hooks used)
|
||||
│ └── README.md
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Open questions to resolve before generating layouts
|
||||
|
||||
1. **Does Copilot CLI load a plugin whose `plugin.json` lives only in `.claude-plugin/`?**
|
||||
Install a test plugin both ways. The answer decides whether to ship one manifest or two.
|
||||
|
||||
2. **What is Copilot's exact install-from-marketplace command?**
|
||||
Run `copilot plugin install --help`. The `update`/`uninstall` commands take a bare plugin
|
||||
name — the `@marketplace` form may not apply.
|
||||
194
.agents/skills/marketplace-architect/scripts/gen_manifests.sh
Executable file
194
.agents/skills/marketplace-architect/scripts/gen_manifests.sh
Executable file
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generate plugin.json (in both locations) and marketplace.json.
|
||||
# Dry-run by default; pass --write to apply.
|
||||
#
|
||||
# Usage:
|
||||
# gen_manifests.sh <repo-root> --marketplace-name <name> [options]
|
||||
#
|
||||
# Options:
|
||||
# --marketplace-name <name> kebab-case marketplace identifier (required)
|
||||
# --author "Name <email>" author string (default: "Unknown <unknown@example.com>")
|
||||
# --plugins-dir <dir> subdir containing plugin folders (default: plugins)
|
||||
# --mirror-github also write to .github/plugin/marketplace.json
|
||||
# --write apply changes (default is dry run)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RESERVED_NAMES="claude-code-marketplace claude-code-plugins claude-plugins-official
|
||||
claude-plugins-community claude-community anthropic-marketplace anthropic-plugins
|
||||
agent-skills anthropic-agent-skills knowledge-work-plugins life-sciences
|
||||
claude-for-legal claude-for-financial-services financial-services-plugins"
|
||||
|
||||
RESERVED_PATTERNS="official-claude anthropic-tools claude-official"
|
||||
|
||||
is_kebab_case() {
|
||||
[[ "$1" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]
|
||||
}
|
||||
|
||||
is_reserved() {
|
||||
local name="$1"
|
||||
for n in $RESERVED_NAMES; do
|
||||
[[ "$name" == "$n" ]] && return 0
|
||||
done
|
||||
for p in $RESERVED_PATTERNS; do
|
||||
[[ "$name" == "$p"* ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
validate_name() {
|
||||
local name="$1" context="$2"
|
||||
local ok=true
|
||||
if ! is_kebab_case "$name"; then
|
||||
echo "ERROR: $context: name '$name' is not kebab-case (lowercase, digits, hyphens only)." >&2
|
||||
ok=false
|
||||
fi
|
||||
if is_reserved "$name"; then
|
||||
echo "ERROR: $context: name '$name' is reserved for official Anthropic use." >&2
|
||||
ok=false
|
||||
fi
|
||||
[[ "$ok" == "true" ]]
|
||||
}
|
||||
|
||||
write_json() {
|
||||
local path="$1" content="$2" dry_run="$3"
|
||||
if [[ "$dry_run" == "true" ]]; then
|
||||
echo ""
|
||||
echo "--- $path (dry run) ---"
|
||||
echo "$content"
|
||||
else
|
||||
mkdir -p "$(dirname "$path")"
|
||||
echo "$content" > "$path"
|
||||
echo " Written: $path"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── parse args ───────────────────────────────────────────────────────────────
|
||||
|
||||
ROOT=""
|
||||
MARKETPLACE_NAME=""
|
||||
AUTHOR="Unknown <unknown@example.com>"
|
||||
PLUGINS_DIR="plugins"
|
||||
MIRROR_GITHUB=false
|
||||
WRITE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--marketplace-name) MARKETPLACE_NAME="$2"; shift 2 ;;
|
||||
--author) AUTHOR="$2"; shift 2 ;;
|
||||
--plugins-dir) PLUGINS_DIR="$2"; shift 2 ;;
|
||||
--mirror-github) MIRROR_GITHUB=true; shift ;;
|
||||
--write) WRITE=true; shift ;;
|
||||
-*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
*) ROOT="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ROOT" || -z "$MARKETPLACE_NAME" ]]; then
|
||||
echo "Usage: gen_manifests.sh <repo-root> --marketplace-name <name> [--write]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ROOT="$(cd "$ROOT" && pwd)"
|
||||
DRY_RUN=$( [[ "$WRITE" == "true" ]] && echo "false" || echo "true" )
|
||||
|
||||
[[ "$DRY_RUN" == "true" ]] && echo "DRY RUN — pass --write to apply changes"
|
||||
|
||||
# Parse author
|
||||
AUTHOR_NAME="${AUTHOR%% <*}"
|
||||
AUTHOR_EMAIL=""
|
||||
if [[ "$AUTHOR" =~ \<(.+)\> ]]; then
|
||||
AUTHOR_EMAIL="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
|
||||
# Validate marketplace name
|
||||
validate_name "$MARKETPLACE_NAME" "marketplace" || exit 1
|
||||
|
||||
# Discover plugins
|
||||
PLUGINS_PATH="$ROOT/$PLUGINS_DIR"
|
||||
if [[ ! -d "$PLUGINS_PATH" ]]; then
|
||||
echo "No plugins directory found at $PLUGINS_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mapfile -t PLUGIN_DIRS < <(find "$PLUGINS_PATH" -mindepth 1 -maxdepth 1 -type d ! -name '.*' | sort)
|
||||
|
||||
if [[ ${#PLUGIN_DIRS[@]} -eq 0 ]]; then
|
||||
echo "No plugin directories found in $PLUGINS_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found ${#PLUGIN_DIRS[@]} plugin(s)"
|
||||
|
||||
# Build plugins array for marketplace.json
|
||||
PLUGINS_JSON="[]"
|
||||
|
||||
for pd in "${PLUGIN_DIRS[@]}"; do
|
||||
pname="$(basename "$pd")"
|
||||
validate_name "$pname" "plugin '$pname'" || exit 1
|
||||
|
||||
# Read existing plugin.json if present
|
||||
existing_claude="$pd/.claude-plugin/plugin.json"
|
||||
existing_root="$pd/plugin.json"
|
||||
existing_desc="Plugin: $pname"
|
||||
existing_name="$pname"
|
||||
|
||||
for existing in "$existing_claude" "$existing_root"; do
|
||||
if [[ -f "$existing" ]] && jq -e . "$existing" >/dev/null 2>&1; then
|
||||
d=$(jq -r '.description // empty' "$existing")
|
||||
n=$(jq -r '.name // empty' "$existing")
|
||||
[[ -n "$d" ]] && existing_desc="$d"
|
||||
[[ -n "$n" ]] && existing_name="$n"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Warn on version
|
||||
for existing in "$existing_claude" "$existing_root"; do
|
||||
if [[ -f "$existing" ]] && jq -e '.version' "$existing" >/dev/null 2>&1; then
|
||||
echo " WARNING: plugin '$pname' sets version in plugin.json. Do not also set it in the marketplace entry — plugin.json wins silently."
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Build plugin.json
|
||||
plugin_json=$(jq -n \
|
||||
--arg name "$existing_name" \
|
||||
--arg desc "$existing_desc" \
|
||||
--arg aname "$AUTHOR_NAME" \
|
||||
--arg aemail "$AUTHOR_EMAIL" \
|
||||
'{name: $name, description: $desc, author: {name: $aname, email: $aemail}}')
|
||||
|
||||
write_json "$pd/plugin.json" "$plugin_json" "$DRY_RUN"
|
||||
write_json "$pd/.claude-plugin/plugin.json" "$plugin_json" "$DRY_RUN"
|
||||
|
||||
source="./$PLUGINS_DIR/$pname"
|
||||
PLUGINS_JSON=$(echo "$PLUGINS_JSON" | jq \
|
||||
--arg name "$existing_name" \
|
||||
--arg src "$source" \
|
||||
--arg desc "$existing_desc" \
|
||||
'. + [{name: $name, source: $src, description: $desc}]')
|
||||
done
|
||||
|
||||
# Build marketplace.json
|
||||
marketplace_json=$(jq -n \
|
||||
--arg name "$MARKETPLACE_NAME" \
|
||||
--arg aname "$AUTHOR_NAME" \
|
||||
--arg aemail "$AUTHOR_EMAIL" \
|
||||
--arg desc "$MARKETPLACE_NAME plugin marketplace" \
|
||||
--argjson plugins "$PLUGINS_JSON" \
|
||||
'{name: $name, owner: {name: $aname, email: $aemail}, description: $desc, plugins: $plugins}')
|
||||
|
||||
write_json "$ROOT/.claude-plugin/marketplace.json" "$marketplace_json" "$DRY_RUN"
|
||||
|
||||
if [[ "$MIRROR_GITHUB" == "true" ]]; then
|
||||
write_json "$ROOT/.github/plugin/marketplace.json" "$marketplace_json" "$DRY_RUN"
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo ""
|
||||
echo "--- End dry run. Pass --write to apply. ---"
|
||||
else
|
||||
echo ""
|
||||
echo "Done. Run scripts/validate.sh to verify."
|
||||
fi
|
||||
121
.agents/skills/marketplace-architect/scripts/inventory.sh
Executable file
121
.agents/skills/marketplace-architect/scripts/inventory.sh
Executable file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scan a repository and classify every asset as skill/command/agent/hook/prompt/MCP.
|
||||
# Outputs a markdown table of findings plus a list of cross-reference warnings.
|
||||
#
|
||||
# Usage: inventory.sh <repo-path>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: inventory.sh <repo-path>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ROOT="$(cd "$1" && pwd)"
|
||||
|
||||
if [[ ! -d "$ROOT" ]]; then
|
||||
echo "Error: $ROOT is not a directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── classify assets ──────────────────────────────────────────────────────────
|
||||
|
||||
declare -a ROWS=()
|
||||
declare -a CROSS_REFS=()
|
||||
|
||||
while IFS= read -r -d '' path; do
|
||||
rel="${path#"$ROOT/"}"
|
||||
name="$(basename "$path")"
|
||||
dir="$(dirname "$rel")"
|
||||
parent="$(basename "$dir")"
|
||||
|
||||
# Skip hidden dirs except .claude-plugin and .github
|
||||
skip=false
|
||||
IFS='/' read -ra parts <<< "$dir"
|
||||
for part in "${parts[@]}"; do
|
||||
if [[ "$part" == .* && "$part" != ".claude-plugin" && "$part" != ".github" && "$part" != ".agents" ]]; then
|
||||
skip=true; break
|
||||
fi
|
||||
done
|
||||
$skip && continue
|
||||
|
||||
asset_type=""
|
||||
|
||||
case "$name" in
|
||||
SKILL.md) asset_type="skill" ;;
|
||||
hooks.json) asset_type="hook" ;;
|
||||
.mcp.json) asset_type="mcp" ;;
|
||||
.lsp.json) asset_type="lsp" ;;
|
||||
plugin.json) asset_type="manifest-plugin" ;;
|
||||
marketplace.json) asset_type="manifest-marketplace" ;;
|
||||
*.agent.md) asset_type="agent-copilot" ;;
|
||||
*.md)
|
||||
if [[ "$parent" == "agents" ]]; then
|
||||
asset_type="agent-claude"
|
||||
elif [[ "$parent" == "commands" ]]; then
|
||||
asset_type="command"
|
||||
elif [[ "$rel" != *"/skills/"* && "$rel" != *"/commands/"* && "$rel" != *"/agents/"* ]]; then
|
||||
asset_type="prompt"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
[[ -n "$asset_type" ]] && ROWS+=("$asset_type|$rel")
|
||||
|
||||
# Check for cross-references in text files
|
||||
case "$name" in *.md|*.json|*.sh)
|
||||
if grep -q '\.\.\/' "$path" 2>/dev/null; then
|
||||
while IFS= read -r line; do
|
||||
lineno="${line%%:*}"
|
||||
content="${line#*:}"
|
||||
CROSS_REFS+=("$rel:$lineno: $content")
|
||||
done < <(grep -n '\.\.\/' "$path" 2>/dev/null | head -20)
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
done < <(find "$ROOT" -type f -print0 | sort -z)
|
||||
|
||||
# ── report ───────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "# Asset Inventory: $ROOT"
|
||||
echo ""
|
||||
echo "## Assets"
|
||||
echo ""
|
||||
echo "| Type | Path |"
|
||||
echo "|---|---|"
|
||||
for row in "${ROWS[@]+"${ROWS[@]}"}"; do
|
||||
type="${row%%|*}"
|
||||
path="${row#*|}"
|
||||
echo "| \`$type\` | \`$path\` |"
|
||||
done | sort
|
||||
|
||||
total="${#ROWS[@]}"
|
||||
echo ""
|
||||
echo "**Total: $total assets**"
|
||||
echo ""
|
||||
|
||||
# Summary by type
|
||||
echo "## Summary by type"
|
||||
echo ""
|
||||
for row in "${ROWS[@]+"${ROWS[@]}"}"; do
|
||||
echo "${row%%|*}"
|
||||
done | sort | uniq -c | while read -r count type; do
|
||||
echo "- \`$type\`: $count"
|
||||
done
|
||||
|
||||
# Cross-reference warnings
|
||||
echo ""
|
||||
if [[ ${#CROSS_REFS[@]} -gt 0 ]]; then
|
||||
echo "## ⚠️ Cross-reference warnings (${#CROSS_REFS[@]} found)"
|
||||
echo ""
|
||||
echo "These \`../\` references will break after install-time caching:"
|
||||
echo ""
|
||||
for ref in "${CROSS_REFS[@]}"; do
|
||||
echo "- \`$ref\`"
|
||||
done
|
||||
else
|
||||
echo "## Cross-references"
|
||||
echo ""
|
||||
echo "No \`../\` cross-references found. Safe to proceed with plugin boundaries."
|
||||
fi
|
||||
332
.agents/skills/marketplace-architect/scripts/validate.sh
Executable file
332
.agents/skills/marketplace-architect/scripts/validate.sh
Executable file
@@ -0,0 +1,332 @@
|
||||
#!/usr/bin/env bash
|
||||
# Validate plugin marketplace manifests for Claude Code and GitHub Copilot CLI.
|
||||
# Wraps `claude plugin validate` (Claude-side) and runs manual checks (Copilot-side).
|
||||
#
|
||||
# Usage:
|
||||
# validate.sh <repo-root>
|
||||
# validate.sh <repo-root> --plugin plugins/my-plugin
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RESERVED_NAMES="claude-code-marketplace claude-code-plugins claude-plugins-official
|
||||
claude-plugins-community claude-community anthropic-marketplace anthropic-plugins
|
||||
agent-skills anthropic-agent-skills knowledge-work-plugins life-sciences
|
||||
claude-for-legal claude-for-financial-services financial-services-plugins"
|
||||
|
||||
RESERVED_PATTERNS="official-claude anthropic-tools claude-official"
|
||||
|
||||
ERRORS=0
|
||||
WARNINGS=0
|
||||
|
||||
error() { echo "ERROR: $1"; ((ERRORS++)) || true; }
|
||||
warn() { echo "WARN: $1"; ((WARNINGS++)) || true; }
|
||||
|
||||
is_kebab_case() { [[ "$1" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; }
|
||||
|
||||
is_reserved() {
|
||||
local name="$1"
|
||||
for n in $RESERVED_NAMES; do [[ "$name" == "$n" ]] && return 0; done
|
||||
for p in $RESERVED_PATTERNS; do [[ "$name" == "$p"* ]] && return 0; done
|
||||
return 1
|
||||
}
|
||||
|
||||
validate_name() {
|
||||
local name="$1" context="$2"
|
||||
[[ -z "$name" ]] && { error "$context: name is missing or empty"; return 0; }
|
||||
is_kebab_case "$name" || error "$context: name '$name' is not kebab-case"
|
||||
if is_reserved "$name"; then
|
||||
error "$context: name '$name' is reserved for official Anthropic use"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
valid_json() {
|
||||
local path="$1"
|
||||
if ! jq -e . "$path" >/dev/null 2>&1; then
|
||||
error "Invalid JSON in $path"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
validate_marketplace_json() {
|
||||
local path="$1"
|
||||
[[ -f "$path" ]] || return 0
|
||||
valid_json "$path" || return 0
|
||||
|
||||
local name
|
||||
name=$(jq -r '.name // empty' "$path")
|
||||
[[ -z "$name" ]] && error "$path: 'name' field is required" || validate_name "$name" "$path"
|
||||
|
||||
local plugins_type
|
||||
plugins_type=$(jq -r 'if .plugins | type == "array" then "ok" else "bad" end' "$path")
|
||||
if [[ "$plugins_type" != "ok" ]]; then
|
||||
error "$path: 'plugins' must be an array"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check each plugin entry
|
||||
local seen_names=()
|
||||
while IFS= read -r pname; do
|
||||
# Duplicate check
|
||||
for seen in "${seen_names[@]+"${seen_names[@]}"}"; do
|
||||
if [[ "$seen" == "$pname" ]]; then error "$path: duplicate plugin name '$pname'"; fi
|
||||
done
|
||||
seen_names+=("$pname")
|
||||
validate_name "$pname" "$path plugin '$pname'"
|
||||
|
||||
# Source path check
|
||||
local src
|
||||
src=$(jq -r --arg n "$pname" '.plugins[] | select(.name==$n) | .source // empty' "$path")
|
||||
if [[ -n "$src" && "$src" != ./* && "$src" != "github" && "$src" != "npm" && "$src" != "url" && "$src" != "git-subdir" ]]; then
|
||||
warn "$path plugin '$pname': relative source '$src' should start with './' for Claude Code compatibility"
|
||||
fi
|
||||
|
||||
# Version duplication warning
|
||||
local has_ver
|
||||
has_ver=$(jq -r --arg n "$pname" '.plugins[] | select(.name==$n) | .version // empty' "$path")
|
||||
if [[ -n "$has_ver" ]]; then
|
||||
warn "$path plugin '$pname': version set in marketplace entry. If also set in plugin.json, plugin.json wins silently."
|
||||
fi
|
||||
|
||||
done < <(jq -r '.plugins[].name // empty' "$path")
|
||||
return 0
|
||||
}
|
||||
|
||||
validate_plugin_json() {
|
||||
local path="$1" marketplace_json="${2:-}"
|
||||
[[ -f "$path" ]] || return 0
|
||||
valid_json "$path" || return 0
|
||||
|
||||
local name
|
||||
name=$(jq -r '.name // empty' "$path")
|
||||
if [[ -z "$name" ]]; then
|
||||
warn "$path: 'name' field missing (plugin dir name will be used)"
|
||||
else
|
||||
validate_name "$name" "$path"
|
||||
|
||||
# Version duplication check
|
||||
if [[ -n "$marketplace_json" && -f "$marketplace_json" ]]; then
|
||||
local pver mver
|
||||
pver=$(jq -r '.version // empty' "$path")
|
||||
mver=$(jq -r --arg n "$name" '.plugins[]? | select(.name==$n) | .version // empty' "$marketplace_json")
|
||||
if [[ -n "$pver" && -n "$mver" ]]; then
|
||||
error "$path: version '$pver' set in both plugin.json and marketplace entry — plugin.json wins silently. Remove one."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
validate_skill_md() {
|
||||
local path="$1"
|
||||
local content
|
||||
content=$(cat "$path")
|
||||
if [[ "$content" != ---* ]]; then
|
||||
warn "$path: SKILL.md has no YAML frontmatter"
|
||||
return
|
||||
fi
|
||||
if ! echo "$content" | awk 'NR>1 && /^---/' | grep -q '^---'; then
|
||||
error "$path: SKILL.md frontmatter not closed"
|
||||
return
|
||||
fi
|
||||
if ! echo "$content" | awk '/^---/{n++; if(n==2) exit} n==1' | grep -q 'description:'; then
|
||||
warn "$path: SKILL.md frontmatter missing 'description' field"
|
||||
fi
|
||||
}
|
||||
|
||||
validate_plugin_dir() {
|
||||
local pd="$1" marketplace_json="${2:-}"
|
||||
|
||||
local claude_manifest="$pd/.claude-plugin/plugin.json"
|
||||
local root_manifest="$pd/plugin.json"
|
||||
|
||||
if [[ ! -f "$claude_manifest" && ! -f "$root_manifest" ]]; then
|
||||
warn "$pd: no plugin.json found (will auto-discover components)"
|
||||
else
|
||||
validate_plugin_json "$claude_manifest" "$marketplace_json"
|
||||
validate_plugin_json "$root_manifest" "$marketplace_json"
|
||||
|
||||
# Sync check — shared identity fields must match; component path fields legitimately diverge
|
||||
if [[ -f "$claude_manifest" && -f "$root_manifest" ]]; then
|
||||
local field cv rv
|
||||
for field in name description version license; do
|
||||
cv=$(jq -r ".$field // empty" "$claude_manifest")
|
||||
rv=$(jq -r ".$field // empty" "$root_manifest")
|
||||
if [[ ( -n "$cv" || -n "$rv" ) && "$cv" != "$rv" ]]; then
|
||||
error "$pd: '$field' differs between .claude-plugin/plugin.json ('$cv') and plugin.json ('$rv')"
|
||||
fi
|
||||
done
|
||||
cv=$(jq -r '.author.name // empty' "$claude_manifest")
|
||||
rv=$(jq -r '.author.name // empty' "$root_manifest")
|
||||
if [[ ( -n "$cv" || -n "$rv" ) && "$cv" != "$rv" ]]; then
|
||||
error "$pd: 'author.name' differs between .claude-plugin/plugin.json ('$cv') and plugin.json ('$rv')"
|
||||
fi
|
||||
cv=$(jq -r '.keywords // [] | sort | join(",")' "$claude_manifest")
|
||||
rv=$(jq -r '.keywords // [] | sort | join(",")' "$root_manifest")
|
||||
if [[ "$cv" != "$rv" ]]; then
|
||||
error "$pd: 'keywords' differs between .claude-plugin/plugin.json and plugin.json"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Components must not be inside .claude-plugin/
|
||||
for bad_dir in skills agents hooks commands; do
|
||||
if [[ -d "$pd/.claude-plugin/$bad_dir" ]]; then
|
||||
error "$pd/.claude-plugin/$bad_dir: only plugin.json belongs in .claude-plugin/; move $bad_dir/ to plugin root"
|
||||
fi
|
||||
done
|
||||
|
||||
# Validate SKILL.md files
|
||||
while IFS= read -r -d '' skill_md; do
|
||||
validate_skill_md "$skill_md"
|
||||
done < <(find "$pd" -name "SKILL.md" -print0 2>/dev/null)
|
||||
|
||||
# Cross-reference check
|
||||
while IFS= read -r -d '' f; do
|
||||
if grep -q '\.\.\/' "$f" 2>/dev/null; then
|
||||
local rel="${f#"$pd/"}"
|
||||
error "$rel: contains '../' reference — plugins cannot access files outside their directory after caching"
|
||||
fi
|
||||
done < <(find "$pd" \( -name "*.md" -o -name "*.json" \) -print0 2>/dev/null)
|
||||
}
|
||||
|
||||
run_claude_validate() {
|
||||
local path="$1"
|
||||
if command -v claude >/dev/null 2>&1; then
|
||||
if ! claude plugin validate "$path" 2>&1; then
|
||||
error "claude plugin validate failed for $path"
|
||||
fi
|
||||
else
|
||||
warn "'claude' CLI not found — skipping claude plugin validate"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── parse args ────────────────────────────────────────────────────────────────
|
||||
|
||||
ROOT=""
|
||||
PLUGIN_ONLY=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--plugin) PLUGIN_ONLY="$2"; shift 2 ;;
|
||||
-*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
*) ROOT="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ROOT" ]]; then
|
||||
echo "Usage: validate.sh <repo-root> [--plugin <path>]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ROOT="$(cd "$ROOT" && pwd)"
|
||||
|
||||
# ── validate marketplace.json ─────────────────────────────────────────────────
|
||||
|
||||
CLAUDE_MARKETPLACE="$ROOT/.claude-plugin/marketplace.json"
|
||||
COPILOT_MARKETPLACE="$ROOT/.github/plugin/marketplace.json"
|
||||
MARKETPLACE_JSON=""
|
||||
|
||||
for mp in "$CLAUDE_MARKETPLACE" "$COPILOT_MARKETPLACE"; do
|
||||
if [[ -f "$mp" ]]; then
|
||||
[[ -z "$MARKETPLACE_JSON" ]] && MARKETPLACE_JSON="$mp"
|
||||
validate_marketplace_json "$mp"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$MARKETPLACE_JSON" ]]; then
|
||||
warn "No marketplace.json found. Expected at .claude-plugin/marketplace.json"
|
||||
fi
|
||||
|
||||
# Marketplace sync check — shared identity fields must match; description/version
|
||||
# legitimately differ in structure (Claude: top-level; Copilot: under metadata)
|
||||
if [[ -f "$CLAUDE_MARKETPLACE" && -f "$COPILOT_MARKETPLACE" ]]; then
|
||||
cm_val=$(jq -r '.name // empty' "$CLAUDE_MARKETPLACE")
|
||||
cp_val=$(jq -r '.name // empty' "$COPILOT_MARKETPLACE")
|
||||
if [[ "$cm_val" != "$cp_val" ]]; then
|
||||
error "marketplace: 'name' differs — .claude-plugin ('$cm_val') vs .github/plugin ('$cp_val')"
|
||||
fi
|
||||
|
||||
cm_val=$(jq -r '.owner.name // empty' "$CLAUDE_MARKETPLACE")
|
||||
cp_val=$(jq -r '.owner.name // empty' "$COPILOT_MARKETPLACE")
|
||||
if [[ ( -n "$cm_val" || -n "$cp_val" ) && "$cm_val" != "$cp_val" ]]; then
|
||||
error "marketplace: 'owner.name' differs — '$cm_val' vs '$cp_val'"
|
||||
fi
|
||||
|
||||
# description: Claude top-level, Copilot under metadata — compare values regardless of path
|
||||
cm_val=$(jq -r '.description // .metadata.description // empty' "$CLAUDE_MARKETPLACE")
|
||||
cp_val=$(jq -r '.metadata.description // .description // empty' "$COPILOT_MARKETPLACE")
|
||||
if [[ ( -n "$cm_val" || -n "$cp_val" ) && "$cm_val" != "$cp_val" ]]; then
|
||||
error "marketplace: description differs between .claude-plugin/marketplace.json and .github/plugin/marketplace.json"
|
||||
fi
|
||||
|
||||
# version: same structural divergence as description
|
||||
cm_val=$(jq -r '.version // .metadata.version // empty' "$CLAUDE_MARKETPLACE")
|
||||
cp_val=$(jq -r '.metadata.version // .version // empty' "$COPILOT_MARKETPLACE")
|
||||
if [[ ( -n "$cm_val" || -n "$cp_val" ) && "$cm_val" != "$cp_val" ]]; then
|
||||
error "marketplace: version differs — '$cm_val' vs '$cp_val'"
|
||||
fi
|
||||
|
||||
# Plugin catalog must be identical across both files
|
||||
cm_plugins=$(jq -r '.plugins[].name' "$CLAUDE_MARKETPLACE" 2>/dev/null | sort)
|
||||
cp_plugins=$(jq -r '.plugins[].name' "$COPILOT_MARKETPLACE" 2>/dev/null | sort)
|
||||
if [[ "$cm_plugins" != "$cp_plugins" ]]; then
|
||||
error "marketplace: plugin lists differ between .claude-plugin/marketplace.json and .github/plugin/marketplace.json"
|
||||
else
|
||||
while IFS= read -r pname; do
|
||||
[[ -z "$pname" ]] && continue
|
||||
cm_val=$(jq -r --arg n "$pname" '.plugins[] | select(.name==$n) | .source // empty' "$CLAUDE_MARKETPLACE")
|
||||
cp_val=$(jq -r --arg n "$pname" '.plugins[] | select(.name==$n) | .source // empty' "$COPILOT_MARKETPLACE")
|
||||
if [[ "$cm_val" != "$cp_val" ]]; then
|
||||
error "marketplace plugin '$pname': source differs — '$cm_val' vs '$cp_val'"
|
||||
fi
|
||||
cm_val=$(jq -r --arg n "$pname" '.plugins[] | select(.name==$n) | .description // empty' "$CLAUDE_MARKETPLACE")
|
||||
cp_val=$(jq -r --arg n "$pname" '.plugins[] | select(.name==$n) | .description // empty' "$COPILOT_MARKETPLACE")
|
||||
if [[ ( -n "$cm_val" || -n "$cp_val" ) && "$cm_val" != "$cp_val" ]]; then
|
||||
error "marketplace plugin '$pname': description differs between the two marketplace.json files"
|
||||
fi
|
||||
done <<< "$cm_plugins"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── validate plugins ──────────────────────────────────────────────────────────
|
||||
|
||||
if [[ -n "$PLUGIN_ONLY" ]]; then
|
||||
validate_plugin_dir "$(cd "$PLUGIN_ONLY" && pwd)" "$MARKETPLACE_JSON"
|
||||
run_claude_validate "$(cd "$PLUGIN_ONLY" && pwd)"
|
||||
else
|
||||
plugins_path="$ROOT/plugins"
|
||||
if [[ -d "$plugins_path" ]]; then
|
||||
while IFS= read -r -d '' pd; do
|
||||
validate_plugin_dir "$pd" "$MARKETPLACE_JSON"
|
||||
run_claude_validate "$pd"
|
||||
done < <(find "$plugins_path" -mindepth 1 -maxdepth 1 -type d ! -name '.*' -print0 | sort -z)
|
||||
else
|
||||
warn "No plugins/ directory found at $ROOT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── check source paths resolve ────────────────────────────────────────────────
|
||||
|
||||
if [[ -n "$MARKETPLACE_JSON" ]]; then
|
||||
while IFS= read -r src; do
|
||||
[[ "$src" != ./* ]] && continue
|
||||
src_path="$ROOT/${src#./}"
|
||||
if [[ ! -d "$src_path" ]]; then error "Marketplace source path '$src' does not exist at $src_path"; fi
|
||||
done < <(jq -r '.plugins[]?.source | strings' "$MARKETPLACE_JSON" 2>/dev/null)
|
||||
fi
|
||||
|
||||
# ── report ────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
if [[ $ERRORS -eq 0 && $WARNINGS -eq 0 ]]; then
|
||||
echo "✓ All checks passed."
|
||||
exit 0
|
||||
elif [[ $ERRORS -eq 0 ]]; then
|
||||
echo "Passed with $WARNINGS warning(s)."
|
||||
exit 0
|
||||
else
|
||||
echo "Failed. Fix $ERRORS error(s) before proceeding."
|
||||
exit 1
|
||||
fi
|
||||
194
.agents/skills/marketplace-architect/tests/test_scripts.sh
Executable file
194
.agents/skills/marketplace-architect/tests/test_scripts.sh
Executable file
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPTS_DIR="$(cd "$(dirname "$0")/../scripts" && pwd)"
|
||||
PASS=0; FAIL=0
|
||||
# Use += to avoid ((var++)) returning 0 exit code when var was 0 under set -e
|
||||
|
||||
# ── helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
tmpdir() { mktemp -d; }
|
||||
|
||||
assert_contains() {
|
||||
local label="$1" expected="$2" actual="$3"
|
||||
if echo "$actual" | grep -qF "$expected"; then
|
||||
echo " PASS: $label"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo " FAIL: $label"
|
||||
echo " expected to contain: $expected"
|
||||
echo " got: $(echo "$actual" | head -5)"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_contains() {
|
||||
local label="$1" unexpected="$2" actual="$3"
|
||||
if echo "$actual" | grep -qF "$unexpected"; then
|
||||
echo " FAIL: $label"
|
||||
echo " expected NOT to contain: $unexpected"
|
||||
FAIL=$((FAIL+1))
|
||||
else
|
||||
echo " PASS: $label"
|
||||
PASS=$((PASS+1))
|
||||
fi
|
||||
}
|
||||
|
||||
assert_exit() {
|
||||
local label="$1" expected="$2" actual="$3"
|
||||
if [[ "$actual" -eq "$expected" ]]; then
|
||||
echo " PASS: $label"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo " FAIL: $label"
|
||||
echo " expected exit $expected, got $actual"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
assert_file_exists() {
|
||||
local label="$1" path="$2"
|
||||
if [[ -f "$path" ]]; then
|
||||
echo " PASS: $label"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo " FAIL: $label"
|
||||
echo " file not found: $path"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
assert_file_absent() {
|
||||
local label="$1" path="$2"
|
||||
if [[ ! -e "$path" ]]; then
|
||||
echo " PASS: $label"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo " FAIL: $label"
|
||||
echo " file should not exist: $path"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
# ── inventory.sh tests ───────────────────────────────────────────────────────
|
||||
|
||||
echo "=== inventory.sh ==="
|
||||
|
||||
# 1. SKILL.md classified as skill
|
||||
t=$(tmpdir)
|
||||
mkdir -p "$t/skills/my-skill"
|
||||
echo "---" > "$t/skills/my-skill/SKILL.md"
|
||||
out=$("$SCRIPTS_DIR/inventory.sh" "$t")
|
||||
assert_contains "SKILL.md classified as skill" "skill" "$out"
|
||||
rm -rf "$t"
|
||||
|
||||
# 2. agents/foo.agent.md classified as agent-copilot
|
||||
t=$(tmpdir)
|
||||
mkdir -p "$t/agents"
|
||||
touch "$t/agents/my-agent.agent.md"
|
||||
out=$("$SCRIPTS_DIR/inventory.sh" "$t")
|
||||
assert_contains "agents/*.agent.md classified as agent-copilot" "agent-copilot" "$out"
|
||||
rm -rf "$t"
|
||||
|
||||
# 3. ../ in file content produces cross-reference warning
|
||||
t=$(tmpdir)
|
||||
mkdir -p "$t/skills/my-skill"
|
||||
printf -- '---\ndescription: test\n---\nSee ../shared/file.md\n' > "$t/skills/my-skill/SKILL.md"
|
||||
out=$("$SCRIPTS_DIR/inventory.sh" "$t")
|
||||
assert_contains "../ cross-reference warning emitted" "Cross-reference" "$out"
|
||||
assert_contains "../ path shown in warning" "../shared/file.md" "$out"
|
||||
rm -rf "$t"
|
||||
|
||||
# ── gen_manifests.sh tests ───────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "=== gen_manifests.sh ==="
|
||||
|
||||
# 4. Without --write, no files are created
|
||||
t=$(tmpdir)
|
||||
mkdir -p "$t/plugins/my-plugin/skills/hello"
|
||||
echo "---" > "$t/plugins/my-plugin/skills/hello/SKILL.md"
|
||||
"$SCRIPTS_DIR/gen_manifests.sh" "$t" --marketplace-name my-marketplace >/dev/null
|
||||
assert_file_absent "dry run: .claude-plugin/marketplace.json not written" "$t/.claude-plugin/marketplace.json"
|
||||
assert_file_absent "dry run: plugin.json not written" "$t/plugins/my-plugin/plugin.json"
|
||||
rm -rf "$t"
|
||||
|
||||
# 5. With --write, creates plugin.json in both locations
|
||||
t=$(tmpdir)
|
||||
mkdir -p "$t/plugins/my-plugin/skills/hello"
|
||||
echo "---" > "$t/plugins/my-plugin/skills/hello/SKILL.md"
|
||||
"$SCRIPTS_DIR/gen_manifests.sh" "$t" --marketplace-name my-marketplace --write >/dev/null
|
||||
assert_file_exists "--write: plugin root plugin.json created" "$t/plugins/my-plugin/plugin.json"
|
||||
assert_file_exists "--write: .claude-plugin/plugin.json created" "$t/plugins/my-plugin/.claude-plugin/plugin.json"
|
||||
rm -rf "$t"
|
||||
|
||||
# 6. Reserved name exits 1
|
||||
t=$(tmpdir)
|
||||
mkdir -p "$t/plugins/my-plugin"
|
||||
set +e
|
||||
out=$("$SCRIPTS_DIR/gen_manifests.sh" "$t" --marketplace-name claude-plugins-official 2>&1)
|
||||
code=$?
|
||||
set -e
|
||||
assert_exit "reserved name: exit 1" 1 "$code"
|
||||
assert_contains "reserved name: error message" "reserved" "$out"
|
||||
rm -rf "$t"
|
||||
|
||||
# ── validate.sh tests ────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "=== validate.sh ==="
|
||||
|
||||
# 7. Invalid JSON exits 1
|
||||
t=$(tmpdir)
|
||||
mkdir -p "$t/.claude-plugin"
|
||||
echo "not json" > "$t/.claude-plugin/marketplace.json"
|
||||
set +e
|
||||
out=$("$SCRIPTS_DIR/validate.sh" "$t" 2>&1)
|
||||
code=$?
|
||||
set -e
|
||||
assert_exit "invalid JSON: exit 1" 1 "$code"
|
||||
assert_contains "invalid JSON: error message" "ERROR" "$out"
|
||||
rm -rf "$t"
|
||||
|
||||
# 8. Reserved marketplace name exits 1
|
||||
t=$(tmpdir)
|
||||
mkdir -p "$t/.claude-plugin"
|
||||
printf '{"name":"claude-plugins-official","plugins":[]}\n' > "$t/.claude-plugin/marketplace.json"
|
||||
set +e
|
||||
out=$("$SCRIPTS_DIR/validate.sh" "$t" 2>&1)
|
||||
code=$?
|
||||
set -e
|
||||
assert_exit "reserved marketplace name: exit 1" 1 "$code"
|
||||
assert_contains "reserved marketplace name: error message" "reserved" "$out"
|
||||
rm -rf "$t"
|
||||
|
||||
# 9. ../ in plugin file exits 1
|
||||
t=$(tmpdir)
|
||||
mkdir -p "$t/.claude-plugin" "$t/plugins/my-plugin/skills/hello"
|
||||
printf '{"name":"my-marketplace","plugins":[{"name":"my-plugin","source":"./plugins/my-plugin"}]}\n' \
|
||||
> "$t/.claude-plugin/marketplace.json"
|
||||
printf -- '---\ndescription: test\n---\nSee ../shared.md\n' \
|
||||
> "$t/plugins/my-plugin/skills/hello/SKILL.md"
|
||||
set +e
|
||||
out=$("$SCRIPTS_DIR/validate.sh" "$t" 2>&1)
|
||||
code=$?
|
||||
set -e
|
||||
assert_exit "..// in plugin: exit 1" 1 "$code"
|
||||
assert_contains "..// in plugin: error message" "../" "$out"
|
||||
rm -rf "$t"
|
||||
|
||||
# 10. No marketplace.json warns but exits 0
|
||||
t=$(tmpdir)
|
||||
set +e
|
||||
out=$("$SCRIPTS_DIR/validate.sh" "$t" 2>&1)
|
||||
code=$?
|
||||
set -e
|
||||
assert_exit "no marketplace.json: exit 0" 0 "$code"
|
||||
assert_contains "no marketplace.json: warning emitted" "WARN" "$out"
|
||||
rm -rf "$t"
|
||||
|
||||
# ── summary ──────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
@@ -12,4 +12,5 @@
|
||||
| `iac` | write-ansible-role, write-terraform-module, write-k8s-manifest, write-docker-compose, proxmox-vm-spec, iac-security-review, write-molecule-test |
|
||||
| `cross-cutting` | zoom-out, caveman, session-handoff, governance-check, git-guardrails, git-commit-message |
|
||||
| `factory` | write-skill, write-adr, write-workflow, write-eval, validate-skill, upgrade-skill, write-issue-spec |
|
||||
| `marketplace` | marketplace-architect — plugin and skill distribution tooling for Claude Code / GitHub Copilot CLI |
|
||||
| `roles` | architect, developer, reviewer, security, qa, ops — Chunk 5 |
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
```yaml
|
||||
version: "1.2"
|
||||
version: "1.5"
|
||||
updated: 2026-05-26
|
||||
|
||||
# when: describes when this skill is loaded — the full trigger context.
|
||||
|
||||
@@ -34,7 +34,8 @@ metadata:
|
||||
Do not include a constraint about body section structure — the template enforces that.
|
||||
Example:
|
||||
- Frontmatter has three fields only: `name`, `description`, and `metadata.category` — add `allowed-tools` only when the skill has a narrow, well-defined tool surface
|
||||
- Body ≤500 lines — move anything longer into separate files in the skill directory -->
|
||||
- Body ≤500 lines — content that explains rather than directs belongs in sub-files, not the body
|
||||
- Sub-files use three spec-defined optional directories: `scripts/` (executable code), `references/` (on-demand docs), `assets/` (templates, data files, lookup tables). File references must be one level deep. Wire each sub-file with an explicit step instruction (e.g. "See references/lookup.md for error codes") — without wiring, the file is never loaded -->
|
||||
|
||||
- <constraint>
|
||||
|
||||
@@ -58,9 +59,10 @@ metadata:
|
||||
<!-- Describe the files or artifacts produced. Include paths and how they are created
|
||||
(copy-fill from template, generated, etc.). State the template used for structured file output.
|
||||
Example:
|
||||
Two files produced for every skill:
|
||||
Two files produced for every skill, plus optional sub-files if the skill requires them:
|
||||
- `SKILL.md` — copy-filled from `SKILL-TEMPLATE.md` at `.agents/skills/<name>/SKILL.md`
|
||||
- `META.md` — copy-filled from `META-TEMPLATE.md` at `.agents/skills/<name>/META.md` -->
|
||||
- `META.md` — copy-filled from `META-TEMPLATE.md` at `.agents/skills/<name>/META.md`
|
||||
- `scripts/`, `references/`, or `assets/` — created only when needed; each file wired with an explicit step instruction -->
|
||||
|
||||
<description of output>
|
||||
|
||||
|
||||
@@ -20,8 +20,9 @@ Negative trigger cases are NOT a required input. The agent proposes them based o
|
||||
## Constraints
|
||||
|
||||
- Write two files for every skill: `SKILL.md` at `.agents/skills/<name>/SKILL.md` and `META.md` alongside it
|
||||
- Frontmatter has three fields only: `name`, `description`, and `metadata.category` — add `allowed-tools` only when the skill has a narrow, well-defined tool surface
|
||||
- Keep the body under 500 lines — move anything longer into separate files in the skill directory
|
||||
- Frontmatter required fields: `name`, `description`, `metadata.category` — add `allowed-tools` only when the skill has a narrow, well-defined tool surface; add `model:` only when the skill's task complexity warrants a specific model tier (see SKILL-TEMPLATE.md for routing guidance)
|
||||
- Keep the body under 500 lines — content that explains rather than directs belongs in sub-files, not the body
|
||||
- Sub-files use three spec-defined optional directories: `scripts/` (executable code), `references/` (on-demand docs), `assets/` (templates, data files, lookup tables); additional files (e.g. `META.md`) are valid at the skill root. File references must be one level deep — no nested chains. Wire each sub-file with an explicit instruction in the step that needs it (e.g. `"See references/lookup.md for error codes"`) — without a wiring instruction the file is never loaded
|
||||
- Use XML tags only when the body has three or more logical sections and exceeds 500 tokens — default to plain prose
|
||||
- Test the trigger description against all three cases — explicit, implicit, negative — before writing any body content. Hard gate: a failed case means revise and retest, not proceed
|
||||
- Check for overlapping skills in `.agents/skills/` before writing anything — if overlap is found, surface it and wait for direction
|
||||
@@ -37,24 +38,29 @@ Negative trigger cases are NOT a required input. The agent proposes them based o
|
||||
|
||||
2. **Grill.** Run a focused grill with the /grill-me skill to reach shared understanding of: skill name, category, purpose, and use cases. One question at a time, with a recommendation for each.
|
||||
|
||||
3. **Write and test the trigger description.** Using the agreed name, category, purpose, and use cases from the grill, draft `description:`. Propose negative trigger cases based on the skill's purpose and adjacent skills — get explicit user confirmation before running tests. Test all three cases and show per-case PASS/FAIL. A failed case means revise and retest — do not proceed.
|
||||
3. **Conflict check.** Spawn a sub-agent: read `docs/ai-constitution.md`, `docs/research/ai-coding-factory/ai-coding-factory-principles.md`, and `docs/notes/factory-integration-decisions.md`, then check the agreed skill purpose and design against all three. Where a factory principle is superseded by an integration decision, the decision takes precedence — do not flag it as a conflict. Return a numbered list of genuine unresolved tensions, or confirm none found. An empty list is a valid result. Hard gate: resolve any findings before proceeding.
|
||||
|
||||
4. **Walk through each section.** For each section in `SKILL-TEMPLATE.md`: propose content, state where it comes from, present alternatives if they exist. Wait for explicit human confirmation before moving to the next section.
|
||||
4. **Write and test the trigger description.** Using the agreed name, category, purpose, and use cases from the grill, draft `description:`. Propose negative trigger cases based on the skill's purpose and adjacent skills — get explicit user confirmation before running tests. Test all three cases and show per-case PASS/FAIL. A failed case means revise and retest — do not proceed.
|
||||
|
||||
5. **Copy both templates.** Copy `SKILL-TEMPLATE.md` to `.agents/skills/<name>/SKILL.md`. Copy `META-TEMPLATE.md` to `.agents/skills/<name>/META.md`. Do not modify content yet — copy first, fill second.
|
||||
5. **Walk through each section.** For each section in `SKILL-TEMPLATE.md`: propose content, state where it comes from, present alternatives if they exist. Wait for explicit human confirmation before moving to the next section.
|
||||
|
||||
6. **Fill both files.** Fill in the copied `SKILL.md` with confirmed section content. Fill in the copied `META.md` with version, updated date, when, source (if applicable), and references (if applicable).
|
||||
6. **Copy both templates.** Copy `SKILL-TEMPLATE.md` to `.agents/skills/<name>/SKILL.md`. Copy `META-TEMPLATE.md` to `.agents/skills/<name>/META.md`. Do not modify content yet — copy first, fill second.
|
||||
|
||||
7. **Invoke `write-eval`.** Do not mark the skill complete without an eval file.
|
||||
7. **Fill both files.** Fill in the copied `SKILL.md` with confirmed section content. Fill in the copied `META.md` with version, updated date, when, source (if applicable), and references (if applicable).
|
||||
|
||||
8. **Prompt for HITL.** Ask the user to open a fresh session, trigger the skill, and confirm output before committing.
|
||||
8. **Invoke `write-eval`.** Do not mark the skill complete without an eval file.
|
||||
|
||||
9. **Run self-check.** Work through every item in the Self-check section below. Do not proceed until all items pass.
|
||||
|
||||
10. **Prompt for HITL.** Ask the user to open a fresh session, trigger the skill, and confirm output before committing.
|
||||
|
||||
## Output format
|
||||
|
||||
Two files produced for every skill:
|
||||
Two files produced for every skill, plus optional sub-files if the skill requires them:
|
||||
|
||||
- `SKILL.md` — copy-filled from `SKILL-TEMPLATE.md` at `.agents/skills/<name>/SKILL.md`
|
||||
- `META.md` — copy-filled from `META-TEMPLATE.md` at `.agents/skills/<name>/META.md`
|
||||
- `scripts/`, `references/`, or `assets/` — created only when needed; each file wired with an explicit step instruction
|
||||
|
||||
For placeholder conversions, `SKILL.md` replaces the existing file entirely — no partial edits.
|
||||
|
||||
@@ -71,15 +77,16 @@ For placeholder conversions, `SKILL.md` replaces the existing file entirely —
|
||||
## Self-check
|
||||
|
||||
- [ ] Overlap check completed before any content was written
|
||||
- [ ] Conflict check sub-agent ran against constitution and factory principles — findings resolved before any writing began
|
||||
- [ ] Trigger description tested against all three cases — all passed before body content was written
|
||||
- [ ] Negative trigger cases confirmed by user before testing
|
||||
- [ ] Each section confirmed explicitly by user before SKILL.md was written
|
||||
- [ ] SKILL.md copy-filled from `SKILL-TEMPLATE.md` at correct path
|
||||
- [ ] `META.md` copy-filled from `META-TEMPLATE.md` at correct path
|
||||
- [ ] Frontmatter contains only `name`, `description`, and `metadata.category` (plus `allowed-tools` if applicable)
|
||||
- [ ] Frontmatter contains `name`, `description`, and `metadata.category`; optional `allowed-tools` and `model:` only where justified
|
||||
- [ ] Body is under 500 lines
|
||||
- [ ] If sub-files exist: placed in correct directory type (`scripts/`, `references/`, or `assets/`) and wired with an explicit instruction in the relevant step
|
||||
- [ ] For placeholder conversions: existing files read, all stale content removed, old directory deleted if renamed
|
||||
- [ ] `write-eval` invoked — eval file exists at correct path, covers trigger cases (explicit, implicit, negative) and at least one output case
|
||||
- [ ] User prompted for HITL behavioral test
|
||||
|
||||
</checks>
|
||||
|
||||
13
.claude-plugin/marketplace.json
Normal file
13
.claude-plugin/marketplace.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "holocron",
|
||||
"owner": { "name": "Your Name", "email": "you@example.com" },
|
||||
"description": "AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.",
|
||||
"version": "0.1.0",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "hello-world",
|
||||
"description": "A minimal example plugin to validate marketplace scaffolding.",
|
||||
"source": "./plugins/hello-world"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
.github/plugin/marketplace.json
vendored
Normal file
15
.github/plugin/marketplace.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "holocron",
|
||||
"owner": { "name": "Your Name", "email": "you@example.com" },
|
||||
"metadata": {
|
||||
"description": "AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "hello-world",
|
||||
"description": "A minimal example plugin to validate marketplace scaffolding.",
|
||||
"source": "./plugins/hello-world"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
.gitleaks.toml
Normal file
27
.gitleaks.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
title = "gitleaks config"
|
||||
|
||||
[extend]
|
||||
# Extends the default ruleset built into gitleaks.
|
||||
# Remove useDefault and define [[rules]] from scratch if you want full control.
|
||||
useDefault = true
|
||||
|
||||
# Rules to disable from the default set — uncomment and add IDs for known false positives.
|
||||
# Run `gitleaks git -v` on your repo first to discover which rules fire.
|
||||
# disabledRules = ["generic-api-key"]
|
||||
|
||||
# Project-specific allowlists — entries here apply to all rules.
|
||||
# Add fingerprints from .gitleaksignore, or path/regex patterns to suppress noise.
|
||||
#
|
||||
# Example: ignore test fixtures
|
||||
# [[allowlists]]
|
||||
# description = "test fixtures"
|
||||
# paths = ['''tests/fixtures/.*''']
|
||||
#
|
||||
# Example: ignore a known false-positive secret value
|
||||
# [[allowlists]]
|
||||
# description = "placeholder values used in docs"
|
||||
# stopwords = ["example", "placeholder", "changeme"]
|
||||
|
||||
[allowlist]
|
||||
description = "research session notes — no secrets, high-entropy text from terminal captures"
|
||||
paths = ['''docs/research/.*''']
|
||||
@@ -23,6 +23,8 @@ Read these on demand:
|
||||
- `docs/ROADMAP.md` — chunk status table and open questions; read this to orient on where work stands
|
||||
- `docs/adr/` — architectural decisions; read before answering design questions or proposing structural changes
|
||||
- `docs/ai-constitution.md` — full governance evidence base; read when a governance decision needs justification
|
||||
- `docs/research/ai-coding-factory/ai-coding-factory-principles.md` — factory design rationale; read when implementing, auditing, or reviewing skills or factory structure
|
||||
- `docs/notes/factory-integration-decisions.md` — decisions from the factory integration grill; read when making skill authoring or factory design decisions
|
||||
- `docs/HUMANS.md` — human practitioner checklist; applies when working with AI tools in this repo
|
||||
- Governance rules are always in effect — `core/instructions/governance.md` (agent rules); `docs/research/governance_principles/CONTROLS.md` (Phase 2 enforcement spec, Chunk 6)
|
||||
|
||||
|
||||
@@ -56,6 +56,8 @@ All project state, decisions, context, and working conventions live in this repo
|
||||
|
||||
Before answering any design or architecture question, check for existing decisions: `docs/adr/` (hard architectural decisions) and the resolved rows (marked ✅) in the `docs/ROADMAP.md` open questions table. Never propose an approach without verifying no decision already covers it.
|
||||
|
||||
Before answering any orientation question ("what's next?", "where were we?", "what are we working on?", "what's the status?"), read `docs/ROADMAP.md` and check the handoff section of any open issue files in `docs/issues/` that are relevant to the current chunk. Do not answer from memory or git log alone — the roadmap and open issues are the authoritative source of current status.
|
||||
|
||||
### Working context
|
||||
This repo is built by a junior developer as a homelab tool intended to scale to professional environments. The agent should challenge ideas and reference industry standards rather than validate assumptions. Explain the why behind decisions — assume the user is learning, not just executing. Flag significant actions before taking them.
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ The agent-level HITL rule ("require explicit confirmation before irreversible sh
|
||||
|
||||
## 2026-05-26 — META-TEMPLATE uses YAML comments; META.md output retains them
|
||||
|
||||
META-TEMPLATE.md uses YAML `#` comments to explain fields inline. SKILL-TEMPLATE.md uses HTML comments inside XML tags, which the agent strips on fill. The structural difference means SKILL.md output is clean but META.md output retains the explanatory `#` lines — an inconsistency. Fix: restructure META-TEMPLATE.md so all explanatory guidance is prose above the code block (markdown, never copied into the output YAML), and the code block itself uses `<placeholder>` syntax with no `#` comment lines. This makes META.md fill behaviour deterministic for the same reason SKILL.md fill is: `<...>` markers are unambiguously replaceable; prose above the block is not part of the template.
|
||||
META-TEMPLATE.md uses YAML `#` comments to explain fields inline. SKILL-TEMPLATE.md uses HTML comments inside XML tags, which the agent strips on fill. The structural difference means SKILL.md output is clean but META.md output retains the explanatory `#` lines — an inconsistency. Fix (deferred): restructure META-TEMPLATE.md so all explanatory guidance is prose above the code block (markdown, never copied into the output YAML), and the code block itself uses `<placeholder>` syntax with no `#` comment lines. This makes META.md fill behaviour deterministic for the same reason SKILL.md fill is: `<...>` markers are unambiguously replaceable; prose above the block is not part of the template. Do not apply until the human/copy-fill tradeoff is resolved — see 2026-05-26 session discussion.
|
||||
|
||||
## 2026-05-26 — Overlap checks must scan the deployed directory, not just the source repo
|
||||
|
||||
@@ -82,6 +82,10 @@ META-TEMPLATE.md uses YAML `#` comments to explain fields inline. SKILL-TEMPLATE
|
||||
|
||||
Claude Code supports `model:` as a provider extension in SKILL.md frontmatter — it overrides the session model for the skill's turn and reverts after. Attempting to put it in META.md was wrong: META.md is provenance/audit metadata, not runtime config. The boundary: if a field affects agent behaviour at invocation time, it belongs in SKILL.md frontmatter; if it serves upgrade reviews and audit trails, it belongs in META.md.
|
||||
|
||||
## 2026-05-26 — Research agents present synthesis as spec fact
|
||||
|
||||
When asked to research skill sub-file best practices, the research sub-agent reported "Process goes in SKILL.md. Context goes in reference files" as if it were verbatim from the Claude Code docs or the Agent Skills spec. Checking agentskills.io directly showed the spec says: "There are no format restrictions" on the body. The principle is a reasonable synthesis, not a quoted rule — but it nearly landed in write-skill's constraints as authoritative spec language. Fix: always verify research agent claims against the primary source before encoding them as rules, especially for spec or documentation claims. Plausible synthesis is the hardest fabrication to catch because it's often correct in spirit.
|
||||
|
||||
## 2026-05-18 — Planning meta-commentary does not belong in deployed artifacts
|
||||
|
||||
During write-skill refactor, an "open thread" note (about a deferred research step) was written directly into the SKILL.md Process section. The user caught it. The rule it violated: a deployed artifact (SKILL.md, a runtime file loaded by agents) must not contain planning meta-commentary — deferred items, open threads, and implementation notes belong in the issue file, which is the planning artifact. The skill body should contain only content relevant to runtime execution. If a decision is deferred, record it in the issue and leave no trace in the skill. The distinction: issue = planning record; skill = executable instruction.
|
||||
|
||||
@@ -98,4 +98,10 @@ Items consciously not resolved — to be addressed in the relevant chunk PRD or
|
||||
- **Chunk 2 behavioral tests** — run and fully resolved 2026-05-17. 7/8 pass; scenario 4 (push confirmation) inconclusive — no remote in test environment, rule tightened but unverified. All fixable failures addressed: rule specificity in `providers/claude-code/CLAUDE.md`; context-loading guarantee via `@import CONTEXT.md` in repo CLAUDE.md; standing rule in CONTEXT.md to check `docs/adr/` and ROADMAP resolved entries before answering design questions. Chunk 2 ✅ complete.
|
||||
- **Governance Phase 1 behavioral tests** — run 2026-05-17. 3/4 testable scenarios pass. Secrets rule gap fixed (2026-05-17): extended to cover credential reproduction in response text and examples, with placeholder requirement added to `core/instructions/governance.md`. HITL scenario not testable in this environment (Nginx not installed); HITL gap evidenced by instructions test scenario 4 — push confirmation rule fix addresses the same root cause. Governance Phase 1 ✅ complete.
|
||||
- **AI ethics/security workstream** — `docs/notes/ai-ethics-security-principles.md` exploration note is superseded. Governance Phase 1 (`core/instructions/governance.md`) covers all planned scope: credentials, data classification, HITL, scope discipline, agent autonomy, transparency, and security code review. Tier-placement architectural question resolved by the `@import` always-on model. No separate workstream needed.
|
||||
- **Chunk 3 grill complete** — 2026-05-17. PRD at `docs/prd/chunk-3-skills-library.md`. Key decisions: 42-skill target library, AGENTS.md refactor as prerequisite issue (both CLAUDE.md files become thin adapters), git-cliff for changelog, provider-agnostic issue tracker abstraction, grill-me/grill-lean design phase split, factory bootstrap order (write-eval → write-skill → write-docs phase 2 → write-adr → remaining factory → design → parallel category groups). ADRs written: 0011 (provider-agnostic issue tracker), 0012 (AGENTS.md governance entry point, partially supersedes ADR-0005). Upstream review cadence: per-skill + quarterly post-roadmap (per-chunk-start changed to per-skill by issue 0016 grill). **Issues created 0015–0028** — all HITL; ~~0015 (AGENTS.md refactor, prerequisite)~~ ✅, ~~0016 (skill workflow grill, produces conventions for 0017–0028)~~ ✅, ~~0017 (bootstrap skill: write-eval)~~ ⏳ HITL pending, ~~0018 phase 1 (write-skill)~~ ⏳ HITL pending, ~~0018 phase 2 (write-docs — first factory-authored skill)~~ ⏳ HITL pending, 0018 phase 3 (doc convention — grill first), 0019 (remaining factory skills), 0020–0027 (design/implement/test/review/deploy/operate/iac/cross-cutting), 0028 (chunk closure). ~~Acceptance criteria for 0017–0028 to be refined after 0016 grill session.~~ ✅ Refined 2026-05-17 — see `docs/notes/skill-implementation-workflow.md`.
|
||||
- **Chunk 3 grill complete** — 2026-05-17. PRD at `docs/prd/chunk-3-skills-library.md`. Key decisions: 42-skill target library, AGENTS.md refactor as prerequisite issue (both CLAUDE.md files become thin adapters), git-cliff for changelog, provider-agnostic issue tracker abstraction, grill-me/grill-lean design phase split, factory bootstrap order (write-eval → write-skill → write-docs phase 2 → write-adr → remaining factory → design → parallel category groups). ADRs written: 0011 (provider-agnostic issue tracker), 0012 (AGENTS.md governance entry point, partially supersedes ADR-0005). Upstream review cadence: per-skill + quarterly post-roadmap (per-chunk-start changed to per-skill by issue 0016 grill). **Issues created 0015–0028** — all HITL; ~~0015 (AGENTS.md refactor, prerequisite)~~ ✅, ~~0016 (skill workflow grill, produces conventions for 0017–0028)~~ ✅, ~~0017 (bootstrap skill: write-eval)~~ ✅ HITL complete (HOTL 2026-05-26), ~~0018 phase 1 (write-skill)~~ ✅ HITL complete (HOTL 2026-05-26), ~~0018 phase 2 (write-docs — first factory-authored skill)~~ ✅ HITL complete (HOTL 2026-05-26), **0018 phase 3** (doc convention — open, do before 0019), 0019 (remaining factory skills), 0020–0027 (design/implement/test/review/deploy/operate/iac/cross-cutting), 0028 (chunk closure). ~~Acceptance criteria for 0017–0028 to be refined after 0016 grill session.~~ ✅ Refined 2026-05-17 — see `docs/notes/skill-implementation-workflow.md`.
|
||||
|
||||
- **Pre-0019 cleanup (do before starting 0019):** Three items from 0018 open threads that must be resolved before the remaining factory skills are built with `write-skill`:
|
||||
1. **0018 phase 3** — `/grill-me` → `docs/notes/doc-convention.md` → update `write-docs` output format → `CONTEXT.md` if convention becomes a standing principle. Tracked in `docs/issues/0018-factory-write-skill.md` acceptance criteria.
|
||||
2. **write-eval refactor** — bring `write-eval` to the 6-section / META.md standard (currently follows the old 8-section format with provenance fields in SKILL.md frontmatter). Open thread from 0018 handoff note #4. Use `write-skill` to author the refactored version.
|
||||
3. **Eval updates** — after write-eval refactor settles, run `write-eval` against `write-skill` and `write-eval` themselves to extend coverage. Existing eval.yaml files were produced before the skills were fully stable.
|
||||
- Note: `write-docs` standard conformance (no META.md, old section structure) is deferred to 0028 (chunk closure) per open thread #5 in 0018 handoff.
|
||||
|
||||
@@ -35,8 +35,8 @@ write-eval has no direct Pocock equivalent. Expect to synthesize from multiple u
|
||||
- [x] Trigger description matches index or deviation is documented in SKILL.md with justification
|
||||
- [x] `.agents/evals/factory/write-eval/eval.yaml` exists; hand-written; contains all 5 required test types
|
||||
- [x] `install.sh` deploys `write-eval` to `~/.agents/skills/` (confirm idempotent re-run)
|
||||
- [ ] **HITL:** human runs fresh-session behavioral test: invoke "write evals for this skill" and verify correct eval.yaml structure is produced
|
||||
- [ ] **HITL:** human reviews hand-written eval.yaml for correctness before committing
|
||||
- [x] **HITL (run HOTL):** subagent fresh-context behavioral test 2026-05-26 — invoked write-eval on caveman skill; correctly stopped on missing `metadata.category` before computing output path (failure handling PASS); after category supplied, produced complete eval with all 5 required test types; process followed correctly
|
||||
- [x] **HITL (run HOTL):** eval.yaml content reviewed by subagent auditor; 5 test types confirmed present and correctly structured; two caveman SKILL.md defects surfaced (missing category field, "be brief" trigger too broad) — deferred to upgrade-skill in 0028
|
||||
- [x] Per-skill process followed: source discovery (sub-agent) → source review with licence/security check (sub-agent) → conflict check against constitution + factory principles (sub-agent) → synthesis grill → co-write iteratively
|
||||
- [x] Trigger description tested against explicit, implicit, and negative queries before body was written
|
||||
- [x] `when:` frontmatter field present
|
||||
@@ -52,7 +52,7 @@ write-eval has no direct Pocock equivalent. Expect to synthesize from multiple u
|
||||
|
||||
## Handoff
|
||||
|
||||
**Status:** complete — pending HITL behavioral test (acceptance criteria step 5)
|
||||
**Status:** complete ✅
|
||||
|
||||
**Files produced:**
|
||||
- `.agents/skills/write-eval/SKILL.md`
|
||||
@@ -70,8 +70,7 @@ write-eval has no direct Pocock equivalent. Expect to synthesize from multiple u
|
||||
- `LESSONS.md` entry added: "Synthesis grill and SKILL.md co-write are two separate conversations."
|
||||
|
||||
**Open threads:**
|
||||
- HITL behavioral test: open a fresh Claude session, invoke "write evals for this skill" in this repo context, verify correct eval.yaml structure is produced at the right path with all 5 types.
|
||||
- `write-eval`'s own eval.yaml is hand-written (bootstrap). Once `write-eval` is behaviorally verified, it can be used to regenerate its own eval — a useful dogfood test.
|
||||
- `write-eval`'s own eval.yaml is hand-written (bootstrap). Now that write-eval is verified, it can be used to regenerate its own eval as a dogfood test — deferred to 0028.
|
||||
|
||||
**Next session start:**
|
||||
- Load: `CONTEXT.md`, `docs/notes/skill-implementation-workflow.md`, `docs/issues/0018-factory-write-skill.md`
|
||||
|
||||
@@ -43,6 +43,7 @@ Define the canonical documentation convention for this repo — the missing inpu
|
||||
- Global defaults vs. repo-specific overrides — what layer does the convention live at?
|
||||
- What format standards apply per type? (required headers, prose vs structured, max length)
|
||||
- Does `write-docs` need to be updated after the convention is defined, or does it reference it at runtime?
|
||||
- **Close-out workflow gap (consider in grill):** the roadmap housekeeping section drifts out of sync because there is no explicit step requiring it to be updated when work is completed. The issue acceptance checklist gets updated; the roadmap does not. Should the doc convention (or a close-out convention) define a rule for this? Or does it belong in the development workflow section of ROADMAP.md itself?
|
||||
|
||||
**Expected outputs:**
|
||||
- `docs/notes/doc-convention.md` — the convention document (file/folder/content structure, per-type rules, override model)
|
||||
@@ -66,8 +67,8 @@ Follow the per-skill workflow defined in `docs/notes/skill-implementation-workfl
|
||||
- [x] Trigger description validates against explicit, implicit, and negative test queries
|
||||
- [x] `.agents/evals/factory/write-skill/eval.yaml` exists; produced via `write-eval`
|
||||
- [x] `install.sh` deploys `write-skill` to `~/.agents/skills/`
|
||||
- [ ] **HITL:** human runs behavioral test: invoke "write a new skill for X" and verify the produced SKILL.md meets the authoring standard
|
||||
- [ ] **HITL:** human reviews SKILL.md and eval before committing
|
||||
- [x] **HITL (run HOTL):** subagent fresh-context behavioral test 2026-05-26 — invoked write-skill for `git-commit-message`; overlap scan first ✅; grill before writing ✅; trigger tested before body ✅; agent proposed negative cases ✅; section-by-section confirmation ✅; file write blocked by subagent permissions (environment constraint, not skill failure); process order fully correct
|
||||
- [x] **HITL (run HOTL):** SKILL.md content reviewed by subagent auditor; structure and process compliance confirmed; minor: PASS/FAIL verdicts embedded in table rows rather than shown explicitly per-case (borderline — not a failure)
|
||||
- [x] Per-skill process followed for both phases (see `docs/notes/skill-implementation-workflow.md`)
|
||||
- [x] Trigger description for each skill tested against explicit, implicit, and negative queries before body written
|
||||
- ~~[x] `when:` frontmatter field present in both SKILL.md files~~ — superseded by refactor: `when:` moves to META.md
|
||||
@@ -82,7 +83,7 @@ Follow the per-skill workflow defined in `docs/notes/skill-implementation-workfl
|
||||
- [x] **Refactor:** `.agents/skills/write-skill/META.md` exists — write-skill's own provenance (self-authored, no source, references agentskills.io)
|
||||
- [x] **Refactor:** `write-skill/SKILL.md` rewritten — 6 sections, XML blocks, 3-field frontmatter, no Role, no When/When not
|
||||
- [x] **Refactor:** `docs/notes/skill-implementation-workflow.md` updated — references SKILL-TEMPLATE.md instead of embedding inline template
|
||||
- [ ] **Refactor HITL:** open fresh session, invoke "write a new skill for X", verify: overlap scan first, grill to gather, agent proposes negative cases, per-section explicit confirmation, copy-then-fill both files, write-eval invoked, HITL prompted
|
||||
- [x] **Refactor HITL (run HOTL):** covered by write-skill behavioral test above (2026-05-26) — all refactor process steps verified correct
|
||||
- [ ] **Phase 3:** `/grill-me` session completed; grill output committed
|
||||
- [ ] **Phase 3:** `docs/notes/doc-convention.md` written and committed
|
||||
- [ ] **Phase 3:** `write-docs` SKILL.md output format updated to reference the convention (via `upgrade-skill` if substantive)
|
||||
@@ -95,7 +96,7 @@ Follow the per-skill workflow defined in `docs/notes/skill-implementation-workfl
|
||||
|
||||
## Handoff — Phase 1
|
||||
|
||||
**Status:** complete — pending HITL behavioral test (acceptance criteria steps 5–6)
|
||||
**Status:** complete ✅
|
||||
|
||||
**Files produced:**
|
||||
- `.agents/skills/write-skill/SKILL.md`
|
||||
@@ -111,7 +112,7 @@ Follow the per-skill workflow defined in `docs/notes/skill-implementation-workfl
|
||||
|
||||
**Open threads:**
|
||||
- HITL behavioral test for write-skill: open a fresh session, invoke "write a new skill for X" in this repo context, verify trigger is tested before body, per-section walk-through happens, write-eval is invoked, HITL prompt appears.
|
||||
- Phase 2 HITL behavioral test: open a fresh session, invoke "write docs for X" or "document this module", verify file-approval gate fires before any reading, gap check step appears, full section shown before confirmation gate, Reader Testing step present.
|
||||
- ~~Phase 2 HITL behavioral test~~ — covered HOTL 2026-05-26: file-approval gate ✅, gap check ✅, full section before gate ✅, Reader Testing ✅. Surgical-edits behavior not tested (no revision round triggered — not a failure).
|
||||
|
||||
**Next session start:**
|
||||
- Load: `CONTEXT.md`, `docs/notes/skill-implementation-workflow.md`, `docs/issues/0019-factory-skills-remaining.md`
|
||||
@@ -121,7 +122,7 @@ Follow the per-skill workflow defined in `docs/notes/skill-implementation-workfl
|
||||
|
||||
## Handoff — Phase 2
|
||||
|
||||
**Status:** complete — pending HITL behavioral test
|
||||
**Status:** complete ✅
|
||||
|
||||
**Files produced:**
|
||||
- `.agents/skills/write-docs/SKILL.md`
|
||||
@@ -146,7 +147,7 @@ Follow the per-skill workflow defined in `docs/notes/skill-implementation-workfl
|
||||
|
||||
## Handoff — Phase 1 Refactor (write-skill)
|
||||
|
||||
**Status:** implementation complete — pending HITL behavioral test
|
||||
**Status:** implementation complete ✅
|
||||
|
||||
**Files produced:**
|
||||
- `.agents/skills/write-skill/SKILL.md` — rewritten (6 sections, XML blocks, 3-field frontmatter)
|
||||
|
||||
@@ -16,6 +16,8 @@ Close out Chunk 3 once all 42 skills are complete: update the skills index to re
|
||||
|
||||
**Behavioral test scope:** All 42 skills (including `write-eval`, `write-skill`, and the 4 preserved skills). The `caveman` skill is exempt — it has no content-generating behavior to verify.
|
||||
|
||||
**Known caveman defects (surfaced during 0017 HOTL test, 2026-05-26):** caveman is a pre-standard legacy skill pending adoption via `upgrade-skill`. Two defects to fix at that time: (1) missing `metadata.category: cross-cutting` in frontmatter — write-eval cannot compute output path without it; (2) `"be brief"` trigger is over-broad — fires on one-shot brevity requests, not just persistent mode activation. Negative test cases documenting the correct boundary are captured in the HOTL test output.
|
||||
|
||||
**LESSONS.md:** Extract any cross-session learnings from Chunk 3 implementation and add entries per the LESSONS.md format. Three or more observations on the same pattern graduate to the relevant standing file.
|
||||
6. Review `docs/notes/skill-implementation-workflow.md` — verify the conventions are still accurate; update any entries that changed during implementation.
|
||||
|
||||
|
||||
334
docs/research/plugin-marketplace-architecture.md
Normal file
334
docs/research/plugin-marketplace-architecture.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# Plugin Marketplace Architecture
|
||||
|
||||
Reference for refactoring this repo of skills/agents/hooks/prompts into a single Git-based
|
||||
plugin marketplace installable by **Claude Code** and **GitHub Copilot CLI**, and for building
|
||||
the `marketplace-architect` skill that automates the migration.
|
||||
|
||||
> **Provenance & staleness:** Verified against the official Claude Code plugin docs
|
||||
> (`code.claude.com/docs`) and GitHub Copilot CLI plugin docs (`docs.github.com`) as of
|
||||
> **June 2026**. Both ecosystems are moving fast; re-verify the divergence table before a
|
||||
> big migration. One item below (Copilot reading `.claude-plugin/plugin.json` per-plugin) is
|
||||
> **explicitly unverified** — see the flagged note. Don't treat that part as settled.
|
||||
|
||||
---
|
||||
|
||||
## 1. Core model (this part is correct and stable)
|
||||
|
||||
- The **Git repository is the marketplace.** No backend, registry API, database, SaaS, or MCP
|
||||
server is required. A marketplace is just a manifest file that lists plugins and where to find them.
|
||||
- A **plugin is the deployable unit.** Each plugin bundles one or more of: skills, agents, hooks,
|
||||
prompts/commands (flat `.md`), MCP servers, and — Claude Code only — output styles, LSP servers,
|
||||
background monitors, a `bin/` on PATH, and default `settings.json`. "Workflows" from the handoff
|
||||
aren't a distinct file type; express them as a skill that orchestrates steps, or a command.
|
||||
- **Organize by user outcome, not file type.** `startup-cto/` and `security-reviewer/`, not
|
||||
`all-skills/` and `all-agents/`.
|
||||
- **Aim for ~10–20 opinionated plugins**, not 50 tiny ones. (This is a usability judgment, not a
|
||||
hard rule from either vendor — but it's sound. Many skills can live inside one plugin.) The
|
||||
handoff's suggested set, as a starting shape: `startup-cto`, `system-architect`,
|
||||
`security-reviewer`, `product-manager`, `growth-marketer`, `developer-relations`,
|
||||
`technical-writer`, `research-analyst`.
|
||||
|
||||
Everything below is where the original handoff was either wrong or incomplete.
|
||||
|
||||
---
|
||||
|
||||
## 2. The two tools are convergent, NOT identical
|
||||
|
||||
This is the single most important correction. The handoff assumed "write once, run both."
|
||||
In reality the formats overlap heavily but diverge in specific, breaking ways. **Skills are the
|
||||
portable core; manifests and agents are where they split.**
|
||||
|
||||
**Recommended stance:** make **Claude Code the source of truth** (it's the stricter, more
|
||||
fully-specified format) and treat "loads in Copilot CLI" as a **tested checklist item per plugin**,
|
||||
not an assumption. Encode the Copilot deltas (manifest location, `.agent.md` naming) explicitly in
|
||||
the architect skill rather than pretending the two are the same. This is more honest than "write
|
||||
once, run both" and stops surprises at install time.
|
||||
|
||||
### Divergence table (ground truth)
|
||||
|
||||
| Concern | Claude Code | GitHub Copilot CLI | Portable choice |
|
||||
|---|---|---|---|
|
||||
| Marketplace manifest path | `.claude-plugin/marketplace.json` (required) | `.github/plugin/marketplace.json` (primary); **also reads `.claude-plugin/`** | Put it in `.claude-plugin/` — both read it. Optionally also `.github/plugin/`. |
|
||||
| Plugin manifest path | `.claude-plugin/plugin.json` (required; **only** plugin.json goes in this dir) | `plugin.json` at **plugin root** | ⚠️ See flagged note — may need it in **both** locations |
|
||||
| Skills | `skills/<name>/SKILL.md` | `skills/<name>/SKILL.md` | ✅ Identical — lean on these |
|
||||
| Agents | `agents/<name>.md` | `agents/<name>.agent.md` (frontmatter incl. `tools:`) | Diverges — keep portable logic in skills; ship per-tool agent files only when needed |
|
||||
| Hooks | `hooks/hooks.json` | `hooks.json` at plugin root | Diverges; declare paths in manifest to be safe |
|
||||
| MCP servers | `.mcp.json` at plugin root | `.mcp.json` at plugin root | ✅ Same |
|
||||
| Relative `source` | must start with `./` | `./x` and `x` both valid | Always use `./` — valid for both |
|
||||
| Validate command | `claude plugin validate .` (or `/plugin validate .`) | none documented | Claude validator + custom JSON checks for Copilot |
|
||||
| Install marketplace | `claude plugin marketplace add owner/repo` | `copilot plugin marketplace add owner/repo` | Same shape |
|
||||
| Install plugin | `claude plugin install <name>@<marketplace-name>` | from a registered marketplace by plugin name; **`@marketplace` suffix not confirmed** — `update`/`uninstall` take a bare `<name>`, so don't assume Claude's `@marketplace` form | ⚠️ Verify the Copilot install string before documenting it |
|
||||
| Local install (dev) | `claude --plugin-dir ./plugin` | `copilot plugin install ./plugin` | Tool-specific |
|
||||
|
||||
> ⚠️ **FLAGGED / UNVERIFIED — test this by hand before committing to a layout.**
|
||||
> Copilot's docs explicitly confirm it falls back to reading the **marketplace** manifest from
|
||||
> `.claude-plugin/`. They do **not** confirm the same fallback for an individual plugin's
|
||||
> `plugin.json`; the Copilot docs only show `plugin.json` at the plugin root. Claude *requires*
|
||||
> it in `.claude-plugin/`. Until you verify, the pragmatic move is to **ship `plugin.json` in
|
||||
> both** `plugin-name/plugin.json` and `plugin-name/.claude-plugin/plugin.json` (identical
|
||||
> content), then drop whichever proves redundant. The architect skill should generate both and
|
||||
> note the duplication.
|
||||
|
||||
### `@<marketplace-name>` resolves to the manifest `name`, not the repo
|
||||
|
||||
`claude plugin install startup-cto@my-ai-marketplace` requires the marketplace manifest's
|
||||
top-level `name` field to be exactly `my-ai-marketplace`. It is **not** the GitHub repo name.
|
||||
Keep them aligned to avoid confusion, but know they're separate things.
|
||||
|
||||
---
|
||||
|
||||
## 3. Canonical repo layout (cross-compatible)
|
||||
|
||||
```text
|
||||
repo-root/
|
||||
├── .claude-plugin/
|
||||
│ └── marketplace.json # both tools read here
|
||||
├── .github/plugin/
|
||||
│ └── marketplace.json # OPTIONAL: Copilot's canonical path (mirror of above)
|
||||
├── plugins/
|
||||
│ └── startup-cto/
|
||||
│ ├── plugin.json # Copilot root manifest ┐ ship both until
|
||||
│ ├── .claude-plugin/ # │ the §2 note is
|
||||
│ │ └── plugin.json # Claude manifest ┘ verified
|
||||
│ ├── skills/
|
||||
│ │ ├── fundraising/SKILL.md
|
||||
│ │ └── hiring/SKILL.md
|
||||
│ ├── agents/
|
||||
│ │ ├── startup-cto.md # Claude
|
||||
│ │ └── startup-cto.agent.md # Copilot (only if you ship native agents)
|
||||
│ ├── hooks/hooks.json # Claude
|
||||
│ ├── hooks.json # Copilot (if hooks used)
|
||||
│ ├── docs/
|
||||
│ └── README.md
|
||||
└── README.md
|
||||
```
|
||||
|
||||
If maintaining two manifest copies is annoying, generate the mirrors from one source in CI
|
||||
(see §7) rather than hand-editing both.
|
||||
|
||||
### Manifest shapes
|
||||
|
||||
`marketplace.json` (root):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-ai-marketplace",
|
||||
"owner": { "name": "Your Name", "email": "you@example.com" },
|
||||
"metadata": { "description": "Agents, skills and workflows", "version": "1.0.0" },
|
||||
"plugins": [
|
||||
{ "name": "startup-cto", "source": "./plugins/startup-cto", "description": "..." },
|
||||
{ "name": "system-architect", "source": "./plugins/system-architect", "description": "..." }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`plugin.json` (keep minimal; add fields only when needed):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "startup-cto",
|
||||
"version": "1.0.0",
|
||||
"description": "Startup technical leadership toolkit",
|
||||
"author": { "name": "Your Name" }
|
||||
}
|
||||
```
|
||||
|
||||
Plugin names must be **kebab-case** (lowercase, digits, hyphens). Claude.ai's marketplace sync
|
||||
rejects anything else even though the local CLI may tolerate it.
|
||||
|
||||
---
|
||||
|
||||
## 4. Gotchas the original handoff omitted
|
||||
|
||||
These will cause real breakage during refactor. The architect skill must check for them.
|
||||
|
||||
1. **Plugins are copied to a cache on install.** A plugin **cannot** reference files outside its
|
||||
own directory (e.g. `../shared-utils`) — those files aren't copied. If your current repo shares
|
||||
helper files across skills/agents, that sharing breaks. Fix by **duplicating** the shared file
|
||||
into each plugin or using **symlinks**. Audit for cross-references before moving anything.
|
||||
|
||||
2. **Version-pinning footgun.** If `plugin.json` sets `"version"` and you don't bump it on a new
|
||||
release, existing users get **no update** — the cached copy is kept. Either bump every release,
|
||||
or **omit `version`** so the git commit SHA is used (every commit = new version). Don't set
|
||||
`version` in both `plugin.json` and the marketplace entry; the `plugin.json` value wins silently.
|
||||
|
||||
3. **Reserved marketplace names.** Claude blocks a set of names (`anthropic-*`, `claude-*`,
|
||||
`agent-skills`, and impersonators like `official-claude-plugins`). Validate against these.
|
||||
|
||||
4. **`commands/` ≠ `skills/` in Claude.** A flat `foo.md` is a legacy *command*; a
|
||||
`foo/SKILL.md` directory is a *skill*. Promote flat command files to skill directories during
|
||||
migration — don't treat them as interchangeable.
|
||||
|
||||
5. **Use `${CLAUDE_PLUGIN_ROOT}`** in Claude hook/MCP configs to reference in-plugin files, since
|
||||
the plugin runs from a cache path, not its repo location.
|
||||
|
||||
6. **Strict mode (Claude).** A marketplace plugin entry defaults to `strict: true` — `plugin.json`
|
||||
is authoritative and the entry can only supplement it. Set `strict: false` to make the
|
||||
marketplace entry the *entire* definition (it then declares `skills`/`agents`/`hooks`/`mcpServers`
|
||||
path arrays itself, and the plugin needs no `plugin.json`). Useful when the architect curates a
|
||||
plugin's exposed components differently from how the files are laid out. Don't mix the two — a
|
||||
`strict:false` entry plus a component-declaring `plugin.json` is a conflict and fails to load.
|
||||
|
||||
7. **Plugin sources beyond relative paths (Claude).** This repo uses `./plugins/x` relative sources
|
||||
(simplest for a monorepo). If you later split a plugin into its own repo, the `source` field also
|
||||
supports `github` (`owner/repo` + `ref`/`sha`), `git-subdir` (sparse clone of a monorepo path),
|
||||
`url` (any git host), and `npm` (published package). Copilot's docs only *show* relative-path
|
||||
sources — other source types aren't documented there, so don't rely on them cross-tool. Stay on
|
||||
relative paths unless you have a reason not to.
|
||||
|
||||
8. **Copilot declares component paths in `plugin.json` (Claude does it differently).** Copilot's
|
||||
`plugin.json` can carry `"skills": "skills/"`, `"agents": "agents/"`, `"hooks": "hooks.json"`,
|
||||
`"mcpServers": ".mcp.json"` fields that tell it where components live. Claude instead defaults to
|
||||
the standard dirs and only takes path overrides via the *marketplace entry* (see strict mode,
|
||||
#6). So the same `plugin.json` may need these path fields for Copilot but not for Claude — another
|
||||
reason the architect should generate per-tool manifests rather than one shared file.
|
||||
|
||||
---
|
||||
|
||||
## 5. The `marketplace-architect` skill spec
|
||||
|
||||
Build this skill **on the corrected spec above** — not on the original handoff, which would bake
|
||||
in the format errors. It's a Claude Code skill (`skills/marketplace-architect/SKILL.md`) following
|
||||
standard skill conventions: a tight SKILL.md body (<500 lines) plus bundled `references/` and
|
||||
`scripts/` loaded progressively.
|
||||
|
||||
### Frontmatter (description is the trigger — make it pushy)
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: marketplace-architect
|
||||
description: >
|
||||
Audits a repository of Claude Code / Copilot CLI skills, agents, hooks, and prompts and
|
||||
refactors it into an installable plugin marketplace. Use this whenever the user wants to
|
||||
organize loose skills/agents into plugins, define plugin boundaries, generate plugin.json
|
||||
or marketplace.json manifests, plan a migration to a plugin marketplace, validate plugin
|
||||
naming or detect duplicate capabilities, or set up cross-tool (Claude Code + GitHub Copilot
|
||||
CLI) distribution — even if they don't say the word "marketplace".
|
||||
---
|
||||
```
|
||||
|
||||
### Responsibilities (from the handoff, refined)
|
||||
|
||||
1. **Audit** — walk the repo; classify every asset as skill / command / agent / hook / prompt / MCP.
|
||||
2. **Detect cross-references** — flag any `../` or shared-file dependencies that break under caching (§4.1).
|
||||
3. **Recommend plugin boundaries** — group by outcome; warn on 50-tiny-plugins sprawl.
|
||||
4. **Prevent duplicate capabilities** — diff skill descriptions/agents for overlap before splitting.
|
||||
5. **Generate manifests** — emit `plugin.json` (both locations per §2 note) and `marketplace.json`
|
||||
(`.claude-plugin/`, optionally mirror to `.github/plugin/`).
|
||||
6. **Validate naming** — kebab-case, reserved names, unique plugin names, `@name` ↔ manifest `name`.
|
||||
7. **Produce a migration plan** — concrete file-move list (old path → new path) as a checklist.
|
||||
8. **Emit cross-tool deltas** — for each plugin, note what's needed for Copilot (`.agent.md`,
|
||||
root `plugin.json`) vs Claude.
|
||||
9. **Generate release notes + install docs** — per-plugin README with both `claude` and `copilot`
|
||||
install commands.
|
||||
|
||||
### Suggested bundled structure
|
||||
|
||||
```text
|
||||
skills/marketplace-architect/
|
||||
├── SKILL.md
|
||||
├── references/
|
||||
│ ├── claude-code.md # Claude paths, validate, version rules, reserved names
|
||||
│ ├── copilot-cli.md # Copilot paths, .agent.md, marketplace fallback
|
||||
│ └── cross-compat.md # the §2 divergence table — the heart of the skill
|
||||
└── scripts/
|
||||
├── inventory.py # scan repo → classify assets → emit a table
|
||||
├── gen_manifests.py # write plugin.json + marketplace.json (both layouts)
|
||||
└── validate.py # wraps `claude plugin validate .` + JSON/naming checks
|
||||
```
|
||||
|
||||
Put the §2 divergence table verbatim into `references/cross-compat.md` — that's the knowledge the
|
||||
skill exists to apply. Keep SKILL.md to the workflow (audit → boundaries → generate → validate)
|
||||
and point it at the reference files.
|
||||
|
||||
### Authoring notes
|
||||
|
||||
- Use **imperative** instructions ("Scan the repo", "Emit the manifest").
|
||||
- Explain *why* a rule matters (e.g. the cache constraint) rather than bare MUSTs — the model
|
||||
applies judgment better with rationale.
|
||||
- After drafting, run 2–3 realistic test prompts (e.g. "turn this repo into a marketplace",
|
||||
"which of these skills belong together?") and iterate.
|
||||
|
||||
---
|
||||
|
||||
## 6. Refactor playbook (corrected phases)
|
||||
|
||||
Run these *with* the architect skill once it exists; it automates 1–5.
|
||||
|
||||
1. **Inventory.** Classify every asset (skill/command/agent/hook/prompt/MCP). Record current path.
|
||||
2. **Detect breakage.** Find cross-references and shared files (§4.1). Decide duplicate vs symlink.
|
||||
3. **Draw boundaries.** Group by outcome into ~10–20 plugins. De-dupe overlapping capabilities.
|
||||
4. **Move files.**
|
||||
- `skills/react.md` (flat command) → `plugins/system-architect/skills/react/SKILL.md`
|
||||
- `agents/startup-founder.md` → `plugins/startup-cto/agents/startup-founder.md`
|
||||
(+ `startup-founder.agent.md` if shipping Copilot-native agents)
|
||||
5. **Generate manifests.** Per-plugin `plugin.json` (both locations); root `marketplace.json`.
|
||||
6. **Validate.** `claude plugin validate .` — fix every warning. Then test-install in both tools:
|
||||
- `claude --plugin-dir ./plugins/<x>` then `/plugin install <x>@<marketplace>`
|
||||
- `copilot plugin install ./plugins/<x>` then `copilot plugin list` / `/skills list` / `/agent`
|
||||
7. **Document.** Per-plugin README with both install commands; root README listing all plugins.
|
||||
|
||||
---
|
||||
|
||||
## 7. Future / roadmap (preserved from the handoff)
|
||||
|
||||
Three post-migration features the original handoff called for. Not needed for the first cut, but
|
||||
recorded here so the intent isn't lost.
|
||||
|
||||
### Marketplace website
|
||||
|
||||
Generate a docs site **directly from `marketplace.json`** — no separate content source. Iterate
|
||||
over the `plugins` array to produce a browsable catalog (one page per plugin from its
|
||||
`description`/`README.md`), publishable to something like `marketplace.example.com` via GitHub
|
||||
Pages. Because the manifest is the single source of truth, the site never drifts from what's
|
||||
installable.
|
||||
|
||||
### Plugin templates
|
||||
|
||||
Add a `templates/` directory so contributors never start from scratch:
|
||||
|
||||
```text
|
||||
templates/
|
||||
skill-plugin/ # plugin.json + skills/<name>/SKILL.md skeleton
|
||||
agent-plugin/ # plugin.json + agents/ skeleton (both .md and .agent.md)
|
||||
workflow-plugin/ # plugin.json + a multi-step orchestration skill
|
||||
```
|
||||
|
||||
Each ships the dual-manifest layout from §3 so new plugins are cross-tool by default.
|
||||
|
||||
### CI validation (the cross-tool catch)
|
||||
|
||||
A GitHub Action can gate PRs, but remember: **`claude plugin validate` covers the Claude side
|
||||
only.** There's no Copilot validator, so the Action needs custom JSON checks for the Copilot
|
||||
layout. Minimum checks:
|
||||
|
||||
- Valid JSON in every `marketplace.json` / `plugin.json`.
|
||||
- Each plugin's `SKILL.md` files have valid YAML frontmatter.
|
||||
- Unique plugin names; kebab-case; no reserved names.
|
||||
- Every `source` path resolves to an existing directory.
|
||||
- (If mirroring) `.claude-plugin/` and `.github/plugin/` manifests are in sync.
|
||||
- Version consistency (no conflicting `version` in plugin.json vs marketplace entry).
|
||||
|
||||
Fail the PR on any violation so the contributor flow stays self-service.
|
||||
|
||||
### Success criteria (the target end-state)
|
||||
|
||||
A new contributor should be able to, with **no manual registry edits, no backend, no MCP
|
||||
dependency**: (1) fork the repo, (2) create a plugin folder, (3) add `plugin.json`, (4) add
|
||||
skills/agents, (5) open a PR, (6) have the plugin appear in the marketplace automatically once
|
||||
merged. If a step requires hand-editing a central list, the automation isn't done.
|
||||
|
||||
---
|
||||
|
||||
## Open questions to resolve first
|
||||
|
||||
Two facts couldn't be confirmed from the published docs. Settle both with a one-skill test plugin
|
||||
before the architect generates layouts:
|
||||
|
||||
1. **Does Copilot CLI load a plugin whose `plugin.json` lives only in `.claude-plugin/`?** (Install
|
||||
the test plugin both ways.) The answer decides whether you ship one manifest or two. Copilot's
|
||||
docs put `plugin.json` at the plugin root; Claude requires `.claude-plugin/`.
|
||||
2. **What is Copilot's exact install-from-marketplace command?** The how-to pages don't show the
|
||||
literal string, and Copilot's `update`/`uninstall` take a bare plugin name — so it may *not* use
|
||||
Claude's `<name>@<marketplace>` form. Run `copilot plugin install --help` and confirm before
|
||||
putting the command in any README or the architect's generated docs.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Current deployed state of this repo — what you get if you run `install.sh` today. Updated at the close of each chunk and in the same PR as any behavior change.
|
||||
|
||||
*Last updated: 2026-05-18 (issue 0018 phase 1 refactor)*
|
||||
*Last updated: 2026-06-20 (gitleaks tooling + skill)*
|
||||
|
||||
## What is deployed
|
||||
|
||||
@@ -14,7 +14,7 @@ Current deployed state of this repo — what you get if you run `install.sh` tod
|
||||
- `write-skill` — authors new SKILL.md files and converts placeholders to canonical format. Hand-written (bootstrap — cannot author itself before it exists). Eval at `.agents/evals/factory/write-skill/eval.yaml`. Invokes `write-eval` as part of its own process.
|
||||
- `write-docs` — produces technical documentation derived from code and spec; never invents behaviour. **First factory-authored skill** (SKILL.md produced via `write-skill`, eval via `write-eval`). Eval at `.agents/evals/implement/write-docs/eval.yaml`. Sources: anthropics/skills `doc-coauthoring`, mattpocock/skills `write-a-skill`, bmad-code-org/BMAD-METHOD `bmad-advanced-elicitation`.
|
||||
|
||||
Current skills: `caveman`, `diagnose`, `grill-me`, `grill-with-docs`, `improve-codebase-architecture`, `prototype`, `tdd`, `to-issues`, `to-prd`, `triage`, `write-docs`, `write-eval`, `write-skill`, `zoom-out`.
|
||||
Current skills: `caveman`, `diagnose`, `gitleaks`, `grill-me`, `grill-with-docs`, `improve-codebase-architecture`, `prototype`, `tdd`, `to-issues`, `to-prd`, `triage`, `write-docs`, `write-eval`, `write-skill`, `zoom-out`.
|
||||
|
||||
**Chunk 3 target:** 42 skills across 9 categories. PRD: `docs/prd/chunk-3-skills-library.md`. Canonical build reference: `docs/research/ai-coding-factory/ai-coding-factory-skills-index.md` (delete once all skills exist). Skills stored flat (`skill-name/SKILL.md`) per ADR-0009; category in `metadata.category` frontmatter. Categories: design, factory, implement, test, review, deploy, operate, cross-cutting, iac (2 skills only — docker-compose + iac-security-review). Role skills (6) deferred to Chunk 5. Gitea skills moved to `providers/gitea/` provider adapter.
|
||||
|
||||
@@ -42,11 +42,14 @@ Current skills: `caveman`, `diagnose`, `grill-me`, `grill-with-docs`, `improve-c
|
||||
- `init-project.sh` — bootstraps a new project (Chunk 6)
|
||||
- Copilot provider adapter (Chunk 7)
|
||||
- Formal CI/pre-commit enforcement of governance rules (Chunk 6)
|
||||
- `setup-gitleaks.sh` not yet wired into `init-project.sh` (Chunk 6) — run manually against new repos
|
||||
|
||||
For chunk planning and open questions, see `docs/ROADMAP.md`.
|
||||
|
||||
## Recent changes
|
||||
|
||||
- 2026-06-20 — Gitleaks secret scanning added. `scripts/setup-gitleaks.sh` installs gitleaks v8.24.2, seeds `.gitleaks.toml` (first run only — project-owned after that), and writes a managed pre-commit hook block that is replaced on re-run. `scripts/gitleaks.toml` is the base config template extending gitleaks defaults. `tests/test-setup-gitleaks.sh` covers 6 behaviors (reject non-git dir, config deploy, hook create, append, stale-block replace, idempotency). `.gitleaks.toml` in repo root adds path allowlist for `docs/research/` (high-entropy terminal captures). `gitleaks` skill added (`cross-cutting`) covering full lifecycle: install, update, tune allowlist, scan modes, resolve real findings. Key lesson: v8.24.2 uses `[allowlist]` (singular); v8.25.0+ uses `[[allowlists]]` — wrong syntax silently does nothing.
|
||||
|
||||
- 2026-05-18 — Issue 0018 phase 1 refactor complete: `write-skill` redesigned from scratch. New files added to skill directory: `SKILL-TEMPLATE.md` (authoritative 6-section template with XML blocks, human-usable), `META-TEMPLATE.md` (provenance schema with inline-commented YAML), `CATEGORIES.md` (self-contained category table), `META.md` (write-skill's own provenance). SKILL.md rewritten: 6 sections replacing 8 (Role and When/When not dropped — not in agentskills.io spec); frontmatter reduced to 3 fields (`name`, `description`, `metadata.category`); provenance fields (`version`, `updated`, `when`, `source`, `references`) moved to META.md (progressive disclosure — not loaded at startup). `docs/notes/skill-implementation-workflow.md` updated to reference SKILL-TEMPLATE.md as the authoritative template.
|
||||
|
||||
- 2026-05-17 — Issue 0018 phase 2 complete: `write-docs` skill written and deployed. First skill produced end-to-end by the factory (SKILL.md via `write-skill`, eval via `write-eval`). Category: implement. Key decisions: file-approval gate before reading (user names files or approves proposals); gap check before drafting (user fills what code doesn't explain); stage skipping allowed with logged reason; full revised section shown before confirmation gate; surgical edits only with per-round delta summary; Reader Testing via scoped sub-agent (doc + questions only, no source files); summary/overview sections written last. Sources: anthropics/skills doc-coauthoring (Reader Testing stage, surgical-edit constraint), mattpocock/skills write-a-skill (trigger pattern), bmad-code-org/BMAD-METHOD bmad-advanced-elicitation (confirmation gate). Open follow-up: documentation convention (file/folder/content structure, global vs repo-specific) — not yet defined.
|
||||
|
||||
9
plugins/hello-world/.claude-plugin/plugin.json
Normal file
9
plugins/hello-world/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "hello-world",
|
||||
"displayName": "Hello World",
|
||||
"description": "A complete scaffold demonstrating every cross-compatible plugin feature for the holocron marketplace.",
|
||||
"author": { "name": "Your Name", "url": "https://github.com/your-org/ai-development" },
|
||||
"homepage": "https://github.com/your-org/ai-development",
|
||||
"license": "MIT",
|
||||
"keywords": ["scaffold", "example", "holocron"]
|
||||
}
|
||||
9
plugins/hello-world/.mcp.json
Normal file
9
plugins/hello-world/.mcp.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"hello-world": {
|
||||
"type": "stdio",
|
||||
"command": "node",
|
||||
"args": ["${CLAUDE_PLUGIN_ROOT}/bin/mcp-server.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
77
plugins/hello-world/README.md
Normal file
77
plugins/hello-world/README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# hello-world
|
||||
|
||||
Complete scaffold plugin for the **holocron** marketplace. Every cross-compatible component
|
||||
is included with a working stub. Copy this directory, rename it, and replace the stubs with
|
||||
your actual content.
|
||||
|
||||
## Directory layout
|
||||
|
||||
```
|
||||
hello-world/
|
||||
├── plugin.json # Copilot CLI manifest — declares component paths
|
||||
├── .claude-plugin/
|
||||
│ └── plugin.json # Claude Code manifest — no component paths (uses defaults)
|
||||
├── skills/
|
||||
│ └── hello-world/
|
||||
│ └── SKILL.md # ✅ identical for both tools
|
||||
├── agents/
|
||||
│ ├── hello-world.md # Claude Code agent (no frontmatter)
|
||||
│ └── hello-world.agent.md # Copilot CLI agent (YAML frontmatter + tools:)
|
||||
├── hooks/
|
||||
│ └── hooks.json # Claude Code hooks (PostToolUse / PreToolUse events)
|
||||
├── hooks.json # Copilot CLI hooks (session_start / session_end events)
|
||||
├── .mcp.json # ✅ MCP server config — identical for both tools
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Cross-compatibility notes
|
||||
|
||||
| Component | Claude Code path | Copilot CLI path | Portable? |
|
||||
|---|---|---|---|
|
||||
| Plugin manifest | `.claude-plugin/plugin.json` | `plugin.json` at root | Ship both |
|
||||
| Skills | `skills/<name>/SKILL.md` | `skills/<name>/SKILL.md` | ✅ Identical |
|
||||
| Agents | `agents/<name>.md` | `agents/<name>.agent.md` | Ship both |
|
||||
| Hooks | `hooks/hooks.json` | `hooks.json` at root | Ship both |
|
||||
| MCP servers | `.mcp.json` at root | `.mcp.json` at root | ✅ Identical |
|
||||
|
||||
## Install via marketplace
|
||||
|
||||
```bash
|
||||
# Claude Code
|
||||
claude plugin marketplace add <owner>/ai-development
|
||||
claude plugin install hello-world@holocron
|
||||
|
||||
# GitHub Copilot CLI
|
||||
copilot plugin marketplace add <owner>/ai-development
|
||||
copilot plugin install hello-world
|
||||
```
|
||||
|
||||
## Local dev install
|
||||
|
||||
```bash
|
||||
# Claude Code
|
||||
claude --plugin-dir ./plugins/hello-world
|
||||
|
||||
# GitHub Copilot CLI
|
||||
copilot plugin install ./plugins/hello-world
|
||||
```
|
||||
|
||||
## Validate (Claude Code)
|
||||
|
||||
```bash
|
||||
claude plugin validate ./plugins/hello-world
|
||||
```
|
||||
|
||||
Copilot CLI has no validate command — run `copilot plugin install ./plugins/hello-world`
|
||||
and check that `/skills list` and `/agent` surface the expected entries.
|
||||
|
||||
## Creating a new plugin from this scaffold
|
||||
|
||||
1. Copy `plugins/hello-world/` → `plugins/<your-plugin-name>/`
|
||||
2. Update `name` in both `plugin.json` files
|
||||
3. Replace `skills/hello-world/SKILL.md` with your skill(s)
|
||||
4. Replace or remove `agents/` files — only include if your plugin ships an agent
|
||||
5. Replace or remove `hooks/` files — only include if your plugin reacts to events
|
||||
6. Replace or remove `.mcp.json` — only include if your plugin ships an MCP server
|
||||
7. Add the new plugin to `.claude-plugin/marketplace.json`
|
||||
8. Run `claude plugin validate ./plugins/<your-plugin-name>`
|
||||
13
plugins/hello-world/agents/hello-world.agent.md
Normal file
13
plugins/hello-world/agents/hello-world.agent.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
name: hello-world
|
||||
description: A demonstration agent for the holocron marketplace scaffold. Confirms agent loading works in GitHub Copilot CLI.
|
||||
tools:
|
||||
- read_file
|
||||
- list_directory
|
||||
---
|
||||
|
||||
You are a demonstration agent included in the `hello-world` scaffold plugin for the **holocron** marketplace.
|
||||
|
||||
When invoked, greet the user, confirm the agent is loaded, and list the components available in this plugin: skills (`skills/`), agents (`agents/`), hooks (`hooks.json`), and MCP servers (`.mcp.json`).
|
||||
|
||||
Replace this file with your agent's actual system prompt and instructions.
|
||||
10
plugins/hello-world/agents/hello-world.md
Normal file
10
plugins/hello-world/agents/hello-world.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: hello-world
|
||||
description: A demonstration agent for the holocron marketplace scaffold. Confirms agent loading works in Claude Code.
|
||||
---
|
||||
|
||||
You are a demonstration agent included in the `hello-world` scaffold plugin for the **holocron** marketplace.
|
||||
|
||||
When invoked, greet the user, confirm the agent is loaded, and list the components available in this plugin: skills (`skills/`), agents (`agents/`), hooks (`hooks/hooks.json`), and MCP servers (`.mcp.json`).
|
||||
|
||||
Replace this file with your agent's actual system prompt and instructions.
|
||||
8
plugins/hello-world/hooks.json
Normal file
8
plugins/hello-world/hooks.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"event": "session_start",
|
||||
"command": "echo '[hello-world] session started'"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
plugins/hello-world/hooks/hooks.json
Normal file
15
plugins/hello-world/hooks/hooks.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "echo '[hello-world] PostToolUse fired'"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
11
plugins/hello-world/plugin.json
Normal file
11
plugins/hello-world/plugin.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "hello-world",
|
||||
"description": "A complete scaffold demonstrating every cross-compatible plugin feature for the holocron marketplace.",
|
||||
"author": { "name": "Your Name", "email": "you@example.com" },
|
||||
"license": "MIT",
|
||||
"keywords": ["scaffold", "example", "holocron"],
|
||||
"agents": "agents/",
|
||||
"skills": ["skills/"],
|
||||
"hooks": "hooks.json",
|
||||
"mcpServers": ".mcp.json"
|
||||
}
|
||||
13
plugins/hello-world/skills/hello-world/SKILL.md
Normal file
13
plugins/hello-world/skills/hello-world/SKILL.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
name: hello-world
|
||||
description: Prove the holocron marketplace plugin is installed and working. Use when the user says "/hello-world", "test plugin install", or "is the holocron marketplace working". Do NOT use for any real task.
|
||||
metadata:
|
||||
category: cross-cutting
|
||||
---
|
||||
|
||||
Respond with exactly:
|
||||
|
||||
> Hello from the **holocron** marketplace! The `hello-world` plugin is installed and working.
|
||||
> Run `claude plugin list` to see all installed plugins.
|
||||
|
||||
Nothing else.
|
||||
24
scripts/gitleaks.toml
Normal file
24
scripts/gitleaks.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
title = "gitleaks config"
|
||||
|
||||
[extend]
|
||||
# Extends the default ruleset built into gitleaks.
|
||||
# Remove useDefault and define [[rules]] from scratch if you want full control.
|
||||
useDefault = true
|
||||
|
||||
# Rules to disable from the default set — uncomment and add IDs for known false positives.
|
||||
# Run `gitleaks git -v` on your repo first to discover which rules fire.
|
||||
# disabledRules = ["generic-api-key"]
|
||||
|
||||
# Global allowlist — applies to all rules.
|
||||
# Note: uses [allowlist] (v8 syntax). v8.25.0+ uses [[allowlists]] (array of tables).
|
||||
# Add path regexes or stopwords to suppress known false positives.
|
||||
#
|
||||
# Example: ignore test fixtures
|
||||
# [allowlist]
|
||||
# description = "test fixtures"
|
||||
# paths = ['''tests/fixtures/.*''']
|
||||
#
|
||||
# Example: ignore a known false-positive secret value
|
||||
# [allowlist]
|
||||
# description = "placeholder values in docs"
|
||||
# stopwords = ["example", "placeholder", "changeme"]
|
||||
124
scripts/setup-gitleaks.sh
Executable file
124
scripts/setup-gitleaks.sh
Executable file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Sets up gitleaks as a git pre-commit hook in a target repository.
|
||||
# Usage: setup-gitleaks.sh [TARGET_REPO]
|
||||
# TARGET_REPO — path to the git repo to configure (default: current directory)
|
||||
# Idempotent: safe to re-run; always replaces the hook block with the current version.
|
||||
|
||||
GITLEAKS_VERSION="8.24.2"
|
||||
GITLEAKS_INSTALL_DIR="${GITLEAKS_INSTALL_DIR:-/usr/local/bin}"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TARGET="${1:-$(pwd)}"
|
||||
HOOK_FILE="$TARGET/.git/hooks/pre-commit"
|
||||
CONFIG_SRC="$SCRIPT_DIR/gitleaks.toml"
|
||||
CONFIG_DEST="$TARGET/.gitleaks.toml"
|
||||
MARKER="# managed by setup-gitleaks.sh"
|
||||
END_MARKER="# end gitleaks"
|
||||
|
||||
# --- Install gitleaks if not present ---
|
||||
|
||||
install_gitleaks() {
|
||||
local os arch tarball url tmp_dir
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux) os="linux" ;;
|
||||
Darwin) os="darwin" ;;
|
||||
*)
|
||||
echo "Error: unsupported OS '$(uname -s)' — install gitleaks manually from https://github.com/gitleaks/gitleaks/releases" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$(uname -m)" in
|
||||
x86_64) arch="x64" ;;
|
||||
aarch64 | arm64) arch="arm64" ;;
|
||||
*)
|
||||
echo "Error: unsupported architecture '$(uname -m)' — install gitleaks manually from https://github.com/gitleaks/gitleaks/releases" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
tarball="gitleaks_${GITLEAKS_VERSION}_${os}_${arch}.tar.gz"
|
||||
url="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${tarball}"
|
||||
tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp_dir"' RETURN
|
||||
|
||||
echo "Installing gitleaks v${GITLEAKS_VERSION}..."
|
||||
curl -fsSL "$url" -o "$tmp_dir/$tarball"
|
||||
tar -xzf "$tmp_dir/$tarball" -C "$tmp_dir" gitleaks
|
||||
install -m 755 "$tmp_dir/gitleaks" "$GITLEAKS_INSTALL_DIR/gitleaks"
|
||||
echo "Installed: $GITLEAKS_INSTALL_DIR/gitleaks"
|
||||
}
|
||||
|
||||
if ! command -v gitleaks &>/dev/null; then
|
||||
install_gitleaks
|
||||
fi
|
||||
|
||||
# --- Validate ---
|
||||
|
||||
if [ ! -d "$TARGET/.git" ]; then
|
||||
echo "Error: $TARGET is not a git repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$CONFIG_SRC" ]; then
|
||||
echo "Error: config template not found at $CONFIG_SRC" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Deploy config ---
|
||||
|
||||
if [ -f "$CONFIG_DEST" ]; then
|
||||
echo "Skipped: $CONFIG_DEST already exists — edit it directly to customise rules."
|
||||
else
|
||||
cp "$CONFIG_SRC" "$CONFIG_DEST"
|
||||
echo "Wrote: $CONFIG_DEST"
|
||||
echo " Commit this file — it belongs in version control."
|
||||
fi
|
||||
|
||||
# --- Deploy hook ---
|
||||
|
||||
hook_block() {
|
||||
cat <<BLOCK
|
||||
|
||||
$MARKER
|
||||
if command -v gitleaks &>/dev/null; then
|
||||
gitleaks git --staged --redact -v
|
||||
else
|
||||
echo "Warning: gitleaks not installed — secret scan skipped (https://github.com/gitleaks/gitleaks/releases)" >&2
|
||||
fi
|
||||
$END_MARKER
|
||||
BLOCK
|
||||
}
|
||||
|
||||
write_hook() {
|
||||
local hook_file="$1"
|
||||
|
||||
if grep -qF "$MARKER" "$hook_file"; then
|
||||
# Remove old block (start marker through end marker inclusive) then append current version
|
||||
awk -v start="$MARKER" -v end="$END_MARKER" '
|
||||
$0 == start { skip=1; next }
|
||||
skip && $0 == end { skip=0; next }
|
||||
!skip { print }
|
||||
' "$hook_file" > "${hook_file}.tmp" && mv "${hook_file}.tmp" "$hook_file"
|
||||
hook_block >> "$hook_file"
|
||||
echo "Updated: $hook_file (gitleaks block replaced)"
|
||||
else
|
||||
hook_block >> "$hook_file"
|
||||
echo "Updated: $hook_file (gitleaks block appended to existing hook)"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -f "$HOOK_FILE" ]; then
|
||||
write_hook "$HOOK_FILE"
|
||||
else
|
||||
{ echo '#!/usr/bin/env bash'; echo 'set -euo pipefail'; hook_block; } > "$HOOK_FILE"
|
||||
chmod +x "$HOOK_FILE"
|
||||
echo "Created: $HOOK_FILE"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Done. Staged secrets will be scanned on every commit in $TARGET."
|
||||
echo "To skip on a single commit: SKIP=gitleaks git commit ..."
|
||||
151
tests/test-setup-gitleaks.sh
Executable file
151
tests/test-setup-gitleaks.sh
Executable file
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SCRIPT="$REPO_ROOT/scripts/setup-gitleaks.sh"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
||||
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
# Fake gitleaks binary — prevents the install step from running during tests
|
||||
FAKE_BIN="$(mktemp -d)"
|
||||
trap 'rm -rf "$FAKE_BIN"' EXIT
|
||||
printf '#!/bin/sh\necho "gitleaks fake"\n' > "$FAKE_BIN/gitleaks"
|
||||
chmod +x "$FAKE_BIN/gitleaks"
|
||||
export PATH="$FAKE_BIN:$PATH"
|
||||
|
||||
# Helper: create an isolated git repo in a temp dir
|
||||
make_repo() {
|
||||
local dir
|
||||
dir="$(mktemp -d)"
|
||||
git -C "$dir" init -q
|
||||
echo "$dir"
|
||||
}
|
||||
|
||||
# Helper: run setup script against a target repo; capture output; always return exit code
|
||||
run_setup() {
|
||||
local target="$1"
|
||||
bash "$SCRIPT" "$target" 2>&1
|
||||
}
|
||||
|
||||
# --- 1. Rejects non-git directory ---
|
||||
echo ""
|
||||
echo "--- rejects non-git directory ---"
|
||||
NON_GIT="$(mktemp -d)"
|
||||
trap 'rm -rf "$NON_GIT"' EXIT
|
||||
if bash "$SCRIPT" "$NON_GIT" >/dev/null 2>&1; then
|
||||
fail "exited 0 for non-git directory — expected exit 1"
|
||||
else
|
||||
pass "exits non-zero for non-git directory"
|
||||
fi
|
||||
|
||||
# --- 2. Config deployed ---
|
||||
echo ""
|
||||
echo "--- config deployed to target repo ---"
|
||||
REPO="$(make_repo)"
|
||||
trap 'rm -rf "$REPO"' EXIT
|
||||
run_setup "$REPO" > /dev/null
|
||||
if [ -f "$REPO/.gitleaks.toml" ]; then
|
||||
pass ".gitleaks.toml created in target repo"
|
||||
else
|
||||
fail ".gitleaks.toml missing from target repo"
|
||||
fi
|
||||
if diff -q "$REPO_ROOT/scripts/gitleaks.toml" "$REPO/.gitleaks.toml" > /dev/null 2>&1; then
|
||||
pass ".gitleaks.toml matches the template"
|
||||
else
|
||||
fail ".gitleaks.toml content differs from template"
|
||||
fi
|
||||
|
||||
# --- 3. Hook created from scratch ---
|
||||
echo ""
|
||||
echo "--- hook created when none exists ---"
|
||||
REPO2="$(make_repo)"
|
||||
trap 'rm -rf "$REPO2"' EXIT
|
||||
run_setup "$REPO2" > /dev/null
|
||||
HOOK="$REPO2/.git/hooks/pre-commit"
|
||||
if [ -f "$HOOK" ]; then
|
||||
pass "pre-commit hook created"
|
||||
else
|
||||
fail "pre-commit hook not created"
|
||||
fi
|
||||
if [ -x "$HOOK" ]; then
|
||||
pass "pre-commit hook is executable"
|
||||
else
|
||||
fail "pre-commit hook is not executable"
|
||||
fi
|
||||
if head -1 "$HOOK" | grep -q "^#!"; then
|
||||
pass "pre-commit hook has a shebang"
|
||||
else
|
||||
fail "pre-commit hook missing shebang"
|
||||
fi
|
||||
if grep -q "gitleaks git --staged" "$HOOK"; then
|
||||
pass "pre-commit hook contains gitleaks command"
|
||||
else
|
||||
fail "pre-commit hook missing gitleaks command"
|
||||
fi
|
||||
if grep -q "# managed by setup-gitleaks.sh" "$HOOK"; then
|
||||
pass "pre-commit hook contains idempotency marker"
|
||||
else
|
||||
fail "pre-commit hook missing idempotency marker"
|
||||
fi
|
||||
|
||||
# --- 4. Appends to existing hook; existing content retained ---
|
||||
echo ""
|
||||
echo "--- appends to existing hook; prior content retained ---"
|
||||
REPO3="$(make_repo)"
|
||||
trap 'rm -rf "$REPO3"' EXIT
|
||||
HOOK3="$REPO3/.git/hooks/pre-commit"
|
||||
printf '#!/usr/bin/env bash\nnpm test\n' > "$HOOK3"
|
||||
chmod +x "$HOOK3"
|
||||
run_setup "$REPO3" > /dev/null
|
||||
if grep -q "npm test" "$HOOK3"; then
|
||||
pass "existing hook content retained after append"
|
||||
else
|
||||
fail "existing hook content lost after append"
|
||||
fi
|
||||
if grep -q "gitleaks git --staged" "$HOOK3"; then
|
||||
pass "gitleaks block appended to existing hook"
|
||||
else
|
||||
fail "gitleaks block missing after append"
|
||||
fi
|
||||
|
||||
# --- 5. Second run replaces stale block; existing content still retained ---
|
||||
echo ""
|
||||
echo "--- second run replaces stale block; existing content still retained ---"
|
||||
# Corrupt the gitleaks block to simulate stale content from an older version
|
||||
sed -i 's/gitleaks git --staged/gitleaks protect --staged/' "$HOOK3"
|
||||
run_setup "$REPO3" > /dev/null
|
||||
if grep -q "npm test" "$HOOK3"; then
|
||||
pass "existing content retained after block replacement"
|
||||
else
|
||||
fail "existing content lost after block replacement"
|
||||
fi
|
||||
marker_count="$(grep -c "# managed by setup-gitleaks.sh" "$HOOK3")"
|
||||
if [ "$marker_count" -eq 1 ]; then
|
||||
pass "gitleaks block appears exactly once after second run"
|
||||
else
|
||||
fail "gitleaks block duplicated — found $marker_count occurrences of marker"
|
||||
fi
|
||||
if grep -q "gitleaks git --staged" "$HOOK3"; then
|
||||
pass "stale gitleaks command replaced with current command"
|
||||
else
|
||||
fail "stale gitleaks command not replaced — block was skipped, not updated"
|
||||
fi
|
||||
|
||||
# --- 6. Third run still idempotent ---
|
||||
echo ""
|
||||
echo "--- repeated runs stay idempotent ---"
|
||||
run_setup "$REPO3" > /dev/null
|
||||
run_setup "$REPO3" > /dev/null
|
||||
marker_count="$(grep -c "# managed by setup-gitleaks.sh" "$HOOK3")"
|
||||
if [ "$marker_count" -eq 1 ]; then
|
||||
pass "gitleaks block still appears exactly once after four total runs"
|
||||
else
|
||||
fail "gitleaks block duplicated — found $marker_count occurrences after four runs"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
Reference in New Issue
Block a user