Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "dd3ok-agent-skills-stash",
"name": "dd3ok-stash",
"owner": {
"name": "dd3ok",
"url": "https://github.com/dd3ok"
Expand All @@ -9,7 +9,7 @@
"name": "stash",
"source": "./adapters/claude-code",
"description": "Search and load local Agent Skills from a separate SKILL.md library on explicit request.",
"version": "0.1.0",
"version": "0.1.1",
"license": "MIT",
"strict": true
}
Expand Down
4 changes: 2 additions & 2 deletions .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "stash",
"version": "0.1.0",
"version": "0.1.1",
"description": "One explicit doorway to a local Agent Skills library—search by name or task and load only the selected SKILL.md.",
"author": {
"name": "dd3ok",
Expand All @@ -23,7 +23,7 @@
],
"skills": "./skills/",
"interface": {
"displayName": "Agent Skills Stash",
"displayName": "Stash",
"shortDescription": "Open local Agent Skills only when requested.",
"longDescription": "Keep a large local Agent Skills library behind one explicit entry point. Search by exact name or task, then load only the selected SKILL.md instructions and required resources.",
"developerName": "dd3ok",
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
run: npm ci

- name: Check generated artifacts
run: npm run build && npm run lint:artifacts
run: npm run lint:artifacts

- name: Enforce routing regression budgets
run: npm run bench
Expand Down
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@

## Unreleased

- Unify the public brand, package, repository, and marketplace identifiers as Stash.
- Reject partial-word compact-name matches that promoted unrelated short queries.
- Restore material routing for dense three-term descriptions at a regression-tested threshold.
- Derive generated Adapter versions from the package manifest.
- Fail validation on stale or orphaned generated Adapter files.
- Add source provenance metadata, attribution, and explicit source-scoped routing by ID, repository display name, or URL.
- Version the source-aware index and routing profile, including cursor invalidation and executable golden cases.
- Keep valid ID-less sources in distinct list-output groups.

## 0.1.0

- Publish as Agent Skills Stash while keeping `stash` as the invocation ID.
- Publish Stash with `stash` as the invocation ID.
- Add a read-only `StashCatalog` Module and CLI.
- Add deterministic exact lookup and evidence-gated lexical discovery.
- Add complete relevant result counts with cursor pagination.
Expand Down
4 changes: 2 additions & 2 deletions README.ko.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Agent Skills Stash
# Stash

[English](README.md) | 한국어

Agent Skills Stash는 `$stash`로 호출합니다. 자주 사용하지 않는
Stash는 `$stash`로 호출합니다. 자주 사용하지 않는
[`SKILL.md`](https://agentskills.io) 패키지를 제품의 기본 검색 경로 밖에
두고, 사용자가 명시적으로 호출하면 정확한 이름으로 스킬을 열거나 작업
내용으로 로컬 보관함을 검색합니다.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Agent Skills Stash
# Stash

English | [한국어](README.ko.md)

Agent Skills Stash, invoked as `$stash`, keeps infrequently used
Stash, invoked as `$stash`, keeps infrequently used
[`SKILL.md`](https://agentskills.io) packages outside the host's normal
discovery path. Invoke it explicitly to open a skill by exact name or search
the local library by task.
Expand Down
25 changes: 21 additions & 4 deletions adapters/antigravity/cli/scripts/stash.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8251,7 +8251,7 @@ async function acquireLock(lockPath) {
}

// src/internal/search.ts
var ROUTING_PROFILE_VERSION = 2;
var ROUTING_PROFILE_VERSION = 3;
var FIELD_WEIGHTS = {
name: 6,
alias: 6,
Expand Down Expand Up @@ -8380,6 +8380,23 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms)
score *= negativePenalty(prepared.record, queryTerms);
return { score, matchedTerms, matchedKinds, reasons };
}
function normalizedTerms(value) {
const normalized = normalizeText(value);
return normalized ? normalized.split(/\s+/u) : [];
}
function containsTermSequence(haystack, needle) {
if (needle.length === 0 || needle.length > haystack.length) {
return false;
}
return haystack.some(
(_, start) => start + needle.length <= haystack.length && needle.every((term, offset) => haystack[start + offset] === term)
);
}
function hasTermBoundaryMatch(left, right) {
const leftTerms = normalizedTerms(left);
const rightTerms = normalizedTerms(right);
return containsTermSequence(leftTerms, rightTerms) || containsTermSequence(rightTerms, leftTerms);
}
function hasPhraseMatch(record, query) {
const compactQuery = compactText(query);
if (!compactQuery) {
Expand All @@ -8389,7 +8406,7 @@ function hasPhraseMatch(record, query) {
return { matched: true, reason: { kind: "name", value: query } };
}
for (const alias of record.aliases) {
if (compactText(alias) === compactQuery || compactText(alias).includes(compactQuery)) {
if (compactText(alias) === compactQuery || hasTermBoundaryMatch(alias, query)) {
return { matched: true, reason: { kind: "alias", value: alias } };
}
}
Expand All @@ -8402,7 +8419,7 @@ function hasPhraseMatch(record, query) {
return { matched: true, reason: { kind: "source", value: source } };
}
}
if (compactText(record.name).includes(compactQuery) || compactQuery.includes(compactText(record.name))) {
if (hasTermBoundaryMatch(record.name, query)) {
return { matched: true, reason: { kind: "name", value: record.name } };
}
return { matched: false };
Expand All @@ -8429,7 +8446,7 @@ function classifyCandidate(prepared, totalRecords, frequenciesByDocument, query,
}
const denseDescriptionEvidence = scored.matchedTerms.size >= 3 && scored.matchedKinds.size === 1 && scored.matchedKinds.has("description");
const materialEvidence = scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) || denseDescriptionEvidence || queryTerms.length === 1 && highPriorityMatch(scored.matchedKinds) && !(scored.matchedKinds.size === 1 && scored.matchedKinds.has("description"));
const evidenceAdjustedThreshold = denseDescriptionEvidence ? materialScoreThreshold * 0.6 : scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) ? materialScoreThreshold * 0.75 : materialScoreThreshold;
const evidenceAdjustedThreshold = denseDescriptionEvidence ? materialScoreThreshold * 0.58 : scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) ? materialScoreThreshold * 0.75 : materialScoreThreshold;
if (materialEvidence && scored.score >= evidenceAdjustedThreshold) {
return {
record,
Expand Down
2 changes: 1 addition & 1 deletion adapters/antigravity/cli/skills/stash.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: stash
description: Search a separate local Agent Skills library by exact name, source, or task and load only the selected SKILL.md instructions. Use only when the user explicitly invokes `/stash` to open an exact stored skill, apply it to a task, list the library, list skills from an author or repository, or find every materially relevant stored skill. Do not invoke Stash implicitly for ordinary work.
---

# Agent Skills Stash
# Stash

Use the bundled CLI to search and read a separate local Agent Skills library. Keep skills intended for normal host discovery and all skill lifecycle management outside this workflow.

Expand Down
2 changes: 1 addition & 1 deletion adapters/antigravity/ide/skills/stash/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: stash
description: Search a separate local Agent Skills library by exact name, source, or task and load only the selected SKILL.md instructions. Use only when the user explicitly invokes `stash` to open an exact stored skill, apply it to a task, list the library, list skills from an author or repository, or find every materially relevant stored skill. Do not invoke Stash implicitly for ordinary work.
---

# Agent Skills Stash
# Stash

Use the bundled CLI to search and read a separate local Agent Skills library. Keep skills intended for normal host discovery and all skill lifecycle management outside this workflow.

Expand Down
25 changes: 21 additions & 4 deletions adapters/antigravity/ide/skills/stash/scripts/stash.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8251,7 +8251,7 @@ async function acquireLock(lockPath) {
}

// src/internal/search.ts
var ROUTING_PROFILE_VERSION = 2;
var ROUTING_PROFILE_VERSION = 3;
var FIELD_WEIGHTS = {
name: 6,
alias: 6,
Expand Down Expand Up @@ -8380,6 +8380,23 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms)
score *= negativePenalty(prepared.record, queryTerms);
return { score, matchedTerms, matchedKinds, reasons };
}
function normalizedTerms(value) {
const normalized = normalizeText(value);
return normalized ? normalized.split(/\s+/u) : [];
}
function containsTermSequence(haystack, needle) {
if (needle.length === 0 || needle.length > haystack.length) {
return false;
}
return haystack.some(
(_, start) => start + needle.length <= haystack.length && needle.every((term, offset) => haystack[start + offset] === term)
);
}
function hasTermBoundaryMatch(left, right) {
const leftTerms = normalizedTerms(left);
const rightTerms = normalizedTerms(right);
return containsTermSequence(leftTerms, rightTerms) || containsTermSequence(rightTerms, leftTerms);
}
function hasPhraseMatch(record, query) {
const compactQuery = compactText(query);
if (!compactQuery) {
Expand All @@ -8389,7 +8406,7 @@ function hasPhraseMatch(record, query) {
return { matched: true, reason: { kind: "name", value: query } };
}
for (const alias of record.aliases) {
if (compactText(alias) === compactQuery || compactText(alias).includes(compactQuery)) {
if (compactText(alias) === compactQuery || hasTermBoundaryMatch(alias, query)) {
return { matched: true, reason: { kind: "alias", value: alias } };
}
}
Expand All @@ -8402,7 +8419,7 @@ function hasPhraseMatch(record, query) {
return { matched: true, reason: { kind: "source", value: source } };
}
}
if (compactText(record.name).includes(compactQuery) || compactQuery.includes(compactText(record.name))) {
if (hasTermBoundaryMatch(record.name, query)) {
return { matched: true, reason: { kind: "name", value: record.name } };
}
return { matched: false };
Expand All @@ -8429,7 +8446,7 @@ function classifyCandidate(prepared, totalRecords, frequenciesByDocument, query,
}
const denseDescriptionEvidence = scored.matchedTerms.size >= 3 && scored.matchedKinds.size === 1 && scored.matchedKinds.has("description");
const materialEvidence = scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) || denseDescriptionEvidence || queryTerms.length === 1 && highPriorityMatch(scored.matchedKinds) && !(scored.matchedKinds.size === 1 && scored.matchedKinds.has("description"));
const evidenceAdjustedThreshold = denseDescriptionEvidence ? materialScoreThreshold * 0.6 : scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) ? materialScoreThreshold * 0.75 : materialScoreThreshold;
const evidenceAdjustedThreshold = denseDescriptionEvidence ? materialScoreThreshold * 0.58 : scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) ? materialScoreThreshold * 0.75 : materialScoreThreshold;
if (materialEvidence && scored.score >= evidenceAdjustedThreshold) {
return {
record,
Expand Down
2 changes: 1 addition & 1 deletion adapters/claude-code/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "stash",
"version": "0.1.0",
"version": "0.1.1",
"description": "Search and load local Agent Skills from a separate SKILL.md library on explicit request.",
"author": {
"name": "dd3ok",
Expand Down
2 changes: 1 addition & 1 deletion adapters/claude-code/skills/stash/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: Search a separate local Agent Skills library by exact name, source,
disable-model-invocation: true
---

# Agent Skills Stash
# Stash

Use the bundled CLI to search and read a separate local Agent Skills library. Keep skills intended for normal host discovery and all skill lifecycle management outside this workflow.

Expand Down
25 changes: 21 additions & 4 deletions adapters/claude-code/skills/stash/scripts/stash.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8251,7 +8251,7 @@ async function acquireLock(lockPath) {
}

// src/internal/search.ts
var ROUTING_PROFILE_VERSION = 2;
var ROUTING_PROFILE_VERSION = 3;
var FIELD_WEIGHTS = {
name: 6,
alias: 6,
Expand Down Expand Up @@ -8380,6 +8380,23 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms)
score *= negativePenalty(prepared.record, queryTerms);
return { score, matchedTerms, matchedKinds, reasons };
}
function normalizedTerms(value) {
const normalized = normalizeText(value);
return normalized ? normalized.split(/\s+/u) : [];
}
function containsTermSequence(haystack, needle) {
if (needle.length === 0 || needle.length > haystack.length) {
return false;
}
return haystack.some(
(_, start) => start + needle.length <= haystack.length && needle.every((term, offset) => haystack[start + offset] === term)
);
}
function hasTermBoundaryMatch(left, right) {
const leftTerms = normalizedTerms(left);
const rightTerms = normalizedTerms(right);
return containsTermSequence(leftTerms, rightTerms) || containsTermSequence(rightTerms, leftTerms);
}
function hasPhraseMatch(record, query) {
const compactQuery = compactText(query);
if (!compactQuery) {
Expand All @@ -8389,7 +8406,7 @@ function hasPhraseMatch(record, query) {
return { matched: true, reason: { kind: "name", value: query } };
}
for (const alias of record.aliases) {
if (compactText(alias) === compactQuery || compactText(alias).includes(compactQuery)) {
if (compactText(alias) === compactQuery || hasTermBoundaryMatch(alias, query)) {
return { matched: true, reason: { kind: "alias", value: alias } };
}
}
Expand All @@ -8402,7 +8419,7 @@ function hasPhraseMatch(record, query) {
return { matched: true, reason: { kind: "source", value: source } };
}
}
if (compactText(record.name).includes(compactQuery) || compactQuery.includes(compactText(record.name))) {
if (hasTermBoundaryMatch(record.name, query)) {
return { matched: true, reason: { kind: "name", value: record.name } };
}
return { matched: false };
Expand All @@ -8429,7 +8446,7 @@ function classifyCandidate(prepared, totalRecords, frequenciesByDocument, query,
}
const denseDescriptionEvidence = scored.matchedTerms.size >= 3 && scored.matchedKinds.size === 1 && scored.matchedKinds.has("description");
const materialEvidence = scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) || denseDescriptionEvidence || queryTerms.length === 1 && highPriorityMatch(scored.matchedKinds) && !(scored.matchedKinds.size === 1 && scored.matchedKinds.has("description"));
const evidenceAdjustedThreshold = denseDescriptionEvidence ? materialScoreThreshold * 0.6 : scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) ? materialScoreThreshold * 0.75 : materialScoreThreshold;
const evidenceAdjustedThreshold = denseDescriptionEvidence ? materialScoreThreshold * 0.58 : scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) ? materialScoreThreshold * 0.75 : materialScoreThreshold;
if (materialEvidence && scored.score >= evidenceAdjustedThreshold) {
return {
record,
Expand Down
4 changes: 2 additions & 2 deletions adapters/codex/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "stash",
"version": "0.1.0",
"version": "0.1.1",
"description": "One explicit doorway to a local Agent Skills library—search by name or task and load only the selected SKILL.md.",
"author": {
"name": "dd3ok",
Expand All @@ -23,7 +23,7 @@
],
"skills": "./skills/",
"interface": {
"displayName": "Agent Skills Stash",
"displayName": "Stash",
"shortDescription": "Open local Agent Skills only when requested.",
"longDescription": "Keep a large local Agent Skills library behind one explicit entry point. Search by exact name or task, then load only the selected SKILL.md instructions and required resources.",
"developerName": "dd3ok",
Expand Down
2 changes: 1 addition & 1 deletion adapters/codex/skills/stash/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: stash
description: Search a separate local Agent Skills library by exact name, source, or task and load only the selected SKILL.md instructions. Use only when the user explicitly invokes `$stash` to open an exact stored skill, apply it to a task, list the library, list skills from an author or repository, or find every materially relevant stored skill. Do not invoke Stash implicitly for ordinary work.
---

# Agent Skills Stash
# Stash

Use the bundled CLI to search and read a separate local Agent Skills library. Keep skills intended for normal host discovery and all skill lifecycle management outside this workflow.

Expand Down
2 changes: 1 addition & 1 deletion adapters/codex/skills/stash/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
interface:
display_name: "Agent Skills Stash"
display_name: "Stash"
short_description: "Search local Agent Skills by name, source, or task"
default_prompt: "Use $stash to list skills from a source, open an exact stored skill, or find every stored skill relevant to a task."

Expand Down
25 changes: 21 additions & 4 deletions adapters/codex/skills/stash/scripts/stash.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8251,7 +8251,7 @@ async function acquireLock(lockPath) {
}

// src/internal/search.ts
var ROUTING_PROFILE_VERSION = 2;
var ROUTING_PROFILE_VERSION = 3;
var FIELD_WEIGHTS = {
name: 6,
alias: 6,
Expand Down Expand Up @@ -8380,6 +8380,23 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms)
score *= negativePenalty(prepared.record, queryTerms);
return { score, matchedTerms, matchedKinds, reasons };
}
function normalizedTerms(value) {
const normalized = normalizeText(value);
return normalized ? normalized.split(/\s+/u) : [];
}
function containsTermSequence(haystack, needle) {
if (needle.length === 0 || needle.length > haystack.length) {
return false;
}
return haystack.some(
(_, start) => start + needle.length <= haystack.length && needle.every((term, offset) => haystack[start + offset] === term)
);
}
function hasTermBoundaryMatch(left, right) {
const leftTerms = normalizedTerms(left);
const rightTerms = normalizedTerms(right);
return containsTermSequence(leftTerms, rightTerms) || containsTermSequence(rightTerms, leftTerms);
}
function hasPhraseMatch(record, query) {
const compactQuery = compactText(query);
if (!compactQuery) {
Expand All @@ -8389,7 +8406,7 @@ function hasPhraseMatch(record, query) {
return { matched: true, reason: { kind: "name", value: query } };
}
for (const alias of record.aliases) {
if (compactText(alias) === compactQuery || compactText(alias).includes(compactQuery)) {
if (compactText(alias) === compactQuery || hasTermBoundaryMatch(alias, query)) {
return { matched: true, reason: { kind: "alias", value: alias } };
}
}
Expand All @@ -8402,7 +8419,7 @@ function hasPhraseMatch(record, query) {
return { matched: true, reason: { kind: "source", value: source } };
}
}
if (compactText(record.name).includes(compactQuery) || compactQuery.includes(compactText(record.name))) {
if (hasTermBoundaryMatch(record.name, query)) {
return { matched: true, reason: { kind: "name", value: record.name } };
}
return { matched: false };
Expand All @@ -8429,7 +8446,7 @@ function classifyCandidate(prepared, totalRecords, frequenciesByDocument, query,
}
const denseDescriptionEvidence = scored.matchedTerms.size >= 3 && scored.matchedKinds.size === 1 && scored.matchedKinds.has("description");
const materialEvidence = scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) || denseDescriptionEvidence || queryTerms.length === 1 && highPriorityMatch(scored.matchedKinds) && !(scored.matchedKinds.size === 1 && scored.matchedKinds.has("description"));
const evidenceAdjustedThreshold = denseDescriptionEvidence ? materialScoreThreshold * 0.6 : scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) ? materialScoreThreshold * 0.75 : materialScoreThreshold;
const evidenceAdjustedThreshold = denseDescriptionEvidence ? materialScoreThreshold * 0.58 : scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) ? materialScoreThreshold * 0.75 : materialScoreThreshold;
if (materialEvidence && scored.score >= evidenceAdjustedThreshold) {
return {
record,
Expand Down
Loading