@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.
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +465 -0
- package/package.json +69 -0
- package/skills/doctor-md-agents/README.md +51 -0
- package/skills/doctor-md-agents/SKILL.md +275 -0
- package/skills/doctor-md-agents/references/rule-model.md +56 -0
- package/skills/doctor-md-agents/references/structure.md +89 -0
- package/skills/doctor-md-agents/scorers/adherence.md +73 -0
- package/skills/doctor-md-agents/scorers/rule-value.md +49 -0
- package/skills/doctor-md-agents/scripts/collect_sessions.py +1135 -0
- package/skills/doctor-md-agents/scripts/extract_rules.py +704 -0
- package/skills/doctor-md-agents/scripts/render_report.py +644 -0
- package/skills/doctor-md-agents/scripts/scan_corrections.py +781 -0
- package/skills/doctor-md-agents/scripts/test_collect_sessions.py +780 -0
- package/skills/doctor-md-agents/scripts/test_extract_rules.py +530 -0
- package/skills/doctor-md-agents/scripts/test_render_report.py +383 -0
- package/skills/doctor-md-agents/scripts/test_scan_corrections.py +343 -0
- package/skills/doctor-md-agents/scripts/test_verify_citations.py +236 -0
- package/skills/doctor-md-agents/scripts/testdata/old_extract_rules_pre_t1.py.txt +507 -0
- package/skills/doctor-md-agents/scripts/verify_citations.py +217 -0
- package/skills/doctor-md-skill/README.md +27 -0
- package/skills/doctor-md-skill/SKILL.md +212 -0
- package/skills/doctor-md-skill/assets/pierre-diffs.js +1531 -0
- package/skills/doctor-md-skill/references/skill-improvements.md +32 -0
- package/skills/doctor-md-skill/references/supported-harnesses.md +44 -0
- package/skills/doctor-md-skill/scorers/code-quality.md +41 -0
- package/skills/doctor-md-skill/scorers/efficiency.md +38 -0
- package/skills/doctor-md-skill/scripts/collect_sessions.py +1112 -0
- package/skills/doctor-md-skill/scripts/detect_machine_sessions.py +315 -0
- package/skills/doctor-md-skill/scripts/render_report.py +545 -0
- package/skills/doctor-md-skill/scripts/test_collect_sessions.py +593 -0
- package/skills/doctor-md-skill/scripts/test_detect_machine_sessions.py +181 -0
- package/skills/doctor-md-skill/scripts/test_render_report.py +241 -0
- package/skills/doctor-md-skill/scripts/test_verify_citations.py +196 -0
- package/skills/doctor-md-skill/scripts/verify_citations.py +221 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Flag local session logs whose "user" turns look machine-generated rather
|
|
3
|
+
than typed by a human -- SKILL.md runs this between collection and scoring
|
|
4
|
+
so this skill does not grade a machine talking to itself.
|
|
5
|
+
|
|
6
|
+
`collect_sessions.py` already excludes sidechains by default, which covers
|
|
7
|
+
subagents. It does not cover top-level sessions launched by other tooling --
|
|
8
|
+
audit harnesses, judges, other skills -- whose "user" turn is a
|
|
9
|
+
programmatically built prompt. On a real corpus these can dominate: one scan
|
|
10
|
+
found 686 of 688 candidate user turns were such templates, median 4024
|
|
11
|
+
characters, repeated verbatim across hundreds of sessions. Grading those for
|
|
12
|
+
efficiency and code quality measures a machine talking to itself and
|
|
13
|
+
silently drags the user's grades around.
|
|
14
|
+
|
|
15
|
+
Three filters flag a session by the SHAPE of its content, never by naming a
|
|
16
|
+
specific tool or skill, so this script stays independent of whatever else
|
|
17
|
+
runs on this machine:
|
|
18
|
+
|
|
19
|
+
1. --max-turn-chars the session's first user turn is longer than this;
|
|
20
|
+
a competent human rarely opens with more (default
|
|
21
|
+
1200 chars).
|
|
22
|
+
2. --template-threshold a normalized prefix of a user turn (any user
|
|
23
|
+
turn in the session, not only the first) that
|
|
24
|
+
repeats across this many or more DISTINCT
|
|
25
|
+
sessions is a template, not a human typing fresh
|
|
26
|
+
words each time -- every session containing that
|
|
27
|
+
prefix is flagged. The grouping key is only the
|
|
28
|
+
first --template-prefix-chars of the folded text
|
|
29
|
+
(default 160): a template's identity is its
|
|
30
|
+
opening, not whatever variable payload follows
|
|
31
|
+
it. The threshold counts DISTINCT SESSIONS, so the
|
|
32
|
+
same text repeated several times inside one
|
|
33
|
+
session is not, by itself, a template.
|
|
34
|
+
3. self-session sentinel a turn containing MACHINE_SESSION_SENTINEL is
|
|
35
|
+
this skill's own scoring prompt sent to a judge
|
|
36
|
+
(see SKILL.md Step 2), not a human talking to an
|
|
37
|
+
agent.
|
|
38
|
+
|
|
39
|
+
Reuses the session-file discovery and parsing already implemented in
|
|
40
|
+
collect_sessions.py (Claude Code, Codex, Pi, Grok Build, ZCode) instead of
|
|
41
|
+
re-implementing five JSONL readers -- this scans the exact same raw logs
|
|
42
|
+
the collector reads.
|
|
43
|
+
|
|
44
|
+
Emits:
|
|
45
|
+
<out>/machine_sessions.json - every flagged session id (format
|
|
46
|
+
"<harness>:<session-id>", matching the
|
|
47
|
+
collector's own sampling key) with its
|
|
48
|
+
reasons and evidence, plus stats and
|
|
49
|
+
template groups for a human to audit.
|
|
50
|
+
|
|
51
|
+
Python 3.9+, stdlib only. Reads only local session logs, writes only under
|
|
52
|
+
--out, makes no network call, and never modifies a session file.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
import argparse
|
|
56
|
+
import json
|
|
57
|
+
import os
|
|
58
|
+
import re
|
|
59
|
+
import sys
|
|
60
|
+
from datetime import datetime, timedelta, timezone
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
|
|
63
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
64
|
+
|
|
65
|
+
from collect_sessions import ( # noqa: E402
|
|
66
|
+
find_claude_session_files, parse_claude_session,
|
|
67
|
+
find_codex_session_files, parse_codex_session,
|
|
68
|
+
find_pi_session_files, parse_pi_session,
|
|
69
|
+
find_grok_session_files, parse_grok_session,
|
|
70
|
+
find_zcode_session_files, parse_zcode_session,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# A turn carrying this marker is this skill's own scoring prompt sent to a
|
|
74
|
+
# judge (see SKILL.md Step 2), not a human turn. SKILL.md prepends this
|
|
75
|
+
# sentinel, on its own line, to every prompt this skill sends to a judge --
|
|
76
|
+
# including a delegated child agent's -- so any future run recognizes its
|
|
77
|
+
# own prior-run prompts in the logs and refuses to score them as if a human
|
|
78
|
+
# had written them.
|
|
79
|
+
MACHINE_SESSION_SENTINEL = "<!-- doctor-md-skill:self-session -->"
|
|
80
|
+
|
|
81
|
+
# Default grouping-key length for template detection (overridable via
|
|
82
|
+
# --template-prefix-chars). Deliberately short: a template's identity is
|
|
83
|
+
# its opening, not whatever variable payload (an id, a JSON blob) follows
|
|
84
|
+
# it -- a key long enough to reach into that payload makes every instance
|
|
85
|
+
# hash differently and defeats the filter on exactly the turns it exists
|
|
86
|
+
# to catch.
|
|
87
|
+
TEMPLATE_PREFIX_CHARS = 160
|
|
88
|
+
TEMPLATE_REPORT_PREFIX_CHARS = 160
|
|
89
|
+
|
|
90
|
+
DEFAULT_MAX_TURN_CHARS = 1200
|
|
91
|
+
DEFAULT_TEMPLATE_THRESHOLD = 3
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def normalize_prefix(text, length=TEMPLATE_PREFIX_CHARS):
|
|
95
|
+
"""Fold whitespace and take the first `length` chars -- the key used to
|
|
96
|
+
group repeated-template candidates regardless of the surrounding
|
|
97
|
+
whitespace or which session wrote them."""
|
|
98
|
+
return re.sub(r"\s+", " ", text).strip()[:length]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def iter_session_user_turns(entries):
|
|
102
|
+
"""Yield the text of every ("user", text) entry, in order."""
|
|
103
|
+
for role, text in entries:
|
|
104
|
+
if role == "user" and text:
|
|
105
|
+
yield text
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def session_contains_sentinel(entries):
|
|
109
|
+
return any(MACHINE_SESSION_SENTINEL in text for _role, text in entries if text)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
HARNESS_SOURCES = {
|
|
113
|
+
"claude": (find_claude_session_files, parse_claude_session, "claude_home"),
|
|
114
|
+
"codex": (find_codex_session_files, parse_codex_session, "codex_home"),
|
|
115
|
+
"pi": (find_pi_session_files, parse_pi_session, "pi_home"),
|
|
116
|
+
"grok": (find_grok_session_files, parse_grok_session, "grok_home"),
|
|
117
|
+
"zcode": (find_zcode_session_files, parse_zcode_session, "zcode_home"),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def collect_candidates(homes, cutoff, include_subagents=False):
|
|
122
|
+
"""Walk every configured harness home and return a list of per-session
|
|
123
|
+
records: {session_key, harness, first_user_turn, user_turns,
|
|
124
|
+
has_sentinel}. `homes` maps harness name -> Path (or None to skip)."""
|
|
125
|
+
records = []
|
|
126
|
+
for harness, (find_files, parse_session, home_key) in HARNESS_SOURCES.items():
|
|
127
|
+
home = homes.get(home_key)
|
|
128
|
+
if home is None:
|
|
129
|
+
continue
|
|
130
|
+
try:
|
|
131
|
+
# Only the Claude Code finder also walks sidechain files; every
|
|
132
|
+
# other finder takes just (home, cutoff).
|
|
133
|
+
if harness == "claude":
|
|
134
|
+
files = find_files(home, cutoff, include_subagents)
|
|
135
|
+
else:
|
|
136
|
+
files = find_files(home, cutoff)
|
|
137
|
+
except OSError:
|
|
138
|
+
continue
|
|
139
|
+
for _mtime, path in files:
|
|
140
|
+
parsed = parse_session(path, set(), include_subagents)
|
|
141
|
+
if parsed is None:
|
|
142
|
+
continue
|
|
143
|
+
meta, _stats, entries, _skills_used = parsed
|
|
144
|
+
user_turns = list(iter_session_user_turns(entries))
|
|
145
|
+
records.append({
|
|
146
|
+
"session_key": f"{harness}:{meta.get('id')}",
|
|
147
|
+
"harness": harness,
|
|
148
|
+
"file": str(path),
|
|
149
|
+
"user_turns": user_turns,
|
|
150
|
+
"first_user_turn": user_turns[0] if user_turns else None,
|
|
151
|
+
"has_sentinel": session_contains_sentinel(entries),
|
|
152
|
+
})
|
|
153
|
+
return records
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def flag_sessions(records, max_turn_chars, template_threshold, template_prefix_chars):
|
|
157
|
+
"""Apply the three filters and return (flagged, dropped_templates,
|
|
158
|
+
stats). `flagged` is a list of {session_key, harness, reasons,
|
|
159
|
+
evidence} sorted by session_key."""
|
|
160
|
+
# Pass 1: collect every (session_key, normalized prefix) pair across
|
|
161
|
+
# every user turn in every session -- not only the first turn, so a
|
|
162
|
+
# template that shows up mid-session is still caught, and so the same
|
|
163
|
+
# text repeated inside ONE session collapses to one entry per prefix
|
|
164
|
+
# (a set, not a count) before the distinct-session threshold is applied.
|
|
165
|
+
prefix_sessions = {} # normalized prefix -> set of session_key
|
|
166
|
+
prefix_occurrences = {} # normalized prefix -> total turn occurrences
|
|
167
|
+
for record in records:
|
|
168
|
+
seen_prefixes_this_session = set()
|
|
169
|
+
for turn in record["user_turns"]:
|
|
170
|
+
prefix = normalize_prefix(turn, length=template_prefix_chars)
|
|
171
|
+
if not prefix:
|
|
172
|
+
continue
|
|
173
|
+
prefix_occurrences[prefix] = prefix_occurrences.get(prefix, 0) + 1
|
|
174
|
+
if prefix not in seen_prefixes_this_session:
|
|
175
|
+
seen_prefixes_this_session.add(prefix)
|
|
176
|
+
prefix_sessions.setdefault(prefix, set()).add(record["session_key"])
|
|
177
|
+
|
|
178
|
+
templated_prefixes = {
|
|
179
|
+
prefix for prefix, sessions in prefix_sessions.items()
|
|
180
|
+
if len(sessions) >= template_threshold
|
|
181
|
+
}
|
|
182
|
+
dropped_templates = [
|
|
183
|
+
{
|
|
184
|
+
"prefix": prefix[:TEMPLATE_REPORT_PREFIX_CHARS],
|
|
185
|
+
"sessions": len(prefix_sessions[prefix]),
|
|
186
|
+
"occurrences": prefix_occurrences[prefix],
|
|
187
|
+
}
|
|
188
|
+
for prefix in templated_prefixes
|
|
189
|
+
]
|
|
190
|
+
dropped_templates.sort(key=lambda d: (-d["sessions"], -d["occurrences"]))
|
|
191
|
+
|
|
192
|
+
flagged = []
|
|
193
|
+
long_first_turn_count = 0
|
|
194
|
+
templated_count = 0
|
|
195
|
+
sentinel_count = 0
|
|
196
|
+
for record in records:
|
|
197
|
+
reasons = []
|
|
198
|
+
evidence = {}
|
|
199
|
+
|
|
200
|
+
first_turn = record["first_user_turn"]
|
|
201
|
+
if first_turn is not None and len(first_turn) > max_turn_chars:
|
|
202
|
+
reasons.append("long_first_turn")
|
|
203
|
+
evidence["first_turn_chars"] = len(first_turn)
|
|
204
|
+
long_first_turn_count += 1
|
|
205
|
+
|
|
206
|
+
session_prefixes = {
|
|
207
|
+
normalize_prefix(turn, length=template_prefix_chars)
|
|
208
|
+
for turn in record["user_turns"]
|
|
209
|
+
}
|
|
210
|
+
matched = session_prefixes & templated_prefixes
|
|
211
|
+
if matched:
|
|
212
|
+
reasons.append("templated")
|
|
213
|
+
sample_prefix = sorted(matched)[0]
|
|
214
|
+
evidence["template_prefix"] = sample_prefix[:TEMPLATE_REPORT_PREFIX_CHARS]
|
|
215
|
+
evidence["template_sessions"] = len(prefix_sessions[sample_prefix])
|
|
216
|
+
templated_count += 1
|
|
217
|
+
|
|
218
|
+
if record["has_sentinel"]:
|
|
219
|
+
reasons.append("self_session_sentinel")
|
|
220
|
+
sentinel_count += 1
|
|
221
|
+
|
|
222
|
+
if reasons:
|
|
223
|
+
flagged.append({
|
|
224
|
+
"session_key": record["session_key"],
|
|
225
|
+
"harness": record["harness"],
|
|
226
|
+
"reasons": reasons,
|
|
227
|
+
"evidence": evidence,
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
flagged.sort(key=lambda f: f["session_key"])
|
|
231
|
+
|
|
232
|
+
stats = {
|
|
233
|
+
"sessions_scanned": len(records),
|
|
234
|
+
"sessions_flagged": len(flagged),
|
|
235
|
+
"flagged_long_first_turn": long_first_turn_count,
|
|
236
|
+
"flagged_templated": templated_count,
|
|
237
|
+
"flagged_self_session_sentinel": sentinel_count,
|
|
238
|
+
"template_groups": len(dropped_templates),
|
|
239
|
+
}
|
|
240
|
+
return flagged, dropped_templates, stats
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def parse_args(argv=None):
|
|
244
|
+
p = argparse.ArgumentParser(description=__doc__)
|
|
245
|
+
p.add_argument("--out", required=True)
|
|
246
|
+
p.add_argument("--days", type=int, default=45, help="lookback window (default: 45)")
|
|
247
|
+
p.add_argument(
|
|
248
|
+
"--claude-home",
|
|
249
|
+
default=os.environ.get("CLAUDE_CONFIG_DIR", "~/.claude"),
|
|
250
|
+
help="Claude Code config directory (default: CLAUDE_CONFIG_DIR or ~/.claude)",
|
|
251
|
+
)
|
|
252
|
+
p.add_argument("--codex-home", default=os.environ.get("CODEX_HOME", "~/.codex"))
|
|
253
|
+
p.add_argument("--pi-home", default="~/.pi/agent")
|
|
254
|
+
p.add_argument("--grok-home", default="~/.grok")
|
|
255
|
+
p.add_argument("--zcode-home", default="~/.zcode")
|
|
256
|
+
p.add_argument("--include-subagents", action="store_true")
|
|
257
|
+
p.add_argument("--max-turn-chars", type=int, default=DEFAULT_MAX_TURN_CHARS,
|
|
258
|
+
help=f"a first user turn longer than this is flagged (default: {DEFAULT_MAX_TURN_CHARS})")
|
|
259
|
+
p.add_argument("--template-threshold", type=int, default=DEFAULT_TEMPLATE_THRESHOLD,
|
|
260
|
+
help="flag a user turn as a machine template when its normalized prefix "
|
|
261
|
+
f"repeats across this many or more distinct sessions (default: {DEFAULT_TEMPLATE_THRESHOLD})")
|
|
262
|
+
p.add_argument("--template-prefix-chars", type=int, default=TEMPLATE_PREFIX_CHARS,
|
|
263
|
+
help=f"chars of whitespace-folded text used as the template-matching key (default: {TEMPLATE_PREFIX_CHARS})")
|
|
264
|
+
return p.parse_args(argv)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def main(argv=None):
|
|
268
|
+
args = parse_args(argv)
|
|
269
|
+
out_dir = Path(args.out).expanduser()
|
|
270
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
271
|
+
|
|
272
|
+
homes = {
|
|
273
|
+
"claude_home": Path(args.claude_home).expanduser(),
|
|
274
|
+
"codex_home": Path(args.codex_home).expanduser(),
|
|
275
|
+
"pi_home": Path(args.pi_home).expanduser(),
|
|
276
|
+
"grok_home": Path(args.grok_home).expanduser(),
|
|
277
|
+
"zcode_home": Path(args.zcode_home).expanduser(),
|
|
278
|
+
}
|
|
279
|
+
cutoff = datetime.now(timezone.utc) - timedelta(days=args.days)
|
|
280
|
+
|
|
281
|
+
records = collect_candidates(homes, cutoff, args.include_subagents)
|
|
282
|
+
flagged, dropped_templates, stats = flag_sessions(
|
|
283
|
+
records, args.max_turn_chars, args.template_threshold, args.template_prefix_chars,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
report = {
|
|
287
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
288
|
+
"window_days": args.days,
|
|
289
|
+
"max_turn_chars": args.max_turn_chars,
|
|
290
|
+
"template_threshold": args.template_threshold,
|
|
291
|
+
"template_prefix_chars": args.template_prefix_chars,
|
|
292
|
+
"sentinel": MACHINE_SESSION_SENTINEL,
|
|
293
|
+
"stats": stats,
|
|
294
|
+
"flagged_sessions": flagged,
|
|
295
|
+
"dropped_templates": dropped_templates,
|
|
296
|
+
}
|
|
297
|
+
out_path = out_dir / "machine_sessions.json"
|
|
298
|
+
out_path.write_text(json.dumps(report, indent=2))
|
|
299
|
+
|
|
300
|
+
print(f"machine_sessions.json: {out_path}")
|
|
301
|
+
print(f"sessions scanned: {stats['sessions_scanned']}")
|
|
302
|
+
print(f"sessions flagged: {stats['sessions_flagged']}")
|
|
303
|
+
print(f" long first turn: {stats['flagged_long_first_turn']}")
|
|
304
|
+
print(f" templated: {stats['flagged_templated']} ({stats['template_groups']} template group(s))")
|
|
305
|
+
print(f" self-session: {stats['flagged_self_session_sentinel']}")
|
|
306
|
+
if stats["sessions_scanned"] and stats["sessions_flagged"] > stats["sessions_scanned"] - stats["sessions_flagged"]:
|
|
307
|
+
print(
|
|
308
|
+
"warning: flagged sessions outnumber kept sessions -- this corpus is mostly "
|
|
309
|
+
"machine-driven and any surviving grades rest on very little",
|
|
310
|
+
file=sys.stderr,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
if __name__ == "__main__":
|
|
315
|
+
main()
|