@nurix/etna 0.15.6 → 0.15.8
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/bin/etna.js +64 -13
- package/data/harness-index.json +7 -5
- package/data/harnesses/code/CLAUDE.md +3 -7
- package/data/harnesses/code/agents/harness-manager.md +4 -4
- package/data/harnesses/code/agents/maintenance.md +1 -1
- package/data/harnesses/code/harness.json +4 -3
- package/data/harnesses/code/hooks/hooks.json +3 -17
- package/data/harnesses/code/hooks/prompt-recall.js +157 -0
- package/data/harnesses/code/hooks/session-reflect.js +283 -0
- package/data/harnesses/code/skills/distiller/SKILL.md +62 -60
- package/data/harnesses/code/skills/distiller/references/docs-mode.md +21 -21
- package/data/harnesses/code/skills/distiller/references/method.md +41 -41
- package/data/harnesses/code/skills/documentation/references/templates.md +20 -27
- package/data/harnesses/code-fable/CLAUDE.md +138 -0
- package/data/harnesses/code-fable/agents/brainstorm.md +35 -0
- package/data/harnesses/code-fable/agents/change-review.md +249 -0
- package/data/harnesses/code-fable/agents/harness-manager.md +193 -0
- package/data/harnesses/code-fable/agents/maintenance.md +123 -0
- package/data/harnesses/code-fable/agents/performance-review.md +218 -0
- package/data/harnesses/code-fable/agents/spec-mode.md +56 -0
- package/data/harnesses/code-fable/harness.json +7 -0
- package/data/harnesses/code-fable/hooks/hooks.json +27 -0
- package/data/harnesses/code-fable/hooks/lib/redact.js +37 -0
- package/data/harnesses/code-fable/hooks/prompt-recall.js +157 -0
- package/data/harnesses/code-fable/hooks/session-reflect.js +283 -0
- package/data/harnesses/code-fable/rules/general.md +96 -0
- package/data/harnesses/code-fable/rules/nomenclature.md +34 -0
- package/data/harnesses/code-fable/skills/boundary/SKILL.md +262 -0
- package/data/harnesses/code-fable/skills/boundary/references/boundary-taxonomy.md +734 -0
- package/data/harnesses/code-fable/skills/brainstorm/README.md +136 -0
- package/data/harnesses/code-fable/skills/brainstorm/SKILL.md +135 -0
- package/data/harnesses/code-fable/skills/brainstorm/harvest.md +26 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/backlog.md +39 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/build.md +37 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/decisions.md +29 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/doctrine.md +31 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/domain-model.md +25 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/glossary.md +19 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/milestones.md +43 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/non-goals.md +21 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/plan-doc.md +61 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/questions.md +35 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/references.md +28 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/rejected.md +29 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/state.md +8 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/tech.md +23 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/thesis.md +21 -0
- package/data/harnesses/code-fable/skills/distiller/SKILL.md +219 -0
- package/data/harnesses/code-fable/skills/distiller/references/docs-mode.md +526 -0
- package/data/harnesses/code-fable/skills/distiller/references/method.md +166 -0
- package/data/harnesses/code-fable/skills/documentation/SKILL.md +127 -0
- package/data/harnesses/code-fable/skills/documentation/references/examples.md +193 -0
- package/data/harnesses/code-fable/skills/documentation/references/style-guide.md +288 -0
- package/data/harnesses/code-fable/skills/documentation/references/templates.md +942 -0
- package/data/harnesses/code-fable/skills/workflow-mapper/SKILL.md +430 -0
- package/data/harnesses/code-fable/skills/workflow-mapper/references/patterns.md +214 -0
- package/data/harnesses/discover/CLAUDE.md +180 -0
- package/data/harnesses/{re → discover}/agents/api.md +3 -3
- package/data/harnesses/{re → discover}/agents/codebase.md +3 -3
- package/data/harnesses/{re → discover}/agents/community.md +3 -3
- package/data/harnesses/{re → discover}/agents/deployed-client-bundle.md +21 -15
- package/data/harnesses/{re → discover}/agents/distribution-artifacts.md +13 -12
- package/data/harnesses/{re → discover}/agents/docs.md +3 -3
- package/data/harnesses/{re → discover}/agents/infra-backend-fingerprint.md +3 -3
- package/data/harnesses/{re → discover}/agents/packages.md +3 -3
- package/data/harnesses/{re → discover}/agents/session.md +29 -11
- package/data/harnesses/{re → discover}/agents/website.md +3 -3
- package/data/harnesses/{re → discover}/agents/wire-capture.md +14 -10
- package/data/harnesses/discover/harness.json +7 -0
- package/data/harnesses/{re/rules/re-discovery.md → discover/rules/discovery.md} +86 -17
- package/data/harnesses/{re/rules/re-evaluation.md → discover/rules/evaluation.md} +40 -4
- package/data/harnesses/discover/rules/ingestion.md +551 -0
- package/data/harnesses/discover/rules/self-correction.md +177 -0
- package/data/harnesses/discover/rules/tradecraft.md +532 -0
- package/data/harnesses/hr/CLAUDE.md +91 -0
- package/data/harnesses/hr/harness.json +7 -0
- package/data/harnesses/hr/skills/interview-eval/SKILL.md +118 -0
- package/data/harnesses/hr/skills/interview-eval/TEMPLATE.md +124 -0
- package/data/harnesses/hr/skills/interview-eval/extract_session.py +418 -0
- package/data/harnesses/hr/skills/interview-eval/rubric.md +219 -0
- package/data/harnesses/kb-agi/harness.json +1 -0
- package/package.json +1 -1
- package/data/harnesses/code/hooks/post-commit.js +0 -136
- package/data/harnesses/code/hooks/post-tool-failure.js +0 -136
- package/data/harnesses/code/hooks/session-stop.js +0 -207
- package/data/harnesses/re/CLAUDE.md +0 -146
- package/data/harnesses/re/harness.json +0 -6
- package/data/harnesses/re/rules/re-ingestion.md +0 -327
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: interview-eval
|
|
3
|
+
description: Evaluate how well a person steered a Claude Code session — the human's prompting, decomposition, verification, debugging, course-correction, and engineering judgment — from one or more session transcripts (.jsonl). Built for interview/hiring assessment of engineers using AI coding agents. Takes 1+ Claude Code session `.jsonl` files (or a directory of them) plus an output directory; writes a per-session `report.md` + `scorecard.yaml`, and (for multiple sessions) a `candidate-rollup.md`. Scores 9 weighted dimensions on a 0–5 band scale against absolute anchors in `rubric.md`, normalizes to /100, maps to a steering level (Exemplary / Strong / Competent / Developing / Weak), applies hard-cap gates for unsafe behavior, and anchors every score to quoted turns. It judges the HUMAN'S steering, not Claude's code. Triggers on "evaluate this Claude Code session", "score how the candidate steered Claude", "judge these session transcripts", "how good is this person's prompting", "assess AI-pairing skill from a session log", or a `.jsonl` transcript handed over for review.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# interview-eval
|
|
7
|
+
|
|
8
|
+
Judge **how a human drove a Claude Code session** — not how good Claude's output was. The unit of evaluation is the person's steering: how they framed the problem, decomposed it, fed context, verified results, debugged, corrected course, and revealed engineering judgment through their prompts. One or many session transcripts in → per-session reports + scorecards out, plus a candidate-level rollup when there are several.
|
|
9
|
+
|
|
10
|
+
This is the **counterpart** to the sibling `evaluator` skill: `evaluator` scores a *repo's artifacts*; `interview-eval` scores the *conversation that produced them*. They share no rubric.
|
|
11
|
+
|
|
12
|
+
## The one rule that governs every score
|
|
13
|
+
|
|
14
|
+
**You are scoring the human, not the model.** Claude's code being clean does not earn the candidate points; Claude's code being buggy does not lose them points — *unless* the human's prompt caused it or the human failed to catch it. Reward the prompt that set Claude up to succeed, the verification that caught Claude's mistake, the redirect that pulled Claude off a bad path. A candidate who wrote three sharp prompts and let a capable agent run is steering *well*, even if the turn count is low. A candidate who typed forty vague nudges and rubber-stamped broken output is steering *badly*, even if something shipped. Turn count, prompt length, and verbosity are **not** quality signals — precision, judgment, and verification are.
|
|
15
|
+
|
|
16
|
+
## Input
|
|
17
|
+
|
|
18
|
+
- **One or more session transcripts** — Claude Code `.jsonl` files (e.g. `0fe33297-….jsonl`). Accept a list of files, a glob, or a directory (evaluate every `*.jsonl` inside).
|
|
19
|
+
- **An output directory** — `out_dir` (default: `./interview-eval-out/`). One subfolder per session, plus a rollup at the root when >1 session.
|
|
20
|
+
- **Optional context** the user may volunteer: the candidate's seniority/role, the task brief they were given, time limits, or whether sessions are independent tasks vs. one continued task. Fold it in; never invent it.
|
|
21
|
+
|
|
22
|
+
If the user just points at this folder, treat every `*.jsonl` as one candidate's sessions unless told otherwise.
|
|
23
|
+
|
|
24
|
+
## Process
|
|
25
|
+
|
|
26
|
+
### 0. Preflight (cheap)
|
|
27
|
+
Only dependency is `python3` (stdlib only). No installs, no network. If `python3` is missing, say so and stop.
|
|
28
|
+
|
|
29
|
+
### 1. Extract an evidence pack per session (deterministic)
|
|
30
|
+
Run the extractor — it never scores, it only surfaces signal:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
python3 <skill_dir>/extract_session.py SESSION.jsonl --out <out_dir>/<session-slug>/ --json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
It emits a markdown evidence pack on stdout and writes `<sessionId>.session.json`. The pack gives you:
|
|
37
|
+
- **Turn-by-turn**: each human prompt (IDE tags stripped) followed by a summary of what the agent did (tool counts + sample targets). This is your primary evidence — quote `U<n>` turn numbers in every finding.
|
|
38
|
+
- **Metrics**: prompt count, wall-clock & idle gaps, tool histogram, permission-mode transitions, plan-mode / subagent / AskUserQuestion / slash-command usage, interrupts, system/API errors.
|
|
39
|
+
- **Auto red-flag signals**: secrets pasted into prompts, destructive commands the agent ran (`rm -rf ~`, force-push, `DROP TABLE`, …). These are **leads to verify**, not verdicts.
|
|
40
|
+
|
|
41
|
+
**Read the full evidence pack before scoring.** For long sessions, also open the raw `.jsonl` around any turn you intend to quote, to confirm context. Do not score from metrics alone — a session can have great metrics and terrible steering, or vice-versa.
|
|
42
|
+
|
|
43
|
+
**Separate noise from signal.** `system_errors` that are `status: 529` / `ECONNRESET` are Anthropic-side overload, not the candidate's fault — never penalize them. `interrupts` may be a sharp early redirect (good) or thrash (bad) — read the surrounding turns to tell which.
|
|
44
|
+
|
|
45
|
+
### 2. Classify the session type
|
|
46
|
+
Pick the dominant type (a session can be a blend — note it): **greenfield-build · feature-add · debugging · refactor · ops/infra · research/Q&A · review/audit**. The type sets which dimensions are *primary*, *secondary*, or *N/A* — see `rubric.md` → "Session-type matrix". A pure debugging session, for example, makes **E** the main event and may mark **B** N/A.
|
|
47
|
+
|
|
48
|
+
### 3. Score the 9 dimensions
|
|
49
|
+
Full band anchors (0–5, with concrete "looks like" descriptions and green/red flags) live in **[`rubric.md`](rubric.md)** — read it before every evaluation; do not score from memory. Summary:
|
|
50
|
+
|
|
51
|
+
| # | Dimension | Weight | The senior question it answers |
|
|
52
|
+
|---|-----------|:------:|--------------------------------|
|
|
53
|
+
| **A** | Problem Framing & Specification | 15 | Did they tell the agent *what* and *why*, at the right altitude, with constraints + acceptance criteria? |
|
|
54
|
+
| **B** | Decomposition & Sequencing | 12 | Did they break work into coherent, dependency-ordered, verifiable increments? |
|
|
55
|
+
| **C** | Context Curation | 10 | Did they ground the agent in real artifacts (files, logs, outputs) instead of making it guess? |
|
|
56
|
+
| **D** | Verification & Skepticism | 18 | Did they treat output as a draft to check — run it, read it, catch errors — rather than rubber-stamp? |
|
|
57
|
+
| **E** | Debugging Collaboration | 10 | When it broke, did they give high-signal diagnostics (repro, exact error, expected-vs-actual, hypothesis)? |
|
|
58
|
+
| **F** | Course Correction & Steering Control | 10 | Did they catch drift early and redirect surgically, instead of letting bad paths run or thrashing? |
|
|
59
|
+
| **G** | Technical & Architectural Judgment | 15 | Do the prompts reveal engineering maturity — operability, failure modes, trade-offs, right-sizing? |
|
|
60
|
+
| **H** | Workflow & Tooling Leverage | 5 | Did they use the harness well (plan mode, subagents, git, slash/skills) and pick the right autonomy? |
|
|
61
|
+
| **I** | Outcome & Efficiency | 5 | Did the session reach a correct, working end state with economy proportional to scope? |
|
|
62
|
+
|
|
63
|
+
Each dimension gets an integer **0–5 band**, a one-paragraph **rationale**, and **≥2 quoted turn citations** (`U7: "i see the rows but they are empty"`). No citation → no score; write `INSUFFICIENT_EVIDENCE` and exclude from the total. Mark genuinely inapplicable dimensions `N/A` and renormalize over the remaining weight.
|
|
64
|
+
|
|
65
|
+
### 4. Apply gates (hard caps), then green flags
|
|
66
|
+
**Gates** cap the overall band regardless of dimension scores — see `rubric.md` → "Gates". They fire only on *confirmed* unsafe steering (pasted live secrets, approved a destructive/irreversible op without checking, knowingly shipped broken output). Auto-detected red flags are candidates for a gate — confirm against the transcript before firing one, and record why.
|
|
67
|
+
|
|
68
|
+
**Green flags** are noted as evidence of strength but never push a dimension above its band's ceiling.
|
|
69
|
+
|
|
70
|
+
### 5. Compute totals and level
|
|
71
|
+
```
|
|
72
|
+
weighted = Σ(band_d / 5 × weight_d) over scored dimensions
|
|
73
|
+
normalized = weighted / Σ(weight_d of scored dimensions) × 100
|
|
74
|
+
```
|
|
75
|
+
Map `normalized` to a **steering level** (after gate caps):
|
|
76
|
+
|
|
77
|
+
| Level | Normalized | Reads as |
|
|
78
|
+
|-------|:----------:|----------|
|
|
79
|
+
| **Exemplary** | 85–100 | Operates the agent as a true force-multiplier; senior+ signal |
|
|
80
|
+
| **Strong** | 70–84 | Confident, effective, low-waste steering; clear senior signal |
|
|
81
|
+
| **Competent** | 55–69 | Gets results; real gaps in verification or framing; mid-level |
|
|
82
|
+
| **Developing** | 40–54 | Works, but reactive/under-specified/under-verified; junior steering |
|
|
83
|
+
| **Weak** | 0–39 | Ineffective or unsafe steering |
|
|
84
|
+
|
|
85
|
+
The level is a **calibration aid, not a verdict** — the human reviewer decides hire/no-hire. Present the level with the evidence that earned it, and call out the 1–2 dimensions that most moved it.
|
|
86
|
+
|
|
87
|
+
### 6. Write outputs (see [`TEMPLATE.md`](TEMPLATE.md))
|
|
88
|
+
Per session, write to `<out_dir>/<session-slug>/`:
|
|
89
|
+
- **`report.md`** — narrative: summary line, level, per-dimension table with citations, standout moments, weaknesses, gates/flags, and a "what a stronger operator would have done differently" section. Shape = TEMPLATE.md.
|
|
90
|
+
- **`scorecard.yaml`** — machine-readable: per-dimension band + rationale + citations, gates, green flags, totals, level.
|
|
91
|
+
|
|
92
|
+
When >1 session, also write `<out_dir>/candidate-rollup.md`:
|
|
93
|
+
- Per-session level + headline.
|
|
94
|
+
- Per-dimension trend across sessions (is verification consistently weak? is framing consistently strong?).
|
|
95
|
+
- **Consistency** read — does the candidate steer the same way under pressure (debugging) as when greenfield?
|
|
96
|
+
- A single calibrated **overall steering level** with the 3 strongest and 3 weakest pieces of evidence across all sessions, and concrete interview follow-up questions to probe the gaps.
|
|
97
|
+
|
|
98
|
+
## Scoring discipline (read before every run)
|
|
99
|
+
|
|
100
|
+
- **Evidence or it didn't happen.** Every band cites real turns. "Seems thorough" is not a finding; `U7–U11 debugged the empty-rows bug by pasting the live API response (U9) and isolating restart behavior (U11)` is.
|
|
101
|
+
- **Absolute bands, not curve.** A lone session is scored against the anchors in `rubric.md`, not against other candidates. With multiple sessions you may *report* relative trends, but each session's band is absolute.
|
|
102
|
+
- **Don't confuse activity with skill.** A 40-turn session is not 40-turns-good. Ask what each turn *accomplished*. Penalize wasted loops; reward turns that each move a verifiable step.
|
|
103
|
+
- **Don't penalize the harness.** API 529s, tool-permission prompts, and model mistakes the candidate then caught are not the candidate's failures. Idle gaps may be the candidate thinking, testing out-of-band, or away — note, don't assume.
|
|
104
|
+
- **Charity with a spine.** Read terse prompts in the best plausible light (typos, brevity ≠ low skill), but do not invent verification or judgment the transcript doesn't show. If you can't see it, it isn't scored there.
|
|
105
|
+
- **Surface the model's contribution honestly.** If Claude proposed the good architecture and the candidate merely said "yes", that is weak **G** with strong-agent luck — say so. If the candidate's prompt *specified* the operational concern, that is strong **G**.
|
|
106
|
+
|
|
107
|
+
## What good and weak steering look like (calibration vignettes)
|
|
108
|
+
|
|
109
|
+
- **Strong A+G+D:** Opens with a data model, the four API operations, and an explicit constraint ("use an in-memory DB"). Later, unprompted, asks for *reconciliation of stuck campaigns* and *idempotent re-enqueue* — operational maturity. Verifies by running the app and reading the actual response body, not by trusting the diff.
|
|
110
|
+
- **Strong E:** On a bug, pastes the literal API response (`U9`), then narrows it — "I see the rows but they are empty" → "restarting the server still shows old rows" — feeding the agent expected-vs-actual until root cause. Converges in a few turns.
|
|
111
|
+
- **Weak D:** Runs the whole session in an auto-accept mode and never once runs, reads, or questions the output; "make it work" → "still not working" → "ok ship it." No evidence the human ever confirmed correctness.
|
|
112
|
+
- **Weak F:** Lets the agent build for many turns down a path the candidate didn't want, then restarts from scratch instead of redirecting; or re-sends the identical prompt three times when it fails instead of adding information.
|
|
113
|
+
- **Gate fires:** Pastes a live API key into a prompt, or tells the agent to `git push --force` to a shared branch and approves it without looking — cap at **Developing** and flag, no matter how good the rest is.
|
|
114
|
+
|
|
115
|
+
## Notes
|
|
116
|
+
- The skill is read-only over the transcripts; it only writes into `out_dir`. Never mutate the source `.jsonl`.
|
|
117
|
+
- If a transcript is corrupt/empty or has zero human turns, write a stub report saying so rather than fabricating a score.
|
|
118
|
+
- Keep reports honest and specific enough that the candidate, if shown the report, would recognize the session. That is the bar for evidence quality.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# interview-eval output templates
|
|
2
|
+
|
|
3
|
+
Two per-session artifacts (`report.md`, `scorecard.yaml`) and one rollup (`candidate-rollup.md`). Fill every `‹…›`. Keep citations to real `U<n>` turns with short verbatim quotes.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## `report.md` (per session)
|
|
8
|
+
|
|
9
|
+
```markdown
|
|
10
|
+
# Steering evaluation — ‹session title›
|
|
11
|
+
`‹session-id›` · ‹session-type› · ‹N› human turns · ‹wall-clock› · cli ‹version›
|
|
12
|
+
|
|
13
|
+
> **Steering level: ‹Exemplary | Strong | Competent | Developing | Weak›** (‹normalized›/100)
|
|
14
|
+
> ‹One-sentence verdict — what kind of operator this session shows, in plain language.›
|
|
15
|
+
|
|
16
|
+
**Headline:** ‹2–3 sentences. The single most important thing about how this person drove the agent — the strength that defines the session and the gap that most held it back.›
|
|
17
|
+
|
|
18
|
+
## Scores
|
|
19
|
+
|
|
20
|
+
| Dim | Dimension | Band | Wt | Note |
|
|
21
|
+
|-----|-----------|:----:|:--:|------|
|
|
22
|
+
| A | Problem Framing & Specification | ‹0–5› | 15 | ‹≤10 words› |
|
|
23
|
+
| B | Decomposition & Sequencing | ‹0–5› | 12 | ‹≤10 words› |
|
|
24
|
+
| C | Context Curation | ‹0–5› | 10 | ‹≤10 words› |
|
|
25
|
+
| D | Verification & Skepticism | ‹0–5› | 18 | ‹≤10 words› |
|
|
26
|
+
| E | Debugging Collaboration | ‹0–5 / N/A› | 10 | ‹≤10 words› |
|
|
27
|
+
| F | Course Correction & Steering | ‹0–5› | 10 | ‹≤10 words› |
|
|
28
|
+
| G | Technical & Architectural Judgment | ‹0–5› | 15 | ‹≤10 words› |
|
|
29
|
+
| H | Workflow & Tooling Leverage | ‹0–5› | 5 | ‹≤10 words› |
|
|
30
|
+
| I | Outcome & Efficiency | ‹0–5› | 5 | ‹≤10 words› |
|
|
31
|
+
| | **Weighted total** | | | **‹normalized›/100** |
|
|
32
|
+
|
|
33
|
+
‹If any gate fired:› **Gate: ‹GATE-NAME›** — ‹what triggered it, with the turn cite›. Level capped at ‹cap›.
|
|
34
|
+
|
|
35
|
+
## Per-dimension findings
|
|
36
|
+
For each dimension, 2–4 sentences of rationale + the turn citations that justify the band.
|
|
37
|
+
|
|
38
|
+
**A — Problem Framing (‹band›).** ‹rationale› _Evidence:_ ‹U1: "…"›, ‹U4: "…"›.
|
|
39
|
+
|
|
40
|
+
**B — Decomposition (‹band›).** ‹rationale› _Evidence:_ ‹…›.
|
|
41
|
+
|
|
42
|
+
‹…repeat C–I…›
|
|
43
|
+
|
|
44
|
+
## Standout moments
|
|
45
|
+
- ✅ ‹The 1–3 best steering moves, each with a turn cite and why it was good.›
|
|
46
|
+
|
|
47
|
+
## Weaknesses
|
|
48
|
+
- ⚠ ‹The 1–3 most costly gaps, each with a turn cite and the consequence.›
|
|
49
|
+
|
|
50
|
+
## What a stronger operator would have done differently
|
|
51
|
+
‹3–5 concrete, specific alternatives — the actual better prompt or move at a named turn. This is the most useful section for a hiring decision; make it sharp, not generic.›
|
|
52
|
+
|
|
53
|
+
## Caveats
|
|
54
|
+
‹Harness noise excluded from scoring (e.g. "8 system errors were all API 529s — not penalized"), idle gaps, anything the transcript couldn't show.›
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## `scorecard.yaml` (per session, machine-readable)
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
session_id: ‹id›
|
|
63
|
+
title: ‹ai-title›
|
|
64
|
+
session_type: ‹greenfield-build | feature-add | debugging | refactor | ops | research | review›
|
|
65
|
+
turns: ‹N›
|
|
66
|
+
duration_human: ‹3h42m›
|
|
67
|
+
|
|
68
|
+
dimensions:
|
|
69
|
+
framing: { band: ‹0-5›, weight: 15, rationale: "‹…›", cites: ["U1", "U4"] }
|
|
70
|
+
decomposition: { band: ‹0-5›, weight: 12, rationale: "‹…›", cites: ["U2", "U4"] }
|
|
71
|
+
context: { band: ‹0-5›, weight: 10, rationale: "‹…›", cites: ["U9"] }
|
|
72
|
+
verification: { band: ‹0-5›, weight: 18, rationale: "‹…›", cites: ["U7", "U11"] }
|
|
73
|
+
debugging: { band: ‹0-5|null›, weight: 10, na: ‹false|true›, rationale: "‹…›", cites: ["U9"] }
|
|
74
|
+
correction: { band: ‹0-5›, weight: 10, rationale: "‹…›", cites: ["U13"] }
|
|
75
|
+
judgment: { band: ‹0-5›, weight: 15, rationale: "‹…›", cites: ["U23"] }
|
|
76
|
+
tooling: { band: ‹0-5›, weight: 5, rationale: "‹…›", cites: ["U17"] }
|
|
77
|
+
outcome: { band: ‹0-5›, weight: 5, rationale: "‹…›", cites: ["U26"] }
|
|
78
|
+
|
|
79
|
+
gates_triggered: [] # e.g. [BLIND-SHIP]
|
|
80
|
+
green_flags: [] # e.g. [empirical-debugging, unprompted-operability]
|
|
81
|
+
|
|
82
|
+
weighted_total: ‹float› # Σ(band/5 × weight) over scored dims
|
|
83
|
+
available_weight: ‹int› # Σ weight of scored (non-N/A) dims
|
|
84
|
+
normalized: ‹float› # weighted_total / available_weight × 100, after gate caps
|
|
85
|
+
level: ‹Exemplary | Strong | Competent | Developing | Weak›
|
|
86
|
+
|
|
87
|
+
evidence_excluded_from_scoring:
|
|
88
|
+
- "‹e.g. 8 API 529 errors — Anthropic-side overload, not candidate fault›"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## `candidate-rollup.md` (only when >1 session)
|
|
94
|
+
|
|
95
|
+
```markdown
|
|
96
|
+
# Candidate steering rollup — ‹candidate / label›
|
|
97
|
+
‹K› sessions · ‹total turns› · ‹combined wall-clock›
|
|
98
|
+
|
|
99
|
+
> **Overall steering level: ‹level›**
|
|
100
|
+
> ‹One-paragraph calibrated read of the candidate as an AI-pairing operator across all sessions.›
|
|
101
|
+
|
|
102
|
+
## Sessions
|
|
103
|
+
| Session | Type | Level | Norm. | Headline |
|
|
104
|
+
|---------|------|-------|:-----:|----------|
|
|
105
|
+
| ‹title› | ‹type› | ‹level› | ‹n› | ‹≤12 words› |
|
|
106
|
+
|
|
107
|
+
## Dimension trend across sessions
|
|
108
|
+
| Dim | S1 | S2 | … | Read |
|
|
109
|
+
|-----|:--:|:--:|---|------|
|
|
110
|
+
| A Framing | ‹b› | ‹b› | | ‹consistent? improving? context-dependent?› |
|
|
111
|
+
| … | | | | |
|
|
112
|
+
|
|
113
|
+
## Consistency
|
|
114
|
+
‹Does the candidate steer the same under pressure (debugging) as greenfield? Where does quality drop — and is the drop in a dimension that matters for the role?›
|
|
115
|
+
|
|
116
|
+
## Strongest evidence (top 3 across all sessions)
|
|
117
|
+
1. ‹session · U‹n› · what it shows›
|
|
118
|
+
|
|
119
|
+
## Weakest evidence (top 3 across all sessions)
|
|
120
|
+
1. ‹session · U‹n› · what it shows›
|
|
121
|
+
|
|
122
|
+
## Interview follow-ups to probe the gaps
|
|
123
|
+
- ‹Concrete question that would confirm/deny the weakest dimension, e.g. "Walk me through how you'd have verified the worker actually drained the queue — what would you have checked?"›
|
|
124
|
+
```
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
extract_session.py — deterministic extractor for interview-eval.
|
|
4
|
+
|
|
5
|
+
Turns a Claude Code session transcript (.jsonl) into a normalized "evidence pack"
|
|
6
|
+
so the LLM judge scores from clean, quoted evidence instead of re-parsing raw JSONL.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python3 extract_session.py SESSION.jsonl [SESSION2.jsonl ...] [--json] [--out DIR]
|
|
10
|
+
|
|
11
|
+
Default output: a human/LLM-readable markdown evidence pack per session on stdout.
|
|
12
|
+
--json: also emit the normalized structure as <out>/<sessionId>.session.json
|
|
13
|
+
--out DIR: write artifacts to DIR (default: alongside the transcript)
|
|
14
|
+
|
|
15
|
+
The extractor is purely mechanical. It NEVER scores. It surfaces signals
|
|
16
|
+
(prompts, tool usage, modes, errors, auto-detected red flags) for the judge.
|
|
17
|
+
"""
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
import os
|
|
22
|
+
from collections import Counter, OrderedDict
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
|
|
25
|
+
# ---------- transcript record helpers ----------
|
|
26
|
+
|
|
27
|
+
IDE_TAG = re.compile(r"<ide_[a-z_]+>.*?</ide_[a-z_]+>", re.DOTALL)
|
|
28
|
+
CMD_NAME = re.compile(r"<command-name>(.*?)</command-name>", re.DOTALL)
|
|
29
|
+
CMD_ARGS = re.compile(r"<command-args>(.*?)</command-args>", re.DOTALL)
|
|
30
|
+
|
|
31
|
+
# Auto-detected red-flag patterns. These are SIGNALS for the judge, not verdicts.
|
|
32
|
+
SECRET_PATTERNS = [
|
|
33
|
+
(re.compile(r"sk-[A-Za-z0-9]{20,}"), "openai-style key"),
|
|
34
|
+
(re.compile(r"ghp_[A-Za-z0-9]{30,}"), "github PAT"),
|
|
35
|
+
(re.compile(r"AKIA[0-9A-Z]{16}"), "aws access key id"),
|
|
36
|
+
(re.compile(r"-----BEGIN (?:RSA |EC )?PRIVATE KEY-----"), "private key"),
|
|
37
|
+
(re.compile(r"(?i)\b(password|passwd|secret|api[_-]?key|token)\b\s*[:=]\s*['\"][^'\"]{6,}"), "inline secret"),
|
|
38
|
+
]
|
|
39
|
+
DANGER_CMD = [
|
|
40
|
+
(re.compile(r"\brm\s+-rf?\s+[/~]"), "rm -rf on root/home path"),
|
|
41
|
+
(re.compile(r"\bgit\s+push\b.*--force\b|\bgit\s+push\b.*\s-f\b"), "force push"),
|
|
42
|
+
(re.compile(r"\bgit\s+reset\s+--hard\b"), "hard reset"),
|
|
43
|
+
(re.compile(r"\bDROP\s+(TABLE|DATABASE)\b", re.I), "drop table/database"),
|
|
44
|
+
(re.compile(r"\bchmod\s+-R\s+777\b"), "chmod 777 -R"),
|
|
45
|
+
(re.compile(r":\s*\(\)\s*\{\s*:\|:&\s*\}\s*;:"), "fork bomb"),
|
|
46
|
+
]
|
|
47
|
+
SLASH_CMD = re.compile(r"^\s*/([A-Za-z][\w:-]*)")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_records(path):
|
|
51
|
+
recs = []
|
|
52
|
+
with open(path, "r", errors="replace") as f:
|
|
53
|
+
for ln, line in enumerate(f, 1):
|
|
54
|
+
line = line.strip()
|
|
55
|
+
if not line:
|
|
56
|
+
continue
|
|
57
|
+
try:
|
|
58
|
+
recs.append(json.loads(line))
|
|
59
|
+
except json.JSONDecodeError:
|
|
60
|
+
recs.append({"type": "_PARSE_ERROR", "_line": ln})
|
|
61
|
+
return recs
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def msg_role(rec):
|
|
65
|
+
return rec.get("message", {}).get("role")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def extract_text(content):
|
|
69
|
+
"""Return concatenated text blocks from a message content (str or list)."""
|
|
70
|
+
if isinstance(content, str):
|
|
71
|
+
return content
|
|
72
|
+
if isinstance(content, list):
|
|
73
|
+
parts = [b.get("text", "") for b in content
|
|
74
|
+
if isinstance(b, dict) and b.get("type") == "text"]
|
|
75
|
+
return "\n".join(p for p in parts if p)
|
|
76
|
+
return ""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def is_tool_result(content):
|
|
80
|
+
return isinstance(content, list) and any(
|
|
81
|
+
isinstance(b, dict) and b.get("type") == "tool_result" for b in content)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def tool_uses(content):
|
|
85
|
+
out = []
|
|
86
|
+
if isinstance(content, list):
|
|
87
|
+
for b in content:
|
|
88
|
+
if isinstance(b, dict) and b.get("type") == "tool_use":
|
|
89
|
+
out.append((b.get("name", "?"), b.get("input", {}) or {}))
|
|
90
|
+
return out
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def tool_target(name, inp):
|
|
94
|
+
"""One-line human-readable summary of a tool call's target."""
|
|
95
|
+
if not isinstance(inp, dict):
|
|
96
|
+
return ""
|
|
97
|
+
for k in ("file_path", "path", "pattern", "command", "url", "query",
|
|
98
|
+
"prompt", "skill", "notebook_path", "description"):
|
|
99
|
+
if k in inp and inp[k]:
|
|
100
|
+
v = str(inp[k]).replace("\n", " ")
|
|
101
|
+
return (v[:140] + "…") if len(v) > 140 else v
|
|
102
|
+
return ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def parse_ts(s):
|
|
106
|
+
if not s:
|
|
107
|
+
return None
|
|
108
|
+
try:
|
|
109
|
+
return datetime.fromisoformat(s.replace("Z", "+00:00"))
|
|
110
|
+
except Exception:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def clean_prompt(text):
|
|
115
|
+
"""Strip IDE context tags; flag slash commands and local-command stdout."""
|
|
116
|
+
stripped = IDE_TAG.sub("", text).strip()
|
|
117
|
+
return stripped
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------- core normalization ----------
|
|
121
|
+
|
|
122
|
+
def normalize(path):
|
|
123
|
+
recs = load_records(path)
|
|
124
|
+
sid = None
|
|
125
|
+
ai_title = None
|
|
126
|
+
version = None
|
|
127
|
+
cwd = None
|
|
128
|
+
branch = None
|
|
129
|
+
|
|
130
|
+
turns = [] # ordered user prompts with following assistant actions
|
|
131
|
+
cur_actions = [] # assistant tool calls / text since last user prompt
|
|
132
|
+
pending_user = None # the open user turn we are attaching actions to
|
|
133
|
+
|
|
134
|
+
tool_counts = Counter()
|
|
135
|
+
perm_modes = Counter()
|
|
136
|
+
perm_transitions = []
|
|
137
|
+
mode_records = []
|
|
138
|
+
interrupts = 0
|
|
139
|
+
sys_errors = []
|
|
140
|
+
parse_errors = 0
|
|
141
|
+
first_ts = None
|
|
142
|
+
last_ts = None
|
|
143
|
+
slash_commands = []
|
|
144
|
+
subagent_calls = [] # Task/Agent tool uses
|
|
145
|
+
askuser_calls = 0
|
|
146
|
+
plan_mode_used = False
|
|
147
|
+
|
|
148
|
+
redflags = [] # auto-detected; (kind, where, detail)
|
|
149
|
+
user_prompt_idx = 0
|
|
150
|
+
|
|
151
|
+
def close_turn():
|
|
152
|
+
nonlocal pending_user, cur_actions
|
|
153
|
+
if pending_user is not None:
|
|
154
|
+
pending_user["actions"] = cur_actions
|
|
155
|
+
turns.append(pending_user)
|
|
156
|
+
cur_actions = []
|
|
157
|
+
pending_user = None
|
|
158
|
+
|
|
159
|
+
for rec in recs:
|
|
160
|
+
t = rec.get("type")
|
|
161
|
+
if t == "_PARSE_ERROR":
|
|
162
|
+
parse_errors += 1
|
|
163
|
+
continue
|
|
164
|
+
ts = parse_ts(rec.get("timestamp"))
|
|
165
|
+
if ts:
|
|
166
|
+
if first_ts is None:
|
|
167
|
+
first_ts = ts
|
|
168
|
+
last_ts = ts
|
|
169
|
+
|
|
170
|
+
if t == "mode" or t == "permission-mode":
|
|
171
|
+
m = rec.get("mode")
|
|
172
|
+
if m:
|
|
173
|
+
mode_records.append(m)
|
|
174
|
+
if m in ("plan", "plan-mode"):
|
|
175
|
+
plan_mode_used = True
|
|
176
|
+
|
|
177
|
+
if sid is None and rec.get("sessionId"):
|
|
178
|
+
sid = rec["sessionId"]
|
|
179
|
+
if rec.get("version"):
|
|
180
|
+
version = rec["version"]
|
|
181
|
+
if rec.get("cwd"):
|
|
182
|
+
cwd = rec["cwd"]
|
|
183
|
+
if rec.get("gitBranch"):
|
|
184
|
+
branch = rec["gitBranch"]
|
|
185
|
+
|
|
186
|
+
if t == "ai-title" and rec.get("aiTitle"):
|
|
187
|
+
ai_title = rec["aiTitle"]
|
|
188
|
+
|
|
189
|
+
if t == "system" and rec.get("level") == "error":
|
|
190
|
+
sys_errors.append({
|
|
191
|
+
"subtype": rec.get("subtype"),
|
|
192
|
+
"error": (str(rec.get("error", ""))[:200]),
|
|
193
|
+
"retryAttempt": rec.get("retryAttempt"),
|
|
194
|
+
"maxRetries": rec.get("maxRetries"),
|
|
195
|
+
"ts": rec.get("timestamp"),
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
if t == "user":
|
|
199
|
+
pm = rec.get("permissionMode")
|
|
200
|
+
if pm:
|
|
201
|
+
perm_modes[pm] += 1
|
|
202
|
+
if not perm_transitions or perm_transitions[-1] != pm:
|
|
203
|
+
perm_transitions.append(pm)
|
|
204
|
+
if pm in ("plan", "plan-mode"):
|
|
205
|
+
plan_mode_used = True
|
|
206
|
+
content = rec.get("message", {}).get("content")
|
|
207
|
+
if msg_role(rec) != "user":
|
|
208
|
+
continue
|
|
209
|
+
raw = extract_text(content)
|
|
210
|
+
# interrupt markers
|
|
211
|
+
if "interrupted by user" in (raw or ""):
|
|
212
|
+
interrupts += 1
|
|
213
|
+
if is_tool_result(content) and not (raw and raw.strip()):
|
|
214
|
+
continue # pure tool result, not a human turn
|
|
215
|
+
if not (raw and raw.strip()):
|
|
216
|
+
continue
|
|
217
|
+
text = clean_prompt(raw)
|
|
218
|
+
if not text:
|
|
219
|
+
continue
|
|
220
|
+
# close prior turn, open a new one
|
|
221
|
+
close_turn()
|
|
222
|
+
user_prompt_idx += 1
|
|
223
|
+
# detect slash command
|
|
224
|
+
sm = SLASH_CMD.match(text)
|
|
225
|
+
if sm:
|
|
226
|
+
slash_commands.append(sm.group(1))
|
|
227
|
+
cm = CMD_NAME.search(text)
|
|
228
|
+
if cm:
|
|
229
|
+
slash_commands.append(cm.group(1).strip().lstrip("/"))
|
|
230
|
+
# red-flag scan on the human prompt text
|
|
231
|
+
for pat, label in SECRET_PATTERNS:
|
|
232
|
+
if pat.search(text):
|
|
233
|
+
redflags.append(("secret-in-prompt", f"U{user_prompt_idx}", label))
|
|
234
|
+
had_ide = bool(IDE_TAG.search(raw))
|
|
235
|
+
pending_user = {
|
|
236
|
+
"idx": user_prompt_idx,
|
|
237
|
+
"ts": rec.get("timestamp"),
|
|
238
|
+
"text": text,
|
|
239
|
+
"len": len(text),
|
|
240
|
+
"had_ide_context": had_ide,
|
|
241
|
+
"permissionMode": pm,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
elif t == "assistant":
|
|
245
|
+
content = rec.get("message", {}).get("content")
|
|
246
|
+
for name, inp in tool_uses(content):
|
|
247
|
+
tool_counts[name] += 1
|
|
248
|
+
tgt = tool_target(name, inp)
|
|
249
|
+
cur_actions.append({"tool": name, "target": tgt})
|
|
250
|
+
if name in ("Task", "Agent"):
|
|
251
|
+
subagent_calls.append({"target": tgt,
|
|
252
|
+
"subagent": inp.get("subagent_type")})
|
|
253
|
+
if name == "AskUserQuestion":
|
|
254
|
+
askuser_calls += 1
|
|
255
|
+
# danger scan on bash commands the agent ran
|
|
256
|
+
if name == "Bash":
|
|
257
|
+
cmd = str(inp.get("command", ""))
|
|
258
|
+
for pat, label in DANGER_CMD:
|
|
259
|
+
if pat.search(cmd):
|
|
260
|
+
redflags.append(("danger-cmd", f"after U{user_prompt_idx}", f"{label}: {cmd[:120]}"))
|
|
261
|
+
atext = extract_text(content)
|
|
262
|
+
if atext and atext.strip():
|
|
263
|
+
cur_actions.append({"assistant_text": atext.strip()[:400]})
|
|
264
|
+
|
|
265
|
+
close_turn()
|
|
266
|
+
|
|
267
|
+
duration_s = None
|
|
268
|
+
if first_ts and last_ts:
|
|
269
|
+
duration_s = int((last_ts - first_ts).total_seconds())
|
|
270
|
+
|
|
271
|
+
prompt_lens = [t["len"] for t in turns]
|
|
272
|
+
metrics = {
|
|
273
|
+
"user_prompts": len(turns),
|
|
274
|
+
"assistant_tool_calls": sum(tool_counts.values()),
|
|
275
|
+
"tool_counts": dict(tool_counts.most_common()),
|
|
276
|
+
"permission_modes": dict(perm_modes),
|
|
277
|
+
"permission_mode_transitions": perm_transitions,
|
|
278
|
+
"plan_mode_used": plan_mode_used,
|
|
279
|
+
"subagent_calls": len(subagent_calls),
|
|
280
|
+
"askuser_calls": askuser_calls,
|
|
281
|
+
"slash_commands": slash_commands,
|
|
282
|
+
"interrupts": interrupts,
|
|
283
|
+
"system_errors": len(sys_errors),
|
|
284
|
+
"parse_errors": parse_errors,
|
|
285
|
+
"duration_seconds": duration_s,
|
|
286
|
+
"duration_human": _human_dur(duration_s),
|
|
287
|
+
"avg_prompt_len": round(sum(prompt_lens) / len(prompt_lens), 1) if prompt_lens else 0,
|
|
288
|
+
"median_prompt_len": _median(prompt_lens),
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
"session_id": sid,
|
|
293
|
+
"title": ai_title,
|
|
294
|
+
"version": version,
|
|
295
|
+
"cwd": cwd,
|
|
296
|
+
"git_branch": branch,
|
|
297
|
+
"start": first_ts.isoformat() if first_ts else None,
|
|
298
|
+
"end": last_ts.isoformat() if last_ts else None,
|
|
299
|
+
"metrics": metrics,
|
|
300
|
+
"turns": turns,
|
|
301
|
+
"subagent_calls": subagent_calls,
|
|
302
|
+
"system_errors": sys_errors,
|
|
303
|
+
"auto_redflags": redflags,
|
|
304
|
+
"source_path": os.path.abspath(path),
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _human_dur(s):
|
|
309
|
+
if s is None:
|
|
310
|
+
return None
|
|
311
|
+
h, rem = divmod(s, 3600)
|
|
312
|
+
m, sec = divmod(rem, 60)
|
|
313
|
+
if h:
|
|
314
|
+
return f"{h}h{m}m"
|
|
315
|
+
if m:
|
|
316
|
+
return f"{m}m{sec}s"
|
|
317
|
+
return f"{sec}s"
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _median(xs):
|
|
321
|
+
if not xs:
|
|
322
|
+
return 0
|
|
323
|
+
s = sorted(xs)
|
|
324
|
+
n = len(s)
|
|
325
|
+
return s[n // 2] if n % 2 else (s[n // 2 - 1] + s[n // 2]) / 2
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# ---------- rendering ----------
|
|
329
|
+
|
|
330
|
+
def render_md(norm):
|
|
331
|
+
m = norm["metrics"]
|
|
332
|
+
L = []
|
|
333
|
+
L.append(f"# Evidence pack — session `{norm['session_id']}`")
|
|
334
|
+
if norm["title"]:
|
|
335
|
+
L.append(f"**Title:** {norm['title']}")
|
|
336
|
+
L.append("")
|
|
337
|
+
L.append("## Session metrics")
|
|
338
|
+
L.append(f"- User prompts (human turns): **{m['user_prompts']}**")
|
|
339
|
+
L.append(f"- Wall-clock: **{m['duration_human']}** ({m['duration_seconds']}s) · cwd: `{norm['cwd']}` · branch: `{norm['git_branch']}` · cli: {norm['version']}")
|
|
340
|
+
L.append(f"- Tool calls: **{m['assistant_tool_calls']}** — {m['tool_counts']}")
|
|
341
|
+
L.append(f"- Permission modes: {m['permission_modes']} · transitions: {' → '.join(m['permission_mode_transitions']) or '(none)'}")
|
|
342
|
+
L.append(f"- Plan mode used: **{m['plan_mode_used']}** · Subagents: **{m['subagent_calls']}** · AskUserQuestion: **{m['askuser_calls']}** · Slash cmds: {m['slash_commands'] or '(none)'}")
|
|
343
|
+
L.append(f"- Interrupts (user stopped a tool): **{m['interrupts']}** · System/API errors: **{m['system_errors']}**")
|
|
344
|
+
L.append(f"- Prompt length: avg **{m['avg_prompt_len']}** chars, median **{m['median_prompt_len']}**")
|
|
345
|
+
L.append("")
|
|
346
|
+
if norm["auto_redflags"]:
|
|
347
|
+
L.append("## ⚠ Auto-detected red-flag signals (verify before scoring)")
|
|
348
|
+
for kind, where, detail in norm["auto_redflags"]:
|
|
349
|
+
L.append(f"- [{kind}] {where}: {detail}")
|
|
350
|
+
L.append("")
|
|
351
|
+
if norm["system_errors"]:
|
|
352
|
+
L.append("## System/API errors (context noise, not always the candidate's fault)")
|
|
353
|
+
for e in norm["system_errors"][:8]:
|
|
354
|
+
L.append(f"- {e.get('subtype')} attempt {e.get('retryAttempt')}/{e.get('maxRetries')}: {e.get('error')}")
|
|
355
|
+
L.append("")
|
|
356
|
+
L.append("## Turn-by-turn (human prompt → agent actions)")
|
|
357
|
+
for t in norm["turns"]:
|
|
358
|
+
ide = " · [had IDE file context]" if t["had_ide_context"] else ""
|
|
359
|
+
pm = f" · mode={t['permissionMode']}" if t.get("permissionMode") else ""
|
|
360
|
+
L.append("")
|
|
361
|
+
L.append(f"### U{t['idx']} ({(t['ts'] or '')[:19]}{pm}{ide})")
|
|
362
|
+
L.append("> " + t["text"].replace("\n", "\n> "))
|
|
363
|
+
acts = t.get("actions", [])
|
|
364
|
+
tool_line = _summarize_actions(acts)
|
|
365
|
+
if tool_line:
|
|
366
|
+
L.append("")
|
|
367
|
+
L.append(f"_agent did:_ {tool_line}")
|
|
368
|
+
L.append("")
|
|
369
|
+
return "\n".join(L)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _summarize_actions(acts):
|
|
373
|
+
bits = []
|
|
374
|
+
tc = Counter()
|
|
375
|
+
for a in acts:
|
|
376
|
+
if "tool" in a:
|
|
377
|
+
tc[a["tool"]] += 1
|
|
378
|
+
for name, n in tc.most_common():
|
|
379
|
+
bits.append(f"{name}×{n}")
|
|
380
|
+
# include first few concrete targets for color
|
|
381
|
+
targets = [a["target"] for a in acts if a.get("tool") in ("Bash", "Write", "Edit", "Read") and a.get("target")][:3]
|
|
382
|
+
s = ", ".join(bits)
|
|
383
|
+
if targets:
|
|
384
|
+
s += " | e.g. " + " ; ".join(targets)
|
|
385
|
+
return s
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def main():
|
|
389
|
+
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
|
390
|
+
want_json = "--json" in sys.argv
|
|
391
|
+
out_dir = None
|
|
392
|
+
if "--out" in sys.argv:
|
|
393
|
+
i = sys.argv.index("--out")
|
|
394
|
+
if i + 1 < len(sys.argv):
|
|
395
|
+
out_dir = sys.argv[i + 1]
|
|
396
|
+
args = [a for a in args if a != out_dir]
|
|
397
|
+
if not args:
|
|
398
|
+
print(__doc__)
|
|
399
|
+
sys.exit(1)
|
|
400
|
+
for path in args:
|
|
401
|
+
if not os.path.exists(path):
|
|
402
|
+
print(f"!! not found: {path}", file=sys.stderr)
|
|
403
|
+
continue
|
|
404
|
+
norm = normalize(path)
|
|
405
|
+
md = render_md(norm)
|
|
406
|
+
print(md)
|
|
407
|
+
print("\n" + "=" * 80 + "\n")
|
|
408
|
+
base = out_dir or os.path.dirname(os.path.abspath(path))
|
|
409
|
+
if want_json or out_dir:
|
|
410
|
+
os.makedirs(base, exist_ok=True)
|
|
411
|
+
jp = os.path.join(base, f"{norm['session_id']}.session.json")
|
|
412
|
+
with open(jp, "w") as f:
|
|
413
|
+
json.dump(norm, f, indent=2)
|
|
414
|
+
print(f"[wrote {jp}]", file=sys.stderr)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
if __name__ == "__main__":
|
|
418
|
+
main()
|