@iseoane/agent-depot 0.2.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +465 -0
  4. package/package.json +69 -0
  5. package/skills/doctor-md-agents/README.md +51 -0
  6. package/skills/doctor-md-agents/SKILL.md +275 -0
  7. package/skills/doctor-md-agents/references/rule-model.md +56 -0
  8. package/skills/doctor-md-agents/references/structure.md +89 -0
  9. package/skills/doctor-md-agents/scorers/adherence.md +73 -0
  10. package/skills/doctor-md-agents/scorers/rule-value.md +49 -0
  11. package/skills/doctor-md-agents/scripts/collect_sessions.py +1135 -0
  12. package/skills/doctor-md-agents/scripts/extract_rules.py +704 -0
  13. package/skills/doctor-md-agents/scripts/render_report.py +644 -0
  14. package/skills/doctor-md-agents/scripts/scan_corrections.py +781 -0
  15. package/skills/doctor-md-agents/scripts/test_collect_sessions.py +780 -0
  16. package/skills/doctor-md-agents/scripts/test_extract_rules.py +530 -0
  17. package/skills/doctor-md-agents/scripts/test_render_report.py +383 -0
  18. package/skills/doctor-md-agents/scripts/test_scan_corrections.py +343 -0
  19. package/skills/doctor-md-agents/scripts/test_verify_citations.py +236 -0
  20. package/skills/doctor-md-agents/scripts/testdata/old_extract_rules_pre_t1.py.txt +507 -0
  21. package/skills/doctor-md-agents/scripts/verify_citations.py +217 -0
  22. package/skills/doctor-md-skill/README.md +27 -0
  23. package/skills/doctor-md-skill/SKILL.md +212 -0
  24. package/skills/doctor-md-skill/assets/pierre-diffs.js +1531 -0
  25. package/skills/doctor-md-skill/references/skill-improvements.md +32 -0
  26. package/skills/doctor-md-skill/references/supported-harnesses.md +44 -0
  27. package/skills/doctor-md-skill/scorers/code-quality.md +41 -0
  28. package/skills/doctor-md-skill/scorers/efficiency.md +38 -0
  29. package/skills/doctor-md-skill/scripts/collect_sessions.py +1112 -0
  30. package/skills/doctor-md-skill/scripts/detect_machine_sessions.py +315 -0
  31. package/skills/doctor-md-skill/scripts/render_report.py +545 -0
  32. package/skills/doctor-md-skill/scripts/test_collect_sessions.py +593 -0
  33. package/skills/doctor-md-skill/scripts/test_detect_machine_sessions.py +181 -0
  34. package/skills/doctor-md-skill/scripts/test_render_report.py +241 -0
  35. package/skills/doctor-md-skill/scripts/test_verify_citations.py +196 -0
  36. package/skills/doctor-md-skill/scripts/verify_citations.py +221 -0
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env python3
2
+ """Verify that a scorer's cited quote actually appears in the transcript it
3
+ claims to come from -- SKILL.md Step 2's mechanical check on scoring results.
4
+
5
+ Both scorers (scorers/efficiency.md, scorers/code-quality.md) are asked to
6
+ copy `quote` verbatim from the transcript they were shown, alongside the
7
+ existing 1-3 sentence `reason`. This script is the only thing that checks
8
+ that claim: it whitespace-folds both the quote and the transcript text and
9
+ requires the folded quote to be a literal substring of the folded
10
+ transcript. It is deliberately a literal-substring check, not a fuzzy
11
+ match -- a paraphrase must fail, and so must a quote that only differs in
12
+ case, because case is not whitespace.
13
+
14
+ On a real run of the sibling skill (doctor-md-agents), four judges all
15
+ claimed to have grep-verified their quotes and 2 of 62 findings still cited
16
+ text that was not there. This script exists so that claim is never taken on
17
+ faith again: SKILL.md aggregates only verified results, and the unverified
18
+ count is surfaced in the report rather than silently dropped.
19
+
20
+ Failure has four distinct reasons, and they are reported separately because
21
+ they mean different things to whoever reads results.verified.json:
22
+
23
+ quote_missing the result carries no usable quote text at all.
24
+ quote_too_short the quote is real but too short to prove anything
25
+ (a 3-character quote is trivially a substring).
26
+ not_found the transcript was read fine; the quote just is not
27
+ in it -- this is the scorer confabulating.
28
+ transcript_unreadable the transcript_path is missing or could not be
29
+ read -- an input-data problem, not a confabulation,
30
+ so it must never be folded into the scorer's error
31
+ rate.
32
+
33
+ Emits:
34
+ <out>/results.verified.json - every input result, in order, with every
35
+ original field preserved, plus `verified`
36
+ and `reason_code` (null when verified),
37
+ and a top-level `stats` block.
38
+
39
+ Python 3.9+, stdlib only. Reads only results_json and the transcript_path
40
+ files it names; writes only under --out; makes no network call; never
41
+ modifies a transcript or the input results file.
42
+ """
43
+
44
+ import argparse
45
+ import json
46
+ import re
47
+ import sys
48
+ from collections import Counter
49
+ from datetime import datetime, timezone
50
+ from pathlib import Path
51
+
52
+ WHITESPACE_RE = re.compile(r"\s+")
53
+
54
+ REASON_CODES = ("quote_missing", "quote_too_short", "not_found", "transcript_unreadable")
55
+
56
+
57
+ def fold_whitespace(text):
58
+ """Collapse every run of whitespace to a single space and strip ends.
59
+
60
+ Deliberately does NOT lowercase and does NOT strip punctuation -- this
61
+ must stay a literal-substring check, not a fuzzy match. A quote that
62
+ differs from the transcript only in case is a real verification
63
+ failure, not noise to smooth over.
64
+ """
65
+ return WHITESPACE_RE.sub(" ", text).strip()
66
+
67
+
68
+ def load_transcript_text(path):
69
+ """Read one transcript file. Returns None if it cannot be read.
70
+
71
+ A separate, patchable function so callers can count/observe real reads
72
+ even though results are cached per run -- see get_folded_transcript.
73
+ """
74
+ try:
75
+ return Path(path).expanduser().read_text(encoding="utf-8", errors="replace")
76
+ except OSError:
77
+ return None
78
+
79
+
80
+ def get_folded_transcript(path, cache):
81
+ """Return the whitespace-folded transcript at `path`, cached per run.
82
+
83
+ Many results usually share a transcript_path (efficiency and
84
+ code-quality both score the same transcript); this makes sure each
85
+ distinct path is only read from disk once per run. `cache` is owned by
86
+ the caller (main), never a module-level dict, so nothing leaks between
87
+ separate script runs or between tests.
88
+ """
89
+ if not isinstance(path, str) or not path:
90
+ return None
91
+ if path in cache:
92
+ return cache[path]
93
+ text = load_transcript_text(path)
94
+ folded = fold_whitespace(text) if text is not None else None
95
+ cache[path] = folded
96
+ return folded
97
+
98
+
99
+ def verify_one(result, min_quote_chars, transcript_cache):
100
+ """Return (verified, reason_code) for one scoring-result dict."""
101
+ quote_raw = result.get("quote")
102
+ if not isinstance(quote_raw, str) or not quote_raw.strip():
103
+ return False, "quote_missing"
104
+
105
+ folded_quote = fold_whitespace(quote_raw)
106
+ if len(folded_quote) < min_quote_chars:
107
+ return False, "quote_too_short"
108
+
109
+ folded_transcript = get_folded_transcript(result.get("transcript_path"), transcript_cache)
110
+ if folded_transcript is None:
111
+ return False, "transcript_unreadable"
112
+
113
+ if folded_quote in folded_transcript:
114
+ return True, None
115
+ return False, "not_found"
116
+
117
+
118
+ def transcript_key(result):
119
+ path = result.get("transcript_path")
120
+ return path if isinstance(path, str) and path else "(missing)"
121
+
122
+
123
+ def parse_args(argv=None):
124
+ p = argparse.ArgumentParser(description=__doc__)
125
+ p.add_argument("results_json", help="path to a results.json ({'results': [...]})")
126
+ p.add_argument("--out", required=True)
127
+ p.add_argument("--min-quote-chars", type=int, default=12,
128
+ help="minimum folded-quote length required to count as evidence (default: 12)")
129
+ return p.parse_args(argv)
130
+
131
+
132
+ def load_results_file(path):
133
+ """Load and structurally validate the input file. Raises ValueError on
134
+ any malformed shape -- caller turns that into a non-zero exit."""
135
+ try:
136
+ raw = path.read_text(encoding="utf-8")
137
+ except OSError as exc:
138
+ raise ValueError(f"could not read {path}: {exc}") from exc
139
+ try:
140
+ data = json.loads(raw)
141
+ except json.JSONDecodeError as exc:
142
+ raise ValueError(f"{path} is not valid JSON: {exc}") from exc
143
+ if not isinstance(data, dict) or not isinstance(data.get("results"), list):
144
+ raise ValueError(f"{path} is not a valid results file (expected {{'results': [...]}})")
145
+ return data["results"]
146
+
147
+
148
+ def main(argv=None):
149
+ args = parse_args(argv)
150
+ results_path = Path(args.results_json).expanduser()
151
+
152
+ try:
153
+ results = load_results_file(results_path)
154
+ except ValueError as exc:
155
+ print(f"error: {exc}", file=sys.stderr)
156
+ sys.exit(1)
157
+
158
+ out_dir = Path(args.out).expanduser()
159
+ out_dir.mkdir(parents=True, exist_ok=True)
160
+
161
+ transcript_cache = {}
162
+ verified_results = []
163
+ reason_counts = Counter()
164
+ by_transcript = {}
165
+ verified_count = 0
166
+ skipped_non_object = 0
167
+
168
+ for result in results:
169
+ if not isinstance(result, dict):
170
+ skipped_non_object += 1
171
+ continue
172
+
173
+ verified, reason_code = verify_one(result, args.min_quote_chars, transcript_cache)
174
+ if verified:
175
+ verified_count += 1
176
+ else:
177
+ reason_counts[reason_code] += 1
178
+
179
+ out_result = dict(result)
180
+ out_result["verified"] = verified
181
+ out_result["reason_code"] = reason_code
182
+ verified_results.append(out_result)
183
+
184
+ key = transcript_key(result)
185
+ bucket = by_transcript.setdefault(key, {"total": 0, "verified": 0, "unverified": 0})
186
+ bucket["total"] += 1
187
+ bucket["verified" if verified else "unverified"] += 1
188
+
189
+ total = len(verified_results)
190
+ stats = {
191
+ "total": total,
192
+ "verified": verified_count,
193
+ "unverified": total - verified_count,
194
+ "skipped_non_object_entries": skipped_non_object,
195
+ "by_reason_code": {code: reason_counts.get(code, 0) for code in REASON_CODES},
196
+ "by_transcript": by_transcript,
197
+ }
198
+
199
+ report = {
200
+ "generated_at": datetime.now(timezone.utc).isoformat(),
201
+ "min_quote_chars": args.min_quote_chars,
202
+ "results": verified_results,
203
+ "stats": stats,
204
+ }
205
+ out_path = out_dir / "results.verified.json"
206
+ out_path.write_text(json.dumps(report, indent=2))
207
+
208
+ print(f"results.verified.json: {out_path}")
209
+ print(f"total results: {stats['total']}")
210
+ print(f"verified: {stats['verified']}")
211
+ print(f"unverified: {stats['unverified']}")
212
+ for code in REASON_CODES:
213
+ count = stats["by_reason_code"][code]
214
+ if count:
215
+ print(f" {code}: {count}")
216
+ if skipped_non_object:
217
+ print(f"warning: skipped {skipped_non_object} non-object result entries", file=sys.stderr)
218
+
219
+
220
+ if __name__ == "__main__":
221
+ main()