distill

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

distill

distill

Post-mission audit and distillation for crew operations. Two modes:
  • capture — fetches mission metadata and CloudWatch logs via the hooks.fellowship.dev API, classifies incidents using the 8-code taxonomy, and POSTs a structured audit JSON to the API
  • analyze — fetches audit records from hooks.fellowship.dev, aggregates recurring failure patterns, manages GitHub issues per pattern, promotes exemplars, and reports trend direction
针对团队操作的任务后审计与提炼工具。包含两种模式:
  • capture(捕获) — 通过hooks.fellowship.dev API获取任务元数据和CloudWatch日志,使用8代码分类法对事件进行分类,并将结构化的审计JSON通过POST请求提交至API
  • analyze(分析) — 从hooks.fellowship.dev获取审计记录,汇总重复出现的失败模式,针对每种模式管理GitHub issue,推广典型案例,并报告趋势走向

When to Use

使用场景

  • capture: after every mission (pass or fail) — triggered by the poller as a follow-up mission
  • analyze: weekly via cron, or manually to survey trends across missions
  • Idempotent — the API rejects duplicate audits for the same job_id (409 response)
  • capture模式:每次任务完成后(无论成功或失败)使用 — 由轮询器作为后续任务触发
  • analyze模式:通过cron每周执行一次,或手动执行以调查跨任务的趋势
  • 幂等性 — API会拒绝针对同一job_id的重复审计请求(返回409响应)

Prerequisites

前置条件

bash
python3 --version                       # needed for log parsing
echo "${PYLOT_DISPATCH_TOKEN:?PYLOT_DISPATCH_TOKEN not set}"  # auth token required
bash
python3 --version                       # 日志解析需要该环境
echo "${PYLOT_DISPATCH_TOKEN:?PYLOT_DISPATCH_TOKEN not set}"  # 需要授权令牌

Failure Taxonomy

失败分类法

CodeNameOriginJSONL Signal
SVSpeed Over VerificationMeiklejohnBash/Write actions with no curl/
gh pr view
/test calls after
MBMemory Without Behavioral ChangeMeiklejohnRULES.md or skill Read early in session; same domain errors appear later
SFSilent Failure SuppressionMeiklejohn
is_error: true
tool results not followed by error-recovery Bash calls
UAUser Model AbsenceMeiklejohnDeploy/publish task; no curl HTTP 200 verification in session
UBUncertainty BlindnessMeiklejohn
--force
flags, skipped checks, or assumed paths without grep/read confirm
IDIntent DriftOursWrite/Edit calls to paths outside original task scope
SGSkill GapOursENOENT/command-not-found errors, or domain tool failures with no skill loaded
CFCoordination FailureOursAgent tool called 3+ times for same deliverable (worker retries)
Use code
??
for incidents that don't fit any category —
??
frequency feeds taxonomy evolution in analyze mode.

代码名称来源JSONL信号
SVSpeed Over Verification(重速度轻验证)MeiklejohnBash/Write操作后未执行curl/
gh pr view
/测试调用
MBMemory Without Behavioral Change(知规则却未执行)Meiklejohn会话早期读取RULES.md或skill文档;后续出现相同领域错误
SFSilent Failure Suppression(静默失败抑制)Meiklejohn
is_error: true
的工具结果后未执行错误恢复的Bash调用
UAUser Model Absence(缺乏用户验证模型)Meiklejohn部署/发布任务;会话中未执行curl HTTP 200验证
UBUncertainty Blindness(盲目忽略不确定性)Meiklejohn使用
--force
标志、跳过检查,或未通过grep/read确认就假设路径存在
IDIntent Drift(意图偏离)自研对原始任务范围外的路径进行Write/Edit调用
SGSkill Gap(技能缺失)自研出现ENOENT/command-not-found错误,或领域工具失败且未加载对应skill
CFCoordination Failure(协调失败)自研针对同一交付物调用Agent工具3次以上(工作节点重试)
对于不符合任何类别的事件,使用代码
??
— analyze模式中
??
的出现频率会推动分类法的演进。

capture Mode

capture模式

Invocation

调用方式

Task field:
distill capture <job_id>
The skill receives
job_id
from the task input (e.g.
task: 'distill capture abc123-uuid'
).
任务字段:
distill capture <job_id>
该skill从任务输入中获取
job_id
(例如:
task: 'distill capture abc123-uuid'
)。

Environment

环境变量

bash
undefined
bash
undefined

Required

必填

PYLOT_DISPATCH_TOKEN # Bearer token for hooks.fellowship.dev

All API calls go to `https://hooks.fellowship.dev` with header:
Authorization: Bearer $PYLOT_DISPATCH_TOKEN
undefined
PYLOT_DISPATCH_TOKEN # hooks.fellowship.dev的Bearer令牌

所有API请求都发送至`https://hooks.fellowship.dev`,并携带以下请求头:
Authorization: Bearer $PYLOT_DISPATCH_TOKEN
undefined

Step 1: Fetch mission metadata + anti-recursion guard

步骤1:获取任务元数据 + 防递归校验

