Skip to content

feat: dynamic skill loading with fingerprint filtering and message rescue#105

Closed
kulvirgit wants to merge 1 commit intomainfrom
feat/dynamic-skill-loading
Closed

feat: dynamic skill loading with fingerprint filtering and message rescue#105
kulvirgit wants to merge 1 commit intomainfrom
feat/dynamic-skill-loading

Conversation

@kulvirgit
Copy link
Collaborator

Summary

  • Environment fingerprint detection — scans project files to detect tech stack (dbt, python, react, etc.) and tags skills accordingly
  • Partition + rescue — skills are partitioned by fingerprint tags; excluded skills are rescued back when the user's message contains a matching tag word (set intersection)
  • MessageContext side channel — passes latest user message text per-turn for rescue matching
  • Config-gated — behind experimental.dynamic_skills config flag, off by default (zero behavioral change without opt-in)

Files changed

File Change
src/altimate/fingerprint/index.ts New — project type detection via file pattern scanning
src/altimate/context/message-context.ts New — per-turn message text side channel
src/config/config.ts Add experimental.dynamic_skills boolean
src/session/prompt.ts Trigger fingerprint detection + set message context (gated)
src/skill/skill.ts Add tags field to skill schema, remove unused trigger field
src/tool/skill.ts Replace filterByFingerprint with partitionByFingerprint + rescueByMessage
test/altimate/fingerprint.test.ts 8 tests for fingerprint detection
test/altimate/skill-filtering.test.ts 25 tests for partition, rescue, and config gating

Test plan

  • bun test ./test/altimate/ — 47 tests pass
  • bunx tsc --noEmit — no new type errors
  • Manual: in a dbt project with dynamic_skills: true, verify dbt skills shown and react skills hidden
  • Manual: type "build a react dashboard" — verify react skills rescued back

@github-actions
Copy link

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

Comment on lines +1 to +5
// altimate_change start - side channel for per-turn user message text
// Follows the same pattern as Fingerprint (module-level cached state, get/set/clear)
export namespace MessageContext {
let current: string | undefined

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The global singletons for MessageContext and Fingerprint cause a race condition in concurrent sessions, leading to incorrect data being used when one session's data overwrites another's.
Severity: MEDIUM

Suggested Fix

Refactor MessageContext and Fingerprint to use a session-scoped or directory-scoped state management system, such as Instance.state(), which is used elsewhere in the codebase. This will ensure that context and fingerprint data are isolated per session, preventing race conditions.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/opencode/src/altimate/context/message-context.ts#L1-L5

Potential issue: The `MessageContext` and `Fingerprint` modules both use a module-level
singleton pattern for caching data (`MessageContext.set`/`get` and a `cached` variable
in `Fingerprint`). In a multi-session environment, when two sessions execute
concurrently, one session can overwrite the cached data of another during an `await`
operation. This race condition leads to sessions processing incorrect data, such as
using the wrong user message for skill selection or the wrong project fingerprint. This
behavior only occurs when the `experimental.dynamic_skills` feature flag is enabled.

Did we get this right? 👍 / 👎 to inform future reviews.

- Environment fingerprint partitions skills into included/excluded pools
- Per-turn message rescue: excluded skills rescued when user message
  contains matching tag words (set intersection)
- `MessageContext` side channel passes latest user message text
- Config-gated via `experimental.dynamic_skills` (off by default)
- Add 39 official skills from dbt-labs, Astronomer, Databricks, Snowflake
- Tag all 50 skills with user-facing terms (dbt, airflow, snowflake, etc.)
@kulvirgit kulvirgit force-pushed the feat/dynamic-skill-loading branch from 624a4b1 to 3788010 Compare March 11, 2026 05:29
Comment on lines 95 to 98
if (!skill) {
const available = await Skill.all().then((x) => Object.keys(x).join(", "))
const available = await Skill.all().then((s) => s.map((x) => x.name).join(", "))
throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The SkillTool's execute function bypasses dynamic skill filtering by looking up skills from the complete unfiltered list, allowing execution of disallowed skills.
Severity: MEDIUM

Suggested Fix

The execute function should validate that the requested skill params.name is present in the allAllowed list that is generated during the tool's initialization. This will enforce the filtering at execution time, not just at the display layer. The error message should also list skills from allAllowed instead of Skill.all().

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/opencode/src/tool/skill.ts#L95-L98

Potential issue: In `packages/opencode/src/tool/skill.ts`, the `SkillTool` filters
skills based on project context (`dynamic_skills`) to generate a list of allowed skills
(`displaySkills`) for the LLM. However, the `execute` function at line 95 retrieves the
skill using `Skill.get(params.name)`, which queries the complete, unfiltered list of all
skills. This bypasses the intended filtering, allowing an LLM to execute a skill that
was meant to be unavailable if it knows its name (e.g., from prior context or the error
message). This undermines the logic of the dynamic skill partitioning feature.

@github-actions
Copy link

This pull request has been automatically closed because it was not updated to meet our contributing guidelines within the 2-hour window.

Feel free to open a new pull request that follows our guidelines.

@github-actions github-actions bot closed this Mar 11, 2026
@kulvirgit kulvirgit deleted the feat/dynamic-skill-loading branch March 13, 2026 02:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant