@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,217 @@
1
+ #!/usr/bin/env python3
2
+ """Verify that a judge's cited quotes actually appear in the transcript they
3
+ claim to come from -- SKILL.md Step 4's mechanical check on findings.json.
4
+
5
+ A judge (scorers/adherence.md) is asked to copy `quote` verbatim from the
6
+ transcript it was shown. This script is the only thing that checks that
7
+ claim: it whitespace-folds both the quote and the transcript text and
8
+ requires the folded quote to be a literal substring of the folded
9
+ transcript. It is deliberately a literal-substring check, not a fuzzy
10
+ match -- a paraphrase must fail, and so must a quote that only differs in
11
+ case, because case is not whitespace.
12
+
13
+ Failure has four distinct reasons, and they are reported separately because
14
+ they mean different things to whoever reads findings.verified.json:
15
+
16
+ quote_missing the finding carries no usable quote text at all.
17
+ quote_too_short the quote is real but too short to prove anything
18
+ (a 3-character quote is trivially a substring).
19
+ not_found the transcript was read fine; the quote just is not
20
+ in it -- this is the judge confabulating.
21
+ transcript_unreadable the transcript_path is missing or could not be
22
+ read -- an input-data problem, not a confabulation,
23
+ so it must never be folded into the judge's error
24
+ rate.
25
+
26
+ Emits:
27
+ <out>/findings.verified.json - every input finding, in order, with every
28
+ original field preserved, plus `verified`
29
+ and `reason_code` (null when verified),
30
+ and a top-level `stats` block.
31
+
32
+ Python 3.9+, stdlib only. Reads only findings_json and the transcript_path
33
+ files it names; writes only under --out; makes no network call; never
34
+ modifies a transcript or the input findings file.
35
+ """
36
+
37
+ import argparse
38
+ import json
39
+ import re
40
+ import sys
41
+ from collections import Counter
42
+ from datetime import datetime, timezone
43
+ from pathlib import Path
44
+
45
+ WHITESPACE_RE = re.compile(r"\s+")
46
+
47
+ REASON_CODES = ("quote_missing", "quote_too_short", "not_found", "transcript_unreadable")
48
+
49
+
50
+ def fold_whitespace(text):
51
+ """Collapse every run of whitespace to a single space and strip ends.
52
+
53
+ Deliberately does NOT lowercase and does NOT strip punctuation -- this
54
+ must stay a literal-substring check, not a fuzzy match. A quote that
55
+ differs from the transcript only in case is a real verification
56
+ failure, not noise to smooth over.
57
+ """
58
+ return WHITESPACE_RE.sub(" ", text).strip()
59
+
60
+
61
+ def load_transcript_text(path):
62
+ """Read one transcript file. Returns None if it cannot be read.
63
+
64
+ A separate, patchable function so callers can count/observe real reads
65
+ even though results are cached per run -- see get_folded_transcript.
66
+ """
67
+ try:
68
+ return Path(path).expanduser().read_text(encoding="utf-8", errors="replace")
69
+ except OSError:
70
+ return None
71
+
72
+
73
+ def get_folded_transcript(path, cache):
74
+ """Return the whitespace-folded transcript at `path`, cached per run.
75
+
76
+ Many findings usually share a transcript_path; this makes sure each
77
+ distinct path is only read from disk once per run, regardless of how
78
+ many findings cite it. `cache` is owned by the caller (main), never a
79
+ module-level dict, so nothing leaks between separate script runs or
80
+ between tests.
81
+ """
82
+ if not isinstance(path, str) or not path:
83
+ return None
84
+ if path in cache:
85
+ return cache[path]
86
+ text = load_transcript_text(path)
87
+ folded = fold_whitespace(text) if text is not None else None
88
+ cache[path] = folded
89
+ return folded
90
+
91
+
92
+ def verify_one(finding, min_quote_chars, transcript_cache):
93
+ """Return (verified, reason_code) for one finding dict."""
94
+ quote_raw = finding.get("quote")
95
+ if not isinstance(quote_raw, str) or not quote_raw.strip():
96
+ return False, "quote_missing"
97
+
98
+ folded_quote = fold_whitespace(quote_raw)
99
+ if len(folded_quote) < min_quote_chars:
100
+ return False, "quote_too_short"
101
+
102
+ folded_transcript = get_folded_transcript(finding.get("transcript_path"), transcript_cache)
103
+ if folded_transcript is None:
104
+ return False, "transcript_unreadable"
105
+
106
+ if folded_quote in folded_transcript:
107
+ return True, None
108
+ return False, "not_found"
109
+
110
+
111
+ def transcript_key(finding):
112
+ path = finding.get("transcript_path")
113
+ return path if isinstance(path, str) and path else "(missing)"
114
+
115
+
116
+ def parse_args(argv=None):
117
+ p = argparse.ArgumentParser(description=__doc__)
118
+ p.add_argument("findings_json", help="path to a findings.json ({'findings': [...]})")
119
+ p.add_argument("--out", required=True)
120
+ p.add_argument("--min-quote-chars", type=int, default=12,
121
+ help="minimum folded-quote length required to count as evidence (default: 12)")
122
+ return p.parse_args(argv)
123
+
124
+
125
+ def load_findings_file(path):
126
+ """Load and structurally validate the input file. Raises ValueError on
127
+ any malformed shape -- caller turns that into a non-zero exit."""
128
+ try:
129
+ raw = path.read_text(encoding="utf-8")
130
+ except OSError as exc:
131
+ raise ValueError(f"could not read {path}: {exc}") from exc
132
+ try:
133
+ data = json.loads(raw)
134
+ except json.JSONDecodeError as exc:
135
+ raise ValueError(f"{path} is not valid JSON: {exc}") from exc
136
+ if not isinstance(data, dict) or not isinstance(data.get("findings"), list):
137
+ raise ValueError(f"{path} is not a valid findings file (expected {{'findings': [...]}})")
138
+ return data["findings"]
139
+
140
+
141
+ def main(argv=None):
142
+ args = parse_args(argv)
143
+ findings_path = Path(args.findings_json).expanduser()
144
+
145
+ try:
146
+ findings = load_findings_file(findings_path)
147
+ except ValueError as exc:
148
+ print(f"error: {exc}", file=sys.stderr)
149
+ sys.exit(1)
150
+
151
+ out_dir = Path(args.out).expanduser()
152
+ out_dir.mkdir(parents=True, exist_ok=True)
153
+
154
+ transcript_cache = {}
155
+ verified_findings = []
156
+ reason_counts = Counter()
157
+ by_transcript = {}
158
+ verified_count = 0
159
+ skipped_non_object = 0
160
+
161
+ for finding in findings:
162
+ if not isinstance(finding, dict):
163
+ skipped_non_object += 1
164
+ continue
165
+
166
+ verified, reason_code = verify_one(finding, args.min_quote_chars, transcript_cache)
167
+ if verified:
168
+ verified_count += 1
169
+ else:
170
+ reason_counts[reason_code] += 1
171
+
172
+ # dict(finding) carries every input field through untouched --
173
+ # including scorers/adherence.md's optional `trigger` field, which
174
+ # this script never verifies (only `quote` is checked mechanically).
175
+ out_finding = dict(finding)
176
+ out_finding["verified"] = verified
177
+ out_finding["reason_code"] = reason_code
178
+ verified_findings.append(out_finding)
179
+
180
+ key = transcript_key(finding)
181
+ bucket = by_transcript.setdefault(key, {"total": 0, "verified": 0, "unverified": 0})
182
+ bucket["total"] += 1
183
+ bucket["verified" if verified else "unverified"] += 1
184
+
185
+ total = len(verified_findings)
186
+ stats = {
187
+ "total": total,
188
+ "verified": verified_count,
189
+ "unverified": total - verified_count,
190
+ "skipped_non_object_entries": skipped_non_object,
191
+ "by_reason_code": {code: reason_counts.get(code, 0) for code in REASON_CODES},
192
+ "by_transcript": by_transcript,
193
+ }
194
+
195
+ report = {
196
+ "generated_at": datetime.now(timezone.utc).isoformat(),
197
+ "min_quote_chars": args.min_quote_chars,
198
+ "findings": verified_findings,
199
+ "stats": stats,
200
+ }
201
+ out_path = out_dir / "findings.verified.json"
202
+ out_path.write_text(json.dumps(report, indent=2))
203
+
204
+ print(f"findings.verified.json: {out_path}")
205
+ print(f"total findings: {stats['total']}")
206
+ print(f"verified: {stats['verified']}")
207
+ print(f"unverified: {stats['unverified']}")
208
+ for code in REASON_CODES:
209
+ count = stats["by_reason_code"][code]
210
+ if count:
211
+ print(f" {code}: {count}")
212
+ if skipped_non_object:
213
+ print(f"warning: skipped {skipped_non_object} non-object finding entries", file=sys.stderr)
214
+
215
+
216
+ if __name__ == "__main__":
217
+ main()
@@ -0,0 +1,27 @@
1
+ # doctor-md-skill
2
+
3
+ It grades your agent setup by scoring recent local agent conversations against efficiency and code-quality rubrics, then drafts concrete skill edits and renders one shareable HTML report. Everything runs locally; nothing is uploaded.
4
+
5
+ ## Design
6
+
7
+ - **Quote-anchored scoring.** A judge asked to "quote or closely paraphrase" the evidence for a finding can invent a quote, and nothing afterwards checks it. On a real run of the sibling skill (`doctor-md-agents`), 2 of 62 findings cited text that was never in the transcript, despite every judge claiming to have grep-verified its quotes. Both scorers here now require a verbatim, character-for-character `quote` alongside the reason, and `scripts/verify_citations.py` mechanically checks it — whitespace-folded, literal-substring, case-sensitive — before anything is aggregated. An unverifiable quote is discarded, not down-scored, and the discarded count is surfaced in the report.
8
+ - **Machine-session exclusion.** The collector already drops sidechains (which covers subagents), but not a top-level session some other tool launched against this same agent — an audit harness, a judge, another skill — whose "user" turn is a programmatically built prompt. On this machine, a real scan found 686 of 688 candidate "user" turns were exactly that: a single template, repeated verbatim across hundreds of sessions. `scripts/detect_machine_sessions.py` flags sessions by shape (an oversized first turn, a prefix repeated across several distinct sessions, or this skill's own `<!-- doctor-md-skill:self-session -->` sentinel) — never by naming a specific tool — and `SKILL.md` drops them before scoring.
9
+ - **Explicit invocation only.** This skill runs only when the user names it directly. `SKILL.md`'s frontmatter sets `disable-model-invocation: true` so the harness itself will not invoke it on inferred intent, and both the description and a body section near the top of `SKILL.md` restate the same rule for an agent that already loaded the skill. A question that merely sounds related — "is my agent setup any good?", "why doesn't skill X ever fire?" — is a question to answer, not a trigger to run a full grading pass.
10
+
11
+ ## Install
12
+
13
+ Symlink this directory into wherever your agent looks for skills, for example:
14
+
15
+ ```bash
16
+ ln -s /path/to/doctor-md-skill ~/.agents/skills/doctor-md-skill
17
+ ```
18
+
19
+ (or `~/.claude/skills/doctor-md-skill`, `~/.codex/skills/doctor-md-skill`, depending on the harness — see `references/supported-harnesses.md`).
20
+
21
+ ## Run the tests
22
+
23
+ Stdlib `unittest` only, Python 3.9+, no network access required:
24
+
25
+ ```bash
26
+ python3 -m unittest discover -s scripts -p 'test_*.py'
27
+ ```
@@ -0,0 +1,212 @@
1
+ ---
2
+ name: "doctor-md-skill"
3
+ description: "Runs only when the user asks for it by name. Grades agent skills by scoring agent conversations against efficiency and code-quality rubrics, using verbatim-quote anchored evidence, then drafts concrete skill edits and a shareable report. Never invoke it on inferred intent, on a related-sounding question — whether a skill is working, whether an agent setup is any good, why a skill never fires — or as a step inside another workflow — only when the user names this skill."
4
+ metadata:
5
+ author: "iseoane"
6
+ version: "1.0"
7
+ disable-model-invocation: true
8
+ ---
9
+ # doctor-md-skill
10
+
11
+ Grade the user's agent setup by scoring recent local agent conversations, then propose concrete skill edits and render one shareable report page.
12
+
13
+ The report can cover conversations in the current repository, conversations in selected projects, or all local conversations. It can evaluate project skills alone or project and global skills together.
14
+
15
+ Everything runs locally. Never upload transcripts, session files, or any excerpt of them anywhere. The only shareable artifact is the report the user chooses to post.
16
+
17
+ ## Explicit invocation only
18
+
19
+ Run this skill only when the user names it directly (for example "doctor-md-skill", "run doctor-md-skill", or an explicit slash/skill invocation). A question that merely sounds related — "is my agent setup any good?", "which of my skills actually work?", "why doesn't skill X ever fire?" — is a question to answer directly, not a request to run a full grading pass over hundreds of sessions. Do not run it as a step another workflow decided it needed on your own initiative. If a request sounds like a fit but did not name this skill, ask the user whether they want to run it rather than starting it. This applies even after this file is already loaded into context: loading it is not the same as being asked to run it. `disable-model-invocation: true` above stops the harness from invoking this skill on inferred intent; this section is the reminder for an agent that has already loaded it.
20
+
21
+ Let `SKILL_ROOT` be the directory containing this SKILL.md.
22
+
23
+ ## Step 0: Start the run
24
+
25
+ ### Verify the executing harness
26
+
27
+ Read `$SKILL_ROOT/references/supported-harnesses.md` and identify the harness executing this skill from the runtime context. If it is unsupported or cannot be identified confidently, follow the reference's stop behavior. Do not create a report directory or read conversation history.
28
+
29
+ ### Ask which conversations to grade
30
+
31
+ First check whether the current directory is inside a git repository:
32
+
33
+ ```bash
34
+ git rev-parse --show-toplevel
35
+ ```
36
+
37
+ Use the harness's user-question tool when available.
38
+
39
+ When a current repository is available, ask **“Which conversations should I grade?”** with:
40
+
41
+ 1. **Conversations in this repository** — recommended.
42
+ 2. **All conversations**.
43
+ 3. **Choose projects to analyze**.
44
+
45
+ When there is no current repository, ask the same question with:
46
+
47
+ 1. **All conversations** — recommended.
48
+ 2. **Choose projects to analyze**.
49
+
50
+ If the user chooses projects, ask for one or more project paths. Expand and validate every path as a git repository before continuing. The run produces one combined report across those projects.
51
+
52
+ ### Ask which skills to evaluate
53
+
54
+ Then ask **“Which skills should I evaluate?”** with:
55
+
56
+ 1. **Project skills + global skills** — recommended.
57
+ 2. **Project skills only**.
58
+
59
+ For an all-conversations run, “Project skills” means skills from local git repositories inferred from the conversations' working directories. After these answers, proceed immediately.
60
+
61
+ Never write artifacts into the user's repo. Create one fresh, collision-free scratch directory per run and use it as `REPORT_DIR` for every artifact:
62
+
63
+ ```bash
64
+ REPORT_DIR="$(mktemp -d "${TMPDIR:-/tmp}/doctor-md-skill-XXXXXXXX")"
65
+ ```
66
+
67
+ ## Step 1: Collect
68
+ Build the collector arguments from the startup answers:
69
+
70
+ - Current repository: `--repo "$REPO"`.
71
+ - Selected projects: repeat `--repo PATH` for every project.
72
+ - All conversations: `--all-conversations`.
73
+ - Project and global skills: add `--include-global-skills`.
74
+ - Project skills only: do not add `--include-global-skills`.
75
+
76
+ ```bash
77
+ python3 "$SKILL_ROOT/scripts/collect_sessions.py" \
78
+ --out "$REPORT_DIR" \
79
+ <conversation-scope arguments> \
80
+ <skill-scope arguments>
81
+ ```
82
+
83
+ By default `--harness auto` scans every locally available supported source. Read `$SKILL_ROOT/references/supported-harnesses.md` for source identifiers, storage details, skill locations, and source-specific override flags.
84
+
85
+ Useful flags:
86
+
87
+ - `--harness VALUE` — which local session sources to scan; use the reference's collector IDs.
88
+ - `--repo PATH` — include a project; repeatable.
89
+ - `--all-conversations` — do not filter conversations by project.
90
+ - `--include-global-skills` — also grade global skills.
91
+ - `--days N` — lookback window (default 45).
92
+ - `--max-sessions N` — cap on sampled sessions (default 12).
93
+ - `--skills-dir PATH` — nonstandard skill locations.
94
+ - `--include-subagents` — include child or sidechain sessions.
95
+
96
+ Read `$REPORT_DIR/inventory.json`. If `sessions_sampled` is 0, tell the user there is nothing recent to score in the selected conversation scope (suggest raising `--days` or choosing different projects) and stop. If `skills_found` is 0, continue — the report becomes a case for creating skills, and `skill_coverage` is 0.
97
+
98
+ ## Step 2: Exclude machine-driven sessions
99
+
100
+ Not every session in local history was a human talking to an agent. Sub-agents are already excluded by the collector (it drops sidechains by default), but a top-level session launched by other tooling — an audit harness, a judge, another skill — still looks like an ordinary session, and its "user" turn is a programmatically built prompt, not something a person typed. Left in the sample, these drag efficiency and code-quality grades around for work a human never did.
101
+
102
+ ```bash
103
+ python3 "$SKILL_ROOT/scripts/detect_machine_sessions.py" \
104
+ --out "$REPORT_DIR" \
105
+ --days <window_days from inventory.json>
106
+ ```
107
+
108
+ This scans the same raw session logs the collector reads and flags session ids (format `<harness>:<session-id>`, matching the collector's own sampling key) whose first user turn is unusually long, whose normalized opening repeats across several distinct sessions (a template), or which carries this skill's own self-session sentinel (see Step 3). It writes `$REPORT_DIR/machine_sessions.json`.
109
+
110
+ Read `machine_sessions.json`. Drop every sampled session in `inventory.json` whose key appears in `flagged_sessions` from the set to be scored in Step 3 — do not delete its transcript file, just exclude it from scoring. Record `sessions_dropped_as_machine` = the number dropped this way. If dropped sessions outnumber the sessions that remain to be scored, say so plainly in the findings: the surviving corpus is mostly machine-driven and the grades rest on very little.
111
+
112
+ ## Step 3: Score each sampled transcript
113
+
114
+ Scoring is based on efficiency and code quality for the sessions sampled, minus whatever Step 2 dropped. Process datasets of 50 transcripts or fewer in a single batch. For datasets with more than 50 transcripts, use parallel batches (20 transcripts per batch recommended). Score batches in the current local agent process, or delegate only to local child agents that keep transcript contents on the user's machine. Pass the following rubrics as context:
115
+
116
+ - `$SKILL_ROOT/scorers/efficiency.md`
117
+ - `$SKILL_ROOT/scorers/code-quality.md`
118
+
119
+ Every prompt sent to a judge — including a delegated child agent's — must start with the self-session sentinel line, on its own line, so any future run of this skill recognizes its own scoring prompts in the logs and never grades itself:
120
+
121
+ ```
122
+ <!-- doctor-md-skill:self-session -->
123
+ ```
124
+
125
+ Instructions: For each remaining transcript in `$REPORT_DIR/transcripts/`, read it and judge it against both rubrics. For each scorer record: `session_id`, `transcript_path` (absolute path to the transcript file under `$REPORT_DIR/transcripts/`), `scorer` (`efficiency` or `code-quality`), `label`, numeric `score` (from the rubric's label table), a 1–3 sentence `reason` citing specifics from the transcript, and `quote` — a verbatim excerpt copied character-for-character from the transcript, per each rubric's Quote section. Apply the code-quality scorer only where the transcript shows code changes; otherwise record `insufficient_evidence` with an empty `quote` and exclude that result from the code-quality average and failed-conversation filter.
126
+
127
+ Write every result to `$REPORT_DIR/results.json` as `{"results": [ {...}, ... ]}`.
128
+
129
+ ## Step 4: Verify quotes
130
+
131
+ A scorer citing a quote is not the same as the quote actually being there — on a real run, judges have claimed to have grep-verified a quote that was not in the transcript. This step is the mechanical check on that claim, and only what survives it is used for anything downstream.
132
+
133
+ ```bash
134
+ python3 "$SKILL_ROOT/scripts/verify_citations.py" "$REPORT_DIR/results.json" --out "$REPORT_DIR"
135
+ ```
136
+
137
+ This writes `$REPORT_DIR/results.verified.json`: every result from Step 3, unchanged, plus `verified` (true/false) and `reason_code` (`null` when verified, else `quote_missing`, `quote_too_short`, `not_found`, or `transcript_unreadable`). Read its `stats.verified` and `stats.unverified`. **Only `verified: true` results are aggregated in Step 5** — an unverified result is not scored down, it is dropped entirely, and its count is surfaced in the report instead. An `insufficient_evidence` result (empty quote by design) is exempt from this filter and still counts in Step 5 as before.
138
+
139
+ If `stats.unverified` is a large share of `stats.total`, say so plainly in the findings: a scoring run with many unverified quotes is a broken run, and if every result for a given scorer was unverified, that scorer has no case this run.
140
+
141
+ ## Step 5: Aggregate
142
+
143
+ - `raw_efficiency` = mean of efficiency scores across all **verified** sessions (plus `insufficient_evidence` sessions, which are exempt from verification).
144
+ - `raw_code_quality` = mean of code-quality scores, excluding `insufficient_evidence`, across all **verified** sessions. If no session had enough evidence, set it to 0.5 and say so in the findings.
145
+ - Curve qualitative rubric means into letter-grade report scores with `curve(score) = 0.5 + 0.5 * score`.
146
+ - `efficiency = curve(raw_efficiency)`.
147
+ - `code_quality = curve(raw_code_quality)`.
148
+ - `skill_coverage` = fraction of sampled (post-Step-2) sessions where at least one installed skill was detected. If `skills_found` is 0, coverage is 0.
149
+ - `overall = 0.5 * efficiency + 0.35 * code_quality + 0.15 * skill_coverage.`
150
+
151
+ Then, define `failed_conversations` from each conversation's raw, uncurved, **verified** scorer results. A conversation fails when at least one applicable, verified efficiency or code-quality score is below `0.5`. An `insufficient_evidence` result does not make a conversation fail. Use only `failed_conversations` as evidence for skill-improvement suggestions and draft skill edits.
152
+
153
+ Then derive the substance:
154
+
155
+ - `top_findings`: the 3 most impactful, specific patterns across sessions. These lead the report and the spoken summary. Make each summary concrete and concise, following the STE-100 standard.
156
+ - `suggestions`: concrete skill changes, if any. Each names a skill (existing or proposed-new) and a specific change: a trigger-description fix so it fires when it should, a missing step or check, a command to encode, a new skill to create. Suggestions must trace back to observed waste or defects in `failed_conversations`, not generic best practices — cite the failed session, scorer, and moment that motivated each one. An installed skill that never triggered in a failed conversation is usually a description problem and worth a suggestion of its own.
157
+
158
+ ## Step 6: Draft skill edits
159
+
160
+ Follow `$SKILL_ROOT/references/skill-improvements.md` to propose improvements to project skills based only on `failed_conversations`.
161
+
162
+ 1. Read the skill's current file (path is in `inventory.json`).
163
+ 2. Write the full improved version to `$REPORT_DIR/proposed/<skill-name>/SKILL.md`, changing only what the evidence justifies. Improve the parts the sessions actually exercised: the trigger description that failed to fire, the missing preflight check, the step the agent had to figure out by trial and error.
164
+ 3. Produce a unified diff between current and proposed (`diff -u <current> <proposed>`) and put it in the suggestion's `diff` field so it renders in the report.
165
+
166
+ For a proposed-new skill, write the complete new SKILL.md to the same `proposed/` directory and set `diff` to its full content as an addition.
167
+
168
+ Do not modify the user's real skill files in this step.
169
+
170
+ ## Step 7: Write report.json and render
171
+ Write `$REPORT_DIR/report.json`. Store the curved `efficiency` and `code_quality` values, literal `skill_coverage`, and weighted `overall` in `scores`; do not store the raw rubric means there. `unverified_quotes` is `results.verified.json`'s `stats.unverified`; `sessions_dropped_as_machine` is Step 2's count.
172
+
173
+ ```json
174
+ {
175
+ "title": "Agent Skill Report",
176
+ "generated_at": "<ISO timestamp>",
177
+ "harness": "<harness from inventory.json>",
178
+ "handle": "<repo_name from inventory.json>",
179
+ "stats": {
180
+ "sessions_analyzed": 0, "sessions_scanned": 0,
181
+ "skills_found": 0, "skills_used": 0, "window_days": 45,
182
+ "sessions_dropped_as_machine": 0, "unverified_quotes": 0
183
+ },
184
+ "scores": {"efficiency": 0.0, "code_quality": 0.0, "skill_coverage": 0.0, "overall": 0.0},
185
+ "top_findings": ["", "", ""],
186
+ "suggestions": [
187
+ {
188
+ "skill": "",
189
+ "change": "<one-sentence summary of the edit>",
190
+ "evidence": "<which session(s) and what happened that motivates this>",
191
+ "proposed_path": "<path under proposed/, if an edit was drafted>",
192
+ "diff": "<unified diff, or full content for a new skill>"
193
+ }
194
+ ]
195
+ }
196
+ ```
197
+
198
+ ```bash
199
+ python3 "$SKILL_ROOT/scripts/render_report.py" "$REPORT_DIR/report.json" --open
200
+ ```
201
+
202
+ This writes a single self-contained `$REPORT_DIR/report.html` and attempts to open it in the default browser. The scorecard, findings, and suggested skill edits appear on one page. Long diffs are collapsed behind a "show more" toggle, and a "share as png" button exports a 1200x675 share image locally. There is no separate card file to open or screenshot.
203
+
204
+ ## Step 8: Output
205
+
206
+ Tell the user the grade and the three findings, in text.
207
+
208
+ Finish every response with this exact summary, substituting the absolute `REPORT_DIR` path:
209
+
210
+ - Your agent skill report: file://$REPORT_DIR/report.html
211
+
212
+ Want me to apply these suggestions to your skills?