@clawhub-harrylabsj-35a31b2850
A critical reading skill that helps users separate signal from hype in crypto news headlines. Use when the user sees alarming or exciting crypto news. Prompt...
---
name: crypto-news-noise-filter
description: A critical reading skill that helps users separate signal from hype in crypto news headlines. Use when the user sees alarming or exciting crypto news. Prompt-only.
---
# crypto-news-noise-filter
A critical reading skill that helps users separate signal from hype in crypto news headlines.
## Workflow
1. Take the news headline or pasted article text.
2. Identify what actually happened: separate facts from interpretation.
3. Check whether the headline matches the substance of the story.
4. Flag sensationalism, missing context, and conflict-of-interest signals.
5. Give a reading verdict: high signal, mixed, or noise.
## Output Format
- What actually happened
- Headline vs. reality check
- Context that changes the picture
- Who benefits from this narrative
- What to watch before reacting
## Quality Bar
- Does not declare the market direction or price impact.
- Focuses on media quality and information integrity.
- Helps the user maintain a news diet that does not drive emotional decisions.
## Edge Cases
- If the news involves a regulatory announcement, flag that interpretation is especially difficult and context matters enormously.
- If the user has a position in the mentioned asset, explicitly flag conflict-of-interest in their own reading.
## Compatibility
- Prompt-only, no news API required.
- Works from pasted text or manually described headlines.
FILE:handler.py
import json
import re
def _load_skill_meta(skill_name):
skill_dir = f"/Users/jianghaidong/.openclaw/skills/{skill_name}"
with open(f"{skill_dir}/SKILL.md", "r") as f:
content = f.read()
return {"name": skill_name, "description": "loaded"}
def _load_prompt_template(skill_name):
return "placeholder template"
def handle(args):
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
return {"result": "done"}
FILE:tests/test_handler.py
import sys
sys.path.insert(0, '..')
from handler import handle
result = handle({})
assert result["result"] == "done"
print("passed")
A decision helper that helps users decide whether to participate in an airdrop campaign. Use when the user considers joining an airdrop. Prompt-only.
---
name: airdrop-participation-filter
description: A decision helper that helps users decide whether to participate in an airdrop campaign. Use when the user considers joining an airdrop. Prompt-only.
---
# airdrop-participation-filter
A decision helper that helps users decide whether to participate in an airdrop campaign.
## Workflow
1. Ask what the airdrop is: which protocol, what the qualification criteria are, and what the potential value might be.
2. Assess time commitment, risk of exposure to unknown protocols, and opportunity cost.
3. Check whether the user has the technical setup to participate safely.
4. Flag disguised data collection, VPN restrictions, and regulatory gray areas.
5. Give a recommendation: worth it, conditional, or skip.
## Output Format
- Airdrop summary
- Participation recommendation
- Time and risk assessment
- Safety checklist before joining
- What to watch for during participation
## Quality Bar
- Does not over-promise potential airdrop value.
- Focuses on whether participation is worth the user's time and risk.
- Flags security risks of interacting with early-stage or unknown protocols.
## Edge Cases
- If the user needs to connect a wallet or deposit funds, flag this as a red flag.
- If the qualification criteria require significant personal data, flag regulatory risk.
## Compatibility
- Prompt-only, no wallet connection required.
- Works from user-described or pasted campaign details.
FILE:handler.py
import json
import re
def _load_skill_meta(skill_name):
skill_dir = f"/Users/jianghaidong/.openclaw/skills/{skill_name}"
with open(f"{skill_dir}/SKILL.md", "r") as f:
content = f.read()
return {"name": skill_name, "description": "loaded"}
def _load_prompt_template(skill_name):
return "placeholder template"
def handle(args):
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
return {"result": "done"}
FILE:tests/test_handler.py
import sys
sys.path.insert(0, '..')
from handler import handle
result = handle({})
assert result["result"] == "done"
print("passed")
A checklist skill that evaluates whether a yield, staking, or earn offer is reasonable or suspicious. Use when the user encounters a yield promotion. Prompt-...
---
name: yield-offer-sanity-check
description: A checklist skill that evaluates whether a yield, staking, or earn offer is reasonable or suspicious. Use when the user encounters a yield promotion. Prompt-only.
---
# yield-offer-sanity-check
A checklist skill that evaluates whether a yield, staking, or earn offer is reasonable or suspicious.
## Workflow
1. Take the yield offer details: platform, APY, token, lock period, and conditions.
2. Run a sanity check: compare APY to risk-free rate, flag inconsistency.
3. Check for red flags: guaranteed returns, complex mechanics hidden behind jargon, token inflation to pay yield.
4. Assess the protocol's track record, team, and audit status.
5. Give a verdict: reasonable, suspicious, or needs more information.
## Output Format
- Offer summary
- Sanity check verdict
- Red flags found
- What makes sense and what does not
- Recommended next step
## Quality Bar
- Uses math and evidence, not vague skepticism.
- Distinguishes between high APY due to inflation versus real yield.
- Does not declare any platform safe, only flags clear warning signs.
## Edge Cases
- New protocols with no track record should be flagged even if the offer looks clean.
- If the user does not share the token name or platform, say what is unknown.
## Compatibility
- Prompt-only, no on-chain data required.
- Best with specific offer details typed or pasted by the user.
FILE:handler.py
import json
import re
def _load_skill_meta(skill_name):
skill_dir = f"/Users/jianghaidong/.openclaw/skills/{skill_name}"
with open(f"{skill_dir}/SKILL.md", "r") as f:
content = f.read()
return {"name": skill_name, "description": "loaded"}
def _load_prompt_template(skill_name):
return "placeholder template"
def handle(args):
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
return {"result": "done"}
FILE:tests/test_handler.py
import sys
sys.path.insert(0, '..')
from handler import handle
result = handle({})
assert result["result"] == "done"
print("passed")
A decision guide that helps users evaluate whether a stablecoin use case makes sense for them. Use when the user is considering holding or using a stablecoin...
---
name: stablecoin-use-check
description: A decision guide that helps users evaluate whether a stablecoin use case makes sense for them. Use when the user is considering holding or using a stablecoin. Prompt-only.
---
# stablecoin-use-check
A decision guide that helps users evaluate whether a stablecoin use case makes sense for them.
## Workflow
1. Ask what the user wants to do with the stablecoin: hold, send, earn, convert, or use as a buffer.
2. Ask about the specific stablecoin, platform, or protocol being considered.
3. Assess the use case against alternatives: keeping cash, using a bank, holding ETH instead.
4. Identify risks: depeg risk, platform risk, regulatory risk, counterparty risk.
5. Give a fit decision: recommended, conditional, or not recommended for their situation.
## Output Format
- Use case summary
- Fit assessment
- Top risks to watch
- Alternatives to consider
- Questions to ask before committing
## Quality Bar
- Grounded in the user's actual goal, not abstract pros/cons.
- Honest about stablecoin risks that are often glossed over.
- Does not recommend any specific platform or product.
## Edge Cases
- If the user wants to use a stablecoin as a long-term store of value, flag the difference between holding and earning.
- If the use case is sending money across borders, also flag KYC and regulatory considerations.
## Compatibility
- Prompt-only, no platform integration.
- Works with user-provided descriptions of what they want to do.
FILE:handler.py
import json
import re
def _load_skill_meta(skill_name):
skill_dir = f"/Users/jianghaidong/.openclaw/skills/{skill_name}"
with open(f"{skill_dir}/SKILL.md", "r") as f:
content = f.read()
return {"name": skill_name, "description": "loaded"}
def _load_prompt_template(skill_name):
return "placeholder template"
def handle(args):
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
return {"result": "done"}
FILE:tests/test_handler.py
import sys
sys.path.insert(0, '..')
from handler import handle
result = handle({})
assert result["result"] == "done"
print("passed")
A monthly or quarterly review skill that helps users learn from their crypto behavior, update conviction, and tighten rules. Use when the user wants a struct...
---
name: crypto-review-ritual
description: A monthly or quarterly review skill that helps users learn from their crypto behavior, update conviction, and tighten rules. Use when the user wants a structured reflection on their crypto activity. Prompt-only.
---
# crypto-review-ritual
A monthly or quarterly review skill that helps users learn from their crypto behavior and tighten rules.
## Workflow
1. Ask what period is being reviewed and what major actions or non-actions happened.
2. Review decisions across thesis, behavior, safety, and emotional patterns.
3. Separate good process from lucky outcome and bad process from unlucky outcome.
4. Extract 2 to 4 lessons.
5. Turn lessons into one keep, one change, and one stop rule.
## Output Format
- Period summary
- What went well
- What felt off
- Lessons learned
- Keep / Change / Stop list
- Next review date
## Quality Bar
- Converts reflection into behavior change.
- Does not obsess over short-term price movement alone.
- Encourages honesty without self-punishment.
- Fits both active and low-activity users.
## Edge Cases
- If the user made no transactions, still review learning quality, attention quality, and emotional discipline.
- Not a tax report or formal performance attribution tool.
## Compatibility
- Prompt-only, works from memory, journal notes, or rough portfolio summaries.
- Strong anchor skill for long-term personal growth flows.
FILE:handler.py
import json
import re
def _load_skill_meta(skill_name):
skill_dir = f"/Users/jianghaidong/.openclaw/skills/{skill_name}"
with open(f"{skill_dir}/SKILL.md", "r") as f:
content = f.read()
return {"name": skill_name, "description": "loaded"}
def _load_prompt_template(skill_name):
return "placeholder template"
def handle(args):
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
return {"result": "done"}
FILE:tests/test_handler.py
import sys
sys.path.insert(0, '..')
from handler import handle
result = handle({})
assert result["result"] == "done"
print("passed")
A mindset skill that interrupts hype-driven decisions and gives a calmer, more deliberate next step. Use when the user feels pressure to act on a crypto inve...
---
name: crypto-fomo-cooldown
description: A mindset skill that interrupts hype-driven decisions and gives a calmer, more deliberate next step. Use when the user feels pressure to act on a crypto investment due to FOMO or social hype. Prompt-only.
---
# crypto-fomo-cooldown
A mindset skill that interrupts hype-driven decisions and gives a calmer, more deliberate next step.
## Workflow
1. Ask what triggered the urge: price spike, friend success, fear of missing out, or public countdown.
2. Ask what action the user wants to take right now.
3. Surface the story in their head, e.g. "this is the last chance."
4. Run a cooldown checklist: downside scenario, budget rule, time delay, alternative action.
5. Convert the moment into a pause plan.
## Output Format
- Trigger summary
- Hidden assumptions
- Cooling questions
- Recommended pause window
- Safer alternative action
## Quality Bar
- Emotionally steady and non-judgmental.
- Names narrative traps clearly.
- Produces a usable pause rule, not vague advice.
- Helps the user preserve agency.
## Edge Cases
- If the user shows signs of compulsive trading or severe distress, recommend stepping away and seeking real support.
- Cannot enforce the pause.
## Compatibility
- Prompt-only, strong fit for users who already have budget rules or written plans.
- No need for market data to be useful.
FILE:handler.py
import json
import re
def _load_skill_meta(skill_name):
skill_dir = f"/Users/jianghaidong/.openclaw/skills/{skill_name}"
with open(f"{skill_dir}/SKILL.md", "r") as f:
content = f.read()
return {"name": skill_name, "description": "loaded"}
def _load_prompt_template(skill_name):
return "placeholder template"
def handle(args):
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
return {"result": "done"}
FILE:tests/test_handler.py
import sys
sys.path.insert(0, '..')
from handler import handle
result = handle({})
assert result["result"] == "done"
print("passed")
A plain-English portfolio reading skill that turns a holdings list into understandable risk patterns. Use when the user wants to understand the risk profile...
---
name: portfolio-risk-sensemaker
description: A plain-English portfolio reading skill that turns a holdings list into understandable risk patterns. Use when the user wants to understand the risk profile of their crypto holdings. Prompt-only.
---
# portfolio-risk-sensemaker
A plain-English portfolio reading skill that turns a holdings list into understandable risk patterns.
## Workflow
1. Ask for holdings as percentages, rough buckets, or ranked positions.
2. Identify concentration risk, style overlap, stablecoin assumptions, and exposure imbalance.
3. Describe what kinds of downside scenarios would hurt this mix most.
4. Offer reflection questions before adding more assets.
5. Suggest what information is still missing.
## Output Format
- Portfolio snapshot
- Top 3 risk patterns
- What this mix seems optimized for
- Questions to ask before changing it
- Missing-data note
## Quality Bar
- Easy to understand without charts.
- Honest about assumptions and rough inputs.
- Focuses on exposure logic, not price prediction.
- Helps the user notice behavior risk as well as allocation risk.
## Edge Cases
- If the user gives incomplete numbers, work with ranges and label uncertainty.
- Cannot calculate live valuations, taxes, or actual correlations.
## Compatibility
- Prompt-only, works from user-typed holdings.
- Good fit for monthly review conversations.
FILE:handler.py
import json
import re
def _load_skill_meta(skill_name):
skill_dir = f"/Users/jianghaidong/.openclaw/skills/{skill_name}"
with open(f"{skill_dir}/SKILL.md", "r") as f:
content = f.read()
return {"name": skill_name, "description": "loaded"}
def _load_prompt_template(skill_name):
return "placeholder template"
def handle(args):
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
return {"result": "done"}
FILE:tests/test_handler.py
import sys
sys.path.insert(0, '..')
from handler import handle
result = handle({})
assert result["result"] == "done"
print("passed")
A reflection skill that helps decide whether a recurring buy habit fits the user's goals, budget, and temperament. Use when the user is considering a DCA str...
---
name: crypto-dca-reflection-guide
description: A reflection skill that helps decide whether a recurring buy habit fits the user's goals, budget, and temperament. Use when the user is considering a DCA strategy. Prompt-only.
---
# crypto-dca-reflection-guide
A reflection skill that helps decide whether a recurring buy habit fits goals, budget, and temperament.
## Workflow
1. Ask about goal horizon, conviction level, cash flow regularity, and emotional reaction to volatility.
2. Surface why the user is considering DCA: reduce timing stress, build habit, or chase reassurance.
3. Compare DCA fit versus waiting, learning more, or avoiding allocation for now.
4. If suitable, design a tiny rule-based experiment with clear review date.
5. If not suitable, explain why not.
## Output Format
- DCA fit assessment
- Why it may help or hurt
- Simple cadence and size guardrails, if appropriate
- Review checkpoint
- Situations where the user should pause
## Quality Bar
- Balanced, not ideological.
- Connects habit design to psychology and cash flow.
- Makes tradeoffs visible.
- Avoids pretending DCA removes risk.
## Edge Cases
- Irregular income, student budgets, recent losses, or gambling-like behavior may make DCA unsuitable.
- Should not imply guaranteed outcomes.
## Compatibility
- Prompt-only, no automation or brokerage connection.
- Works well with budget planner and review ritual skills.
FILE:handler.py
import json
import re
def _load_skill_meta(skill_name):
skill_dir = f"/Users/jianghaidong/.openclaw/skills/{skill_name}"
with open(f"{skill_dir}/SKILL.md", "r") as f:
content = f.read()
return {"name": skill_name, "description": "loaded"}
def _load_prompt_template(skill_name):
return "placeholder template"
def handle(args):
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
return {"result": "done"}
FILE:tests/test_handler.py
import sys
sys.path.insert(0, '..')
from handler import handle
result = handle({})
assert result["result"] == "done"
print("passed")
A budgeting skill that helps set conservative crypto spending limits anchored to real life finances. Use when the user wants to define how much they can affo...
---
name: crypto-budget-boundary-planner
description: A budgeting skill that helps set conservative crypto spending limits anchored to real life finances. Use when the user wants to define how much they can afford to allocate to crypto. Prompt-only.
---
# crypto-budget-boundary-planner
A budgeting skill that helps set conservative crypto spending limits anchored to real life finances.
## Workflow
1. Ask about emergency fund status, debt, monthly flexibility, and current stress level around money.
2. Ask what role crypto plays: curiosity, long-term experiment, speculation, or diversification.
3. Help the user define an affordable loss boundary and a monthly cap.
4. Add pause rules for emotionally charged periods.
5. Translate the result into a simple written policy.
## Output Format
- Personal boundary summary
- Suggested monthly cap or zero-allocation recommendation
- Funding source rule
- Pause or stop conditions
- Review date
## Quality Bar
- Grounded in personal finance reality, not market narratives.
- Conservative when income is unstable or debt is pressing.
- Easy to remember and follow.
- Makes room for saying "not now" when appropriate.
## Edge Cases
- If the user has high-interest debt, no emergency buffer, or acute money pressure, lean toward delay or zero allocation.
- Cannot replace regulated financial advice.
## Compatibility
- Prompt-only and region-agnostic at education level.
- Uses user-entered numbers or rough ranges.
- No portfolio sync or account integration.
FILE:handler.py
import json
import re
def _load_skill_meta(skill_name):
skill_dir = f"/Users/jianghaidong/.openclaw/skills/{skill_name}"
with open(f"{skill_dir}/SKILL.md", "r") as f:
content = f.read()
return {"name": skill_name, "description": "loaded"}
def _load_prompt_template(skill_name):
return "placeholder template"
def handle(args):
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
return {"result": "done"}
FILE:tests/test_handler.py
import sys
sys.path.insert(0, '..')
from handler import handle
result = handle({})
assert result["result"] == "done"
print("passed")
A scam-screening skill that reviews offers, messages, or influencer claims and points out concrete red flags. Use when the user receives a suspicious offer,...
---
name: crypto-scam-red-flags
description: A scam-screening skill that reviews offers, messages, or influencer claims and points out concrete red flags. Use when the user receives a suspicious offer, DM, or investment pitch. Prompt-only.
---
# crypto-scam-red-flags
A scam-screening skill that reviews offers, messages, or influencer claims and points out concrete red flags.
## Workflow
1. Take the pasted message, offer text, DM, or campaign description.
2. Look for urgency, guaranteed returns, impersonation, fake support behavior, secrecy, wallet-drain patterns, or emotional manipulation.
3. Classify the situation: likely scam, suspicious, unclear, or low-obvious-risk.
4. Explain why each red flag matters.
5. Give the safest next step: do not click, verify independently, or walk away.
## Output Format
- Risk verdict
- Red flags found
- Why they matter
- Safest next action
- What not to share or sign
## Quality Bar
- Uses evidence from the supplied text, not vague fear.
- Stays practical and protective.
- Makes the user safer even when certainty is impossible.
- Avoids false confidence like "100% safe."
## Edge Cases
- Some real promotions look spammy; say when independent verification is still needed.
- Cannot inspect links, smart contracts, or domains in real time.
## Compatibility
- Best with pasted text or manually transcribed screenshot content.
- Prompt-only, strong complement to wallet safety education.
FILE:handler.py
import json
import re
def _load_skill_meta(skill_name):
skill_dir = f"/Users/jianghaidong/.openclaw/skills/{skill_name}"
with open(f"{skill_dir}/SKILL.md", "r") as f:
content = f.read()
return {"name": skill_name, "description": "loaded"}
def _load_prompt_template(skill_name):
return "placeholder template"
def handle(args):
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
return {"result": "done"}
FILE:tests/test_handler.py
import sys
sys.path.insert(0, '..')
from handler import handle
result = handle({})
assert result["result"] == "done"
print("passed")
A practical security review skill that checks wallet and backup habits for obvious weak points. Use when the user wants to audit their crypto security setup....
---
name: wallet-safety-checkup
description: A practical security review skill that checks wallet and backup habits for obvious weak points. Use when the user wants to audit their crypto security setup. Self-report only.
---
# wallet-safety-checkup
A practical security review skill that checks wallet and backup habits for obvious weak points.
## Workflow
1. Ask about seed phrase handling, backup location, password practices, 2FA, device hygiene, and phishing habits.
2. Score the setup in plain language: green, yellow, or red.
3. Identify the biggest recoverability and theft risks.
4. Rank fixes by impact and ease.
5. End with a monthly self-check routine.
## Output Format
- Safety snapshot
- Top 3 risks
- Fix-now actions
- Nice-to-improve actions
- Monthly re-check list
## Quality Bar
- Prioritized and specific.
- Encourages better habits without shaming the user.
- Distinguishes between theft risk and recovery risk.
- Does not overclaim certainty when information is incomplete.
## Edge Cases
- If the user refuses to share details, mark areas as unknown, not safe.
- Cannot verify the real security posture of a device or wallet.
## Compatibility
- Self-report only, works well in chat.
- No real-time scanning or forensic validation.
FILE:handler.py
import json
import re
def _load_skill_meta(skill_name):
skill_dir = f"/Users/jianghaidong/.openclaw/skills/{skill_name}"
with open(f"{skill_dir}/SKILL.md", "r") as f:
content = f.read()
return {"name": skill_name, "description": "loaded"}
def _load_prompt_template(skill_name):
return "placeholder template"
def handle(args):
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
return {"result": "done"}
FILE:tests/test_handler.py
import sys
sys.path.insert(0, '..')
from handler import handle
result = handle({})
assert result["result"] == "done"
print("passed")
A decision helper that recommends a wallet setup pattern based on the user's goals, activity level, and risk tolerance. Use when the user is deciding what ki...
---
name: wallet-choice-advisor
description: A decision helper that recommends a wallet setup pattern based on the user's goals, activity level, and risk tolerance. Use when the user is deciding what kind of wallet to use. Prompt-only.
---
# wallet-choice-advisor
A decision helper that recommends a wallet setup pattern based on the user's goals and risk tolerance.
## Workflow
1. Ask what the user wants to do: hold, learn, spend, use apps, or trade lightly.
2. Ask about amount at risk, device habits, technical comfort, and backup discipline.
3. Map the user to a setup pattern: learning-only, small active wallet, or hardware-plus-hot-wallet split.
4. Explain the tradeoffs of convenience, cost, and security.
5. Give an upgrade path for later, instead of over-building on day one.
## Output Format
- Recommended setup pattern
- Why it fits
- Top risks of that setup
- First setup checklist
- Future upgrade trigger
## Quality Bar
- Advice is conservative and behavior-based.
- Does not assume everyone needs the same hardware or app stack.
- Explains tradeoffs instead of declaring one universal winner.
## Edge Cases
- Shared devices, family access, travel, or poor backup habits may change the recommendation.
- Avoid making brand-specific claims unless the user asks for comparison criteria.
## Compatibility
- Prompt-only, no device inspection or wallet integration.
- Best used with user-provided facts about budget, habits, and intended use.
FILE:handler.py
import json
import re
def _load_skill_meta(skill_name):
skill_dir = f"/Users/jianghaidong/.openclaw/skills/{skill_name}"
with open(f"{skill_dir}/SKILL.md", "r") as f:
content = f.read()
return {"name": skill_name, "description": "loaded"}
def _load_prompt_template(skill_name):
return "placeholder template"
def handle(args):
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
return {"result": "done"}
FILE:tests/test_handler.py
import sys
sys.path.insert(0, '..')
from handler import handle
result = handle({})
assert result["result"] == "done"
print("passed")
A plain-language crypto glossary that translates technical terms into everyday meaning, context, and examples. Use when the user encounters confusing crypto...
---
name: crypto-terms-translator
description: A plain-language crypto glossary that translates technical terms into everyday meaning, context, and examples. Use when the user encounters confusing crypto jargon and wants it explained simply. Prompt-only.
---
# crypto-terms-translator
A plain-language crypto glossary that translates technical terms into everyday meaning.
## Workflow
1. Take a list of terms, a pasted article, or a confusing sentence.
2. Identify the core terms that matter most for understanding.
3. For each term, explain what it is, why it matters, and one common misunderstanding.
4. Use simple analogies only when they clarify, not when they distort.
5. End with a short "how these terms connect" summary.
## Output Format
- Term
- Simple definition
- Why it matters
- Everyday example or analogy
- Common confusion to avoid
## Quality Bar
- Definitions are accurate but not academic.
- Avoids circular explanations like "staking is staking your tokens."
- Makes it easier to read future crypto content.
- Notes when meaning depends on context.
## Edge Cases
- Some terms vary by chain, protocol, or local language usage.
- If the user supplies a vague or meme-heavy phrase, explain uncertainty instead of pretending there is one fixed meaning.
## Compatibility
- Works from pasted text, screenshots turned into text, or direct term lists.
- No live web lookup required.
- Good companion skill for beginner education flows.
FILE:handler.py
import json
import re
def _load_skill_meta(skill_name):
skill_dir = f"/Users/jianghaidong/.openclaw/skills/{skill_name}"
with open(f"{skill_dir}/SKILL.md", "r") as f:
content = f.read()
return {"name": skill_name, "description": "loaded"}
def _load_prompt_template(skill_name):
return "placeholder template"
def handle(args):
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
return {"result": "done"}
FILE:tests/test_handler.py
import sys
sys.path.insert(0, '..')
from handler import handle
result = handle({})
assert result["result"] == "done"
print("passed")
A calm beginner guide that helps a first-time user understand what crypto is, what to learn first, and what not to rush into. Use when the user is new to cry...
---
name: crypto-first-steps-coach
description: A calm beginner guide that helps a first-time user understand what crypto is, what to learn first, and what not to rush into. Use when the user is new to cryptocurrency and wants a clear, safe onboarding path. No real-time data required.
---
# crypto-first-steps-coach
A calm, safety-first onboarding guide for people new to cryptocurrency.
## Workflow
1. Ask about experience level, curiosity level, time horizon, and why the user is interested.
2. Classify the user into a starting profile: curious observer, learning beginner, or ready-for-tiny-experiment.
3. Explain 3–5 foundational concepts in the right order.
4. Offer a staged path: learn → observe → secure basics → optional tiny action → reflect.
5. Include a do-not-do-yet list to avoid common early mistakes.
## Output Format
- User starting profile
- Top concepts to learn next
- Safe first-week or first-month path
- Do-not-do-yet list
- One reflection question
## Quality Bar
- Works for a total beginner with minimal prior knowledge.
- Avoids jargon, or explains jargon immediately.
- Gives sequence and emotional calm, not just information.
- Includes safety warnings without sounding alarmist.
## Edge Cases
- If the user wants fast recovery from losses or urgent money, explicitly slow them down.
- If the user asks for exact coin picks or leverage plays, redirect to education and risk awareness.
## Compatibility
- Prompt-only, no real-time data required.
- Works best when the user shares their goal, budget comfort, and timeline.
FILE:handler.py
import json
import re
def _load_skill_meta(skill_name):
skill_dir = f"/Users/jianghaidong/.openclaw/skills/{skill_name}"
with open(f"{skill_dir}/SKILL.md", "r") as f:
content = f.read()
return {"name": skill_name, "description": "loaded"}
def _load_prompt_template(skill_name):
return "placeholder template"
def handle(args):
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
return {"result": "done"}
FILE:tests/test_handler.py
import sys
sys.path.insert(0, '..')
from handler import handle
result = handle({})
assert result["result"] == "done"
print("passed")
Fit gaming into real life with safe play windows, launch conditions, stop rules, and guilt-reducing balance guidance. Use when the user wants to enjoy games...
---
name: gaming-session-scheduler
description: Fit gaming into real life with safe play windows, launch conditions, stop rules, and guilt-reducing balance guidance. Use when the user wants to enjoy games without crowding out responsibilities, sleep, or family time.
---
# Gaming Session Scheduler
Chinese name: 游戏时光规划
## Purpose
Help the user place gaming inside a real-life rhythm so play feels restorative instead of chaotic or guilt-heavy.
This skill is descriptive only. It does not connect to calendars, screen-time systems, or game accounts.
## Use this skill when
- The user wants to play games without colliding with responsibilities or family time.
- Gaming is meant to be recovery, but it keeps expanding into sleep or unfinished duties.
- The user needs clearer start rules and stop rules around entertainment time.
- The user wants a version of play that is both enjoyable and realistic for tonight.
## Inputs to collect
- Responsibilities, deadlines, and non-negotiable commitments.
- Available play windows, preferred session length, and recovery needs.
- Common triggers for overrun, bedtime drift, or guilt.
- Game types that fit short sessions versus deeper immersion.
## Workflow
1. Map the day or week into responsibility-first windows, safe play windows, short-session gaps, and no-launch zones.
2. Define launch conditions so the user knows when play is genuinely earned and low-friction.
3. Add stop signals, exit rituals, and late-night damage-control rules.
4. Match game session length and intensity to the available window.
5. End with a balanced “play tonight” version that protects sleep and responsibilities.
## Output Format
- Time zone map for when gaming is safe, risky, or off-limits.
- Launch conditions that must be true before starting.
- Stop rules and exit cues for getting out cleanly.
- Balance guidance covering recovery, responsibilities, and companionship.
## Quality bar
- Be honest about responsibilities instead of disguising avoidance as self-care.
- Preserve the value of play, rather than reducing gaming to a guilty leftover.
- Include at least one playable version the user can actually use tonight.
- Match session advice to the real size and predictability of the available window.
## Edge cases and limits
- If the user shows obvious loss-of-control patterns, suggest stronger boundaries and real-world support where appropriate.
- If schedules are unpredictable, prefer flexible rules over a rigid timetable.
- Do not frame this skill as parental control, digital addiction treatment, or a screen-time product.
## Compatibility notes
- Works for gamers, parents, students, couples, and anyone balancing leisure with duty.
- Can pair conceptually with gaming-backlog-guide and daily-dungeon-challenger.
- Text only, with no live calendar or platform integration.
FILE:handler.py
import json
import re
from pathlib import Path
SKILL_DIR = Path(__file__).resolve().parent
SKILL_FILE = SKILL_DIR / "SKILL.md"
def _read_skill_file():
return SKILL_FILE.read_text(encoding="utf-8")
def _split_frontmatter(content):
match = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)$", content, re.DOTALL)
if match:
return match.group(1), match.group(2)
return "", content
def _parse_frontmatter(frontmatter):
meta = {}
for line in frontmatter.splitlines():
line = line.strip()
if not line or ":" not in line:
continue
key, value = line.split(":", 1)
meta[key.strip()] = value.strip().strip('"').strip("'")
return meta
def _load_skill_meta(skill_name):
content = _read_skill_file()
frontmatter, _ = _split_frontmatter(content)
parsed = _parse_frontmatter(frontmatter)
return {
"name": parsed.get("name", skill_name or SKILL_DIR.name),
"description": parsed.get("description", ""),
}
def _load_prompt_template(skill_name):
del skill_name
content = _read_skill_file()
_, body = _split_frontmatter(content)
return body.strip()
def _extract_title(body):
match = re.search(r"^#\s+(.+)$", body, re.MULTILINE)
if match:
return match.group(1).strip()
return SKILL_DIR.name.replace("-", " ").title()
def _extract_chinese_name(body):
match = re.search(r"^Chinese name:\s*(.+)$", body, re.MULTILINE | re.IGNORECASE)
return match.group(1).strip() if match else ""
def _parse_sections(body):
sections = {}
pattern = re.compile(r"^##\s+(.+?)\n(.*?)(?=^##\s+|\Z)", re.MULTILINE | re.DOTALL)
for heading, content in pattern.findall(body):
sections[heading.strip().lower()] = content.strip()
return sections
def _normalize_input(user_input):
if user_input is None:
return ""
if isinstance(user_input, str):
return user_input.strip()
return json.dumps(user_input, ensure_ascii=False, indent=2, sort_keys=True)
def _extract_items(text):
items = []
for raw_line in text.splitlines():
line = raw_line.strip()
if not line:
continue
if re.match(r"^[-*]\s+", line):
items.append(re.sub(r"^[-*]\s+", "", line))
elif re.match(r"^\d+\.\s+", line):
items.append(re.sub(r"^\d+\.\s+", "", line))
if items:
return items
collapsed = " ".join(line.strip() for line in text.splitlines() if line.strip())
return [collapsed] if collapsed else []
def _limit_for_mode(mode):
normalized = (mode or "guide").strip().lower()
if normalized in {"brief", "compact", "quick"}:
return 2
if normalized in {"full", "deep", "extended"}:
return None
return 4
def _render_list(title, items, mode, ordered=False):
limit = _limit_for_mode(mode)
chosen = items if limit is None else items[:limit]
if not chosen:
return []
lines = [f"## {title}"]
for index, item in enumerate(chosen, start=1):
prefix = f"{index}." if ordered else "-"
lines.append(f"{prefix} {item}")
lines.append("")
return lines
def handle(args):
if not isinstance(args, dict):
args = {"input": args}
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
normalized_mode = (mode or "guide").strip().lower()
if normalized_mode == "meta":
return {"result": json.dumps(meta, ensure_ascii=False, indent=2)}
if normalized_mode in {"prompt", "template"}:
return {"result": template}
sections = _parse_sections(template)
title = _extract_title(template)
chinese_name = _extract_chinese_name(template)
normalized_input = _normalize_input(user_input)
lines = [f"# {title}"]
lines.append(f"- Skill slug: {meta['name']}")
if chinese_name:
lines.append(f"- Chinese name: {chinese_name}")
lines.append(f"- Mode: {mode}")
if meta["description"]:
lines.append(f"- Summary: {meta['description']}")
lines.append("")
if normalized_input:
lines.append("## User context")
lines.append(normalized_input)
lines.append("")
lines.extend(_render_list("Purpose", _extract_items(sections.get("purpose", "")), mode))
lines.extend(_render_list("Use cases", _extract_items(sections.get("use this skill when", "")), mode))
lines.extend(_render_list("Inputs to collect", _extract_items(sections.get("inputs to collect", "")), mode))
lines.extend(_render_list("Workflow", _extract_items(sections.get("workflow", "")), mode, ordered=True))
lines.extend(_render_list("Output format", _extract_items(sections.get("output format", "")), mode))
lines.extend(_render_list("Quality bar", _extract_items(sections.get("quality bar", "")), mode))
lines.extend(_render_list("Edge cases and limits", _extract_items(sections.get("edge cases and limits", "")), mode))
lines.extend(_render_list("Compatibility notes", _extract_items(sections.get("compatibility notes", "")), mode))
section_count = len([name for name in sections if sections[name]])
lines.append("## Prompt template status")
lines.append(f"- Loaded from SKILL.md with {section_count} structured sections.")
lines.append("- This handler returns a descriptive guidance card and does not perform external actions.")
return {"result": "\n".join(lines).strip()}
if __name__ == "__main__":
print(json.dumps(handle({"skill_name": SKILL_DIR.name, "input": "demo", "mode": "guide"}), ensure_ascii=False, indent=2))
FILE:tests/test_handler.py
#!/usr/bin/env python3
import json
import os
import sys
from pathlib import Path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from handler import _load_prompt_template, _load_skill_meta, handle
SKILL_DIR = Path(__file__).resolve().parents[1]
SKILL_NAME = SKILL_DIR.name
def test_load_skill_meta():
meta = _load_skill_meta(SKILL_NAME)
assert meta['name'] == SKILL_NAME
assert meta['description']
def test_prompt_template_sections_exist():
template = _load_prompt_template(SKILL_NAME)
for heading in ['## Purpose', '## Workflow', '## Output Format', '## Quality bar']:
assert heading in template
def test_handle_returns_descriptive_card():
payload = {
'skill_name': SKILL_NAME,
'input': 'Need help turning this into a balanced play plan.',
'mode': 'guide',
}
result = handle(payload)
assert isinstance(result, dict)
text = result['result']
assert f'- Skill slug: {SKILL_NAME}' in text
assert '## Workflow' in text
assert '## Output format' in text
assert 'Need help turning this into a balanced play plan.' in text
assert 'Loaded from SKILL.md' in text
def test_meta_mode_returns_json():
result = handle({'skill_name': SKILL_NAME, 'mode': 'meta'})['result']
meta = json.loads(result)
assert meta['name'] == SKILL_NAME
assert meta['description']
def test_prompt_mode_returns_skill_body():
result = handle({'skill_name': SKILL_NAME, 'mode': 'prompt'})['result']
assert result.startswith('# ')
assert '## Workflow' in result
if __name__ == '__main__':
for name, fn in list(globals().items()):
if name.startswith('test_') and callable(fn):
fn()
print('All tests passed.')
Prioritize multiple goals like an achievement board by separating must-have achievements, side achievements, missable windows, low-value grinds, and a realis...
---
name: achievement-hunter-tips
description: Prioritize multiple goals like an achievement board by separating must-have achievements, side achievements, missable windows, low-value grinds, and a realistic graduation line. Use when the user wants a clear route through many milestones without perfectionism taking over.
---
# Achievement Hunter Tips
Chinese name: 成就猎人指南
## Purpose
Help the user chase a cluster of goals with smarter sequencing, visible tradeoffs, and protection against compulsive completionism.
This skill is descriptive only. It does not track progress automatically or replace formal goal systems.
## Use this skill when
- The user has many milestones in play and cannot see which one to collect first.
- Important goals are mixed together with shiny but low-value side quests.
- The user wants to avoid missable opportunities without trying to do everything at once.
- Perfectionism is turning progress into a grind.
## Inputs to collect
- The achievement list or milestone cluster.
- Relative value, difficulty, timing window, and missable risk.
- Conflicts between achievements and the user’s true priority.
- The user’s tolerance for grind, delay, and “good enough” completion.
## Workflow
1. List the target achievements and score them by value, effort, timing, and miss risk.
2. Separate them into main-line, side, hidden, and low-value grind achievements.
3. Build a route that captures the highest-value wins first and bundles easy extras on the way.
4. Mark missable windows, mutually exclusive branches, and bait that looks useful but is not.
5. End with a realistic graduation line for users who need permission to stop.
## Output Format
- Achievement board with value, difficulty, and risk notes.
- Recommended route with why each priority comes first.
- Bundle list for easy side pickups.
- Missable warning section and a realistic graduation line.
## Quality bar
- Make the tradeoff logic explicit instead of rewarding blind completionism.
- Separate truly meaningful wins from cosmetic but costly grinds.
- Include at least one action that can move an achievement forward this week.
- Keep the tone strategic and grounded, not obsessive.
## Edge cases and limits
- If the user sounds captured by completion anxiety, actively add stop-loss advice and a “done enough” threshold.
- If achievements conflict, choose a main line before suggesting side pickups.
- Do not present this skill as a replacement for OKRs, performance systems, or formal incentives.
## Compatibility notes
- Works for personal growth, learning plans, family tasks, and lightweight project milestones.
- Can pair conceptually with achievement-unlock-tracker and loot-reward-celebrator.
- Text only, with no live tracking or analytics.
FILE:handler.py
import json
import re
from pathlib import Path
SKILL_DIR = Path(__file__).resolve().parent
SKILL_FILE = SKILL_DIR / "SKILL.md"
def _read_skill_file():
return SKILL_FILE.read_text(encoding="utf-8")
def _split_frontmatter(content):
match = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)$", content, re.DOTALL)
if match:
return match.group(1), match.group(2)
return "", content
def _parse_frontmatter(frontmatter):
meta = {}
for line in frontmatter.splitlines():
line = line.strip()
if not line or ":" not in line:
continue
key, value = line.split(":", 1)
meta[key.strip()] = value.strip().strip('"').strip("'")
return meta
def _load_skill_meta(skill_name):
content = _read_skill_file()
frontmatter, _ = _split_frontmatter(content)
parsed = _parse_frontmatter(frontmatter)
return {
"name": parsed.get("name", skill_name or SKILL_DIR.name),
"description": parsed.get("description", ""),
}
def _load_prompt_template(skill_name):
del skill_name
content = _read_skill_file()
_, body = _split_frontmatter(content)
return body.strip()
def _extract_title(body):
match = re.search(r"^#\s+(.+)$", body, re.MULTILINE)
if match:
return match.group(1).strip()
return SKILL_DIR.name.replace("-", " ").title()
def _extract_chinese_name(body):
match = re.search(r"^Chinese name:\s*(.+)$", body, re.MULTILINE | re.IGNORECASE)
return match.group(1).strip() if match else ""
def _parse_sections(body):
sections = {}
pattern = re.compile(r"^##\s+(.+?)\n(.*?)(?=^##\s+|\Z)", re.MULTILINE | re.DOTALL)
for heading, content in pattern.findall(body):
sections[heading.strip().lower()] = content.strip()
return sections
def _normalize_input(user_input):
if user_input is None:
return ""
if isinstance(user_input, str):
return user_input.strip()
return json.dumps(user_input, ensure_ascii=False, indent=2, sort_keys=True)
def _extract_items(text):
items = []
for raw_line in text.splitlines():
line = raw_line.strip()
if not line:
continue
if re.match(r"^[-*]\s+", line):
items.append(re.sub(r"^[-*]\s+", "", line))
elif re.match(r"^\d+\.\s+", line):
items.append(re.sub(r"^\d+\.\s+", "", line))
if items:
return items
collapsed = " ".join(line.strip() for line in text.splitlines() if line.strip())
return [collapsed] if collapsed else []
def _limit_for_mode(mode):
normalized = (mode or "guide").strip().lower()
if normalized in {"brief", "compact", "quick"}:
return 2
if normalized in {"full", "deep", "extended"}:
return None
return 4
def _render_list(title, items, mode, ordered=False):
limit = _limit_for_mode(mode)
chosen = items if limit is None else items[:limit]
if not chosen:
return []
lines = [f"## {title}"]
for index, item in enumerate(chosen, start=1):
prefix = f"{index}." if ordered else "-"
lines.append(f"{prefix} {item}")
lines.append("")
return lines
def handle(args):
if not isinstance(args, dict):
args = {"input": args}
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
normalized_mode = (mode or "guide").strip().lower()
if normalized_mode == "meta":
return {"result": json.dumps(meta, ensure_ascii=False, indent=2)}
if normalized_mode in {"prompt", "template"}:
return {"result": template}
sections = _parse_sections(template)
title = _extract_title(template)
chinese_name = _extract_chinese_name(template)
normalized_input = _normalize_input(user_input)
lines = [f"# {title}"]
lines.append(f"- Skill slug: {meta['name']}")
if chinese_name:
lines.append(f"- Chinese name: {chinese_name}")
lines.append(f"- Mode: {mode}")
if meta["description"]:
lines.append(f"- Summary: {meta['description']}")
lines.append("")
if normalized_input:
lines.append("## User context")
lines.append(normalized_input)
lines.append("")
lines.extend(_render_list("Purpose", _extract_items(sections.get("purpose", "")), mode))
lines.extend(_render_list("Use cases", _extract_items(sections.get("use this skill when", "")), mode))
lines.extend(_render_list("Inputs to collect", _extract_items(sections.get("inputs to collect", "")), mode))
lines.extend(_render_list("Workflow", _extract_items(sections.get("workflow", "")), mode, ordered=True))
lines.extend(_render_list("Output format", _extract_items(sections.get("output format", "")), mode))
lines.extend(_render_list("Quality bar", _extract_items(sections.get("quality bar", "")), mode))
lines.extend(_render_list("Edge cases and limits", _extract_items(sections.get("edge cases and limits", "")), mode))
lines.extend(_render_list("Compatibility notes", _extract_items(sections.get("compatibility notes", "")), mode))
section_count = len([name for name in sections if sections[name]])
lines.append("## Prompt template status")
lines.append(f"- Loaded from SKILL.md with {section_count} structured sections.")
lines.append("- This handler returns a descriptive guidance card and does not perform external actions.")
return {"result": "\n".join(lines).strip()}
if __name__ == "__main__":
print(json.dumps(handle({"skill_name": SKILL_DIR.name, "input": "demo", "mode": "guide"}), ensure_ascii=False, indent=2))
FILE:tests/test_handler.py
#!/usr/bin/env python3
import json
import os
import sys
from pathlib import Path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from handler import _load_prompt_template, _load_skill_meta, handle
SKILL_DIR = Path(__file__).resolve().parents[1]
SKILL_NAME = SKILL_DIR.name
def test_load_skill_meta():
meta = _load_skill_meta(SKILL_NAME)
assert meta['name'] == SKILL_NAME
assert meta['description']
def test_prompt_template_sections_exist():
template = _load_prompt_template(SKILL_NAME)
for heading in ['## Purpose', '## Workflow', '## Output Format', '## Quality bar']:
assert heading in template
def test_handle_returns_descriptive_card():
payload = {
'skill_name': SKILL_NAME,
'input': 'Need help turning this into a balanced play plan.',
'mode': 'guide',
}
result = handle(payload)
assert isinstance(result, dict)
text = result['result']
assert f'- Skill slug: {SKILL_NAME}' in text
assert '## Workflow' in text
assert '## Output format' in text
assert 'Need help turning this into a balanced play plan.' in text
assert 'Loaded from SKILL.md' in text
def test_meta_mode_returns_json():
result = handle({'skill_name': SKILL_NAME, 'mode': 'meta'})['result']
meta = json.loads(result)
assert meta['name'] == SKILL_NAME
assert meta['description']
def test_prompt_mode_returns_skill_body():
result = handle({'skill_name': SKILL_NAME, 'mode': 'prompt'})['result']
assert result.startswith('# ')
assert '## Workflow' in result
if __name__ == '__main__':
for name, fn in list(globals().items()):
if name.startswith('test_') and callable(fn):
fn()
print('All tests passed.')
Manage energy for a high-pressure period by planning pre-fight storage, mid-fight pacing, recovery windows, red lines, and overload warnings. Use when the us...
---
name: boss-fight-stamina-manager
description: Manage energy for a high-pressure period by planning pre-fight storage, mid-fight pacing, recovery windows, red lines, and overload warnings. Use when the user is preparing for exams, deadlines, travel, launches, or other boss-fight periods.
---
# Boss Fight Stamina Manager
Chinese name: BOSS 战精力管理
## Purpose
Help the user budget physical, mental, and emotional stamina for a demanding period so they do not burn out before the key moment.
This skill is descriptive only. It does not replace medical care, wearables, or health tracking systems.
## Use this skill when
- The user is entering a high-pressure sprint and wants to avoid the crash cycle.
- Important work is coming, but recovery keeps getting treated as optional.
- The user can push hard for a while and then suddenly drops offline.
- The user needs a clearer line between necessary effort and dangerous overextension.
## Inputs to collect
- The boss fight period, stakes, and timing.
- Energy peaks, low points, and known drain sources.
- Available recovery methods, sleep constraints, and non-negotiable duties.
- Warning signs that usually appear before overload.
## Workflow
1. Define the pressure window and separate it into pre-fight, fight, and recovery phases.
2. Estimate stamina demand across deep work, coordination, logistics, and emotional load.
3. Set a realistic energy budget with fueling actions and protected recovery blocks.
4. Mark red lines, overload signals, and what must be cut first if capacity shrinks.
5. End with a shortest viable recovery routine before the next round begins.
## Output Format
- Stamina budget with high-load, medium-load, and recovery zones.
- Pre-fight refuel actions such as sleep, nutrition, prep, and information reduction.
- In-fight pacing rules for when to push and when to conserve.
- Post-fight recovery reset with red lines and overload warnings.
## Quality bar
- Treat recovery as part of the main plan, not an optional bonus.
- Match the budget to real life, not fantasy concentration levels.
- State at least one non-negotiable red line the user should not cross.
- Give practical reduction priorities if everything cannot fit.
## Edge cases and limits
- If the user reports alarming health symptoms or severe ongoing sleep problems, advise real-world professional support.
- If the high-pressure task cannot shrink, cut optional damage first instead of pretending capacity will expand.
- Do not frame this skill as medical advice, athletic coaching, or formal health management.
## Compatibility notes
- Can pair conceptually with boss-battle-calendar and daily-dungeon-challenger.
- Useful for exams, launches, travel prep, deadlines, and family logistics surges.
- Text only, with no device or calendar integration.
FILE:handler.py
import json
import re
from pathlib import Path
SKILL_DIR = Path(__file__).resolve().parent
SKILL_FILE = SKILL_DIR / "SKILL.md"
def _read_skill_file():
return SKILL_FILE.read_text(encoding="utf-8")
def _split_frontmatter(content):
match = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)$", content, re.DOTALL)
if match:
return match.group(1), match.group(2)
return "", content
def _parse_frontmatter(frontmatter):
meta = {}
for line in frontmatter.splitlines():
line = line.strip()
if not line or ":" not in line:
continue
key, value = line.split(":", 1)
meta[key.strip()] = value.strip().strip('"').strip("'")
return meta
def _load_skill_meta(skill_name):
content = _read_skill_file()
frontmatter, _ = _split_frontmatter(content)
parsed = _parse_frontmatter(frontmatter)
return {
"name": parsed.get("name", skill_name or SKILL_DIR.name),
"description": parsed.get("description", ""),
}
def _load_prompt_template(skill_name):
del skill_name
content = _read_skill_file()
_, body = _split_frontmatter(content)
return body.strip()
def _extract_title(body):
match = re.search(r"^#\s+(.+)$", body, re.MULTILINE)
if match:
return match.group(1).strip()
return SKILL_DIR.name.replace("-", " ").title()
def _extract_chinese_name(body):
match = re.search(r"^Chinese name:\s*(.+)$", body, re.MULTILINE | re.IGNORECASE)
return match.group(1).strip() if match else ""
def _parse_sections(body):
sections = {}
pattern = re.compile(r"^##\s+(.+?)\n(.*?)(?=^##\s+|\Z)", re.MULTILINE | re.DOTALL)
for heading, content in pattern.findall(body):
sections[heading.strip().lower()] = content.strip()
return sections
def _normalize_input(user_input):
if user_input is None:
return ""
if isinstance(user_input, str):
return user_input.strip()
return json.dumps(user_input, ensure_ascii=False, indent=2, sort_keys=True)
def _extract_items(text):
items = []
for raw_line in text.splitlines():
line = raw_line.strip()
if not line:
continue
if re.match(r"^[-*]\s+", line):
items.append(re.sub(r"^[-*]\s+", "", line))
elif re.match(r"^\d+\.\s+", line):
items.append(re.sub(r"^\d+\.\s+", "", line))
if items:
return items
collapsed = " ".join(line.strip() for line in text.splitlines() if line.strip())
return [collapsed] if collapsed else []
def _limit_for_mode(mode):
normalized = (mode or "guide").strip().lower()
if normalized in {"brief", "compact", "quick"}:
return 2
if normalized in {"full", "deep", "extended"}:
return None
return 4
def _render_list(title, items, mode, ordered=False):
limit = _limit_for_mode(mode)
chosen = items if limit is None else items[:limit]
if not chosen:
return []
lines = [f"## {title}"]
for index, item in enumerate(chosen, start=1):
prefix = f"{index}." if ordered else "-"
lines.append(f"{prefix} {item}")
lines.append("")
return lines
def handle(args):
if not isinstance(args, dict):
args = {"input": args}
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
normalized_mode = (mode or "guide").strip().lower()
if normalized_mode == "meta":
return {"result": json.dumps(meta, ensure_ascii=False, indent=2)}
if normalized_mode in {"prompt", "template"}:
return {"result": template}
sections = _parse_sections(template)
title = _extract_title(template)
chinese_name = _extract_chinese_name(template)
normalized_input = _normalize_input(user_input)
lines = [f"# {title}"]
lines.append(f"- Skill slug: {meta['name']}")
if chinese_name:
lines.append(f"- Chinese name: {chinese_name}")
lines.append(f"- Mode: {mode}")
if meta["description"]:
lines.append(f"- Summary: {meta['description']}")
lines.append("")
if normalized_input:
lines.append("## User context")
lines.append(normalized_input)
lines.append("")
lines.extend(_render_list("Purpose", _extract_items(sections.get("purpose", "")), mode))
lines.extend(_render_list("Use cases", _extract_items(sections.get("use this skill when", "")), mode))
lines.extend(_render_list("Inputs to collect", _extract_items(sections.get("inputs to collect", "")), mode))
lines.extend(_render_list("Workflow", _extract_items(sections.get("workflow", "")), mode, ordered=True))
lines.extend(_render_list("Output format", _extract_items(sections.get("output format", "")), mode))
lines.extend(_render_list("Quality bar", _extract_items(sections.get("quality bar", "")), mode))
lines.extend(_render_list("Edge cases and limits", _extract_items(sections.get("edge cases and limits", "")), mode))
lines.extend(_render_list("Compatibility notes", _extract_items(sections.get("compatibility notes", "")), mode))
section_count = len([name for name in sections if sections[name]])
lines.append("## Prompt template status")
lines.append(f"- Loaded from SKILL.md with {section_count} structured sections.")
lines.append("- This handler returns a descriptive guidance card and does not perform external actions.")
return {"result": "\n".join(lines).strip()}
if __name__ == "__main__":
print(json.dumps(handle({"skill_name": SKILL_DIR.name, "input": "demo", "mode": "guide"}), ensure_ascii=False, indent=2))
FILE:tests/test_handler.py
#!/usr/bin/env python3
import json
import os
import sys
from pathlib import Path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from handler import _load_prompt_template, _load_skill_meta, handle
SKILL_DIR = Path(__file__).resolve().parents[1]
SKILL_NAME = SKILL_DIR.name
def test_load_skill_meta():
meta = _load_skill_meta(SKILL_NAME)
assert meta['name'] == SKILL_NAME
assert meta['description']
def test_prompt_template_sections_exist():
template = _load_prompt_template(SKILL_NAME)
for heading in ['## Purpose', '## Workflow', '## Output Format', '## Quality bar']:
assert heading in template
def test_handle_returns_descriptive_card():
payload = {
'skill_name': SKILL_NAME,
'input': 'Need help turning this into a balanced play plan.',
'mode': 'guide',
}
result = handle(payload)
assert isinstance(result, dict)
text = result['result']
assert f'- Skill slug: {SKILL_NAME}' in text
assert '## Workflow' in text
assert '## Output format' in text
assert 'Need help turning this into a balanced play plan.' in text
assert 'Loaded from SKILL.md' in text
def test_meta_mode_returns_json():
result = handle({'skill_name': SKILL_NAME, 'mode': 'meta'})['result']
meta = json.loads(result)
assert meta['name'] == SKILL_NAME
assert meta['description']
def test_prompt_mode_returns_skill_body():
result = handle({'skill_name': SKILL_NAME, 'mode': 'prompt'})['result']
assert result.startswith('# ')
assert '## Workflow' in result
if __name__ == '__main__':
for name, fn in list(globals().items()):
if name.startswith('test_') and callable(fn):
fn()
print('All tests passed.')
Turn a shared task into a co-op mission with clear roles, handoff points, sync rhythm, fallback coverage, and low-conflict alignment language. Use when two o...
---
name: co-op-mission-planner
description: Turn a shared task into a co-op mission with clear roles, handoff points, sync rhythm, fallback coverage, and low-conflict alignment language. Use when two or more people need to coordinate a family task, study effort, or small project.
---
# Co-op Mission Planner
Chinese name: 合作任务规划
## Purpose
Help the user translate a shared responsibility into a co-op mission with visible roles, sequence, and backup plans.
This skill is descriptive only. It does not create real task boards or send coordination messages.
## Use this skill when
- Two or more people need to collaborate and responsibility feels fuzzy.
- The user wants cleaner handoffs in a family task, study partnership, or small project.
- Everyone assumes someone else will handle the next step.
- The user wants a gentler way to talk about roles without sounding bossy.
## Inputs to collect
- Shared objective and what “mission clear” means.
- Participants, strengths, constraints, and availability windows.
- Known blockers, dependencies, and likely drop points.
- Preferred communication tone and sync frequency.
## Workflow
1. Clarify the mission objective, finish line, and who is in the party.
2. Map players into lead, support, logistics, observer, or cleanup roles when useful.
3. Break the mission into steps with explicit owners, handoff items, and timing.
4. Add sync checkpoints, fallback coverage, and what happens if someone drops offline.
5. End with a low-conflict alignment script the user can say out loud.
## Output Format
- Mission objective with a clear win condition.
- Role assignment with who leads, supports, and closes.
- Co-op flow showing sequence, handoffs, and sync points.
- Risk and fallback notes for drop-offs, delays, or unclear ownership.
## Quality bar
- Roles must be concrete enough that no one needs to guess ownership.
- Every important step should include a visible handoff object or confirmation point.
- Include at least one natural sentence the user can use to align without escalating tension.
- Keep the plan light enough for informal collaboration, not corporate process theater.
## Edge cases and limits
- If collaborators do not actually share the same goal, address alignment before task splitting.
- If the relationship is sensitive, use softer phrasing and smaller commitments.
- Do not position this skill as a substitute for formal project management, contracts, or governance.
## Compatibility notes
- Works for partners, families, friends, study buddies, and lightweight teams.
- Can pair conceptually with npc-dialogue-rehearser or quest-chain-decomposer.
- Text only, with no live board creation.
FILE:handler.py
import json
import re
from pathlib import Path
SKILL_DIR = Path(__file__).resolve().parent
SKILL_FILE = SKILL_DIR / "SKILL.md"
def _read_skill_file():
return SKILL_FILE.read_text(encoding="utf-8")
def _split_frontmatter(content):
match = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)$", content, re.DOTALL)
if match:
return match.group(1), match.group(2)
return "", content
def _parse_frontmatter(frontmatter):
meta = {}
for line in frontmatter.splitlines():
line = line.strip()
if not line or ":" not in line:
continue
key, value = line.split(":", 1)
meta[key.strip()] = value.strip().strip('"').strip("'")
return meta
def _load_skill_meta(skill_name):
content = _read_skill_file()
frontmatter, _ = _split_frontmatter(content)
parsed = _parse_frontmatter(frontmatter)
return {
"name": parsed.get("name", skill_name or SKILL_DIR.name),
"description": parsed.get("description", ""),
}
def _load_prompt_template(skill_name):
del skill_name
content = _read_skill_file()
_, body = _split_frontmatter(content)
return body.strip()
def _extract_title(body):
match = re.search(r"^#\s+(.+)$", body, re.MULTILINE)
if match:
return match.group(1).strip()
return SKILL_DIR.name.replace("-", " ").title()
def _extract_chinese_name(body):
match = re.search(r"^Chinese name:\s*(.+)$", body, re.MULTILINE | re.IGNORECASE)
return match.group(1).strip() if match else ""
def _parse_sections(body):
sections = {}
pattern = re.compile(r"^##\s+(.+?)\n(.*?)(?=^##\s+|\Z)", re.MULTILINE | re.DOTALL)
for heading, content in pattern.findall(body):
sections[heading.strip().lower()] = content.strip()
return sections
def _normalize_input(user_input):
if user_input is None:
return ""
if isinstance(user_input, str):
return user_input.strip()
return json.dumps(user_input, ensure_ascii=False, indent=2, sort_keys=True)
def _extract_items(text):
items = []
for raw_line in text.splitlines():
line = raw_line.strip()
if not line:
continue
if re.match(r"^[-*]\s+", line):
items.append(re.sub(r"^[-*]\s+", "", line))
elif re.match(r"^\d+\.\s+", line):
items.append(re.sub(r"^\d+\.\s+", "", line))
if items:
return items
collapsed = " ".join(line.strip() for line in text.splitlines() if line.strip())
return [collapsed] if collapsed else []
def _limit_for_mode(mode):
normalized = (mode or "guide").strip().lower()
if normalized in {"brief", "compact", "quick"}:
return 2
if normalized in {"full", "deep", "extended"}:
return None
return 4
def _render_list(title, items, mode, ordered=False):
limit = _limit_for_mode(mode)
chosen = items if limit is None else items[:limit]
if not chosen:
return []
lines = [f"## {title}"]
for index, item in enumerate(chosen, start=1):
prefix = f"{index}." if ordered else "-"
lines.append(f"{prefix} {item}")
lines.append("")
return lines
def handle(args):
if not isinstance(args, dict):
args = {"input": args}
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
normalized_mode = (mode or "guide").strip().lower()
if normalized_mode == "meta":
return {"result": json.dumps(meta, ensure_ascii=False, indent=2)}
if normalized_mode in {"prompt", "template"}:
return {"result": template}
sections = _parse_sections(template)
title = _extract_title(template)
chinese_name = _extract_chinese_name(template)
normalized_input = _normalize_input(user_input)
lines = [f"# {title}"]
lines.append(f"- Skill slug: {meta['name']}")
if chinese_name:
lines.append(f"- Chinese name: {chinese_name}")
lines.append(f"- Mode: {mode}")
if meta["description"]:
lines.append(f"- Summary: {meta['description']}")
lines.append("")
if normalized_input:
lines.append("## User context")
lines.append(normalized_input)
lines.append("")
lines.extend(_render_list("Purpose", _extract_items(sections.get("purpose", "")), mode))
lines.extend(_render_list("Use cases", _extract_items(sections.get("use this skill when", "")), mode))
lines.extend(_render_list("Inputs to collect", _extract_items(sections.get("inputs to collect", "")), mode))
lines.extend(_render_list("Workflow", _extract_items(sections.get("workflow", "")), mode, ordered=True))
lines.extend(_render_list("Output format", _extract_items(sections.get("output format", "")), mode))
lines.extend(_render_list("Quality bar", _extract_items(sections.get("quality bar", "")), mode))
lines.extend(_render_list("Edge cases and limits", _extract_items(sections.get("edge cases and limits", "")), mode))
lines.extend(_render_list("Compatibility notes", _extract_items(sections.get("compatibility notes", "")), mode))
section_count = len([name for name in sections if sections[name]])
lines.append("## Prompt template status")
lines.append(f"- Loaded from SKILL.md with {section_count} structured sections.")
lines.append("- This handler returns a descriptive guidance card and does not perform external actions.")
return {"result": "\n".join(lines).strip()}
if __name__ == "__main__":
print(json.dumps(handle({"skill_name": SKILL_DIR.name, "input": "demo", "mode": "guide"}), ensure_ascii=False, indent=2))
FILE:tests/test_handler.py
#!/usr/bin/env python3
import json
import os
import sys
from pathlib import Path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from handler import _load_prompt_template, _load_skill_meta, handle
SKILL_DIR = Path(__file__).resolve().parents[1]
SKILL_NAME = SKILL_DIR.name
def test_load_skill_meta():
meta = _load_skill_meta(SKILL_NAME)
assert meta['name'] == SKILL_NAME
assert meta['description']
def test_prompt_template_sections_exist():
template = _load_prompt_template(SKILL_NAME)
for heading in ['## Purpose', '## Workflow', '## Output Format', '## Quality bar']:
assert heading in template
def test_handle_returns_descriptive_card():
payload = {
'skill_name': SKILL_NAME,
'input': 'Need help turning this into a balanced play plan.',
'mode': 'guide',
}
result = handle(payload)
assert isinstance(result, dict)
text = result['result']
assert f'- Skill slug: {SKILL_NAME}' in text
assert '## Workflow' in text
assert '## Output format' in text
assert 'Need help turning this into a balanced play plan.' in text
assert 'Loaded from SKILL.md' in text
def test_meta_mode_returns_json():
result = handle({'skill_name': SKILL_NAME, 'mode': 'meta'})['result']
meta = json.loads(result)
assert meta['name'] == SKILL_NAME
assert meta['description']
def test_prompt_mode_returns_skill_body():
result = handle({'skill_name': SKILL_NAME, 'mode': 'prompt'})['result']
assert result.startswith('# ')
assert '## Workflow' in result
if __name__ == '__main__':
for name, fn in list(globals().items()):
if name.startswith('test_') and callable(fn):
fn()
print('All tests passed.')
Design low-friction idle, light-interaction, and micro-progress actions for fragmented or low-energy time while protecting recovery. Use when the user wants...
---
name: idle-reward-optimizer
description: Design low-friction idle, light-interaction, and micro-progress actions for fragmented or low-energy time while protecting recovery. Use when the user wants gentle gains from waiting windows, transitions, or tired periods without over-optimizing every minute.
---
# Idle Reward Optimizer
Chinese name: 挂机收益优化
## Purpose
Help the user turn fragmented or low-energy windows into gentle progress loops without stealing recovery.
This skill is descriptive only. It does not create reminders, automations, or time-tracking systems.
## Use this skill when
- The user keeps losing small pockets of time to mindless scrolling.
- The user wants useful actions for waiting, commuting, transitions, or recovery periods.
- The user has low energy and needs options lighter than full-focus work.
- The user wants a repeatable “idle reward” system that feels kind instead of punishing.
## Inputs to collect
- Fragmented time windows and their usual length.
- Low-energy periods, common locations, and interruption level.
- Tasks or themes that benefit from tiny amounts of progress.
- Recovery needs, boundaries, and times that should stay empty.
## Workflow
1. Map the user’s fragmented windows, low-energy zones, and common waiting scenes.
2. Sort candidate actions into idle, light interaction, micro-progress, and maintenance buckets.
3. Match each scene with one low-friction action pack that fits the real energy cost.
4. Add reuse rules so the user can repeat the pack without re-deciding every time.
5. End with leave-blank rules for windows that should stay restful.
## Output Format
- Fragmented time map with scene, energy level, and safe action intensity.
- Idle reward actions that need almost no thought.
- Micro-progress actions that fit inside one to five minutes.
- Leave-blank rules that protect rest and recovery.
## Quality bar
- Protect recovery first, instead of trying to monetize every spare minute.
- Every suggested action must be genuinely light enough for the stated context.
- Include at least one reusable action loop that can compound over time.
- Keep the plan realistic for family life, commuting, or interruptions.
## Edge cases and limits
- If the user sounds depleted, prioritize restorative idle options before productivity ideas.
- If time windows are highly unpredictable, use scene-based menus rather than fixed schedules.
- Do not present this skill as a replacement for timers, trackers, or automation tools.
## Compatibility notes
- Can pair conceptually with game-inventory-manager and boss-fight-stamina-manager.
- Works well for family life, commuting gaps, transition time, and recovery periods.
- Text only, with no reminder or scheduling integration.
FILE:handler.py
import json
import re
from pathlib import Path
SKILL_DIR = Path(__file__).resolve().parent
SKILL_FILE = SKILL_DIR / "SKILL.md"
def _read_skill_file():
return SKILL_FILE.read_text(encoding="utf-8")
def _split_frontmatter(content):
match = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)$", content, re.DOTALL)
if match:
return match.group(1), match.group(2)
return "", content
def _parse_frontmatter(frontmatter):
meta = {}
for line in frontmatter.splitlines():
line = line.strip()
if not line or ":" not in line:
continue
key, value = line.split(":", 1)
meta[key.strip()] = value.strip().strip('"').strip("'")
return meta
def _load_skill_meta(skill_name):
content = _read_skill_file()
frontmatter, _ = _split_frontmatter(content)
parsed = _parse_frontmatter(frontmatter)
return {
"name": parsed.get("name", skill_name or SKILL_DIR.name),
"description": parsed.get("description", ""),
}
def _load_prompt_template(skill_name):
del skill_name
content = _read_skill_file()
_, body = _split_frontmatter(content)
return body.strip()
def _extract_title(body):
match = re.search(r"^#\s+(.+)$", body, re.MULTILINE)
if match:
return match.group(1).strip()
return SKILL_DIR.name.replace("-", " ").title()
def _extract_chinese_name(body):
match = re.search(r"^Chinese name:\s*(.+)$", body, re.MULTILINE | re.IGNORECASE)
return match.group(1).strip() if match else ""
def _parse_sections(body):
sections = {}
pattern = re.compile(r"^##\s+(.+?)\n(.*?)(?=^##\s+|\Z)", re.MULTILINE | re.DOTALL)
for heading, content in pattern.findall(body):
sections[heading.strip().lower()] = content.strip()
return sections
def _normalize_input(user_input):
if user_input is None:
return ""
if isinstance(user_input, str):
return user_input.strip()
return json.dumps(user_input, ensure_ascii=False, indent=2, sort_keys=True)
def _extract_items(text):
items = []
for raw_line in text.splitlines():
line = raw_line.strip()
if not line:
continue
if re.match(r"^[-*]\s+", line):
items.append(re.sub(r"^[-*]\s+", "", line))
elif re.match(r"^\d+\.\s+", line):
items.append(re.sub(r"^\d+\.\s+", "", line))
if items:
return items
collapsed = " ".join(line.strip() for line in text.splitlines() if line.strip())
return [collapsed] if collapsed else []
def _limit_for_mode(mode):
normalized = (mode or "guide").strip().lower()
if normalized in {"brief", "compact", "quick"}:
return 2
if normalized in {"full", "deep", "extended"}:
return None
return 4
def _render_list(title, items, mode, ordered=False):
limit = _limit_for_mode(mode)
chosen = items if limit is None else items[:limit]
if not chosen:
return []
lines = [f"## {title}"]
for index, item in enumerate(chosen, start=1):
prefix = f"{index}." if ordered else "-"
lines.append(f"{prefix} {item}")
lines.append("")
return lines
def handle(args):
if not isinstance(args, dict):
args = {"input": args}
skill_name = args.get("skill_name", "")
user_input = args.get("input", "")
mode = args.get("mode", "guide")
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
normalized_mode = (mode or "guide").strip().lower()
if normalized_mode == "meta":
return {"result": json.dumps(meta, ensure_ascii=False, indent=2)}
if normalized_mode in {"prompt", "template"}:
return {"result": template}
sections = _parse_sections(template)
title = _extract_title(template)
chinese_name = _extract_chinese_name(template)
normalized_input = _normalize_input(user_input)
lines = [f"# {title}"]
lines.append(f"- Skill slug: {meta['name']}")
if chinese_name:
lines.append(f"- Chinese name: {chinese_name}")
lines.append(f"- Mode: {mode}")
if meta["description"]:
lines.append(f"- Summary: {meta['description']}")
lines.append("")
if normalized_input:
lines.append("## User context")
lines.append(normalized_input)
lines.append("")
lines.extend(_render_list("Purpose", _extract_items(sections.get("purpose", "")), mode))
lines.extend(_render_list("Use cases", _extract_items(sections.get("use this skill when", "")), mode))
lines.extend(_render_list("Inputs to collect", _extract_items(sections.get("inputs to collect", "")), mode))
lines.extend(_render_list("Workflow", _extract_items(sections.get("workflow", "")), mode, ordered=True))
lines.extend(_render_list("Output format", _extract_items(sections.get("output format", "")), mode))
lines.extend(_render_list("Quality bar", _extract_items(sections.get("quality bar", "")), mode))
lines.extend(_render_list("Edge cases and limits", _extract_items(sections.get("edge cases and limits", "")), mode))
lines.extend(_render_list("Compatibility notes", _extract_items(sections.get("compatibility notes", "")), mode))
section_count = len([name for name in sections if sections[name]])
lines.append("## Prompt template status")
lines.append(f"- Loaded from SKILL.md with {section_count} structured sections.")
lines.append("- This handler returns a descriptive guidance card and does not perform external actions.")
return {"result": "\n".join(lines).strip()}
if __name__ == "__main__":
print(json.dumps(handle({"skill_name": SKILL_DIR.name, "input": "demo", "mode": "guide"}), ensure_ascii=False, indent=2))
FILE:tests/test_handler.py
#!/usr/bin/env python3
import json
import os
import sys
from pathlib import Path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from handler import _load_prompt_template, _load_skill_meta, handle
SKILL_DIR = Path(__file__).resolve().parents[1]
SKILL_NAME = SKILL_DIR.name
def test_load_skill_meta():
meta = _load_skill_meta(SKILL_NAME)
assert meta['name'] == SKILL_NAME
assert meta['description']
def test_prompt_template_sections_exist():
template = _load_prompt_template(SKILL_NAME)
for heading in ['## Purpose', '## Workflow', '## Output Format', '## Quality bar']:
assert heading in template
def test_handle_returns_descriptive_card():
payload = {
'skill_name': SKILL_NAME,
'input': 'Need help turning this into a balanced play plan.',
'mode': 'guide',
}
result = handle(payload)
assert isinstance(result, dict)
text = result['result']
assert f'- Skill slug: {SKILL_NAME}' in text
assert '## Workflow' in text
assert '## Output format' in text
assert 'Need help turning this into a balanced play plan.' in text
assert 'Loaded from SKILL.md' in text
def test_meta_mode_returns_json():
result = handle({'skill_name': SKILL_NAME, 'mode': 'meta'})['result']
meta = json.loads(result)
assert meta['name'] == SKILL_NAME
assert meta['description']
def test_prompt_mode_returns_skill_body():
result = handle({'skill_name': SKILL_NAME, 'mode': 'prompt'})['result']
assert result.startswith('# ')
assert '## Workflow' in result
if __name__ == '__main__':
for name, fn in list(globals().items()):
if name.startswith('test_') and callable(fn):
fn()
print('All tests passed.')
Match the user’s current mood, available time, platform habits, and energy level to the right kind of game experience, then suggest a low-friction way to sta...
---
name: gaming-backlog-guide
description: Match the user’s current mood, available time, platform habits, and energy level to the right kind of game experience, then suggest a low-friction way to start. Use when the user feels game paralysis or wants a healthier way to choose what to play next.
---
# Gaming Backlog Guide
Chinese name: 游戏荒推荐指南
## Purpose
Help the user find the right kind of game experience for the current moment instead of doom-scrolling the backlog or starting something that does not fit the available time and energy.
This skill is descriptive only. It does not fetch live release news, store prices, or review scores.
## Use this skill when
- The user wants to play something but cannot choose.
- The user feels low energy, bored, or stuck in backlog guilt.
- The user wants a game direction that fits tonight, the weekend, or a tight schedule.
- The user wants a better leisure match instead of a giant title dump.
## Inputs to collect
- Current mood
- Available play time
- Platform habits
- Budget or backlog pressure
- Recently played genres
- Desired experience or feeling
## Workflow
1. Read mood, energy, time, platform, and budget signals.
2. Infer the kind of experience the user actually needs right now.
3. Recommend 2 to 3 game directions, not a random pile of titles.
4. Separate a play-now option, a weekend option, and one type to avoid for now.
5. End with one easiest-start suggestion.
## Output Format
- Current need profile
- Best-fit directions
- Start here
- Avoid for now
- Tonight’s easiest start
## Quality bar
- The recommendation must match time and energy honestly.
- The output should emphasize experience fit, not title spam.
- Include at least one option the user can start with low friction.
- Stay honest about not having live launch or pricing data.
## Edge cases and limits
- If the user wants brand-new release news, explain that this skill does not provide real-time launch data.
- If budget is tight, prefer backlog, free, or low-cost directions.
- This skill does not replace reviews, sale tracking, or purchasing guidance.
## Compatibility notes
- Works for solo leisure planning, family play, and reward-based downtime.
- Can pair conceptually with gaming-session-scheduler.
- Fully dialogue-based, no store integration required.
FILE:handler.py
#!/usr/bin/env python3
import json
import os
import re
from typing import Any, Dict, List, Tuple
SKILL_SLUG = 'gaming-backlog-guide'
PROMPT_TEMPLATE = """Match the user's current mood, time, energy, and platform habits to the right kind of game experience.
Recommend the best-fit directions, separate a play-now option from a weekend option, flag one type to avoid for now, and end with the easiest possible start."""
PROFILES = {
'low_friction': {
'label': 'Low-friction decompression',
'fit': 'Short, low-energy windows where starting matters more than mastery.',
'entry': 'Choose sessions that reset cleanly in 10 to 30 minutes, such as short runs, puzzle bursts, or bite-size matches.',
'example_direction': 'short roguelite runs, puzzle slices, racing laps, or quick arcade loops',
'avoid_when': 'you are hoping for a giant emotional journey tonight but only have scraps of attention',
},
'comfort': {
'label': 'Comfort companion',
'fit': 'Moments when the user wants something familiar, warm, and emotionally safe.',
'entry': 'Return to a known world, low-stakes sim, or familiar progression loop that does not need a fresh tutorial.',
'example_direction': 'cozy management, life sim check-ins, familiar RPG side content, or soft routine games',
'avoid_when': 'you are clearly bored by your existing habits and need novelty instead of emotional safety',
},
'novelty': {
'label': 'Fresh novelty sampler',
'fit': 'When the user is bored, stale, or curious and needs a new flavor without a giant commitment.',
'entry': 'Sample one new genre, demo, short indie, or self-contained first hour before committing to a major campaign.',
'example_direction': 'demo-first exploration, a short indie experiment, or one unfamiliar genre with a low entry cost',
'avoid_when': 'the user is exhausted enough that even learning a new control scheme feels expensive',
},
'immersion': {
'label': 'Deep immersion weekend arc',
'fit': 'Longer windows with enough energy for story, systems, or campaign depth.',
'entry': 'Open a title that rewards 90 minutes or more, ideally with one clear chapter or objective to finish.',
'example_direction': 'story RPG arcs, strategy campaigns, long-form survival crafting, or open-world quest blocks',
'avoid_when': 'the available time is short or the user is already mentally drained',
},
'social': {
'label': 'Shared couch or co-op energy',
'fit': 'Moments when play is really about connection, not solo optimization.',
'entry': 'Pick something with quick onboarding, short rounds, and room for conversation or teamwork.',
'example_direction': 'couch co-op, party co-op, family-friendly challenge runs, or asymmetrical shared play',
'avoid_when': 'the current mood is private recovery and the user does not want extra coordination',
},
}
def _load_skill_meta(skill_name: str) -> Dict[str, str]:
path = os.path.join(os.path.dirname(__file__), 'SKILL.md')
with open(path, 'r', encoding='utf-8') as handle:
content = handle.read()
match = re.search(r'^---\s*\n(.*?)\n---\s*', content, re.DOTALL | re.MULTILINE)
meta: Dict[str, str] = {}
if match:
for line in match.group(1).splitlines():
if ':' in line:
key, value = line.split(':', 1)
meta[key.strip()] = value.strip()
meta['skill_name'] = skill_name or meta.get('name', SKILL_SLUG)
return meta
def _load_prompt_template(skill_name: str) -> str:
del skill_name
return PROMPT_TEMPLATE
def _normalize_key(key: Any) -> str:
return re.sub(r'[^a-z0-9_\u4e00-\u9fff]+', '_', str(key).strip().lower()).strip('_')
def _clean_text(value: Any) -> str:
if value is None:
return ''
if isinstance(value, str):
return value.strip()
if isinstance(value, (int, float)):
return str(value)
return json.dumps(value, ensure_ascii=False)
def _normalize_map(data: Dict[str, Any]) -> Dict[str, Any]:
return {_normalize_key(key): value for key, value in data.items()}
def _parse_text_map(raw: str) -> Dict[str, Any]:
data: Dict[str, Any] = {}
for line in raw.splitlines():
if re.search(r'[::]', line):
key, value = re.split(r'[::]', line, maxsplit=1)
data[_normalize_key(key)] = value.strip()
return data
def _coerce_input(user_input: Any) -> Tuple[str, Dict[str, Any]]:
if isinstance(user_input, dict):
return json.dumps(user_input, ensure_ascii=False), _normalize_map(user_input)
raw = _clean_text(user_input)
if raw.startswith('{') and raw.endswith('}'):
try:
parsed = json.loads(raw)
if isinstance(parsed, dict):
return raw, _normalize_map(parsed)
except json.JSONDecodeError:
pass
return raw, _parse_text_map(raw)
def _pick(data: Dict[str, Any], keys: List[str], default: str = '') -> str:
for key in keys:
normalized = _normalize_key(key)
if normalized in data:
text = _clean_text(data[normalized])
if text:
return text
return default
def _to_int(value: Any, default: int = 0) -> int:
if isinstance(value, int):
return value
text = _clean_text(value)
match = re.search(r'(\d+)', text)
return int(match.group(1)) if match else default
def _contains_any(text: str, keywords: List[str]) -> bool:
lowered = text.lower()
return any(word in lowered for word in keywords)
def _score_profiles(raw: str, data: Dict[str, Any]) -> Tuple[Dict[str, int], int, str, str, str]:
mood = _pick(data, ['mood', 'energy', 'emotion', '心情'])
time_text = _pick(data, ['time', 'minutes', 'available_time', '空闲时间'])
platform = _pick(data, ['platform', 'device', '平台'], 'not specified')
budget = _pick(data, ['budget', 'price_limit', '预算'], 'not specified')
recent = _pick(data, ['recent_games', 'recent_types', 'genres', '最近在玩'])
desired = _pick(data, ['desired_experience', 'want', 'goal', '想要的体验'])
together = _pick(data, ['with_whom', 'co_op', 'group', '一起玩'])
minutes = _to_int(time_text, 0)
context = ' '.join([raw, mood, platform, budget, recent, desired, together]).lower()
scores = {name: 0 for name in PROFILES}
if minutes:
if minutes <= 30:
scores['low_friction'] += 5
scores['comfort'] += 2
scores['immersion'] -= 2
elif minutes <= 90:
scores['comfort'] += 2
scores['novelty'] += 2
else:
scores['immersion'] += 5
scores['comfort'] += 1
if _contains_any(context, ['tired', 'drained', 'low energy', 'decompress', 'unwind', '疲惫', '放松']):
scores['low_friction'] += 4
scores['comfort'] += 2
scores['immersion'] -= 1
if _contains_any(context, ['cozy', 'warm', 'familiar', 'safe', 'comfort', '治愈', '熟悉']):
scores['comfort'] += 4
if _contains_any(context, ['bored', 'stale', 'fresh', 'new', 'curious', 'novelty', '腻了', '新鲜感']):
scores['novelty'] += 4
if _contains_any(context, ['story', 'immersive', 'epic', 'campaign', 'deep', '沉浸', '主线']):
scores['immersion'] += 4
if _contains_any(context, ['friend', 'family', 'partner', 'co-op', 'coop', 'couch', 'together', '亲子', '一起']):
scores['social'] += 5
if _contains_any(context, ['budget', 'cheap', 'free', 'backlog', 'already own', '低成本', '库存']) or 'low' in budget.lower():
scores['comfort'] += 1
scores['novelty'] += 1
if all(score == 0 for score in scores.values()):
scores['comfort'] = 2
scores['low_friction'] = 1
return scores, minutes, platform, budget, recent or 'not specified'
def _rank_profiles(scores: Dict[str, int]) -> List[Tuple[str, int]]:
return sorted(scores.items(), key=lambda item: (item[1], PROFILES[item[0]]['label']), reverse=True)
def _tonight_start(profile_id: str, minutes: int, budget: str) -> str:
if profile_id == 'low_friction':
return 'Pick something you can launch in under 2 minutes and finish or pause cleanly inside one short sitting.'
if profile_id == 'comfort':
return 'Reopen a familiar save or a known cozy loop so the first five minutes feel frictionless.'
if profile_id == 'novelty':
return 'Try one demo, one short indie, or one new genre sample, but cap the experiment at the first 30 to 45 minutes.'
if profile_id == 'immersion':
return 'Open the campaign only if you can protect a real block of time and stop after one chapter or objective.'
note = 'Keep setup and explanation light so the social energy goes into play instead of logistics.'
if 'low' in budget.lower() or 'not specified' in budget.lower():
note += ' Prefer something already in the library or available at low cost.'
return note
def _build_result(raw: str, data: Dict[str, Any], template: str) -> str:
scores, minutes, platform, budget, recent = _score_profiles(raw, data)
ranked = _rank_profiles(scores)
top_ids = [item[0] for item in ranked[:3]]
avoid_id = ranked[-1][0]
play_now = top_ids[0]
weekend = next((profile_id for profile_id in top_ids if profile_id == 'immersion'), top_ids[1] if len(top_ids) > 1 else top_ids[0])
lines: List[str] = []
lines.append('# Gaming Backlog Match')
lines.append('')
lines.append('## Current Need Profile')
lines.append(f'- Platform read: {platform}')
if minutes:
lines.append(f'- Available time: about {minutes} minutes')
else:
lines.append('- Available time: not fully specified, so the guide defaults to lower-friction choices first.')
lines.append(f'- Budget pressure: {budget}')
lines.append(f'- Recent pattern: {recent}')
lines.append(f"- Decision frame: {template.splitlines()[0]}")
lines.append('')
lines.append('## Best-Fit Directions')
for index, profile_id in enumerate(top_ids, start=1):
profile = PROFILES[profile_id]
lines.append(f"### {index}. {profile['label']}")
lines.append(f"- Why it fits: {profile['fit']}")
lines.append(f"- Entry advice: {profile['entry']}")
lines.append(f"- Example direction: {profile['example_direction']}")
lines.append('')
lines.append('## Start Here')
lines.append(f"- Play now: {PROFILES[play_now]['label']} — {_tonight_start(play_now, minutes, budget)}")
lines.append(f"- Weekend slot: {PROFILES[weekend]['label']} — {PROFILES[weekend]['entry']}")
if 'low' in budget.lower() or 'tight' in budget.lower() or 'not specified' in budget.lower():
lines.append('- Budget note: start with backlog, free, or already-owned options before adding purchase pressure.')
else:
lines.append('- Budget note: still prefer the experience match first, then decide whether new spending is justified.')
lines.append('')
lines.append('## Avoid for Now')
lines.append(f"- {PROFILES[avoid_id]['label']} — avoid this if {PROFILES[avoid_id]['avoid_when']}.")
lines.append('')
lines.append("## Tonight's Easiest Start")
lines.append(f"- {_tonight_start(play_now, minutes, budget)}")
return '\n'.join(lines)
def handle(args: Dict[str, Any]) -> Dict[str, str]:
skill_name = args.get('skill_name', SKILL_SLUG) or SKILL_SLUG
user_input = args.get('input', '')
mode = args.get('mode', 'guide')
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
if mode == 'meta':
return {'result': json.dumps(meta, ensure_ascii=False, indent=2)}
if mode == 'prompt':
return {'result': template}
raw, data = _coerce_input(user_input)
return {'result': _build_result(raw, data, template)}
if __name__ == '__main__':
demo = {
'mood': 'tired and want to decompress',
'time': 25,
'platform': 'Switch',
'budget': 'low',
'recent_types': 'long RPGs',
}
print(json.dumps(handle({'skill_name': SKILL_SLUG, 'input': demo}), ensure_ascii=False, indent=2))
FILE:tests/test_handler.py
#!/usr/bin/env python3
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from handler import handle
def test_low_friction_profile_for_tired_short_session():
result = handle({
'skill_name': 'gaming-backlog-guide',
'input': {
'mood': 'tired and want to decompress',
'time': 25,
'platform': 'Switch',
'budget': 'low',
'recent_types': 'long RPGs',
},
})['result']
assert 'Low-friction decompression' in result
assert "Tonight's Easiest Start" in result
assert 'Avoid for Now' in result
def test_social_case_is_supported():
result = handle({
'skill_name': 'gaming-backlog-guide',
'input': 'Mood: playful\nTime: 60 minutes\nPlatform: PC\nWith whom: family on the couch\nBudget: low',
})['result']
assert 'Best-Fit Directions' in result
assert ('Shared couch or co-op energy' in result) or ('Comfort companion' in result)
def test_prompt_mode_returns_template():
result = handle({'skill_name': 'gaming-backlog-guide', 'mode': 'prompt'})['result']
assert 'mood, time, energy' in result.lower()
assert 'weekend option' in result.lower()
if __name__ == '__main__':
for name, fn in list(globals().items()):
if name.startswith('test_') and callable(fn):
fn()
print('All tests passed.')
Reframe a messy real-life situation as a strategy game board so the user can see victory conditions, resource lines, phases, and the next best moves. Use whe...
---
name: strategy-game-mentor
description: Reframe a messy real-life situation as a strategy game board so the user can see victory conditions, resource lines, phases, and the next best moves. Use when the user needs clearer tradeoffs instead of reactive firefighting.
---
# Strategy Game Mentor
Chinese name: 策略游戏思维导师
## Purpose
Use strategy-game language to help the user shift from reacting turn by turn to thinking in positions, phases, and resource tradeoffs.
This skill is descriptive only. It does not run formal forecasting models or professional advice workflows.
## Use this skill when
- The user feels trapped in short-term reactions.
- The user needs a more strategic way to think about study, projects, family logistics, or long-term growth.
- The user wants opening, midgame, and endgame guidance.
- The user needs help seeing what to protect, contest, or let go.
## Inputs to collect
- Current situation
- Goal or victory condition
- Resources
- Constraints
- Main sources of pressure or resistance
- Time window
## Workflow
1. Read the current board state, resources, constraints, and pressure sources.
2. Infer the most useful strategic stance.
3. Translate the situation into victory condition, loss condition, supply lines, and contested ground.
4. Give opening, midgame, and endgame priorities.
5. End with decision reminders and one likely misread.
## Output Format
- Board state
- Recommended stance
- Three-phase plan
- Decision reminders
- Likely misread
## Quality bar
- The output must expose tradeoffs, not just stack more tasks.
- The advice must land on real next moves.
- At least one common misread should be named clearly.
- Stay honest when the situation has too many unknowns.
## Edge cases and limits
- If the real issue is execution paralysis, shrink the first move instead of pretending it is a pure strategy problem.
- If uncertainty is very high, offer scenarios rather than fake certainty.
- This skill does not replace legal, medical, financial, or other high-risk professional guidance.
## Compatibility notes
- Works for planning, study strategy, project sequencing, and life review.
- Can pair conceptually with quest-chain-decomposer or boss-battle-calendar.
- Fully dialogue-based, no real-time modeling required.
FILE:handler.py
#!/usr/bin/env python3
import json
import os
import re
from typing import Any, Dict, List, Tuple
SKILL_SLUG = 'strategy-game-mentor'
PROMPT_TEMPLATE = """Reframe the user's situation as a strategy game board.
Identify the best strategic stance, clarify victory and loss conditions, name supply lines and pressure points, then give opening, midgame, endgame, and decision reminders. Keep it practical, not theatrical."""
PROFILES = {
'stabilize': {
'label': 'Stabilize the Board',
'summary': 'Patch leaks and restore reliable supply lines before taking new fights.',
'opening': 'Stop the biggest leak, cut one optional front, and secure the next controllable move.',
'midgame': 'Rebuild repeatable routines, buffers, or visibility so surprises hurt less.',
'endgame': 'Expand only after the base stops wobbling for several turns.',
'protect': 'core energy, one essential deliverable, and the people or systems keeping the map functioning',
'contest': 'the recurring leak that keeps draining time or morale',
'let_go': 'side quests, prestige tasks, and anything that creates motion without stability',
'misread': 'Mistaking frantic activity for restored control.',
},
'positioning': {
'label': 'Claim Better Positioning',
'summary': 'Improve leverage, information, and options before forcing outcomes.',
'opening': 'Clarify the map, rank fronts by leverage, and move toward the position that makes later turns easier.',
'midgame': 'Build optionality, gather clean information, and avoid low-value skirmishes.',
'endgame': 'Convert the better position into a focused push when the path is clear.',
'protect': 'decision quality and the flexibility to respond after new information appears',
'contest': 'foggy priorities and scattered commitments',
'let_go': 'premature commitments made only to feel decisive',
'misread': 'Treating urgency as proof that a commitment should happen now.',
},
'timing_push': {
'label': 'Timing Push',
'summary': 'Concentrate force into a short window that matters more than perfect balance.',
'opening': 'Define the exact window, compress prep, and remove every distraction that does not help the push.',
'midgame': 'Protect execution bandwidth and keep support resources flowing into the main objective.',
'endgame': 'Close decisively, then stop instead of drifting into sloppy overtime.',
'protect': 'the narrow window, focus bandwidth, and the quality of the main attempt',
'contest': 'distraction, fragmentation, and fear-driven last-minute scope growth',
'let_go': 'nice-to-have improvements that belong after the push, not before it',
'misread': 'Trying to optimize every flank instead of winning the window that actually matters.',
},
'attrition': {
'label': 'Attrition and Sustainable Value',
'summary': 'Win by trading patiently, preserving endurance, and compounding small edges.',
'opening': 'Choose repeatable trades you can sustain and avoid flashy swings that break your own economy.',
'midgame': 'Keep pressure steady, document progress, and let the opponent or problem spend more energy than you do.',
'endgame': 'Close when the cumulative edge becomes obvious, not before.',
'protect': 'endurance, consistency, and evidence of incremental advantage',
'contest': 'slow leaks and recurring inefficiencies',
'let_go': 'dramatic but low-probability gambits',
'misread': 'Getting bored with compounding and abandoning the plan right before it pays off.',
},
'expansion': {
'label': 'Expand from Advantage',
'summary': 'Use a strong base to open new lanes without losing control of the old ones.',
'opening': 'Confirm the current base is real, then choose the one expansion lane with the best upside-to-friction ratio.',
'midgame': 'Add structure so the new front does not cannibalize the core base.',
'endgame': 'Lock in the gain and make sure the original engine still runs cleanly.',
'protect': 'the profitable base that funds the new move',
'contest': 'overextension and hidden maintenance costs',
'let_go': 'opening multiple new fronts at once just because momentum feels good',
'misread': 'Confusing a temporary high with actual spare capacity.',
},
}
def _load_skill_meta(skill_name: str) -> Dict[str, str]:
path = os.path.join(os.path.dirname(__file__), 'SKILL.md')
with open(path, 'r', encoding='utf-8') as handle:
content = handle.read()
match = re.search(r'^---\s*\n(.*?)\n---\s*', content, re.DOTALL | re.MULTILINE)
meta: Dict[str, str] = {}
if match:
for line in match.group(1).splitlines():
if ':' in line:
key, value = line.split(':', 1)
meta[key.strip()] = value.strip()
meta['skill_name'] = skill_name or meta.get('name', SKILL_SLUG)
return meta
def _load_prompt_template(skill_name: str) -> str:
del skill_name
return PROMPT_TEMPLATE
def _normalize_key(key: Any) -> str:
return re.sub(r'[^a-z0-9_\u4e00-\u9fff]+', '_', str(key).strip().lower()).strip('_')
def _clean_text(value: Any) -> str:
if value is None:
return ''
if isinstance(value, str):
return value.strip()
if isinstance(value, (int, float)):
return str(value)
return json.dumps(value, ensure_ascii=False)
def _normalize_map(data: Dict[str, Any]) -> Dict[str, Any]:
return {_normalize_key(key): value for key, value in data.items()}
def _parse_text_map(raw: str) -> Dict[str, Any]:
data: Dict[str, Any] = {}
for line in raw.splitlines():
if re.search(r'[::]', line):
key, value = re.split(r'[::]', line, maxsplit=1)
data[_normalize_key(key)] = value.strip()
return data
def _coerce_input(user_input: Any) -> Tuple[str, Dict[str, Any]]:
if isinstance(user_input, dict):
return json.dumps(user_input, ensure_ascii=False), _normalize_map(user_input)
raw = _clean_text(user_input)
if raw.startswith('{') and raw.endswith('}'):
try:
parsed = json.loads(raw)
if isinstance(parsed, dict):
return raw, _normalize_map(parsed)
except json.JSONDecodeError:
pass
return raw, _parse_text_map(raw)
def _pick(data: Dict[str, Any], keys: List[str], default: str = '') -> str:
for key in keys:
normalized = _normalize_key(key)
if normalized in data:
text = _clean_text(data[normalized])
if text:
return text
return default
def _split_items(value: Any) -> List[str]:
if isinstance(value, list):
return [_clean_text(item) for item in value if _clean_text(item)]
text = _clean_text(value)
if not text:
return []
parts = [re.sub(r'^[\-•\d.\s]+', '', part).strip() for part in re.split(r'[\n,;,;]+', text)]
return [part for part in parts if part]
def _contains_any(text: str, keywords: List[str]) -> bool:
lowered = text.lower()
return any(word in lowered for word in keywords)
def _score_profiles(raw: str, data: Dict[str, Any]) -> Tuple[str, Dict[str, str], List[str], List[str]]:
situation = _pick(data, ['situation', 'current_situation', '现状'])
goal = _pick(data, ['goal', 'victory_condition', '目标'])
resources = _split_items(_pick(data, ['resources', 'assets', '资源']))
constraints = _split_items(_pick(data, ['constraints', 'limits', '约束']))
pressure = _pick(data, ['pressure', 'resistance', 'opponent', '阻力', '对手'])
time_window = _pick(data, ['time_window', 'deadline', 'window', '时间窗口'])
context = ' '.join([raw, situation, goal, pressure, time_window, ' '.join(resources), ' '.join(constraints)]).lower()
scores = {name: 0 for name in PROFILES}
if _contains_any(context, ['deadline', 'exam', 'launch', 'sprint', 'window', 'one week', 'this week', '冲刺', '考试']):
scores['timing_push'] += 5
if time_window and goal:
scores['timing_push'] += 2
if _contains_any(context, ['exam day', 'launch day', 'submission', 'test day', '考试日']):
scores['timing_push'] += 1
if _contains_any(context, ['overwhelmed', 'chaos', 'too many fires', 'fatigue', 'low energy', 'mess', 'wobbling', '崩', '乱']):
scores['stabilize'] += 5
if _contains_any(context, ['priority', 'position', 'leverage', 'option', 'unclear', 'choose', 'path', '优先级', '取舍']):
scores['positioning'] += 4
if _contains_any(context, ['long game', 'season', 'sustainable', 'steady', 'recurring', 'compound', '长期', '稳定']):
scores['attrition'] += 4
if _contains_any(context, ['opportunity', 'expand', 'growth', 'new lane', 'runway', 'spare capacity', '扩张', '增长']):
scores['expansion'] += 4
if constraints and len(constraints) >= 2:
scores['stabilize'] += 1
scores['positioning'] += 1
if resources and len(resources) >= 3:
scores['expansion'] += 1
scores['positioning'] += 1
if all(score == 0 for score in scores.values()):
scores['positioning'] = 2
scores['stabilize'] = 1
profile_id = sorted(scores.items(), key=lambda item: (item[1], PROFILES[item[0]]['label']), reverse=True)[0][0]
extracted = {
'situation': situation or 'current board state not fully specified',
'goal': goal or 'victory condition not fully specified',
'pressure': pressure or 'resistance is mostly internal friction or diffuse uncertainty',
'time_window': time_window or 'time window not fully specified',
}
return profile_id, extracted, resources, constraints
def _loss_condition(goal: str, constraints: List[str], pressure: str) -> str:
if constraints:
return f"Letting {constraints[0]} keep blocking the line of play until the goal stalls."
if pressure and pressure != 'resistance is mostly internal friction or diffuse uncertainty':
return f'Allowing {pressure} to dictate the tempo for too many turns.'
return 'Spreading effort too thin and never converting progress into a clear win state.'
def _build_result(raw: str, data: Dict[str, Any], template: str) -> str:
profile_id, extracted, resources, constraints = _score_profiles(raw, data)
profile = PROFILES[profile_id]
lines: List[str] = []
lines.append('# Strategy Map')
lines.append('')
lines.append('## Board State')
lines.append(f"- Current map: {extracted['situation']}")
lines.append(f"- Victory condition: {extracted['goal']}")
lines.append(f"- Loss condition: {_loss_condition(extracted['goal'], constraints, extracted['pressure'])}")
if resources:
lines.append(f"- Supply lines: {', '.join(resources)}")
else:
lines.append('- Supply lines: not fully specified, so protect time, clarity, and stamina as default resources.')
if constraints:
lines.append(f"- Contested ground: {', '.join(constraints)}")
else:
lines.append('- Contested ground: main friction point not fully specified.')
lines.append(f"- Pressure source: {extracted['pressure']}")
lines.append(f"- Time window: {extracted['time_window']}")
lines.append('')
lines.append('## Recommended Stance')
lines.append(f"- Stance: {profile['label']}")
lines.append(f"- Why now: {profile['summary']}")
lines.append(f"- Framing guardrail: {template.splitlines()[0]}")
lines.append('')
lines.append('## Three-Phase Plan')
lines.append(f"- Opening: {profile['opening']}")
lines.append(f"- Midgame: {profile['midgame']}")
lines.append(f"- Endgame: {profile['endgame']}")
lines.append('')
lines.append('## Decision Reminders')
lines.append(f"- Protect: {profile['protect']}")
lines.append(f"- Contest: {profile['contest']}")
lines.append(f"- Let go: {profile['let_go']}")
lines.append(f"- Likely misread: {profile['misread']}")
return '\n'.join(lines)
def handle(args: Dict[str, Any]) -> Dict[str, str]:
skill_name = args.get('skill_name', SKILL_SLUG) or SKILL_SLUG
user_input = args.get('input', '')
mode = args.get('mode', 'guide')
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
if mode == 'meta':
return {'result': json.dumps(meta, ensure_ascii=False, indent=2)}
if mode == 'prompt':
return {'result': template}
raw, data = _coerce_input(user_input)
return {'result': _build_result(raw, data, template)}
if __name__ == '__main__':
demo = {
'situation': 'one week before an exam with too many unfinished review tasks',
'goal': 'secure the highest-yield topics before exam day',
'resources': '2 focused hours each morning, class notes',
'constraints': 'low energy at night, family logistics',
'resistance': 'panic and jumping between topics',
'deadline': 'exam day next week',
}
print(json.dumps(handle({'skill_name': SKILL_SLUG, 'input': demo}), ensure_ascii=False, indent=2))
FILE:tests/test_handler.py
#!/usr/bin/env python3
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from handler import handle
def test_timing_push_profile_for_deadline_case():
result = handle({
'skill_name': 'strategy-game-mentor',
'input': {
'situation': 'one week before an exam with too many unfinished review tasks',
'goal': 'secure the highest-yield topics before exam day',
'resources': '2 focused hours each morning, class notes',
'constraints': 'low energy at night, family logistics',
'resistance': 'panic and jumping between topics',
'deadline': 'exam day next week',
},
})['result']
assert 'Timing Push' in result
assert 'Three-Phase Plan' in result
assert 'Decision Reminders' in result
def test_string_input_supported():
result = handle({
'skill_name': 'strategy-game-mentor',
'input': 'Situation: too many parallel home admin tasks\nGoal: regain control this month\nResources: evenings, checklist habit\nConstraints: low energy\nPressure: scattered obligations',
})['result']
assert 'Board State' in result
assert 'Recommended Stance' in result
def test_prompt_mode_returns_template():
result = handle({'skill_name': 'strategy-game-mentor', 'mode': 'prompt'})['result']
assert 'strategy game board' in result.lower()
assert 'opening, midgame, endgame' in result.lower()
if __name__ == '__main__':
for name, fn in list(globals().items()):
if name.startswith('test_') and callable(fn):
fn()
print('All tests passed.')
Translate a real-life growth direction into a grounded RPG-style character build with class identity, attribute priorities, build route, and short upgrade ac...
---
name: rpg-character-builder
description: Translate a real-life growth direction into a grounded RPG-style character build with class identity, attribute priorities, build route, and short upgrade actions. Use when the user wants a motivating but realistic frame for personal development.
---
# RPG Character Builder
Chinese name: RPG 角色构建
## Purpose
Turn personal growth into a grounded character build so the user can see identity, strengths, gaps, and the next upgrade path more clearly.
This skill is descriptive only. It does not replace coaching, psychological assessment, or formal career evaluation.
## Use this skill when
- The user wants a motivating frame for self-development.
- The user has many aspirations but no coherent identity anchor.
- The user wants short upgrade actions tied to values instead of vague self-improvement slogans.
- The user wants a role card that feels vivid without becoming unrealistic fantasy.
## Inputs to collect
- Current life situation
- Ideal role or identity
- Values
- Strengths
- Weaknesses or current bottlenecks
- Skills the user wants to grow next
## Workflow
1. Read the user’s current context, desired identity, and value pattern.
2. Infer a main class plus a secondary class that fits the user’s real strengths.
3. Map strengths and gaps into grounded attributes and practical build advice.
4. Describe the current build, the next phase, and misallocated points to avoid.
5. End with 1 to 3 weekly upgrade actions.
## Output Format
- Character card
- Attribute points
- Build route
- This week’s level-up actions
## Quality bar
- Keep the framing vivid but realistic.
- Connect every attribute to practical behavior.
- Reflect the user’s values, not just what sounds impressive.
- End with upgrade actions that can be done in real life this week.
## Edge cases and limits
- If the user is unclear about identity, offer a light starter build first.
- If the user is obviously escaping reality, gently return the build to practical constraints.
- This skill does not replace major life, mental health, or career decisions.
## Compatibility notes
- Works for self-development, study planning, career reflection, and family encouragement.
- Can pair conceptually with level-up-skill-tree or achievement-unlock-tracker.
- Fully dialogue-based, no graphics system required.
FILE:handler.py
#!/usr/bin/env python3
import json
import os
import re
from typing import Any, Dict, List, Tuple
SKILL_SLUG = 'rpg-character-builder'
PROMPT_TEMPLATE = """Translate a real-life growth direction into a grounded RPG build.
Infer a main class, a secondary class, practical attributes, a build route, and 1 to 3 weekly upgrade actions. Keep the language vivid but realistic."""
ARCHETYPES = {
'strategist': {
'label': 'Strategist',
'summary': 'Wins by reading the map clearly before spending effort.',
'keywords': ['analysis', 'strategy', 'clarity', 'think', 'read', 'learn', 'research', 'plan', 'logic', 'ai'],
'stats': ['Insight', 'Discipline'],
},
'builder': {
'label': 'Builder',
'summary': 'Turns ideas into reliable systems, structures, and repeatable output.',
'keywords': ['build', 'ship', 'system', 'process', 'execute', 'routine', 'habit', 'organize', 'make'],
'stats': ['Craft', 'Discipline'],
},
'guide': {
'label': 'Guide',
'summary': 'Levels up through teaching, support, and steady care for others.',
'keywords': ['teach', 'help', 'support', 'family', 'parent', 'mentor', 'care', 'communicate', 'coach'],
'stats': ['Influence', 'Resilience'],
},
'explorer': {
'label': 'Explorer',
'summary': 'Grows by curiosity, experimentation, and contact with new terrain.',
'keywords': ['explore', 'curious', 'experiment', 'try', 'discover', 'adventure', 'novelty'],
'stats': ['Insight', 'Resilience'],
},
'creator': {
'label': 'Creator',
'summary': 'Turns internal vision into expressive work that others can feel or use.',
'keywords': ['write', 'design', 'create', 'story', 'art', 'expression', 'craft', 'content'],
'stats': ['Craft', 'Influence'],
},
}
STAT_KEYWORDS = {
'Insight': ['analysis', 'learn', 'study', 'read', 'research', 'reflect', 'strategy', 'clarity', 'curious'],
'Discipline': ['discipline', 'routine', 'consistency', 'execute', 'system', 'habit', 'finish', 'reliable'],
'Resilience': ['calm', 'grit', 'recover', 'patient', 'steady', 'stamina', 'courage', 'endure'],
'Influence': ['teach', 'guide', 'communicate', 'support', 'lead', 'connect', 'parent', 'coach'],
'Craft': ['build', 'create', 'write', 'design', 'make', 'ship', 'practice', 'tool'],
}
ACTION_LIBRARY = {
'Insight': 'Write one short note that names what season of growth you are actually in and what winning this month looks like.',
'Discipline': 'Protect one repeatable 20-minute practice block on at least 4 days this week.',
'Resilience': 'Add one recovery anchor to the week, such as a walk, stretch, or early stop rule before mental fatigue spills over.',
'Influence': 'Explain your current build to one trusted person and ask for one concrete feedback question.',
'Craft': 'Ship one small artifact this week, such as a note, checklist, outline, demo, or draft, instead of only thinking about it.',
}
def _load_skill_meta(skill_name: str) -> Dict[str, str]:
path = os.path.join(os.path.dirname(__file__), 'SKILL.md')
with open(path, 'r', encoding='utf-8') as handle:
content = handle.read()
match = re.search(r'^---\s*\n(.*?)\n---\s*', content, re.DOTALL | re.MULTILINE)
meta: Dict[str, str] = {}
if match:
for line in match.group(1).splitlines():
if ':' in line:
key, value = line.split(':', 1)
meta[key.strip()] = value.strip()
meta['skill_name'] = skill_name or meta.get('name', SKILL_SLUG)
return meta
def _load_prompt_template(skill_name: str) -> str:
del skill_name
return PROMPT_TEMPLATE
def _normalize_key(key: Any) -> str:
return re.sub(r'[^a-z0-9_\u4e00-\u9fff]+', '_', str(key).strip().lower()).strip('_')
def _clean_text(value: Any) -> str:
if value is None:
return ''
if isinstance(value, str):
return value.strip()
if isinstance(value, (int, float)):
return str(value)
return json.dumps(value, ensure_ascii=False)
def _normalize_map(data: Dict[str, Any]) -> Dict[str, Any]:
return {_normalize_key(key): value for key, value in data.items()}
def _parse_text_map(raw: str) -> Dict[str, Any]:
data: Dict[str, Any] = {}
for line in raw.splitlines():
if re.search(r'[::]', line):
key, value = re.split(r'[::]', line, maxsplit=1)
data[_normalize_key(key)] = value.strip()
return data
def _coerce_input(user_input: Any) -> Tuple[str, Dict[str, Any]]:
if isinstance(user_input, dict):
return json.dumps(user_input, ensure_ascii=False), _normalize_map(user_input)
raw = _clean_text(user_input)
if raw.startswith('{') and raw.endswith('}'):
try:
parsed = json.loads(raw)
if isinstance(parsed, dict):
return raw, _normalize_map(parsed)
except json.JSONDecodeError:
pass
return raw, _parse_text_map(raw)
def _pick(data: Dict[str, Any], keys: List[str], default: str = '') -> str:
for key in keys:
normalized = _normalize_key(key)
if normalized in data:
text = _clean_text(data[normalized])
if text:
return text
return default
def _split_items(value: Any) -> List[str]:
if isinstance(value, list):
return [_clean_text(item) for item in value if _clean_text(item)]
text = _clean_text(value)
if not text:
return []
parts = [re.sub(r'^[\-•\d.\s]+', '', part).strip() for part in re.split(r'[\n,;,;]+', text)]
return [part for part in parts if part]
def _choose_classes(raw: str, data: Dict[str, Any]) -> Tuple[str, str, str]:
current_state = _pick(data, ['current_state', 'situation', '现状'])
ideal_role = _pick(data, ['ideal_role', 'target_role', '理想角色'])
values = ' '.join(_split_items(_pick(data, ['values', '价值观'])))
strengths = ' '.join(_split_items(_pick(data, ['strengths', '优势'])))
desired = ' '.join(_split_items(_pick(data, ['desired_skills', 'skills', '想练的能力'])))
context = ' '.join([raw, current_state, ideal_role, values, strengths, desired]).lower()
scores: Dict[str, int] = {name: 0 for name in ARCHETYPES}
for name, config in ARCHETYPES.items():
for keyword in config['keywords']:
if keyword in context:
scores[name] += 1
if ideal_role and config['label'].lower() in ideal_role.lower():
scores[name] += 2
ranked = sorted(scores.items(), key=lambda item: (item[1], ARCHETYPES[item[0]]['label']), reverse=True)
primary = ranked[0][0] if ranked[0][1] > 0 else 'strategist'
secondary = next((name for name, _ in ranked[1:] if name != primary), 'builder')
return primary, secondary, current_state or 'life context not fully specified'
def _score_stats(raw: str, data: Dict[str, Any], primary: str, secondary: str) -> Dict[str, int]:
positives = ' '.join([
raw,
_pick(data, ['values', '价值观']),
_pick(data, ['strengths', '优势']),
_pick(data, ['desired_skills', 'skills', '想练的能力']),
]).lower()
negatives = _pick(data, ['weaknesses', 'gaps', 'blockers', '短板']).lower()
scores: Dict[str, int] = {}
for stat, keywords in STAT_KEYWORDS.items():
value = 2
hits = sum(1 for keyword in keywords if keyword in positives)
misses = sum(1 for keyword in keywords if keyword in negatives)
value += min(hits, 2)
value -= min(misses, 2)
if stat in ARCHETYPES[primary]['stats']:
value += 1
if stat in ARCHETYPES[secondary]['stats']:
value += 1
scores[stat] = max(1, min(value, 5))
return scores
def _hidden_talent(stats: Dict[str, int], strengths: List[str]) -> str:
lowered_strengths = ' '.join(strengths).lower()
for stat, score in sorted(stats.items(), key=lambda item: item[1], reverse=True):
if score >= 4 and stat.lower() not in lowered_strengths:
return stat
return max(stats, key=stats.get)
def _misallocated_points(weaknesses: List[str], stats: Dict[str, int]) -> str:
if weaknesses:
return f"Do not ignore the stated weak spot, especially {weaknesses[0]}, while chasing a shiny side class."
lowest = min(stats, key=stats.get)
return f'Do not spend all your points on prestige moves while {lowest} is still the obvious bottleneck.'
def _weekly_actions(stats: Dict[str, int], desired_skills: List[str]) -> List[str]:
ordered_stats = sorted(stats.items(), key=lambda item: item[1])
actions: List[str] = []
for stat, _ in ordered_stats[:3]:
actions.append(ACTION_LIBRARY[stat])
if desired_skills:
actions[0] = f"Use your next small action to move `{desired_skills[0]}` from idea status into one visible practice rep."
deduped: List[str] = []
for action in actions:
if action not in deduped:
deduped.append(action)
return deduped[:3]
def _build_result(raw: str, data: Dict[str, Any], template: str) -> str:
primary, secondary, current_state = _choose_classes(raw, data)
stats = _score_stats(raw, data, primary, secondary)
strengths = _split_items(_pick(data, ['strengths', '优势']))
weaknesses = _split_items(_pick(data, ['weaknesses', 'gaps', 'blockers', '短板']))
desired_skills = _split_items(_pick(data, ['desired_skills', 'skills', '想练的能力']))
values = _split_items(_pick(data, ['values', '价值观']))
ideal_role = _pick(data, ['ideal_role', 'target_role', '理想角色'], 'not fully specified')
main_class = ARCHETYPES[primary]
sub_class = ARCHETYPES[secondary]
hidden = _hidden_talent(stats, strengths)
lines: List[str] = []
lines.append('# Grounded RPG Build')
lines.append('')
lines.append('## Character Card')
lines.append(f"- Main class: {main_class['label']}")
lines.append(f"- Secondary class: {sub_class['label']}")
lines.append(f"- Current world state: {current_state}")
lines.append(f"- Target identity: {ideal_role}")
if values:
lines.append(f"- Value alignment: {', '.join(values)}")
lines.append(f"- Build fantasy kept realistic: {main_class['summary']} {sub_class['summary']}")
lines.append(f"- Build frame: {template.splitlines()[0]}")
lines.append('')
lines.append('## Attribute Points')
for stat, score in stats.items():
lines.append(f'- {stat}: {score}/5')
if strengths:
lines.append(f"- Current strengths: {', '.join(strengths)}")
if weaknesses:
lines.append(f"- Current bottlenecks: {', '.join(weaknesses)}")
lines.append(f'- Hidden talent to lean into: {hidden}')
lines.append('')
lines.append('## Build Route')
lines.append(f"- Current build: a {main_class['label']} core with {sub_class['label']} support.")
if desired_skills:
lines.append(f"- Next phase focus: train {', '.join(desired_skills[:2])} without abandoning your base class.")
else:
lowest = min(stats, key=stats.get)
lines.append(f'- Next phase focus: stabilize {lowest} so the current strengths stop carrying the whole build alone.')
lines.append(f"- Avoid misallocation: {_misallocated_points(weaknesses, stats)}")
lines.append('')
lines.append("## This Week's Level-Up Actions")
for index, action in enumerate(_weekly_actions(stats, desired_skills), start=1):
lines.append(f'{index}. {action}')
return '\n'.join(lines)
def handle(args: Dict[str, Any]) -> Dict[str, str]:
skill_name = args.get('skill_name', SKILL_SLUG) or SKILL_SLUG
user_input = args.get('input', '')
mode = args.get('mode', 'guide')
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
if mode == 'meta':
return {'result': json.dumps(meta, ensure_ascii=False, indent=2)}
if mode == 'prompt':
return {'result': template}
raw, data = _coerce_input(user_input)
return {'result': _build_result(raw, data, template)}
if __name__ == '__main__':
demo = {
'current_state': 'parent learning AI at home',
'ideal_role': 'a calm strategist who can teach others',
'values': 'growth, family, clarity',
'strengths': ['analysis', 'care', 'curiosity'],
'weaknesses': ['consistency', 'overthinking'],
'desired_skills': ['shipping small projects', 'clear teaching'],
}
print(json.dumps(handle({'skill_name': SKILL_SLUG, 'input': demo}), ensure_ascii=False, indent=2))
FILE:tests/test_handler.py
#!/usr/bin/env python3
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from handler import handle
def test_grounded_build_sections_exist():
result = handle({
'skill_name': 'rpg-character-builder',
'input': {
'current_state': 'parent learning AI at home',
'ideal_role': 'a calm strategist who can teach others',
'values': 'growth, family, clarity',
'strengths': ['analysis', 'care', 'curiosity'],
'weaknesses': ['consistency', 'overthinking'],
'desired_skills': ['shipping small projects', 'clear teaching'],
},
})['result']
assert 'Character Card' in result
assert 'Build Route' in result
assert "This Week's Level-Up Actions" in result
assert ('Strategist' in result) or ('Guide' in result)
def test_string_input_supported():
result = handle({
'skill_name': 'rpg-character-builder',
'input': 'Current state: changing careers\nIdeal role: builder who teaches clearly\nStrengths: writing, systems\nWeaknesses: stamina\nDesired skills: shipping, facilitation',
})['result']
assert 'Grounded RPG Build' in result
assert 'Attribute Points' in result
def test_prompt_mode_returns_template():
result = handle({'skill_name': 'rpg-character-builder', 'mode': 'prompt'})['result']
assert 'grounded rpg build' in result.lower()
assert 'weekly upgrade actions' in result.lower()
if __name__ == '__main__':
for name, fn in list(globals().items()):
if name.startswith('test_') and callable(fn):
fn()
print('All tests passed.')
Clarify a card game win condition and turn it into a stable structure across main engine, draw, interaction, finishers, and backup lines. Use when the user w...
---
name: tcg-strategy-advisor
description: Clarify a card game win condition and turn it into a stable structure across main engine, draw, interaction, finishers, and backup lines. Use when the user wants a cleaner deck plan without pretending to know the live metagame.
---
# TCG Strategy Advisor
Chinese name: 卡牌游戏策略顾问
## Purpose
Use card-game thinking to clarify resource curve, role, and win condition before the user greedily adds too many powerful but mismatched cards.
This skill is descriptive only. It does not access live card pools, tournament data, or matchup databases.
## Use this skill when
- The user wants to understand whether a deck idea is aggro, control, combo, midrange, or ramp.
- The user has many strong card ideas but no stable structure.
- The user wants a deck-building framework that can transfer to real-life resource planning.
- The user needs simple language instead of tournament jargon.
## Inputs to collect
- Declared archetype or desired play pattern
- Win condition
- Expected speed or key turn
- Core components or cards
- Major risks or consistency problems
- Whether the user wants a strict or flexible shell
## Workflow
1. Identify how the deck is actually trying to win.
2. Map the idea to a primary strategy profile.
3. Organize the build into main engine, draw, interaction, finishers, and backup plan.
4. Explain early, mid, and late-game priorities.
5. Highlight common greed traps and cuts the user should make first.
## Output Format
- Strategy identity
- Build structure
- Match tempo
- Common mistakes
- Transferable resource lesson
## Quality bar
- Every recommendation must point back to the win condition.
- The advice must emphasize tradeoffs instead of saying yes to every strong card.
- The structure should make sense to a newcomer.
- The output must stay honest about not using live metagame data.
## Edge cases and limits
- If the user does not name a specific game, stay at a transferable TCG structure level.
- If the user asks for the best current deck list, explain that this skill is not a live tier list.
- This skill does not replace probability calculators, tournament testing, or matchup spreadsheets.
## Compatibility notes
- Works for physical TCGs, digital card games, and real-life resource-planning metaphors.
- Can pair conceptually with strategy-game-mentor.
- Fully dialogue-based, no external database required.
FILE:handler.py
#!/usr/bin/env python3
import json
import os
import re
from typing import Any, Dict, List, Tuple
SKILL_SLUG = 'tcg-strategy-advisor'
PROMPT_TEMPLATE = """Identify the user's real win condition and map it to a stable TCG structure.
Cover main engine, draw or consistency, interaction, finishers, backup line, match tempo, and common greed traps. Do not pretend to know the live metagame."""
ARCHETYPES = {
'aggro': {
'label': 'Aggro / Tempo Pressure',
'summary': 'Win before the opponent fully stabilizes.',
'main_engine': 'Cheap threats, curve discipline, and pressure pieces that matter from turn one.',
'draw': 'Light but efficient refill so the deck does not stall after the first wave.',
'interaction': 'Low-cost removal, bounce, or combat tricks that preserve the lead.',
'finish': 'Reach damage, burst turns, or sticky threats that convert a small gap into a win.',
'backup': 'A small resilient package that keeps the clock running if the opener gets answered.',
'early': 'Spend mana cleanly, establish the first clock, and force bad blocks or awkward answers.',
'mid': 'Keep pressure high and trade only when it protects the clock.',
'late': 'Count exact reach and avoid pretending the deck wants a twenty-turn game.',
'curve': 'Bias the deck toward early turns. If too many cards cost four or more, the engine is lying about its identity.',
'mistakes': [
'Keeping slow hands because every card looks powerful.',
'Adding too many cute top-end cards and ruining the early curve.',
'Trading away pressure for value that the deck is not built to exploit long-term.',
],
},
'control': {
'label': 'Control / Resource Denial',
'summary': 'Survive, answer threats efficiently, and win after the opponent runs out of meaningful pressure.',
'main_engine': 'Reliable answers, card advantage, and repeatable resource stabilization.',
'draw': 'High-quality filtering, draw, or recursion so the deck keeps seeing the right half.',
'interaction': 'Removal, counters, sweeps, and defensive tools sized for the expected pace.',
'finish': 'A few durable closers, not a pile of expensive vanity cards.',
'backup': 'Secondary value engines that still matter when the primary finisher is buried.',
'early': 'Preserve life total and card parity without spending premium answers too loosely.',
'mid': 'Trade at favorable rates and turn the game into a smaller decision tree.',
'late': 'Lock the game with superior resources and finish with minimal exposure.',
'curve': 'Respect the early defense slots. A control shell that cannot survive the first pressure wave is not a control deck yet.',
'mistakes': [
'Loading up on finishers while cutting early interaction.',
'Using premium answers on low-value threats because of panic.',
'Drawing cards without checking whether the deck still has time to cash them in.',
],
},
'combo': {
'label': 'Combo / Engine Assembly',
'summary': 'Assemble a specific interaction and convert it into a decisive swing or one-turn finish.',
'main_engine': 'The exact pieces, tutors, or setup cards that enable the combo turn.',
'draw': 'Filtering, tutoring, and redundancy that increase assembly consistency.',
'interaction': 'Protection, stall tools, or selective removal that buy the needed setup window.',
'finish': 'The actual combo conversion step, plus a clean explanation of how it ends the game.',
'backup': 'A secondary fair plan so the deck does not auto-lose when one piece is missing.',
'early': 'Find pieces, protect life total, and avoid wasting combo resources on low-value exchanges.',
'mid': 'Sequence setup carefully and identify when to hold versus jam the engine.',
'late': 'Either execute decisively or pivot into the backup line before dead draws pile up.',
'curve': 'Redundancy matters more than flashy one-ofs. Every card should either find, protect, or convert the engine.',
'mistakes': [
'Adding too many fun side packages and lowering combo density.',
'Keeping hands that have no setup path because one card looks exciting.',
'Forgetting to define a backup line when the main combo is disrupted.',
],
},
'midrange': {
'label': 'Midrange / Flexible Value',
'summary': 'Win by playing efficient threats and answers that can switch roles across the game.',
'main_engine': 'Flexible threats that matter when ahead or behind.',
'draw': 'Moderate draw or recursion to stop the shell from running out of gas.',
'interaction': 'Versatile answers that are rarely dead in hand.',
'finish': 'A few sticky value closers that turn board control into inevitability.',
'backup': 'Role-switch tools so the deck can become slightly faster or slower by matchup.',
'early': 'Contest the board without overcommitting to one role too early.',
'mid': 'Identify whether you are the beatdown or the value deck in this specific game.',
'late': 'Close with sticky threats instead of hoarding cards forever.',
'curve': 'The deck should have live plays across the whole curve. Too much top-end or too much cheap fluff will blur the plan.',
'mistakes': [
'Trying to include every strong standalone card and losing the deck’s center of gravity.',
'Refusing to decide whether to race or trade in a given matchup.',
'Treating flexible cards as a reason to skip real curve discipline.',
],
},
'ramp': {
'label': 'Ramp / Top-End Conversion',
'summary': 'Accelerate resources early, then land threats that invalidate smaller exchanges.',
'main_engine': 'Mana acceleration, cost reduction, or resource engines that jump the curve.',
'draw': 'Filtering that finds either acceleration early or payoffs later.',
'interaction': 'Stall tools that buy the time needed to cash in the acceleration.',
'finish': 'Top-end payoffs that swing the game hard enough to justify the slower start.',
'backup': 'Mid-cost stabilizers so the deck is not dead when the dream draw misses.',
'early': 'Prioritize acceleration only if the deck can survive the tempo loss.',
'mid': 'Bridge from ramp pieces into stabilizers or immediate-impact threats.',
'late': 'Make every top-end draw matter instead of playing expensive cards that do not swing the board.',
'curve': 'The payoff cards must justify the weak early turns. If the finish is only medium, the ramp shell is overpaying for fantasy.',
'mistakes': [
'Stuffing the deck with huge cards and forgetting survival tools.',
'Playing acceleration without enough meaningful payoffs.',
'Assuming all big cards are finishers when some simply extend the game.',
],
},
}
def _load_skill_meta(skill_name: str) -> Dict[str, str]:
path = os.path.join(os.path.dirname(__file__), 'SKILL.md')
with open(path, 'r', encoding='utf-8') as handle:
content = handle.read()
match = re.search(r'^---\s*\n(.*?)\n---\s*', content, re.DOTALL | re.MULTILINE)
meta: Dict[str, str] = {}
if match:
for line in match.group(1).splitlines():
if ':' in line:
key, value = line.split(':', 1)
meta[key.strip()] = value.strip()
meta['skill_name'] = skill_name or meta.get('name', SKILL_SLUG)
return meta
def _load_prompt_template(skill_name: str) -> str:
del skill_name
return PROMPT_TEMPLATE
def _normalize_key(key: Any) -> str:
return re.sub(r'[^a-z0-9_\u4e00-\u9fff]+', '_', str(key).strip().lower()).strip('_')
def _clean_text(value: Any) -> str:
if value is None:
return ''
if isinstance(value, str):
return value.strip()
if isinstance(value, (int, float)):
return str(value)
return json.dumps(value, ensure_ascii=False)
def _normalize_map(data: Dict[str, Any]) -> Dict[str, Any]:
return {_normalize_key(key): value for key, value in data.items()}
def _parse_text_map(raw: str) -> Dict[str, Any]:
data: Dict[str, Any] = {}
for line in raw.splitlines():
if re.search(r'[::]', line):
key, value = re.split(r'[::]', line, maxsplit=1)
data[_normalize_key(key)] = value.strip()
return data
def _coerce_input(user_input: Any) -> Tuple[str, Dict[str, Any]]:
if isinstance(user_input, dict):
return json.dumps(user_input, ensure_ascii=False), _normalize_map(user_input)
raw = _clean_text(user_input)
if raw.startswith('{') and raw.endswith('}'):
try:
parsed = json.loads(raw)
if isinstance(parsed, dict):
return raw, _normalize_map(parsed)
except json.JSONDecodeError:
pass
return raw, _parse_text_map(raw)
def _pick(data: Dict[str, Any], keys: List[str], default: str = '') -> str:
for key in keys:
normalized = _normalize_key(key)
if normalized in data:
text = _clean_text(data[normalized])
if text:
return text
return default
def _to_int(value: Any, default: int = 0) -> int:
if isinstance(value, int):
return value
text = _clean_text(value)
match = re.search(r'(\d+)', text)
return int(match.group(1)) if match else default
def _contains_any(text: str, keywords: List[str]) -> bool:
lowered = text.lower()
return any(word in lowered for word in keywords)
def _score_archetypes(raw: str, data: Dict[str, Any]) -> Tuple[Dict[str, int], str, int, str]:
scores = {name: 0 for name in ARCHETYPES}
declared = _pick(data, ['archetype', 'style', 'plan', '套路', '风格'])
win_condition = _pick(data, ['win_condition', 'goal', 'victory_condition', 'how_to_win', '胜利条件'])
core = _pick(data, ['core_components', 'core_cards', 'engine', 'key_cards', '核心组件'])
risks = _pick(data, ['risks', 'problems', 'consistency_problem', '担忧'])
turns = _to_int(_pick(data, ['turns_to_win', 'key_turn', 'speed', '启动回合', '回合']), 0)
game_name = _pick(data, ['game', 'title', 'tcg', '游戏'])
context = ' '.join([raw, declared, win_condition, core, risks, game_name]).lower()
if _contains_any(context, ['aggro', 'tempo', 'rush', 'pressure', 'fast', 'beatdown', '快攻', '抢节奏']):
scores['aggro'] += 5
if _contains_any(context, ['control', 'stall', 'deny', 'removal', 'counter', 'grind', '控制', '拖后期']):
scores['control'] += 5
if _contains_any(context, ['combo', 'assemble', 'engine', 'synergy', 'loop', 'otk', '连招', '组合技']):
scores['combo'] += 5
if _contains_any(context, ['midrange', 'flexible', 'value', 'board-based', 'mid game', '中速', '价值']):
scores['midrange'] += 4
if _contains_any(context, ['ramp', 'mana', 'big threat', 'top end', 'accelerate', '资源积累', '跳费']):
scores['ramp'] += 5
if turns:
if turns <= 4:
scores['aggro'] += 3
elif turns <= 6:
scores['midrange'] += 2
scores['combo'] += 1
else:
scores['control'] += 2
scores['ramp'] += 2
scores['combo'] += 1
if _contains_any(context, ['draw', 'filter', 'consistency', 'mulligan', '过牌', '稳定']):
scores['combo'] += 1
scores['control'] += 1
scores['midrange'] += 1
if _contains_any(context, ['protect', 'backup line', 'fallback', 'secondary plan', '保护', '备用方案']):
scores['combo'] += 1
scores['control'] += 1
if all(score == 0 for score in scores.values()):
scores['midrange'] = 2
scores['control'] = 1
return scores, win_condition or 'not fully specified', turns, game_name or 'generic TCG / CCG context'
def _rank_archetypes(scores: Dict[str, int]) -> List[Tuple[str, int]]:
return sorted(scores.items(), key=lambda item: (item[1], ARCHETYPES[item[0]]['label']), reverse=True)
def _build_result(raw: str, data: Dict[str, Any], template: str) -> str:
scores, win_condition, turns, game_name = _score_archetypes(raw, data)
profile_id = _rank_archetypes(scores)[0][0]
profile = ARCHETYPES[profile_id]
lines: List[str] = []
lines.append('# TCG Structure Review')
lines.append('')
lines.append('## Strategy Identity')
lines.append(f"- Profile: {profile['label']}")
lines.append(f"- Core plan: {profile['summary']}")
lines.append(f'- Stated win condition: {win_condition}')
if turns:
lines.append(f'- Expected key turn: around turn {turns}')
else:
lines.append('- Expected key turn: not specified, so the shell is inferred from the stated role and keywords.')
lines.append(f'- Context read: {game_name}')
lines.append(f"- Guardrail: {template.splitlines()[0]}")
lines.append('')
lines.append('## Build Structure')
lines.append(f"- Main engine: {profile['main_engine']}")
lines.append(f"- Draw / consistency: {profile['draw']}")
lines.append(f"- Interaction: {profile['interaction']}")
lines.append(f"- Finishers: {profile['finish']}")
lines.append(f"- Backup plan: {profile['backup']}")
lines.append(f"- Curve reminder: {profile['curve']}")
lines.append('')
lines.append('## Match Tempo')
lines.append(f"- Early game: {profile['early']}")
lines.append(f"- Mid game: {profile['mid']}")
lines.append(f"- Late game: {profile['late']}")
lines.append('')
lines.append('## Common Mistakes')
for item in profile['mistakes']:
lines.append(f'- {item}')
lines.append('')
lines.append('## Transferable Resource Lesson')
lines.append('- Build around the win condition first, then fund consistency, then add interaction, then cut greed.')
lines.append('- If a card does not help the main plan, the backup plan, or the survival window, it is probably a luxury slot.')
return '\n'.join(lines)
def handle(args: Dict[str, Any]) -> Dict[str, str]:
skill_name = args.get('skill_name', SKILL_SLUG) or SKILL_SLUG
user_input = args.get('input', '')
mode = args.get('mode', 'guide')
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
if mode == 'meta':
return {'result': json.dumps(meta, ensure_ascii=False, indent=2)}
if mode == 'prompt':
return {'result': template}
raw, data = _coerce_input(user_input)
return {'result': _build_result(raw, data, template)}
if __name__ == '__main__':
demo = {
'archetype': 'combo',
'win_condition': 'assemble two engine pieces and protect them',
'turns_to_win': 6,
'core_components': 'engine piece A, engine piece B',
}
print(json.dumps(handle({'skill_name': SKILL_SLUG, 'input': demo}), ensure_ascii=False, indent=2))
FILE:tests/test_handler.py
#!/usr/bin/env python3
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from handler import handle
def test_combo_profile_selected():
result = handle({
'skill_name': 'tcg-strategy-advisor',
'input': {
'archetype': 'combo',
'win_condition': 'assemble two engine pieces and protect them',
'turns_to_win': 6,
'core_components': 'engine piece A, engine piece B',
},
})['result']
assert 'Combo / Engine Assembly' in result
assert 'Build Structure' in result
assert 'Common Mistakes' in result
def test_aggro_string_input_supported():
result = handle({
'skill_name': 'tcg-strategy-advisor',
'input': 'Style: aggro\nGoal: win before slower decks stabilize\nKey turn: 4\nRisks: too many expensive cards',
})['result']
assert 'Aggro / Tempo Pressure' in result
assert 'Match Tempo' in result
def test_prompt_mode_returns_template():
result = handle({'skill_name': 'tcg-strategy-advisor', 'mode': 'prompt'})['result']
assert 'win condition' in result.lower()
assert 'live metagame' in result.lower()
if __name__ == '__main__':
for name, fn in list(globals().items()):
if name.startswith('test_') and callable(fn):
fn()
print('All tests passed.')
Match a group to the right board game style by weighing player count, time, experience level, competition appetite, and social mood. Use when the user wants...
---
name: boardgame-picker
description: Match a group to the right board game style by weighing player count, time, experience level, competition appetite, and social mood. Use when the user wants a fast category recommendation plus a backup option and one type to avoid.
---
# Boardgame Picker
Chinese name: 桌游选择器
## Purpose
Help a group stop overthinking and start playing by matching the table to the right board game category.
This skill is descriptive only. It does not check live catalog, pricing, or stock availability.
## Use this skill when
- The user needs a board game direction for a gathering, family table, or light social night.
- The table has mixed experience, mixed ages, or unclear taste.
- The group wants a quick category choice instead of browsing endless titles.
- The user wants a backup plan if the room energy changes.
## Inputs to collect
- Player count
- Age mix or newcomer level
- Available time
- Competition appetite
- Desired mood or atmosphere
- Tolerance for rules friction or mistakes
## Workflow
1. Read the table size, time, experience, and social mood.
2. Map the situation to board game categories such as party, cooperative, deduction, family, or strategy.
3. Recommend the best 1 to 2 categories, plus one backup and one type to avoid.
4. Add mechanism keywords and classic example directions when useful.
5. End with one fast-start suggestion so the group can begin tonight.
## Output Format
- Table snapshot
- Recommended types with reasons
- Backup type
- Avoid this round
- Quick start suggestion
## Quality bar
- The recommendation must match players, time, and rules tolerance.
- Explanations must say why a category fits, not just list titles.
- Include at least one low-friction way to start quickly.
- Stay realistic when the table has mixed ages or mixed experience.
## Edge cases and limits
- If the user asks for exact titles, only offer common example directions, not a live database recommendation.
- If age or skill gaps are large, prefer easy-to-explain formats over clever but fragile ones.
- This skill does not guarantee a specific title is available in the room.
## Compatibility notes
- Works for family, friends, party, and parent-child situations.
- Can pair conceptually with gaming-session-scheduler or co-op-mission-planner.
- Fully dialogue-based, no real-time API required.
FILE:handler.py
#!/usr/bin/env python3
import json
import os
import re
from typing import Any, Dict, List, Tuple
SKILL_SLUG = 'boardgame-picker'
PROMPT_TEMPLATE = """Match the user's group to the best board game style.
Read player count, available time, newcomer level, competition appetite, and table mood. Recommend the best 1 to 2 categories, one backup category, one type to avoid, and one fast-start suggestion. Do not pretend to have live catalog data."""
TYPE_LIBRARY = {
'party': {
'label': 'Party / Icebreaker',
'best_for': 'Large or lively groups that need fast rules and frequent laughter.',
'keywords': 'word play, drawing, clueing, reaction, fast rounds',
'examples': 'Codenames, Just One, Telestrations',
'avoid_when': 'the table wants deep planning or a very serious competitive arc',
},
'coop': {
'label': 'Cooperative / Team Challenge',
'best_for': 'Groups that want shared wins, teamwork, and lower direct conflict.',
'keywords': 'cooperation, crisis management, shared puzzle, team communication',
'examples': 'Pandemic, The Crew, Forbidden Desert',
'avoid_when': 'alpha-player behavior is likely or the group wants strong rivalry',
},
'deduction': {
'label': 'Deduction / Bluff / Mystery',
'best_for': 'Players who enjoy reading people, hidden information, and social tension.',
'keywords': 'hidden roles, bluffing, accusations, logic clues',
'examples': 'Avalon, Deception: Murder in Hong Kong, Coup',
'avoid_when': 'the table has low trust, low energy, or large experience gaps',
},
'family': {
'label': 'Family / Gateway',
'best_for': 'Mixed ages, newcomers, and groups that want low explanation cost.',
'keywords': 'set collection, tile laying, route building, light drafting',
'examples': 'Ticket to Ride, Azul, Kingdomino',
'avoid_when': 'the table specifically wants a heavy brain-burn challenge',
},
'strategy': {
'label': 'Strategy / Planning',
'best_for': 'Smaller tables with time, focus, and appetite for deeper decisions.',
'keywords': 'engine building, worker placement, area control, long-term planning',
'examples': 'Splendor, Everdell, 7 Wonders Duel',
'avoid_when': 'the room is impatient, very new, or only has a short time window',
},
}
def _load_skill_meta(skill_name: str) -> Dict[str, str]:
path = os.path.join(os.path.dirname(__file__), 'SKILL.md')
with open(path, 'r', encoding='utf-8') as handle:
content = handle.read()
match = re.search(r'^---\s*\n(.*?)\n---\s*', content, re.DOTALL | re.MULTILINE)
meta: Dict[str, str] = {}
if match:
for line in match.group(1).splitlines():
if ':' in line:
key, value = line.split(':', 1)
meta[key.strip()] = value.strip()
meta['skill_name'] = skill_name or meta.get('name', SKILL_SLUG)
return meta
def _load_prompt_template(skill_name: str) -> str:
del skill_name
return PROMPT_TEMPLATE
def _normalize_key(key: Any) -> str:
return re.sub(r'[^a-z0-9_\u4e00-\u9fff]+', '_', str(key).strip().lower()).strip('_')
def _clean_text(value: Any) -> str:
if value is None:
return ''
if isinstance(value, str):
return value.strip()
if isinstance(value, (int, float)):
return str(value)
return json.dumps(value, ensure_ascii=False)
def _normalize_map(data: Dict[str, Any]) -> Dict[str, Any]:
normalized: Dict[str, Any] = {}
for key, value in data.items():
normalized[_normalize_key(key)] = value
return normalized
def _parse_text_map(raw: str) -> Dict[str, Any]:
data: Dict[str, Any] = {}
for line in raw.splitlines():
if re.search(r'[::]', line):
key, value = re.split(r'[::]', line, maxsplit=1)
data[_normalize_key(key)] = value.strip()
return data
def _coerce_input(user_input: Any) -> Tuple[str, Dict[str, Any]]:
if isinstance(user_input, dict):
return json.dumps(user_input, ensure_ascii=False), _normalize_map(user_input)
raw = _clean_text(user_input)
if raw.startswith('{') and raw.endswith('}'):
try:
parsed = json.loads(raw)
if isinstance(parsed, dict):
return raw, _normalize_map(parsed)
except json.JSONDecodeError:
pass
return raw, _parse_text_map(raw)
def _pick(data: Dict[str, Any], keys: List[str], default: str = '') -> str:
for key in keys:
normalized = _normalize_key(key)
if normalized in data:
text = _clean_text(data[normalized])
if text:
return text
return default
def _to_int(value: Any, default: int = 0) -> int:
if isinstance(value, int):
return value
text = _clean_text(value)
match = re.search(r'(\d+)', text)
return int(match.group(1)) if match else default
def _contains_any(text: str, keywords: List[str]) -> bool:
lowered = text.lower()
return any(word in lowered for word in keywords)
def _infer_players(text: str) -> int:
match = re.search(r'(\d+)\s*(?:players?|ppl|people|人)', text.lower())
return int(match.group(1)) if match else 0
def _infer_minutes(text: str) -> int:
match = re.search(r'(\d+)\s*(?:min|mins|minute|minutes|分钟)', text.lower())
return int(match.group(1)) if match else 0
def _score_profiles(raw: str, data: Dict[str, Any]) -> Tuple[Dict[str, int], int, int, str]:
scores = {name: 0 for name in TYPE_LIBRARY}
players = _to_int(_pick(data, ['players', 'player_count', '人数']), 0) or _infer_players(raw)
minutes = _to_int(_pick(data, ['duration', 'minutes', 'time', '时长']), 0) or _infer_minutes(raw)
experience = _pick(data, ['experience', 'newbie', 'skill_level', '是否新手', '新手'])
competition = _pick(data, ['competition', 'competitive', 'intensity', '竞争强度'])
mood = _pick(data, ['mood', 'vibe', 'atmosphere', '氛围'])
age_mix = _pick(data, ['age_mix', 'age', 'ages', '年龄层'])
tolerance = _pick(data, ['tolerance', 'fault_tolerance', 'rules_tolerance', '容错度'])
context = ' '.join([raw, experience, competition, mood, age_mix, tolerance]).lower()
if players >= 6:
scores['party'] += 4
scores['deduction'] += 2
scores['coop'] += 1
scores['strategy'] -= 2
elif 4 <= players <= 5:
scores['family'] += 2
scores['coop'] += 2
scores['deduction'] += 1
elif 2 <= players <= 3:
scores['strategy'] += 3
scores['family'] += 1
scores['coop'] += 1
scores['party'] -= 1
if minutes:
if minutes <= 30:
scores['party'] += 3
scores['family'] += 2
scores['coop'] += 1
scores['strategy'] -= 2
elif minutes <= 60:
scores['family'] += 2
scores['coop'] += 1
scores['deduction'] += 1
else:
scores['strategy'] += 3
scores['coop'] += 2
scores['party'] -= 1
if _contains_any(context, ['new', 'newbie', 'beginner', 'first time', 'family', 'kids', 'children', 'mixed ages', '亲子', '新手']):
scores['family'] += 4
scores['party'] += 2
scores['coop'] += 1
scores['strategy'] -= 3
scores['deduction'] -= 1
if _contains_any(context, ['cooperate', 'co-op', 'cooperative', 'team', 'together', 'help each other', 'teamwork', '合作']):
scores['coop'] += 4
if _contains_any(context, ['mystery', 'bluff', 'detective', 'deduction', 'hidden role', 'social deduction', '推理', '狼人']):
scores['deduction'] += 4
if _contains_any(context, ['lively', 'laugh', 'party', 'icebreaker', 'social', 'noisy', '欢乐', '热闹']):
scores['party'] += 4
if _contains_any(context, ['strategy', 'planning', 'deep', 'thinky', 'heavy', 'brain burn', '规划', '烧脑']):
scores['strategy'] += 3
if _contains_any(context, ['low competition', 'gentle', 'casual', 'light pressure', 'friendly', '不想太卷', '低竞争']):
scores['coop'] += 3
scores['family'] += 2
scores['deduction'] -= 1
if _contains_any(context, ['high competition', 'duel', 'cutthroat', 'intense', '想赢', '高竞争']):
scores['strategy'] += 2
scores['deduction'] += 1
if _contains_any(context, ['easy mistakes', 'low rules friction', 'forgiving', '容错', '轻松']):
scores['family'] += 2
scores['party'] += 1
scores['strategy'] -= 1
return scores, players, minutes, context
def _sorted_types(scores: Dict[str, int]) -> List[Tuple[str, int]]:
return sorted(scores.items(), key=lambda item: (item[1], TYPE_LIBRARY[item[0]]['label']), reverse=True)
def _table_tempo(players: int, minutes: int, context: str) -> str:
tempo_bits: List[str] = []
if players:
tempo_bits.append(f'{players} players')
else:
tempo_bits.append('player count not fully specified')
if minutes:
tempo_bits.append(f'about {minutes} minutes')
else:
tempo_bits.append('time window not fully specified')
if _contains_any(context, ['new', 'newbie', 'family', 'kids', 'mixed ages', '新手', '亲子']):
tempo_bits.append('newcomer-friendly table')
if _contains_any(context, ['lively', 'party', 'social', 'laugh', '欢乐', '热闹']):
tempo_bits.append('high social energy')
elif _contains_any(context, ['calm', 'gentle', 'cozy', '合作', 'friendly']):
tempo_bits.append('gentle room energy')
return ', '.join(tempo_bits)
def _recommended_reason(profile_id: str, players: int, minutes: int, context: str) -> str:
reason_bits = [TYPE_LIBRARY[profile_id]['best_for']]
if profile_id == 'party' and players >= 6:
reason_bits.append('The table is large enough that quick turns and easy laughter matter more than deep optimization.')
if profile_id == 'coop' and _contains_any(context, ['low competition', 'team', 'co-op', 'cooperative', '合作']):
reason_bits.append('Shared wins lower social friction and keep mixed skill players engaged.')
if profile_id == 'family' and _contains_any(context, ['new', 'family', 'kids', 'mixed ages', '新手', '亲子']):
reason_bits.append('The room likely benefits from low explanation cost and forgiving decisions.')
if profile_id == 'deduction' and _contains_any(context, ['mystery', 'bluff', 'deduction', 'hidden role', '推理']):
reason_bits.append('The group seems to enjoy reading people and talking through uncertainty.')
if profile_id == 'strategy' and minutes >= 60:
reason_bits.append('The time window is long enough to reward planning instead of rushing through rules.')
return ' '.join(reason_bits)
def _avoid_reason(profile_id: str, context: str) -> str:
del context
return f"Avoid this style if {TYPE_LIBRARY[profile_id]['avoid_when']}."
def _quick_start(top_profile: str, context: str) -> str:
if top_profile == 'party':
return 'Explain only the win condition, run one demo round, and keep the first teach under 3 minutes.'
if top_profile == 'coop':
return 'Name one shared objective out loud, assign a first move to the least experienced player, and treat round one as a learning turn.'
if top_profile == 'family':
return 'Lead with the easiest scoring rule first, then add the optional detail only after one visible turn.'
if top_profile == 'deduction':
return 'Before the first round, tell the table whether this session should feel playful or competitive so bluff tension stays fun.'
return 'Show the scoring arc first, then let players touch components before explaining every edge case.'
def _build_result(raw: str, data: Dict[str, Any], template: str) -> str:
scores, players, minutes, context = _score_profiles(raw, data)
ranked = _sorted_types(scores)
top_ids = [item[0] for item in ranked[:2]]
backup_id = ranked[2][0] if len(ranked) > 2 else ranked[0][0]
avoid_id = ranked[-1][0]
lines: List[str] = []
lines.append('# Boardgame Match Card')
lines.append('')
lines.append('## Table Snapshot')
lines.append(f'- Situation read: {_table_tempo(players, minutes, context)}')
lines.append(f"- Decision frame: {template.splitlines()[0]}")
lines.append(f"- Strongest fit right now: {TYPE_LIBRARY[top_ids[0]]['label']}")
lines.append('')
lines.append('## Recommended Types')
for idx, profile_id in enumerate(top_ids, start=1):
profile = TYPE_LIBRARY[profile_id]
lines.append(f"### {idx}. {profile['label']}")
lines.append(f"- Why it fits: {_recommended_reason(profile_id, players, minutes, context)}")
lines.append(f"- Mechanism keywords: {profile['keywords']}")
lines.append(f"- Example direction: classic titles like {profile['examples']} can help, but this is not a live catalog lookup.")
lines.append('')
lines.append('## Backup Type')
lines.append(f"- {TYPE_LIBRARY[backup_id]['label']} — useful if the room energy shifts after the first game.")
lines.append(f"- Why keep it in reserve: {_recommended_reason(backup_id, players, minutes, context)}")
lines.append('')
lines.append('## Avoid This Round')
lines.append(f"- {TYPE_LIBRARY[avoid_id]['label']} — {_avoid_reason(avoid_id, context)}")
lines.append('')
lines.append('## Quick Start')
lines.append(f"- {_quick_start(top_ids[0], context)}")
return '\n'.join(lines)
def handle(args: Dict[str, Any]) -> Dict[str, str]:
skill_name = args.get('skill_name', SKILL_SLUG) or SKILL_SLUG
user_input = args.get('input', '')
mode = args.get('mode', 'guide')
meta = _load_skill_meta(skill_name)
template = _load_prompt_template(skill_name)
if mode == 'meta':
return {'result': json.dumps(meta, ensure_ascii=False, indent=2)}
if mode == 'prompt':
return {'result': template}
raw, data = _coerce_input(user_input)
return {'result': _build_result(raw, data, template)}
if __name__ == '__main__':
print(json.dumps(handle({'skill_name': SKILL_SLUG, 'input': 'Players: 6\nTime: 30 minutes\nMood: lively and social', 'mode': 'guide'}), ensure_ascii=False, indent=2))
FILE:tests/test_handler.py
#!/usr/bin/env python3
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from handler import handle
def test_large_social_group_prefers_party():
result = handle({
'skill_name': 'boardgame-picker',
'input': {
'players': 7,
'duration': 25,
'experience': 'mostly new players',
'mood': 'lively and social',
'competition': 'low',
},
})['result']
assert 'Party / Icebreaker' in result
assert 'Quick Start' in result
assert 'Avoid This Round' in result
def test_mixed_age_table_gets_safe_option():
result = handle({
'skill_name': 'boardgame-picker',
'input': 'Players: 4\nTime: 40 minutes\nAge mix: parent and kids\nCompetition: low\nMood: teamwork and light pressure',
})['result']
assert 'Table Snapshot' in result
assert 'Recommended Types' in result
assert ('Cooperative / Team Challenge' in result) or ('Family / Gateway' in result)
def test_prompt_mode_returns_template():
result = handle({'skill_name': 'boardgame-picker', 'mode': 'prompt'})['result']
assert 'board game style' in result.lower()
assert 'backup category' in result.lower()
if __name__ == '__main__':
for name, fn in list(globals().items()):
if name.startswith('test_') and callable(fn):
fn()
print('All tests passed.')