elelem-skills 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f2f20ca978d891df63d961aa6addc64e332b8c714951c7175350676fbbd8ede
4
- data.tar.gz: d6e2469954a584ba74ead6eda9f53752115c9a4413a5788c9ae9991689c7d4a4
3
+ metadata.gz: 941ba03698ec16a9c0c6bffa098b25192cb633d1c22c34d8099400ff0baf87c7
4
+ data.tar.gz: 1656947d3135a5b4679d492d6c1912d1f6b5b2fefe3434eb7c5b36e19c34fbbb
5
5
  SHA512:
6
- metadata.gz: 114a7a7d6d8f5c98fef7fba0731fbbb3fd69a7d0fda9a135aa5d30baf9c0e8ccd9f9283c1a7102b1445b188d1977051e1bdc5034b0635f25542613451162ae1f
7
- data.tar.gz: d022633dc2203c10b1a7c72cb2c72877c4da3478e4a3bcfdb1990db10da0d61aab86c0c160d39219480757c8bdcf0081aa28260261902b99a484f2a219b0675b
6
+ metadata.gz: b53aabc68112a4164f942da764691458c6a488324a4ef58c4866977bb984d800f4b0aca0f29f75f6a72d10ee0b86b378ff64ab1e5f16166e7aac193232c2e3cb
7
+ data.tar.gz: deaf2dee82d9dd91ba1731d80311533ee8506973aa28023f4b9abb05e1c66cb6b60d246ea7c9b5f6ea0f40a9314669c39d657ed0f94874a803324de431608d35
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # elelem-skills
2
+
3
+ Loads [agent skills](https://github.com/anthropics/skills) files under an `.agents/skills/` directory and exposes them to an `elelem` agent.
data/Rakefile CHANGED
@@ -1,4 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- task default: %i[]
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: %i[spec]
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: scrum
3
+ description: Story-driven workflow for building a feature -- plan it into stories, design tasks, build test-first, review, then verify. Stories live in .agents/backlog/. Use when the user wants to work through a feature as a tracked, multi-step process rather than a single one-off change.
4
+ ---
5
+
6
+ # Scrum
7
+
8
+ A five-phase workflow, one story at a time, tracked as a markdown file in
9
+ `.agents/backlog/`. Each phase is a separate file in this skill's directory
10
+ -- read the one for the phase you're in.
11
+
12
+ # Phases
13
+
14
+ | Phase | File | When |
15
+ |---|---|---|
16
+ | Plan | [plan.md](plan.md) | Nothing is broken into stories yet |
17
+ | Design | [design.md](design.md) | A story exists but has no Tasks, or its Tasks are stale |
18
+ | Build | [build.md](build.md) | A story has Tasks and needs implementing |
19
+ | Review | [review.md](review.md) | A story's Tasks are all checked off |
20
+ | Verify | [verify.md](verify.md) | A story passed review |
21
+
22
+ Read only the phase file you need for the current request -- each one is
23
+ self-contained and says what to read the story for and what to write back.
24
+
25
+ # Story Lifecycle
26
+
27
+ A story file moves through the phases in order: plan creates it, design adds
28
+ Tasks, build implements and checks them off, review approves the diff, verify
29
+ confirms it works end-to-end and closes it out. Each phase only reads/writes
30
+ the story file and the code it describes -- nothing here tracks phases across
31
+ turns, so name the story explicitly once it exists (`.agents/backlog/NNN-*.md`)
32
+ rather than relying on this skill to remember where you left off.
@@ -0,0 +1,29 @@
1
+ Implement a story's tasks, one at a time, test-first.
2
+
3
+ # Role
4
+ - Work through the Tasks in the story the user specifies (see plan.md for the naming convention)
5
+ - Follow TDD: write a failing test, implement the minimal code to pass, then move on
6
+ - Check off each task in the story file as it's completed
7
+
8
+ # Process
9
+ 1. **Focus** - If the user didn't name a story, list .agents/backlog/ and stop -- do not guess which one
10
+ 2. **Read** - Load the story and its Tasks
11
+ 3. **Red** - Write a failing test for the next unchecked task
12
+ 4. **Green** - Write the minimal code to make it pass
13
+ 5. **Verify** - Run the test suite
14
+ 6. **Check** - Mark the task complete in the story file, then repeat from step 3 for the next task
15
+
16
+ # Task Completion
17
+ When a task is done, edit the story file:
18
+ ```markdown
19
+ # Tasks
20
+
21
+ * [x] Create FooService in lib/foo_service.rb ← mark done
22
+ * [ ] Add #bar method to handle X ← next task
23
+ ```
24
+
25
+ # Guidelines
26
+ - One task at a time, in order
27
+ - Minimal diffs -- only what the current task needs
28
+ - No defensive code or speculative abstraction
29
+ - Run the test suite after every change, not just at the end
@@ -0,0 +1,32 @@
1
+ Research the codebase and turn a story's intent into a concrete task list.
2
+
3
+ # Role
4
+ - Read the story from .agents/backlog/ (see plan.md for the naming convention)
5
+ - Explore the codebase to find where the change belongs and what it touches
6
+ - Fill in the story's Tasks section
7
+ - Note risks or dependencies in the story's DESCRIPTION or SEE ALSO
8
+
9
+ Do not write or edit application/test code in this skill -- only the story
10
+ file. That's what makes design safe to run before committing to an approach.
11
+
12
+ # Process
13
+ 1. **Review** - Read the story in .agents/backlog/
14
+ 2. **Explore** - Trace code paths, find the extension points and existing patterns to follow
15
+ 3. **Plan** - Break the story into small, ordered, testable tasks
16
+ 4. **Update** - Edit the story file's Tasks section
17
+
18
+ # Task Format
19
+ In the story's # Tasks section:
20
+ ```markdown
21
+ # Tasks
22
+
23
+ * [ ] Write a failing spec for FooService#bar in spec/foo_service_spec.rb
24
+ * [ ] Create FooService#bar in lib/foo_service.rb to pass it
25
+ * [ ] Update config/routes.rb to add the endpoint
26
+ ```
27
+
28
+ # Guidelines
29
+ - Tasks should be small, atomic, and independently testable
30
+ - Order by dependency (write the test before the implementation task it drives)
31
+ - Reference specific files to modify or create
32
+ - One task should map to roughly one red-green cycle during the build phase
@@ -0,0 +1,67 @@
1
+ Turn the user's request into a backlog of small, testable user stories.
2
+
3
+ # Role
4
+ - Interview the user before writing anything. Ask clarifying questions, one
5
+ turn at a time, until you are at least 95% sure you understand the
6
+ request -- personas, goals, edge cases, what's explicitly out of scope.
7
+ - Never make assumptions to fill a gap. If something is unclear or
8
+ unstated, ask about it instead of guessing.
9
+ - Break large requests into small, independently deliverable stories.
10
+ - Capture acceptance criteria in testable terms.
11
+ - Write each story to .agents/backlog/ as a separate file.
12
+
13
+ # Asking Questions
14
+ This is a chat REPL: each response you give ends your turn and the user's
15
+ next message continues the same conversation, so a real back-and-forth
16
+ interview works here. To ask, just respond with the question as plain text
17
+ and stop -- do not call a tool, do not write a story file yet. Ask one
18
+ focused question (or a short related group) at a time rather than a long
19
+ questionnaire in one message; that keeps the interview conversational and
20
+ lets the user's answer to one question inform the next.
21
+
22
+ (If you're running as `elelem ask` rather than `elelem chat`, there's no
23
+ next turn to read an answer from. Say so, then proceed with your best
24
+ understanding and mark the actual gaps as SEE ALSO items -- this is the
25
+ one situation where documenting an assumption is the only option.)
26
+
27
+ # Process
28
+ 1. **Read** - Take in the user's request; identify the distinct capabilities inside it.
29
+ 2. **Interview** - Ask clarifying questions, one exchange at a time, until you're at least 95% sure you understand each capability -- what it is, who it's for, and what's out of scope.
30
+ 3. **Scope** - Split into stories small enough to finish in one build session.
31
+ 4. **Document** - Write each as a story file (template below).
32
+ 5. **List** - After writing, list the story filenames and one-line summaries back to the user so they can redirect before build starts.
33
+
34
+ # Story Template
35
+ ```markdown
36
+ As a `[persona]`, I `[want to]`, so that `[goal]`.
37
+
38
+ # SYNOPSIS
39
+
40
+ <one-line summary>
41
+
42
+ # DESCRIPTION
43
+
44
+ <detailed explanation, informed by the interview -- not by assumptions>
45
+
46
+ # SEE ALSO
47
+
48
+ * [ ] <related files or concepts; not a place to park unanswered questions>
49
+
50
+ # Tasks
51
+
52
+ * [ ] TBD (filled in during the design phase, see design.md)
53
+
54
+ # Acceptance Criteria
55
+
56
+ * [ ] <testable criterion>
57
+ ```
58
+
59
+ # Naming Convention
60
+ Files: .agents/backlog/NNN-short-name.md (e.g., 001-user-login.md). NNN is
61
+ zero-padded and continues from the highest existing number in the directory.
62
+
63
+ # Guidelines
64
+ - One story per file
65
+ - Stories should be small enough to complete in one session
66
+ - Acceptance criteria must be objectively testable -- ask "how will we know this is done?"
67
+ - Don't write a story until the interview has resolved its open questions
@@ -1,25 +1,24 @@
1
- You are in review mode. Verify changes meet acceptance criteria.
1
+ Review code changes against a story's acceptance criteria.
2
2
 
3
3
  # Role
4
- - Review code changes against story acceptance criteria
4
+ - Review code changes against the story's acceptance criteria
5
5
  - Check test coverage
6
6
  - Identify bugs, security issues, and quality concerns
7
7
 
8
8
  # Process
9
- 1. **Context** - Read the story from .elelem/backlog/
9
+ 1. **Context** - Read the story from .agents/backlog/
10
10
  2. **Diff** - Run `git diff` to see changes
11
- 3. **Trace** - Read surrounding context
12
- 4. **Verify** - Check each acceptance criterion
13
- 5. **Report** - Summarize findings
11
+ 3. **Trace** - Read surrounding context for anything the diff touches
12
+ 4. **Verify** - Check each acceptance criterion against the actual code
13
+ 5. **Report** - Summarize findings in the format below
14
14
 
15
15
  # Review Checklist
16
- - [ ] All tasks in story are checked off
16
+ - [ ] All tasks in the story are checked off
17
17
  - [ ] Acceptance criteria are satisfied
18
18
  - [ ] Tests exist and pass
19
19
  - [ ] No logic errors or edge case bugs
20
20
  - [ ] No security vulnerabilities
21
21
  - [ ] No performance issues
22
- - [ ] SOLID principles followed
23
22
  - [ ] Code is readable and minimal
24
23
 
25
24
  # Output Format
@@ -40,5 +39,5 @@ Severity: critical | warning | nit
40
39
 
41
40
  # Guidelines
42
41
  - Be specific: cite file:line
43
- - Suggest fixes
44
- - Distinguish blocking from non-blocking issues
42
+ - Suggest fixes, not just problems
43
+ - Distinguish blocking issues from non-blocking ones
@@ -0,0 +1,38 @@
1
+ Smoke-test a finished feature end-to-end and record what you found.
2
+
3
+ # Role
4
+ - Run the feature as a user would, not by re-reading the code
5
+ - Walk through the happy path and at least one realistic error case
6
+ - Verify the actual behavior matches the story's intent
7
+ - Record demo notes in the story file
8
+
9
+ # Process
10
+ 1. **Setup** - Read the story in .agents/backlog/ to know what to demo
11
+ 2. **Execute** - Run the feature end-to-end (real commands, not a read-through)
12
+ 3. **Observe** - Note actual behavior, output, and any issues
13
+ 4. **Document** - Append demo notes to the story file (format below)
14
+ 5. **Report** - Summarize the result
15
+
16
+ # Demo Checklist
17
+ - [ ] Feature works as described in the story
18
+ - [ ] Happy path completes successfully
19
+ - [ ] At least one error case was tried and handled gracefully
20
+ - [ ] Actual output matches the story's acceptance criteria
21
+
22
+ # Story Update
23
+ Append to the story file:
24
+ ```markdown
25
+ # Demo Notes
26
+
27
+ Status: ACCEPTED | NEEDS WORK
28
+
29
+ Observations:
30
+ - <what was tested>
31
+ - <what worked>
32
+ - <what needs attention, if anything>
33
+ ```
34
+
35
+ # Guidelines
36
+ - Test from the user's perspective, not the developer's
37
+ - Try realistic scenarios, not just the exact example from the story
38
+ - Be honest about gaps -- NEEDS WORK is a valid, useful outcome
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module Skills
5
+ class Catalog
6
+ GEM_SKILLS = File.expand_path("builtin", __dir__).freeze
7
+ REMOTE_SKILLS = File.expand_path("~/.agents/remote-skills").freeze
8
+
9
+ LOAD_PATHS = [
10
+ GEM_SKILLS,
11
+ "~/.agents/skills",
12
+ ".agents/skills"
13
+ ].freeze
14
+
15
+ def initialize(load_paths: LOAD_PATHS, remote_skills: REMOTE_SKILLS)
16
+ @load_paths = load_paths
17
+ @remote_skills = remote_skills
18
+ end
19
+
20
+ def all
21
+ @all ||= discover
22
+ end
23
+
24
+ def find(name)
25
+ all.find { |skill| skill.name == name }
26
+ end
27
+
28
+ def index(limit: nil)
29
+ summaries = all.map(&:summary)
30
+ summaries = summaries.map { |text| truncate(text, limit) } if limit
31
+ summaries.join("\n")
32
+ end
33
+
34
+ private
35
+
36
+ def discover
37
+ seen = {}
38
+
39
+ @load_paths.each do |path|
40
+ scan_one_level(path, seen)
41
+ scan_remote(seen) if path == GEM_SKILLS && @remote_skills
42
+ end
43
+
44
+ seen.values
45
+ end
46
+
47
+ def scan_one_level(path, seen)
48
+ dir = File.expand_path(path)
49
+ return unless File.directory?(dir)
50
+
51
+ Dir["#{dir}/*/SKILL.md"].sort.each { |skill_md| load_into(skill_md, seen) }
52
+ end
53
+
54
+ def scan_remote(seen)
55
+ dir = File.expand_path(@remote_skills)
56
+ return unless File.directory?(dir)
57
+
58
+ Dir["#{dir}/*/*/SKILL.md"].sort.each { |skill_md| load_into(skill_md, seen) }
59
+ end
60
+
61
+ def load_into(skill_md, seen)
62
+ skill = SkillFile.load(skill_md)
63
+ return unless skill
64
+
65
+ seen[skill.name] = skill
66
+ rescue => e
67
+ Elelem.logger.warn("elelem-skills: failed to load #{skill_md}: #{e.message}")
68
+ end
69
+
70
+ def truncate(text, limit)
71
+ return text if text.length <= limit
72
+
73
+ "#{text[0, limit]}..."
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+ require "json"
6
+ require "net/http"
7
+ require "uri"
8
+
9
+ module Elelem
10
+ module Skills
11
+ class Fetcher
12
+ SCHEMA = "https://schemas.agentskills.io/discovery/0.2.0/schema.json"
13
+
14
+ FetchError = Class.new(StandardError)
15
+
16
+ def initialize(install_dir: Catalog::REMOTE_SKILLS, http: Net::HTTP)
17
+ @install_dir = install_dir
18
+ @http = http
19
+ end
20
+
21
+ # Fetches https://<origin>/.well-known/agent-skills/index.json
22
+ def fetch(origin)
23
+ base = normalize(origin)
24
+ index_url = URI.join(base, "/.well-known/agent-skills/index.json")
25
+ index = get_json(index_url)
26
+
27
+ unless index["$schema"] == SCHEMA
28
+ raise FetchError, "unrecognized index schema #{index["$schema"].inspect} at #{index_url} (expected #{SCHEMA})"
29
+ end
30
+
31
+ host_dir = File.join(@install_dir, sanitize(base.host))
32
+ (index["skills"] || []).filter_map { |entry| install(entry, index_url, host_dir) }
33
+ end
34
+
35
+ private
36
+
37
+ def normalize(origin)
38
+ origin = "https://#{origin}" unless origin.start_with?("http://", "https://")
39
+ URI.parse(origin)
40
+ end
41
+
42
+ def sanitize(host)
43
+ host.to_s.downcase.gsub(/[^a-z0-9.-]/, "_")
44
+ end
45
+
46
+ def install(entry, index_url, host_dir)
47
+ name, type, url, digest = entry.values_at("name", "type", "url", "digest")
48
+
49
+ unless type == "skill-md"
50
+ Elelem.logger.warn("elelem-skills: fetch: skipping #{name.inspect}, unsupported type #{type.inspect} (archives are not implemented)")
51
+ return nil
52
+ end
53
+
54
+ body = get_raw(URI.join(index_url, url))
55
+ verify!(name, body, digest)
56
+
57
+ dir = File.join(host_dir, sanitize(name))
58
+ FileUtils.mkdir_p(dir)
59
+ File.write(File.join(dir, "SKILL.md"), body)
60
+
61
+ name
62
+ end
63
+
64
+ def verify!(name, body, digest)
65
+ expected = digest.to_s.delete_prefix("sha256:")
66
+ actual = Digest::SHA256.hexdigest(body)
67
+ return if expected == actual
68
+
69
+ raise FetchError, "digest mismatch for #{name.inspect}: expected sha256:#{expected}, got sha256:#{actual}"
70
+ end
71
+
72
+ def get_json(uri)
73
+ JSON.parse(get_raw(uri))
74
+ rescue JSON::ParserError => e
75
+ raise FetchError, "invalid JSON at #{uri}: #{e.message}"
76
+ end
77
+
78
+ def get_raw(uri)
79
+ response = @http.get_response(uri)
80
+ raise FetchError, "GET #{uri} -> #{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
81
+
82
+ response.body
83
+ rescue FetchError
84
+ raise
85
+ rescue => e
86
+ raise FetchError, "GET #{uri} failed: #{e.class}: #{e.message}"
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module Skills
5
+ class Setup
6
+ SUMMARY_LIMIT = 400
7
+
8
+ def self.call(agent)
9
+ new(agent, Catalog.new).call
10
+ end
11
+
12
+ def initialize(agent, catalog)
13
+ @agent = agent
14
+ @catalog = catalog
15
+ end
16
+
17
+ def call
18
+ return if @catalog.all.empty?
19
+
20
+ register_tool
21
+ register_command
22
+ end
23
+
24
+ private
25
+
26
+ def register_tool
27
+ catalog = @catalog
28
+
29
+ @agent.toolbox.add("skill",
30
+ description: "Load a skill's instructions by name. Available skills:\n#{catalog.index(limit: SUMMARY_LIMIT)}",
31
+ params: { name: { type: "string", description: "the skill name" } },
32
+ required: ["name"]
33
+ ) do |a|
34
+ skill = catalog.find(a["name"])
35
+ next { error: "unknown skill: #{a["name"]}" } unless skill
36
+
37
+ { name: skill.name, dir: skill.dir, content: skill.body }
38
+ end
39
+ end
40
+
41
+ def register_command
42
+ catalog = @catalog
43
+ agent = @agent
44
+ completions = -> { catalog.all.map(&:name) }
45
+
46
+ agent.commands.register("skills", description: "List available skills, or `/skills fetch <origin>` to install from a remote index", completions: completions) do |arg|
47
+ if arg&.start_with?("fetch ")
48
+ fetch(agent, arg.delete_prefix("fetch ").strip)
49
+ elsif arg && !arg.empty?
50
+ skill = catalog.find(arg)
51
+ skill ? agent.output.say(skill.body) : agent.output.say("Unknown skill: #{arg}")
52
+ else
53
+ agent.output.say(catalog.index)
54
+ end
55
+ end
56
+ end
57
+
58
+ def fetch(agent, origin)
59
+ if origin.empty?
60
+ agent.output.say("Usage: /skills fetch <origin>")
61
+ return
62
+ end
63
+
64
+ names = Fetcher.new.fetch(origin)
65
+ agent.output.say(names.empty? ? "No skills installed from #{origin}." : "Installed: #{names.join(", ")}\nRestart elelem to pick them up (/reload does not currently re-register this gem's tool -- see README).")
66
+ rescue Fetcher::FetchError => e
67
+ agent.output.say(e.message, as: :error)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Elelem.configure do |config|
4
- config.setup(:skill) do |agent|
5
- agent.commands.register("skill", description: "Invoke a skill (not yet implemented)") do |_args|
6
- agent.output.say NotImplementedError.new("skills are not implemented yet").message
3
+ module Elelem
4
+ module Skills
5
+ Skill = Struct.new(:name, :description, :dir, :body) do
6
+ def summary
7
+ "#{name}: #{description}"
8
+ end
7
9
  end
8
10
  end
9
11
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module Skills
5
+ module SkillFile
6
+ def self.load(path)
7
+ parse(File.read(path), path: path)
8
+ end
9
+
10
+ def self.parse(text, path:)
11
+ frontmatter, body = split(text)
12
+ unless frontmatter
13
+ Elelem.logger.warn("elelem-skills: #{path} has no frontmatter, skipping")
14
+ return nil
15
+ end
16
+
17
+ attrs = YAML.safe_load(frontmatter)
18
+ name, description = attrs["name"], attrs["description"]
19
+ unless name && description
20
+ Elelem.logger.warn("elelem-skills: #{path} is missing name or description, skipping")
21
+ return nil
22
+ end
23
+
24
+ Skill.new(name, description, File.dirname(path), body.strip)
25
+ end
26
+
27
+ def self.split(text)
28
+ return [nil, text] unless text.start_with?("---\n")
29
+
30
+ _, frontmatter, body = text.split(/^---\s*$/, 3)
31
+ return [nil, text] unless frontmatter && body
32
+
33
+ [frontmatter, body]
34
+ end
35
+ private_class_method :split
36
+ end
37
+ end
38
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Elelem
4
4
  module Skills
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
data/lib/elelem/skills.rb CHANGED
@@ -1,6 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "elelem"
4
+ require "yaml"
4
5
 
5
6
  require_relative "skills/version"
6
7
  require_relative "skills/skill"
8
+ require_relative "skills/skill_file"
9
+ require_relative "skills/catalog"
10
+ require_relative "skills/fetcher"
11
+ require_relative "skills/setup"
12
+
13
+ Elelem.configure do |config|
14
+ config.setup(:skills) do |agent|
15
+ Elelem::Skills::Setup.call(agent)
16
+ end
17
+ end