chore: add shared skills catalog (19 skills), installers, manifest, validator
This commit is contained in:
21
skills/research-deep/LICENSE
Normal file
21
skills/research-deep/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Lan Zheng
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
104
skills/research-deep/SKILL.md
Normal file
104
skills/research-deep/SKILL.md
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
name: research-deep
|
||||
description: Read research outline, launch independent agent for each item for deep research. Disable task output.
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# Research Deep - Deep Research
|
||||
|
||||
## Trigger
|
||||
`/research-deep`
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Auto-locate Outline
|
||||
Find `*/outline.yaml` file in current working directory, read items list, execution config (including items_per_agent).
|
||||
|
||||
### Step 2: Resume Check
|
||||
- Check completed JSON files in output_dir
|
||||
- Skip completed items
|
||||
|
||||
### Step 3: Batch Execution
|
||||
- Batch by batch_size (before launching the next batch, ask the user to confirm via `ask_user_question`)
|
||||
- Each agent handles items_per_agent items
|
||||
- Launch one background web-search subagent per batch via the `subagent` tool (run_in_background: true). Its prompt is the Prompt Template below with the {xxx} variables filled, immediately followed by the research methodology loaded from `.agents/skills/research/agents/web-search-agent.md` (resolved against the session workspace) with every `{RESEARCH_SKILL_DIR}` placeholder replaced by the absolute `research` skill directory. The subagent writes its JSON file itself and returns only a one-line completion note (task output disabled).
|
||||
|
||||
**Parameter Retrieval**:
|
||||
- `{topic}`: topic field from outline.yaml
|
||||
- `{item_name}`: item's name field
|
||||
- `{item_related_info}`: item's complete yaml content (name + category + description etc.)
|
||||
- `{output_dir}`: execution.output_dir from outline.yaml (default: ./results)
|
||||
- `{fields_path}`: absolute path to {topic}/fields.yaml
|
||||
- `{output_path}`: absolute path to {output_dir}/{item_name_slug}.json (slugify item_name: replace spaces with _, remove special chars)
|
||||
- `{validator_path}`: absolute path to this skill's bundled `validate_json.py` (this skill's Base directory from `<skill_resources>` + `validate_json.py`)
|
||||
|
||||
**Hard Constraint**: The following prompt must be strictly reproduced, only replacing variables in {xxx}, do not modify structure or wording.
|
||||
|
||||
**Prompt Template**:
|
||||
```python
|
||||
prompt = f"""## Task
|
||||
Research {item_related_info}, output structured JSON to {output_path}
|
||||
|
||||
## Field Definitions
|
||||
Read {fields_path} to get all field definitions
|
||||
|
||||
## Output Requirements
|
||||
1. Output JSON according to fields defined in fields.yaml
|
||||
2. Mark uncertain field values with [uncertain]
|
||||
3. Add uncertain array at the end of JSON, listing all uncertain field names
|
||||
4. All field values must be in English
|
||||
|
||||
## Output Path
|
||||
{output_path}
|
||||
|
||||
## Validation
|
||||
After completing JSON output, run validation script to ensure complete field coverage:
|
||||
python3 {validator_path} -f {fields_path} -j {output_path}
|
||||
Task is complete only after validation passes.
|
||||
"""
|
||||
```
|
||||
|
||||
**One-shot Example** (assuming researching GitHub Copilot):
|
||||
```
|
||||
## Task
|
||||
Research name: GitHub Copilot
|
||||
category: International Product
|
||||
description: Developed by Microsoft/GitHub, first mainstream AI coding assistant, ~40% market share, output structured JSON to {project_dir}/results/GitHub_Copilot.json
|
||||
|
||||
## Field Definitions
|
||||
Read {project_dir}/fields.yaml to get all field definitions
|
||||
|
||||
## Output Requirements
|
||||
1. Output JSON according to fields defined in fields.yaml
|
||||
2. Mark uncertain field values with [uncertain]
|
||||
3. Add uncertain array at the end of JSON, listing all uncertain field names
|
||||
4. All field values must be in English
|
||||
|
||||
## Output Path
|
||||
{project_dir}/results/GitHub_Copilot.json
|
||||
|
||||
## Validation
|
||||
After completing JSON output, run validation script to ensure complete field coverage:
|
||||
python3 {validator_path} -f {project_dir}/fields.yaml -j {project_dir}/results/GitHub_Copilot.json
|
||||
Task is complete only after validation passes.
|
||||
```
|
||||
|
||||
### Step 4: Wait and Monitor
|
||||
- Wait for current batch to complete
|
||||
- Launch next batch
|
||||
- Display progress
|
||||
|
||||
### Step 5: Summary Report
|
||||
After all complete, output:
|
||||
- Completion count
|
||||
- Failed/uncertain marked items
|
||||
- Output directory
|
||||
|
||||
## Agent Config
|
||||
- Background execution: Yes
|
||||
- Task Output: Disabled (agent has explicit output file when complete)
|
||||
- Resume support: Yes
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Require python3 with PyYAML. Export RESEARCH_DEEP_SKILL_DIR as this skill's absolute directory (the Base directory from `<skill_resources>`) on its own line before running the validator, e.g. `python3 "$RESEARCH_DEEP_SKILL_DIR/validate_json.py" -f <fields.yaml> -j <result.json>`; the launcher fills `{validator_path}` with that absolute path. (On the Windows shell tool the export is `$env:RESEARCH_DEEP_SKILL_DIR = "<path>"` on its own line within the same pwsh invocation.)
|
||||
198
skills/research-deep/validate_json.py
Normal file
198
skills/research-deep/validate_json.py
Normal file
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
CATEGORY_MAPPING = {
|
||||
"basic_info": ["basic_info", "Basic Info"],
|
||||
"technical_features": ["technical_features", "technical_characteristics", "Technical Features"],
|
||||
"performance_metrics": ["performance_metrics", "performance", "Performance Metrics"],
|
||||
"milestone_significance": ["milestone_significance", "milestones", "Milestone Significance"],
|
||||
"business_info": ["business_info", "commercial_info", "Business Info"],
|
||||
"competition_ecosystem": ["competition_ecosystem", "competition", "Competition Ecosystem"],
|
||||
"history": ["history", "History"],
|
||||
"market_positioning": ["market_positioning", "market", "Market Positioning"],
|
||||
}
|
||||
|
||||
_SKIP_KEYS = {"_source_file", "uncertain"}
|
||||
|
||||
|
||||
def load_fields_yaml(fields_path):
|
||||
"""Parse fields.yaml in the single schema the research skills emit:
|
||||
|
||||
fields:
|
||||
<category>:
|
||||
- {name: ..., description: ..., detail_level: ...}
|
||||
...
|
||||
uncertain: []
|
||||
|
||||
This is the ONLY accepted shape. A fields.yaml that does not match fails
|
||||
loudly instead of silently passing with zero fields.
|
||||
|
||||
Required semantics (so the validator can never pass vacuously / "lie"):
|
||||
- if ANY field carries an explicit `required:` key -> opt-in, preserve it
|
||||
- else (detail_level-style, no markers) -> ALL fields required, because the
|
||||
script's stated purpose is COMPLETE field coverage.
|
||||
"""
|
||||
with fields_path.open(encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
defs = [] # (name, category, required_or_None)
|
||||
|
||||
fn = data.get("fields")
|
||||
if not isinstance(fn, dict):
|
||||
print(f"[ERROR] fields.yaml must use the `fields: {{<category>: [{{name, ...}}]}}` shape; got {type(fn).__name__ if fn is not None else 'None'}.")
|
||||
sys.exit(1)
|
||||
|
||||
for cname, flist in fn.items():
|
||||
if cname in _SKIP_KEYS:
|
||||
continue
|
||||
if not isinstance(flist, list):
|
||||
print(f"[ERROR] category `{cname}` must map to a list of field dicts; got {type(flist).__name__}.")
|
||||
sys.exit(1)
|
||||
for field in flist:
|
||||
if isinstance(field, dict) and "name" in field:
|
||||
defs.append((str(field["name"]), str(cname), field.get("required", None)))
|
||||
else:
|
||||
print(f"[ERROR] field entry under `{cname}` must be a dict with a `name` key; got {field!r}.")
|
||||
sys.exit(1)
|
||||
|
||||
if not defs:
|
||||
print("[ERROR] fields.yaml parsed zero fields. Ensure at least one category with field dicts.")
|
||||
sys.exit(1)
|
||||
|
||||
all_fields = {n for n, _, _ in defs}
|
||||
if any(r is not None for _, _, r in defs):
|
||||
required_fields = {n for n, _, r in defs if r}
|
||||
else:
|
||||
required_fields = set(all_fields)
|
||||
field_categories = {n: c for n, c, _ in defs}
|
||||
return all_fields, required_fields, field_categories
|
||||
|
||||
|
||||
def extract_json_fields(data, category_mapping=None):
|
||||
category_mapping = CATEGORY_MAPPING if category_mapping is None else category_mapping
|
||||
nested_keys = {k for keys in category_mapping.values() for k in keys}
|
||||
fields = set()
|
||||
stack = [(data, True)]
|
||||
while stack:
|
||||
obj, is_category_level = stack.pop()
|
||||
if isinstance(obj, dict):
|
||||
for k, v in obj.items():
|
||||
if k in _SKIP_KEYS:
|
||||
continue
|
||||
if is_category_level and k in nested_keys:
|
||||
if isinstance(v, dict):
|
||||
stack.append((v, True))
|
||||
continue
|
||||
fields.add(k)
|
||||
elif isinstance(obj, list):
|
||||
stack.extend((item, is_category_level) for item in obj if isinstance(item, dict))
|
||||
return fields
|
||||
|
||||
|
||||
def validate_json(json_path, all_fields, required_fields, field_categories):
|
||||
with json_path.open(encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
json_fields = extract_json_fields(data)
|
||||
covered = all_fields & json_fields
|
||||
missing = all_fields - json_fields
|
||||
extra = json_fields - all_fields
|
||||
missing_required = missing & required_fields
|
||||
missing_by_category = defaultdict(list)
|
||||
for field in missing:
|
||||
missing_by_category[field_categories.get(field, "Unknown")].append(field)
|
||||
return {
|
||||
"file": json_path.name,
|
||||
"total_defined": len(all_fields),
|
||||
"covered": len(covered),
|
||||
"missing": len(missing),
|
||||
"extra": len(extra),
|
||||
"coverage_rate": len(covered) / len(all_fields) * 100 if all_fields else 100,
|
||||
"missing_required": sorted(missing_required),
|
||||
"missing_optional": sorted(missing - required_fields),
|
||||
"missing_by_category": {k: sorted(v) for k, v in missing_by_category.items()},
|
||||
"extra_fields": sorted(extra),
|
||||
"valid": len(missing_required) == 0,
|
||||
}
|
||||
|
||||
|
||||
def print_result(result, verbose=True):
|
||||
status = "PASS" if result["valid"] else "FAIL"
|
||||
line = "=" * 60
|
||||
print(f"\n{line}")
|
||||
print(f"[{status}] {result['file']}")
|
||||
print(line)
|
||||
print(f"Coverage: {result['coverage_rate']:.1f}% ({result['covered']}/{result['total_defined']})")
|
||||
if result["missing_required"]:
|
||||
print(f"\n[ERROR] Missing required fields ({len(result['missing_required'])}):")
|
||||
print("\n".join(f" - {f}" for f in result["missing_required"]))
|
||||
if verbose and result["missing_optional"]:
|
||||
missing_required = set(result["missing_required"])
|
||||
print(f"\n[WARN] Missing optional fields ({len(result['missing_optional'])}):")
|
||||
for cat in sorted(result["missing_by_category"]):
|
||||
optional = [f for f in result["missing_by_category"][cat] if f not in missing_required]
|
||||
if optional:
|
||||
print(f" [{cat}]: {', '.join(optional)}")
|
||||
if verbose and result["extra_fields"]:
|
||||
extra = result["extra_fields"]
|
||||
print(f"\n[INFO] Extra fields ({len(extra)}):")
|
||||
print(f" {', '.join(extra[:10])}")
|
||||
if len(extra) > 10:
|
||||
print(f" ... and {len(extra) - 10} more")
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description="Validate whether JSON files cover all fields defined in fields.yaml")
|
||||
parser.add_argument("--fields", "-f", type=str, help="Path to fields.yaml", default="fields.yaml")
|
||||
parser.add_argument("--json", "-j", type=str, nargs="*", help="JSON file paths to validate")
|
||||
parser.add_argument("--dir", "-d", type=str, help="Directory containing JSON files", default="results")
|
||||
parser.add_argument("--quiet", "-q", action="store_true", help="Show summary only")
|
||||
args = parser.parse_args()
|
||||
fields_path = Path(args.fields)
|
||||
if not fields_path.exists():
|
||||
for p in (Path.cwd() / "fields.yaml", Path.cwd().parent / "fields.yaml"):
|
||||
if p.exists():
|
||||
fields_path = p
|
||||
break
|
||||
if not fields_path.exists():
|
||||
print(f"[ERROR] fields.yaml not found: {fields_path}")
|
||||
sys.exit(1)
|
||||
print(f"Field definition file: {fields_path}")
|
||||
all_fields, required_fields, field_categories = load_fields_yaml(fields_path)
|
||||
print(f"Total fields: {len(all_fields)} (required: {len(required_fields)}, optional: {len(all_fields) - len(required_fields)})")
|
||||
json_files = (
|
||||
[Path(p) for p in args.json]
|
||||
if args.json
|
||||
else sorted(Path(args.dir).glob("*.json")) if Path(args.dir).exists() else []
|
||||
)
|
||||
if not json_files:
|
||||
print("[WARN] No JSON files found")
|
||||
sys.exit(0)
|
||||
results = []
|
||||
for json_path in json_files:
|
||||
if not json_path.exists():
|
||||
print(f"[WARN] File not found: {json_path}")
|
||||
continue
|
||||
result = validate_json(json_path, all_fields, required_fields, field_categories)
|
||||
results.append(result)
|
||||
print_result(result, verbose=not args.quiet)
|
||||
line = "=" * 60
|
||||
print(f"\n{line}")
|
||||
print("Summary")
|
||||
print(line)
|
||||
passed = sum(1 for r in results if r["valid"])
|
||||
avg_coverage = sum(r["coverage_rate"] for r in results) / len(results) if results else 0
|
||||
print(f"Validation passed: {passed}/{len(results)}")
|
||||
print(f"Average coverage: {avg_coverage:.1f}%")
|
||||
if passed < len(results):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user