diff --git a/.githooks/pre-push b/.githooks/pre-push index 602b087e..f9b9e422 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -44,8 +44,11 @@ while IFS= read -r line; do fi # Commit count limit guard — prevent pushing massive commits from wrong base branch + # Exclude commits that already exist on remote tracking branches (--remotes=origin) + # so that merged upstream/remote branches (e.g. origin/main, origin/next-fix) do not + # inflate the count of genuinely new outgoing commits. if [ "$remote_sha" != "0000000000000000000000000000000000000000" ] && [ -n "$remote_sha" ]; then - COMMIT_COUNT=$(git rev-list --count "$remote_sha..$local_sha" 2>/dev/null || echo 0) + COMMIT_COUNT=$(git rev-list --count "$local_sha" --not "$remote_sha" --remotes=origin 2>/dev/null || echo 0) else DEFAULT_BASE="origin/main" case "$local_ref" in @@ -65,7 +68,7 @@ while IFS= read -r line; do MIN_COUNT=999999 for cand in origin/next-feat origin/next-fix origin/main origin/master; do if git rev-parse --verify "$cand" >/dev/null 2>&1; then - cnt=$(git rev-list --count "$cand..$local_sha" 2>/dev/null || echo 999999) + cnt=$(git rev-list --count "$local_sha" --not "$cand" --remotes=origin 2>/dev/null || echo 999999) if [ "$cnt" -lt "$MIN_COUNT" ]; then MIN_COUNT="$cnt" DEFAULT_BASE="$cand" @@ -73,7 +76,7 @@ while IFS= read -r line; do fi done fi - COMMIT_COUNT=$(git rev-list --count "$DEFAULT_BASE..$local_sha" 2>/dev/null || echo 0) + COMMIT_COUNT=$(git rev-list --count "$local_sha" --not "$DEFAULT_BASE" --remotes=origin 2>/dev/null || echo 0) fi BASE_REF="${DEFAULT_BASE:-origin/main}" diff --git a/hooks/hooks.json b/hooks/hooks.json index a4b32a1d..afe14c47 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -273,10 +273,6 @@ { "matcher": "Skill", "hooks": [ - { - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/skills/hook-kit/resources/remind-rag-flag-on-skill.sh" - }, { "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/skills/hook-kit/resources/block-supervisor-loop-work.sh" diff --git a/skills/cc-plugin/scripts/cache-cleanup.sh b/skills/cc-plugin/scripts/cache-cleanup.sh deleted file mode 100755 index aba5f2dc..00000000 --- a/skills/cc-plugin/scripts/cache-cleanup.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/bin/bash -# Plugin cache cleanup script -# Keeps only the latest version directory for each plugin -# Latest = most recently created directory (by birthtime) - -set -euo pipefail - -CACHE_DIR="${HOME}/.claude/plugins/cache" -DRY_RUN=false -VERBOSE=false - -usage() { - echo "Usage: $0 [OPTIONS]" - echo "" - echo "Options:" - echo " -n, --dry-run Show what would be deleted without actually deleting" - echo " -v, --verbose Show detailed information" - echo " -h, --help Show this help message" -} - -usage_exit() { - usage - exit "${1:-2}" -} - -while [[ $# -gt 0 ]]; do - case $1 in - -n|--dry-run) DRY_RUN=true; shift ;; - -v|--verbose) VERBOSE=true; shift ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown option: $1" >&2; usage_exit 2 ;; - esac -done - -log() { - if [[ "$VERBOSE" == true ]]; then - echo "$@" - fi -} - -cleanup_marketplace() { - local marketplace_dir="$1" - local marketplace_name - marketplace_name=$(basename "$marketplace_dir") - - log "Processing marketplace: $marketplace_name" - - # Skip if not a directory - [[ -d "$marketplace_dir" ]] || return 0 - - # Skip temp directories - if [[ "$marketplace_name" == temp_git_* ]]; then - if [[ "$DRY_RUN" == true ]]; then - echo "[DRY-RUN] Would delete temp directory: $marketplace_dir" - else - echo "Deleting temp directory: $marketplace_dir" - rm -rf "$marketplace_dir" - fi - return 0 - fi - - # Process each plugin in the marketplace - for plugin_dir in "$marketplace_dir"/*/; do - [[ -d "$plugin_dir" ]] || continue - - local plugin_name=$(basename "$plugin_dir") - log " Processing plugin: $plugin_name" - - # Get all version directories with their birthtime - local versions=() - local times=() - - for version_dir in "$plugin_dir"/*/; do - [[ -d "$version_dir" ]] || continue - local version_name=$(basename "$version_dir") - - # Get a creation/modification time (portable best-effort). - # 1) macOS/BSD: birthtime via `stat -f "%B"` - # 2) Linux: birthtime via `stat -c "%W"` (returns 0 when filesystem doesn't track it) - # 3) Fallback (any platform): modification time via `stat -c "%Y"` so versions are still ordered - local birthtime - if birthtime=$(stat -f "%B" "$version_dir" 2>/dev/null); then - : - elif birthtime=$(stat -c "%W" "$version_dir" 2>/dev/null) && [[ "$birthtime" != "0" ]]; then - : - elif birthtime=$(stat -c "%Y" "$version_dir" 2>/dev/null); then - : - else - birthtime=0 - fi - - versions+=("$version_name") - times+=("$birthtime") - done - - # Skip if only one or no versions - if [[ ${#versions[@]} -le 1 ]]; then - log " Only ${#versions[@]} version(s), skipping" - continue - fi - - # Find the latest version (highest birthtime) - local latest_idx=0 - local latest_time=${times[0]} - - for i in "${!times[@]}"; do - if [[ ${times[$i]} -gt $latest_time ]]; then - latest_time=${times[$i]} - latest_idx=$i - fi - done - - local latest_version=${versions[$latest_idx]} - log " Latest version: $latest_version (birthtime: $latest_time)" - - # Delete old versions - for i in "${!versions[@]}"; do - if [[ $i -ne $latest_idx ]]; then - local old_version=${versions[$i]} - local old_dir="${plugin_dir}${old_version}" - - if [[ "$DRY_RUN" == true ]]; then - echo "[DRY-RUN] Would delete: $old_dir" - else - echo "Deleting old cache: $old_dir" - rm -rf "$old_dir" - fi - fi - done - done -} - -main() { - if [[ ! -d "$CACHE_DIR" ]]; then - echo "Cache directory not found: $CACHE_DIR" - exit 1 - fi - - echo "Plugin cache cleanup" - echo "====================" - if [[ "$DRY_RUN" == true ]]; then - echo "Mode: DRY-RUN (no changes will be made)" - fi - echo "" - - # Process each marketplace - for marketplace_dir in "$CACHE_DIR"/*/; do - cleanup_marketplace "$marketplace_dir" - done - - echo "" - echo "Done!" -} - -main diff --git a/skills/cleanup/fa-prune.md b/skills/cleanup/fa-prune.md index eba09385..59e01369 100644 --- a/skills/cleanup/fa-prune.md +++ b/skills/cleanup/fa-prune.md @@ -163,24 +163,32 @@ When the section is found in archive: | 2 | Add a new entry to HOT without restoring, even when the same keyword exists in archive | Restore → add recurrence marker → append new entry body | | 3 | Assume "archive is a permanent cleanup" | Archive = COLD cache. Can return to HOT immediately on recurrence | -### 8. RAG dispatch (`--rag=:`, vendor-agnostic) +### 8. RAG dispatch (workspace-config resolved, vendor-agnostic) Same abstract contract as the `/archive` skill. Simultaneously store COLD-demoted sections to a RAG receiver to strengthen semantic search / recurrence detection (Section 7-1). -**Invocation format**: +**Receiver resolution**: read the workspace bindings config through +`bash /resources/workspace-config.sh --export` and use the +`WSCFG_RAG_*` values it exports. The binding lives in the config, so swapping +vendors stays a one-line config edit and **no caller flag is required**. -``` -/cleanup fa-prune --rag=: -``` +| Resolved state | Behavior | +|---|---| +| `WSCFG_RAG_KIND` unset / `none` / resolver unavailable | **Skip quietly** — no dispatch, no warning, no block | +| `WSCFG_RAG_KIND` set to a receiver kind | Dispatch per section using `WSCFG_RAG_ENDPOINT` + the matching `WSCFG_RAG_COLLECTION_*` | +| `--no-rag` passed | Skip regardless of the resolved binding | +| `--rag=:` passed | Optional override, takes precedence over the resolved binding | -Or when invoking `Skill("cleanup", "fa-prune")`, include `--rag=:` in args. The caller (Claude) specifies the receiver available in the environment — the vendor name is the caller's domain; the callee (fa-prune) only receives the receiver identifier and dispatches. +`kind: none` means the role is unconfigured, and the config's own contract states +consumers must **skip rather than block**. Never warn, fail, or deny a call merely +because a receiver is absent or a flag was omitted. #### Applicable matrix | Task | Dispatch target | Default behavior | |------|-------------|------------| -| COLD-demoted sections (after Section 4 execution) | Store 1 per section to the receiver | `--rag` not specified = no dispatch | -| Backfill (bulk-store existing archive to the receiver) | All archive sections | Explicit `--rag` + `--backfill` flag | +| COLD-demoted sections (after Section 4 execution) | Store 1 per section to the receiver | Resolved receiver; `kind: none` = no dispatch | +| Backfill (bulk-store existing archive to the receiver) | All archive sections | Explicit `--backfill` flag | #### Receiver protocol (vendor-agnostic) @@ -200,24 +208,29 @@ The receiver uses an idempotent id (e.g., sha1(`fa-archive::`)) for | # | Don't | Do | |---|-------------|-----------------| -| 1 | Hardcode a specific vendor (vector DB / embedding model / MCP tool name directly in fa-prune.md) | Let the caller specify the receiver via `--rag=<skill>:<topic>`. Keep the callee vendor-agnostic | +| 1 | Hardcode a specific vendor (vector DB / embedding model / MCP tool name directly in fa-prune.md) | Resolve the receiver from the workspace config. Keep the callee vendor-agnostic | | 2 | Ignore section-level granularity and store an entire archive file as 1 chunk | Store per-section — Section 7-1 semantic search depends on section-level matching | | 3 | Omit metadata | 4 metadata keys required (type/project/date/category) + source_file/section_title | -| 4 | Auto-dispatch when `--rag` is not specified | If not specified = no dispatch. Auto-supply is the caller's (Claude's) responsibility (`skill-usage.md` "Auto-supply available vendor dispatch when invoking a shared skill") | +| 4 | Block, warn, or demand a flag when no receiver is configured | `kind: none` = skip quietly and continue. An absent receiver is a valid state, not an error | | 5 | Also delete from the receiver on restore (Section 7-2) | Restore only brings back to HOT. Receiver data is kept (archive history also helps semantic search) | **HTTP fallback script (when receiver MCP is down)**: `scripts/fa-batch-store.py` — consumes `--cut-dir` output or `--backfill <archive.md>` + `--skip-existing` (idempotent). Do not write ad-hoc inline store scripts. #### Self-check (right before running fa-prune) -1. Is the `--rag=<skill>:<topic>` flag included in the invocation? -2. If not included + a RAG receiver is available (a RAG store tool exists in the caller's environment) → the caller must auto-supply (skill-usage.md HARD STOP) -3. On COLD demote, call the receiver for each section, then write to the archive file -4. Backfill mode: bulk-store existing archive files to the receiver via `--backfill --rag=<skill>:<topic>` +1. Resolve the receiver via `workspace-config.sh --export` (or `--json`) — do not require a caller flag +2. If `WSCFG_RAG_KIND` is unset or `none` (and no `--rag` override): + - Archive COLD-demoted sections to disk + - Skip receiver call and skip count-equality requirements + - Report 0 chunks stored (`receiver: none (unconfigured)`) +3. When a receiver is resolved or explicitly overridden: + - Call the receiver for each COLD section + - Stored chunk count must equal demoted section count +4. Backfill mode: bulk-store existing archive files to the receiver via `--backfill` #### RAG store quantity reporting obligation (HARD STOP) -After fa-prune completes, **state the number of chunks added quantitatively at the end of the response**. If N sections were demoted to COLD + N were stored to the RAG receiver, state that number exactly. +After fa-prune completes, **state the number of chunks added quantitatively at the end of the response**. When a receiver is active, if N sections were demoted to COLD + N were stored to the RAG receiver, state that number exactly. When no receiver is configured (`kind: none`), state 0 chunks stored with N sections demoted. ``` RAG store summary: N chunks added (receiver: <skill>:<topic>) @@ -231,7 +244,7 @@ COLD demoted sections: N |---|-------|----| | 1 | Status-only "RAG store per section complete" | Quantitative "RAG store summary: 3 chunks added (receiver: <skill>:<topic>)" | | 2 | Report only mid-response, omit from the end | Show the RAG summary block **again** at the end of the response | -| 3 | Mismatch between demote count and store count (e.g., 3 demoted but only 2 stored) | demote count = store count = reported count to the user. Verify all 3 match | +| 3 | Mismatch between demote count and store count when a receiver is active (e.g., 3 demoted but only 2 stored) | demote count = store count = reported count to the user (when receiver is active). Verify all 3 match | Detailed format rule: see `~/.agents/rules/skill-usage.md` "RAG store report format obligation" section. diff --git a/skills/cleanup/retrospect.md b/skills/cleanup/retrospect.md index 64e2c564..aea9cfe3 100644 --- a/skills/cleanup/retrospect.md +++ b/skills/cleanup/retrospect.md @@ -165,7 +165,7 @@ Add a section to `~/.claude/skills/cleanup/data/failed-attempts.md` (HOT). This **A HOT entry must also reach the RAG receiver at write time, not only when `fa-prune` later demotes it to COLD.** `fa-prune.md` Section 8 only dispatches archive-bound (COLD) sections — a freshly-written HOT entry stays invisible to semantic search until it goes stale enough to be archived (often weeks/months later). This defeats the "Recurrence pre-check" Stage 0 RAG search that `fix.md`/this file's own Step 1.5 mandate: it can only ever find *old* patterns, never a paraphrased recurrence of something recorded last week. -Immediately after 4-2's file write, store a structured chunk to the same abstract RAG receiver contract fa-prune.md Section 8 uses (`--rag=<skill>:<topic>`, or whichever RAG-store tool is registered in the environment): +Immediately after 4-2's file write, store a structured chunk to the same abstract RAG receiver contract fa-prune.md Section 8 uses (receiver resolved from the workspace bindings config; skip quietly when none is configured): | Field | Value | |-------|-------| diff --git a/skills/cleanup/run.md b/skills/cleanup/run.md index 35a4543d..41e436c8 100644 --- a/skills/cleanup/run.md +++ b/skills/cleanup/run.md @@ -54,7 +54,7 @@ Each step clearly distinguishes between **automatic skill calls** and **user-dec | Step | Invocation obligation (automatic) | Ask (user decision) | Auto-invocation condition | |------|------------------|------------------|---------------| | Step 0 | Call `TaskList` | — | Clean up when TaskList has completed tasks | -| Step 0.5 (4.5 Resume import) | RAG receiver import dispatch (`--rag=<skill>:<topic>`) for each discovered file | — | RAG receiver readyz response + research-*/plan-* discovered | +| Step 0.5 (4.5 Resume import) | RAG receiver import dispatch (receiver resolved from the workspace config) for each discovered file | — | RAG receiver readyz response + research-*/plan-* discovered | | Step 1 | `Skill("commit-tidy")` or `/commit-tidy` | Decide split strategy (internal ask inside the skill) | When there is 1+ uncommitted change | | Step 2 (Self-Improve) | **`Skill("claudify", "improve")` call mandatory** — retrospect + automation review + pattern detect | How to handle findings (internal Phase 2 ask inside the skill) | **Always** (regardless of whether the conversation had mistakes/patterns — the skill judges) | | Step 3 (Knowledge Persist) | **`Skill("claudify", "persist")` call mandatory** + RAG receiver import dispatch 3-C.1 | Storage location (internal ask inside the skill) | **Always** + auto-import when the RAG receiver readyz responds | diff --git a/skills/code-workflow/steps.md b/skills/code-workflow/steps.md index 39cdb09e..c2bf9380 100644 --- a/skills/code-workflow/steps.md +++ b/skills/code-workflow/steps.md @@ -10,9 +10,9 @@ The core 4-stage procedure (Steps 0-3). Step 4 (Implement) is in [implement.md]( 3. If the plan has a Phase order, check **up to which Phase has been completed currently** 4. If there is an incomplete Phase, **proceed from that Phase** — do not skip to subsequent Phases (merge, deploy, etc.) -4.5. **Resume RAG re-dispatch (optional, abstract contract)**: When the caller supplied a `--rag=<skill>:<topic>` flag (see "Research/plan artifact dispatch" below), re-invoke the receiver on every existing `research-*.md` / `plan-*.md` found in Step 0 (item 1). This refreshes any indexed content that may have drifted out of sync with the file. Idempotency is the receiver's responsibility. +4.5. **Resume RAG re-dispatch (optional, abstract contract)**: When an explicit `--rag=<skill>:<topic>` receiver is supplied, dispatch to that receiver first. Otherwise, when the workspace config resolves a RAG receiver (see "Research/plan artifact dispatch" below), re-invoke the receiver on every existing `research-*.md` / `plan-*.md` found in Step 0 (item 1). This refreshes any indexed content that may have drifted out of sync with the file. Idempotency is the receiver's responsibility. - When no `--rag` flag is supplied — or no compatible receiver is available in the caller's environment — skip this step. Research/plan files in `{output-dir}` remain the primary deliverable; recall is via direct `Read` / `Grep`. + When no override is provided and the config resolves no receiver (`kind: none`) — or the resolved receiver is unreachable — skip this step quietly. Research/plan files in `{output-dir}` remain the primary deliverable; recall is via direct `Read` / `Grep`. Failure policy: receiver unreachable → warning + Step 0 continues. The file artifact preservation is primary. @@ -92,18 +92,19 @@ Read and understand the relevant code **deeply**, then write findings to `{outpu ### Research artifact dispatch (optional, abstract contract) -The `research-*.md` file is the **primary deliverable**. After every Write/Edit, the caller may optionally dispatch the artifact to a registered receiver (any RAG index, semantic store, memory service, doc cache, etc.) for cross-session discoverability — but this generic skill does not name a vendor. - -#### Flag +The `research-*.md` file is the **primary deliverable**. After every Write/Edit, the caller may optionally dispatch the artifact to a registered receiver (any RAG index, semantic store, memory service, doc cache, etc.) for cross-session discoverability. +#### Receiver resolution + ```text -/code-workflow ... --rag=<skill>:<topic> +bash <hook-kit-skill>/resources/workspace-config.sh --json # read .roles.rag fields ``` -- `<skill>` — name of a registered skill that owns a research-dispatch topic -- `<topic>` — topic within that skill responsible for accepting the artifact -- When the flag is omitted, the file write is the only deliverable. No vendor is assumed -- When the flag is supplied, dispatch fires **after every Write/Edit** completion (not at Step 1 end). Receiver handles idempotency +- Explicit `--rag=<skill>:<topic>` override always takes precedence when provided +- When no override is given: + - `roles.rag.kind` unset / `"none"` / resolver unavailable — the file write is the only deliverable. No vendor is assumed, and nothing warns or blocks + - `roles.rag.kind` set — dispatch fires **after each Write/Edit** completion, using `roles.rag.endpoint` plus the matching `roles.rag.collections.*`. Receiver handles idempotency +- `--rag=<skill>:<topic>` stays available as an explicit per-call override. It is never required, and its absence is never an error #### Contract for receivers (vendor skills implement this) @@ -118,15 +119,15 @@ Receivers consult vendor-side documentation for accepted metadata keys, chunking #### Skip conditions -- No `--rag` flag supplied by caller -- Caller-specified `<skill>:<topic>` not available in the current environment — fail-non-blocking: warning + Step 1 continues +- The workspace config resolves no receiver (`kind: none`) +- The resolved receiver is unreachable in the current environment — fail-non-blocking: warning + Step 1 continues - File content unchanged (receiver decides via its own idempotency) | # | Don't | Do | |---|-------|-----| -| 1 | Hardcode a specific RAG vendor (URL, skill name, MCP tool name) in this generic skill | Use `--rag=<skill>:<topic>` flag at the call site; vendor skill implements the receiver protocol | +| 1 | Hardcode a specific RAG vendor (URL, skill name, MCP tool name) in this generic skill | Resolve the receiver from the workspace config; the vendor skill implements the receiver protocol | | 2 | Block Step 1 on dispatch failure | Warning + continue. Artifact preservation is primary | -| 3 | Defer dispatch to Step 1 completion when the flag is supplied | When `--rag` is set, dispatch after every Write/Edit. Receiver's idempotency keeps it cheap | +| 3 | Defer dispatch to Step 1 completion when a receiver is configured | When a receiver resolves, dispatch after every Write/Edit. Receiver's idempotency keeps it cheap | | 4 | Enumerate compatible receivers inside this skill | Caller knows which receivers are available; this skill declares only the abstract surface | **Why abstract**: research recall paths vary by environment (different RAG vendors, context7 cache, project memory, etc.). Hardcoding a vendor in this generic skill couples it to one stack. The flag keeps coupling at the call site — the caller specifies the receiver vendor at invocation time, and the receiver skill implements the actual storage / index protocol. @@ -233,23 +234,23 @@ The plan body starts after the frontmatter with `# Plan: [Title]` heading. The h ### Plan artifact dispatch (optional, abstract contract) -The `plan-*.md` file is the **primary deliverable**. Same abstract contract as research dispatch above — caller supplies `--rag=<skill>:<topic>` flag, this skill stays vendor-agnostic. Receiver consumes via `CODEWORKFLOW_RAG_FILE` / `CODEWORKFLOW_RAG_METADATA_JSON` env vars or `CODEWORKFLOW_RAG_INPUT_JSON` file mode. +The `plan-*.md` file is the **primary deliverable**. Same abstract contract as research dispatch above — the receiver is resolved from the workspace config, so this skill stays vendor-agnostic. Receiver consumes via `CODEWORKFLOW_RAG_FILE` / `CODEWORKFLOW_RAG_METADATA_JSON` env vars or `CODEWORKFLOW_RAG_INPUT_JSON` file mode. **Order relative to "Plan post-write ask"**: **ask first → dispatch**. 1. Plan Write/Edit complete (initial `plan-*.md` write OR revision update) 2. Run Plan post-write ask (below) — resolve undecided items 3. After ask answers received, apply decisions → Edit `plan-*.md` -4. If `--rag` flag is supplied, dispatch the post-ask plan version to the receiver +4. If the config resolves a receiver, dispatch the post-ask plan version to it 5. (If subsequent Edits occur, repeat from step 2 — receiver handles idempotency) Rationale: ask-driven Edits typically resolve the largest unresolved decisions. Dispatching after ask captures the "settled" plan rather than an in-flight state. | # | Don't | Do | |---|-------|-----| -| 1 | Skip dispatch for in-flight plan revisions (only dispatch "final") | When `--rag` is supplied, every Write/Edit dispatches. Receiver's idempotency handles unchanged sections | +| 1 | Dispatch in-flight plan drafts before the user post-write ask | Dispatch after the ask decisions are applied (and on subsequent post-ask Edits). Receiver's idempotency handles unchanged sections | | 2 | Block the ask on dispatch success | Dispatch happens after ask. ask is the synchronous user-blocking step; dispatch is async-safe | -| 3 | Pick a default vendor when `--rag` is omitted | Omitted flag = file write only. No vendor is assumed | +| 3 | Pick a default vendor when the config resolves none | `kind: none` = file write only. No vendor is assumed | ### Plan post-write ask (HARD STOP — required immediately after writing/updating the plan file) diff --git a/skills/git-repo/doctor.md b/skills/git-repo/doctor.md index 6488ce43..9e1a10e9 100644 --- a/skills/git-repo/doctor.md +++ b/skills/git-repo/doctor.md @@ -160,8 +160,10 @@ In `.githooks/pre-push`: ```sh # Commit count limit guard — prevent pushing massive commits from wrong base branch +# Exclude commits that already exist on remote tracking branches (--remotes=origin) +# so that merged upstream/remote branches do not inflate the count of new outgoing commits. if [ "$remote_sha" != "0000000000000000000000000000000000000000" ] && [ -n "$remote_sha" ]; then - COMMIT_COUNT=$(git rev-list --count "$remote_sha..$local_sha" 2>/dev/null || echo 0) + COMMIT_COUNT=$(git rev-list --count "$local_sha" --not "$remote_sha" --remotes=origin 2>/dev/null || echo 0) else DEFAULT_BASE="origin/main" case "$local_ref" in @@ -181,7 +183,7 @@ else MIN_COUNT=999999 for cand in origin/next-feat origin/next-fix origin/main origin/master; do if git rev-parse --verify "$cand" >/dev/null 2>&1; then - cnt=$(git rev-list --count "$cand..$local_sha" 2>/dev/null || echo 999999) + cnt=$(git rev-list --count "$local_sha" --not "$cand" --remotes=origin 2>/dev/null || echo 999999) if [ "$cnt" -lt "$MIN_COUNT" ]; then MIN_COUNT="$cnt" DEFAULT_BASE="$cand" @@ -189,7 +191,7 @@ else fi done fi - COMMIT_COUNT=$(git rev-list --count "$DEFAULT_BASE..$local_sha" 2>/dev/null || echo 0) + COMMIT_COUNT=$(git rev-list --count "$local_sha" --not "$DEFAULT_BASE" --remotes=origin 2>/dev/null || echo 0) fi MAX_COMMITS="${PUSH_MAX_COMMITS:-5}" diff --git a/skills/hook-kit/hook-registry.yaml b/skills/hook-kit/hook-registry.yaml index 0fa6f3f8..f4161ab6 100644 --- a/skills/hook-kit/hook-registry.yaml +++ b/skills/hook-kit/hook-registry.yaml @@ -1061,18 +1061,6 @@ hooks: event: Stop matcher: '' command: bash ${CLAUDE_PLUGIN_ROOT}/skills/hook-kit/resources/reap-stale-wrapper-shells.sh -- id: remind-rag-flag-on-skill - owner_skill: hook-kit - marketplace: es6kr-skills - status: active - implementations: - - runtime: sh - file: skills/hook-kit/resources/remind-rag-flag-on-skill.sh - registrations: - - surface: hooks.json - event: PreToolUse - matcher: Skill - command: ${CLAUDE_PLUGIN_ROOT}/skills/hook-kit/resources/remind-rag-flag-on-skill.sh - id: session-id-inject owner_skill: session marketplace: es6kr-skills diff --git a/skills/hook-kit/resources/block-research-plan-without-rag.sh b/skills/hook-kit/resources/block-research-plan-without-rag.sh index 089bbe61..8bc94492 100755 --- a/skills/hook-kit/resources/block-research-plan-without-rag.sh +++ b/skills/hook-kit/resources/block-research-plan-without-rag.sh @@ -8,8 +8,11 @@ # - .omc/plans/*.md (plan or research patterns) # Action: Inject a stderr reminder to dispatch the artifact via RAG receiver. # -# Background: skill-usage.md "Generic skill invocation must auto-supply available -# vendor dispatch" (HARD STOP) recurred 3 times: +# Receiver binding comes from the workspace bindings config, not from a caller +# flag: when the `rag` role resolves to `kind: none` this hook stays silent, per +# that config's contract that consumers skip rather than block. +# +# Background: the caller-side dispatch rule recurred 3 times: # 1. /session archive — qdrant receiver available, --rag not supplied # 2. /session archive — MCP-only detection, missed network receiver # 3. /code-workflow — research/plan written, no qdrant-store call, archived @@ -46,9 +49,29 @@ case "$FILE_PATH" in *.bak/*|*/.bak/*|*~|*.archived) exit 0 ;; esac -# Best-effort: check session transcript for prior qdrant-store invocation. +# Check if opt-out --no-rag is present in tool input, frontmatter, or transcript +if echo "$INPUT" | grep -qE -- '--no-rag|rag:[[:space:]]*false|no_rag:[[:space:]]*true' 2>/dev/null; then + exit 0 +fi + +# Receiver gate: only warn when this workspace actually binds a RAG receiver. +# An unconfigured role (`kind: none`) or an unresolvable config is a valid state, +# so the hook exits quietly instead of nagging for a flag that is not required. +WSCFG_SHIM="$(dirname "$0")/workspace-config.sh" +RAG_KIND="" +if [[ -x "$WSCFG_SHIM" ]]; then + RAG_KIND=$(bash "$WSCFG_SHIM" --export 2>/dev/null | sed -n 's/^WSCFG_RAG_KIND=//p' | head -1) +fi +[[ -z "$RAG_KIND" || "$RAG_KIND" == "none" ]] && exit 0 + +# Best-effort: check session transcript for prior qdrant-store invocation or --no-rag opt-out. TRANSCRIPT="${CLAUDE_TRANSCRIPT_PATH:-}" if [[ -n "$TRANSCRIPT" && -r "$TRANSCRIPT" ]]; then + # Detect opt-out flag in transcript + if grep -qE -- '--no-rag' "$TRANSCRIPT" 2>/dev/null; then + exit 0 + fi + # Detect BOTH dispatch surfaces: # (a) MCP tool call — mcp__<vendor>__*-store # (b) CLI dispatch — the receiver topic's own documented script path, which is @@ -66,15 +89,14 @@ fi cat >&2 <<EOF [block-research-plan-without-rag] $FILE_PATH -RAG dispatch missing. skill-usage.md "Generic skill invocation must auto-supply -available vendor dispatch" rule applies (caller responsibility). +RAG dispatch missing. This workspace binds a RAG receiver (WSCFG_RAG_KIND=$RAG_KIND), +so the artifact should reach it before the file is archived away. Required action (pick one): - 1. Call mcp__qdrant__qdrant-store to store body + metadata - 2. When invoking code-workflow/fix, explicitly supply --rag=<skill>:<topic> flag - 3. Skipping is only allowed when receiver candidates = 0 (MCP unavailable + skill registry - receiver topics = 0) — silent skip is forbidden otherwise + 1. Dispatch to the receiver resolved by workspace-config.sh (or explicit --rag=<skill>:<topic>) + 2. Pass --no-rag when this artifact is deliberately not indexed -RAG store is mandatory before archiving to .bak/ (prevents permanent data loss). +No flag is required to dispatch — the binding is resolved from the workspace config. +Storing before archiving to .bak/ is what prevents permanent data loss. EOF exit 2 diff --git a/skills/hook-kit/resources/edit-guard.sh b/skills/hook-kit/resources/edit-guard.sh index 8b2fbcb8..1035261d 100755 --- a/skills/hook-kit/resources/edit-guard.sh +++ b/skills/hook-kit/resources/edit-guard.sh @@ -356,7 +356,9 @@ check_vendor_in_generic_skill() { echo -e "$violations" echo "Required action:" echo " - Replace vendor Skill() invocations with abstract dispatch:" - echo " --rag=<skill>:<topic> flag (caller supplies vendor)" + echo " resolve the receiver from the workspace config" + echo " (workspace-config.sh --export -> WSCFG_<ROLE>_*);" + echo " a --<verb>=<skill>:<topic> flag stays an optional override" echo " - Replace private network IPs / internal domains with env-var" echo " contract (e.g., RAG_TARGET_URL)" echo " - Replace mcp__<vendor>__* tool names with abstract receiver" diff --git a/skills/hook-kit/resources/remind-rag-flag-on-skill.sh b/skills/hook-kit/resources/remind-rag-flag-on-skill.sh deleted file mode 100755 index ebc1d13c..00000000 --- a/skills/hook-kit/resources/remind-rag-flag-on-skill.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env bash -# PreToolUse:Skill — Remind to supply --rag=<skill>:<topic> when invoking -# a generic skill that exposes a --rag dispatch contract. -# -# Generic skills supporting --rag dispatch (whitelist): -# - cleanup:fa-prune (Section 8) -# - archive (any topic) (RAG dispatch section) -# - code-workflow (any) (RAG dispatch contract at steps.md) -# -# Behavior: -# - If invoking a whitelisted skill without --rag= in args → DENY with -# reminder. Caller can re-invoke with --rag=<skill>:<topic> or include -# 'no-rag-dispatch' in args to opt out for this call. -# -# Override keyword in args body: 'no-rag-dispatch' -# -# Exit codes: -# 0 = allow -# 2 = block + stderr reminder - -set -uo pipefail - -INPUT=$(cat) - -TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) -[[ "$TOOL_NAME" != "Skill" ]] && exit 0 - -SKILL=$(echo "$INPUT" | jq -r '.tool_input.skill // empty' 2>/dev/null) -ARGS=$(echo "$INPUT" | jq -r '.tool_input.args // empty' 2>/dev/null) - -[[ -z "$SKILL" ]] && exit 0 - -# Whitelist matching -is_whitelist=0 -case "$SKILL" in - cleanup) - if [[ "$ARGS" == fa-prune* ]] || [[ "$ARGS" == run* ]] || [[ -z "$ARGS" ]]; then is_whitelist=1; fi - ;; - archive|code-workflow) - is_whitelist=1 - ;; -esac - -[[ "$is_whitelist" -eq 0 ]] && exit 0 - -# --rag= present → allow -if echo "$ARGS" | grep -qE -- '--rag=[A-Za-z0-9_-]+:[A-Za-z0-9_-]+'; then - exit 0 -fi - -# Opt-out keyword in args → allow -if echo "$ARGS" | grep -q 'no-rag-dispatch'; then - exit 0 -fi - -# Whitelist + no --rag + no opt-out → DENY with reminder -{ - echo "DENIED: invoking generic skill with --rag dispatch contract but no --rag flag supplied." - echo "" - echo "Why blocked:" - echo " - Skill '$SKILL' (args: '$ARGS') is in the --rag dispatch whitelist" - echo " - skill-usage.md 'Generic skill invocation must auto-supply available vendor dispatch' rule (caller responsibility)" - echo " - Missing --rag means generated artifacts (research/plan/archived sections) won't be indexed to the RAG store" - echo "" - echo "Required action (pick one before retrying):" - echo " 1. Re-invoke with --rag=<receiver-skill>:<topic>" - echo " Example: Skill('cleanup', 'fa-prune --rag=es6kr:qdrant-import')" - echo " The receiver-skill is whichever RAG-store skill you have registered." - echo " 2. If the current environment has no RAG receiver (find/store tool absent)," - echo " include 'no-rag-dispatch' anywhere in the args to opt out for this call." - echo " Example: Skill('cleanup', 'fa-prune no-rag-dispatch')" - echo "" - echo "Whitelist (skills with --rag dispatch contract):" - echo " - cleanup:fa-prune" - echo " - archive (any topic)" - echo " - code-workflow (any topic)" -} >&2 -exit 2 diff --git a/skills/session/archive.md b/skills/session/archive.md index d94ce4c5..eee4fc51 100644 --- a/skills/session/archive.md +++ b/skills/session/archive.md @@ -54,27 +54,21 @@ If the session is the **currently active** session (Claude Code has it open), th Archive moves the JSONL out of the active project, so post-archive recall depends on whatever external index the caller wants populated (semantic index, full-text store, summary cache, etc.). This skill stays **vendor-agnostic** — it only declares the dispatch surface; implementations live in vendor skills. -#### Auto-supply is the default (HARD STOP) +#### The receiver comes from the workspace config -`--rag` is **not** an opt-in extra. When a RAG receiver is available in the environment, the caller **must auto-supply** `--rag=<skill>:<topic>`. File-move-only is correct **only** when no receiver is available **or** the user passed `--no-rag`. This mirrors the always-on caller-side dispatch rule (`skill-usage.md` caller-side "auto-supply available vendor dispatch" rule); archive executes it as a **procedure step**, not as caller discretion. +Resolve it with `bash <hook-kit-skill>/resources/workspace-config.sh --export`, then read the exported `WSCFG_RAG_*` values **before reporting the archive result**: -Run the 3-axis receiver scan **before reporting the archive result**: - -1. **MCP server** — is a vector-store tool registered (`mcp__<vendor>__*-store` / `*-find`)? → candidate vendor skill -2. **Skill registry** — does a skill expose a RAG-store topic (e.g. `es6kr:qdrant-import`)? (MCP absent does not mean receiver absent) -3. **Reachability** — does the receiver endpoint answer (`curl -m 8 <endpoint>/healthz`)? - -Branch: -- 0 receivers → file move only -- 1 receiver reachable → **auto-supply `--rag=<skill>:<topic>`** (do not ask, do not present it as optional) -- 2+ → AskUserQuestion to pick -- Uncertain → ask; do not silently skip +- `WSCFG_RAG_KIND` unset / `none` / resolver unavailable → **file move only, quietly**. An unconfigured role is a valid state, not a missing step +- `WSCFG_RAG_KIND` set → dispatch using `WSCFG_RAG_ENDPOINT` plus the matching `WSCFG_RAG_COLLECTION_*` +- `--no-rag` → skip regardless of the resolved binding +- `--rag=<skill>:<topic>` → explicit per-call override, wins over the resolved binding | # | Don't | Do | |---|-------|-----| -| 1 | Finish with file-move-only + "run with `--rag` if you want" while a receiver is available | Auto-supply `--rag` as the default. Offering it as optional = regression | -| 2 | Read the "abstract contract" framing and treat dispatch as skippable | "optional" describes the flag's omittability at the contract layer, not the caller's choice. With a receiver present, supply is mandatory | -| 3 | Skip the 3-axis scan because the archive script has no `--rag` flag | Script lacking the flag → do the dispatch manually after the move (see the qdrant-import receiver). The scan + dispatch is still mandatory | +| 1 | Finish with file-move-only while the config binds a receiver | A resolved binding means dispatch is part of the procedure, not caller discretion | +| 2 | Probe MCP tool lists or healthcheck endpoints to decide whether a receiver exists | Read the resolved binding. Guessing is what the config replaced | +| 3 | Skip dispatch because the archive script has no `--rag` flag | Script lacking the flag → dispatch manually after the move, using the resolved `WSCFG_RAG_*` values | +| 4 | Warn or block because no flag was passed | No flag is required. Absence of a binding is a quiet skip | #### Flag @@ -84,7 +78,7 @@ Branch: - `<skill>` — name of a registered skill that owns a RAG-store topic - `<topic>` — topic within that skill responsible for accepting archived content -- When the flag is omitted, archive performs file move only — but **callers are expected to auto-supply the flag** when a receiver is available in the environment (see `skill-usage.md` caller-side dispatch rule) +- The flag is an optional override. When it is omitted the receiver is resolved from the workspace config, and archive performs a file move only if that config binds none #### Contract for receivers (vendor skills implement this) @@ -98,9 +92,9 @@ Caller (this skill) passes payload via environment variables; receiver skill cho Receivers consult vendor-side documentation for their accepted metadata keys, chunking strategy, and idempotency rules. This skill does **not** define those — see the targeted skill's docs. #### Skip conditions - + - Session is dead (< 10 lines, no assistant response) — use `purge`, no archival value -- User explicitly omits `--rag` — file move only +- User explicitly passes `--no-rag` — file move only (omitted `--rag` resolves to workspace binding, which skips quietly if `kind: none`) - Caller-specified `<skill>:<topic>` not available in this environment — abort with a clear error; do not silently skip the move **Why abstract**: archived sessions stay readable via `Read`, but discoverability collapses (outside Claude Code's session list, no `/session search` reach). External indexing is the only content-level recall path. Naming a specific index vendor here would couple this generic skill to one environment's stack; the flag keeps the coupling at the call site. @@ -184,4 +178,4 @@ After restore, Claude Code's session list will pick it up on next refresh. 3. Has the destination path been previewed and confirmed not to already exist? 4. Did you preserve the UUID filename (no rename)? 5. Is the destination under `~/.claude/projects/.bak/` using the flat `<project-key>_<uuid>.jsonl` naming? -6. Is a RAG receiver available in this environment (3-axis scan: MCP store tool / skill RAG-store topic / endpoint reachable)? If yes, did you **auto-supply** `--rag=<skill>:<topic>`? File-move-only is correct only with no receiver or an explicit `--no-rag`. (This skill does not pick a default vendor, but auto-supply is mandatory when a receiver exists — see §2.5 "Auto-supply is the default".) +6. Is a RAG receiver configured in the workspace config (`WSCFG_RAG_KIND` set)? If yes, did you dispatch to the resolved receiver (or explicit `--rag` override)? File-move-only is correct only with `kind: none` or an explicit `--no-rag`. diff --git a/skills/session/classify.md b/skills/session/classify.md index 160bf96a..88fbefcc 100644 --- a/skills/session/classify.md +++ b/skills/session/classify.md @@ -169,8 +169,8 @@ mcp__serena__write_memory({ }) ``` -3. If `--rag` and a RAG MCP is detected (see Section 8), store the distilled knowledge - to RAG before archiving. +3. If `--rag` is passed and the workspace config binds a receiver (see Section 8), store the + distilled knowledge to that receiver before archiving. 4. Archive via the [`archive`](./archive.md) topic (same `archive-session.sh` as A — not hard delete) @@ -191,21 +191,22 @@ mcp__claude-sessions-mcp__clear_sessions({ **archived** via the `mv` procedure in §6-A, not bulk-deleted — they may still hold recoverable context. -### 8. RAG Save Recommendation (when a RAG / vector store MCP is available) +### 8. RAG Save Recommendation (when the workspace binds a RAG receiver) -**Trigger detection** — Skip this entire section if no RAG / vector store MCP is registered in the current context. Do not hard-wire to a specific vendor. +**Trigger detection** — resolve the receiver from the workspace bindings config rather than probing the environment for vendor tool names: -Detection patterns (any match qualifies — scan deferred tool list or system reminders): +```text +bash <hook-kit-skill>/resources/workspace-config.sh --export # exports WSCFG_RAG_* +``` + +| Resolved state | Behavior | +|---|---| +| `WSCFG_RAG_KIND` unset / `none` / resolver unavailable | Skip this entire section quietly | +| `WSCFG_RAG_KIND` set | Proceed, using `WSCFG_RAG_ENDPOINT` + the matching `WSCFG_RAG_COLLECTION_*` | -| Vendor | Tool name pattern | -|--------|-------------------| -| Qdrant | `mcp__qdrant__qdrant-store`, `mcp__qdrant__qdrant-find` | -| Chroma | `mcp__chroma__*-add`, `mcp__chroma__*-query` | -| Weaviate | `mcp__weaviate__*-store`, `mcp__weaviate__*-search` | -| Pinecone | `mcp__pinecone__*-upsert`, `mcp__pinecone__*-query` | -| Generic | Any MCP tool whose name matches `*-(store|add|upsert|index)` paired with `*-(find|query|search)` against a vector index | +Scanning the deferred-tool list for vendor-specific tool names is not a substitute: a receiver can be reachable over HTTP with no MCP binding at all, and naming vendors inside a generic skill is exactly what the portability rule forbids. The config is the single source of truth, so swapping vendors stays a one-line config edit. -If at least one RAG MCP is detected, evaluate every session classified as **B (Keep)** or **C (Extract then Delete)** for semantic-search value and emit an additional table. Sessions in category A (Delete Recommended) are excluded. +When a receiver resolves, evaluate every session classified as **B (Keep)** or **C (Extract then Delete)** for semantic-search value and emit an additional table. Sessions in category A (Delete Recommended) are excluded. #### Criteria — sessions worth saving to RAG diff --git a/skills/skill-kit/invoke-discipline.md b/skills/skill-kit/invoke-discipline.md index 72d7b172..458dd5c7 100644 --- a/skills/skill-kit/invoke-discipline.md +++ b/skills/skill-kit/invoke-discipline.md @@ -103,36 +103,32 @@ If any match, this rule applies. Single-topic skills (SKILL.md only, no topic fi | 2 | "Doesn't work in Bash, I'll do it manually" decision | `!` prefix = Claude Code feature for user to run interactive commands directly in session | | 3 | Enter manual procedure without user confirmation | AskUserQuestion: "script `!` run vs manual handling" — only enter manual path after confirmation | -## 5. Generic skill vendor dispatch auto-supply (HARD STOP) +## 5. Generic skill vendor dispatch resolution -When a generic skill exposes dispatch flags (`--<verb>=<skill>:<topic>` form), the caller (Claude) must auto-detect available environment receivers + supply them. Even if the user didn't explicitly type it, it's the caller's responsibility. +When a generic skill exposes dispatch flags (`--<verb>=<skill>:<topic>` form), the binding comes from the **workspace bindings config**, not from a caller-typed flag and not from an environment scan. Resolve it with `bash <hook-kit-skill>/resources/workspace-config.sh --export` and read the exported `WSCFG_<ROLE>_*` values. ### Don't / Do | # | Don't | Do | |---|-------|----| -| 1 | User didn't type the flag → skip dispatch | Detect available receivers → auto-supply. User explicit typing = receiver selection override | -| 2 | "Generic skill, so OK without flag" judgment | No flag invocation = information loss. If receiver available, dispatch is default | -| 3 | Receiver auto-dispatch judged as ambiguous user intent | Receiver registered in environment = intent stated. Auto-supply is safe | -| 4 | Multiple candidates → silently pick first without asking | Multiple candidates → AskUserQuestion for user decision | -| 5 | Receiver presence judged by MCP existence only → silent skip if absent | Receiver can operate as network endpoint without MCP. Scan all 3 axes: MCP + receiver topic + reachability. Uncertain = ask, not silent skip | +| 1 | Demand a flag, warn, or block because the caller omitted one | The flag is an optional per-call override. Its absence is never an error | +| 2 | Probe the environment (MCP tool list, endpoint healthchecks) to guess a receiver | Read the resolved binding. The config is the single source of truth | +| 3 | Read `kind: none` as "something is missing" | `kind: none` = the role is deliberately unconfigured → skip quietly and continue | +| 4 | Pick a vendor default when the config resolves none | No binding = no dispatch. The primary deliverable (the file write) still stands | +| 5 | Hardcode a vendor endpoint or collection name inside the generic skill | Keep vendor detail in the config so a swap is a one-line edit; the skill only consumes `WSCFG_*` | -### Auto-detection procedure (caller responsibility, 3-axis) +### Resolution procedure -Before calling generic skill: +Before calling a generic skill: -1. Grep calling target skill topic docs — confirm `--<verb>=<skill>:<topic>` or abstract dispatch contract pattern -2. Available receiver candidate 3-axis scan (check all): - - **MCP server**: `mcp__<vendor>__*` → vendor skill candidate - - **Skill registry**: whether receiver topic with dispatch protocol declared exists (MCP absent ≠ receiver absent) - - **Endpoint reachability**: healthcheck stated in receiver topic (`curl -m 6 <endpoint>/healthz`) +1. `bash <hook-kit-skill>/resources/workspace-config.sh --export` +2. Read `WSCFG_<ROLE>_KIND` for the role in question (`RAG`, `BACKLOG`, `CHECKLIST`, …) 3. Branch: - - 0 → omit flag - - 1 reachable confirmed → auto-supply - - Uncertain → AskUserQuestion (skip vs dispatch). Silent skip forbidden - - 2+ → AskUserQuestion for selection + - unset / `none` / resolver unavailable → skip quietly, no warning, no ask + - set → dispatch using the accompanying `WSCFG_<ROLE>_*` values + - caller passed an explicit `--<verb>=<skill>:<topic>` → that override wins -**Self-check**: dispatch flag exposure / 3-axis scan / branch decision / auto-supply default applied. +**Self-check**: resolver consulted / `kind: none` treated as a quiet skip / no environment guessing / no flag demanded of the caller. ## 6. Verify a skill's own skip/precondition checks before recommending its action (HARD STOP) diff --git a/skills/skill-kit/portability.md b/skills/skill-kit/portability.md index 5d360fd0..52720a15 100644 --- a/skills/skill-kit/portability.md +++ b/skills/skill-kit/portability.md @@ -63,8 +63,8 @@ This rule operates on a different axis from Rule A: A prevents dead references a | # | Don't | Do | |---|-------|-----| -| 1 | Hardcode internal URLs/IPs directly in a generic skill topic body | Declare only an abstract dispatch flag (`--rag=<skill>:<topic>`). Keep URLs inside the vendor skill | -| 2 | Explicitly call a vendor skill from a generic skill (e.g., `Skill("<vendor>", "...")`) | Accept it as a call-site parameter (e.g., `--rag=<skill>:<topic>` — the caller specifies the vendor) | +| 1 | Hardcode internal URLs/IPs directly in a generic skill topic body | Resolve the receiver from the workspace config (`workspace-config.sh --export` → `WSCFG_<ROLE>_*`). Keep URLs in the config and inside the vendor skill | +| 2 | Explicitly call a vendor skill from a generic skill (e.g., `Skill("<vendor>", "...")`) | Read the binding from the workspace config; accept a call-site flag (e.g., `--rag=<skill>:<topic>`) only as an optional override, never as a requirement | | 3 | Add a vendor-specific verification procedure to a generic skill | Declare only the abstract contract (let the receiver own its healthcheck); put actual verification in the vendor skill | | 4 | "Write vendor-specific code inside the generic skill and have the vendor call it" | Reverse the direction: the generic skill declares the interface, the vendor skill implements the receiver. Coupling belongs only in the call-site flag | | 5 | Write a new implementation in the generic skill when an existing dispatch pattern already exists in a vendor skill | Grep for existing dispatch patterns before editing — if found, follow that pattern |