Fetch mission record from the Pylot API. Extract metadata and apply the anti-recursion guard before doing any further work.
bash
MISSION=$(curl -sf \
  -H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN" \
  "https://hooks.fellowship.dev/missions/${job_id}")

if [ $? -ne 0 ] || [ -z "$MISSION" ]; then
  echo "ERROR: Failed to fetch mission $job_id"
  exit 1
fi
从Pylot API获取任务记录。提取元数据并应用防递归校验后,再执行后续操作。
bash
MISSION=$(curl -sf \
  -H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN" \
  "https://hooks.fellowship.dev/missions/${job_id}")

if [ $? -ne 0 ] || [ -z "$MISSION" ]; then
  echo "ERROR: Failed to fetch mission $job_id"
  exit 1
fi

Extract metadata fields

提取元数据字段

TASK=$(echo "$MISSION" | python3 -c "import json,sys; m=json.load(sys.stdin); print(m.get('task',''))") STATUS=$(echo "$MISSION" | python3 -c "import json,sys; m=json.load(sys.stdin); print(m.get('status',''))") EXIT_CODE=$(echo "$MISSION" | python3 -c "import json,sys; m=json.load(sys.stdin); print(m.get('exit_code',''))") TEAM=$(echo "$MISSION" | python3 -c "import json,sys; m=json.load(sys.stdin); print(m.get('team',''))") AGENT=$(echo "$MISSION" | python3 -c "import json,sys; m=json.load(sys.stdin); print(m.get('agent',''))") STARTED_AT=$(echo "$MISSION" | python3 -c "import json,sys; m=json.load(sys.stdin); print(m.get('started_at',''))") FINISHED_AT=$(echo "$MISSION" | python3 -c "import json,sys; m=json.load(sys.stdin); print(m.get('finished_at',''))") REPORT=$(echo "$MISSION" | python3 -c "import json,sys; m=json.load(sys.stdin); print(m.get('report',''))")
TASK=$(echo "$MISSION" | python3 -c "import json,sys; m=json.load(sys.stdin); print(m.get('task',''))") STATUS=$(echo "$MISSION" | python3 -c "import json,sys; m=json.load(sys.stdin); print(m.get('status',''))") EXIT_CODE=$(echo "$MISSION" | python3 -c "import json,sys; m=json.load(sys.stdin); print(m.get('exit_code',''))") TEAM=$(echo "$MISSION" | python3 -c "import json,sys; m=json.load(sys.stdin); print(m.get('team',''))") AGENT=$(echo "$MISSION" | python3 -c "import json,sys; m=json.load(sys.stdin); print(m.get('agent',''))") STARTED_AT=$(echo "$MISSION" | python3 -c "import json,sys; m=json.load(sys.stdin); print(m.get('started_at',''))") FINISHED_AT=$(echo "$MISSION" | python3 -c "import json,sys; m=json.load(sys.stdin); print(m.get('finished_at',''))") REPORT=$(echo "$MISSION" | python3 -c "import json,sys; m=json.load(sys.stdin); print(m.get('report',''))")

Anti-recursion guard — never capture a capture/audit/analyze mission

防递归校验 — 绝不捕获capture/audit/analyze类型的任务

if echo "$TASK" | grep -qiE 'distill|capture|audit|analyze'; then echo "Anti-recursion: skipping capture for task: $TASK" exit 0 fi
undefined
if echo "$TASK" | grep -qiE 'distill|capture|audit|analyze'; then echo "Anti-recursion: skipping capture for task: $TASK" exit 0 fi
undefined

Step 2: Stream CloudWatch logs via SSE

步骤2:通过SSE流式获取CloudWatch日志

bash
curl -sf \
  -H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN" \
  -H "Accept: text/event-stream" \
  "https://hooks.fellowship.dev/missions/${job_id}/logs/stream" \
  > /tmp/distill_logs_${job_id}.txt

echo "Log stream saved."
The SSE stream delivers CloudWatch log lines as JSON events. Each
data:
line is a JSON object with
timestamp
/
ts
and
message
/
msg
fields.
bash
curl -sf \
  -H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN" \
  -H "Accept: text/event-stream" \
  "https://hooks.fellowship.dev/missions/${job_id}/logs/stream" \
  > /tmp/distill_logs_${job_id}.txt

echo "Log stream saved."
SSE流以JSON事件形式传递CloudWatch日志行。每个
data:
行都是包含
timestamp
/
ts
message
/
msg
字段的JSON对象。

Step 3: Parse SSE events — extract signals

步骤3:解析SSE事件 — 提取信号

Write and run the SSE parser:
bash
cat > /tmp/distill_parse_sse.py << 'PYEOF'
import re, sys, json, collections

log_file = sys.argv[1]
signals = {
    "session_complete": True,
    "tool_counts": {},
    "error_tool_results": [],
    "workers_spawned": 0,
    "workers_spawned_sessions": [],
    "skills_loaded": [],
    "verification_calls": [],
    "total_input_tokens": 0,
    "total_output_tokens": 0,
    "original_prompt": None,
    "write_paths": [],
    "bash_calls": [],
    "git_signals": {
        "branch_created": False,
        "pushed": False,
        "push_force": False,
        "pr_created": False,
        "issue_closed": False,
        "committed_to_main": False,
        "push_verified": False
    },
    "timing": {
        "container_start_ts": None,
        "first_claude_call_ts": None,
        "first_token_ts": None,
        "done_signal_ts": None
    }
}
tool_counts = collections.Counter()

try:
    with open(log_file) as f:
        content = f.read()
except Exception as e:
    signals["session_complete"] = False
    print(json.dumps(signals))
    sys.exit(0)
编写并运行SSE解析器:
bash
cat > /tmp/distill_parse_sse.py << 'PYEOF'
import re, sys, json, collections

log_file = sys.argv[1]
signals = {
    "session_complete": True,
    "tool_counts": {},
    "error_tool_results": [],
    "workers_spawned": 0,
    "workers_spawned_sessions": [],
    "skills_loaded": [],
    "verification_calls": [],
    "total_input_tokens": 0,
    "total_output_tokens": 0,
    "original_prompt": None,
    "write_paths": [],
    "bash_calls": [],
    "git_signals": {
        "branch_created": False,
        "pushed": False,
        "push_force": False,
        "pr_created": False,
        "issue_closed": False,
        "committed_to_main": False,
        "push_verified": False
    },
    "timing": {
        "container_start_ts": None,
        "first_claude_call_ts": None,
        "first_token_ts": None,
        "done_signal_ts": None
    }
}
tool_counts = collections.Counter()

try:
    with open(log_file) as f:
        content = f.read()
except Exception as e:
    signals["session_complete"] = False
    print(json.dumps(signals))
    sys.exit(0)

Parse SSE data lines — each is a CloudWatch log entry JSON

Parse SSE data lines — each is a CloudWatch log entry JSON

for line in content.splitlines(): if not line.startswith("data: "): continue raw = line[6:].strip() if not raw or raw == "[DONE]": continue try: entry = json.loads(raw) except json.JSONDecodeError: signals["session_complete"] = False continue
ts = entry.get("timestamp") or entry.get("ts")
msg = entry.get("message", "") or entry.get("msg", "")
msg_lower = msg.lower()

# Timing markers
timing = signals["timing"]
if not timing["container_start_ts"] and any(k in msg_lower for k in [
        "container start", "ecs task started", "bootstrap"]):
    timing["container_start_ts"] = ts
if not timing["first_claude_call_ts"] and any(k in msg_lower for k in [
        "claude api", "anthropic.messages.create", "invoking claude"]):
    timing["first_claude_call_ts"] = ts
if not timing["first_token_ts"] and any(k in msg_lower for k in [
        "first token", "stream started", "content_block_start"]):
    timing["first_token_ts"] = ts
if not timing["done_signal_ts"] and re.search(r'\bdone\b', msg_lower):
    timing["done_signal_ts"] = ts

# Tool call detection from log message text
tool_match = re.search(
    r'\b(Read|Bash|Edit|Write|Grep|Glob|Agent|WebFetch|WebSearch|TodoWrite|TodoRead)\b',
    msg
)
if tool_match:
    tool = tool_match.group(1)
    tool_counts[tool] += 1

    if tool == "Agent":
        signals["workers_spawned"] += 1
    elif tool == "Write":
        path_match = re.search(r'file_path["\s:]+([^\s",}]+)', msg)
        if path_match:
            signals["write_paths"].append(path_match.group(1))
    elif tool == "Read":
        path_match = re.search(r'file_path["\s:]+([^\s",}]+)', msg)
        if path_match and ".claude/skills" in path_match.group(1):
            parts = path_match.group(1).split(".claude/skills/")
            if len(parts) > 1:
                skill_name = parts[1].split("/")[0]
                if skill_name and skill_name not in signals["skills_loaded"]:
                    signals["skills_loaded"].append(skill_name)
    elif tool == "Bash":
        cmd_match = re.search(r'command["\s:]+(.+?)(?:,\s*"|\}|$)', msg)
        if cmd_match:
            cmd = cmd_match.group(1).strip('"')[:300]
            signals["bash_calls"].append(cmd)
            if any(v in cmd for v in ["curl ", "gh pr view", "gh pr merge", "gh pr list", "gh run"]):
                signals["verification_calls"].append(cmd[:200])
            gs = signals["git_signals"]
            if re.search(r'git\s+(checkout\s+-b|switch\s+-c)', cmd):
                gs["branch_created"] = True
            if re.search(r'git\s+push', cmd):
                if "--force" in cmd or " -f " in cmd:
                    gs["push_force"] = True
                gs["pushed"] = True
            if "gh pr create" in cmd:
                gs["pr_created"] = True
            if "gh issue close" in cmd.lower():
                gs["issue_closed"] = True
            if (re.search(r'git\s+log.*--remotes', cmd) or
                    re.search(r'gh\s+api.*commits/', cmd) or
                    re.search(r'git\s+log.*origin/', cmd)):
                gs["push_verified"] = True
            if re.search(r'\bclaude\s+(-p|--print)\b', cmd):
                signals["workers_spawned"] += 1
                sid = re.search(r'--session-id\s+(\S+)', cmd)
                if sid:
                    signals["workers_spawned_sessions"].append(sid.group(1))

# Error patterns — ENOENT, permission denied, timeout, crash
error_patterns = ["enoent", "permission denied", "timeout", "crash", "error:", "exception:"]
if any(p in msg_lower for p in error_patterns):
    signals["error_tool_results"].append(msg[:300])

# Original prompt — first assistant message with task context
if not signals["original_prompt"] and "task:" in msg_lower:
    signals["original_prompt"] = msg[:500]

# Token usage from log lines
tok_in = re.search(r'input_tokens["\s:]+(\d+)', msg)
if tok_in:
    signals["total_input_tokens"] += int(tok_in.group(1))
tok_out = re.search(r'output_tokens["\s:]+(\d+)', msg)
if tok_out:
    signals["total_output_tokens"] += int(tok_out.group(1))
for line in content.splitlines(): if not line.startswith("data: "): continue raw = line[6:].strip() if not raw or raw == "[DONE]": continue try: entry = json.loads(raw) except json.JSONDecodeError: signals["session_complete"] = False continue
ts = entry.get("timestamp") or entry.get("ts")
msg = entry.get("message", "") or entry.get("msg", "")
msg_lower = msg.lower()

# Timing markers
timing = signals["timing"]
if not timing["container_start_ts"] and any(k in msg_lower for k in [
        "container start", "ecs task started", "bootstrap"]):
    timing["container_start_ts"] = ts
if not timing["first_claude_call_ts"] and any(k in msg_lower for k in [
        "claude api", "anthropic.messages.create", "invoking claude"]):
    timing["first_claude_call_ts"] = ts
if not timing["first_token_ts"] and any(k in msg_lower for k in [
        "first token", "stream started", "content_block_start"]):
    timing["first_token_ts"] = ts
if not timing["done_signal_ts"] and re.search(r'\bdone\b', msg_lower):
    timing["done_signal_ts"] = ts

# Tool call detection from log message text
tool_match = re.search(
    r'\b(Read|Bash|Edit|Write|Grep|Glob|Agent|WebFetch|WebSearch|TodoWrite|TodoRead)\b',
    msg
)
if tool_match:
    tool = tool_match.group(1)
    tool_counts[tool] += 1

    if tool == "Agent":
        signals["workers_spawned"] += 1
    elif tool == "Write":
        path_match = re.search(r'file_path["\s:]+([^\s",}]+)', msg)
        if path_match:
            signals["write_paths"].append(path_match.group(1))
    elif tool == "Read":
        path_match = re.search(r'file_path["\s:]+([^\s",}]+)', msg)
        if path_match and ".claude/skills" in path_match.group(1):
            parts = path_match.group(1).split(".claude/skills/")
            if len(parts) > 1:
                skill_name = parts[1].split("/")[0]
                if skill_name and skill_name not in signals["skills_loaded"]:
                    signals["skills_loaded"].append(skill_name)
    elif tool == "Bash":
        cmd_match = re.search(r'command["\s:]+(.+?)(?:,\s*"|\}|$)', msg)
        if cmd_match:
            cmd = cmd_match.group(1).strip('"')[:300]
            signals["bash_calls"].append(cmd)
            if any(v in cmd for v in ["curl ", "gh pr view", "gh pr merge", "gh pr list", "gh run"]):
                signals["verification_calls"].append(cmd[:200])
            gs = signals["git_signals"]
            if re.search(r'git\s+(checkout\s+-b|switch\s+-c)', cmd):
                gs["branch_created"] = True
            if re.search(r'git\s+push', cmd):
                if "--force" in cmd or " -f " in cmd:
                    gs["push_force"] = True
                gs["pushed"] = True
            if "gh pr create" in cmd:
                gs["pr_created"] = True
            if "gh issue close" in cmd.lower():
                gs["issue_closed"] = True
            if (re.search(r'git\s+log.*--remotes', cmd) or
                    re.search(r'gh\s+api.*commits/', cmd) or
                    re.search(r'git\s+log.*origin/', cmd)):
                gs["push_verified"] = True
            if re.search(r'\bclaude\s+(-p|--print)\b', cmd):
                signals["workers_spawned"] += 1
                sid = re.search(r'--session-id\s+(\S+)', cmd)
                if sid:
                    signals["workers_spawned_sessions"].append(sid.group(1))

# Error patterns — ENOENT, permission denied, timeout, crash
error_patterns = ["enoent", "permission denied", "timeout", "crash", "error:", "exception:"]
if any(p in msg_lower for p in error_patterns):
    signals["error_tool_results"].append(msg[:300])

# Original prompt — first assistant message with task context
if not signals["original_prompt"] and "task:" in msg_lower:
    signals["original_prompt"] = msg[:500]

# Token usage from log lines
tok_in = re.search(r'input_tokens["\s:]+(\d+)', msg)
if tok_in:
    signals["total_input_tokens"] += int(tok_in.group(1))
tok_out = re.search(r'output_tokens["\s:]+(\d+)', msg)
if tok_out:
    signals["total_output_tokens"] += int(tok_out.group(1))

committed_to_main heuristic

committed_to_main heuristic

if any(re.search(r'git\s+commit', b) for b in signals["bash_calls"]): signals["git_signals"]["committed_to_main"] = not signals["git_signals"]["branch_created"]
signals["tool_counts"] = dict(tool_counts) print(json.dumps(signals, default=str)) PYEOF
SIGNALS=$(python3 /tmp/distill_parse_sse.py "/tmp/distill_logs_${job_id}.txt") echo "Signals extracted."
undefined
if any(re.search(r'git\s+commit', b) for b in signals["bash_calls"]): signals["git_signals"]["committed_to_main"] = not signals["git_signals"]["branch_created"]
signals["tool_counts"] = dict(tool_counts) print(json.dumps(signals, default=str)) PYEOF
SIGNALS=$(python3 /tmp/distill_parse_sse.py "/tmp/distill_logs_${job_id}.txt") echo "Signals extracted."
undefined

Step 4: Extract timing metrics from log timestamps

步骤4:从日志时间戳提取时序指标

bash
TIMING_METRICS=$(python3 << PYEOF
import json
from datetime import datetime

signals = json.loads('''$SIGNALS''')
finished_at = "$FINISHED_AT"
timing = signals.get("timing", {})

def parse_ts(ts):
    if not ts:
        return None
    try:
        return datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
    except Exception:
        return None

def ms_between(a, b):
    if a and b:
        return round((b - a).total_seconds() * 1000)
    return None

container_start   = parse_ts(timing.get("container_start_ts"))
first_claude_call = parse_ts(timing.get("first_claude_call_ts"))
first_token       = parse_ts(timing.get("first_token_ts"))
done_signal       = parse_ts(timing.get("done_signal_ts"))
finished          = parse_ts(finished_at)

print(json.dumps({
    "cold_start_ms":    ms_between(container_start, first_claude_call),
    "first_token_ms":   ms_between(first_claude_call, first_token),
    "done_to_ended_ms": ms_between(done_signal, finished)
}))
PYEOF
)
  • cold_start_ms — time from container start log to first Claude API call log
  • first_token_ms — time from first Claude API call to first token received
  • done_to_ended_ms — time from
    done
    signal in logs to
    finished_at
    in missions DB
bash
TIMING_METRICS=$(python3 << PYEOF
import json
from datetime import datetime

signals = json.loads('''$SIGNALS''')
finished_at = "$FINISHED_AT"
timing = signals.get("timing", {})

def parse_ts(ts):
    if not ts:
        return None
    try:
        return datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
    except Exception:
        return None

def ms_between(a, b):
    if a and b:
        return round((b - a).total_seconds() * 1000)
    return None

container_start   = parse_ts(timing.get("container_start_ts"))
first_claude_call = parse_ts(timing.get("first_claude_call_ts"))
first_token       = parse_ts(timing.get("first_token_ts"))
done_signal       = parse_ts(timing.get("done_signal_ts"))
finished          = parse_ts(finished_at)

print(json.dumps({
    "cold_start_ms":    ms_between(container_start, first_claude_call),
    "first_token_ms":   ms_between(first_claude_call, first_token),
    "done_to_ended_ms": ms_between(done_signal, finished)
}))
PYEOF
)
  • cold_start_ms — 从容器启动日志到首次Claude API调用日志的时间
  • first_token_ms — 从首次Claude API调用到收到第一个token的时间
  • done_to_ended_ms — 从日志中的
    done
    信号到任务数据库中
    finished_at
    的时间

Step 5: Classify incidents

步骤5:事件分类

Apply taxonomy rules to the extracted signals. Produce an
incidents
array.
Write and run the classifier:
bash
cat > /tmp/distill_classify.py << 'PYEOF'
import json, re, sys

signals = json.loads(sys.argv[1])
report_text = open(sys.argv[2]).read().lower()  # mission report text (from API)

tool_counts = signals.get("tool_counts", {})
bash_calls = signals.get("bash_calls", [])
bash_all = " ".join(bash_calls).lower()
error_results = signals.get("error_tool_results", [])
write_paths = signals.get("write_paths", [])
verification_calls = signals.get("verification_calls", [])
workers_spawned = signals.get("workers_spawned", 0)
skills_loaded = signals.get("skills_loaded", [])
original_prompt = (signals.get("original_prompt") or "").lower()
git_signals = signals.get("git_signals", {})

incidents = []
what_worked = []
missed_opportunity = ""
将分类法规则应用于提取的信号,生成
incidents
数组。
编写并运行分类器:
bash
cat > /tmp/distill_classify.py << 'PYEOF'
import json, re, sys

signals = json.loads(sys.argv[1])
report_text = open(sys.argv[2]).read().lower()  # mission report text (from API)

tool_counts = signals.get("tool_counts", {})
bash_calls = signals.get("bash_calls", [])
bash_all = " ".join(bash_calls).lower()
error_results = signals.get("error_tool_results", [])
write_paths = signals.get("write_paths", [])
verification_calls = signals.get("verification_calls", [])
workers_spawned = signals.get("workers_spawned", 0)
skills_loaded = signals.get("skills_loaded", [])
original_prompt = (signals.get("original_prompt") or "").lower()
git_signals = signals.get("git_signals", {})

incidents = []
what_worked = []
missed_opportunity = ""

SV: Speed Over Verification

SV: Speed Over Verification

Signal: significant tool activity but no verification calls

Signal: significant tool activity but no verification calls

action_count = (tool_counts.get("Bash", 0) + tool_counts.get("Edit", 0) + tool_counts.get("Write", 0) + tool_counts.get("Agent", 0)) if action_count > 5 and len(verification_calls) == 0: incidents.append({ "code": "SV", "description": "Significant activity with no verification calls (curl/gh pr view)", "evidence": f"{action_count} action tool calls, {len(verification_calls)} verification calls" })
action_count = (tool_counts.get("Bash", 0) + tool_counts.get("Edit", 0) + tool_counts.get("Write", 0) + tool_counts.get("Agent", 0)) if action_count > 5 and len(verification_calls) == 0: incidents.append({ "code": "SV", "description": "Significant activity with no verification calls (curl/gh pr view)", "evidence": f"{action_count} action tool calls, {len(verification_calls)} verification calls" })

MB: Memory Without Behavioral Change

MB: Memory Without Behavioral Change

Signal: skills or rules were loaded but error patterns suggest they were ignored

Signal: skills or rules were loaded but error patterns suggest they were ignored

rules_read = any("rules.md" in b.lower() for b in bash_calls) or
any("rules" in p.lower() for p in write_paths) if rules_read and len(error_results) >= 2: incidents.append({ "code": "MB", "description": "RULES.md was read but error patterns suggest rules were deprioritized", "evidence": f"RULES.md accessed, {len(error_results)} error tool results in session" })
rules_read = any("rules.md" in b.lower() for b in bash_calls) or
any("rules" in p.lower() for p in write_paths) if rules_read and len(error_results) >= 2: incidents.append({ "code": "MB", "description": "RULES.md was read but error patterns suggest rules were deprioritized", "evidence": f"RULES.md accessed, {len(error_results)} error tool results in session" })

SF: Silent Failure Suppression

SF: Silent Failure Suppression

Signal: multiple errors encountered; no recovery bash calls follow

Signal: multiple errors encountered; no recovery bash calls follow

if len(error_results) >= 2: # Check if there are bash calls after the errors # This is a heuristic: if errors exist and no verification, flag it recovery_indicators = ["retry", "fix", "debug", "error", "fail"] recovery_bash = [b for b in bash_calls if any(r in b.lower() for r in recovery_indicators)] if len(recovery_bash) == 0: incidents.append({ "code": "SF", "description": "Multiple tool errors without visible recovery steps", "evidence": f"{len(error_results)} error results: {'; '.join(error_results[:2])[:200]}" })
if len(error_results) >= 2: # Check if there are bash calls after the errors # This is a heuristic: if errors exist and no verification, flag it recovery_indicators = ["retry", "fix", "debug", "error", "fail"] recovery_bash = [b for b in bash_calls if any(r in b.lower() for r in recovery_indicators)] if len(recovery_bash) == 0: incidents.append({ "code": "SF", "description": "Multiple tool errors without visible recovery steps", "evidence": f"{len(error_results)} error results: {'; '.join(error_results[:2])[:200]}" })

UA: User Model Absence

UA: User Model Absence

Signal: deploy/publish task with no HTTP 200 check

Signal: deploy/publish task with no HTTP 200 check

deploy_keywords = ["deploy", "publish", "vercel", "heroku", "production", "live", "release"] is_deploy_task = any(re.search(r'\b' + kw + r'\b', original_prompt) for kw in deploy_keywords) if is_deploy_task and "curl" not in bash_all: incidents.append({ "code": "UA", "description": "Deploy/publish task with no HTTP response verification", "evidence": "Task involves deploy; no curl call found in session" })
deploy_keywords = ["deploy", "publish", "vercel", "heroku", "production", "live", "release"] is_deploy_task = any(re.search(r'\b' + kw + r'\b', original_prompt) for kw in deploy_keywords) if is_deploy_task and "curl" not in bash_all: incidents.append({ "code": "UA", "description": "Deploy/publish task with no HTTP response verification", "evidence": "Task involves deploy; no curl call found in session" })

UB: Uncertainty Blindness

UB: Uncertainty Blindness

Signal: --force flags, --skip flags, or --no-verify in bash calls

Signal: --force flags, --skip flags, or --no-verify in bash calls

force_calls = [b for b in bash_calls if any(f in b for f in ["--force", "--no-verify", "--skip", "-f "])] if force_calls: incidents.append({ "code": "UB", "description": "Force flags or verification skips used without documented rationale", "evidence": f"{len(force_calls)} commands with force/skip flags: {force_calls[0][:150]}" })
force_calls = [b for b in bash_calls if any(f in b for f in ["--force", "--no-verify", "--skip", "-f "])] if force_calls: incidents.append({ "code": "UB", "description": "Force flags or verification skips used without documented rationale", "evidence": f"{len(force_calls)} commands with force/skip flags: {force_calls[0][:150]}" })

ID: Intent Drift

ID: Intent Drift

Signal: Write calls to paths outside expected scope (reports, src dirs unrelated to task)

Signal: Write calls to paths outside expected scope (reports, src dirs unrelated to task)

if write_paths: task_hint = original_prompt[:100] unexpected_writes = [p for p in write_paths if not any(kw in p.lower() for kw in ["reports/", "tmp/", ".claude/"])] if len(unexpected_writes) > 3: incidents.append({ "code": "ID", "description": "Write calls suggest scope creep beyond original task", "evidence": f"{len(unexpected_writes)} write calls outside expected paths: {', '.join(unexpected_writes[:3])}" })
if write_paths: task_hint = original_prompt[:100] unexpected_writes = [p for p in write_paths if not any(kw in p.lower() for kw in ["reports/", "tmp/", ".claude/"])] if len(unexpected_writes) > 3: incidents.append({ "code": "ID", "description": "Write calls suggest scope creep beyond original task", "evidence": f"{len(unexpected_writes)} write calls outside expected paths: {', '.join(unexpected_writes[:3])}" })

SG: Skill Gap

SG: Skill Gap

Signal: ENOENT, command not found, or domain-specific tool failures

Signal: ENOENT, command not found, or domain-specific tool failures

gap_patterns = ["enoent", "command not found", "no such file", "not installed", "permission denied"] gap_errors = [e for e in error_results if any(p in e.lower() for p in gap_patterns)] if gap_errors: incidents.append({ "code": "SG", "description": "Tool or command failures suggesting missing capability or setup", "evidence": f"{len(gap_errors)} errors: {gap_errors[0][:200]}" })
gap_patterns = ["enoent", "command not found", "no such file", "not installed", "permission denied"] gap_errors = [e for e in error_results if any(p in e.lower() for p in gap_patterns)] if gap_errors: incidents.append({ "code": "SG", "description": "Tool or command failures suggesting missing capability or setup", "evidence": f"{len(gap_errors)} errors: {gap_errors[0][:200]}" })

CF: Coordination Failure

CF: Coordination Failure

Signal: Agent tool called 3+ times (worker retries)

Signal: Agent tool called 3+ times (worker retries)

if workers_spawned >= 3: incidents.append({ "code": "CF", "description": f"Agent spawned {workers_spawned} times — suggests worker retry loops or coordination issues", "evidence": f"{workers_spawned} Agent tool calls in session" })
if workers_spawned >= 3: incidents.append({ "code": "CF", "description": f"Agent spawned {workers_spawned} times — suggests worker retry loops or coordination issues", "evidence": f"{workers_spawned} Agent tool calls in session" })

Git workflow classification rules

Git workflow classification rules

has_git_commit = any("git commit" in b.lower() for b in bash_calls)
has_git_commit = any("git commit" in b.lower() for b in bash_calls)

SV: committed to main without PR — skipped review entirely

SV: committed to main without PR — skipped review entirely

if git_signals.get("committed_to_main") and not git_signals.get("pr_created"): incidents.append({ "code": "SV", "description": "Committed directly to main without creating a feature branch or PR", "evidence": "git commit detected, no branch created, no gh pr create in session" })
if git_signals.get("committed_to_main") and not git_signals.get("pr_created"): incidents.append({ "code": "SV", "description": "Committed directly to main without creating a feature branch or PR", "evidence": "git commit detected, no branch created, no gh pr create in session" })

SF: committed but never pushed — work trapped locally

SF: committed but never pushed — work trapped locally

if has_git_commit and not git_signals.get("pushed"): incidents.append({ "code": "SF", "description": "Committed but never pushed — work never reached the remote", "evidence": "git commit found in session but no git push detected" })
if has_git_commit and not git_signals.get("pushed"): incidents.append({ "code": "SF", "description": "Committed but never pushed — work never reached the remote", "evidence": "git commit found in session but no git push detected" })

UA: closed issue without verifying the push landed on remote

UA: closed issue without verifying the push landed on remote

if git_signals.get("issue_closed") and not git_signals.get("push_verified"): incidents.append({ "code": "UA", "description": "Issue closed without verifying commit exists on remote", "evidence": "gh issue close detected but no git log --remotes or gh api commit check found" })
if git_signals.get("issue_closed") and not git_signals.get("push_verified"): incidents.append({ "code": "UA", "description": "Issue closed without verifying commit exists on remote", "evidence": "gh issue close detected but no git log --remotes or gh api commit check found" })

UB: force-pushed without documented rationale (git push --force)

UB: force-pushed without documented rationale (git push --force)

if git_signals.get("push_force"): incidents.append({ "code": "UB", "description": "Force-pushed without documented rationale", "evidence": "git push --force detected in session" })
if git_signals.get("push_force"): incidents.append({ "code": "UB", "description": "Force-pushed without documented rationale", "evidence": "git push --force detected in session" })

Determine what worked (heuristic from tool activity)

Determine what worked (heuristic from tool activity)

if len(verification_calls) > 0: what_worked.append(f"Verification steps executed ({len(verification_calls)} calls)") if skills_loaded: what_worked.append(f"Skills loaded: {', '.join(skills_loaded)}") if tool_counts.get("Agent", 0) > 0 and workers_spawned < 3: what_worked.append(f"Workers deployed efficiently ({workers_spawned} spawned)") if not incidents: what_worked.append("No failure patterns detected")
if len(verification_calls) > 0: what_worked.append(f"Verification steps executed ({len(verification_calls)} calls)") if skills_loaded: what_worked.append(f"Skills loaded: {', '.join(skills_loaded)}") if tool_counts.get("Agent", 0) > 0 and workers_spawned < 3: what_worked.append(f"Workers deployed efficiently ({workers_spawned} spawned)") if not incidents: what_worked.append("No failure patterns detected")

Missed opportunity hint

Missed opportunity hint

incident_codes_found = [i["code"] for i in incidents] if "SF" in incident_codes_found and not git_signals.get("pushed"): missed_opportunity = "Committed but never pushed — add git push after every commit, then verify with git log --remotes" elif "SV" in incident_codes_found and git_signals.get("committed_to_main"): missed_opportunity = "Create a feature branch before committing — git checkout -b feat/... then open a PR" elif "UA" in incident_codes_found and git_signals.get("issue_closed"): missed_opportunity = "Verify the commit is on the remote before closing the issue — gh api repos/{owner}/{repo}/commits/{sha}" elif "SV" in incident_codes_found: missed_opportunity = "Add verification step before exit — curl or gh pr view would confirm success" elif "SG" in incident_codes_found: missed_opportunity = "Load the relevant skill before attempting the domain operation" elif "UA" in incident_codes_found: missed_opportunity = "curl production URL after deploy — HTTP 200 is the only real success signal"
result = { "incidents": incidents, "what_worked": what_worked, "missed_opportunity": missed_opportunity } print(json.dumps(result)) PYEOF
echo "$REPORT" > /tmp/distill_report_${job_id}.txt CLASSIFIED=$(python3 /tmp/distill_classify.py "$SIGNALS" "/tmp/distill_report_${job_id}.txt")
undefined
incident_codes_found = [i["code"] for i in incidents] if "SF" in incident_codes_found and not git_signals.get("pushed"): missed_opportunity = "Committed but never pushed — add git push after every commit, then verify with git log --remotes" elif "SV" in incident_codes_found and git_signals.get("committed_to_main"): missed_opportunity = "Create a feature branch before committing — git checkout -b feat/... then open a PR" elif "UA" in incident_codes_found and git_signals.get("issue_closed"): missed_opportunity = "Verify the commit is on the remote before closing the issue — gh api repos/{owner}/{repo}/commits/{sha}" elif "SV" in incident_codes_found: missed_opportunity = "Add verification step before exit — curl or gh pr view would confirm success" elif "SG" in incident_codes_found: missed_opportunity = "Load the relevant skill before attempting the domain operation" elif "UA" in incident_codes_found: missed_opportunity = "curl production URL after deploy — HTTP 200 is the only real success signal"
result = { "incidents": incidents, "what_worked": what_worked, "missed_opportunity": missed_opportunity } print(json.dumps(result)) PYEOF
echo "$REPORT" > /tmp/distill_report_${job_id}.txt CLASSIFIED=$(python3 /tmp/distill_classify.py "$SIGNALS" "/tmp/distill_report_${job_id}.txt")
undefined

Step 6: Extract rules compliance

步骤6:提取规则合规性

bash
HAS_REPORT="True"
[ -z "$REPORT" ] && HAS_REPORT="False"

COMPLIANCE=$(python3 << PYEOF
import json, sys

signals = json.loads('''$SIGNALS''')
task = "$TASK".lower()
tool_counts = signals.get("tool_counts", {})
bash_calls = signals.get("bash_calls", [])
write_paths = signals.get("write_paths", [])
skills_loaded = signals.get("skills_loaded", [])
verification_calls = signals.get("verification_calls", [])
git_signals = signals.get("git_signals", {})

is_feature = any(kw in task for kw in
                 ["implement", "feature", "add", "build", "create", "fix", "update"])

compliance = {
    "R1_headless": "AskUserQuestion" not in tool_counts,
    "R3_no_code": ("Edit" not in tool_counts and
                   not any("src/" in p or "app/" in p for p in write_paths)),
    "R4_read_skills": len(skills_loaded) > 0,
    "R6_verify": len(verification_calls) > 0,
    "R7_report": $HAS_REPORT,  # True if mission API returned a non-empty report field
    "R_branch": git_signals.get("branch_created", False),
    "R_push": git_signals.get("pushed", False),
    "R_pr": git_signals.get("pr_created", False) or not is_feature
}
print(json.dumps(compliance))
PYEOF
)
bash
HAS_REPORT="True"
[ -z "$REPORT" ] && HAS_REPORT="False"

COMPLIANCE=$(python3 << PYEOF
import json, sys

signals = json.loads('''$SIGNALS''')
task = "$TASK".lower()
tool_counts = signals.get("tool_counts", {})
bash_calls = signals.get("bash_calls", [])
write_paths = signals.get("write_paths", [])
skills_loaded = signals.get("skills_loaded", [])
verification_calls = signals.get("verification_calls", [])
git_signals = signals.get("git_signals", {})

is_feature = any(kw in task for kw in
                 ["implement", "feature", "add", "build", "create", "fix", "update"])

compliance = {
    "R1_headless": "AskUserQuestion" not in tool_counts,
    "R3_no_code": ("Edit" not in tool_counts and
                   not any("src/" in p or "app/" in p for p in write_paths)),
    "R4_read_skills": len(skills_loaded) > 0,
    "R6_verify": len(verification_calls) > 0,
    "R7_report": $HAS_REPORT,  # True if mission API returned a non-empty report field
    "R_branch": git_signals.get("branch_created", False),
    "R_push": git_signals.get("pushed", False),
    "R_pr": git_signals.get("pr_created", False) or not is_feature
}
print(json.dumps(compliance))
PYEOF
)

Step 7: Build and POST audit JSON

步骤7:构建并POST审计JSON

Assemble the audit JSON and POST it to the Pylot API:
bash
AUDIT_JSON=$(python3 << PYEOF
import json
from datetime import datetime

signals   = json.loads('''$SIGNALS''')
classified = json.loads('''$CLASSIFIED''')
compliance = json.loads('''$COMPLIANCE''')
timing    = json.loads('''$TIMING_METRICS''')

started_at  = "$STARTED_AT"
finished_at = "$FINISHED_AT"

def parse_ts(ts):
    if not ts:
        return None
    try:
        return datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
    except Exception:
        return None

duration_minutes = 0
s, e = parse_ts(started_at), parse_ts(finished_at)
if s and e:
    duration_minutes = round((e - s).total_seconds() / 60)

input_tok  = signals.get("total_input_tokens", 0)
output_tok = signals.get("total_output_tokens", 0)
cost_usd   = round((input_tok / 1_000_000 * 3.0) + (output_tok / 1_000_000 * 15.0), 2)

audit = {
    "job_id":            "$job_id",
    "team":              "$TEAM",
    "agent":             "$AGENT",
    "task_summary":      "$TASK",
    "status":            "$STATUS",
    "exit_code":         "$EXIT_CODE",
    "started_at":        started_at,
    "finished_at":       finished_at,
    "duration_minutes":  duration_minutes,
    "cost_usd":          cost_usd,
    "token_usage":       {"input": input_tok, "output": output_tok},
    "timing_metrics":    timing,
    "workers_spawned":   signals.get("workers_spawned", 0),
    "skills_loaded":     signals.get("skills_loaded", []),
    "session_complete":  signals.get("session_complete", True),
    "rules_compliance":  compliance,
    "incidents":         classified.get("incidents", []),
    "what_worked":       classified.get("what_worked", []),
    "missed_opportunity": classified.get("missed_opportunity", "")
}
print(json.dumps(audit, indent=2))
PYEOF
)
组装审计JSON并提交至Pylot API:
bash
AUDIT_JSON=$(python3 << PYEOF
import json
from datetime import datetime

signals   = json.loads('''$SIGNALS''')
classified = json.loads('''$CLASSIFIED''')
compliance = json.loads('''$COMPLIANCE''')
timing    = json.loads('''$TIMING_METRICS''')

started_at  = "$STARTED_AT"
finished_at = "$FINISHED_AT"

def parse_ts(ts):
    if not ts:
        return None
    try:
        return datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
    except Exception:
        return None

duration_minutes = 0
s, e = parse_ts(started_at), parse_ts(finished_at)
if s and e:
    duration_minutes = round((e - s).total_seconds() / 60)

input_tok  = signals.get("total_input_tokens", 0)
output_tok = signals.get("total_output_tokens", 0)
cost_usd   = round((input_tok / 1_000_000 * 3.0) + (output_tok / 1_000_000 * 15.0), 2)

audit = {
    "job_id":            "$job_id",
    "team":              "$TEAM",
    "agent":             "$AGENT",
    "task_summary":      "$TASK",
    "status":            "$STATUS",
    "exit_code":         "$EXIT_CODE",
    "started_at":        started_at,
    "finished_at":       finished_at,
    "duration_minutes":  duration_minutes,
    "cost_usd":          cost_usd,
    "token_usage":       {"input": input_tok, "output": output_tok},
    "timing_metrics":    timing,
    "workers_spawned":   signals.get("workers_spawned", 0),
    "skills_loaded":     signals.get("skills_loaded", []),
    "session_complete":  signals.get("session_complete", True),
    "rules_compliance":  compliance,
    "incidents":         classified.get("incidents", []),
    "what_worked":       classified.get("what_worked", []),
    "missed_opportunity": classified.get("missed_opportunity", "")
}
print(json.dumps(audit, indent=2))
PYEOF
)

POST audit to Pylot API

POST audit to Pylot API

HTTP_STATUS=$(curl -sf -w "%{http_code}" -o /tmp/distill_audit_response.txt
-X POST
-H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN"
-H "Content-Type: application/json"
-d "$AUDIT_JSON"
"https://hooks.fellowship.dev/missions/${job_id}/audit")
if [ "$HTTP_STATUS" = "200" ] || [ "$HTTP_STATUS" = "201" ]; then echo "Audit posted: $HTTP_STATUS" elif [ "$HTTP_STATUS" = "409" ]; then echo "Audit already exists for $job_id — skipping (idempotent)" else echo "ERROR: Audit POST failed with HTTP $HTTP_STATUS" cat /tmp/distill_audit_response.txt exit 1 fi

---
HTTP_STATUS=$(curl -sf -w "%{http_code}" -o /tmp/distill_audit_response.txt
-X POST
-H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN"
-H "Content-Type: application/json"
-d "$AUDIT_JSON"
"https://hooks.fellowship.dev/missions/${job_id}/audit")
if [ "$HTTP_STATUS" = "200" ] || [ "$HTTP_STATUS" = "201" ]; then echo "Audit posted: $HTTP_STATUS" elif [ "$HTTP_STATUS" = "409" ]; then echo "Audit already exists for $job_id — skipping (idempotent)" else echo "ERROR: Audit POST failed with HTTP $HTTP_STATUS" cat /tmp/distill_audit_response.txt exit 1 fi

---

analyze Mode

analyze模式

Invocation

调用方式

bash
/distill analyze
bash
/distill analyze

Environment

环境变量

bash
undefined
bash
undefined

Required

必填

PYLOT_DISPATCH_TOKEN # Bearer token for hooks.fellowship.dev
PYLOT_DISPATCH_TOKEN # hooks.fellowship.dev的Bearer令牌

Optional

可选

ISSUE_REPO # Target repo for GitHub issues (default: fellowship-dev/commander)

All API calls include the header:
Authorization: Bearer $PYLOT_DISPATCH_TOKEN
undefined
ISSUE_REPO # GitHub issue的目标仓库(默认:fellowship-dev/commander)

所有API请求都携带以下请求头:
Authorization: Bearer $PYLOT_DISPATCH_TOKEN
undefined

Step 1: Fetch audit data

步骤1:获取审计数据

Fetch audit records and aggregated stats from
hooks.fellowship.dev
for the past 7 days, plus the prior 7-day period for trend comparison:
bash
AUTH_HEADER="Authorization: Bearer $PYLOT_DISPATCH_TOKEN"

AUDITS=$(curl -sf \
  -H "$AUTH_HEADER" \
  "https://hooks.fellowship.dev/audits?days=7")

if [ $? -ne 0 ] || [ -z "$AUDITS" ]; then
  echo "ERROR: Failed to fetch audits from hooks.fellowship.dev"
  exit 1
fi

STATS=$(curl -sf \
  -H "$AUTH_HEADER" \
  "https://hooks.fellowship.dev/audits/stats?days=7")

if [ $? -ne 0 ] || [ -z "$STATS" ]; then
  echo "ERROR: Failed to fetch stats from hooks.fellowship.dev"
  exit 1
fi
hooks.fellowship.dev
获取过去7天的审计记录和聚合统计数据,同时获取前7天的数据用于趋势对比:
bash
AUTH_HEADER="Authorization: Bearer $PYLOT_DISPATCH_TOKEN"

AUDITS=$(curl -sf \
  -H "$AUTH_HEADER" \
  "https://hooks.fellowship.dev/audits?days=7")

if [ $? -ne 0 ] || [ -z "$AUDITS" ]; then
  echo "ERROR: Failed to fetch audits from hooks.fellowship.dev"
  exit 1
fi

STATS=$(curl -sf \
  -H "$AUTH_HEADER" \
  "https://hooks.fellowship.dev/audits/stats?days=7")

if [ $? -ne 0 ] || [ -z "$STATS" ]; then
  echo "ERROR: Failed to fetch stats from hooks.fellowship.dev"
  exit 1
fi

Prior period (days 8–14) for trend comparison — fallback to {} if endpoint unsupported

前一周期(第8-14天)用于趋势对比 — 如果端点不支持则返回{}

PRIOR_STATS=$(curl -sf
-H "$AUTH_HEADER"
"https://hooks.fellowship.dev/audits/stats?days=14&offset=7" || echo "{}")
echo "Audit data fetched."
undefined
PRIOR_STATS=$(curl -sf
-H "$AUTH_HEADER"
"https://hooks.fellowship.dev/audits/stats?days=14&offset=7" || echo "{}")
echo "Audit data fetched."
undefined

Step 2: Aggregate and identify recurring patterns

步骤2:聚合并识别重复模式

Group records by skill name, team, and incident code. Flag any combination with 3 or more occurrences in the 7-day window as a recurring pattern.
bash
cat > /tmp/distill_aggregate.py << 'PYEOF'
import json, sys, collections

audits = json.loads(sys.argv[1])
stats  = json.loads(sys.argv[2])

if isinstance(audits, dict):
    audits = audits.get("records") or audits.get("audits") or []

pattern_counts  = collections.Counter()
pattern_records = collections.defaultdict(list)

for audit in audits:
    skill = audit.get("skill_name") or audit.get("skill") or "unknown"
    team  = audit.get("team", "unknown")
    for inc in audit.get("incidents", []):
        code = inc.get("code", "??")
        key  = (skill, team, code)
        pattern_counts[key] += 1
        pattern_records[key].append({
            "id":          audit.get("job_id") or audit.get("id"),
            "description": inc.get("description", ""),
            "evidence":    inc.get("evidence", "")
        })

recurring = [
    {
        "skill":   k[0],
        "team":    k[1],
        "code":    k[2],
        "count":   pattern_counts[k],
        "records": pattern_records[k]
    }
    for k, cnt in pattern_counts.items()
    if cnt >= 3
]

failure_rates = {
    entry.get("code"): entry.get("rate") or entry.get("failure_rate")
    for entry in (stats.get("by_code") or stats.get("failure_rates") or [])
}

print(json.dumps({
    "total_audits":       len(audits),
    "recurring_patterns": recurring,
    "failure_rates":      failure_rates
}, indent=2))
PYEOF

AGGREGATE=$(python3 /tmp/distill_aggregate.py "$AUDITS" "$STATS")
PATTERN_COUNT=$(python3 -c "import json,sys; print(len(json.load(sys.stdin)['recurring_patterns']))" <<< "$AGGREGATE")
echo "Aggregation complete. ${PATTERN_COUNT} recurring patterns identified."
按skill名称、团队和事件代码对记录进行分组。将7天内出现3次及以上的组合标记为重复模式。
bash
cat > /tmp/distill_aggregate.py << 'PYEOF'
import json, sys, collections

audits = json.loads(sys.argv[1])
stats  = json.loads(sys.argv[2])

if isinstance(audits, dict):
    audits = audits.get("records") or audits.get("audits") or []

pattern_counts  = collections.Counter()
pattern_records = collections.defaultdict(list)

for audit in audits:
    skill = audit.get("skill_name") or audit.get("skill") or "unknown"
    team  = audit.get("team", "unknown")
    for inc in audit.get("incidents", []):
        code = inc.get("code", "??")
        key  = (skill, team, code)
        pattern_counts[key] += 1
        pattern_records[key].append({
            "id":          audit.get("job_id") or audit.get("id"),
            "description": inc.get("description", ""),
            "evidence":    inc.get("evidence", "")
        })

recurring = [
    {
        "skill":   k[0],
        "team":    k[1],
        "code":    k[2],
        "count":   pattern_counts[k],
        "records": pattern_records[k]
    }
    for k, cnt in pattern_counts.items()
    if cnt >= 3
]

failure_rates = {
    entry.get("code"): entry.get("rate") or entry.get("failure_rate")
    for entry in (stats.get("by_code") or stats.get("failure_rates") or [])
}

print(json.dumps({
    "total_audits":       len(audits),
    "recurring_patterns": recurring,
    "failure_rates":      failure_rates
}, indent=2))
PYEOF

AGGREGATE=$(python3 /tmp/distill_aggregate.py "$AUDITS" "$STATS")
PATTERN_COUNT=$(python3 -c "import json,sys; print(len(json.load(sys.stdin)['recurring_patterns']))" <<< "$AGGREGATE")
echo "Aggregation complete. ${PATTERN_COUNT} recurring patterns identified."

Step 3: GitHub issue management

步骤3:GitHub issue管理

For each recurring pattern, search for an open issue whose title contains the skill name and incident code. Comment with updated counts and exemplar IDs if found; create a new issue if not.
bash
echo "$AGGREGATE" > /tmp/distill_agg.json
ISSUE_REPO="${ISSUE_REPO:-fellowship-dev/commander}"
python3 << 'PYEOF'
import json, os, subprocess

with open("/tmp/distill_agg.json") as _f:
    agg = json.load(_f)
issue_repo = os.environ.get("ISSUE_REPO", "fellowship-dev/commander")

for pattern in agg.get("recurring_patterns", []):
    skill   = pattern["skill"]
    code    = pattern["code"]
    count   = pattern["count"]
    team    = pattern["team"]
    records = pattern["records"]

    title_prefix = f"[distill] {skill}: recurring {code}"
    title_full   = f"{title_prefix} ({count} occurrences in 7 days)"

    search = subprocess.run(
        ["gh", "issue", "list",
         "--repo", issue_repo,
         "--search", f"{title_prefix} in:title is:open",
         "--json", "number,title", "--limit", "5"],
        capture_output=True, text=True
    )
    existing_issues = json.loads(search.stdout or "[]")
    match = next(
        (i for i in existing_issues if skill in i["title"] and code in i["title"]),
        None
    )

    mission_ids = [r["id"] for r in records if r.get("id")]
    ids_str     = ", ".join(str(m) for m in mission_ids[:5])
    rate        = agg["failure_rates"].get(code)
    rate_str    = f"{rate}%" if isinstance(rate, (int, float)) else str(rate or "N/A")

    if match:
        comment = (
            f"**Updated count**: {count} occurrences in the last 7 days\n\n"
            f"**New exemplar mission IDs**: {ids_str}\n\n"
            f"**Failure rate (7-day)**: {rate_str}"
        )
        result = subprocess.run(
            ["gh", "issue", "comment", str(match["number"]), "--repo", issue_repo, "--body", comment],
            capture_output=True, text=True
        )
        status = "Commented on" if result.returncode == 0 else "WARN: failed to comment on"
        print(f"{status} issue #{match['number']}: {title_prefix}")
    else:
        evidence_lines = "\n".join(
            f"- {r['id']}: {r.get('description', '')[:120]}"
            for r in records[:10]
            if r.get("id")
        )
        body = f"""## Recurring Failure Pattern

**Skill**: \`{skill}\`
**Incident code**: \`{code}\`
**Team**: {team}
**Occurrences**: {count} in the last 7 days
**Failure rate (7-day)**: {rate_str}
针对每个重复模式,搜索标题包含skill名称和事件代码的开放issue。如果找到,则更新计数并添加典型案例ID的评论;如果未找到,则创建新issue。
bash
echo "$AGGREGATE" > /tmp/distill_agg.json
ISSUE_REPO="${ISSUE_REPO:-fellowship-dev/commander}"
python3 << 'PYEOF'
import json, os, subprocess

with open("/tmp/distill_agg.json") as _f:
    agg = json.load(_f)
issue_repo = os.environ.get("ISSUE_REPO", "fellowship-dev/commander")

for pattern in agg.get("recurring_patterns", []):
    skill   = pattern["skill"]
    code    = pattern["code"]
    count   = pattern["count"]
    team    = pattern["team"]
    records = pattern["records"]

    title_prefix = f"[distill] {skill}: recurring {code}"
    title_full   = f"{title_prefix} ({count} occurrences in 7 days)"

    search = subprocess.run(
        ["gh", "issue", "list",
         "--repo", issue_repo,
         "--search", f"{title_prefix} in:title is:open",
         "--json", "number,title", "--limit", "5"],
        capture_output=True, text=True
    )
    existing_issues = json.loads(search.stdout or "[]")
    match = next(
        (i for i in existing_issues if skill in i["title"] and code in i["title"]),
        None
    )

    mission_ids = [r["id"] for r in records if r.get("id")]
    ids_str     = ", ".join(str(m) for m in mission_ids[:5])
    rate        = agg["failure_rates"].get(code)
    rate_str    = f"{rate}%" if isinstance(rate, (int, float)) else str(rate or "N/A")

    if match:
        comment = (
            f"**Updated count**: {count} occurrences in the last 7 days\n\n"
            f"**New exemplar mission IDs**: {ids_str}\n\n"
            f"**Failure rate (7-day)**: {rate_str}"
        )
        result = subprocess.run(
            ["gh", "issue", "comment", str(match["number"]), "--repo", issue_repo, "--body", comment],
            capture_output=True, text=True
        )
        status = "Commented on" if result.returncode == 0 else "WARN: failed to comment on"
        print(f"{status} issue #{match['number']}: {title_prefix}")
    else:
        evidence_lines = "\n".join(
            f"- {r['id']}: {r.get('description', '')[:120]}"
            for r in records[:10]
            if r.get("id")
        )
        body = f"""## Recurring Failure Pattern

**Skill**: \`{skill}\`
**Incident code**: \`{code}\`
**Team**: {team}
**Occurrences**: {count} in the last 7 days
**Failure rate (7-day)**: {rate_str}

Exemplar Mission IDs

Exemplar Mission IDs

{evidence_lines}

Generated by /distill analyze — fellowship-dev/dogfooded-skills """ result = subprocess.run( ["gh", "issue", "create", "--repo", issue_repo, "--title", title_full, "--body", body], capture_output=True, text=True ) if result.returncode == 0: print(f"Created issue: {result.stdout.strip()}") else: print(f"WARN: Failed to create issue for {skill}/{code}: {result.stderr[:200]}") PYEOF
undefined
{evidence_lines}

Generated by /distill analyze — fellowship-dev/dogfooded-skills """ result = subprocess.run( ["gh", "issue", "create", "--repo", issue_repo, "--title", title_full, "--body", body], capture_output=True, text=True ) if result.returncode == 0: print(f"Created issue: {result.stdout.strip()}") else: print(f"WARN: Failed to create issue for {skill}/{code}: {result.stderr[:200]}") PYEOF
undefined

Step 4: Promote exemplar

步骤4:推广典型案例

For each recurring pattern, identify the single most instructive failure — the record with the richest evidence field — and promote it via
PATCH /audits/{id}/exemplar
.
bash
echo "$AGGREGATE" > /tmp/distill_agg.json
python3 << 'PYEOF'
import json, os, urllib.request, urllib.error

token = os.environ.get("PYLOT_DISPATCH_TOKEN", "")
with open("/tmp/distill_agg.json") as _f:
    agg = json.load(_f)

for pattern in agg.get("recurring_patterns", []):
    records = pattern.get("records", [])
    if not records:
        continue

    exemplar = max(
        records,
        key=lambda r: len(r.get("evidence", "") or r.get("description", ""))
    )
    eid = exemplar.get("id")
    if not eid:
        print(f"WARN: No ID for exemplar in {pattern['skill']}/{pattern['code']}, skipping")
        continue

    req = urllib.request.Request(
        f"https://hooks.fellowship.dev/audits/{eid}/exemplar",
        method="PATCH",
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type":  "application/json"
        },
        data=b"{}"
    )
    try:
        with urllib.request.urlopen(req) as resp:
            print(f"Promoted exemplar {eid} for {pattern['skill']}/{pattern['code']} — HTTP {resp.status}")
    except urllib.error.HTTPError as e:
        print(f"WARN: PATCH /audits/{eid}/exemplar → HTTP {e.code}")
    except Exception as e:
        print(f"WARN: PATCH /audits/{eid}/exemplar → {e}")
PYEOF
针对每个重复模式,识别最具指导性的失败案例 — 即证据字段最丰富的记录,并通过
PATCH /audits/{id}/exemplar
将其标记为典型案例。
bash
echo "$AGGREGATE" > /tmp/distill_agg.json
python3 << 'PYEOF'
import json, os, urllib.request, urllib.error

token = os.environ.get("PYLOT_DISPATCH_TOKEN", "")
with open("/tmp/distill_agg.json") as _f:
    agg = json.load(_f)

for pattern in agg.get("recurring_patterns", []):
    records = pattern.get("records", [])
    if not records:
        continue

    exemplar = max(
        records,
        key=lambda r: len(r.get("evidence", "") or r.get("description", ""))
    )
    eid = exemplar.get("id")
    if not eid:
        print(f"WARN: No ID for exemplar in {pattern['skill']}/{pattern['code']}, skipping")
        continue

    req = urllib.request.Request(
        f"https://hooks.fellowship.dev/audits/{eid}/exemplar",
        method="PATCH",
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type":  "application/json"
        },
        data=b"{}"
    )
    try:
        with urllib.request.urlopen(req) as resp:
            print(f"Promoted exemplar {eid} for {pattern['skill']}/{pattern['code']} — HTTP {resp.status}")
    except urllib.error.HTTPError as e:
        print(f"WARN: PATCH /audits/{eid}/exemplar → HTTP {e.code}")
    except Exception as e:
        print(f"WARN: PATCH /audits/{eid}/exemplar → {e}")
PYEOF

Step 5: Trend comparison

步骤5:趋势对比

Compare incident counts from the current 7-day window against the prior 7-day period. Report
improving
(fewer occurrences),
stable
, or
worsening
(more occurrences) per incident code.
bash
echo "$STATS" > /tmp/distill_stats.json
echo "$PRIOR_STATS" > /tmp/distill_prior_stats.json
python3 << 'PYEOF'
import json

with open("/tmp/distill_stats.json") as _f:
    stats = json.load(_f)
with open("/tmp/distill_prior_stats.json") as _f:
    prior_stats = json.load(_f)

def extract_counts(s):
    return {
        e.get("code"): e.get("count", 0)
        for e in (s.get("by_code") or s.get("failure_rates") or [])
    }

current = extract_counts(stats)
prior   = extract_counts(prior_stats)
codes   = sorted(set(current) | set(prior))

print("## Trend Report (current 7 days vs prior 7 days)\n")
print(f"{'Code':<6} {'Current':>9} {'Prior':>9}  Trend")
print("-" * 38)
for code in codes:
    c = current.get(code, 0)
    p = prior.get(code, 0)
    if   c < p: trend = "improving"
    elif c > p: trend = "worsening"
    else:       trend = "stable"
    print(f"{code:<6} {c:>9} {p:>9}  {trend}")
PYEOF

对比当前7天与前7天的事件数量,针对每个事件代码报告
improving(改善)
(出现次数减少)、
stable(稳定)
worsening(恶化)
(出现次数增加)。
bash
echo "$STATS" > /tmp/distill_stats.json
echo "$PRIOR_STATS" > /tmp/distill_prior_stats.json
python3 << 'PYEOF'
import json

with open("/tmp/distill_stats.json") as _f:
    stats = json.load(_f)
with open("/tmp/distill_prior_stats.json") as _f:
    prior_stats = json.load(_f)

def extract_counts(s):
    return {
        e.get("code"): e.get("count", 0)
        for e in (s.get("by_code") or s.get("failure_rates") or [])
    }

current = extract_counts(stats)
prior   = extract_counts(prior_stats)
codes   = sorted(set(current) | set(prior))

print("## Trend Report (current 7 days vs prior 7 days)\n")
print(f"{'Code':<6} {'Current':>9} {'Prior':>9}  Trend")
print("-" * 38)
for code in codes:
    c = current.get(code, 0)
    p = prior.get(code, 0)
    if   c < p: trend = "improving"
    elif c > p: trend = "worsening"
    else:       trend = "stable"
    print(f"{code:<6} {c:>9} {p:>9}  {trend}")
PYEOF

Error Handling

错误处理

gh
auth failure in analyze
— check
GH_TOKEN
export. The skill sources it from
claude-buddy/.env
. Provide token explicitly if env differs.
python3 not available — all parsing uses stdlib only (json, sys, collections, pathlib). No pip install needed.
Duplicate analyze issues — the issue creation step checks for existing titles before creating. Run analyze weekly; do not re-run on the same day unless audit files changed.

analyze模式中gh认证失败 — 检查
GH_TOKEN
是否已导出。该skill从
claude-buddy/.env
读取令牌,如果环境不同请显式提供令牌。
python3不可用 — 所有解析仅使用标准库(json、sys、collections、pathlib),无需pip安装依赖。
重复的analyze issue — issue创建步骤会在创建前检查是否存在相同标题。每周运行一次analyze;除非审计文件发生变化,否则不要在同一天重复运行。

Crew YAML Integration

Crew YAML集成

Declare distill in your
crew.yml
to get it via
npx skills
and schedule weekly analysis:
yaml
skills:
  - repo: fellowship-dev/dogfooded-skills@latest
    pick: [distill]

cron:
  - schedule: "0 12 * * 1"   # weekly Monday noon
    command: "/distill analyze"
Capture is dispatched by the poller after every mission — not declared in YAML.

crew.yml
中声明distill,即可通过
npx skills
获取并安排每周分析任务:
yaml
skills:
  - repo: fellowship-dev/dogfooded-skills@latest
    pick: [distill]

cron:
  - schedule: "0 12 * * 1"   # 每周一中午12点
    command: "/distill analyze"
capture模式由轮询器在每次任务完成后自动触发 — 无需在YAML中声明。

Audit JSON Schema Reference

审计JSON Schema参考

json
{
  "job_id": "2026-04-12-speckit-fellowship-dev-myrepo-branch",
  "date": "2026-04-12",
  "crew": "tooling",
  "task_summary": "Implement distill skill for post-mission auditing",
  "outcome": "done",
  "session_complete": true,
  "duration_minutes": 47,
  "cost_usd": 3.21,
  "token_usage": { "input": 850000, "output": 45000 },
  "workers_spawned": 2,
  "skills_loaded": ["project-registry", "speckit-runner"],
  "rules_compliance": {
    "R1_headless": true,
    "R3_no_code": true,
    "R4_read_skills": true,
    "R6_verify": true,
    "R7_report": true
  },
  "incidents": [
    {
      "code": "SV",
      "description": "Deploy actions with no HTTP verification",
      "evidence": "17 Bash calls, 0 curl calls"
    }
  ],
  "what_worked": ["Skills loaded on boot", "Worker deployed efficiently"],
  "missed_opportunity": "curl production URL before exit"
}

json
{
  "job_id": "2026-04-12-speckit-fellowship-dev-myrepo-branch",
  "date": "2026-04-12",
  "crew": "tooling",
  "task_summary": "Implement distill skill for post-mission auditing",
  "outcome": "done",
  "session_complete": true,
  "duration_minutes": 47,
  "cost_usd": 3.21,
  "token_usage": { "input": 850000, "output": 45000 },
  "workers_spawned": 2,
  "skills_loaded": ["project-registry", "speckit-runner"],
  "rules_compliance": {
    "R1_headless": true,
    "R3_no_code": true,
    "R4_read_skills": true,
    "R6_verify": true,
    "R7_report": true
  },
  "incidents": [
    {
      "code": "SV",
      "description": "Deploy actions with no HTTP verification",
      "evidence": "17 Bash calls, 0 curl calls"
    }
  ],
  "what_worked": ["Skills loaded on boot", "Worker deployed efficiently"],
  "missed_opportunity": "curl production URL before exit"
}

Critical Rules

关键规则

  • Idempotent — POST /missions/{job_id}/audit returns 409 if already captured; exit 0 on 409
  • One issue per pattern — deduplicate by
    [distill]
    title prefix before creating GitHub issues
  • ??
    codes are growth signals
    — track frequency; 3+
    ??
    in analyze means the taxonomy needs a new code
  • Labels must exist before
    gh issue create
    — the skill creates them if missing
  • 幂等性 — POST /missions/{job_id}/audit如果已存在则返回409;收到409时退出码为0
  • 每个模式对应一个issue — 创建GitHub issue前,通过
    [distill]
    标题前缀进行去重
  • ??
    代码是分类法演进信号
    — 跟踪其出现频率;analyze模式中出现3次及以上
    ??
    意味着分类法需要新增代码
  • 标签必须存在 — 在执行
    gh issue create
    前,skill会自动创建缺失的标签