Multi-turn eval megabranch: eval builder + claim/evidence pipeline + upstream pre-integration - #1567
Multi-turn eval megabranch: eval builder + claim/evidence pipeline + upstream pre-integration#1567chiang-daniel wants to merge 234 commits into
Conversation
- Add DataGuide.source (manual | kiln_pro); save endpoint preserves it on edit - Remove batch_guidance end-to-end (API, _combine_guidance, UI, tests) - Refine metaprompter branches on source: kiln_pro is feedback-only surgical edits - Drop # Reference Inputs from analyze + refine; resolve task runtime prompt via default run config - New data_guide_chooser comparison page; revert synth intro to set-up/skip - Persist uploaded copilot docs to Document Library (tag data_guide_example) - Simplify Generation Options modal copy - CopilotAuthPage skips connect screen when already connected - Show Source in saved Data Guide properties; restore card top padding Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the /respond SDK module and its supporting wire types (RespondRequest/Response, SyntheticUserDriverConfig, ConversationTurn, the nested SyntheticUserInfo model). Per-turn synthetic-user invocation moves to OSS at libs/core/kiln_ai/synthetic_user/ in a subsequent commit. Collapses SyntheticUserCase.synthetic_user_info to a single tagged blob string: <persona>...</persona><goal>...</goal><behavior_guidance>...</behavior_guidance> The server treats the blob as opaque; the local player parses it. Adds a typed `code` literal on /generate's 502 response (llm_unavailable | upstream_invalid_output) so callers can discriminate between transient model failures and unparseable model output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OSS-side per-turn synthetic-user invocation — the replacement for kiln_server's removed /respond endpoint. Lives in libs/core/kiln_ai/synthetic_user/ so the runner can call the LLM using the user's own provider keys rather than a hosted endpoint. Modules: - models — Pydantic SyntheticUserInfo (parsed form) + SyntheticUserDriverConfig. - parser — tagged-blob ↔ SyntheticUserInfo. Required: <persona>, <goal>; optional: <behavior_guidance>. Unknown tags ignored (forward-compat). - role_swap — flips eval-frame user/assistant labels into LLM-frame labels; raises on system/tool roles (the driver filters those upstream) and on non-string content. - prompt — persona-playing system prompt. No <DONE>/<CANCEL> guidance: drive loop is fixed-length; SU stays engaged across the conversation. - driver — SyntheticUserDriver. Parses the blob once at construction, renders the system prompt once, builds the adapter once. respond() filters visible roles, role-swaps, prepends the system prompt as prior_trace[0], calls adapter.invoke_returning_run_output (in-memory — the SU never persists a TaskRun), returns the raw string. 56 unit tests covering: parser roundtrip / required-tag enforcement / whitespace / unknown-tag forward-compat; role_swap empty/alternating/ preserves-order/raises-on-system-or-tool; prompt structural assertions (persona/goal/conventions present, behavior_guidance only when set, no <DONE>/<CANCEL>); driver happy path, role-swap shape, custom visible_roles, ends-on-assistant invariant, non-string output guard, parse-error on construction, adapter reuse across turns. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up an OpenAPI description on GenerateSyntheticUsersResponse.cases documenting the strict-N batch contract. No shape change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Thin async wrapper around the SDK's /v1/synthetic_user/generate endpoint. The SDK now parses 401/422/500/502 into typed response models, so the wrapper switches on the parsed type rather than reading raw bytes — 502 surfaces its typed `code` literal (llm_unavailable | upstream_invalid_output) directly to callers. No retry loop. /generate is a once-per-batch authoring call; kiln_server's pipeline already retries transient provider failures internally before returning 502, so a 502 reaching us is a genuine per-batch failure that should propagate. Drops the v1 client's SyntheticUserTransientError + backoff machinery. No /respond. Per-turn synthetic-user invocation lives at libs/core/kiln_ai/synthetic_user/ and runs locally with the user's keys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an explicit "your entire output is the user's next message, verbatim
and nothing else: no narration, no meta-commentary, no quotes, no labels
like 'User:'" clause to the persona-playing system prompt.
A team running similar SU-driven evals reported the persona-playing
model frequently breaks character — narrating ("I would now ask..."),
self-evaluating, or labeling its output. This clause pins that down at
the prompt boundary so we don't end up reaching for post-processing
band-aids later.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
drive_loop.py:
- drive_case(*, case, target_invoker, su_driver, turns, on_turn) runs the
loop for exactly `turns` iterations — no early termination, no
stop_signal plumbing. Returns DriveCaseResult(chain) with the persisted
TaskRun chain.
- TargetInvoker + TurnHook Protocols. The SU driver does all role
filtering / role swap / invariant checks internally; the drive loop
passes the cumulative trace as-is.
runner.py:
- run_cases_batch is an async generator yielding typed BatchEvents
(BatchStartedEvent / TurnCompletedEvent / CaseCompletedEvent /
CaseFailedEvent / BatchCompletedEvent). No stop_signal/stop_reason
fields — drive loop is fixed-length.
- Constructs a SyntheticUserDriver per case; a malformed
synthetic_user_info blob surfaces as a CaseFailedEvent for that case
alone (other cases continue).
- _make_target_invoker / _build_input_source / _tag_leaf patterns kept
from the prior v1 commits (target persistence + SU attribution
unchanged). input_source now carries the opaque blob on the root run
+ slim {batch_tag, turn_index} on subsequent turns.
- Per-case try/except now WRAPS _tag_leaf too, so a save_to_file failure
surfaces as case_failed instead of silently disappearing into
asyncio.gather(return_exceptions=True). Same try also wraps the
target_invoker construction.
- Case tasks are kicked off before the first BatchStartedEvent yield and
the entire drain loop is inside a try/finally that cancels them on
consumer disconnect — fixes the v1 issue where browser disconnect kept
the request alive for the full duration of every in-flight case.
14 tests cover: input validation, happy-path event stream, leaf tagging,
auto-generated batch_tag, malformed blob → case_failed, target invoke
failure → case_failed, tag-save failure → case_failed, concurrency
semaphore enforcing max-in-flight, root vs slim input_source
attribution, and consumer cancellation propagating to case tasks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two routes for the multi-turn synthetic-user data-generation pipeline: - POST .../multiturn_sdg/generate_cases (sync JSON) - POST .../multiturn_sdg/run_cases_batch (SSE via CancellableStreamingResponse) Wires connect_multiturn_sdg_api into desktop_server.make_app and registers the Multiturn SDG tag in kiln_server's tags_metadata so the regenerated api_schema.d.ts surfaces the routes in the typed client. Both routes guard task.turn_mode == multiturn before doing any upstream work and route SyntheticUserClient typed errors through to faithful HTTP statuses (401/422/502 preserved, not collapsed). The SSE route threads build_save_context(request) into run_cases_batch and uses an isinstance whitelist on the JSON encoder so future Pydantic types on the wire need explicit review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rename total_cost -> target_total_cost on CaseCompletedEvent and BatchCompletedEvent. The runner only sees target adapter spend; the SU driver's per-turn cost isn't rolled up here. Old name was misleading in a beta where users pick the SU model. - Thread an optional save_context through run_cases_batch and wrap the leaf-tag save. Adapter writes inside adapter.invoke still bypass — a kiln_ai-side gap shared with the chat SSE pattern, documented in the runner docstring. - Add a re-run idempotency test for _tag_leaf to lock in the spec's "set-union + sort, preserves pre-existing tags" contract. - Drop the dead UNSET/None branch in client._code_or_default; the remaining one-liner has identical behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rename DEFAULT_TURNS -> MAX_TURNS_DEFAULT to match spec naming. - Name asyncio.create_task instances so debug dumps point at this code. - Pre-assert non-empty seed_prompt in drive_case (assert-loud invariant). - Document invariants on _make_target_invoker (sequential-per-case), _tag_leaf (one-writer-per-leaf), and _close_when_done (final put on cancel path goes into the void). - Drop the unreachable generic fallback in _to_http_exception; tighten the param type to the two real subclasses so the type checker enforces exhaustiveness at the call site. - Log a warning in _format_validation_detail when every item is skipped so a silent SDK shape drift surfaces. - Tests: parameterize turns<1 with negatives, lock in _event_to_payload's unregistered-event guard, and couple the auto-batch_tag test to the public regex instead of the implementation. - Stale "Phase 3" docstring scrub + f-string cosmetic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root TaskRun's input_source.properties now carries the decomposed SU case context — persona, goal, behavior_guidance (when present), seed_prompt — instead of the opaque tagged blob. Lets dataset readers and eval tooling inspect SU attribution by direct property access rather than re-parsing the XML each time. The blob is losslessly reconstructable from these fields via build_synthetic_user_info if a downstream tool needs the original wire form. Parse happens once per case in _build_input_source on the root turn; the SU driver constructor already validated the blob, so the re-parse here can't surface a new error class. behavior_guidance is omitted when the parser returns None (the DataSource validator rejects empty strings). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SyntheticUserDriver.respond now returns (message, cost) — the per-call cost is read from the in-memory TaskRun's usage.cost (the only place SU spend surfaces, since SU turns aren't persisted as TaskRuns). drive_case accumulates su_total_cost across turns and exposes it on DriveCaseResult. The runner adds it to the leaf's cumulative_usage.cost to produce an honest CaseCompletedEvent.total_cost — renamed from target_total_cost since the field now reports total spend, not just the target adapter's. BatchCompletedEvent.total_cost sums across successful cases the same way. Matters now because the SU model is user-selectable: someone picking Sonnet for higher-quality probes would have had ~half their spend invisible under the old target-only total. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…source Every input to the filter has stronger upstream protection now: seed_prompt is asserted non-empty in drive_case; persona and goal are required-non-empty by parse_synthetic_user_info; behavior_guidance is already conditionally skipped if None; the remaining keys are Pydantic- validated or non-string. The filter was guarding nothing. The DataSource validator stays as the real backstop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure relocation + boundary update; behavior unchanged.
run_cases_batch and drive_case now live at
libs/core/kiln_ai/synthetic_user/{runner,drive_loop}.py alongside the
existing SyntheticUserDriver. Same neighborhood as EvalRunner /
RagJobRunner / ExtractorRunner — runners belong in libs/core.
To make libs/core SDK-agnostic, introduce a small
kiln_ai.synthetic_user.SyntheticUserCase Pydantic model (two fields,
field-identical to the kiln_server SDK's case shape). The
multiturn_sdg_api route validates dicts straight into the libs/core type
via Pydantic, so the runner never sees the SDK class. The SDK case is
still used for `/generate_cases` output via `to_dict()` — nothing
about that pro-gated authoring path changes.
Tests move with the code. studio_server keeps only the SDK-wrapper
SyntheticUserClient and the FastAPI route, which is exactly the
established shape for eval_api driving EvalRunner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… role_swap Tool-using targets emit assistant turns with content=None and tool_calls set — pure tool dispatches, not user-facing speech. Pre-this-fix, those hit role_swap's strict-content invariant and crashed the SU run. Gemini's suggestion (coerce None → "") would have let them through but degraded the SU LLM's conversation view to consecutive user turns with empty content — silently worse than the crash. The right place to filter is at the driver, next to the existing visible_message_roles filter — "what's visible to the SU" is the driver's responsibility. role_swap stays strict on None content (the trip wire for any caller bypassing the driver's filter). Filter predicate: drop assistant turns where content is None. Keep assistant turns that carry text alongside tool_calls — the text is user-facing speech the SU should respond to. Addresses gemini-code-assist comment on PR #1441 / role_swap.py without applying the suggested empty-string coercion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…context Fix comment numbering in driver.py (4→5), correct "greedy" to "non-greedy" in parser.py, remove inaccurate drive-loop claim from studio_server __init__. Strip historical /respond migration references, remove app-layer concerns (SSE, @no_write_lock) from SDK-level docstrings, deduplicate cost-attribution explanations across driver/runner/drive_loop. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
get_eval_configs_score_summary calls normalize_rating on every output score, and normalize_rating raises on custom-typed scores (they have no normalization). Once custom scores become creatable, one custom score whose json_key collides with a human rating (e.g. a metric named "Overall Rating") turns into an unhandled 500 for the whole endpoint. Correlation against human ratings is undefined for unbounded metrics, so skip them in the loop. Landed ahead of the datamodel change that makes custom scores creatable, so the endpoint is never exposed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pairs a tool result to its originating call by tool_call_id and returns the text content, flattening list-of-blocks content. Guards the sandbox error surface: falsy ids return "" instead of pairing with legacy entries lacking the key (None == None), malformed blocks (text: None) coerce instead of raising mid-scorer, and first-match-wins on duplicate ids is documented. Co-authored-by: Mike Chatzidakis <mike@getkiln.ai> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Returns (link_text, target) pairs for inline markdown links; the advertised use is link checks, so the extraction must not mangle real-world targets. Handles one level of balanced parens in URLs (wikipedia-style) and strips optional titles; images are excluded and inline code spans ignored. The unsupported subset (reference-style links, nested brackets, angle-bracket targets) is documented and the behavior pinned with tests. Never raises. Co-authored-by: Mike Chatzidakis <mike@getkiln.ai> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Role counter for trace messages. The docstring is explicit that this counts messages, not tool calls -- one assistant message may carry several tool calls or none -- and points tool-call counting at get_tool_calls/count_tool_calls, so sample scorers don't misuse it. Co-authored-by: Mike Chatzidakis <mike@getkiln.ai> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The assistant's own output surface for corruption checks: message content (string or text blocks), refusal text, and reasoning (include_reasoning=True default). Tool results are never included. Tool-call ARGUMENTS are opt-in (include_tool_calls=False default): they're JSON-serialized, so non-ASCII text can appear as \uXXXX escapes and mislead corruption regexes -- documented. Tool NAMES are never included; they're schema identifiers, not emitted text. Co-authored-by: Mike Chatzidakis <mike@getkiln.ai> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
get_usage_totals, get_total_latency_ms, and get_error_tool_results: health-metric primitives over per-message trace fields (MessageUsage, latency_ms, is_error/error_message -- all real persisted fields). The trace is a sandboxed scorer's only access to usage data. Caveats live in the docstrings: absent usage sums to 0.0 (indistinguishable from a genuine zero), latency sums across seeded multi-session traces, and error results only include explicitly flagged failures. Usage values are duck-typed dict-or-object: JSON transports deliver dicts, the in-process pickle transport delivers MessageUsage objects -- covered end-to-end by a real-sandbox test. Co-authored-by: Mike Chatzidakis <mike@getkiln.ai> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Allow TaskOutputRatingType.custom on EvalOutputScore: unbounded numeric metrics (tokens, cost, latency, counts) validated as any finite number. Deletes the validator that rejected custom outright. Product constraint, enforced two ways: judges structurally can't emit custom keys (build_score_schema skips them), so one custom score makes the whole eval code-eval-only. EvalConfig rejects LLM-judge configs on custom-score evals at creation; BaseEval re-checks at construction as defense in depth, since load-from-file validation runs before the parent link exists (hand-edited files). Polish over the reference implementation: dead validator removed, EvalOutputScore.type description updated + api_schema.d.ts regenerated, rating_name() gets a 'custom' label. Co-authored-by: Mike Chatzidakis <mike@getkiln.ai> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both markdown regexes were quadratic (found in the phase's adversarial
review): a run of backticks made the code-span pattern backtrack one
character per start position (200k backticks = ~90s), and adjacent
whitespace consumers around a possibly-empty target did the same for
"[a](" + spaces. These run in the sandbox on model output, and the
global code-eval lock turns one stalled scorer into a stalled queue.
Code-span pattern now matches backtick runs in one pass; the link
pattern captures the parenthesized interior (one level of nesting) and
splits target from optional title in a second, unambiguous pass. All
previous semantics pinned by the existing tests are unchanged; adds
adjacent-link and unquoted-space cases plus timing regression tests
(pathological 100k-char inputs, generously bounded).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review finding on the custom-score feature: the guard only rejected LLM judges, but the check-type adapters (exact_match, pattern_match, etc.) fill every declared score key with 0.0/1.0 via build_binary_scores -- a custom "total cost" metric would silently record meaningless binary values. The product constraint is that only code evals can produce custom metrics, so EvalConfig and the BaseEval defense-in-depth check now reject any non-code-eval config on a custom-score eval (EvalConfig.is_llm_judge is replaced by is_code_eval). Same review, same feature: count_human_evals now skips custom scores like the correlation loop does -- humans can't rate a custom metric, so counting them pinned every item at "partially rated" and the eval progress UI could never pass the human-ratings step. The compare-judges page stops rendering permanently-empty columns (and no-op sort keys) for custom scores, and the code-eval snippet generator describes custom scores as unbounded metrics instead of pass/fail. Adds an end-to-end round-trip test (custom-score EvalRun through a real parent chain on disk), a check-type rejection test, and rated-count assertions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Score validation: an int too large for float (10**400) raised a raw OverflowError from the float coercion / math.isfinite before the finiteness guards could fire -- both layers now report the intended finite-number error instead of throwing. Helper hardening on malformed trace data: a present-but-null (or non-dict) "function" in a tool call no longer raises from get_tool_calls or get_assistant_emitted_text; _field returns None if a pickled object's attribute access throws. get_tool_result_content's block flattening now matches get_assistant_emitted_text: textless blocks are dropped (no stray newlines) and legacy blocks carrying text under "content" are read. Docstring corrections: the pickle transport is multiprocessing into the sandbox subprocess, not in-process (the B5 helper commit message overstates this); get_tool_result_content pairs all get_tool_results shapes, not only role "tool". Also replaces a stale model_construct workaround in the score-summary test -- custom scores construct normally now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two implementations of the usage sum exist deliberately -- typed save-time validation for TaskRun.usage vs never-raise duck-typing in the sandbox helper. Cross-check them on the same trace and assert the helper's keys cover every MessageUsage field, so a new usage field or a semantic drift fails CI instead of quietly disagreeing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eval helper fixes + custom-typed eval scores (supersedes #1568)
* Add GPT-5.6 Sol, Terra, and Luna to model list (openai, openrouter) * Fix GPT-5.6 Terra and Luna editorial notes (were copied from Sol flagship) * Demote GPT-5.4 featured_rank so only the current flagship is featured * Generalize copied OpenRouter reasoning comment in GPT-5.6 entries --------- Co-authored-by: Claude <noreply@anthropic.com>
cleanup_session closed per-session AsyncExitStacks in creation (FIFO) order; with two live local MCP sessions their stdio/ClientSession anyio cancel scopes interleave on the request task, so the first aclose exits a non-current scope, corrupting the task's cancel-scope stack (surfaces as 500 'No response returned.' via the middleware unwind). Reverse the teardown so scope blocks close strictly LIFO. Validated live against a two-server run config (api_mcp + models_mcp). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 9badc5b)
get_eval_config_score_summary, get_eval_results_summary, and get_run_config_eval_scores now serve EvalInput-sourced evals instead of 400ing or silently omitting them. New expected_item_ids_for_eval helper dispatches on the eval's slice source (TaskRun ids via eval_set_filter_id, EvalInput ids via eval_input_filter_id); compute_score_summary keys each EvalRun on (dataset_id | eval_input_id); get_eval_progress reuses the same helper so the endpoints can't drift. The results-summary cache is keyed (source, filter_id) since both filter grammars share tag::. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add Eval Data is hidden with accurate copy on the spec eval page and the cross-eval compare empty-state (the add-data flow tags TaskRuns, which EvalInput evals don't read). The compare page's stale 'multi-turn coming soon' banner becomes a stored-trace-tier note shown only for full_trace + eval_set_filter_id evals. The generate-tab picker alert no longer claims builder evals aren't tag-defined. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After run_comparison, the harness now asserts score_summary, eval_results_summary, and run-config eval_scores all report real sizes, scores, and completion for the EvalInput-sourced eval (zero extra model calls). Green live 2026-07-15. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… reuse
Multi-turn EvalInput evals now drive each conversation once per (scenario,
run config) and share it across sibling eval configs, so comparing judges
scores the same samples instead of paying for and confounding fresh drives.
- EvalRun gains additive drive_fingerprint ("v1:" + sha256 of drive config
+ resolved run-config properties + scenario content; reference data and
judge properties excluded — they shape judgment, not the drive).
- New compute_drive_fingerprint helper (drive_fingerprint.py) with
determinism/sensitivity unit tests.
- v2 persist sites always store task_run_trace (full_trace and success-only
gates removed): chain-leaf task-run-eval, single-turn EvalInput,
single-turn fresh-gen, and multi-turn synthetic records keep their
conversation even on judge skips. eval_config_eval (golden calibration)
and the legacy g_eval path are untouched.
- _run_v2_multi_turn_synthetic_job consults a reuse index built once per
runner invocation (O(1) per job): healthy stored traces (full turn count,
ends with assistant text) matching (fingerprint, run_config_id) skip the
drive and are judged via new EvalTaskInput.from_eval_input_trace. Fresh
drives publish in-memory so same-invocation siblings reuse them;
first-write-wins with a deterministic pick across racing writers.
Tombstones and fingerprint-less records never satisfy reuse; reuse never
crosses run configs; no TaskRuns are written by eval-time drives.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 2 of eval trace reuse: the reuse index now scans every v2 eval config under the task instead of only the job's sibling configs, so two judges forced onto separate Evals (custom-score constraint) score the same driven conversation instead of paying for and confounding two drives. Same (drive_fingerprint, task_run_config_id) key, same healthy- trace rules, same first-write-wins pick — now ordered by (eval id, config id, run id) across evals. Health stays keyed to the current eval's turn setting; records driven with different turns fingerprint differently and were never reachable anyway. Cross-eval unit tests cover the hit (no drive, byte-identical trace and fingerprint persisted), different drive configs never matching, the run-config boundary holding across evals, and the deterministic pick spanning evals. Paid harness Step 7 gains the reuse leg: driven records must carry a v1: fingerprint, and a second judge config re-running run_comparison must consume the stored conversations — byte-identical traces, equal fingerprints, zero new TaskRuns — with the existing transience and trace-inequality-across-run-configs assertions unchanged. Verified green against kiln_server b94d955 (second judge pass 1.2s vs 13.5s drive pass). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
KIL-749: EvalRun.dataset_id keyed on the unsaved fresh generation (id None) and crashed every TaskRun-backed run_comparison item; now keys on the dataset item, matching dedup. Recoverable skips (missing_drive_config / type_not_available) re-collect once their blocking condition lifts and the replacing job deletes the superseded tombstone (partial KIL-755). Cost recording: drive_case_for_eval returns DriveCaseResult; v2 records persist task_run_usage (agent usage + SU driver cost; reused traces record none). run_comparison 400s up front on drive-config/run-config problems; run_calibration 400s on an empty golden set. Save guards: tag-normalized 409, json_key 422 on non-ASCII-only names, reference_answer 400. Judge refine-at-save fallback now logs + captures telemetry instead of silently dropping the validator message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/web_ui/static/fonts/OFL.txt (1)
1-93: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winLicense file addition requires human sign-off, not agent action.
Adding a font license file is a legal/licensing action. As per coding guidelines, "Agents must never make legal decisions, including completing CLA attestations, setting license metadata tags, or adding license files; humans must handle these actions." Please confirm a human has explicitly reviewed and approved bundling this license file (and the underlying Inter font) before merge.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/web_ui/static/fonts/OFL.txt` around lines 1 - 93, Human review and explicit approval are required before adding the OFL.txt license and bundling the underlying Inter font. Do not modify or finalize this licensing addition until a human confirms approval for both the license file and font distribution.Source: Coding guidelines
🧹 Nitpick comments (9)
AGENTS.md (1)
63-71: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueFix minor grammatical errors in the new instructions.
There are slight grammatical issues with "a CLA attestations" and "in metadata file".
📝 Proposed grammatical fixes
### Never Make Legal Decisions as an Agent Agents are not allowed to make any legal decisions, including: - - Filling out a CLA attestations in a PR template - - Setting a license tag in metadata file (OSS/MIT/etc) + - Filling out a CLA attestation in a PR template + - Setting a license tag in a metadata file (OSS/MIT/etc) - Adding license files These all must be done by humans.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@AGENTS.md` around lines 63 - 71, Correct the grammar in the “Never Make Legal Decisions as an Agent” instructions: change “a CLA attestations” to “a CLA attestation” and add the missing article in “in metadata file” so it reads “in a metadata file.”app/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/builder/claim_card.svelte (1)
20-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider moving
tokenize_evidenceintoclaim_evidence.ts.It's a pure string-parsing function with no Svelte dependency, matching the shape of
resolve_citation_span(which already lives inclaim_evidence.tsand is unit-tested inclaim_evidence.test.ts). Every other non-trivial parsing helper in this feature has dedicated tests; this one currently doesn't, and can't easily get them while it's embedded in the component.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/web_ui/src/routes/`(app)/specs/[project_id]/[task_id]/builder/claim_card.svelte around lines 20 - 46, Move the pure tokenize_evidence helper from the Svelte component into claim_evidence.ts alongside resolve_citation_span, export it for reuse, and import it in claim_card.svelte without changing its behavior. Add unit tests in claim_evidence.test.ts covering citation tokenization, unmatched citations, plain text, and mixed evidence.libs/core/kiln_ai/synthetic_user/parser.py (1)
7-8: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDocstring says "Greedy" but the regex is non-greedy. Line 8 describes "Greedy first match" while
_extract(Line 27) uses.*?(non-greedy) and its own docstring (Line 26) says "non-greedy content." The non-greedy form is what makes first-match-wins and duplicate-tag handling correct; the "Greedy" wording could mislead a future edit toward.*and break that. Suggest rewording to "First match, non-greedy content per known tag."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libs/core/kiln_ai/synthetic_user/parser.py` around lines 7 - 8, Update the tag-rules documentation near _extract to replace “Greedy first match per known tag” with wording that accurately describes first-match behavior using non-greedy content. Keep the existing nested-tag limitation and regex implementation unchanged.app/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup/guide_refine_view.svelte (1)
99-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDrop the "replaces the old standalone button" reference.
This comment explains what changed relative to the prior UI rather than documenting current behavior.
As per path instructions, "Do not add comments in code explaining changes; explain changes in chat instead."
✏️ Proposed fix
// --- Edit chooser dialog --- // The header "Edit" action opens this first, letting the user pick between - // editing the guide text by hand or reviewing generated examples (and - // refining). Replaces the old standalone "Refine Data Guide" button. + // editing the guide text by hand or reviewing generated examples (and + // refining).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/web_ui/src/routes/`(app)/generate/[project_id]/[task_id]/data_guide_setup/guide_refine_view.svelte around lines 99 - 103, Remove the historical “Replaces the old standalone ‘Refine Data Guide’ button” wording from the comment above edit_chooser_dialog, leaving only documentation of the dialog’s current behavior.Source: Path instructions
app/web_ui/src/lib/stores/kiln_pro_batch_store.ts (1)
38-60: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueOptional: chain polls with
setTimeoutto avoid overlapping in-flight requests.
setIntervalfires everyPOLL_INTERVAL_MSregardless of whether the previous asynctick()has resolved. If a status GET ever exceeds the interval, requests overlap and a slower/older response can land after a newer one. Self-scheduling the next poll after each completion sidesteps this.♻️ Suggested pattern
- timer = setInterval(tick, POLL_INTERVAL_MS) - // Kick off an immediate poll so the UI updates without waiting a full tick. - tick() + const loop = async () => { + await tick() + if (timer !== null || is_cancelled()) return + timer = setTimeout(loop, POLL_INTERVAL_MS) + } + // Kick off an immediate poll so the UI updates without waiting a full tick. + loop()Note this requires reworking
stop()/timerto asetTimeouthandle. Given it polls the local server every 1s, the risk is low.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/web_ui/src/lib/stores/kiln_pro_batch_store.ts` around lines 38 - 60, Optionally replace the setInterval scheduling around the async tick function with a self-scheduling setTimeout loop that schedules the next poll only after each tick completes, preventing overlapping requests and stale responses. Update timer and stop to manage the timeout handle while preserving immediate polling, cancellation, error handling, and stopping when the status is no longer running.app/desktop/studio_server/jobs/test_api.py (1)
105-114: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winSeveral tests leave long-sleeping jobs uncancelled, unlike the rest of the file.
test_list_filter_by_type(project_scoped sleeps 5s),test_list_filter_by_project_id(two 5s project_scoped jobs),test_list_limit, andtest_list_since_excludes_older(multiple ~2.5s noop jobs) never cancel or await-terminal the jobs they create, unlike every other job-creating test in this file. This leaves danglingasyncio.sleep()tasks that get destroyed (not cancelled) when the test's event loop tears down, typically producing "Task was destroyed but it is pending!" warnings.Fixing this once in the
registryfixture teardown is more robust than patching each test individually.♻️ Proposed fixture-level cleanup
-@pytest.fixture -def registry(monkeypatch): +@pytest_asyncio.fixture +async def registry(monkeypatch): """Patch a fresh registry in for isolation, then register the test workers.""" reg = JobRegistry(max_concurrent=10) monkeypatch.setattr(jobs_api, "job_registry", reg) reg.register_type(NoopJobWorker) reg.register_type(ProjectScopedWorker) reg.register_type(ReconcileCompleteWorker) reg.register_type(NonPausableWorker) - return reg + yield reg + # Guard against tests forgetting to clean up their jobs: cancel anything + # still non-terminal so no sleeping task outlives the test's event loop. + for job_id, record in list(reg._jobs.items()): + if not record.status.is_terminal: + await _safe_cancel(reg, job_id)Also applies to: 253-262, 274-286, 288-293, 296-304
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/desktop/studio_server/jobs/test_api.py` around lines 105 - 114, Update the registry fixture to perform teardown cleanup after each test: cancel all jobs still tracked by the JobRegistry and await their terminal completion before the event loop is closed. Keep the existing isolated registry setup and worker registrations unchanged, and use the registry’s existing job-management APIs rather than modifying the individual list/filter tests.app/web_ui/src/lib/stores/jobs_store.ts (1)
207-224: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
synced/connectionexports bypass the subscriber-count-driven connection lifecycle.The exported
synced/connectionreadables wrap the raw internal writable.subscribedirectly, unlikejobs/active_jobs_countwhich go through the customsubscribethat incrementssubscriber_countto open/close the EventSource. Today every consumer also subscribes tojobs/active_jobs_countalongside them, so the connection still opens — but any future component that subscribes only tosynced/connection(without the job list) would silently never triggerconnect(), freezing atidle/falseforever.♻️ Proposed fix: route synced/connection through the same subscriber-count tracking
+ function wrap_readable<T>(store: { subscribe: Readable<T>["subscribe"] }): Readable<T>["subscribe"] { + return (run, invalidate) => { + if (subscriber_count === 0) { + connect() + } + subscriber_count += 1 + const unsub = store.subscribe(run, invalidate) + return () => { + unsub() + subscriber_count -= 1 + if (subscriber_count <= 0) { + subscriber_count = 0 + disconnect() + } + } + } + } + return { subscribe, - synced: { subscribe: synced.subscribe } as Readable<boolean>, - connection: { - subscribe: connection.subscribe, - } as Readable<JobsConnection>, + synced: { subscribe: wrap_readable(synced) } as Readable<boolean>, + connection: { subscribe: wrap_readable(connection) } as Readable<JobsConnection>, set_project, _disconnect: disconnect, }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/web_ui/src/lib/stores/jobs_store.ts` around lines 207 - 224, Update the returned `synced` and `connection` readables in `createJobsStore` to subscribe through the store’s custom subscriber-counting `subscribe` lifecycle, rather than directly wrapping the internal writable subscriptions. Ensure subscribers to either export independently trigger `connect()` and teardown via `disconnect()` consistently with `jobs` and `active_jobs_count`.app/web_ui/src/routes/(app)/jobs/+page.svelte (1)
53-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMark this placeholder page with a
TODOper repo convention.The temporary nature is only conveyed via UI text; a
TODOcomment would make it discoverable by TODO scans/repo tooling ahead of the "final PR" cleanup.As per coding guidelines: "Use
TODOcomments for temporary code, placeholders, or work that must be addressed before merging, and remove allTODOcomments before the final PR."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/web_ui/src/routes/`(app)/jobs/+page.svelte around lines 53 - 57, Add a TODO comment adjacent to the AppPage declaration identifying this route as a temporary placeholder that must be removed before merging. Keep the existing title, subtitle, and action_buttons behavior unchanged.Source: Coding guidelines
libs/server/kiln_server/test_document_api.py (1)
4781-4821: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest coverage gap vs. sibling tests.
Unlike
test_run_extractor_config_no_tags/test_run_extractor_config_with_tags, this test doesn't assertmock_run_extractor.assert_called_once()or verifyextractor_runner.documents, leaving the end-to-end wiring (filtered docs actually passed to the runner) unverified for the newdocument_idspath.✅ Suggested additional assertions
mock_get_documents_filtered.assert_called_once_with( mock_project, exclude_extracted_by_extractor_config_id=mock_extractor_config.id, target_tags=None, target_document_ids=["doc-a", "doc-b", "doc-c"], ) + mock_run_extractor.assert_called_once() + call_args = mock_run_extractor.call_args[0] + extractor_runner = call_args[0] + assert len(extractor_runner.documents) == 1 + assert extractor_runner.documents[0].id == document.id🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libs/server/kiln_server/test_document_api.py` around lines 4781 - 4821, Extend test_run_extractor_config_with_document_ids to assert mock_run_extractor is called exactly once and verify its extractor_runner argument contains the filtered document in extractor_runner.documents. Preserve the existing request, filtering-arguments, and successful-response assertions while covering that document_ids-selected documents are passed to the runner.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/desktop/studio_server/jobs/api.py`:
- Around line 276-289: Change the get_job_errors endpoint from async def to a
standard def function so FastAPI executes the synchronous
job_registry.run_id_for and error_log.read_errors calls in its threadpool, while
preserving the existing run ID resolution and empty-list behavior.
In `@app/web_ui/src/app.css`:
- Around line 2-15: Remove the quotes around InterVariable in both `@font-face`
font-family declarations and update every downstream font-family reference to
use the unquoted name, ensuring the web UI lint and format checks pass.
In `@app/web_ui/src/routes/`(app)/+layout.svelte:
- Around line 454-457: Remove the existing ProgressWidget invocation from the
sidebar footer, leaving DataGuideProgressWidget as the sole progress widget
inside the footer li.
In
`@app/web_ui/src/routes/`(app)/generate/[project_id]/[task_id]/data_guide_setup/guide_refine_view.svelte:
- Around line 81-83: Update the template around the settings widget and Verify
Changes controls so the settings widget always renders, removing the
editing_has_changes conditional that hides it when there are no edits. Keep it
disabled using the existing verify_disabled state, and ensure the related UI
behavior matches the comment’s “always shown but disabled” intent.
In
`@app/web_ui/src/routes/`(app)/generate/[project_id]/[task_id]/data_guide/refine/+page.svelte:
- Around line 116-122: Update both preview-sample initialization paths,
including run_initial_preview and handle_refine, to pass the API response
through dedupe_by_input before assigning preview_samples or mapping into
reviewed_samples. Match the existing behavior in data_guide_setup/+page.svelte
while preserving the current reviewed-samples fields.
In
`@app/web_ui/src/routes/`(app)/specs/[project_id]/[task_id]/builder/claim_evidence.ts:
- Around line 156-186: Guard the final judgement grading in
build_claim_review_payload so a verdict with agrees === null cannot be passed to
graded_claim and persisted as "disagree". Validate final_judgement_verdict
before constructing the payload, using the existing reviewed-state behavior and
failing or otherwise rejecting incomplete reviews consistently with the
surrounding flow.
In
`@app/web_ui/src/routes/`(app)/specs/[project_id]/[task_id]/builder/plan_prompts_table.svelte:
- Around line 45-66: Replace the hard-coded edit_dialog_id and
document.getElementById lookups in open_edit and save_edit with a local dialog
element reference using Svelte bind:this. Use that reference to call showModal
and close, remove both `@ts-expect-error` directives, and preserve the existing
edit and save behavior.
In
`@app/web_ui/src/routes/`(app)/specs/[project_id]/[task_id]/compare/+page.svelte:
- Around line 1090-1106: Update the conditional rendering block around
eval_data_cache[section.eval_id]?.eval_input_filter_id to first ensure
eval_data_cache[section.eval_id] is loaded, matching the guard used by the
nearby has_default_eval_config === false branch. Keep showing “Add Eval Data”
only for loaded evals without eval_input_filter_id, and show the eval-builder
message when the loaded metadata identifies an EvalInput-typed slice.
---
Outside diff comments:
In `@app/web_ui/static/fonts/OFL.txt`:
- Around line 1-93: Human review and explicit approval are required before
adding the OFL.txt license and bundling the underlying Inter font. Do not modify
or finalize this licensing addition until a human confirms approval for both the
license file and font distribution.
---
Nitpick comments:
In `@AGENTS.md`:
- Around line 63-71: Correct the grammar in the “Never Make Legal Decisions as
an Agent” instructions: change “a CLA attestations” to “a CLA attestation” and
add the missing article in “in metadata file” so it reads “in a metadata file.”
In `@app/desktop/studio_server/jobs/test_api.py`:
- Around line 105-114: Update the registry fixture to perform teardown cleanup
after each test: cancel all jobs still tracked by the JobRegistry and await
their terminal completion before the event loop is closed. Keep the existing
isolated registry setup and worker registrations unchanged, and use the
registry’s existing job-management APIs rather than modifying the individual
list/filter tests.
In `@app/web_ui/src/lib/stores/jobs_store.ts`:
- Around line 207-224: Update the returned `synced` and `connection` readables
in `createJobsStore` to subscribe through the store’s custom subscriber-counting
`subscribe` lifecycle, rather than directly wrapping the internal writable
subscriptions. Ensure subscribers to either export independently trigger
`connect()` and teardown via `disconnect()` consistently with `jobs` and
`active_jobs_count`.
In `@app/web_ui/src/lib/stores/kiln_pro_batch_store.ts`:
- Around line 38-60: Optionally replace the setInterval scheduling around the
async tick function with a self-scheduling setTimeout loop that schedules the
next poll only after each tick completes, preventing overlapping requests and
stale responses. Update timer and stop to manage the timeout handle while
preserving immediate polling, cancellation, error handling, and stopping when
the status is no longer running.
In
`@app/web_ui/src/routes/`(app)/generate/[project_id]/[task_id]/data_guide_setup/guide_refine_view.svelte:
- Around line 99-103: Remove the historical “Replaces the old standalone ‘Refine
Data Guide’ button” wording from the comment above edit_chooser_dialog, leaving
only documentation of the dialog’s current behavior.
In `@app/web_ui/src/routes/`(app)/jobs/+page.svelte:
- Around line 53-57: Add a TODO comment adjacent to the AppPage declaration
identifying this route as a temporary placeholder that must be removed before
merging. Keep the existing title, subtitle, and action_buttons behavior
unchanged.
In
`@app/web_ui/src/routes/`(app)/specs/[project_id]/[task_id]/builder/claim_card.svelte:
- Around line 20-46: Move the pure tokenize_evidence helper from the Svelte
component into claim_evidence.ts alongside resolve_citation_span, export it for
reuse, and import it in claim_card.svelte without changing its behavior. Add
unit tests in claim_evidence.test.ts covering citation tokenization, unmatched
citations, plain text, and mixed evidence.
In `@libs/core/kiln_ai/synthetic_user/parser.py`:
- Around line 7-8: Update the tag-rules documentation near _extract to replace
“Greedy first match per known tag” with wording that accurately describes
first-match behavior using non-greedy content. Keep the existing nested-tag
limitation and regex implementation unchanged.
In `@libs/server/kiln_server/test_document_api.py`:
- Around line 4781-4821: Extend test_run_extractor_config_with_document_ids to
assert mock_run_extractor is called exactly once and verify its extractor_runner
argument contains the filtered document in extractor_runner.documents. Preserve
the existing request, filtering-arguments, and successful-response assertions
while covering that document_ids-selected documents are passed to the runner.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: fca809ee-655c-4a03-ac83-12a36a813d43
⛔ Files ignored due to path filters (47)
app/desktop/studio_server/api_client/kiln_ai_server_client/api/chat/get_client_version_policy_v1_chat_version_policy_get.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/api/copilot/batch_plan_v1_copilot_batch_plan_post.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/api/copilot/build_claim_evidence_v1_copilot_build_claim_evidence_post.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/api/copilot/draft_input_data_guide_v1_copilot_draft_input_data_guide_post.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/api/copilot/refine_judge_prompt_v1_copilot_refine_judge_prompt_post.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/api/jobs/get_data_guide_job_result_v1_jobs_data_guide_job_job_id_result_get.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/api/jobs/start_data_guide_job_v1_jobs_data_guide_job_start_post.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/api/synthetic_user/__init__.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/api/synthetic_user/generate_v1_synthetic_user_generate_post.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/__init__.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/batch_plan_input.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/batch_plan_output.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/build_claim_evidence_input.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/build_claim_evidence_output.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/change.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/citation.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/claim.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/client_version_policy.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/data_guide_job_output.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/data_guide_job_result_response.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/draft_input_data_guide_input.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/draft_input_data_guide_output.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/expected_result.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/final_judgement.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/generate_synthetic_users_request.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/generate_synthetic_users_response.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/generate_v1_synthetic_user_generate_post_response_401.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/generate_v1_synthetic_user_generate_post_response_500.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/generate_v1_synthetic_user_generate_post_response_502.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/generate_v1_synthetic_user_generate_post_response_502_code.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/graded_claim.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/graded_final_judgement.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/graded_trace.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/human_grade.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/jinja_input_transform.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/job_type.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/judge_score.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/kiln_agent_run_config_properties.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/refine_judge_prompt_input.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/refine_judge_prompt_output.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/source.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/synthetic_user_case.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/validation_error.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/desktop/studio_server/api_client/kiln_ai_server_client/models/validation_error_context.pyis excluded by!app/desktop/studio_server/api_client/kiln_ai_server_client/**app/web_ui/package-lock.jsonis excluded by!**/package-lock.jsonapp/web_ui/static/fonts/InterVariable-Italic.woff2is excluded by!**/*.woff2app/web_ui/static/fonts/InterVariable.woff2is excluded by!**/*.woff2
📒 Files selected for processing (256)
.agents/skills/claude-maintain-models/SKILL.md.agents/skills/release-digest/SKILL.md.agents/skills/release-digest/scripts/gather_changes.pyAGENTS.mdapp/desktop/desktop_server.pyapp/desktop/dev_server.pyapp/desktop/git_sync/middleware.pyapp/desktop/git_sync/save_context.pyapp/desktop/git_sync/test_save_context.pyapp/desktop/studio_server/api_models/copilot_models.pyapp/desktop/studio_server/api_models/eval_builder_models.pyapp/desktop/studio_server/batch_plan_api.pyapp/desktop/studio_server/copilot_api.pyapp/desktop/studio_server/data_gen_api.pyapp/desktop/studio_server/eval_api.pyapp/desktop/studio_server/eval_builder_api.pyapp/desktop/studio_server/jobs/__init__.pyapp/desktop/studio_server/jobs/api.pyapp/desktop/studio_server/jobs/error_log.pyapp/desktop/studio_server/jobs/events.pyapp/desktop/studio_server/jobs/models.pyapp/desktop/studio_server/jobs/registry.pyapp/desktop/studio_server/jobs/test_api.pyapp/desktop/studio_server/jobs/test_error_log.pyapp/desktop/studio_server/jobs/test_events.pyapp/desktop/studio_server/jobs/test_registry.pyapp/desktop/studio_server/jobs/workers/__init__.pyapp/desktop/studio_server/jobs/workers/noop.pyapp/desktop/studio_server/multiturn_sdg_api.pyapp/desktop/studio_server/provider_api.pyapp/desktop/studio_server/synthetic_user/__init__.pyapp/desktop/studio_server/synthetic_user/client.pyapp/desktop/studio_server/synthetic_user/test_client.pyapp/desktop/studio_server/test_batch_plan_api.pyapp/desktop/studio_server/test_copilot_api.pyapp/desktop/studio_server/test_data_gen_api.pyapp/desktop/studio_server/test_eval_api.pyapp/desktop/studio_server/test_eval_builder_api.pyapp/desktop/studio_server/test_eval_builder_e2e_paid.pyapp/desktop/studio_server/test_multiturn_sdg_api.pyapp/desktop/studio_server/utils/copilot_utils.pyapp/desktop/studio_server/utils/eval_builder_utils.pyapp/desktop/studio_server/utils/response_utils.pyapp/desktop/studio_server/utils/test_copilot_utils.pyapp/web_ui/.env.exampleapp/web_ui/src/app.cssapp/web_ui/src/lib/api_schema.d.tsapp/web_ui/src/lib/chat/streaming_chat.tsapp/web_ui/src/lib/components/SidebarJobsIndicator.svelteapp/web_ui/src/lib/components/SidebarJobsIndicator.test.tsapp/web_ui/src/lib/components/add_example_dialog.svelteapp/web_ui/src/lib/components/eval_types/code_eval_helpers.test.tsapp/web_ui/src/lib/components/eval_types/code_eval_helpers.tsapp/web_ui/src/lib/components/extraction_dialog.svelteapp/web_ui/src/lib/components/extractor_picker.svelteapp/web_ui/src/lib/components/jobs_dialog.component.test.tsapp/web_ui/src/lib/components/jobs_dialog.svelteapp/web_ui/src/lib/components/jobs_table.svelteapp/web_ui/src/lib/components/jobs_table.test.tsapp/web_ui/src/lib/eval/default_judge.tsapp/web_ui/src/lib/stores/data_guide_job_store.tsapp/web_ui/src/lib/stores/job_status.test.tsapp/web_ui/src/lib/stores/job_status.tsapp/web_ui/src/lib/stores/jobs_api.test.tsapp/web_ui/src/lib/stores/jobs_api.tsapp/web_ui/src/lib/stores/jobs_dialog.test.tsapp/web_ui/src/lib/stores/jobs_dialog.tsapp/web_ui/src/lib/stores/jobs_store.test.tsapp/web_ui/src/lib/stores/jobs_store.tsapp/web_ui/src/lib/stores/kiln_pro_batch_store.tsapp/web_ui/src/lib/ui/animations/refining_animation.svelteapp/web_ui/src/lib/ui/data_guide_progress_widget.svelteapp/web_ui/src/lib/ui/dialog.svelteapp/web_ui/src/lib/ui/documents_table.svelteapp/web_ui/src/lib/ui/floating_menu.svelteapp/web_ui/src/lib/ui/floating_menu_types.tsapp/web_ui/src/lib/ui/icons/copy_documents_icon.svelteapp/web_ui/src/lib/ui/icons/csv_icon.svelteapp/web_ui/src/lib/ui/icons/document_icon.svelteapp/web_ui/src/lib/ui/icons/jobs_icon.svelteapp/web_ui/src/lib/ui/icons/settings_gear_icon.svelteapp/web_ui/src/lib/ui/icons/stars_icon.svelteapp/web_ui/src/lib/ui/increment_ui.svelteapp/web_ui/src/lib/ui/increment_ui.test.tsapp/web_ui/src/lib/ui/kiln_copilot/copilot_auth_page.svelteapp/web_ui/src/lib/ui/run_config_component/run_config_component.svelteapp/web_ui/src/lib/ui/see_all_dialog.svelteapp/web_ui/src/lib/utils/copilot_utils.tsapp/web_ui/src/lib/utils/dedupe_by_input.tsapp/web_ui/src/lib/utils/eval_steps_utils.tsapp/web_ui/src/lib/utils/formatters.tsapp/web_ui/src/lib/utils/get_eval_steps.test.tsapp/web_ui/src/lib/utils/sse.test.tsapp/web_ui/src/lib/utils/sse.tsapp/web_ui/src/lib/utils/task_run_picker.svelteapp/web_ui/src/routes/(app)/+layout.svelteapp/web_ui/src/routes/(app)/docs/library/[project_id]/+page.svelteapp/web_ui/src/routes/(app)/docs/library/[project_id]/upload_file_dialog.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_gen_intro.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide/+page.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide/refine/+page.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide/refine_handoff_store.tsapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_chooser/+page.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_chooser/+page.tsapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup/+page.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup/generation_settings_trigger.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup/guide_preview.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup/guide_refine_view.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup/guide_setup_form.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup/run_options_tiles.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup_copilot/+page.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup_copilot/+page.tsapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup_copilot/[job_id]/+page.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup_copilot/[job_id]/+page.tsapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup_copilot/add_manual_structured_dialog.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup_copilot/add_samples_picker_dialog.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup_copilot/all_samples_dialog.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup_copilot/existing_run_picker_dialog.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup_copilot/import_csv_dialog.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup_copilot/input_examples_uploader.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup_copilot/pending_draft_store.tsapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup_copilot/select_from_library_dialog.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/data_guide_setup_copilot/tag_first_selector.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/generate_samples_modal.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/generated_data_node.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/kiln_pro_batch_plan.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/kiln_pro_inputs.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/kiln_pro_plan_summary.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/kiln_pro_plans_table.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/kiln_pro_prompts_table.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/qna/+page.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/qna/extraction_dialog.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/synth/+page.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/synth_batch_chooser.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/synth_data_guidance_datamodel.test.tsapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/synth_data_guidance_datamodel.tsapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/synth_data_guide.svelteapp/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/synth_kiln_pro.svelteapp/web_ui/src/routes/(app)/generate/data_guide_pro_auth/+page.svelteapp/web_ui/src/routes/(app)/generate/data_guide_pro_auth/+page.tsapp/web_ui/src/routes/(app)/jobs/+page.svelteapp/web_ui/src/routes/(app)/sidebar_rail.svelteapp/web_ui/src/routes/(app)/sidebar_rail_item.svelteapp/web_ui/src/routes/(app)/sidebar_rail_item.test.tsapp/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/+page.svelteapp/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/[spec_id]/[eval_id]/+page.svelteapp/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/[spec_id]/[eval_id]/compare_run_configs/+page.svelteapp/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/[spec_id]/[eval_id]/eval_configs/+page.svelteapp/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/builder/+page.svelteapp/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/builder/+page.tsapp/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/builder/batch_plan_approval.svelteapp/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/builder/batch_plan_guidance.tsapp/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/builder/claim_card.svelteapp/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/builder/claim_evidence.test.tsapp/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/builder/claim_evidence.tsapp/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/builder/claim_evidence_review.svelteapp/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/builder/claim_trace_modal.svelteapp/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/builder/plan_prompts_table.svelteapp/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/compare/+page.svelteapp/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/spec_builder/+page.svelteapp/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/spec_builder/create_spec_form.svelteapp/web_ui/src/routes/(fullscreen)/setup/(setup)/create_task/edit_task.test.tsapp/web_ui/static/fonts/OFL.txtapp/web_ui/tailwind.config.jsapp/web_ui/tests/e2e/act/discover/docs-library.spec.tsapp/web_ui/tests/e2e/act_data_guide_actions.spec.tsapp/web_ui/tests/e2e/act_data_guide_routing.spec.tsapp/web_ui/tests/e2e/act_kiln_pro_batch_generate.spec.tsapp/web_ui/tests/e2e/act_kiln_pro_batch_plan.spec.tsapp/web_ui/tests/e2e/mock_kiln_server/server.tslibs/core/kiln_ai/adapters/data_gen/data_gen_prompts.pylibs/core/kiln_ai/adapters/data_gen/data_gen_task.pylibs/core/kiln_ai/adapters/data_gen/test_data_gen_task.pylibs/core/kiln_ai/adapters/eval/base_eval.pylibs/core/kiln_ai/adapters/eval/drive_fingerprint.pylibs/core/kiln_ai/adapters/eval/eval_helpers.pylibs/core/kiln_ai/adapters/eval/eval_runner.pylibs/core/kiln_ai/adapters/eval/registry.pylibs/core/kiln_ai/adapters/eval/test_base_eval.pylibs/core/kiln_ai/adapters/eval/test_drive_fingerprint.pylibs/core/kiln_ai/adapters/eval/test_eval_helpers.pylibs/core/kiln_ai/adapters/eval/test_eval_runner.pylibs/core/kiln_ai/adapters/eval/test_registry.pylibs/core/kiln_ai/adapters/eval/test_v2_eval_code_eval.pylibs/core/kiln_ai/adapters/eval/v2_eval_code_eval.pylibs/core/kiln_ai/adapters/ml_model_list.pylibs/core/kiln_ai/adapters/model_adapters/adapter_stream.pylibs/core/kiln_ai/adapters/model_adapters/litellm_adapter.pylibs/core/kiln_ai/adapters/model_adapters/test_adapter_stream.pylibs/core/kiln_ai/adapters/model_adapters/test_litellm_adapter.pylibs/core/kiln_ai/adapters/model_adapters/test_thinking_level_paid.pylibs/core/kiln_ai/datamodel/__init__.pylibs/core/kiln_ai/datamodel/claim_review.pylibs/core/kiln_ai/datamodel/copilot_models/input_data_guide.pylibs/core/kiln_ai/datamodel/data_guide.pylibs/core/kiln_ai/datamodel/eval.pylibs/core/kiln_ai/datamodel/json_schema.pylibs/core/kiln_ai/datamodel/task_run.pylibs/core/kiln_ai/datamodel/test_claim_review.pylibs/core/kiln_ai/datamodel/test_eval_model.pylibs/core/kiln_ai/datamodel/test_json_schema.pylibs/core/kiln_ai/synthetic_user/__init__.pylibs/core/kiln_ai/synthetic_user/case.pylibs/core/kiln_ai/synthetic_user/drive_loop.pylibs/core/kiln_ai/synthetic_user/driver.pylibs/core/kiln_ai/synthetic_user/eval_drive.pylibs/core/kiln_ai/synthetic_user/models.pylibs/core/kiln_ai/synthetic_user/parser.pylibs/core/kiln_ai/synthetic_user/prompt.pylibs/core/kiln_ai/synthetic_user/role_swap.pylibs/core/kiln_ai/synthetic_user/runner.pylibs/core/kiln_ai/synthetic_user/test_case.pylibs/core/kiln_ai/synthetic_user/test_drive_loop.pylibs/core/kiln_ai/synthetic_user/test_driver.pylibs/core/kiln_ai/synthetic_user/test_eval_drive.pylibs/core/kiln_ai/synthetic_user/test_models.pylibs/core/kiln_ai/synthetic_user/test_parser.pylibs/core/kiln_ai/synthetic_user/test_prompt.pylibs/core/kiln_ai/synthetic_user/test_role_swap.pylibs/core/kiln_ai/synthetic_user/test_runner.pylibs/core/kiln_ai/tools/mcp_session_manager.pylibs/core/kiln_ai/utils/jinja_engine.pylibs/core/kiln_ai/utils/test_jinja_engine.pylibs/server/kiln_server/document_api.pylibs/server/kiln_server/server.pylibs/server/kiln_server/spec_api.pylibs/server/kiln_server/test_document_api.pylibs/server/kiln_server/utils/agent_checks/annotations/delete_api_jobs_id.jsonlibs/server/kiln_server/utils/agent_checks/annotations/get_api_jobs.jsonlibs/server/kiln_server/utils/agent_checks/annotations/get_api_jobs_events.jsonlibs/server/kiln_server/utils/agent_checks/annotations/get_api_jobs_id.jsonlibs/server/kiln_server/utils/agent_checks/annotations/get_api_jobs_id_errors.jsonlibs/server/kiln_server/utils/agent_checks/annotations/get_api_jobs_id_result.jsonlibs/server/kiln_server/utils/agent_checks/annotations/get_api_jobs_id_wait.jsonlibs/server/kiln_server/utils/agent_checks/annotations/get_api_projects_project_id_tasks_task_id_copilot_data_guide_job_job_id_result.jsonlibs/server/kiln_server/utils/agent_checks/annotations/get_api_projects_project_id_tasks_task_id_copilot_data_guide_job_job_id_status.jsonlibs/server/kiln_server/utils/agent_checks/annotations/get_api_projects_project_id_tasks_task_id_generate_inputs_batch_job_id.jsonlibs/server/kiln_server/utils/agent_checks/annotations/get_api_projects_project_id_tasks_task_id_generate_outputs_batch_job_id.jsonlibs/server/kiln_server/utils/agent_checks/annotations/get_api_provider_verify_kiln_copilot_api_key.jsonlibs/server/kiln_server/utils/agent_checks/annotations/post_api_copilot_classify_spec_description.jsonlibs/server/kiln_server/utils/agent_checks/annotations/post_api_jobs_id_cancel.jsonlibs/server/kiln_server/utils/agent_checks/annotations/post_api_jobs_id_pause.jsonlibs/server/kiln_server/utils/agent_checks/annotations/post_api_jobs_id_resume.jsonlibs/server/kiln_server/utils/agent_checks/annotations/post_api_jobs_type.jsonlibs/server/kiln_server/utils/agent_checks/annotations/post_api_projects_project_id_tasks_task_id_copilot_batch_plan.jsonlibs/server/kiln_server/utils/agent_checks/annotations/post_api_projects_project_id_tasks_task_id_copilot_data_guide_job_start.jsonlibs/server/kiln_server/utils/agent_checks/annotations/post_api_projects_project_id_tasks_task_id_copilot_parse_import_file.jsonlibs/server/kiln_server/utils/agent_checks/annotations/post_api_projects_project_id_tasks_task_id_eval_builder_build_claims.jsonlibs/server/kiln_server/utils/agent_checks/annotations/post_api_projects_project_id_tasks_task_id_eval_builder_refine_judge.jsonlibs/server/kiln_server/utils/agent_checks/annotations/post_api_projects_project_id_tasks_task_id_eval_builder_review_pipeline.jsonlibs/server/kiln_server/utils/agent_checks/annotations/post_api_projects_project_id_tasks_task_id_eval_builder_review_traces.jsonlibs/server/kiln_server/utils/agent_checks/annotations/post_api_projects_project_id_tasks_task_id_generate_inputs_batch.jsonlibs/server/kiln_server/utils/agent_checks/annotations/post_api_projects_project_id_tasks_task_id_generate_outputs_batch.jsonlibs/server/kiln_server/utils/agent_checks/annotations/post_api_projects_project_id_tasks_task_id_multiturn_sdg_generate_cases.jsonlibs/server/kiln_server/utils/agent_checks/annotations/post_api_projects_project_id_tasks_task_id_multiturn_sdg_run_cases_batch.jsonspecs/projects/evals_v2/spec_fidelity_review/unit_27-code-eval.md
💤 Files with no reviewable changes (2)
- app/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/qna/extraction_dialog.svelte
- app/web_ui/src/routes/(app)/generate/[project_id]/[task_id]/generated_data_node.svelte
| async def get_job_errors( | ||
| id: Annotated[str, Path(description="The job id.")], | ||
| run_id: Annotated[ | ||
| str | None, | ||
| Query(description="Read the error log for a specific past run id."), | ||
| ] = None, | ||
| ) -> list[dict[str, Any]]: | ||
| # Always 200, never errors (functional_spec §5). A plain non-reconciling | ||
| # lookup of the current run_id — we don't recompute state for a | ||
| # best-effort diagnostic read. | ||
| resolved_run_id = run_id or job_registry.run_id_for(id) | ||
| if resolved_run_id is None: | ||
| return [] | ||
| return error_log.read_errors(resolved_run_id) |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win
Avoid blocking the event loop with synchronous file I/O.
error_log.read_errors performs synchronous file I/O. Since this endpoint is defined as async def, the synchronous file read will block the asyncio event loop.
To offload this blocking call to FastAPI's threadpool, you can define the endpoint as a standard def function instead of async def (since job_registry.run_id_for is also synchronous).
⚡ Proposed fix
- async def get_job_errors(
+ def get_job_errors(
id: Annotated[str, Path(description="The job id.")],
run_id: Annotated[
str | None,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async def get_job_errors( | |
| id: Annotated[str, Path(description="The job id.")], | |
| run_id: Annotated[ | |
| str | None, | |
| Query(description="Read the error log for a specific past run id."), | |
| ] = None, | |
| ) -> list[dict[str, Any]]: | |
| # Always 200, never errors (functional_spec §5). A plain non-reconciling | |
| # lookup of the current run_id — we don't recompute state for a | |
| # best-effort diagnostic read. | |
| resolved_run_id = run_id or job_registry.run_id_for(id) | |
| if resolved_run_id is None: | |
| return [] | |
| return error_log.read_errors(resolved_run_id) | |
| def get_job_errors( | |
| id: Annotated[str, Path(description="The job id.")], | |
| run_id: Annotated[ | |
| str | None, | |
| Query(description="Read the error log for a specific past run id."), | |
| ] = None, | |
| ) -> list[dict[str, Any]]: | |
| # Always 200, never errors (functional_spec §5). A plain non-reconciling | |
| # lookup of the current run_id — we don't recompute state for a | |
| # best-effort diagnostic read. | |
| resolved_run_id = run_id or job_registry.run_id_for(id) | |
| if resolved_run_id is None: | |
| return [] | |
| return error_log.read_errors(resolved_run_id) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/desktop/studio_server/jobs/api.py` around lines 276 - 289, Change the
get_job_errors endpoint from async def to a standard def function so FastAPI
executes the synchronous job_registry.run_id_for and error_log.read_errors calls
in its threadpool, while preserving the existing run ID resolution and
empty-list behavior.
| @font-face { | ||
| font-family: "InterVariable"; | ||
| font-style: normal; | ||
| font-weight: 100 900; | ||
| font-display: swap; | ||
| src: url("/fonts/InterVariable.woff2") format("woff2"); | ||
| } | ||
| @font-face { | ||
| font-family: "InterVariable"; | ||
| font-style: italic; | ||
| font-weight: 100 900; | ||
| font-display: swap; | ||
| src: url("/fonts/InterVariable-Italic.woff2") format("woff2"); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Drop the quotes around InterVariable to satisfy stylelint.
Stylelint (font-family-name-quotes) flags the quoted name on lines 3 and 10, which will fail the web lint gate. Ensure every downstream font-family reference uses the unquoted name too. As per coding guidelines, web UI lint/format checks must pass before merging.
🔧 Proposed fix
`@font-face` {
- font-family: "InterVariable";
+ font-family: InterVariable;
font-style: normal;
@@
`@font-face` {
- font-family: "InterVariable";
+ font-family: InterVariable;
font-style: italic;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @font-face { | |
| font-family: "InterVariable"; | |
| font-style: normal; | |
| font-weight: 100 900; | |
| font-display: swap; | |
| src: url("/fonts/InterVariable.woff2") format("woff2"); | |
| } | |
| @font-face { | |
| font-family: "InterVariable"; | |
| font-style: italic; | |
| font-weight: 100 900; | |
| font-display: swap; | |
| src: url("/fonts/InterVariable-Italic.woff2") format("woff2"); | |
| } | |
| `@font-face` { | |
| font-family: InterVariable; | |
| font-style: normal; | |
| font-weight: 100 900; | |
| font-display: swap; | |
| src: url("/fonts/InterVariable.woff2") format("woff2"); | |
| } | |
| `@font-face` { | |
| font-family: InterVariable; | |
| font-style: italic; | |
| font-weight: 100 900; | |
| font-display: swap; | |
| src: url("/fonts/InterVariable-Italic.woff2") format("woff2"); | |
| } |
🧰 Tools
🪛 Stylelint (17.14.0)
[error] 3-3: Expected no quotes around "InterVariable" (font-family-name-quotes)
(font-family-name-quotes)
[error] 10-10: Expected no quotes around "InterVariable" (font-family-name-quotes)
(font-family-name-quotes)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/web_ui/src/app.css` around lines 2 - 15, Remove the quotes around
InterVariable in both `@font-face` font-family declarations and update every
downstream font-family reference to use the unquoted name, ensuring the web UI
lint and format checks pass.
Sources: Coding guidelines, Linters/SAST tools
| <li class="mt-auto pt-2 bg-transparent"> | ||
| <DataGuideProgressWidget /> | ||
| <ProgressWidget /> | ||
| </li> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Two progress widgets now render side-by-side instead of one.
The line-range summary describes this as swapping ProgressWidget for DataGuideProgressWidget, but the code adds DataGuideProgressWidget while leaving the existing <ProgressWidget /> in place — both now render in the sidebar footer <li>. If this wasn't intentional (i.e. ProgressWidget should have been removed), it duplicates progress UI for every user.
🔧 If the old widget should be removed
<li class="mt-auto pt-2 bg-transparent">
<DataGuideProgressWidget />
- <ProgressWidget />
</li>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <li class="mt-auto pt-2 bg-transparent"> | |
| <DataGuideProgressWidget /> | |
| <ProgressWidget /> | |
| </li> | |
| <li class="mt-auto pt-2 bg-transparent"> | |
| <DataGuideProgressWidget /> | |
| </li> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/web_ui/src/routes/`(app)/+layout.svelte around lines 454 - 457, Remove
the existing ProgressWidget invocation from the sidebar footer, leaving
DataGuideProgressWidget as the sole progress widget inside the footer li.
| // Verify Changes (and the settings widget above it) are always shown but | ||
| // disabled until there's a non-empty edit to verify. | ||
| $: verify_disabled = !editing_has_changes || editing_is_empty |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Settings widget is hidden, not just disabled, when there are no edits.
Comment says the widget is "always shown but disabled," but the template gates it entirely behind {#if editing_has_changes}. Either the conditional should be dropped so the widget always renders (matching the comment/intended UX), or the comment should be corrected to describe the actual hide/show behavior.
Also applies to: 370-376
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app/web_ui/src/routes/`(app)/generate/[project_id]/[task_id]/data_guide_setup/guide_refine_view.svelte
around lines 81 - 83, Update the template around the settings widget and Verify
Changes controls so the settings widget always renders, removing the
editing_has_changes conditional that hides it when there are no edits. Keep it
disabled using the existing verify_disabled state, and ensure the related UI
behavior matches the comment’s “always shown but disabled” intent.
| if (!data) throw new KilnError("No preview inputs returned", null) | ||
|
|
||
| preview_samples = data as PreviewSample[] | ||
| reviewed_samples = preview_samples.map((s) => ({ | ||
| input: s.input, | ||
| output: s.output, | ||
| looks_good: undefined, | ||
| })) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Missing dedupe_by_input here, unlike the sibling setup flow.
Both run_initial_preview and handle_refine map preview samples straight into reviewed_samples without deduping. The parallel flow in data_guide_setup/+page.svelte wraps the identical API response in dedupe_by_input(data as PreviewSample[]) before mapping. Since both hit the same data_gen_guide_preview endpoint, this page can surface duplicate rows that the setup flow already guards against.
🔧 Proposed fix
+import { dedupe_by_input } from "$lib/utils/dedupe_by_input"
...
if (!data) throw new KilnError("No preview inputs returned", null)
- preview_samples = data as PreviewSample[]
+ preview_samples = dedupe_by_input(data as PreviewSample[])
...
if (!preview_data) throw new KilnError("No preview inputs returned", null)
- guide = refined_guide
- preview_samples = preview_data as PreviewSample[]
+ guide = refined_guide
+ preview_samples = dedupe_by_input(preview_data as PreviewSample[])Also applies to: 190-194
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app/web_ui/src/routes/`(app)/generate/[project_id]/[task_id]/data_guide/refine/+page.svelte
around lines 116 - 122, Update both preview-sample initialization paths,
including run_initial_preview and handle_refine, to pass the API response
through dedupe_by_input before assigning preview_samples or mapping into
reviewed_samples. Match the existing behavior in data_guide_setup/+page.svelte
while preserving the current reviewed-samples fields.
| function graded_claim(claim: Claim, verdict: ClaimVerdict): GradedClaim { | ||
| return { | ||
| claim: claim.claim, | ||
| evidence: claim.evidence, | ||
| expected_result: claim.expected_result, | ||
| human_grade: verdict.agrees ? "agree" : "disagree", | ||
| human_feedback: verdict.why.trim() || null, | ||
| } | ||
| } | ||
|
|
||
| // Build the persisted per-claim grades for one reviewed trace. Only claims | ||
| // the reviewer actually graded are included (sub-claim verdicts are | ||
| // optional); the final judgement is always graded by the time save is | ||
| // reachable (is_trace_reviewed gates it). | ||
| export function build_claim_review_payload( | ||
| trace: TraceClaims, | ||
| review: TraceReview, | ||
| ): ClaimReviewPayload { | ||
| return { | ||
| judge_score: trace.judge_score, | ||
| judge_reasoning: trace.judge_reasoning, | ||
| claims: trace.claims | ||
| .map((claim, i) => ({ claim, verdict: review.claim_verdicts[i] })) | ||
| .filter(({ verdict }) => verdict && verdict.agrees !== null) | ||
| .map(({ claim, verdict }) => graded_claim(claim, verdict)), | ||
| final_judgement: graded_claim( | ||
| trace.final_judgement, | ||
| review.final_judgement_verdict, | ||
| ), | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
build_claim_review_payload can silently mislabel an ungraded final judgement as "disagree".
graded_claim maps verdict.agrees truthy→"agree", falsy→"disagree", so agrees === null (not yet reviewed) collapses into "disagree". build_claim_review_payload filters ungraded sub-claims before grading them, but calls graded_claim on final_judgement_verdict unconditionally. The only current call site (build_graded_traces) is safe because it pre-filters via is_trace_reviewed, but the function itself has no guard — a future direct call (bypassing that filter) would persist a wrong, silent grade rather than failing.
🛡️ Proposed guard
export function build_claim_review_payload(
trace: TraceClaims,
review: TraceReview,
): ClaimReviewPayload {
+ if (review.final_judgement_verdict.agrees === null) {
+ throw new Error(
+ "build_claim_review_payload called before the final judgement was graded",
+ )
+ }
return {
judge_score: trace.judge_score,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function graded_claim(claim: Claim, verdict: ClaimVerdict): GradedClaim { | |
| return { | |
| claim: claim.claim, | |
| evidence: claim.evidence, | |
| expected_result: claim.expected_result, | |
| human_grade: verdict.agrees ? "agree" : "disagree", | |
| human_feedback: verdict.why.trim() || null, | |
| } | |
| } | |
| // Build the persisted per-claim grades for one reviewed trace. Only claims | |
| // the reviewer actually graded are included (sub-claim verdicts are | |
| // optional); the final judgement is always graded by the time save is | |
| // reachable (is_trace_reviewed gates it). | |
| export function build_claim_review_payload( | |
| trace: TraceClaims, | |
| review: TraceReview, | |
| ): ClaimReviewPayload { | |
| return { | |
| judge_score: trace.judge_score, | |
| judge_reasoning: trace.judge_reasoning, | |
| claims: trace.claims | |
| .map((claim, i) => ({ claim, verdict: review.claim_verdicts[i] })) | |
| .filter(({ verdict }) => verdict && verdict.agrees !== null) | |
| .map(({ claim, verdict }) => graded_claim(claim, verdict)), | |
| final_judgement: graded_claim( | |
| trace.final_judgement, | |
| review.final_judgement_verdict, | |
| ), | |
| } | |
| } | |
| function graded_claim(claim: Claim, verdict: ClaimVerdict): GradedClaim { | |
| return { | |
| claim: claim.claim, | |
| evidence: claim.evidence, | |
| expected_result: claim.expected_result, | |
| human_grade: verdict.agrees ? "agree" : "disagree", | |
| human_feedback: verdict.why.trim() || null, | |
| } | |
| } | |
| // Build the persisted per-claim grades for one reviewed trace. Only claims | |
| // the reviewer actually graded are included (sub-claim verdicts are | |
| // optional); the final judgement is always graded by the time save is | |
| // reachable (is_trace_reviewed gates it). | |
| export function build_claim_review_payload( | |
| trace: TraceClaims, | |
| review: TraceReview, | |
| ): ClaimReviewPayload { | |
| if (review.final_judgement_verdict.agrees === null) { | |
| throw new Error( | |
| "build_claim_review_payload called before the final judgement was graded", | |
| ) | |
| } | |
| return { | |
| judge_score: trace.judge_score, | |
| judge_reasoning: trace.judge_reasoning, | |
| claims: trace.claims | |
| .map((claim, i) => ({ claim, verdict: review.claim_verdicts[i] })) | |
| .filter(({ verdict }) => verdict && verdict.agrees !== null) | |
| .map(({ claim, verdict }) => graded_claim(claim, verdict)), | |
| final_judgement: graded_claim( | |
| trace.final_judgement, | |
| review.final_judgement_verdict, | |
| ), | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app/web_ui/src/routes/`(app)/specs/[project_id]/[task_id]/builder/claim_evidence.ts
around lines 156 - 186, Guard the final judgement grading in
build_claim_review_payload so a verdict with agrees === null cannot be passed to
graded_claim and persisted as "disagree". Validate final_judgement_verdict
before constructing the payload, using the existing reviewed-state behavior and
failing or otherwise rejecting incomplete reviews consistently with the
surrounding flow.
| const edit_dialog_id = "builder_plan_prompt_edit_dialog" | ||
| let editing_index: number | null = null | ||
| let editing_value = "" | ||
|
|
||
| function open_edit(index: number) { | ||
| editing_index = index | ||
| editing_value = prompts[index] | ||
| // @ts-expect-error showModal is not typed on HTMLElement | ||
| document.getElementById(edit_dialog_id)?.showModal() | ||
| } | ||
|
|
||
| function save_edit() { | ||
| if (editing_index !== null) { | ||
| const value = editing_value.trim() | ||
| if (value && value !== prompts[editing_index]) { | ||
| on_edit?.(editing_index, value) | ||
| } | ||
| } | ||
| editing_index = null | ||
| // @ts-expect-error close is not typed on HTMLElement | ||
| document.getElementById(edit_dialog_id)?.close() | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Avoid global getElementById lookup for the edit dialog — use bind:this instead.
The dialog is opened/closed via a hard-coded DOM id (document.getElementById(edit_dialog_id)). If this component is ever mounted twice on the same page (plausible given it's a general-purpose "builder-local plan table"), both instances would share the same DOM id, and one instance's edit action could open/mutate the wrong instance's dialog. A local element reference sidesteps this and also removes the two @ts-expect-error casts.
🛡️ Proposed fix
- const edit_dialog_id = "builder_plan_prompt_edit_dialog"
let editing_index: number | null = null
let editing_value = ""
+ let edit_dialog_el: HTMLDialogElement | null = null
function open_edit(index: number) {
editing_index = index
editing_value = prompts[index]
- // `@ts-expect-error` showModal is not typed on HTMLElement
- document.getElementById(edit_dialog_id)?.showModal()
+ edit_dialog_el?.showModal()
}
function save_edit() {
if (editing_index !== null) {
const value = editing_value.trim()
if (value && value !== prompts[editing_index]) {
on_edit?.(editing_index, value)
}
}
editing_index = null
- // `@ts-expect-error` close is not typed on HTMLElement
- document.getElementById(edit_dialog_id)?.close()
+ edit_dialog_el?.close()
}-<dialog id={edit_dialog_id} class="modal">
+<dialog bind:this={edit_dialog_el} class="modal">Also applies to: 218-218
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app/web_ui/src/routes/`(app)/specs/[project_id]/[task_id]/builder/plan_prompts_table.svelte
around lines 45 - 66, Replace the hard-coded edit_dialog_id and
document.getElementById lookups in open_edit and save_edit with a local dialog
element reference using Svelte bind:this. Use that reference to call showModal
and close, remove both `@ts-expect-error` directives, and preserve the existing
edit and save behavior.
| {#if eval_data_cache[section.eval_id]?.eval_input_filter_id} | ||
| <!-- EvalInput-typed slice: data is minted by the | ||
| eval builder; the add-data flow tags TaskRuns, | ||
| which doesn't apply. --> | ||
| <div class="text-xs text-gray-500"> | ||
| No eval data. This eval's data is | ||
| created by the eval builder. | ||
| </div> | ||
| {:else} | ||
| <button | ||
| class="btn btn-xs mt-1" | ||
| on:click={() => | ||
| navigateToAddData(section.eval_id)} | ||
| > | ||
| Add Eval Data | ||
| </button> | ||
| {/if} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Gate on eval_data_cache loading state before choosing "Add Eval Data" vs. "No eval data".
eval_data_cache[section.eval_id]?.eval_input_filter_id is falsy while the eval metadata is still loading (before fetch_eval_data resolves), so this briefly renders — and lets users click — "Add Eval Data" for evals whose data is actually minted by the eval builder, then flips to the correct message once the fetch completes. The sibling branch just above (has_default_eval_config === false, lines ~983-1004) already guards this correctly with eval_data_cache[section.eval_id] !== undefined; this new block should follow the same pattern.
🛡️ Proposed fix
<div class="text-left">
{`#if` getEvalDatasetSize(section.eval_id) === 0}
- {`#if` eval_data_cache[section.eval_id]?.eval_input_filter_id}
+ {`#if` eval_data_cache[section.eval_id] === undefined}
+ <div class="mt-1">
+ <div class="loading loading-spinner loading-xs"></div>
+ </div>
+ {:else if eval_data_cache[section.eval_id]?.eval_input_filter_id}
<!-- EvalInput-typed slice: data is minted by the
eval builder; the add-data flow tags TaskRuns,
which doesn't apply. -->📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {#if eval_data_cache[section.eval_id]?.eval_input_filter_id} | |
| <!-- EvalInput-typed slice: data is minted by the | |
| eval builder; the add-data flow tags TaskRuns, | |
| which doesn't apply. --> | |
| <div class="text-xs text-gray-500"> | |
| No eval data. This eval's data is | |
| created by the eval builder. | |
| </div> | |
| {:else} | |
| <button | |
| class="btn btn-xs mt-1" | |
| on:click={() => | |
| navigateToAddData(section.eval_id)} | |
| > | |
| Add Eval Data | |
| </button> | |
| {/if} | |
| {`#if` eval_data_cache[section.eval_id] === undefined} | |
| <div class="mt-1"> | |
| <div class="loading loading-spinner loading-xs"></div> | |
| </div> | |
| {:else if eval_data_cache[section.eval_id]?.eval_input_filter_id} | |
| <!-- EvalInput-typed slice: data is minted by the | |
| eval builder; the add-data flow tags TaskRuns, | |
| which doesn't apply. --> | |
| <div class="text-xs text-gray-500"> | |
| No eval data. This eval's data is | |
| created by the eval builder. | |
| </div> | |
| {:else} | |
| <button | |
| class="btn btn-xs mt-1" | |
| on:click={() => | |
| navigateToAddData(section.eval_id)} | |
| > | |
| Add Eval Data | |
| </button> | |
| {/if} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app/web_ui/src/routes/`(app)/specs/[project_id]/[task_id]/compare/+page.svelte
around lines 1090 - 1106, Update the conditional rendering block around
eval_data_cache[section.eval_id]?.eval_input_filter_id to first ensure
eval_data_cache[section.eval_id] is loaded, matching the guard used by the
nearby has_default_eval_config === false branch. Keep showing “Add Eval Data”
only for loaded evals without eval_input_filter_id, and show the eval-builder
message when the loaded metadata identifies an EvalInput-typed slice.
Replace the builder-local plan-approval fork (batch_plan_approval + plan_prompts_table, both deleted) with the kiln_pro components the /generate surface ships on main: kiln_pro_batch_plan for plan approval and kiln_pro_plans_table (plain-text status column) as the live board while the pipeline runs. One plan-review surface across the app; the builder keeps its wizard chrome (Back / Continue to Review) outside the shared component. Traded away with the fork: per-row edit-in-place, status pills, and pagination — deletion + regenerate cover plan edits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shared component's "Generate Batch (N)" implies quick sample generation; in the builder the same click starts a long, paid conversation drive. kiln_pro_batch_plan gains an optional generate_button_label prop (default null renders the original label, so /generate is unchanged) and the builder passes "Drive N Conversations". Animation titles track the sub-step they resolve into (Planning Batch → the Batch Plan screen; Creating Synthetic Users → the drive), and the live board is titled Driving Conversations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One production branch for the multi-turn eval work, per the agreed topology: forked from feat/multiturn-megabranch, containing all eval-builder / claim-evidence integration work (previously on dchiang/claim-evidence-review via PR #1441's chain) plus pre-integrated upstream tips: scosman/evals_v2, scosman/code_tools, and sfierro/sdg-batch-plan (which brings data-guide-v2-jobs and the eval-runner transient-retry fix).
Replaces PR #1441 and the dchiang/multiturn-synthetic-user middle hop - this branch supersedes that chain. When the pre-integrated upstream branches land on main and main flows into the megabranch, git dedupes the shared commits.
Notable resolutions in the sdg-batch-plan merge: /generate's prompts table keeps Sam's simplified version and the eval builder forks its full-featured variant locally; eval_steps_utils resurrected at its new $lib/utils location; the retry fix now also covers the v2 multi-turn re-drive path (new test).
Full checks green per commit; paid e2e harness run against the matching kiln_server megabranch.
🤖 Generated with Claude Code