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 +4 -4
- data/.rspec +1 -0
- data/README.md +3 -0
- data/Rakefile +5 -1
- data/lib/elelem/skills/builtin/scrum/SKILL.md +32 -0
- data/lib/elelem/skills/builtin/scrum/build.md +29 -0
- data/lib/elelem/skills/builtin/scrum/design.md +32 -0
- data/lib/elelem/skills/builtin/scrum/plan.md +67 -0
- data/lib/elelem/skills/{prompts/review.erb → builtin/scrum/review.md} +9 -10
- data/lib/elelem/skills/builtin/scrum/verify.md +38 -0
- data/lib/elelem/skills/catalog.rb +77 -0
- data/lib/elelem/skills/fetcher.rb +90 -0
- data/lib/elelem/skills/setup.rb +71 -0
- data/lib/elelem/skills/skill.rb +6 -4
- data/lib/elelem/skills/skill_file.rb +38 -0
- data/lib/elelem/skills/version.rb +1 -1
- data/lib/elelem/skills.rb +11 -0
- data/spec/elelem/skills/catalog_spec.rb +135 -0
- data/spec/elelem/skills/fetcher_spec.rb +158 -0
- data/spec/elelem/skills/setup_spec.rb +66 -0
- data/spec/elelem/skills/skill_file_spec.rb +59 -0
- data/spec/elelem/skills/skill_spec.rb +11 -0
- data/spec/fixtures/skills/no-description/SKILL.md +5 -0
- data/spec/fixtures/skills/tdd/SKILL.md +8 -0
- data/spec/spec_helper.rb +13 -0
- metadata +66 -8
- data/lib/elelem/skills/prompts/build.erb +0 -39
- data/lib/elelem/skills/prompts/design.erb +0 -41
- data/lib/elelem/skills/prompts/plan.erb +0 -59
- data/lib/elelem/skills/prompts/verify.erb +0 -39
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 941ba03698ec16a9c0c6bffa098b25192cb633d1c22c34d8099400ff0baf87c7
|
|
4
|
+
data.tar.gz: 1656947d3135a5b4679d492d6c1912d1f6b5b2fefe3434eb7c5b36e19c34fbbb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b53aabc68112a4164f942da764691458c6a488324a4ef58c4866977bb984d800f4b0aca0f29f75f6a72d10ee0b86b378ff64ab1e5f16166e7aac193232c2e3cb
|
|
7
|
+
data.tar.gz: deaf2dee82d9dd91ba1731d80311533ee8506973aa28023f4b9abb05e1c66cb6b60d246ea7c9b5f6ea0f40a9314669c39d657ed0f94874a803324de431608d35
|
data/.rspec
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--require spec_helper
|
data/README.md
ADDED
data/Rakefile
CHANGED
|
@@ -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
|
-
|
|
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 .
|
|
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
|
|
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
|
data/lib/elelem/skills/skill.rb
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
Elelem
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
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
|