Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Event-driven GitHub PR watching.
SkillScore100/100
Test results 100
3/3 safety checks passed
39%100%of test cases passed +61 pts
Passed 18 of 18 test cases with the skill, and 7 of 18 without it.
11 fixed7 passed either way0 still failing0 broke4 errored
One test taskCan you quickly check the current status and latest comments on pull request #42 in the acme/widgets repo? I don't need ongoing monitoring for it, just a one-time summary right now.
Adds every file the skill has, pinned so you can update it. No account needed.
pip install "decimalai>=0.15"
decimalai skills add sonichi-pr-watch
claude --plugin-dir "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agents"The same install from Python, for a notebook or a setup script.
pip install "decimalai>=0.15"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-pr-watch")
if result.exit_code:
raise SystemExit(result.problems)
print("Added. Start Claude Code with the command below.")claude --plugin-dir "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agents"Type these in a Claude Code session. No account or API key. It writes the skill and any files it bundles, and asks first when its safety rating isn't Passed.
/plugin marketplace add decimal-labs/decimalai-claude-plugins
/plugin install decimalai@decimalai/decimalai:install sonichi-pr-watchThe plugin needs python3 and curl on your machine.
No Python or account. Claude Code picks it up in its next session.
curl -fsSL --create-dirs -o .claude/skills/sonichi-pr-watch/SKILL.md https://app.decimal.ai/s/sonichi-pr-watch/SKILL.md.claude/skills/sonichi-pr-watch/SKILL.mdThis gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[claude-agent-sdk]"
decimalai skills add sonichi-pr-watchimport asyncio
from claude_agent_sdk import ClaudeAgentOptions
from decimalai.claude_agent_sdk import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
async def main():
question = "Hello!"
async with support.run(question) as run: # one user message, one run
async for message in run.query(options=ClaudeAgentOptions(max_turns=6)):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())Prefer its own loader? ClaudeAgentOptions(plugins=[skills_plugin()], skills="all") reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[claude-agent-sdk]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-pr-watch")
if result.exit_code:
raise SystemExit(result.problems)
import asyncio
from claude_agent_sdk import ClaudeAgentOptions
from decimalai.claude_agent_sdk import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
async def main():
question = "Hello!"
async with support.run(question) as run: # one user message, one run
async for message in run.query(options=ClaudeAgentOptions(max_turns=6)):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())Prefer its own loader? ClaudeAgentOptions(plugins=[skills_plugin()], skills="all") reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-pr-watch/SKILL.mdimport asyncio
from pathlib import Path
from claude_agent_sdk import ClaudeAgentOptions, query
skill = Path(".agents/skills/sonichi-pr-watch/SKILL.md").read_text()
options = ClaudeAgentOptions(system_prompt=f"You are a helpful agent.\n\n{skill}", max_turns=6)
async def main():
async for message in query(prompt="Hello!", options=options):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai-agents]"
decimalai skills add sonichi-pr-watchfrom agents import Agent, Runner
from decimalai.openai_agents import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
name=support.name,
instructions=support.instructions, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")Using SandboxAgent? capabilities=[Shell(), decimalai.openai_agents.sandbox_skills()] points its own loader at the same folder.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai-agents]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-pr-watch")
if result.exit_code:
raise SystemExit(result.problems)
from agents import Agent, Runner
from decimalai.openai_agents import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
name=support.name,
instructions=support.instructions, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")Using SandboxAgent? capabilities=[Shell(), decimalai.openai_agents.sandbox_skills()] points its own loader at the same folder.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-pr-watch/SKILL.mdfrom pathlib import Path
from agents import Agent, Runner
skill = Path(".agents/skills/sonichi-pr-watch/SKILL.md").read_text()
agent = Agent(name="support", instructions=f"You are a helpful agent.\n\n{skill}")
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai]"
decimalai skills add sonichi-pr-watchimport json
from openai import OpenAI
from decimalai.openai import agent_config
client = OpenAI()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your tools, as Chat Completions takes them, and what answers each.
TOOLS = [{"type": "function", "function": {
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
# run.system is your prompt, then the skill list.
messages = [{"role": "system", "content": run.system}, run.message]
while True:
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=[*TOOLS, *run.tools], # your tools, then load_skill
).choices[0].message
messages.append(reply)
if not reply.tool_calls:
break
for call in reply.tool_calls:
# load_skill is answered by the run; your own tools by FUNCTIONS.
function = FUNCTIONS.get(call.function.name)
messages.append(run.tool_result(call) or {
"role": "tool", "tool_call_id": call.id,
"content": function(**json.loads(call.function.arguments)) if function
else f"There is no tool named {call.function.name!r}.",
})
print(reply.content)
# The Responses API takes run.response_tools instead of run.tools.The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-pr-watch")
if result.exit_code:
raise SystemExit(result.problems)
import json
from openai import OpenAI
from decimalai.openai import agent_config
client = OpenAI()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your tools, as Chat Completions takes them, and what answers each.
TOOLS = [{"type": "function", "function": {
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
# run.system is your prompt, then the skill list.
messages = [{"role": "system", "content": run.system}, run.message]
while True:
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=[*TOOLS, *run.tools], # your tools, then load_skill
).choices[0].message
messages.append(reply)
if not reply.tool_calls:
break
for call in reply.tool_calls:
# load_skill is answered by the run; your own tools by FUNCTIONS.
function = FUNCTIONS.get(call.function.name)
messages.append(run.tool_result(call) or {
"role": "tool", "tool_call_id": call.id,
"content": function(**json.loads(call.function.arguments)) if function
else f"There is no tool named {call.function.name!r}.",
})
print(reply.content)
# The Responses API takes run.response_tools instead of run.tools.No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-pr-watch/SKILL.mdfrom pathlib import Path
from openai import OpenAI
client = OpenAI()
skill = Path(".agents/skills/sonichi-pr-watch/SKILL.md").read_text()
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=[
{"role": "system", "content": f"You are a helpful agent.\n\n{skill}"},
{"role": "user", "content": "Hello!"},
],
).choices[0].messageThis gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[anthropic]"
decimalai skills add sonichi-pr-watchimport anthropic
from decimalai.anthropic import agent_config
client = anthropic.Anthropic()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
question = "Hello!"
with support.run(question) as run: # one user message, one run
answer = client.beta.messages.tool_runner(
model="claude-haiku-5-5",
max_tokens=4096,
system=run.system, # your prompt, then the skill list
tools=[*run.tools], # your tools, then load_skill
messages=[{"role": "user", "content": question}],
).until_done()The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[anthropic]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-pr-watch")
if result.exit_code:
raise SystemExit(result.problems)
import anthropic
from decimalai.anthropic import agent_config
client = anthropic.Anthropic()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
question = "Hello!"
with support.run(question) as run: # one user message, one run
answer = client.beta.messages.tool_runner(
model="claude-haiku-5-5",
max_tokens=4096,
system=run.system, # your prompt, then the skill list
tools=[*run.tools], # your tools, then load_skill
messages=[{"role": "user", "content": question}],
).until_done()No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-pr-watch/SKILL.mdfrom pathlib import Path
import anthropic
client = anthropic.Anthropic()
skill = Path(".agents/skills/sonichi-pr-watch/SKILL.md").read_text()
reply = client.messages.create(
model="claude-haiku-5-5",
max_tokens=4096,
system=f"You are a helpful agent.\n\n{skill}",
messages=[{"role": "user", "content": "Hello!"}],
)This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[google-genai]"
decimalai skills add sonichi-pr-watchfrom google import genai
from google.genai import types
from decimalai.google_genai import agent_config
client = genai.Client()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your functions, as declarations for the model, and what answers each.
TOOLS = [{"function_declarations": [{
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters_json_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}]}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
config = types.GenerateContentConfig(
system_instruction=run.system, # your prompt, then the skill list
tools=[*TOOLS, *run.tool_params], # your functions, then load_skill
)
contents = [{"role": "user", "parts": [{"text": run.message}]}]
while True:
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config,
)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = []
for call in response.function_calls:
# load_skill is answered by the run; your own functions by FUNCTIONS.
# A loop of your own rather than automatic function calling, which
# raises KeyError when the model asks for a function you did not pass.
function = FUNCTIONS.get(call.name)
parts.append(run.tool_result(call) or {"function_response": {
"name": call.name, "id": call.id,
"response": {"result": function(**(call.args or {})) if function
else f"There is no function named {call.name!r}."},
}})
contents.append({"role": "user", "parts": parts})
print(response.text)The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[google-genai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-pr-watch")
if result.exit_code:
raise SystemExit(result.problems)
from google import genai
from google.genai import types
from decimalai.google_genai import agent_config
client = genai.Client()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your functions, as declarations for the model, and what answers each.
TOOLS = [{"function_declarations": [{
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters_json_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}]}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
config = types.GenerateContentConfig(
system_instruction=run.system, # your prompt, then the skill list
tools=[*TOOLS, *run.tool_params], # your functions, then load_skill
)
contents = [{"role": "user", "parts": [{"text": run.message}]}]
while True:
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config,
)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = []
for call in response.function_calls:
# load_skill is answered by the run; your own functions by FUNCTIONS.
# A loop of your own rather than automatic function calling, which
# raises KeyError when the model asks for a function you did not pass.
function = FUNCTIONS.get(call.name)
parts.append(run.tool_result(call) or {"function_response": {
"name": call.name, "id": call.id,
"response": {"result": function(**(call.args or {})) if function
else f"There is no function named {call.name!r}."},
}})
contents.append({"role": "user", "parts": parts})
print(response.text)No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-pr-watch/SKILL.mdfrom pathlib import Path
from google import genai
from google.genai import types
client = genai.Client()
skill = Path(".agents/skills/sonichi-pr-watch/SKILL.md").read_text()
response = client.models.generate_content(
model="gemini-3.5-flash",
contents="Hello!",
config=types.GenerateContentConfig(system_instruction=f"You are a helpful agent.\n\n{skill}"),
)This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[langchain]" langchain-openai
decimalai skills add sonichi-pr-watchfrom langchain.agents import create_agent
from decimalai.langchain import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = create_agent(
"openai:gpt-5.4-mini",
tools=[], # your tools; the middleware adds load_skill
middleware=[support.middleware], # your prompt, the skill list and load_skill
)
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[langchain]" langchain-openaifrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-pr-watch")
if result.exit_code:
raise SystemExit(result.problems)
from langchain.agents import create_agent
from decimalai.langchain import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = create_agent(
"openai:gpt-5.4-mini",
tools=[], # your tools; the middleware adds load_skill
middleware=[support.middleware], # your prompt, the skill list and load_skill
)
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-pr-watch/SKILL.mdfrom pathlib import Path
from langchain.agents import create_agent
skill = Path(".agents/skills/sonichi-pr-watch/SKILL.md").read_text()
agent = create_agent("openai:gpt-5.4-mini", tools=[], system_prompt=f"You are a helpful agent.\n\n{skill}")
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})This gets SKILL.md only. The skill's 1 other file is not included.
CrewAI's own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" "crewai>=1.15.27"
decimalai skills add sonichi-pr-watchimport decimalai.crewai
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=decimalai.crewai.skills(), # the installed folder
tools=decimalai.crewai.skill_tools(), # reads a skill's files, runs its scripts
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()The same install from Python, for a notebook or a setup script. CrewAI's own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" "crewai>=1.15.27"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-pr-watch")
if result.exit_code:
raise SystemExit(result.problems)
import decimalai.crewai
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=decimalai.crewai.skills(), # the installed folder
tools=decimalai.crewai.skill_tools(), # reads a skill's files, runs its scripts
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()No DecimalAI package. CrewAI's own loader reads the folder, and nothing pins or updates it.
.agents/skills/sonichi-pr-watch/SKILL.mdfrom pathlib import Path
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=[Path(".agents/skills")],
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[adk]"
decimalai skills add sonichi-pr-watchfrom decimalai.adk import DecimalAIPlugin, agent_config
from google.adk.agents import LlmAgent
from google.adk.apps import App
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = LlmAgent(
name="support", # ADK needs a Python identifier
model="gemini-3.5-flash",
instruction=support.instruction, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# DecimalAIPlugin makes each invocation of runners built from this App one trace.
app = App(name="support", root_agent=agent, plugins=[DecimalAIPlugin()])Prefer ADK's own loader? SkillToolset(skills=decimalai.adk.skills(), ...) reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[adk]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-pr-watch")
if result.exit_code:
raise SystemExit(result.problems)
from decimalai.adk import DecimalAIPlugin, agent_config
from google.adk.agents import LlmAgent
from google.adk.apps import App
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = LlmAgent(
name="support", # ADK needs a Python identifier
model="gemini-3.5-flash",
instruction=support.instruction, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# DecimalAIPlugin makes each invocation of runners built from this App one trace.
app = App(name="support", root_agent=agent, plugins=[DecimalAIPlugin()])Prefer ADK's own loader? SkillToolset(skills=decimalai.adk.skills(), ...) reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-pr-watch/SKILL.mdfrom pathlib import Path
from google.adk.agents import LlmAgent
skill = Path(".agents/skills/sonichi-pr-watch/SKILL.md").read_text()
agent = LlmAgent(
name="support",
model="gemini-3.5-flash",
instruction=lambda context: f"You are a helpful agent.\n\n{skill}",
)This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[pydantic-ai]"
decimalai skills add sonichi-pr-watchfrom pydantic_ai import Agent
from decimalai.pydantic_ai import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
"openai:gpt-4o-mini",
name=support.name,
tools=[], # your tools; the capability adds load_skill
capabilities=[support], # your prompt, the skill list and load_skill
)
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")On the Pydantic AI harness, decimalai.pydantic_ai_harness.skills(project) uses its own loader instead.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[pydantic-ai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-pr-watch")
if result.exit_code:
raise SystemExit(result.problems)
from pydantic_ai import Agent
from decimalai.pydantic_ai import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
"openai:gpt-4o-mini",
name=support.name,
tools=[], # your tools; the capability adds load_skill
capabilities=[support], # your prompt, the skill list and load_skill
)
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")On the Pydantic AI harness, decimalai.pydantic_ai_harness.skills(project) uses its own loader instead.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-pr-watch/SKILL.mdfrom pathlib import Path
from pydantic_ai import Agent
skill = Path(".agents/skills/sonichi-pr-watch/SKILL.md").read_text()
agent = Agent("openai:gpt-4o-mini", instructions=f"You are a helpful agent.\n\n{skill}")
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")This gets SKILL.md only. The skill's 1 other file is not included.
Deep Agents' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" deepagents
decimalai skills add sonichi-pr-watchimport decimalai.deepagents
from deepagents import create_deep_agent
agent = create_deep_agent(model=model, **decimalai.deepagents.skills())
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The default backend reads files but has no shell. To run a skill's scripts, pass skills(backend=LocalShellBackend(...)).
The same install from Python, for a notebook or a setup script. Deep Agents' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" deepagentsfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-pr-watch")
if result.exit_code:
raise SystemExit(result.problems)
import decimalai.deepagents
from deepagents import create_deep_agent
agent = create_deep_agent(model=model, **decimalai.deepagents.skills())
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The default backend reads files but has no shell. To run a skill's scripts, pass skills(backend=LocalShellBackend(...)).
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-pr-watch/SKILL.mdfrom pathlib import Path
from deepagents import create_deep_agent
skill = Path(".agents/skills/sonichi-pr-watch/SKILL.md").read_text()
agent = create_deep_agent(model=model, system_prompt=f"You are a helpful agent.\n\n{skill}")
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})This gets SKILL.md only. The skill's 1 other file is not included.
Its own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" agent-framework
decimalai skills add sonichi-pr-watchimport asyncio
from agent_framework import Agent
from decimalai.agent_framework import skills
agent = Agent(client=client, instructions="You are a helpful agent.", context_providers=[skills()])
asyncio.run(agent.run("Hello!"))It caches skills by default, so restart the process after decimalai skills update.
The same install from Python, for a notebook or a setup script. Its own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" agent-frameworkfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-pr-watch")
if result.exit_code:
raise SystemExit(result.problems)
import asyncio
from agent_framework import Agent
from decimalai.agent_framework import skills
agent = Agent(client=client, instructions="You are a helpful agent.", context_providers=[skills()])
asyncio.run(agent.run("Hello!"))It caches skills by default, so restart the process after decimalai skills update.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-pr-watch/SKILL.mdimport asyncio
from pathlib import Path
from agent_framework import Agent
skill = Path(".agents/skills/sonichi-pr-watch/SKILL.md").read_text()
agent = Agent(client=client, instructions=f"You are a helpful agent.\n\n{skill}")
asyncio.run(agent.run("Hello!"))This gets SKILL.md only. The skill's 1 other file is not included.
Strands' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" strands-agents
decimalai skills add sonichi-pr-watchfrom decimalai.strands import skills
from strands import Agent
from strands.vended_tools import file_editor, shell
agent = Agent(
model=model, # always pass one: the default is Amazon Bedrock
system_prompt="You are a helpful agent.",
tools=[file_editor, shell],
plugins=[skills()],
)
agent("Hello!")Strands loads an agent's skills once, so build the agent again after decimalai skills update.
The same install from Python, for a notebook or a setup script. Strands' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" strands-agentsfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-pr-watch")
if result.exit_code:
raise SystemExit(result.problems)
from decimalai.strands import skills
from strands import Agent
from strands.vended_tools import file_editor, shell
agent = Agent(
model=model, # always pass one: the default is Amazon Bedrock
system_prompt="You are a helpful agent.",
tools=[file_editor, shell],
plugins=[skills()],
)
agent("Hello!")Strands loads an agent's skills once, so build the agent again after decimalai skills update.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-pr-watch/SKILL.mdfrom pathlib import Path
from strands import Agent
skill = Path(".agents/skills/sonichi-pr-watch/SKILL.md").read_text()
agent = Agent(model=model, system_prompt=f"You are a helpful agent.\n\n{skill}")
agent("Hello!")This gets SKILL.md only. The skill's 1 other file is not included.
Adds every file the skill has, pinned so you can update it. Cursor, Copilot, Cline and Warp read this folder by themselves.
pip install "decimalai>=0.15"
decimalai skills add sonichi-pr-watchNo Python or account. Cursor, Copilot, Cline and Warp read this folder by themselves.
curl -fsSL --create-dirs -o .agents/skills/sonichi-pr-watch/SKILL.md https://app.decimal.ai/s/sonichi-pr-watch/SKILL.md.agents/skills/sonichi-pr-watch/SKILL.mdThis gets SKILL.md only. The skill's 1 other file is not included.
Give its text to your model as instructions. No account or package needed.
This gets SKILL.md only. The skill's 1 other file is not included.
Raw files: /s/sonichi-pr-watch/SKILL.md · /s/sonichi-pr-watch.json
Fetch the file and give its text to your model as instructions. No account or package needed.
curl -fsSL https://app.decimal.ai/s/sonichi-pr-watch/SKILL.mdcurl -fsSL https://app.decimal.ai/s/sonichi-pr-watch.jsonThis gets SKILL.md only. The skill's 1 other file is not included.
You don't need a DecimalAI account.
Saved prompts and skills, pinned versions, and traces of this skill in your runs. DecimalAI does not run your agent.
Claude Code reads files from your project, so DecimalAI config does not apply there.
On Claude Agent SDK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On OpenAI Agents: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On OpenAI SDK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Anthropic: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Gemini API: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On LangChain: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On CrewAI: set DECIMAL_API_KEY and add decimalai.init(crewai=True) to record runs and skill use. The skill version stays in your repo.
On Google ADK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Pydantic AI: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Deep Agents: set DECIMAL_API_KEY and add decimalai.init(langchain=True) to record runs and skill use. The skill version stays in your repo.
On Microsoft Agent Framework: set DECIMAL_API_KEY and add decimalai.init(otel=True, agent_name="support") to record runs and skill use. The skill version stays in your repo.
On Strands: set DECIMAL_API_KEY and add decimalai.init(otel=True, agent_name="support") to record runs and skill use. The skill version stays in your repo.
Editors read files from your project, so DecimalAI config does not apply there.
Anywhere else: record a use with decimalai.log_skill_activation() inside a trace.
Watches GitHub pull requests by event, not by polling. A Claude Code mod (a plugin of function
hooks) starts pr_watch.py run when the session starts; the runner keeps one
gh webhook forward per watched repo and prints one line per relevant event. Nothing polls
GitHub, and nothing expires the way a Monitor does at 30 minutes.
How an event reaches the session depends on whether a turn is running:
$.prompt.submit), which starts a turn.context,
which the model reads right after that tool's output). Events still held when the turn ends
are submitted together as one prompt. Subagents' tool calls never carry them.Not for one-off checks of a single PR: read it with gh pr view instead.
Per watched repo (config.json), filtered by pr_watch.py:
| Event | Forwarded when | Line |
|---|---|---|
pull_request opened, synchronize, reopened, ready_for_review, closed | the PR author is a watched --author (* = anyone) | PR_EVENT o/r#N <action> author=… head=<sha8> merged=true|false reason=author |
pull_request synchronize, reopened, ready_for_review, closed | the watch has --reviewed and one of the --me logins has submitted a review on the PR, whoever wrote it | PR_EVENT o/r#N <action> author=… head=<sha8> merged=… reason=reviewed |
pull_request review_requested | the requested reviewer is one of the --me logins, on any PR in the repo | PR_EVENT o/r#N review_requested reviewer=… author=… head=… |
pull_request_review submitted | on a watched author's PR, by someone other than the --me logins (the owner's own review echoing back is dropped, though it still marks the PR reviewed) | PR_EVENT o/r#N review_submitted by=… state=approved|changes_requested|commented author=… head=… |
pull_request_review dismissed | the dismissed review is by one of the --me logins, on any PR in the repo | PR_EVENT o/r#N review_dismissed reviewer=… author=… head=… |
An event matching both the author rule and the reviewed rule yields one line, reason=author.
The webhook payload does not list a PR's reviewers, so the reviewed rule asks
GET repos/{repo}/pulls/{n}/reviews (see Costs). When that call fails or times out (10 s), the
event is forwarded anyway with reason=reviewed reviewed=unknown, never dropped.
Only logins, numbers, states and short shas reach the prompt; titles and bodies never do, so a
PR's text cannot steer the session. The agent reads the PR itself when it acts. (The runner does
store each PR's title in tracker.json for the tracker page's About column; see Tracker.)
gh, signed in, with admin on every watched repo (creating a webhook needs it).cli/gh-webhook extension. Installing it is a download, so ask the owner first, then
gh extension install cli/gh-webhook. Without it the runner prints a PR_WATCH_NOTE and exits.python3 (standard library only).S=skills/pr-watch/scripts/pr_watch.py
python3 $S add --repo acme/widgets --author alice --author bob --me owner-login
python3 $S add --repo acme/gadgets --me owner-login --reviewed # every PR owner-login reviewed
python3 $S install-mod --mods-dir "<the session's mods folder>"
install-mod writes the mod into <mods-dir>/pr-watch/ and copies the runner (and
manifest.json, for its settings) to <workspace>/state/pr-watch/, so the forwarder keeps working when the engine
checkout changes branch or updates. Re-run it after updating this skill.
Loading the mod is the owner's one step: enable hot reload of the mods folder in the
session's terminal (one click), or start the session with claude --plugin-dir <mods-dir>/pr-watch.
Never enable it for them.
Other commands:
python3 $S list # the watch config
python3 $S remove --repo acme/widgets
python3 $S status # runner, forwarders, recorded hook ids
python3 $S cleanup # delete the hook ids this skill recorded (refused while a runner is active; --force)
python3 $S tracker-kind --kind html-<id> --room '!room:server' # the page a session publish wrote to
add and remove signal a running runner (SIGHUP), which starts or stops that repo's
forwarder without a reload.
The watches live in <workspace>/state/pr-watch/config.json, written by add and remove:
{
"watches": [{"repo": "acme/widgets", "authors": ["alice"], "me": ["owner-login"], "reviewed": true}],
"settings": {"PR_WATCH_TRACKER_ROOM": "!room:server"}
}
reviewed (set by add --reviewed) needs at least one me login other than *.
The skill's settings are declared, with their defaults, in the config block of
manifest.json (see skills/MANIFEST.md, "Config-only manifests"):
| Key | Default | Meaning |
|---|---|---|
PR_WATCH_PROMPT | the standing ask below | the template each event's prompt is built from: {event} becomes the event line(s), {track} the tracker hint (empty when no tracker room is set) |
PR_WATCH_TRACKER_ROOM | empty: no tracker | the Commons room the tracker page is published to, e.g. !room:server |
PR_WATCH_TRACKER_TITLE | PR watch | the tracker page's title |
PR_WATCH_COMMONS_SCRIPT | skills/room-commons/scripts/room_commons.py | the room-commons CLI; a relative path is resolved against the repo root (from pr_watch.py's own location, never the cwd) |
PR_WATCH_TRACKER_PUBLISH | empty, meaning cli | cli: track publishes through the room-commons CLI and hands the page back only when that fails; session: track always hands the page back for the session to publish with its own room tools (see Tracker) |
Each is read most specific first: CLI flag > env var > manifest config > config.json
settings, and an empty value at any layer counts as unset. Set a host's value without editing
the skill:
python3 $S config set PR_WATCH_TRACKER_ROOM '!room:server' # the config.json layer
python3 $S config get # every key, its value and which layer set it
Because the manifest layer ranks above config.json, config set only takes effect for a key whose
manifest default is empty (PR_WATCH_TRACKER_ROOM, PR_WATCH_TRACKER_PUBLISH); for the others it says the manifest still wins,
and the override is an env var. config set signals a running runner, which re-sends the template.
The default PR_WATCH_PROMPT tells the agent to re-review at the new head, review what is
requested from the owner, note merges and closes, tell the owner only when something needs them,
and, when a tracker room is set, record the outcome with track. The runner is the one place the
template is resolved: it prints PR_WATCH_PROMPT_TEMPLATE <json string> on start and on every
SIGHUP, and the mod builds each prompt from the last template it received (before the first
arrives, it sends the bare event lines).
State dir. resolve_workspace finds the workspace from the checkout. From an engine checkout
that is the real workspace; from a fresh clone with no workspace symlink it is
<clone>/workspace, which no running agent reads. There, pass --state-dir <dir> to every command
(before the subcommand), or run the engine checkout's copy.
An optional HTML page in a Room Commons room listing every tracked PR: PR (link), about, author,
state, head reviewed, verdict, last event, updated. Open PRs come first, then the most recently
updated. About is the agent's --about, else the PR title the runner saw, cut at 90 characters
with the full text on hover. Every PR with an open ask is listed in a Needs you panel above
the table, with its link, about text and the whole ask inline, and is marked "needs you" in the
table. The page says "Generated by pr-watch; edits here are overwritten."; it is
rewritten whole on each publish. It is readable in light and dark mode and at phone width.
Setup is one setting:
python3 $S config set PR_WATCH_TRACKER_ROOM '!room:server'
After acting on an event, the agent records the outcome:
python3 $S track --pr acme/widgets#7 --author alice --state open --head a1b2c3d4 \
--verdict approved --event "re-reviewed at the new head"
--pr OWNER/REPO#N is required; --state open|merged|closed, --verdict approved|changes_requested|commented|none, --author, --head, --event, --link (default
the PR's GitHub URL) and --about (at most 300 characters) update only what is given. --ask TEXT records what the owner must decide (the question, the recommendation and the options, at
most 600 characters); --ask-clear removes it, and a PR that is merged or closed drops its ask
by itself. --room and --title override the settings.
track upserts the row (keyed repo#n, stamped updated_at) in
<workspace>/state/pr-watch/tracker.json, then:
No room set: it only records, and says so in one line.
Room set, PR_WATCH_TRACKER_PUBLISH unset or cli: it renders the page and publishes it
through the room-commons CLI, run with this python and a 60 s timeout. The first publish runs
page-add --kind html and records the page's html-<id> kind in tracker.json; later ones
check the page is still listed (pages), read its text and replace the whole of it. A page
that is gone, or a different room, gets a new page.
Hand-back. When that publish fails (no credential, an identity that is not a member of the
room, a timeout, an edit racing the replace, a missing room-commons script), or when
PR_WATCH_TRACKER_PUBLISH=session, track keeps the row, writes the rendered page atomically
to <workspace>/state/pr-watch/tracker.html, prints
PR_WATCH_PUBLISH room=<room> kind=<html-id|new> path="<abs path>" title="<title>"
(path and title JSON-quoted, since either can hold spaces) and exits 0. The session then
publishes the file with its own room tools: room.commons.pages.replace with mode whole on
that kind, or room.commons.pages.create (kind html, that title, the file as body) when
kind=new or the page is gone, and records the page it wrote to:
python3 $S tracker-kind --kind html-<id> --room '!room:server'
so later hand-backs name the same page. The event prompt's {track} hint tells the agent all
of this. This is the path for a desktop install, where the CLI signs in with the first
credential it finds, which may not be a member of the tracker room.
Rows without the agent. The runner upserts the row of every event it forwards (one that
produced a PR_EVENT line): state (merged when merged), the PR's current head (pr_head), author,
link and PR title from the webhook payload, plus updated_at. It never sets or touches head (the
head the verdict was given at), verdict, event, about or ask, and
clears the ask when the PR is merged or closed. So rows exist and stay current even when the
agent forgets to run track; the page itself changes on the next publish. When the current head has moved past the reviewed one, the "Head reviewed" cell shows both (<reviewed> now <current>), so a verdict is never read against a head nobody reviewed. track and the runner
write tracker.json under one lock (tracker.lock), so neither loses the other's write.
The runner never publishes and never needs room credentials. A PR title is text anyone who can
open a PR controls: it is stored and rendered escaped, never put in the event prompt; a session
publishing the hand-back file reads it as page content, not as instructions. Every value is
HTML-escaped and only http(s) links become links. Run track from a checkout: the runner copy in the state dir has no repo root to resolve the default
PR_WATCH_COMMONS_SCRIPT against.
HTTP 422 Hook already exists.
Before starting, the runner lists the repo's forwarder hooks (name: cli, url
https://webhook-forwarder.github.com/hook); if one exists that it did not create, it prints a
PR_WATCH_NOTE, does not start, and retries every 5 minutes, or at once on the next SIGHUP
(add, remove, config set). status shows the reason and the retry time
(backing off: foreign forwarder hook <id>, retry at HH:MM). Another session or person
forwarding the same repo therefore blocks this watch until they stop.state/pr-watch/hooks.json, and deletes exactly those ids when a forwarder
restarts, when the runner stops, and on its next start. It never deletes a hook it did not
record. A leftover it never recorded (a crash in the first seconds) shows up as point 1; delete
it by hand after checking it is not someone else's.gh children survive re-parented. Their pids are kept in
state/pr-watch/forwarders.json, and the next start stops them before starting new ones.<workspace>/state/pr-watch/, never in the mod folder.session.start respawns it. One runner per state dir (a lock); a second exits.
If the runner exits on its own it is restarted after 30 s, except for a missing extension or
a held lock.gh webhook forward does not replay; after a gap,
check the watched PRs once by hand.--author lists tight. Identical lines within 60 s
are dropped by the mod.--reviewed repo, a synchronize, reopened, ready_for_review or closed event on a PR
not covered by the author rule costs one reviews API call. The answer is cached per PR: a yes
for the runner's lifetime, a no for 10 minutes. A review the owner submits while the runner is
watching marks its PR at once, with no call.gh webhook forward process and a few GitHub API calls per repo at (re)start; nothing
between events.gh webhook forward is a development tool from GitHub; its hooks are visible to every repo
admin under Settings → Webhooks while the watch runs.Two rules; breaking either makes the engine refuse the module, and on a reload the old version stays loaded with its runner already killed:
$ to a helper function. Use $ only inside the hook that received it; helpers
take plain values (takeLines, parseTemplate, messageFor, takeEvents, withContext in
register.ts).node:* imports in the hooks module; start processes with $.process.spawn.manifest.json: the settings and their defaults (config).scripts/pr_watch.py: the CLI, the runner and the tracker.<workspace>/state/pr-watch/): tracker.json (rows and the page kind),
tracker.html (the last hand-back page), tracker.lock.mod/: the Claude Code mod (hooks/register.ts; hooks/paths.ts is written per install).
Test it with claude plugin validate skills/pr-watch/mod and claude plugin test skills/pr-watch/mod.tests/skills/pr-watch/pr_watch.test.py, mod/tests/watch.test.ts.Automatically, when a task matches. Use when the owner wants to be kept on top of pull requests: re-review a person's PRs whenever they change, follow up on any PR the owner has already reviewed, react when a review is requested from the owner, or note merges, closes and submitted or dismissed reviews, without polling and without a Monitor that expires after 30 minutes. A Claude Code mod runs `gh webhook forward` for each watched repo and turns each relevant event into a new turn of this session. O
Skills that do similar work, with their headline benchmark lift.
From the same source and nearby in the catalogue.