flonat-research 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/domain-reviewer.md +336 -0
- package/.claude/agents/fixer.md +226 -0
- package/.claude/agents/paper-critic.md +370 -0
- package/.claude/agents/peer-reviewer.md +289 -0
- package/.claude/agents/proposal-reviewer.md +215 -0
- package/.claude/agents/referee2-reviewer.md +367 -0
- package/.claude/agents/references/journal-referee-profiles.md +354 -0
- package/.claude/agents/references/paper-critic/council-personas.md +77 -0
- package/.claude/agents/references/paper-critic/council-prompts.md +198 -0
- package/.claude/agents/references/peer-reviewer/report-template.md +199 -0
- package/.claude/agents/references/peer-reviewer/sa-prompts.md +260 -0
- package/.claude/agents/references/peer-reviewer/security-scan.md +188 -0
- package/.claude/agents/references/proposal-reviewer/report-template.md +144 -0
- package/.claude/agents/references/proposal-reviewer/sa-prompts.md +149 -0
- package/.claude/agents/references/referee-config.md +114 -0
- package/.claude/agents/references/referee2-reviewer/audit-checklists.md +287 -0
- package/.claude/agents/references/referee2-reviewer/report-template.md +334 -0
- package/.claude/rules/design-before-results.md +52 -0
- package/.claude/rules/ignore-agents-md.md +17 -0
- package/.claude/rules/ignore-gemini-md.md +17 -0
- package/.claude/rules/lean-claude-md.md +45 -0
- package/.claude/rules/learn-tags.md +99 -0
- package/.claude/rules/overleaf-separation.md +67 -0
- package/.claude/rules/plan-first.md +175 -0
- package/.claude/rules/read-docs-first.md +50 -0
- package/.claude/rules/scope-discipline.md +28 -0
- package/.claude/settings.json +125 -0
- package/.context/current-focus.md +33 -0
- package/.context/preferences/priorities.md +36 -0
- package/.context/preferences/task-naming.md +28 -0
- package/.context/profile.md +29 -0
- package/.context/projects/_index.md +41 -0
- package/.context/projects/papers/nudge-exp.md +22 -0
- package/.context/projects/papers/uncertainty.md +31 -0
- package/.context/resources/claude-scientific-writer-review.md +48 -0
- package/.context/resources/cunningham-multi-analyst-agents.md +104 -0
- package/.context/resources/cunningham-multilang-code-audit.md +62 -0
- package/.context/resources/google-ai-co-scientist-review.md +72 -0
- package/.context/resources/karpathy-llm-council-review.md +58 -0
- package/.context/resources/multi-coder-reliability-protocol.md +175 -0
- package/.context/resources/pedro-santanna-takeaways.md +96 -0
- package/.context/resources/venue-rankings/abs_ajg_2024.csv +1823 -0
- package/.context/resources/venue-rankings/abs_ajg_2024_econ.csv +356 -0
- package/.context/resources/venue-rankings/cabs_4_4star_theory.csv +40 -0
- package/.context/resources/venue-rankings/core_2026.csv +801 -0
- package/.context/resources/venue-rankings.md +147 -0
- package/.context/workflows/README.md +69 -0
- package/.context/workflows/daily-review.md +91 -0
- package/.context/workflows/meeting-actions.md +108 -0
- package/.context/workflows/replication-protocol.md +155 -0
- package/.context/workflows/weekly-review.md +113 -0
- package/.mcp-server-biblio/formatters.py +158 -0
- package/.mcp-server-biblio/pyproject.toml +11 -0
- package/.mcp-server-biblio/server.py +678 -0
- package/.mcp-server-biblio/sources/__init__.py +14 -0
- package/.mcp-server-biblio/sources/base.py +73 -0
- package/.mcp-server-biblio/sources/formatters.py +83 -0
- package/.mcp-server-biblio/sources/models.py +22 -0
- package/.mcp-server-biblio/sources/multi_source.py +243 -0
- package/.mcp-server-biblio/sources/openalex_source.py +183 -0
- package/.mcp-server-biblio/sources/scopus_source.py +309 -0
- package/.mcp-server-biblio/sources/wos_source.py +508 -0
- package/.mcp-server-biblio/uv.lock +896 -0
- package/.scripts/README.md +161 -0
- package/.scripts/ai_pattern_density.py +446 -0
- package/.scripts/conf +445 -0
- package/.scripts/config.py +122 -0
- package/.scripts/count_inventory.py +275 -0
- package/.scripts/daily_digest.py +288 -0
- package/.scripts/done +177 -0
- package/.scripts/extract_meeting_actions.py +223 -0
- package/.scripts/focus +176 -0
- package/.scripts/generate-codex-agents-md.py +217 -0
- package/.scripts/inbox +194 -0
- package/.scripts/notion_helpers.py +325 -0
- package/.scripts/openalex/query_helpers.py +306 -0
- package/.scripts/papers +227 -0
- package/.scripts/query +223 -0
- package/.scripts/session-history.py +201 -0
- package/.scripts/skill-health.py +516 -0
- package/.scripts/skill-log-miner.py +273 -0
- package/.scripts/sync-to-codex.sh +252 -0
- package/.scripts/task +213 -0
- package/.scripts/tasks +190 -0
- package/.scripts/week +206 -0
- package/CLAUDE.md +197 -0
- package/LICENSE +21 -0
- package/MEMORY.md +38 -0
- package/README.md +269 -0
- package/docs/agents.md +44 -0
- package/docs/bibliography-setup.md +55 -0
- package/docs/council-mode.md +36 -0
- package/docs/getting-started.md +245 -0
- package/docs/hooks.md +38 -0
- package/docs/mcp-servers.md +82 -0
- package/docs/notion-setup.md +109 -0
- package/docs/rules.md +33 -0
- package/docs/scripts.md +303 -0
- package/docs/setup-overview/setup-overview.pdf +0 -0
- package/docs/skills.md +70 -0
- package/docs/system.md +159 -0
- package/hooks/block-destructive-git.sh +66 -0
- package/hooks/context-monitor.py +114 -0
- package/hooks/postcompact-restore.py +157 -0
- package/hooks/precompact-autosave.py +181 -0
- package/hooks/promise-checker.sh +124 -0
- package/hooks/protect-source-files.sh +81 -0
- package/hooks/resume-context-loader.sh +53 -0
- package/hooks/startup-context-loader.sh +102 -0
- package/package.json +51 -0
- package/packages/cli-council/.github/workflows/claude-code-review.yml +44 -0
- package/packages/cli-council/.github/workflows/claude.yml +50 -0
- package/packages/cli-council/README.md +100 -0
- package/packages/cli-council/pyproject.toml +43 -0
- package/packages/cli-council/src/cli_council/__init__.py +19 -0
- package/packages/cli-council/src/cli_council/__main__.py +185 -0
- package/packages/cli-council/src/cli_council/backends/__init__.py +8 -0
- package/packages/cli-council/src/cli_council/backends/base.py +81 -0
- package/packages/cli-council/src/cli_council/backends/claude.py +25 -0
- package/packages/cli-council/src/cli_council/backends/codex.py +27 -0
- package/packages/cli-council/src/cli_council/backends/gemini.py +26 -0
- package/packages/cli-council/src/cli_council/checkpoint.py +212 -0
- package/packages/cli-council/src/cli_council/config.py +51 -0
- package/packages/cli-council/src/cli_council/council.py +391 -0
- package/packages/cli-council/src/cli_council/models.py +46 -0
- package/packages/llm-council/.github/workflows/claude-code-review.yml +44 -0
- package/packages/llm-council/.github/workflows/claude.yml +50 -0
- package/packages/llm-council/README.md +453 -0
- package/packages/llm-council/pyproject.toml +42 -0
- package/packages/llm-council/src/llm_council/__init__.py +23 -0
- package/packages/llm-council/src/llm_council/__main__.py +259 -0
- package/packages/llm-council/src/llm_council/checkpoint.py +193 -0
- package/packages/llm-council/src/llm_council/client.py +253 -0
- package/packages/llm-council/src/llm_council/config.py +232 -0
- package/packages/llm-council/src/llm_council/council.py +482 -0
- package/packages/llm-council/src/llm_council/models.py +46 -0
- package/packages/mcp-bibliography/MEMORY.md +31 -0
- package/packages/mcp-bibliography/_app.py +226 -0
- package/packages/mcp-bibliography/formatters.py +158 -0
- package/packages/mcp-bibliography/log/2026-03-13-2100.md +35 -0
- package/packages/mcp-bibliography/pyproject.toml +15 -0
- package/packages/mcp-bibliography/run.sh +20 -0
- package/packages/mcp-bibliography/scholarly_formatters.py +83 -0
- package/packages/mcp-bibliography/server.py +1857 -0
- package/packages/mcp-bibliography/tools/__init__.py +28 -0
- package/packages/mcp-bibliography/tools/_registry.py +19 -0
- package/packages/mcp-bibliography/tools/altmetric.py +107 -0
- package/packages/mcp-bibliography/tools/core.py +92 -0
- package/packages/mcp-bibliography/tools/dblp.py +52 -0
- package/packages/mcp-bibliography/tools/openalex.py +296 -0
- package/packages/mcp-bibliography/tools/opencitations.py +102 -0
- package/packages/mcp-bibliography/tools/openreview.py +179 -0
- package/packages/mcp-bibliography/tools/orcid.py +131 -0
- package/packages/mcp-bibliography/tools/scholarly.py +575 -0
- package/packages/mcp-bibliography/tools/unpaywall.py +63 -0
- package/packages/mcp-bibliography/tools/zenodo.py +123 -0
- package/packages/mcp-bibliography/uv.lock +711 -0
- package/scripts/setup.sh +143 -0
- package/skills/beamer-deck/SKILL.md +199 -0
- package/skills/beamer-deck/references/quality-rubric.md +54 -0
- package/skills/beamer-deck/references/review-prompts.md +106 -0
- package/skills/bib-validate/SKILL.md +261 -0
- package/skills/bib-validate/references/council-mode.md +34 -0
- package/skills/bib-validate/references/deep-verify.md +79 -0
- package/skills/bib-validate/references/fix-mode.md +36 -0
- package/skills/bib-validate/references/openalex-verification.md +45 -0
- package/skills/bib-validate/references/preprint-check.md +31 -0
- package/skills/bib-validate/references/ref-manager-crossref.md +41 -0
- package/skills/bib-validate/references/report-template.md +82 -0
- package/skills/code-archaeology/SKILL.md +141 -0
- package/skills/code-review/SKILL.md +265 -0
- package/skills/code-review/references/quality-rubric.md +67 -0
- package/skills/consolidate-memory/SKILL.md +208 -0
- package/skills/context-status/SKILL.md +126 -0
- package/skills/creation-guard/SKILL.md +230 -0
- package/skills/devils-advocate/SKILL.md +130 -0
- package/skills/devils-advocate/references/competing-hypotheses.md +83 -0
- package/skills/init-project/SKILL.md +115 -0
- package/skills/init-project-course/references/memory-and-settings.md +92 -0
- package/skills/init-project-course/references/organise-templates.md +94 -0
- package/skills/init-project-course/skill.md +147 -0
- package/skills/init-project-light/skill.md +139 -0
- package/skills/init-project-research/SKILL.md +368 -0
- package/skills/init-project-research/references/atlas-pipeline-sync.md +70 -0
- package/skills/init-project-research/references/atlas-schema.md +81 -0
- package/skills/init-project-research/references/confirmation-report.md +39 -0
- package/skills/init-project-research/references/domain-profile-template.md +104 -0
- package/skills/init-project-research/references/interview-round3.md +34 -0
- package/skills/init-project-research/references/literature-discovery.md +43 -0
- package/skills/init-project-research/references/scaffold-details.md +197 -0
- package/skills/init-project-research/templates/field-calibration.md +60 -0
- package/skills/init-project-research/templates/pipeline-manifest.md +63 -0
- package/skills/init-project-research/templates/run-all.sh +116 -0
- package/skills/init-project-research/templates/seed-files.md +337 -0
- package/skills/insights-deck/SKILL.md +151 -0
- package/skills/interview-me/SKILL.md +157 -0
- package/skills/latex/SKILL.md +141 -0
- package/skills/latex/references/latex-configs.md +183 -0
- package/skills/latex-autofix/SKILL.md +230 -0
- package/skills/latex-autofix/references/known-errors.md +183 -0
- package/skills/latex-autofix/references/quality-rubric.md +50 -0
- package/skills/latex-health-check/SKILL.md +161 -0
- package/skills/learn/SKILL.md +220 -0
- package/skills/learn/scripts/validate_skill.py +265 -0
- package/skills/lessons-learned/SKILL.md +201 -0
- package/skills/literature/SKILL.md +335 -0
- package/skills/literature/references/agent-templates.md +393 -0
- package/skills/literature/references/bibliometric-apis.md +44 -0
- package/skills/literature/references/cli-council-search.md +79 -0
- package/skills/literature/references/openalex-api-guide.md +371 -0
- package/skills/literature/references/openalex-common-queries.md +381 -0
- package/skills/literature/references/openalex-workflows.md +248 -0
- package/skills/literature/references/reference-manager-sync.md +36 -0
- package/skills/literature/references/scopus-api-guide.md +208 -0
- package/skills/literature/references/wos-api-guide.md +308 -0
- package/skills/multi-perspective/SKILL.md +311 -0
- package/skills/multi-perspective/references/computational-many-analysts.md +77 -0
- package/skills/pipeline-manifest/SKILL.md +226 -0
- package/skills/pre-submission-report/SKILL.md +153 -0
- package/skills/process-reviews/SKILL.md +244 -0
- package/skills/process-reviews/references/rr-routing.md +101 -0
- package/skills/project-deck/SKILL.md +87 -0
- package/skills/project-safety/SKILL.md +135 -0
- package/skills/proofread/SKILL.md +254 -0
- package/skills/proofread/references/quality-rubric.md +104 -0
- package/skills/python-env/SKILL.md +57 -0
- package/skills/quarto-deck/SKILL.md +226 -0
- package/skills/quarto-deck/references/markdown-format.md +143 -0
- package/skills/quarto-deck/references/quality-rubric.md +54 -0
- package/skills/save-context/SKILL.md +174 -0
- package/skills/session-log/SKILL.md +98 -0
- package/skills/shared/concept-validation-gate.md +161 -0
- package/skills/shared/council-protocol.md +265 -0
- package/skills/shared/distribution-diagnostics.md +164 -0
- package/skills/shared/engagement-stratified-sampling.md +218 -0
- package/skills/shared/escalation-protocol.md +74 -0
- package/skills/shared/external-audit-protocol.md +205 -0
- package/skills/shared/intercoder-reliability.md +256 -0
- package/skills/shared/mcp-degradation.md +81 -0
- package/skills/shared/method-probing-questions.md +163 -0
- package/skills/shared/multi-language-conventions.md +143 -0
- package/skills/shared/paid-api-safety.md +174 -0
- package/skills/shared/palettes.md +90 -0
- package/skills/shared/progressive-disclosure.md +92 -0
- package/skills/shared/project-documentation-content.md +443 -0
- package/skills/shared/project-documentation-format.md +281 -0
- package/skills/shared/project-documentation.md +100 -0
- package/skills/shared/publication-output.md +138 -0
- package/skills/shared/quality-scoring.md +70 -0
- package/skills/shared/reference-resolution.md +77 -0
- package/skills/shared/research-quality-rubric.md +165 -0
- package/skills/shared/rhetoric-principles.md +54 -0
- package/skills/shared/skill-design-patterns.md +272 -0
- package/skills/shared/skill-index.md +240 -0
- package/skills/shared/system-documentation.md +334 -0
- package/skills/shared/tikz-rules.md +402 -0
- package/skills/shared/validation-tiers.md +121 -0
- package/skills/shared/venue-guides/README.md +46 -0
- package/skills/shared/venue-guides/cell_press_style.md +483 -0
- package/skills/shared/venue-guides/conferences_formatting.md +564 -0
- package/skills/shared/venue-guides/cs_conference_style.md +463 -0
- package/skills/shared/venue-guides/examples/cell_summary_example.md +247 -0
- package/skills/shared/venue-guides/examples/medical_structured_abstract.md +313 -0
- package/skills/shared/venue-guides/examples/nature_abstract_examples.md +213 -0
- package/skills/shared/venue-guides/examples/neurips_introduction_example.md +245 -0
- package/skills/shared/venue-guides/journals_formatting.md +486 -0
- package/skills/shared/venue-guides/medical_journal_styles.md +535 -0
- package/skills/shared/venue-guides/ml_conference_style.md +556 -0
- package/skills/shared/venue-guides/nature_science_style.md +405 -0
- package/skills/shared/venue-guides/reviewer_expectations.md +417 -0
- package/skills/shared/venue-guides/venue_writing_styles.md +321 -0
- package/skills/split-pdf/SKILL.md +172 -0
- package/skills/split-pdf/methodology.md +48 -0
- package/skills/sync-notion/SKILL.md +93 -0
- package/skills/system-audit/SKILL.md +157 -0
- package/skills/system-audit/references/sub-agent-prompts.md +294 -0
- package/skills/task-management/SKILL.md +131 -0
- package/skills/update-focus/SKILL.md +204 -0
- package/skills/update-project-doc/SKILL.md +194 -0
- package/skills/validate-bib/SKILL.md +242 -0
- package/skills/validate-bib/references/council-mode.md +34 -0
- package/skills/validate-bib/references/deep-verify.md +71 -0
- package/skills/validate-bib/references/openalex-verification.md +45 -0
- package/skills/validate-bib/references/preprint-check.md +31 -0
- package/skills/validate-bib/references/report-template.md +62 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Skill Health Assessment — Phase A, Step 2
|
|
4
|
+
|
|
5
|
+
Reads observation events from ~/.claude/ecc/observations-*.jsonl,
|
|
6
|
+
correlates pre/post events, computes per-skill health metrics,
|
|
7
|
+
and outputs a structured report.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
uv run python .scripts/skill-health.py # full report
|
|
11
|
+
uv run python .scripts/skill-health.py --skill proofread # single skill
|
|
12
|
+
uv run python .scripts/skill-health.py --health failing # filter by health
|
|
13
|
+
uv run python .scripts/skill-health.py --activity dormant # filter by activity
|
|
14
|
+
uv run python .scripts/skill-health.py --top 10 # top N most-used
|
|
15
|
+
uv run python .scripts/skill-health.py --purge # run maintenance
|
|
16
|
+
uv run python .scripts/skill-health.py --json # JSON output
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
from collections import defaultdict
|
|
24
|
+
from datetime import datetime, timedelta, timezone
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from statistics import median
|
|
27
|
+
|
|
28
|
+
ECC_DIR = Path.home() / ".claude" / "ecc"
|
|
29
|
+
ROLLING_WINDOW_DAYS = 30
|
|
30
|
+
PURGE_DAYS = 90
|
|
31
|
+
MAX_TOTAL_SIZE_MB = 50
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_args():
|
|
35
|
+
p = argparse.ArgumentParser(description="Skill health assessment")
|
|
36
|
+
p.add_argument("--skill", help="Show detail for a single skill")
|
|
37
|
+
p.add_argument("--health", choices=["failing", "declining", "watch", "healthy", "insufficient_data", "low_observability"],
|
|
38
|
+
help="Filter by health status")
|
|
39
|
+
p.add_argument("--activity", choices=["active", "regular", "dormant"],
|
|
40
|
+
help="Filter by activity status")
|
|
41
|
+
p.add_argument("--top", type=int, help="Show top N most-used skills")
|
|
42
|
+
p.add_argument("--purge", action="store_true", help="Run maintenance (delete old files)")
|
|
43
|
+
p.add_argument("--json", action="store_true", help="Output as JSON")
|
|
44
|
+
p.add_argument("--all-time", action="store_true", help="Use all data, not rolling window")
|
|
45
|
+
return p.parse_args()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_events(window_start: datetime | None = None) -> list[dict]:
|
|
49
|
+
"""Load all events from daily JSONL files, optionally filtered by date."""
|
|
50
|
+
events = []
|
|
51
|
+
if not ECC_DIR.exists():
|
|
52
|
+
return events
|
|
53
|
+
|
|
54
|
+
for filepath in sorted(ECC_DIR.glob("observations-*.jsonl")):
|
|
55
|
+
# Extract date from filename for quick filtering
|
|
56
|
+
try:
|
|
57
|
+
file_date_str = filepath.stem.replace("observations-", "")
|
|
58
|
+
file_date = datetime.strptime(file_date_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
|
|
59
|
+
if window_start and file_date < window_start - timedelta(days=1):
|
|
60
|
+
continue
|
|
61
|
+
except ValueError:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
with open(filepath) as f:
|
|
66
|
+
for line_num, line in enumerate(f, 1):
|
|
67
|
+
line = line.strip()
|
|
68
|
+
if not line:
|
|
69
|
+
continue
|
|
70
|
+
try:
|
|
71
|
+
event = json.loads(line)
|
|
72
|
+
if event.get("schema") == "skill-event.v1":
|
|
73
|
+
events.append(event)
|
|
74
|
+
except json.JSONDecodeError:
|
|
75
|
+
print(f" Warning: malformed line {line_num} in {filepath.name}", file=sys.stderr)
|
|
76
|
+
except OSError as e:
|
|
77
|
+
print(f" Warning: cannot read {filepath}: {e}", file=sys.stderr)
|
|
78
|
+
|
|
79
|
+
# Also load rule-based outcome logs
|
|
80
|
+
outcomes_file = ECC_DIR / "skill-outcomes.jsonl"
|
|
81
|
+
if outcomes_file.exists():
|
|
82
|
+
try:
|
|
83
|
+
with open(outcomes_file) as f:
|
|
84
|
+
for line_num, line in enumerate(f, 1):
|
|
85
|
+
line = line.strip()
|
|
86
|
+
if not line:
|
|
87
|
+
continue
|
|
88
|
+
try:
|
|
89
|
+
record = json.loads(line)
|
|
90
|
+
# Convert outcome log to skill-event.v1 format
|
|
91
|
+
if "skill" in record and "outcome" in record:
|
|
92
|
+
ts = record.get("timestamp", "")
|
|
93
|
+
if window_start and ts:
|
|
94
|
+
try:
|
|
95
|
+
record_dt = datetime.fromisoformat(ts)
|
|
96
|
+
if record_dt.tzinfo is None:
|
|
97
|
+
record_dt = record_dt.replace(tzinfo=timezone.utc)
|
|
98
|
+
if record_dt < window_start:
|
|
99
|
+
continue
|
|
100
|
+
except ValueError:
|
|
101
|
+
pass
|
|
102
|
+
events.append({
|
|
103
|
+
"schema": "skill-event.v1",
|
|
104
|
+
"phase": "outcome",
|
|
105
|
+
"skill": record["skill"],
|
|
106
|
+
"timestamp": ts,
|
|
107
|
+
"outcome": record["outcome"],
|
|
108
|
+
"outcome_source": "rule",
|
|
109
|
+
"heuristic_version": 1,
|
|
110
|
+
"error_class": record.get("note", "")[:100] if record.get("outcome") != "success" else None,
|
|
111
|
+
"session_hash": record.get("session", ""),
|
|
112
|
+
"project_label": record.get("project", ""),
|
|
113
|
+
"project_hash": record.get("project", ""),
|
|
114
|
+
})
|
|
115
|
+
except json.JSONDecodeError:
|
|
116
|
+
print(f" Warning: malformed line {line_num} in skill-outcomes.jsonl", file=sys.stderr)
|
|
117
|
+
except OSError:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
return events
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def correlate_events(events: list[dict]) -> dict:
|
|
124
|
+
"""
|
|
125
|
+
Group events by skill, correlate pre/post by invocation_id.
|
|
126
|
+
Returns: {skill_name: {"pre": [...], "post": [...], "paired": [...]}}
|
|
127
|
+
"""
|
|
128
|
+
by_skill: dict[str, dict] = defaultdict(lambda: {"pre": [], "post": [], "paired": [], "mined": []})
|
|
129
|
+
|
|
130
|
+
# Index by invocation_id for correlation
|
|
131
|
+
pre_index: dict[str, dict] = {}
|
|
132
|
+
post_index: dict[str, dict] = {}
|
|
133
|
+
|
|
134
|
+
for event in events:
|
|
135
|
+
skill = event.get("skill", "unknown")
|
|
136
|
+
phase = event.get("phase", "")
|
|
137
|
+
inv_id = event.get("invocation_id", "")
|
|
138
|
+
|
|
139
|
+
if phase == "pre":
|
|
140
|
+
by_skill[skill]["pre"].append(event)
|
|
141
|
+
if inv_id:
|
|
142
|
+
pre_index[inv_id] = event
|
|
143
|
+
elif phase == "post":
|
|
144
|
+
by_skill[skill]["post"].append(event)
|
|
145
|
+
if inv_id:
|
|
146
|
+
post_index[inv_id] = event
|
|
147
|
+
elif phase == "mined":
|
|
148
|
+
by_skill[skill]["mined"].append(event)
|
|
149
|
+
elif phase == "outcome":
|
|
150
|
+
by_skill[skill]["post"].append(event) # outcome events have the same shape as post events
|
|
151
|
+
|
|
152
|
+
# Pair pre/post events by invocation_id
|
|
153
|
+
for inv_id, pre_event in pre_index.items():
|
|
154
|
+
if inv_id in post_index:
|
|
155
|
+
post_event = post_index[inv_id]
|
|
156
|
+
skill = pre_event.get("skill", "unknown")
|
|
157
|
+
try:
|
|
158
|
+
pre_ts = datetime.fromisoformat(pre_event["timestamp"])
|
|
159
|
+
post_ts = datetime.fromisoformat(post_event["timestamp"])
|
|
160
|
+
duration_ms = int((post_ts - pre_ts).total_seconds() * 1000)
|
|
161
|
+
by_skill[skill]["paired"].append({
|
|
162
|
+
"pre": pre_event,
|
|
163
|
+
"post": post_event,
|
|
164
|
+
"duration_ms": max(0, duration_ms),
|
|
165
|
+
})
|
|
166
|
+
except (KeyError, ValueError):
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
return dict(by_skill)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _windowed_success_rate(post_events: list[dict], now: datetime, days: int) -> float | None:
|
|
173
|
+
"""Compute success rate over the last N days from post/outcome events."""
|
|
174
|
+
cutoff = now - timedelta(days=days)
|
|
175
|
+
success = 0
|
|
176
|
+
classified = 0
|
|
177
|
+
for post in post_events:
|
|
178
|
+
ts_str = post.get("timestamp", "")
|
|
179
|
+
if not ts_str:
|
|
180
|
+
continue
|
|
181
|
+
try:
|
|
182
|
+
ts = datetime.fromisoformat(ts_str)
|
|
183
|
+
if ts.tzinfo is None:
|
|
184
|
+
ts = ts.replace(tzinfo=timezone.utc)
|
|
185
|
+
except ValueError:
|
|
186
|
+
continue
|
|
187
|
+
if ts < cutoff:
|
|
188
|
+
continue
|
|
189
|
+
outcome = post.get("outcome", "unknown")
|
|
190
|
+
if outcome in ("success", "error"):
|
|
191
|
+
classified += 1
|
|
192
|
+
if outcome == "success":
|
|
193
|
+
success += 1
|
|
194
|
+
return success / classified if classified >= 3 else None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
TREND_THRESHOLD = 0.10 # 10% drop triggers "worsening"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def compute_metrics(skill_data: dict, now: datetime) -> dict:
|
|
201
|
+
"""Compute health metrics for a single skill."""
|
|
202
|
+
pre_events = skill_data["pre"]
|
|
203
|
+
post_events = skill_data["post"]
|
|
204
|
+
paired = skill_data["paired"]
|
|
205
|
+
mined_events = skill_data.get("mined", [])
|
|
206
|
+
|
|
207
|
+
# Total invocations: prefer hook data, fall back to mined
|
|
208
|
+
total = len(pre_events)
|
|
209
|
+
if total == 0:
|
|
210
|
+
total = len(post_events)
|
|
211
|
+
# Add mined invocations (each mined event = 1 session mention)
|
|
212
|
+
total += len(mined_events)
|
|
213
|
+
|
|
214
|
+
# Outcome classification
|
|
215
|
+
outcomes = defaultdict(int)
|
|
216
|
+
error_classes = defaultdict(int)
|
|
217
|
+
for post in post_events:
|
|
218
|
+
outcome = post.get("outcome", "unknown")
|
|
219
|
+
outcomes[outcome] += 1
|
|
220
|
+
if outcome == "error" and post.get("error_class"):
|
|
221
|
+
error_classes[post["error_class"]] += 1
|
|
222
|
+
|
|
223
|
+
classified = outcomes.get("success", 0) + outcomes.get("error", 0)
|
|
224
|
+
unknown_count = outcomes.get("unknown", 0)
|
|
225
|
+
|
|
226
|
+
success_rate = outcomes["success"] / classified if classified > 0 else None
|
|
227
|
+
error_rate = outcomes["error"] / total if total > 0 else 0.0
|
|
228
|
+
unknown_rate = unknown_count / total if total > 0 else 1.0
|
|
229
|
+
|
|
230
|
+
# --- Trend detection (7d vs 30d windowed success rates) ---
|
|
231
|
+
rate_7d = _windowed_success_rate(post_events, now, 7)
|
|
232
|
+
rate_30d = _windowed_success_rate(post_events, now, 30)
|
|
233
|
+
|
|
234
|
+
if rate_7d is not None and rate_30d is not None:
|
|
235
|
+
delta = rate_7d - rate_30d
|
|
236
|
+
if delta <= -TREND_THRESHOLD:
|
|
237
|
+
trend = "worsening"
|
|
238
|
+
declining = True
|
|
239
|
+
elif delta >= TREND_THRESHOLD:
|
|
240
|
+
trend = "improving"
|
|
241
|
+
declining = False
|
|
242
|
+
else:
|
|
243
|
+
trend = "stable"
|
|
244
|
+
declining = False
|
|
245
|
+
else:
|
|
246
|
+
trend = "insufficient_data"
|
|
247
|
+
declining = False
|
|
248
|
+
|
|
249
|
+
# Duration from paired events
|
|
250
|
+
durations = [p["duration_ms"] for p in paired if p["duration_ms"] > 0]
|
|
251
|
+
median_duration = median(durations) if durations else None
|
|
252
|
+
|
|
253
|
+
# Last used
|
|
254
|
+
timestamps = []
|
|
255
|
+
for e in pre_events + post_events + mined_events:
|
|
256
|
+
try:
|
|
257
|
+
timestamps.append(datetime.fromisoformat(e["timestamp"]))
|
|
258
|
+
except (KeyError, ValueError):
|
|
259
|
+
pass
|
|
260
|
+
last_used = max(timestamps) if timestamps else None
|
|
261
|
+
|
|
262
|
+
# Project diversity
|
|
263
|
+
project_hashes = set()
|
|
264
|
+
for e in pre_events + mined_events:
|
|
265
|
+
ph = e.get("project_hash")
|
|
266
|
+
if ph:
|
|
267
|
+
project_hashes.add(ph)
|
|
268
|
+
|
|
269
|
+
# Re-invocation rate (from hook data only — mined data is per-session)
|
|
270
|
+
sessions = defaultdict(int)
|
|
271
|
+
for e in pre_events:
|
|
272
|
+
sh = e.get("session_hash")
|
|
273
|
+
if sh:
|
|
274
|
+
sessions[sh] += 1
|
|
275
|
+
# Mined events with mention_count > 1 suggest re-invocation
|
|
276
|
+
for e in mined_events:
|
|
277
|
+
sh = e.get("session_hash")
|
|
278
|
+
mc = e.get("mention_count", 1)
|
|
279
|
+
if sh:
|
|
280
|
+
sessions[sh] += mc
|
|
281
|
+
sessions_with_reinvoke = sum(1 for count in sessions.values() if count >= 2)
|
|
282
|
+
reinvoke_rate = sessions_with_reinvoke / len(sessions) if sessions else 0.0
|
|
283
|
+
|
|
284
|
+
# Top errors
|
|
285
|
+
top_errors = sorted(error_classes.items(), key=lambda x: -x[1])[:3]
|
|
286
|
+
|
|
287
|
+
# Skill mtime (most recent)
|
|
288
|
+
mtimes = [e.get("skill_mtime") for e in pre_events if e.get("skill_mtime")]
|
|
289
|
+
latest_mtime = max(mtimes) if mtimes else None
|
|
290
|
+
|
|
291
|
+
# Health status
|
|
292
|
+
if classified < 10:
|
|
293
|
+
health = "insufficient_data"
|
|
294
|
+
elif unknown_rate > 0.7:
|
|
295
|
+
health = "low_observability"
|
|
296
|
+
elif error_rate >= 0.3:
|
|
297
|
+
health = "failing"
|
|
298
|
+
elif declining:
|
|
299
|
+
health = "declining"
|
|
300
|
+
elif error_rate >= 0.1:
|
|
301
|
+
health = "watch"
|
|
302
|
+
else:
|
|
303
|
+
health = "healthy"
|
|
304
|
+
|
|
305
|
+
# Activity status
|
|
306
|
+
if last_used:
|
|
307
|
+
days_since = (now - last_used).days
|
|
308
|
+
if days_since <= 7:
|
|
309
|
+
activity = "active"
|
|
310
|
+
elif days_since <= 30:
|
|
311
|
+
activity = "regular"
|
|
312
|
+
else:
|
|
313
|
+
activity = "dormant"
|
|
314
|
+
else:
|
|
315
|
+
activity = "dormant"
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
"total_invocations": total,
|
|
319
|
+
"classified_invocations": classified,
|
|
320
|
+
"success_rate": round(success_rate, 3) if success_rate is not None else None,
|
|
321
|
+
"error_rate": round(error_rate, 3),
|
|
322
|
+
"unknown_rate": round(unknown_rate, 3),
|
|
323
|
+
"error_count": outcomes.get("error", 0),
|
|
324
|
+
"rate_7d": round(rate_7d, 3) if rate_7d is not None else None,
|
|
325
|
+
"rate_30d": round(rate_30d, 3) if rate_30d is not None else None,
|
|
326
|
+
"trend": trend,
|
|
327
|
+
"declining": declining,
|
|
328
|
+
"median_duration_ms": int(median_duration) if median_duration else None,
|
|
329
|
+
"last_used": last_used.isoformat() if last_used else None,
|
|
330
|
+
"project_count": len(project_hashes),
|
|
331
|
+
"re_invocation_rate": round(reinvoke_rate, 3),
|
|
332
|
+
"top_errors": [{"class": cls, "count": cnt} for cls, cnt in top_errors],
|
|
333
|
+
"health": health,
|
|
334
|
+
"activity": activity,
|
|
335
|
+
"latest_skill_mtime": latest_mtime,
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def purge_old_files():
|
|
340
|
+
"""Delete observation files older than PURGE_DAYS. Enforce total size cap."""
|
|
341
|
+
if not ECC_DIR.exists():
|
|
342
|
+
print("No ecc directory found.")
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
cutoff = datetime.now() - timedelta(days=PURGE_DAYS)
|
|
346
|
+
files = sorted(ECC_DIR.glob("observations-*.jsonl"))
|
|
347
|
+
deleted = 0
|
|
348
|
+
|
|
349
|
+
# Phase 1: delete by age
|
|
350
|
+
for f in files:
|
|
351
|
+
try:
|
|
352
|
+
date_str = f.stem.replace("observations-", "")
|
|
353
|
+
file_date = datetime.strptime(date_str, "%Y-%m-%d")
|
|
354
|
+
if file_date < cutoff:
|
|
355
|
+
f.unlink()
|
|
356
|
+
deleted += 1
|
|
357
|
+
print(f" Purged: {f.name} (older than {PURGE_DAYS} days)")
|
|
358
|
+
except (ValueError, OSError):
|
|
359
|
+
pass
|
|
360
|
+
|
|
361
|
+
# Phase 2: enforce size cap
|
|
362
|
+
remaining = sorted(ECC_DIR.glob("observations-*.jsonl"))
|
|
363
|
+
total_size = sum(f.stat().st_size for f in remaining if f.exists())
|
|
364
|
+
max_bytes = MAX_TOTAL_SIZE_MB * 1024 * 1024
|
|
365
|
+
|
|
366
|
+
while total_size > max_bytes and remaining:
|
|
367
|
+
oldest = remaining.pop(0)
|
|
368
|
+
size = oldest.stat().st_size
|
|
369
|
+
oldest.unlink()
|
|
370
|
+
total_size -= size
|
|
371
|
+
deleted += 1
|
|
372
|
+
print(f" Purged: {oldest.name} (size cap)")
|
|
373
|
+
|
|
374
|
+
print(f"Maintenance complete. {deleted} files purged. "
|
|
375
|
+
f"{len(list(ECC_DIR.glob('observations-*.jsonl')))} files remaining.")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def format_table(report: dict, args) -> str:
|
|
379
|
+
"""Format the health report as a readable table."""
|
|
380
|
+
if not report:
|
|
381
|
+
return "No observation data found. The skill observer hook may not have fired yet.\n" \
|
|
382
|
+
"Invoke any skill (e.g., /context-status) and check ~/.claude/ecc/ for data."
|
|
383
|
+
|
|
384
|
+
# Sort by total invocations descending
|
|
385
|
+
items = sorted(report.items(), key=lambda x: -x[1]["total_invocations"])
|
|
386
|
+
|
|
387
|
+
if args.top:
|
|
388
|
+
items = items[:args.top]
|
|
389
|
+
|
|
390
|
+
lines = []
|
|
391
|
+
lines.append(f"{'Skill':<30} {'Invocations':>11} {'Success':>8} {'Errors':>7} {'Trend':<13} {'Health':<18} {'Activity':<10} {'Last Used':<12}")
|
|
392
|
+
lines.append("-" * 115)
|
|
393
|
+
|
|
394
|
+
for skill, m in items:
|
|
395
|
+
sr = f"{m['success_rate']:.0%}" if m["success_rate"] is not None else "n/a"
|
|
396
|
+
ec = str(m["error_count"])
|
|
397
|
+
lu = m["last_used"][:10] if m["last_used"] else "never"
|
|
398
|
+
|
|
399
|
+
# Trend indicator
|
|
400
|
+
trend = m.get("trend", "")
|
|
401
|
+
if trend == "worsening":
|
|
402
|
+
trend_display = "!! DECLINING"
|
|
403
|
+
elif trend == "improving":
|
|
404
|
+
trend_display = "^ improving"
|
|
405
|
+
elif trend == "stable":
|
|
406
|
+
trend_display = "-- stable"
|
|
407
|
+
else:
|
|
408
|
+
trend_display = " n/a"
|
|
409
|
+
|
|
410
|
+
# Health indicator
|
|
411
|
+
health_display = m["health"]
|
|
412
|
+
if m["health"] == "failing":
|
|
413
|
+
health_display = "!! FAILING"
|
|
414
|
+
elif m["health"] == "declining":
|
|
415
|
+
health_display = "!! DECLINING"
|
|
416
|
+
elif m["health"] == "watch":
|
|
417
|
+
health_display = "? WATCH"
|
|
418
|
+
elif m["health"] == "healthy":
|
|
419
|
+
health_display = "ok HEALTHY"
|
|
420
|
+
|
|
421
|
+
lines.append(f"{skill:<30} {m['total_invocations']:>11} {sr:>8} {ec:>7} {trend_display:<13} {health_display:<18} {m['activity']:<10} {lu:<12}")
|
|
422
|
+
|
|
423
|
+
# Summary
|
|
424
|
+
total_skills = len(report)
|
|
425
|
+
failing = sum(1 for m in report.values() if m["health"] == "failing")
|
|
426
|
+
declining_count = sum(1 for m in report.values() if m["health"] == "declining")
|
|
427
|
+
watching = sum(1 for m in report.values() if m["health"] == "watch")
|
|
428
|
+
healthy = sum(1 for m in report.values() if m["health"] == "healthy")
|
|
429
|
+
insufficient = sum(1 for m in report.values() if m["health"] in ("insufficient_data", "low_observability"))
|
|
430
|
+
|
|
431
|
+
lines.append("")
|
|
432
|
+
lines.append(f"Summary: {total_skills} skills observed | "
|
|
433
|
+
f"{failing} failing | {declining_count} declining | {watching} watch | {healthy} healthy | {insufficient} insufficient data")
|
|
434
|
+
|
|
435
|
+
return "\n".join(lines)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def format_detail(skill: str, metrics: dict) -> str:
|
|
439
|
+
"""Format detailed view for a single skill."""
|
|
440
|
+
lines = [f"Skill: {skill}", "=" * 40]
|
|
441
|
+
|
|
442
|
+
for key, val in metrics.items():
|
|
443
|
+
if key == "top_errors" and val:
|
|
444
|
+
lines.append(f" top_errors:")
|
|
445
|
+
for err in val:
|
|
446
|
+
lines.append(f" - {err['class']} ({err['count']}x)")
|
|
447
|
+
else:
|
|
448
|
+
lines.append(f" {key}: {val}")
|
|
449
|
+
|
|
450
|
+
return "\n".join(lines)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def main():
|
|
454
|
+
args = parse_args()
|
|
455
|
+
|
|
456
|
+
if args.purge:
|
|
457
|
+
purge_old_files()
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
now = datetime.now(timezone.utc)
|
|
461
|
+
window_start = None if args.all_time else now - timedelta(days=ROLLING_WINDOW_DAYS)
|
|
462
|
+
|
|
463
|
+
events = load_events(window_start)
|
|
464
|
+
if not events:
|
|
465
|
+
if args.json:
|
|
466
|
+
print(json.dumps({"skills": {}, "meta": {"event_count": 0}}))
|
|
467
|
+
else:
|
|
468
|
+
print("No observation data found in ~/.claude/ecc/.")
|
|
469
|
+
print("The skill observer hook may not have fired yet.")
|
|
470
|
+
print("Invoke any skill and check ~/.claude/ecc/ for observation files.")
|
|
471
|
+
return
|
|
472
|
+
|
|
473
|
+
# Filter by window
|
|
474
|
+
if window_start:
|
|
475
|
+
events = [e for e in events
|
|
476
|
+
if datetime.fromisoformat(e.get("timestamp", "2000-01-01T00:00:00+00:00")) >= window_start]
|
|
477
|
+
|
|
478
|
+
by_skill = correlate_events(events)
|
|
479
|
+
|
|
480
|
+
# Compute metrics
|
|
481
|
+
report = {}
|
|
482
|
+
for skill_name, data in by_skill.items():
|
|
483
|
+
report[skill_name] = compute_metrics(data, now)
|
|
484
|
+
|
|
485
|
+
# Apply filters
|
|
486
|
+
if args.health:
|
|
487
|
+
report = {k: v for k, v in report.items() if v["health"] == args.health}
|
|
488
|
+
if args.activity:
|
|
489
|
+
report = {k: v for k, v in report.items() if v["activity"] == args.activity}
|
|
490
|
+
|
|
491
|
+
# Output
|
|
492
|
+
if args.skill:
|
|
493
|
+
if args.skill in report:
|
|
494
|
+
if args.json:
|
|
495
|
+
print(json.dumps({args.skill: report[args.skill]}, indent=2))
|
|
496
|
+
else:
|
|
497
|
+
print(format_detail(args.skill, report[args.skill]))
|
|
498
|
+
else:
|
|
499
|
+
print(f"No data for skill '{args.skill}' in the current window.")
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
if args.json:
|
|
503
|
+
print(json.dumps({
|
|
504
|
+
"skills": report,
|
|
505
|
+
"meta": {
|
|
506
|
+
"event_count": len(events),
|
|
507
|
+
"window_days": ROLLING_WINDOW_DAYS if not args.all_time else "all",
|
|
508
|
+
"generated_at": now.isoformat(),
|
|
509
|
+
}
|
|
510
|
+
}, indent=2))
|
|
511
|
+
else:
|
|
512
|
+
print(format_table(report, args))
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
if __name__ == "__main__":
|
|
516
|
+
main()
|