
Improvement Generator
- 1 installs
- 6 repo stars
- Updated April 13, 2026
- lanyasheng/auto-improvement-orchestrator-skill
Generates ranked improvement candidates for a target Claude skill, injecting prior failure traces so the next round avoids repeating the same failed dimension.
About
Produces ranked improvement candidates for a target skill from target analysis, feedback signals, and failure traces. A developer uses it as stage 1 of the auto-improvement pipeline, or standalone to propose fixes for one skill.
- Trace-aware generation deprioritizes candidate categories that failed in the previous iteration
- Outputs JSON candidates with category, risk_level, execution_plan, and priority_score
Improvement Generator by the numbers
- 1 all-time installs (skills.sh)
- Ranked #642 of 782 Skill Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/lanyasheng/auto-improvement-orchestrator-skill --skill improvement-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 6 |
| Last updated | April 13, 2026 |
| Repository | lanyasheng/auto-improvement-orchestrator-skill ↗ |
What it does
Generates ranked improvement candidates for a target Claude skill, injecting prior failure traces so the next round avoids repeating the same failed dimension.
Files
Improvement Generator
Produces ranked improvement candidates from target analysis, feedback signals, and failure traces.
When to Use
- 为目标 skill 生成结构化改进候选
- 把上次失败的 trace 注入下一轮(trace-aware reflection)
- 根据 trace 自动降低上次失败类别的候选优先级
- 结合 memory 和 feedback 多源信号生成高优先级候选
- 批量生成多个 skill 的候选列表供 discriminator 打分
- 在 autoloop 场景下由 orchestrator 自动调用,注入历史 trace
- 手动调试单个 skill 的改进方向时作为独立工具使用
- 对比有/无 trace 生成结果来验证 trace 注入是否生效
When NOT to Use
- 给候选打分 → use
improvement-discriminator - 评估 skill 结构 → use
improvement-learner - 全流程 → use
improvement-orchestrator - 执行已批准的变更 → use
improvement-executor - 门禁验证 → use
improvement-gate
Why Trace-Aware Generation Matters
问题: 没有 trace 注入时,LLM 每次都从零开始生成候选。如果上一轮在 accuracy 维度失败了,下一轮很可能再次生成相同类别的候选 — 因为 LLM 不知道上次失败了。实测中无 trace 重试的重复失败率高达 60-70%。
Tradeoff: trace 注入增加了 prompt 长度(约 200-500 tokens),但大幅降低了重复失败率。Because trace 包含失败维度、失败原因、已尝试策略三个关键信号,generator 可以在生成阶段就避开已知死路,而不是等到 discriminator 打分后才发现。这比 "生成 → 打分 → 发现重复 → 重新生成" 的循环节省 1-2 轮迭代。
Trace-Aware Generation
Previous failure on "accuracy" dimension
→ deprioritize candidates of the same category as the failed one
→ prioritize other dimensions' improvements instead
→ if same category failed ≥2 times, skip entirely and try adjacent dimensions<example> 正确: 第一次失败后注入 trace 重试 $ python3 scripts/propose.py --target /path/to/skill --trace failure_trace.json --output candidates.json → 生成的候选会自动避开上次失败的 accuracy 维度策略 </example>
<anti-example> 错误: 失败后不注入 trace 直接重试 → 没有 trace 信息,generator 无法降低失败类别的优先级,容易重复生成同类候选 → 失败 ≥3 次的自动跳过逻辑在 improvement-learner 中,不在 generator </anti-example>
Trace JSON Structure
trace 文件记录上一轮失败的完整上下文,generator 解析后调整候选优先级:
{
"iteration": 2,
"failed_dimension": "accuracy",
"failed_category": "add_code_examples",
"failure_reason": "code example added but not syntactically valid",
"attempted_strategies": ["append_bash_example", "append_python_snippet"],
"scores_before": {"accuracy": 0.67, "coverage": 0.85},
"scores_after": {"accuracy": 0.63, "coverage": 0.85}
}generator 收到这个 trace 后会:(1) 把 add_code_examples 类别的优先级降到最低,(2) 从 coverage/trigger_quality 等未失败维度寻找候选,(3) 如果 accuracy 下的其他类别(如 add_output_artifacts)未尝试过则仍可生成。
CLI
# Basic generation
python3 scripts/propose.py --target /path/to/skill --output candidates.json
# With failure trace (retry loop)
python3 scripts/propose.py --target /path/to/skill --trace failure.json --output candidates.json
# With memory/feedback sources
python3 scripts/propose.py --target /path/to/skill --source memory.json --output candidates.jsonOutput Artifacts
| Request | Deliverable |
|---|---|
| Generate | JSON array of ranked candidates with category, risk_level, execution_plan |
| With trace | Same format, priorities adjusted based on failure analysis |
| With memory | Candidates informed by historical patterns and past successes |
| With feedback | Candidates prioritized by user correction hotspots |
每个候选的 JSON 结构包含 category(改进类别)、risk_level(low/medium/high)、execution_plan(具体修改步骤)、priority_score(0-1 综合优先级)、trace_adjusted(是否被 trace 调整过优先级)。
Related Skills
- improvement-discriminator: Scores the candidates this skill produces
- improvement-orchestrator: Calls generator as stage 1
- improvement-learner: Provides evaluation data that informs candidate selection
- improvement-executor: Executes the top-ranked candidate approved by gate
- session-feedback-analyzer: Generates feedback.jsonl that feeds into candidate prioritization
[
{
"type": "coverage",
"succeeded": true,
"context": {
"dimension": "coverage",
"scores": {
"coverage": 1.0,
"accuracy": 0.8,
"efficiency": 1.0,
"reliability": 1.0,
"security": 0.8
}
},
"timestamp": "2026-04-02T12:14:18Z",
"hit_count": 1
},
{
"type": "instruction",
"succeeded": true,
"context": {
"dimension": "accuracy",
"scores": {
"coverage": 1.0,
"accuracy": 0.8,
"efficiency": 1.0,
"reliability": 1.0,
"security": 0.8
}
},
"timestamp": "2026-04-02T12:14:19Z",
"hit_count": 1
}
]
improvement-generator
Auto-generated README for improvement-generator skill.
#!/usr/bin/env python3
"""Proposer for the first runnable generic-skill lane."""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
_REPO_ROOT = Path(__file__).resolve().parents[3]
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
from lib.common import (
SCHEMA_VERSION,
choose_doc_file,
choose_guardrail_file,
choose_reference_file,
classify_feedback,
compute_target_profile,
expand_source,
load_source_paths,
normalize_target,
protected_target,
read_json,
slugify,
utc_now_iso,
write_json,
)
from lib.state_machine import (
DEFAULT_STATE_ROOT,
ensure_tree,
make_run_id,
update_state,
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Propose improvement candidates for generic-skill lane")
parser.add_argument("--lane", default="generic-skill")
parser.add_argument("--target", required=True, help="Target skill/file path")
parser.add_argument("--source", action="append", default=[], help="Optional memory/learnings/.feedback source")
parser.add_argument("--max-candidates", type=int, default=4)
parser.add_argument("--state-root", default=str(DEFAULT_STATE_ROOT))
parser.add_argument("--run-id", default=None)
parser.add_argument("--output", default=None)
parser.add_argument("--trace", default=None, help="Path to a failure trace JSON from a previous run")
return parser.parse_args()
def build_docs_candidate(target: Path, feedback_buckets: dict[str, list[str]], idx: int) -> dict | None:
doc_file = choose_doc_file(target)
if not doc_file:
return None
limitation_signal = feedback_buckets.get("limitations", [])
example_signal = feedback_buckets.get("examples", [])
rationale_parts = []
if limitation_signal:
rationale_parts.append("反馈提到用户会把 skill 误解为可直接执行/集成能力")
if example_signal:
rationale_parts.append("反馈提到 README/用法说明需要更明确的示例或边界")
if not rationale_parts:
rationale_parts.append("目标存在 Markdown 文档,可先做低风险文案补充来降低误用")
lines = [
"This skill is advisory/planning-oriented. It does not connect to external delivery platforms, schedule sends, or manage subscribers directly.",
"When answering requests, keep the strategy inside the skill and explicitly call out when execution, analytics, or platform operations require a separate automation or operator workflow.",
]
if example_signal:
lines.append("For production usage, pair the skill with a separate execution lane/tool for actual publishing, scheduling, or analytics collection.")
return {
"id": f"cand-{idx:02d}-{slugify(doc_file.stem)}",
"title": f"补充 {doc_file.name} 的执行边界说明",
"target_path": str(doc_file),
"category": "docs",
"rationale": ";".join(rationale_parts),
"risk_level": "low",
"proposed_change_summary": "追加一个短的 operator note/limitations 补充段,明确这是策略型 skill,不直接代替外部执行工具。",
"stage": "proposed",
"source_refs": limitation_signal[:2] + example_signal[:1],
"executor_support": True,
"execution_plan": {
"action": "append_markdown_section",
"section_heading": "## Operator Notes",
"content_lines": lines,
},
}
def build_reference_candidate(target: Path, feedback_buckets: dict[str, list[str]], idx: int) -> dict | None:
reference_file = choose_reference_file(target)
if not reference_file:
return None
workflow_signal = feedback_buckets.get("workflow", [])
rationale = "反馈中提到执行流程/阶段推进需要更可见;references 目录适合补充 control-plane-friendly 说明。"
lines = [
"Stage artifacts should remain machine-readable so a later control plane can read current stage, truth anchor, and next owner without parsing free-form prose.",
"Prefer explicit fields such as `stage`, `status`, `next_step`, `next_owner`, and `truth_anchor` in every receipt or state artifact.",
]
if workflow_signal:
lines.append("For manual review, keep a pending promotion receipt rather than silently leaving unpromoted edits in the working tree.")
return {
"id": f"cand-{idx:02d}-{slugify(reference_file.stem)}",
"title": f"补充 {reference_file.name} 的状态推进说明",
"target_path": str(reference_file),
"category": "reference",
"rationale": rationale,
"risk_level": "low",
"proposed_change_summary": "在 reference 文档末尾追加一小节,说明 machine-readable artifacts / truth_anchor / next_owner 字段约定。",
"stage": "proposed",
"source_refs": workflow_signal[:2],
"executor_support": True,
"execution_plan": {
"action": "append_markdown_section",
"section_heading": "## Control-Plane-Friendly Notes",
"content_lines": lines,
},
}
def build_guardrail_candidate(target: Path, feedback_buckets: dict[str, list[str]], idx: int) -> dict | None:
guardrail_file = choose_guardrail_file(target)
if not guardrail_file:
return None
risk_signal = feedback_buckets.get("guardrails", []) or feedback_buckets.get("limitations", [])
return {
"id": f"cand-{idx:02d}-{slugify(guardrail_file.stem)}",
"title": f"补充 {guardrail_file.name} 的保守执行说明",
"target_path": str(guardrail_file),
"category": "guardrail",
"rationale": "反馈提到风险/边界,需要在 guardrail 文档里加一句保守执行原则,避免把规划性 skill 当成自动执行器。",
"risk_level": "low",
"proposed_change_summary": "追加简短 guardrail 条目:低风险文档才能自动 keep,其他候选应进入 pending/review。",
"stage": "proposed",
"source_refs": risk_signal[:2],
"executor_support": True,
"execution_plan": {
"action": "append_markdown_section",
"section_heading": "## Conservative Auto-Promote Rule",
"content_lines": [
"Only low-risk docs/reference/guardrail edits should be auto-kept in the first runnable version.",
"Anything that changes prompt structure, workflow behavior, tests, or code paths should stay pending for explicit human promotion.",
],
},
}
def build_prompt_candidate(target: Path, feedback_buckets: dict[str, list[str]], idx: int) -> dict | None:
prompt_target = target / "SKILL.md" if target.is_dir() else target
if not prompt_target.exists():
return None
prompt_signal = feedback_buckets.get("prompt", []) or feedback_buckets.get("workflow", [])
return {
"id": f"cand-{idx:02d}-{slugify(prompt_target.stem)}-prompt",
"title": "重构入口 prompt 的导航结构",
"target_path": str(prompt_target.resolve()),
"category": "prompt",
"rationale": "入口 prompt/skill 导航重构可能影响触发行为与发现路径,属于中风险改动,第一版不自动执行。",
"risk_level": "medium",
"proposed_change_summary": "把长段提示改成更短的导航结构,保留更多约束前移到顶部。",
"stage": "proposed",
"source_refs": prompt_signal[:2],
"executor_support": False,
"execution_plan": {
"action": "unsupported_manual_refactor",
"notes": "留给后续 skill-evaluator/judge + human review。",
},
}
def build_tests_candidate(target: Path, feedback_buckets: dict[str, list[str]], idx: int) -> dict:
test_signal = feedback_buckets.get("tests", [])
target_path = target / "SKILL.md" if target.is_dir() else target
return {
"id": f"cand-{idx:02d}-{slugify(target.name)}-tests",
"title": "为 skill 增加 smoke-check/validation 用例",
"target_path": str(target_path.resolve()),
"category": "tests",
"rationale": "测试/验证资产对长期自动改进很重要,但会引入额外结构与约束,第一版只做候选保留,不自动落地。",
"risk_level": "medium",
"proposed_change_summary": "补一组 smoke examples 或校验清单,供后续 evaluator/control-plane 消费。",
"stage": "proposed",
"source_refs": test_signal[:2],
"executor_support": False,
"execution_plan": {
"action": "unsupported_manual_test_addition",
"notes": "待后续接 skill-evaluator / hidden benchmark 后实现。",
},
}
def build_workflow_candidate(target: Path, feedback_buckets: dict[str, list[str]], idx: int) -> dict:
workflow_signal = feedback_buckets.get("workflow", [])
target_path = target / "SKILL.md" if target.is_dir() else target
return {
"id": f"cand-{idx:02d}-{slugify(target.name)}-workflow",
"title": "补充从建议到执行的 workflow adapter 说明",
"target_path": str(target_path.resolve()),
"category": "workflow",
"rationale": "workflow 改动会影响未来 control-plane 接口与用户预期,当前版本应先进入 hold/pending,而不是自动改。",
"risk_level": "medium",
"proposed_change_summary": "增加执行流程图、adapter 接口说明或 orchestration 钩子文案。",
"stage": "proposed",
"source_refs": workflow_signal[:2],
"executor_support": False,
"execution_plan": {
"action": "unsupported_manual_workflow_change",
"notes": "后续在 richer adapter/control-plane 接入时实现。",
},
}
def load_failure_trace(trace_path: Path | None) -> dict | None:
"""Load a failure trace from a previous run."""
if not trace_path or not trace_path.exists():
return None
return read_json(trace_path)
def adjust_candidates_from_trace(candidates: list, trace: dict) -> list:
"""Adjust candidate priorities based on failure trace."""
failed_id = trace.get("candidate_id", "")
failed_category = ""
# Extract category from candidate_id (e.g., "cand-01-docs" -> "docs")
parts = failed_id.split("-")
if len(parts) >= 3:
failed_category = parts[-1]
adjusted = []
for c in candidates:
if c["category"] == failed_category:
# Deprioritize the same category that failed
c["rationale"] = (
f"[Retry] Previous {failed_category} attempt failed: "
f"{trace.get('reason', 'unknown')}. {c['rationale']}"
)
# Move to end
adjusted.append(c)
else:
adjusted.insert(0, c) # Boost alternatives
return adjusted
def _find_evaluator_failures(feedback_entries: list[dict]) -> list[dict]:
"""Extract evaluator baseline failure data from source entries.
Source entries from expand_source() have {path, kind, snippet} format,
not the raw JSON content. We need to read the actual file if it looks
like a baseline-failures JSON.
"""
for entry in feedback_entries:
if not isinstance(entry, dict):
continue
# Direct match (if raw JSON was passed)
if entry.get("type") == "evaluator_baseline_failures":
return entry.get("failed_tasks", [])
# Match via source entry path
entry_path = entry.get("path", "")
if "baseline-failures" in entry_path or "eval-trace" in entry_path:
try:
data = read_json(Path(entry_path))
if data.get("type") == "evaluator_baseline_failures":
return data.get("failed_tasks", [])
except Exception:
continue
return []
def _llm_propose_skill_fix(target: Path, failed_tasks: list[dict]) -> dict | None:
"""Use LLM to propose a SKILL.md fix based on evaluator failures.
Reads the current SKILL.md, sends it + failure details to claude -p,
and asks for a targeted fix.
"""
import shutil
import subprocess
import json as _json
skill_md = target / "SKILL.md" if target.is_dir() else target
if not skill_md.exists():
return None
if shutil.which("claude") is None:
return None
skill_content = skill_md.read_text(encoding="utf-8")
failures_text = "\n".join(
f"- Task '{t['task_id']}': {t.get('details', t.get('error', 'unknown'))}"
for t in failed_tasks
)
prompt = (
"You are improving a SKILL.md file based on evaluator task failures.\n\n"
f"## Current SKILL.md\n```\n{skill_content[:3000]}\n```\n\n"
f"## Failed Tasks\n{failures_text}\n\n"
"Analyze why these tasks failed and propose a TARGETED change to SKILL.md "
"that would fix the failures without breaking what already works.\n\n"
"Respond with ONLY a JSON object:\n"
'{{"section_heading": "## <heading to add/replace>", '
'"action": "append_markdown_section" or "replace_markdown_section", '
'"content_lines": ["line1", "line2", ...], '
'"rationale": "why this fixes the failures"}}'
)
try:
result = subprocess.run(
["claude", "-p", "--output-format", "json"],
input=prompt, capture_output=True, text=True, timeout=120,
)
if result.returncode != 0:
return None
# Parse claude output
try:
claude_out = _json.loads(result.stdout)
text = claude_out.get("result", result.stdout)
except (_json.JSONDecodeError, TypeError):
text = result.stdout
# Extract JSON from response (may be wrapped in markdown)
text = text.strip()
# Find the first { and last } to extract JSON robustly
first_brace = text.find("{")
last_brace = text.rfind("}")
if first_brace >= 0 and last_brace > first_brace:
text = text[first_brace:last_brace + 1]
parsed = _json.loads(text)
return {
"id": "cand-01-eval-fix",
"title": f"Fix SKILL.md based on {len(failed_tasks)} evaluator failure(s)",
"target_path": str(skill_md.resolve()),
"category": "prompt",
"rationale": parsed.get("rationale", "LLM-proposed fix for evaluator failures"),
"risk_level": "low",
"proposed_change_summary": f"Targeted SKILL.md fix for failed tasks: {', '.join(t['task_id'] for t in failed_tasks)}",
"stage": "proposed",
"source_refs": [f"evaluator:{t['task_id']}" for t in failed_tasks],
"executor_support": True,
"execution_plan": {
"action": parsed.get("action", "append_markdown_section"),
"section_heading": parsed.get("section_heading", "## Output Format"),
"content_lines": parsed.get("content_lines", []),
},
}
except Exception:
return None
def _llm_analyze_and_propose(target: Path, max_candidates: int) -> list[dict] | None:
"""Use LLM to analyze a SKILL.md and propose quality improvements.
This is the default proposal path (Plan A). Reads the target SKILL.md,
sends it to claude CLI for analysis, and returns concrete improvement
candidates based on actual content issues rather than generic templates.
Returns None if claude CLI is unavailable or parsing fails, so the
caller can fall back to template builders.
"""
import shutil
import subprocess
import json as _json
skill_md = target / "SKILL.md" if target.is_dir() else target
if not skill_md.exists():
return None
if shutil.which("claude") is None:
return None
skill_content = skill_md.read_text(encoding="utf-8")
prompt = (
"You are a skill quality analyst. Analyze this SKILL.md and identify "
f"the top {max_candidates} concrete quality issues.\n\n"
f"## SKILL.md Content\n```\n{skill_content[:4000]}\n```\n\n"
"For each issue, propose a specific fix. Respond with ONLY a JSON array "
"where each element has these fields:\n"
"- title: short description of the fix\n"
"- target_path: relative path within the skill (use \"SKILL.md\" if targeting the main file)\n"
"- category: one of docs/reference/guardrail/prompt/workflow/tests\n"
"- risk_level: low/medium/high\n"
"- rationale: why this change improves quality\n"
"- proposed_change_summary: what will be changed\n"
"- executor_support: boolean, true if the change can be applied automatically\n"
"- execution_plan: object with 'action' (append_markdown_section/replace_markdown_section/insert_before) "
"and 'content_lines' (array of strings to add/replace)\n\n"
"Focus on actionable, specific improvements — not generic advice. "
"Prefer low-risk changes that can be auto-applied. "
"Return ONLY the JSON array, no surrounding text."
)
try:
result = subprocess.run(
["claude", "-p", "--output-format", "json"],
input=prompt, capture_output=True, text=True, timeout=120,
)
if result.returncode != 0:
return None
# Parse claude output (may be wrapped in JSON envelope)
try:
claude_out = _json.loads(result.stdout)
text = claude_out.get("result", result.stdout)
except (_json.JSONDecodeError, TypeError):
text = result.stdout
# Extract JSON array from response (may be wrapped in markdown code blocks)
text = text.strip()
if "```" in text:
# Strip markdown fences
lines = text.split("\n")
inside = False
json_lines = []
for line in lines:
if line.strip().startswith("```"):
inside = not inside
continue
if inside:
json_lines.append(line)
text = "\n".join(json_lines).strip()
# Try to find JSON array boundaries
first_bracket = text.find("[")
last_bracket = text.rfind("]")
if first_bracket >= 0 and last_bracket > first_bracket:
text = text[first_bracket:last_bracket + 1]
parsed = _json.loads(text)
if not isinstance(parsed, list) or not parsed:
return None
# Convert parsed items to candidate dicts
candidates = []
skill_md_resolved = str(skill_md.resolve())
for idx, item in enumerate(parsed[:max_candidates], start=1):
if not isinstance(item, dict):
continue
# Resolve target_path relative to skill directory
item_target = item.get("target_path", "SKILL.md")
if target.is_dir():
resolved_path = str((target / item_target).resolve())
else:
resolved_path = skill_md_resolved
category = item.get("category", "docs")
valid_categories = {"docs", "reference", "guardrail", "prompt", "workflow", "tests"}
if category not in valid_categories:
category = "docs"
risk = item.get("risk_level", "low")
if risk not in {"low", "medium", "high"}:
risk = "low"
exec_plan = item.get("execution_plan", {})
if not isinstance(exec_plan, dict):
exec_plan = {}
# Ensure required fields in execution_plan
if "action" not in exec_plan:
exec_plan["action"] = "append_markdown_section"
if "content_lines" not in exec_plan:
exec_plan["content_lines"] = []
candidates.append({
"id": f"cand-{idx:02d}-llm-{slugify(category)}",
"title": item.get("title", f"LLM-proposed improvement #{idx}"),
"target_path": resolved_path,
"category": category,
"rationale": item.get("rationale", "LLM-identified quality issue"),
"risk_level": risk,
"proposed_change_summary": item.get("proposed_change_summary", ""),
"stage": "proposed",
"source_refs": ["llm-analysis"],
"executor_support": bool(item.get("executor_support", False)),
"execution_plan": exec_plan,
})
return candidates if candidates else None
except Exception:
return None
def generate_candidates(target: Path, feedback_entries: list[dict], max_candidates: int) -> list[dict]:
# Priority 1: Evaluator failure-driven fix (highest-signal input)
eval_failures = _find_evaluator_failures(feedback_entries)
if eval_failures:
llm_fix = _llm_propose_skill_fix(target, eval_failures)
if llm_fix:
# Put the evaluator-driven fix first, then add template candidates
candidates = [llm_fix]
# Still generate template candidates as fallbacks
feedback_buckets = classify_feedback(feedback_entries)
idx = 2
for builder in [build_docs_candidate, build_reference_candidate]:
c = builder(target, feedback_buckets, idx)
if c:
candidates.append(c)
idx += 1
if len(candidates) >= max_candidates:
break
return candidates[:max_candidates]
# Priority 2: LLM analysis of target skill (default path)
llm_candidates = _llm_analyze_and_propose(target, max_candidates)
if llm_candidates:
return llm_candidates[:max_candidates]
# Priority 3: Template fallback (when claude CLI is unavailable)
feedback_buckets = classify_feedback(feedback_entries)
builders = [
build_docs_candidate,
build_reference_candidate,
build_guardrail_candidate,
build_prompt_candidate,
]
candidates: list[dict] = []
idx = 1
for builder in builders:
candidate = builder(target, feedback_buckets, idx)
if candidate:
candidates.append(candidate)
idx += 1
if len(candidates) >= max_candidates:
return candidates[:max_candidates]
for builder in (build_workflow_candidate, build_tests_candidate):
if len(candidates) >= max_candidates:
break
candidates.append(builder(target, feedback_buckets, idx))
idx += 1
if not candidates:
doc_file = choose_doc_file(target)
if doc_file:
candidates.append({
"id": "cand-01-fallback-docs",
"title": f"补充 {doc_file.name} 的使用边界",
"target_path": str(doc_file),
"category": "docs",
"rationale": "即使没有外部反馈,文档澄清也是 generic-skill lane 最安全的第一步。",
"risk_level": "low",
"proposed_change_summary": "追加一个短的 operator note 说明此 skill 的边界。",
"stage": "proposed",
"source_refs": [],
"executor_support": True,
"execution_plan": {
"action": "append_markdown_section",
"section_heading": "## Operator Notes",
"content_lines": [
"This skill is advisory/planning-oriented and should be paired with external tooling for operational execution.",
],
},
})
return candidates[:max_candidates]
def main() -> int:
args = parse_args()
state_root = Path(args.state_root).expanduser().resolve()
ensure_tree(state_root)
target = normalize_target(args.target)
run_id = args.run_id or make_run_id(target)
source_paths = load_source_paths(target, args.source)
source_entries: list[dict] = []
for source in source_paths:
source_entries.extend(expand_source(source))
trace_path = Path(args.trace).expanduser().resolve() if args.trace else None
failure_trace = load_failure_trace(trace_path)
target_profile = compute_target_profile(target)
candidates = generate_candidates(target, source_entries, args.max_candidates)
if failure_trace:
candidates = adjust_candidates_from_trace(candidates, failure_trace)
for candidate in candidates:
candidate["lane"] = args.lane
candidate["protected_target"] = protected_target(candidate["target_path"])
candidate["created_at"] = utc_now_iso()
output_path = Path(args.output).expanduser().resolve() if args.output else state_root / "candidate_versions" / f"{run_id}.json"
artifact = {
"schema_version": SCHEMA_VERSION,
"lane": args.lane,
"run_id": run_id,
"stage": "proposed",
"status": "success" if target_profile["exists"] else "target_missing",
"created_at": utc_now_iso(),
"target": target_profile,
"input_sources": source_entries,
"candidate_count": len(candidates),
"candidates": candidates,
"next_step": "rank_candidates",
"next_owner": "critic",
"truth_anchor": str(output_path),
"failure_trace_used": failure_trace is not None,
}
write_json(output_path, artifact)
update_state(
state_root,
run_id=run_id,
stage="proposed",
status=artifact["status"],
target_path=str(target),
truth_anchor=str(output_path),
extra={
"candidate_count": len(candidates),
"source_count": len(source_entries),
},
)
print(str(output_path))
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""Tests for the improvement-generator propose module."""
from __future__ import annotations
import json
import sys
from pathlib import Path
import importlib.util
import pytest
# repo root so we can import lib.common and the propose module
_REPO_ROOT = Path(__file__).resolve().parents[3]
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
REPO_ROOT = _REPO_ROOT
# The scripts directory uses dashes in skill names but Python needs underscores
# for imports. We import via importlib to work around the path layout.
_propose_path = REPO_ROOT / "skills" / "improvement-generator" / "scripts" / "propose.py"
_spec = importlib.util.spec_from_file_location("propose", _propose_path)
propose = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(propose)
# ---------------------------------------------------------------------------
# load_failure_trace
# ---------------------------------------------------------------------------
class TestLoadFailureTrace:
def test_returns_none_for_none_path(self):
assert propose.load_failure_trace(None) is None
def test_returns_none_for_missing_file(self, tmp_path):
missing = tmp_path / "does_not_exist.json"
assert propose.load_failure_trace(missing) is None
def test_loads_valid_trace(self, tmp_path):
trace_file = tmp_path / "trace.json"
trace_data = {
"candidate_id": "cand-01-docs",
"reason": "section already present",
}
trace_file.write_text(json.dumps(trace_data), encoding="utf-8")
result = propose.load_failure_trace(trace_file)
assert result == trace_data
# ---------------------------------------------------------------------------
# adjust_candidates_from_trace
# ---------------------------------------------------------------------------
class TestAdjustCandidatesFromTrace:
@staticmethod
def _make_candidate(category: str, idx: int = 1) -> dict:
return {
"id": f"cand-{idx:02d}-{category}",
"category": category,
"rationale": f"Original rationale for {category}",
}
def test_failed_category_moves_to_end(self):
trace = {"candidate_id": "cand-01-docs", "reason": "duplicate section"}
candidates = [
self._make_candidate("docs", 1),
self._make_candidate("reference", 2),
self._make_candidate("guardrail", 3),
]
adjusted = propose.adjust_candidates_from_trace(candidates, trace)
# "docs" should be last
assert adjusted[-1]["category"] == "docs"
# alternatives should be boosted to front
assert adjusted[0]["category"] in ("reference", "guardrail")
def test_failed_candidate_rationale_includes_reason(self):
trace = {"candidate_id": "cand-01-docs", "reason": "duplicate section"}
candidates = [self._make_candidate("docs", 1)]
adjusted = propose.adjust_candidates_from_trace(candidates, trace)
assert "[Retry]" in adjusted[0]["rationale"]
assert "duplicate section" in adjusted[0]["rationale"]
def test_alternatives_boosted_to_front(self):
trace = {"candidate_id": "cand-01-docs", "reason": "err"}
candidates = [
self._make_candidate("docs", 1),
self._make_candidate("reference", 2),
]
adjusted = propose.adjust_candidates_from_trace(candidates, trace)
assert adjusted[0]["category"] == "reference"
assert adjusted[1]["category"] == "docs"
def test_no_match_keeps_order(self):
trace = {"candidate_id": "cand-01-nonexistent", "reason": "err"}
candidates = [
self._make_candidate("docs", 1),
self._make_candidate("reference", 2),
]
adjusted = propose.adjust_candidates_from_trace(candidates, trace)
# All go to front via insert(0), so order reverses among non-matched
assert len(adjusted) == 2
# All candidates remain present
categories = {c["category"] for c in adjusted}
assert categories == {"docs", "reference"}
def test_empty_candidates(self):
trace = {"candidate_id": "cand-01-docs", "reason": "err"}
assert propose.adjust_candidates_from_trace([], trace) == []
# ---------------------------------------------------------------------------
# Candidate builders with a real temp directory
# ---------------------------------------------------------------------------
class TestCandidateBuilders:
@pytest.fixture
def skill_dir(self, tmp_path):
"""Create a minimal skill directory structure."""
(tmp_path / "SKILL.md").write_text("# Test Skill\n", encoding="utf-8")
(tmp_path / "README.md").write_text("# README\n", encoding="utf-8")
refs = tmp_path / "references"
refs.mkdir()
(refs / "guardrails.md").write_text("# Guardrails\n", encoding="utf-8")
(refs / "overview.md").write_text("# Overview\n", encoding="utf-8")
return tmp_path
def test_build_docs_candidate(self, skill_dir):
buckets = {"limitations": ["some limitation"], "examples": []}
result = propose.build_docs_candidate(skill_dir, buckets, 1)
assert result is not None
assert result["category"] == "docs"
assert result["risk_level"] == "low"
assert result["id"].startswith("cand-01-")
def test_build_reference_candidate(self, skill_dir):
buckets = {"workflow": ["stage transition"]}
result = propose.build_reference_candidate(skill_dir, buckets, 2)
assert result is not None
assert result["category"] == "reference"
def test_build_guardrail_candidate(self, skill_dir):
buckets = {"guardrails": ["risk signal"], "limitations": []}
result = propose.build_guardrail_candidate(skill_dir, buckets, 3)
assert result is not None
assert result["category"] == "guardrail"
def test_build_docs_returns_none_for_empty_dir(self, tmp_path):
buckets = {"limitations": [], "examples": []}
# tmp_path has no markdown files
result = propose.build_docs_candidate(tmp_path, buckets, 1)
assert result is None
def test_generate_candidates_produces_list(self, skill_dir):
candidates = propose.generate_candidates(skill_dir, [], 4)
assert isinstance(candidates, list)
assert len(candidates) > 0
assert len(candidates) <= 4