voidx 1.0.0__py3-none-any.whl
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.
- voidx/__init__.py +3 -0
- voidx/agent/__init__.py +0 -0
- voidx/agent/agents.py +439 -0
- voidx/agent/attachments.py +235 -0
- voidx/agent/graph.py +463 -0
- voidx/agent/graph_components/__init__.py +1 -0
- voidx/agent/graph_components/compaction.py +268 -0
- voidx/agent/graph_components/permissions.py +139 -0
- voidx/agent/graph_components/run_loop.py +532 -0
- voidx/agent/graph_components/runtime.py +14 -0
- voidx/agent/graph_components/streaming.py +351 -0
- voidx/agent/graph_components/subagent.py +278 -0
- voidx/agent/graph_components/tool_execution.py +208 -0
- voidx/agent/runtime_context.py +368 -0
- voidx/agent/slash.py +466 -0
- voidx/agent/slash_components/__init__.py +1 -0
- voidx/agent/slash_components/code_ide.py +68 -0
- voidx/agent/slash_components/lsp.py +105 -0
- voidx/agent/slash_components/mcp.py +332 -0
- voidx/agent/slash_components/model.py +419 -0
- voidx/agent/slash_components/runtime.py +55 -0
- voidx/agent/slash_components/skills.py +94 -0
- voidx/agent/state.py +32 -0
- voidx/agent/task_state.py +278 -0
- voidx/agent/tool_filters.py +27 -0
- voidx/config.py +707 -0
- voidx/llm/__init__.py +0 -0
- voidx/llm/catalog.py +188 -0
- voidx/llm/compaction.py +267 -0
- voidx/llm/context.py +43 -0
- voidx/llm/instruction.py +220 -0
- voidx/llm/provider.py +312 -0
- voidx/llm/usage.py +341 -0
- voidx/lsp/__init__.py +30 -0
- voidx/lsp/client.py +259 -0
- voidx/lsp/config.py +172 -0
- voidx/lsp/detector.py +512 -0
- voidx/lsp/errors.py +19 -0
- voidx/lsp/manager.py +280 -0
- voidx/lsp/schema.py +179 -0
- voidx/lsp/service.py +103 -0
- voidx/main.py +154 -0
- voidx/mcp/__init__.py +33 -0
- voidx/mcp/client.py +458 -0
- voidx/mcp/manager.py +267 -0
- voidx/mcp/schema.py +112 -0
- voidx/mcp/tool.py +122 -0
- voidx/mcp_servers/__init__.py +1 -0
- voidx/mcp_servers/web.py +104 -0
- voidx/memory/__init__.py +0 -0
- voidx/memory/context_frames.py +188 -0
- voidx/memory/model_profiles.py +98 -0
- voidx/memory/runtime_state.py +240 -0
- voidx/memory/session.py +272 -0
- voidx/memory/store.py +245 -0
- voidx/memory/transcript.py +137 -0
- voidx/permission/__init__.py +28 -0
- voidx/permission/engine.py +430 -0
- voidx/permission/evaluate.py +114 -0
- voidx/permission/sandbox.py +280 -0
- voidx/permission/schema.py +24 -0
- voidx/permission/service.py +314 -0
- voidx/permission/wildcard.py +34 -0
- voidx/skills/__init__.py +18 -0
- voidx/skills/bundled/superpowers/receiving-code-review/SKILL.md +30 -0
- voidx/skills/bundled/superpowers/requesting-code-review/SKILL.md +27 -0
- voidx/skills/bundled/superpowers/systematic-debugging/SKILL.md +36 -0
- voidx/skills/bundled/superpowers/test-driven-development/SKILL.md +33 -0
- voidx/skills/bundled/superpowers/verification-before-completion/SKILL.md +31 -0
- voidx/skills/bundled/superpowers/writing-plans/SKILL.md +27 -0
- voidx/skills/policy.py +97 -0
- voidx/skills/registry.py +162 -0
- voidx/skills/schema.py +47 -0
- voidx/skills/service.py +199 -0
- voidx/tools/__init__.py +0 -0
- voidx/tools/agent.py +81 -0
- voidx/tools/base.py +86 -0
- voidx/tools/bash.py +105 -0
- voidx/tools/file_ops.py +193 -0
- voidx/tools/lsp.py +155 -0
- voidx/tools/registry.py +104 -0
- voidx/tools/repomap.py +238 -0
- voidx/tools/search.py +162 -0
- voidx/tools/task_status.py +57 -0
- voidx/tools/task_tracker.py +81 -0
- voidx/tools/todo.py +82 -0
- voidx/tools/web_content.py +357 -0
- voidx/tools/web_mcp.py +107 -0
- voidx/tools/webfetch.py +155 -0
- voidx/tools/websearch.py +276 -0
- voidx/ui/__init__.py +0 -0
- voidx/ui/app.py +1033 -0
- voidx/ui/app_components/__init__.py +1 -0
- voidx/ui/app_components/clipboard_image.py +245 -0
- voidx/ui/app_components/commands.py +18 -0
- voidx/ui/app_components/controls.py +29 -0
- voidx/ui/app_components/file_picker.py +115 -0
- voidx/ui/app_components/formatting.py +187 -0
- voidx/ui/app_components/git_changes.py +51 -0
- voidx/ui/app_components/rendering.py +1169 -0
- voidx/ui/browse.py +160 -0
- voidx/ui/capture.py +169 -0
- voidx/ui/code_ide.py +251 -0
- voidx/ui/commands.py +83 -0
- voidx/ui/console.py +381 -0
- voidx/ui/console_components/__init__.py +1 -0
- voidx/ui/console_components/formatting.py +96 -0
- voidx/ui/console_components/streaming.py +253 -0
- voidx/ui/diff.py +331 -0
- voidx/ui/dock.py +372 -0
- voidx/ui/dock_components/__init__.py +1 -0
- voidx/ui/dock_components/formatting.py +123 -0
- voidx/ui/dock_components/nodes.py +401 -0
- voidx/ui/dock_components/state.py +51 -0
- voidx/ui/event_components/__init__.py +1 -0
- voidx/ui/event_components/schema.py +249 -0
- voidx/ui/events.py +341 -0
- voidx/ui/session_changes.py +163 -0
- voidx/ui/startup.py +161 -0
- voidx/ui/transcript.py +148 -0
- voidx/ui/tree.py +316 -0
- voidx-1.0.0.dist-info/METADATA +59 -0
- voidx-1.0.0.dist-info/RECORD +126 -0
- voidx-1.0.0.dist-info/WHEEL +5 -0
- voidx-1.0.0.dist-info/entry_points.txt +2 -0
- voidx-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: receiving-code-review
|
|
3
|
+
description: Use when receiving review feedback, requested optimizations, or reviewer comments before implementing them.
|
|
4
|
+
triggers:
|
|
5
|
+
- review feedback
|
|
6
|
+
- code review feedback
|
|
7
|
+
- reviewer says
|
|
8
|
+
- feedback says
|
|
9
|
+
- review comment
|
|
10
|
+
- 优化点
|
|
11
|
+
- 审查意见
|
|
12
|
+
- 评审意见
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Receiving Code Review for voidx
|
|
16
|
+
|
|
17
|
+
Use this skill when the user or another reviewer gives feedback to implement.
|
|
18
|
+
|
|
19
|
+
Core rule: verify feedback against the codebase before changing code.
|
|
20
|
+
|
|
21
|
+
Workflow:
|
|
22
|
+
1. Read the full feedback.
|
|
23
|
+
2. Restate the concrete requested changes if needed.
|
|
24
|
+
3. Check the relevant code and tests.
|
|
25
|
+
4. Decide whether each item is correct for this codebase.
|
|
26
|
+
5. Push back with technical reasons when feedback is wrong or unnecessary.
|
|
27
|
+
6. If feedback is valid, implement one coherent item at a time.
|
|
28
|
+
7. Verify with targeted tests or commands before reporting.
|
|
29
|
+
|
|
30
|
+
Do not give performative agreement. Technical correctness and the user's intent decide what changes.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: requesting-code-review
|
|
3
|
+
description: Use after substantial implementation work, complex bug fixes, or before merging to request a focused review.
|
|
4
|
+
triggers:
|
|
5
|
+
- request review
|
|
6
|
+
- ask for review
|
|
7
|
+
- before merge
|
|
8
|
+
- pre-merge
|
|
9
|
+
- review this change
|
|
10
|
+
- 复核一下
|
|
11
|
+
- 合并前
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Requesting Code Review for voidx
|
|
15
|
+
|
|
16
|
+
Use this skill after substantial implementation work, complex bug fixes, or before merge.
|
|
17
|
+
|
|
18
|
+
In voidx, request review with `agent(review)` when available.
|
|
19
|
+
|
|
20
|
+
Review brief should include:
|
|
21
|
+
1. What changed.
|
|
22
|
+
2. Requirements or plan being checked.
|
|
23
|
+
3. Files changed or relevant diff range.
|
|
24
|
+
4. Verification already run.
|
|
25
|
+
5. Specific risks to inspect.
|
|
26
|
+
|
|
27
|
+
Act on review findings by severity. Fix correctness, security, and broken behavior before proceeding. Push back if the review is wrong and explain the evidence.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: systematic-debugging
|
|
3
|
+
description: Use when debugging bugs, failed tests, build failures, tracebacks, crashes, or unexpected behavior.
|
|
4
|
+
triggers:
|
|
5
|
+
- bug
|
|
6
|
+
- failed
|
|
7
|
+
- failure
|
|
8
|
+
- traceback
|
|
9
|
+
- error
|
|
10
|
+
- crash
|
|
11
|
+
- broken
|
|
12
|
+
- test failure
|
|
13
|
+
- build failure
|
|
14
|
+
- 报错
|
|
15
|
+
- 失败
|
|
16
|
+
- 异常
|
|
17
|
+
- 崩溃
|
|
18
|
+
- 排查
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Systematic Debugging for voidx
|
|
22
|
+
|
|
23
|
+
Use this skill before proposing or applying fixes for bugs, failed tests, build failures, crashes, or unexpected behavior.
|
|
24
|
+
|
|
25
|
+
Core rule: find the root cause before changing code.
|
|
26
|
+
|
|
27
|
+
Workflow:
|
|
28
|
+
1. Read the full error, traceback, logs, or failing assertion.
|
|
29
|
+
2. Reproduce the issue with the smallest reliable command or steps.
|
|
30
|
+
3. Check recent changes with read-only tools such as `grep`, `read`, and safe `bash`.
|
|
31
|
+
4. Form one concrete hypothesis from evidence.
|
|
32
|
+
5. Verify the hypothesis with a targeted command, diagnostic, or code read.
|
|
33
|
+
6. Only then make the smallest fix.
|
|
34
|
+
7. Run the reproduction command again and report the evidence.
|
|
35
|
+
|
|
36
|
+
For flaky or non-reproducible failures, gather more evidence instead of guessing.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-driven-development
|
|
3
|
+
description: Use before implementing features, bug fixes, refactors, or behavior changes.
|
|
4
|
+
triggers:
|
|
5
|
+
- implement
|
|
6
|
+
- feature
|
|
7
|
+
- bugfix
|
|
8
|
+
- refactor
|
|
9
|
+
- behavior change
|
|
10
|
+
- add support
|
|
11
|
+
- fix bug
|
|
12
|
+
- 实现
|
|
13
|
+
- 修复
|
|
14
|
+
- 重构
|
|
15
|
+
- 功能
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Test-Driven Development for voidx
|
|
19
|
+
|
|
20
|
+
Use this skill before writing production code for a feature, bug fix, refactor, or behavior change.
|
|
21
|
+
|
|
22
|
+
Core rule: write a test that fails for the intended reason before writing the implementation.
|
|
23
|
+
|
|
24
|
+
Workflow:
|
|
25
|
+
1. Identify the smallest behavior to prove.
|
|
26
|
+
2. Add or update a focused test.
|
|
27
|
+
3. Run the targeted test and confirm it fails for the expected reason.
|
|
28
|
+
4. Implement the smallest code change that makes the test pass.
|
|
29
|
+
5. Run the targeted test again.
|
|
30
|
+
6. Refactor only after the test is green.
|
|
31
|
+
7. Run the relevant broader test set before reporting completion.
|
|
32
|
+
|
|
33
|
+
Allowed exceptions: pure documentation, prompt-only edits, generated assets, or configuration-only changes. If you skip TDD for one of these, say why briefly.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verification-before-completion
|
|
3
|
+
description: Use before claiming work is complete, fixed, passing, ready, or safe to merge.
|
|
4
|
+
triggers:
|
|
5
|
+
- done
|
|
6
|
+
- complete
|
|
7
|
+
- fixed
|
|
8
|
+
- passing
|
|
9
|
+
- ready
|
|
10
|
+
- verify
|
|
11
|
+
- verified
|
|
12
|
+
- 完成
|
|
13
|
+
- 修好了
|
|
14
|
+
- 通过
|
|
15
|
+
- 验证
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Verification Before Completion for voidx
|
|
19
|
+
|
|
20
|
+
Use this skill before saying work is complete, fixed, passing, ready, or safe to merge.
|
|
21
|
+
|
|
22
|
+
Core rule: evidence before completion claims.
|
|
23
|
+
|
|
24
|
+
Checklist:
|
|
25
|
+
1. Identify the command or check that proves the claim.
|
|
26
|
+
2. Run the full relevant command in this turn.
|
|
27
|
+
3. Read the exit code and failure count.
|
|
28
|
+
4. If the check fails, report the actual failure and next step.
|
|
29
|
+
5. If the check passes, report the command and result.
|
|
30
|
+
|
|
31
|
+
Do not rely on earlier runs, assumptions, or subagent summaries without verifying the actual state.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: writing-plans
|
|
3
|
+
description: Use when turning a spec, requirements, or agreed design into an implementation plan before editing code.
|
|
4
|
+
triggers:
|
|
5
|
+
- implementation plan
|
|
6
|
+
- write a plan
|
|
7
|
+
- planning
|
|
8
|
+
- spec
|
|
9
|
+
- requirements
|
|
10
|
+
- 计划
|
|
11
|
+
- 实施方案
|
|
12
|
+
- 需求
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Writing Plans for voidx
|
|
16
|
+
|
|
17
|
+
Use this skill when the user has a spec, requirements, or agreed design and wants an implementation plan.
|
|
18
|
+
|
|
19
|
+
Plan structure:
|
|
20
|
+
1. Goal: one sentence.
|
|
21
|
+
2. Context: current code paths and constraints.
|
|
22
|
+
3. Files: exact files to create or modify.
|
|
23
|
+
4. Tasks: small ordered steps with checkboxes.
|
|
24
|
+
5. Tests: targeted commands and expected results.
|
|
25
|
+
6. Risks: edge cases, compatibility, and rollback concerns.
|
|
26
|
+
|
|
27
|
+
Keep plans executable by voidx: use exact paths, concrete commands, and voidx tool names. Do not force git worktrees or commits unless the user asked for them.
|
voidx/skills/policy.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Workflow skill activation policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class WorkflowSkillActivation:
|
|
10
|
+
name: str
|
|
11
|
+
reason: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
WORKFLOW_SKILL_PRIORITY = {
|
|
15
|
+
"systematic-debugging": 10,
|
|
16
|
+
"receiving-code-review": 20,
|
|
17
|
+
"writing-plans": 30,
|
|
18
|
+
"test-driven-development": 40,
|
|
19
|
+
"verification-before-completion": 50,
|
|
20
|
+
"requesting-code-review": 60,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def workflow_skill_activations(
|
|
25
|
+
user_text: str,
|
|
26
|
+
*,
|
|
27
|
+
agent: str = "",
|
|
28
|
+
task_intent: str | None = None,
|
|
29
|
+
interaction_mode: str | None = None,
|
|
30
|
+
) -> list[WorkflowSkillActivation]:
|
|
31
|
+
text = user_text.strip().lower()
|
|
32
|
+
agent_name = (agent or "").strip().lower()
|
|
33
|
+
intent = (task_intent or "").strip().lower()
|
|
34
|
+
mode = (interaction_mode or "").strip().lower()
|
|
35
|
+
activations: dict[str, WorkflowSkillActivation] = {}
|
|
36
|
+
|
|
37
|
+
def add(name: str, reason: str) -> None:
|
|
38
|
+
activations.setdefault(name, WorkflowSkillActivation(name=name, reason=reason))
|
|
39
|
+
|
|
40
|
+
if intent == "debug":
|
|
41
|
+
add("systematic-debugging", "debug intent")
|
|
42
|
+
add("verification-before-completion", "debug lifecycle")
|
|
43
|
+
|
|
44
|
+
if agent_name == "implement":
|
|
45
|
+
add("test-driven-development", "implement role")
|
|
46
|
+
add("verification-before-completion", "implement lifecycle")
|
|
47
|
+
elif intent == "implement":
|
|
48
|
+
add("test-driven-development", "implement intent")
|
|
49
|
+
add("verification-before-completion", "implement lifecycle")
|
|
50
|
+
|
|
51
|
+
if agent_name == "plan":
|
|
52
|
+
add("writing-plans", "plan role")
|
|
53
|
+
|
|
54
|
+
if intent == "review" and _contains_any(text, _REVIEW_FEEDBACK_TERMS):
|
|
55
|
+
add("receiving-code-review", "review feedback")
|
|
56
|
+
|
|
57
|
+
if intent == "design" and _contains_any(text, _PLAN_TERMS):
|
|
58
|
+
add("writing-plans", "planning intent")
|
|
59
|
+
|
|
60
|
+
if mode == "plan":
|
|
61
|
+
add("writing-plans", "plan mode")
|
|
62
|
+
|
|
63
|
+
return sorted(
|
|
64
|
+
activations.values(),
|
|
65
|
+
key=lambda item: (WORKFLOW_SKILL_PRIORITY.get(item.name, 999), item.name),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def workflow_skill_sort_key(name: str) -> tuple[int, str]:
|
|
70
|
+
return (WORKFLOW_SKILL_PRIORITY.get(name, 999), name)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _contains_any(text: str, terms: tuple[str, ...]) -> bool:
|
|
74
|
+
return any(term in text for term in terms)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
_REVIEW_FEEDBACK_TERMS = (
|
|
78
|
+
"review feedback",
|
|
79
|
+
"code review feedback",
|
|
80
|
+
"review comment",
|
|
81
|
+
"reviewer says",
|
|
82
|
+
"feedback says",
|
|
83
|
+
"优化点",
|
|
84
|
+
"审查意见",
|
|
85
|
+
"评审意见",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
_PLAN_TERMS = (
|
|
89
|
+
"implementation plan",
|
|
90
|
+
"write a plan",
|
|
91
|
+
"planning",
|
|
92
|
+
"spec",
|
|
93
|
+
"requirements",
|
|
94
|
+
"计划",
|
|
95
|
+
"实施方案",
|
|
96
|
+
"需求",
|
|
97
|
+
)
|
voidx/skills/registry.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Local skill discovery from SKILL.md files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from voidx.skills.schema import SkillDefinition, SkillMeta, SkillScope
|
|
9
|
+
|
|
10
|
+
SKILL_FILENAME = "SKILL.md"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def normalize_skill_name(name: str) -> str:
|
|
14
|
+
return name.strip().lower()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SkillParseError(ValueError):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SkillRegistry:
|
|
22
|
+
"""Discovers bundled, global, and project skills.
|
|
23
|
+
|
|
24
|
+
Search order is bundled first, then global, then project. Later sources
|
|
25
|
+
override earlier sources with the same skill name.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
workspace: str,
|
|
31
|
+
*,
|
|
32
|
+
bundled_dir: Path | None = None,
|
|
33
|
+
global_dir: Path | None = None,
|
|
34
|
+
project_dir: Path | None = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
self.workspace = Path(workspace).resolve()
|
|
37
|
+
self.bundled_dir = bundled_dir or (Path(__file__).resolve().parent / "bundled" / "superpowers")
|
|
38
|
+
self.global_dir = global_dir or (Path.home() / ".voidx" / "skills")
|
|
39
|
+
self.project_dir = project_dir or (self.workspace / ".voidx" / "skills")
|
|
40
|
+
|
|
41
|
+
def discover(self) -> list[SkillDefinition]:
|
|
42
|
+
skills: dict[str, SkillDefinition] = {}
|
|
43
|
+
for scope, root in (
|
|
44
|
+
("bundled", self.bundled_dir),
|
|
45
|
+
("global", self.global_dir),
|
|
46
|
+
("project", self.project_dir),
|
|
47
|
+
):
|
|
48
|
+
for skill in self._discover_root(root, scope):
|
|
49
|
+
skills[normalize_skill_name(skill.name)] = skill
|
|
50
|
+
return sorted(skills.values(), key=lambda item: item.name)
|
|
51
|
+
|
|
52
|
+
def get(self, name: str) -> SkillDefinition | None:
|
|
53
|
+
target = normalize_skill_name(name)
|
|
54
|
+
for skill in self.discover():
|
|
55
|
+
if normalize_skill_name(skill.name) == target:
|
|
56
|
+
return skill
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
def _discover_root(self, root: Path, scope: SkillScope) -> list[SkillDefinition]:
|
|
60
|
+
if not root.exists() or not root.is_dir():
|
|
61
|
+
return []
|
|
62
|
+
skills: list[SkillDefinition] = []
|
|
63
|
+
for skill_file in sorted(root.glob(f"*/{SKILL_FILENAME}")):
|
|
64
|
+
try:
|
|
65
|
+
skills.append(parse_skill_file(skill_file, scope=scope))
|
|
66
|
+
except SkillParseError:
|
|
67
|
+
continue
|
|
68
|
+
return skills
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_skill_file(path: Path, *, scope: SkillScope) -> SkillDefinition:
|
|
72
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
73
|
+
fields, body = _split_frontmatter(text)
|
|
74
|
+
name = str(fields.get("name") or path.parent.name).strip()
|
|
75
|
+
if not name:
|
|
76
|
+
raise SkillParseError(f"Skill at {path} has no name")
|
|
77
|
+
meta = SkillMeta(
|
|
78
|
+
name=name,
|
|
79
|
+
description=str(fields.get("description") or ""),
|
|
80
|
+
enabled=_coerce_bool(fields.get("enabled"), default=True),
|
|
81
|
+
triggers=[str(item).strip() for item in _coerce_list(fields.get("triggers")) if str(item).strip()],
|
|
82
|
+
scope=scope,
|
|
83
|
+
)
|
|
84
|
+
return SkillDefinition(meta=meta, path=path.resolve(), body=body.strip())
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _split_frontmatter(text: str) -> tuple[dict[str, Any], str]:
|
|
88
|
+
lines = text.splitlines()
|
|
89
|
+
if not lines or lines[0].strip() != "---":
|
|
90
|
+
return {}, text
|
|
91
|
+
|
|
92
|
+
end_index = None
|
|
93
|
+
for index, line in enumerate(lines[1:], start=1):
|
|
94
|
+
if line.strip() == "---":
|
|
95
|
+
end_index = index
|
|
96
|
+
break
|
|
97
|
+
if end_index is None:
|
|
98
|
+
raise SkillParseError("Unclosed frontmatter")
|
|
99
|
+
|
|
100
|
+
frontmatter = "\n".join(lines[1:end_index])
|
|
101
|
+
body = "\n".join(lines[end_index + 1:])
|
|
102
|
+
return _parse_frontmatter(frontmatter), body
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _parse_frontmatter(text: str) -> dict[str, Any]:
|
|
106
|
+
result: dict[str, Any] = {}
|
|
107
|
+
current_key: str | None = None
|
|
108
|
+
for raw_line in text.splitlines():
|
|
109
|
+
line = raw_line.rstrip()
|
|
110
|
+
stripped = line.strip()
|
|
111
|
+
if not stripped or stripped.startswith("#"):
|
|
112
|
+
continue
|
|
113
|
+
if stripped.startswith("- ") and current_key:
|
|
114
|
+
current = result.setdefault(current_key, [])
|
|
115
|
+
if isinstance(current, list):
|
|
116
|
+
current.append(_parse_scalar(stripped[2:].strip()))
|
|
117
|
+
continue
|
|
118
|
+
if ":" not in line:
|
|
119
|
+
continue
|
|
120
|
+
key, value = line.split(":", 1)
|
|
121
|
+
current_key = key.strip()
|
|
122
|
+
value = value.strip()
|
|
123
|
+
if not current_key:
|
|
124
|
+
continue
|
|
125
|
+
if value == "":
|
|
126
|
+
result[current_key] = []
|
|
127
|
+
else:
|
|
128
|
+
result[current_key] = _parse_scalar(value)
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _parse_scalar(value: str) -> Any:
|
|
133
|
+
value = value.strip()
|
|
134
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
|
|
135
|
+
value = value[1:-1]
|
|
136
|
+
lower = value.lower()
|
|
137
|
+
if lower in {"true", "yes", "on"}:
|
|
138
|
+
return True
|
|
139
|
+
if lower in {"false", "no", "off"}:
|
|
140
|
+
return False
|
|
141
|
+
if value.startswith("[") and value.endswith("]"):
|
|
142
|
+
inner = value[1:-1].strip()
|
|
143
|
+
if not inner:
|
|
144
|
+
return []
|
|
145
|
+
return [_parse_scalar(part.strip()) for part in inner.split(",")]
|
|
146
|
+
return value
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _coerce_bool(value: Any, *, default: bool) -> bool:
|
|
150
|
+
if value is None:
|
|
151
|
+
return default
|
|
152
|
+
if isinstance(value, bool):
|
|
153
|
+
return value
|
|
154
|
+
return str(value).strip().lower() not in {"false", "no", "off", "0"}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _coerce_list(value: Any) -> list[Any]:
|
|
158
|
+
if value is None:
|
|
159
|
+
return []
|
|
160
|
+
if isinstance(value, list):
|
|
161
|
+
return value
|
|
162
|
+
return [value]
|
voidx/skills/schema.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Types for local skill discovery and selection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
SkillScope = Literal["bundled", "global", "project"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SkillMeta(BaseModel):
|
|
15
|
+
name: str
|
|
16
|
+
description: str = ""
|
|
17
|
+
enabled: bool = True
|
|
18
|
+
triggers: list[str] = Field(default_factory=list)
|
|
19
|
+
scope: SkillScope = "project"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SkillDefinition(BaseModel):
|
|
23
|
+
meta: SkillMeta
|
|
24
|
+
path: Path
|
|
25
|
+
body: str
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def name(self) -> str:
|
|
29
|
+
return self.meta.name
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def source_dir(self) -> Path:
|
|
33
|
+
return self.path.parent
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SkillSelectionConfig(BaseModel):
|
|
37
|
+
enabled: set[str] = Field(default_factory=set)
|
|
38
|
+
disabled: set[str] = Field(default_factory=set)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SkillMatch(BaseModel):
|
|
42
|
+
skill: SkillDefinition
|
|
43
|
+
reason: str
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def name(self) -> str:
|
|
47
|
+
return self.skill.name
|
voidx/skills/service.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Runtime skill selection and instruction rendering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
|
|
8
|
+
from voidx.skills.policy import workflow_skill_activations, workflow_skill_sort_key
|
|
9
|
+
from voidx.skills.registry import SkillRegistry, normalize_skill_name
|
|
10
|
+
from voidx.skills.schema import SkillDefinition, SkillMatch, SkillSelectionConfig
|
|
11
|
+
|
|
12
|
+
_EXPLICIT_REF_RE = re.compile(r"(?<![\w.-])\$([A-Za-z0-9_.-]+)")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SkillService:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
registry: SkillRegistry,
|
|
19
|
+
*,
|
|
20
|
+
selection: SkillSelectionConfig | None = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
self._registry = registry
|
|
23
|
+
self._selection = selection or SkillSelectionConfig()
|
|
24
|
+
|
|
25
|
+
def list_skills(self) -> list[SkillDefinition]:
|
|
26
|
+
return self._registry.discover()
|
|
27
|
+
|
|
28
|
+
def get(self, name: str) -> SkillDefinition | None:
|
|
29
|
+
return self._registry.get(name)
|
|
30
|
+
|
|
31
|
+
def enabled_skills(self) -> list[SkillDefinition]:
|
|
32
|
+
return [skill for skill in self.list_skills() if self.is_enabled(skill)]
|
|
33
|
+
|
|
34
|
+
def is_enabled(self, skill: SkillDefinition) -> bool:
|
|
35
|
+
name = normalize_skill_name(skill.name)
|
|
36
|
+
if name in self._normalized(self._selection.disabled):
|
|
37
|
+
return False
|
|
38
|
+
if name in self._normalized(self._selection.enabled):
|
|
39
|
+
return True
|
|
40
|
+
return skill.meta.enabled
|
|
41
|
+
|
|
42
|
+
def select(
|
|
43
|
+
self,
|
|
44
|
+
user_text: str,
|
|
45
|
+
*,
|
|
46
|
+
agent: str = "",
|
|
47
|
+
task_intent: str | None = None,
|
|
48
|
+
interaction_mode: str | None = None,
|
|
49
|
+
limit: int = 5,
|
|
50
|
+
) -> list[SkillMatch]:
|
|
51
|
+
text = user_text.strip()
|
|
52
|
+
has_context = bool(agent or task_intent or interaction_mode)
|
|
53
|
+
if not text and not has_context:
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
skills = self.enabled_skills()
|
|
57
|
+
skills_by_name = {normalize_skill_name(skill.name): skill for skill in skills}
|
|
58
|
+
explicit = self._explicit_refs(text)
|
|
59
|
+
matches: list[SkillMatch] = []
|
|
60
|
+
seen: set[str] = set()
|
|
61
|
+
|
|
62
|
+
def add_match(skill: SkillDefinition | None, reason: str) -> None:
|
|
63
|
+
if skill is None:
|
|
64
|
+
return
|
|
65
|
+
name = normalize_skill_name(skill.name)
|
|
66
|
+
if name in seen:
|
|
67
|
+
return
|
|
68
|
+
seen.add(name)
|
|
69
|
+
matches.append(SkillMatch(skill=skill, reason=reason))
|
|
70
|
+
|
|
71
|
+
if explicit:
|
|
72
|
+
for name in sorted(explicit, key=workflow_skill_sort_key):
|
|
73
|
+
add_match(skills_by_name.get(name), "explicit")
|
|
74
|
+
for activation in workflow_skill_activations(
|
|
75
|
+
text,
|
|
76
|
+
agent=agent,
|
|
77
|
+
task_intent=task_intent,
|
|
78
|
+
interaction_mode=interaction_mode,
|
|
79
|
+
):
|
|
80
|
+
add_match(skills_by_name.get(normalize_skill_name(activation.name)), activation.reason)
|
|
81
|
+
return matches[:limit]
|
|
82
|
+
|
|
83
|
+
for activation in workflow_skill_activations(
|
|
84
|
+
text,
|
|
85
|
+
agent=agent,
|
|
86
|
+
task_intent=task_intent,
|
|
87
|
+
interaction_mode=interaction_mode,
|
|
88
|
+
):
|
|
89
|
+
add_match(skills_by_name.get(normalize_skill_name(activation.name)), activation.reason)
|
|
90
|
+
|
|
91
|
+
text_matches: list[SkillMatch] = []
|
|
92
|
+
lowered = text.lower()
|
|
93
|
+
for skill in skills:
|
|
94
|
+
if normalize_skill_name(skill.name) in seen:
|
|
95
|
+
continue
|
|
96
|
+
reason = self._match_reason(skill, lowered)
|
|
97
|
+
if reason:
|
|
98
|
+
text_matches.append(SkillMatch(skill=skill, reason=reason))
|
|
99
|
+
text_matches.sort(key=lambda match: workflow_skill_sort_key(match.name))
|
|
100
|
+
matches.extend(text_matches)
|
|
101
|
+
return matches[:limit]
|
|
102
|
+
|
|
103
|
+
def activation_summaries(
|
|
104
|
+
self,
|
|
105
|
+
user_text: str,
|
|
106
|
+
*,
|
|
107
|
+
agent: str = "",
|
|
108
|
+
task_intent: str | None = None,
|
|
109
|
+
interaction_mode: str | None = None,
|
|
110
|
+
limit: int = 5,
|
|
111
|
+
) -> list[str]:
|
|
112
|
+
return [
|
|
113
|
+
f"{match.name} ({match.reason})"
|
|
114
|
+
for match in self.select(
|
|
115
|
+
user_text,
|
|
116
|
+
agent=agent,
|
|
117
|
+
task_intent=task_intent,
|
|
118
|
+
interaction_mode=interaction_mode,
|
|
119
|
+
limit=limit,
|
|
120
|
+
)
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
def instructions_for(
|
|
124
|
+
self,
|
|
125
|
+
user_text: str,
|
|
126
|
+
*,
|
|
127
|
+
agent: str = "",
|
|
128
|
+
task_intent: str | None = None,
|
|
129
|
+
interaction_mode: str | None = None,
|
|
130
|
+
limit: int = 5,
|
|
131
|
+
) -> list[str]:
|
|
132
|
+
return [
|
|
133
|
+
self.render_instruction(match.skill)
|
|
134
|
+
for match in self.select(
|
|
135
|
+
user_text,
|
|
136
|
+
agent=agent,
|
|
137
|
+
task_intent=task_intent,
|
|
138
|
+
interaction_mode=interaction_mode,
|
|
139
|
+
limit=limit,
|
|
140
|
+
)
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def render_instruction(skill: SkillDefinition) -> str:
|
|
145
|
+
description = skill.meta.description.strip()
|
|
146
|
+
header = f"Skill instructions from: {skill.path}\nSkill: {skill.name}"
|
|
147
|
+
if description:
|
|
148
|
+
header += f"\nDescription: {description}"
|
|
149
|
+
return f"{header}\n\n{skill.body}".strip()
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _normalized(values: Iterable[str]) -> set[str]:
|
|
153
|
+
return {normalize_skill_name(value) for value in values}
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def _explicit_refs(text: str) -> set[str]:
|
|
157
|
+
return {normalize_skill_name(match.group(1)) for match in _EXPLICIT_REF_RE.finditer(text)}
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def _match_reason(skill: SkillDefinition, lowered_text: str) -> str:
|
|
161
|
+
name = normalize_skill_name(skill.name)
|
|
162
|
+
if _contains_phrase(lowered_text, name):
|
|
163
|
+
return "name"
|
|
164
|
+
|
|
165
|
+
for trigger in skill.meta.triggers:
|
|
166
|
+
normalized = trigger.strip().lower()
|
|
167
|
+
if normalized and normalized in lowered_text:
|
|
168
|
+
return f"trigger:{trigger}"
|
|
169
|
+
|
|
170
|
+
description_terms = _significant_terms(skill.meta.description)
|
|
171
|
+
if description_terms and sum(1 for term in description_terms if term in lowered_text) >= 2:
|
|
172
|
+
return "description"
|
|
173
|
+
return ""
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _contains_phrase(text: str, phrase: str) -> bool:
|
|
177
|
+
if not phrase:
|
|
178
|
+
return False
|
|
179
|
+
pattern = r"(?<![\w.-])" + re.escape(phrase) + r"(?![\w.-])"
|
|
180
|
+
return bool(re.search(pattern, text))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _significant_terms(text: str) -> set[str]:
|
|
184
|
+
stop = {
|
|
185
|
+
"with",
|
|
186
|
+
"from",
|
|
187
|
+
"that",
|
|
188
|
+
"this",
|
|
189
|
+
"when",
|
|
190
|
+
"into",
|
|
191
|
+
"using",
|
|
192
|
+
"skill",
|
|
193
|
+
"skills",
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
term
|
|
197
|
+
for term in re.findall(r"[A-Za-z][A-Za-z0-9_-]{3,}", text.lower())
|
|
198
|
+
if term not in stop
|
|
199
|
+
}
|
voidx/tools/__init__.py
ADDED
|
File without changes
|