{"name":"Lucie","description":"Learning Understanding & Correction Intelligence Engine — Analyzes student submissions from external assessments (Gradescope), generates narrative reports with story/weakness/advice that feed into student profiles and Gaston's Notes. Also verifies TA grading decisions for quality assurance.","url":"https://lucie.xmmersia.com","version":"2.23.3","protocolVersion":"0.2.5","documentationUrl":"https://github.com/Ayahualulco/Xmmersia-LUCIE","provider":{"organization":"Xmmersia","url":"https://xmmersia.com"},"capabilities":{"streaming":false,"pushNotifications":false,"stateTransitionHistory":false,"extensions":[{"uri":"https://xmmersia.com/extensions/education/v1","description":"Educational assessment analysis and narrative generation","required":true}]},"defaultInputModes":["application/json"],"defaultOutputModes":["application/json"],"skills":[{"id":"analyze_submission","name":"Analyze Submission","description":"Analyze a single student's submission from an external assessment and generate a narrative report with story/weakness/advice. Not the quiz path: quizzes are read locally through the conclave, with Claude on the UVA seat or GLM (Marc's ruling, 2026-11-11). The ta_graded mode, the default, is switched off since 2.20.1 unless the server's LUCIE_LEGACY_QUIZ_PIPELINE is exactly \"on\"; completion_check still runs on the legacy vision pipeline (config/models.json).","tags":["analysis","vision","ocr","feedback","submission","assessment"],"examples":["{\"skill\": \"analyze_submission\", \"parameters\": {\"submission_pdf\": \"base64...\", \"solution_pdf\": \"base64...\", \"computing_id\": \"abc123\", \"student_name\": \"John Doe\", \"ta_grades\": {...}}}","{\"skill\": \"analyze_submission\", \"parameters\": {\"submission_pdf\": \"base64...\", \"solution_pdf\": \"base64...\", \"computing_id\": \"abc123\", \"mode\": \"completion_check\"}}"],"inputModes":["application/json"],"outputModes":["application/json"]},{"id":"batch_analyze","name":"Batch Analyze","description":"SWITCHED OFF since 2.20.0: answers {\"status\": \"legacy_pipeline_disabled\"} unless the server's LUCIE_LEGACY_QUIZ_PIPELINE is exactly \"on\" (no default turns it on). Why: quizzes stay graded by the human grader, and Lucie's explanation of that marking now runs locally through the conclave, with Claude on the UVA seat or GLM (Marc's ruling, 2026-11-11, superseding GLM only of 2026-11-06), with the name box masked; this path could send the Gradescope cover page and put the grader's name into a Claude prompt. When enabled: process multiple submissions from an assessment pack. Parses Gradescope CSVs, matches PDFs, runs analysis on each, and returns results. Storage handled by Portal via Le Veilleur. In completion_check mode, accepts the same three-layer witness inputs as analyze_submission (guidance_text, context_text, solution_structured_text) — applied batch-wide to every student; when guidance_text is omitted, Lucie loads her own Layer 1 from config/guidance.md.","tags":["batch","analysis","gradescope","bulk"],"examples":["{\"skill\": \"batch_analyze\", \"parameters\": {\"assessment_id\": \"week01_lecture01_quiz\", \"submission_pdfs\": [...], \"solution_pdf\": \"base64...\", \"csv_files\": [...], \"mode\": \"ta_graded\"}}","{\"skill\": \"batch_analyze\", \"parameters\": {\"assessment_id\": \"week01_lecture01_attendance\", \"submission_pdfs\": [...], \"solution_pdf\": \"base64...\", \"mode\": \"completion_check\", \"context_text\": \"what this quiz tests...\", \"solution_structured_text\": \"AI-readable answer key...\"}}"],"inputModes":["application/json"],"outputModes":["application/json"]},{"id":"verify_grading","name":"Verify TA Grading","description":"SWITCHED OFF since 2.23.0: answers {\"status\": \"legacy_pipeline_disabled\"} unless the server's LUCIE_LEGACY_QUIZ_PIPELINE is exactly \"on\", as batch_analyze does (before 2.23.0 it reached the legacy vision pipeline with the variable off). When enabled: audit TA grading decisions by comparing their rubric markings against AI evaluation, in blind grading mode, and flag discrepancies for instructor review. The quiz path is the local conclave, with Claude on the UVA seat or GLM.","tags":["verification","audit","ta","grading","rubric","blind"],"examples":["{\"skill\": \"verify_grading\", \"parameters\": {\"csv_data\": \"...\", \"solution_pdf\": \"base64...\", \"submission_pdfs\": [...], \"pages\": [6], \"question_context\": \"Question 2: Preferences\"}}","{\"skill\": \"verify_grading\", \"parameters\": {\"assessment_id\": \"midterm_q2\", \"csv_data\": \"...\", \"solution_pdf\": \"base64...\", \"submission_pdfs\": [...], \"pages\": [5,6]}}"],"inputModes":["application/json"],"outputModes":["application/json"]},{"id":"health_check","name":"Health Check","description":"Check Lucie's operational status including vision engine availability and agent connections.","tags":["health","status","diagnostics"],"examples":["{\"skill\": \"health_check\", \"parameters\": {}}"],"inputModes":["application/json"],"outputModes":["application/json"]}],"xmmersia":{"agentCoreVersion":"1.0.0","inheritsFrom":"BaseAgent","type":"mate","role":"specialist","visibility":"background","skillPattern":"modular","skillDiscovery":"automatic","skillDirectory":"/skills","identity":{"essence":"I am Lucie — Learning Understanding & Correction Intelligence Engine. I transform raw assessment data into insights that help students grow, and ensure fair grading through vigilant verification.","purpose":"To bridge the gap between external assessments and personalized learning by generating narrative reports that illuminate student understanding patterns, while maintaining grading integrity through AI-powered verification.","principles":["Accuracy above all — quizzes are read locally through the conclave, with Claude on the UVA seat or GLM, and the human grader's mark stays the mark","Narratives that guide, not just grade","Every assessment tells a story of understanding","Connect patterns across assessments for holistic insight","Fair grading is a right — verify without bias"],"codeOfHonor":"Le Code d'Honneur de Xmmersia"},"configuration":{"modes":{"ta_graded":{"description":"Analyze submissions with TA-provided Gradescope CSV grades. Switched off since 2.20.0 (since 2.20.1 analyze_submission answers legacy_pipeline_disabled at the top of the call, before any image is read or client built) unless LUCIE_LEGACY_QUIZ_PIPELINE is exactly \"on\"; the quiz path is the local conclave evaluations → feedback → records → release commands, with Claude on the UVA seat or GLM","purpose":"Explain TA grades, identify patterns, generate narrative"},"completion_check":{"description":"Analyze submissions without grades — check engagement","purpose":"Determine if student meaningfully engaged, provide feedback"},"verify":{"description":"Blind grading mode to audit TA decisions. Switched off since 2.23.0 (verify_grading answers legacy_pipeline_disabled) unless LUCIE_LEGACY_QUIZ_PIPELINE is exactly \"on\"","purpose":"Flag discrepancies between TA marks and AI assessment"}},"vision":{"quizPath":"The conclave: Claude on the UVA seat, or GLM (glm-5p3-flash on Fireworks). Marc's ruling, 2026-11-11, supersedes GLM only for quizzes (2026-11-06)","quizReader":"Claude on the UVA seat, or glm-5p3-flash","legacyPipeline":"The legacy vision pipeline (Claude and Gemini, config/models.json) is off unless LUCIE_LEGACY_QUIZ_PIPELINE is exactly \"on\""}},"changelog":{"2.23.3":"The solution on the question, and release --pages. Conclave 0.17.3. records puts solution on questions[].solution as a list of stripped non-empty strings and nowhere else: the seat's feedback files carry it as a list of three strings, which 0.17.0 dropped because it copied a string only. A string becomes a one-element list, a list keeps its string items, absent or empty omits the key, and another type omits it and counts solution_unreadable in the stage summary. solution comes from a feedback file written by the seat's script or by hand; the feedback stage does not write it (README and records.py docstring corrected). release gains --pages <folder>, optional and only with --records: for each posted or replaced token, <folder>/<token>-p1.jpg is re-encoded through PyMuPDF to a web copy no wider than 1400 px at JPEG quality 80 and posted right after its record to Le Veilleur's store_assessment_attachment as {computing_id, offering_id, assessment_id, name: page-1.jpg, content_type: image/jpeg, data_base64}. Each outcome row gains page: posted, replaced, refused:<reason>, no_page (no image, not a failure) or not_attempted (record refused or not reached); a page never counts toward the five-failure stop; the dry run counts tokens with an image; stdout and the log carry tokens only. skills[] unchanged. Contributes to seq 4134; closes none.","2.23.2":"release --test-id accepts the test personas (test_student, test_ms3ae, jam4test), so a hand-made record can be posted to the persona the Portal's View As uses before any student is touched. Conclave 0.17.2. The exemption is --test-id only: the filename-stem rule and the --metadata join stay shape-only. Contributes to seq 4134; closes none.","2.23.1":"Release joins through submission_metadata.yml. Conclave 0.17.1. A Gradescope export names each copy <submission id>.pdf, so the filename stem is not a computing ID (the dry run on the W11 L19 folder counted 180 of 180 malformed). python -m conclave release gains --metadata <file>, optional: when it names a Gradescope submission_metadata.yml, each mapped filename is looked up there, the entry's one submitter's :email: is read (Ruby-symbol or plain keys), and the computing ID is the local part of a @virginia.edu address. No other field is read: :name: and :sid: are never assigned. A filename with no entry (no metadata), an entry with zero or several submitters (multi submitter), an entry with no usable email (no email), or an address at another domain or with a local part of the wrong shape (malformed) refuses the whole run before any post. The dry run prints joined, no metadata, multi submitter and no email beside the existing lines, and release-log.json's plan block carries the new counts. --metadata with --test-record is refused. Without --metadata the stem rule applies as before, for a folder already named by ID. The identity rule's one exception is written into tokens.py's docstring: release opens the file when, and only when, --metadata names it, and reads its email field only. Nothing printed, logged or raised carries a name, an email or a computing ID; an unreadable file refuses with a class name only. skills[] unchanged.","2.23.0":"Solution in the records, and the release command. Conclave 0.17.0. Marc's ruling of 2026-11-11: the quiz path on the UVA seat supersedes \"GLM only for quizzes\" (2026-11-06); skills[], the identity principles and the configuration block now say the quiz path reads with Claude on the UVA seat or GLM through the conclave, and the legacy dual-vision wording is gone. records copies a feedback file's solution (a string) unchanged onto the question entry, omitted when absent, and its model onto each item as reader_model; Le Veilleur's v1 validator allows both. New zero-model python -m conclave release reads a records folder, joins token → filename → computing ID through the submissions folder's token_map.json (L2 and L3 shapes, through tokens.TokenMap), refuses the whole run before any post on an unmapped token, an ID not matching ^[a-z]{2,3}[0-9][a-z]{1,3}$ or an offering mismatch, stamps released_at on an in-memory copy (the files are never rewritten), and posts {computing_id, offering_id, record} to Le Veilleur's store_assessment_record through utils/a2a_client.py (LE_VEILLEUR_URL, XMMERSIA_AGENT_SECRET; either missing refuses by name). Outcomes per token are posted, replaced or refused:<reason>; five consecutive failures stop the run; --dry-run needs no address or secret; --only and --test-record/--test-id post exactly what they name; stdout and release-log.json carry tokens, outcomes and counts only. verify_grading is now gated by LUCIE_LEGACY_QUIZ_PIPELINE like batch_analyze, with the same message (it reached DualVision with the variable off). The score pattern steps over \\(…\\) TeX, so a fraction in TeX passes and 7/10 in prose still fails; inputs_digest (no callers) is removed. 810 tests run, 2 skipped. Contributes to seq 4134; closes none.","2.22.1":"The two sentences enforced by code. Conclave 0.16.1. The two-readers sentence is no longer the model's: when reading_differs is true, code adds DIFFERS_LINE (\"Two readers can see the same page differently, and this note is one reading of your work.\") once, after the model's explanation; the explanation prompt no longer asks for it, and the accept schema refuses a reply containing \"same page differently\". The closing line is now the last sentence the student reads: code puts it at the end of next_step when there is one (the page renders next_step after the explanation), otherwise at the end of the explanation, exactly once. The contact sheet renders full pages with the masks again (it is Marc's mask check); the spec's crop stays on the question images only. Records unchanged in shape; skills[] unchanged. 775 tests run, 2 skipped. Contributes to seq 4134; closes none.","2.22.0":"Explanations under ruling 5, on the certain copies. Conclave 0.16.0. Marc's ruling of 2026-11-08: the grader's score is a clue, not the truth, and stays as graded; Lucie writes from the student's work. The explanation prompt explains what the work did and why the grader's rung applies, never states or argues a score, writes to you, and never uses the words rung, ladder, grader or rubric, which the accept schema now refuses. The schema's agrees becomes reading_differs: when the description of the work does not fit the grader's rung, the last sentence says plainly that two readers can see the same page differently, and the copy is explained, not held (not_supported is gone; feedback_summary.json counts reading_differs). Code, not the model, ends every explained copy with one fixed sentence pointing to Report a problem or office hours. feedback gains --notes (a NOTES FOR THIS QUESTION block after the key), --forbid (repeatable regexes added to the accept schema for the run) and --compare (a copy not marked certain is held as uncertain with no call; a certain one is explained from its agreed reading). The agent card's vision block names the conclave, GLM only, as the quiz path and the legacy pipeline as off unless LUCIE_LEGACY_QUIZ_PIPELINE is exactly on. Fold-in: the sheets stage honours the spec's crop, and a moved crop re-renders. Records keep understanding and next_step; skills[] unchanged. 771 tests run, 2 skipped. Contributes to seq 4134; closes none.","2.21.0":"Zoom, two reads compared by algebra, and a review page. Conclave 0.15.0. A spec may declare a crop per page (page-relative 0-1) and its own dpi; pages renders the full page, paints the masks, then cuts the crop, so the mask rule never depends on it, and a crop outside 0-1 is refused; the spec dpi wins over a seat's and the run's. run --seat runs one seat of a seats file, so two seats read one pass after the other. compare-reads compares two reads' variable_chosen, p_m and q_m with SymPy (equal when the difference simplifies to 0; each value is the last clean value of its text, and one that still will not parse is a disagreement) and, by Marc's ruling, the first_order_condition by its last math segment, recording foc_text_only rather than a disagreement when either will not parse; model text reaches the parser only through a character whitelist, with no builtins. A copy whose reads disagree is uncertain, and held; a flagged sign alone holds nothing and is shown on the review page. hidden-rung runs the hidden rung alone on certain copies, from the first seat's reading, with no explanation call and no key. review writes a local page for Marc (uncertain copies, a per-cell sample of rung disagreements, agreed copies with a flagged sign, a grader x Lucie table for foc_text_only copies, images embedded, written only if the identifier scan is clean). scan is the identifier scan: email, computing-ID and submission-ID shapes, skipping only URLs found verbatim in Le Curriculum's course data, read-only. Fold-ins: strip_gradescope_pages prints counts, never file names; run_summary.json is merged across passes (a --retry-failed pass no longer leaves zeros); verify_grading and health_check build their vision and report clients on first use; conclave/README's quiz commands name the run folder the workspace uses. sympy joins conclave/requirements.txt. 759 tests run, 2 skipped. Contributes to seq 4134; closes none.","2.20.1":"Two fixes and a tracked tool. Conclave 0.14.1. The legacy quiz gate is at the very top: analyze_submission's ta_graded path (its default mode) now refuses before validating, decoding a PDF or reading an image, as batch_analyze already did, and both skills build DualVision and ReportGenerator on first use, so with LUCIE_LEGACY_QUIZ_PIPELINE off no model client is constructed (not at agent start-up either) and no request is sent. Records: a held copy (agrees false, possible_identifier, a failed or invalid output, or no ok feedback file) carries understanding = the grader's rung text (the same as ta_line), next_step null and lucie_status \"held\"; an explained copy carries lucie_status \"explained\"; both pass Le Veilleur's v1 validator once stamped. strip_gradescope_pages.py is tracked at the repo root with one test on a synthetic PDF. 698 tests run, 2 skipped. Contributes to seq 4134; closes none.","2.20.0":"Quizzes: a name mask, the grader's marks by token, a GLM explanation stage, v1 records, and the old pipeline off. Conclave 0.14.0. A spec may declare masks, white rectangles per page in page-relative 0-1 coordinates; pages.py paints them after rendering and before encoding, and page 1 may be read only when the spec masks page 1 itself (a mask elsewhere unlocks nothing; a rectangle outside 0-1 is refused). New zero-call python -m conclave evaluations reads a Gradescope evaluations export into score, adjustment, comment and applied items per token, dropping every identity column unprinted (strip_evaluations.py moved into the library). New python -m conclave feedback: text only, exactly one GLM seat (anything else refused before a key is read), two calls per copy, a hidden rung chosen from the describe read and ladder alone and written only to calibration.csv, and an explanation of the grader's rung (2-3 sentences to you, a next_step below the top rung, agrees) checked with jsonschema, one retry, then held; agrees false, possible_identifier and failures go to held.json with no Lucie text. New zero-call python -m conclave records writes one xmm.assessment_record.v1 per token with ta_line, ta_comment, understanding and next_step on the item, the topic from Le Curriculum topics.json and the attendance-quiz solution video. batch_analyze and ReportGenerator's ta_graded path are off unless LUCIE_LEGACY_QUIZ_PIPELINE is exactly on: they answer legacy_pipeline_disabled with one identifier-free log line. Contributes to seq 4134; closes none.","2.19.0":"Gemini thinking, capped on assessments and recorded everywhere. Conclave L14, 0.13.0. A gemini seat may carry assess_thinking, a thinking level (minimal, low, medium, high) or budget (an integer, -1 or more), sent as google-genai 2.6.0's config.thinking_config on assessment calls only; refused on anthropic and openai_compat seats. In the assessment request hash only when set, never in the read hash, so stored reads stay reusable and a seat without it hashes as under 2.18. Every gemini reply's usage_metadata.thoughts_token_count is recorded as reasoning_tokens and added into output_tokens; no hash moves. Contributes to the MT02 grading arc; closes none.","2.18.2":"The June migration's Render leftovers, in words only. render.yaml is deleted (nothing referenced it; the Procfile is the deploy entry). The README deployment section describes Coolify (Hetzner): a Nixpacks build from the Procfile, env vars set in Coolify, a redeploy on push. The requirements.txt anthropic comment no longer names a model (the model is chosen in code). Two comments that called the poller fix the Render timeout bug now call it the health-check timeout bug. This changelog gains the missing 2.17.1 and 2.18.1 entries. Closes none. No code, dependency or skill changed.","2.18.1":"requirements.txt comment names Coolify, not Render. The gunicorn comment said (Render deployment); it now says Coolify (Hetzner). Drift found by the 2.17.1 certifi campaign. Closes none. Comment only; no code or dependency changed.","2.18.0":"Per-seat extra_body for openai_compat seats (conclave L13, 0.12.0). A seat may carry extra_body, a JSON object merged into the top level of the Chat Completions request after the standard keys, e.g. {\"reasoning_effort\": \"none\"} on Fireworks or {\"reasoning\": {\"effort\": \"none\"}} on OpenRouter, so a reasoning model stops spending its output budget on unseen thinking. Refused on anthropic and gemini seats; may not name model, messages, response_format, max_tokens, max_completion_tokens, temperature, stream or n. Hashed into request_settings as canonical JSON, so changing it re-buys the read; a seat without one hashes exactly as before. Usage gains reasoning_tokens (openai_compat completion_tokens_details.reasoning_tokens or usage.reasoning_tokens; 0 when absent, always 0 on anthropic and gemini). Skills unchanged. Contributes to the MT02 grading arc; closes none.","2.17.1":"certifi floor for the eMudhra CA compromise. requirements.txt adds certifi>=2026.10.19 below httpx: httpx trusts certifi's CA bundle, not the OS store, and 2026.10.19 is the first release without the eight compromised eMudhra intermediates (disclosed 2026-10-19). Contributes to seq TBD; closes none. No code changed; conclave/requirements.txt (local-only) untouched.","2.17.0":"Prompt caching on the anthropic seat (conclave L12, 0.11.0). build_anthropic_payload now sends the prompt text first, as a text block carrying cache_control {type: ephemeral}, then the page images, in the same single user message, so the static per-question text that repeats across every copy is a cacheable prefix; no system block, no warm-up call, concurrency unchanged, prompt text unchanged. Usage gains cache_read_input_tokens and cache_creation_input_tokens (0 by default), read from the anthropic response's usage, summed with the rest and written in every usage record and usage_total; the gemini and openai_compat seats leave them at 0. No request hash covers block order or cache_control, so earlier reads and assessments stay reusable. skills[] unchanged. Contributes to seq 3425; closes none.","2.16.0":"conclave sheets --verdicts-overlay (conclave L11, 0.10.0). A JSON file of final decisions (schema conclave-verdicts-overlay/1: per token, question and part, the click list with weights, an empty look list, the do_not_click list, a note) replaces the computed verdicts on the parts it names, after the verdict engine and before the tally, so the TA cards, for_marc.html and every count follow it. points and readers are kept, a non-empty note goes in front of why as 'Final decision (overlay): …', and the part says overlay: true. Validated once and refused before any file is written on any mismatch: schema, exam_id, every token, question, part and rubric in the run, spec and map, click and do_not_click naming each rubric exactly once, numeric weights. manifest.json gains verdicts_overlay {sha256, copy_count}, null without the flag; release_id unchanged. verdicts.py untouched, skills[] unchanged. Closes none.","2.15.2":"A second Gradescope rubric input: the 26F Econ 3010 Week 08 Lecture 14 weekly quiz, Question 1 (1.1 supply curve, 0.6; 1.2 sketch, 0.4), 8 items on positive scoring, text verbatim from the approved item list. conclave/gradescope/rubrics/ gains Econ3010-26F_w08-l14-quiz.md and the script gen_rubric_js.py writes from it (LF). Data only: template.js, gen_rubric_js.py and the 4170 pair unchanged, no pipeline code changes, conclave stays 0.9.0, skills[] unchanged. Closes none.","2.15.1":"The Gradescope push folder is LF-only. gen_rubric_js.py writes LF (newline='\\n'), and conclave/gradescope/.gitattributes pins the folder to LF in the working tree whatever core.autocrlf says, so the regeneration test compares raw bytes and holds on Windows checkouts. tests/test_gradescope_push.py drops its CRLF normalisation and adds a check that the generated script has no CR. No pipeline code changes, conclave stays 0.9.0, skills[] unchanged. Closes none.","2.15.0":"The Gradescope rubric push. conclave/gradescope/ holds a browser-console tool generated from an approved rubric markdown file: on the assignment's Create Rubric page it writes every rubric item through Gradescope's own rubric_items endpoint with the signed-in session (CSRF token and question IDs read from the page at run time; no credential, no student data), then reads the rubric back and checks every weight and text against the file. MODE 'verify', the default, only checks. Its first input is the 26F Econ 4170 Midterm 01 rubric: 15 questions, 232 items. tests/test_gradescope_push.py pins the parse, the generated script and the template's defaults. A tool folder, not a package: no pipeline code changes, conclave stays 0.9.0, skills[] unchanged.","2.14.0":"The conclave grades an exam that is not Econ 3010's shape, and exports reads for clustering. Conclave L10, 0.9.0. A spec with one version scores every copy without a printed version field (status single_version); a header letter naming another version is still a version conflict, and two or more versions behave as before. key.json may declare _key_cards {question: [field, ...]}; without it the built-in Econ 3010 table applies, and a question in neither gets no card. pages.json may mark a question extra_credit. manifest.json gains page_count, contact_pages and, per question, extra_credit: the Portal's contract, documented in conclave/README.md. The contact sheet and the hand-graded message follow page_count (2-15 and \"sixteen\" for 16 pages; a spec under 12 pages keeps today's output). A unanimous deny says \"click -N\" with the item's own points. New zero-call python -m conclave export-reads <run> --out <file.jsonl>: {token, question, seat, model, read} per successful read, failures counted on stdout. conclave/seats.describe.example.json holds two Gemini seats for a run-twice describe pass.","2.13.0":"Submission IDs on every row, and a placeholder row for a hand-graded copy. Conclave L9, 0.8.0. Every <question>.json row carries submission_id (digits from the copy's file name, else null), and manifest.json lists submission_ids per token. sheets and grade take --hand-graded <id>[,<id>…]: each ID gets a placeholder row in every <question>.json at its place in submission-ID order (copy_index / copy_count renumbered, so card N is Gradescope's submission N), with a derived c-<8 hex> token, verdicts_withheld \"hand_graded\", a TA-facing ta_message, no images, items or verdicts, and the name and computing ID only under --identify; the manifest lists it under hand_graded. An ID that is a copy in the submissions folder is refused. Sheets stage only: no read, no assessment, no provider call; without the flag nothing else changes.","2.12.1":"The why names follow-through, and says \"AI readers\". Conclave L8.1, 0.7.1. When a part clicks a feature rubric, its why ends by naming the do-not-click feature rubrics whose item every reader's check flagged follow_through (the check result's own flags, never its evidence text): \"Rubrics N, M… judged from the student's own drawing (follow-through) — do not click them.\" TA text never says \"seat\": the ta_note templates, the why they feed and student_summary's marker (now \"(AI readers differ)\") say \"AI reader(s)\"; internal ids (seat-a, the manifest's seats) are unchanged. Text only: no verdict, vote or checkpoint changes.","2.12.0":"A sixth question and the coarse map. Conclave L8, 0.7.0. The extra credit is read like any question (q6); sheets.KEY_CARD_FIELDS gains q6 (t, x_intercept, c_intercept, jump_at, jump_size, filled, open). A gradescope.json rubric may draw on several check features: features (known to checks.FEATURES for its item) with fire partial (clicks when at least one feature clicks and at least one awards) or all (clicks when every feature clicks); each feature is settled by the L7 rule first, and an unsettled feature makes a look, never a click. A blank rubric may carry one source (features + fire, or item + source assess) and clicks when the part is blank or that source fires. Anything else still refuses the run; an L7 map loads exactly as before. The why, the readers rows and the tally name every feature a rubric drew on. No checkpoint is invalidated; skills unchanged","2.11.0":"Verdicts per Gradescope rubric. Conclave L7, 0.6.0. The spec's gradescope.json (the item → Gradescope-rubric map, deliberately outside spec_hash and scoring_hash) is loaded and minimally validated — n contiguous, text present, source shapes, real items — and a malformed map refuses the run; map_hash is the sha256 of its bytes and enters no request. run_check passes a check's per-feature results through unchanged; score/<token>.json is output only, so no checkpoint is invalidated. At the sheets stage each mapped part of each copy gets click / look / do_not_click per rubric, from both readers: check features, extraction fields (award_when, >=N numeric or string equality) and the item's assessment. Both deny clicks, both award leaves it, and any split, uncertainty or missing reading is a look — no split ever clicks; always_look, requires_award, per-part blank (read_state_<part>, else the question's read_state), nothing_usable (never a click) and correct follow the map. Each part carries a why and per-reader rows assembled from the existing ta_notes and evidence lines; no model writes anything, and a sheets-only rerun recomputes them. manifest.json gains rubrics, readers, map_hash, spec_version and verdicts_withheld; <q>.json rows gain copy_index and copy_count and, under --identify only, name (group submissions joined with ' & ', multi_submitter true). for_marc.html and cases.html gain per-rubric verdict counts and a histogram of look reasons. Not deployed, not a skill; skills[] unchanged.","2.10.0":"The bundle carries what a TA reads: rubric, key cards and rules in the manifest, what-the-student-did and what-to-do per row, the check's evidence line; an uncertain check no longer blocks a deny. Conclave L6, 0.5.0. manifest.json gains rubric (items.json fields per question), key_cards (per question, per version, the key values a TA needs, copied from key.json) and ta_rules (ta_rules.md verbatim, when present; outside scoring_hash). Each <q>.json row gains student_summary, composed deterministically from the seats' reads over cases.json with seat disagreement marked; each item gains a templated ta_note and, per seat, check_evidence. On an M item an uncertain check now blocks an award only. Nothing is generated by a model. Not deployed, not a skill.","2.9.0":"Penalise once: a check never auto-denies a follow-through item — it flags, a human rules slip-or-new-problem (spec v7.1, ruling 6). Conclave L5, 0.4.0. On an item with follow_through true, a check's deny makes a D vote uncertain and no longer vetoes an M assessment; the vote carries provenance follow_through_check and the item the flag follow_through, and the recommendation is TA look with reason follow_through unless two or more votes are cast and all are deny, so an M item is still denied by a unanimous assessment. follow_through joins the scoring flags as non-forcing; unknown check flags are still tolerated. The assessment prompt marks such items [follow-through] and its frame gains one line, so every assessment re-plans once. Not deployed, not a skill; skills[] unchanged.","2.8.1":"Opus 5.5 compatibility: optional sampling parameters, effort, reply read by block type, provider error text kept (conclave L4, 0.3.1). SMOKE-2's five reads on claude-opus-5-5 all failed HTTP 400, recorded as code unknown with a detail of HTTP 400: the seat sent temperature, which Opus 4.7 and later refuse, and the L3 status classifier dropped the provider's message after looking in it for the word schema. A seat's temperature may now be null and is then sent by no builder; an anthropic seat may name an effort (low, medium, high), sent as output_config.effort and part of the request hash only when set, so no unchanged seat's reads go stale; no builder sends a thinking field. The anthropic reply is the first text block, past any thinking block, and no text block is an empty read; stop_reason stays the finish reason and max_tokens is truncated. A failed HTTP call's detail is HTTP <status>: <provider message>, taken from error.message and scrubbed of URLs, paths, emails and keys; a 400 naming the schema or output_config is schema_rejected, any other 400 the new code invalid_request. seats.example.json is now Opus 5.5 (no temperature, medium effort, 16000 tokens) and Gemini 3.8 Flash, and no longer tracks config/models.json. Not deployed, not a skill; skills[] unchanged. 28 offline tests added, 387 pass. Related to seq 3174; closes none.","2.8.0":"The conclave learns to abstain (L3): read-gated assessment, three-state routing, quorum, content-bound reuse, evidence with location, bundle. A live run on a blank copy showed a seat whose extraction had failed still being sent an assessment and awarding six items it invented; the plan now consults each seat's own read, and only an ok read that says response_present is assessed. A read that is not ok is no vote on any item. Extractions may carry read_state: no_response_visible is a deny carrying verify_blank, which always sends the item to a TA, so two blank reads are never a unanimous zero; unreadable_or_uncertain is no vote and flags the question unreadable; an absent read_state (spec v5 or older) means response_present, with one warning per run; any other value is schema_invalid. One cast vote is TA look (single_vote). An M item's uncertain check vetoes its assessment, and a high-stakes item with any uncertain check is a TA look. Every read and assessment records the sha256 of its PDF, and both request hashes cover it with the seat's model, temperature and token limit; a copy replaced at the same path is re-read, two copies with the same bytes are one refusal (duplicate_content), and scoring_hash is no longer compared, so a checks.py edit re-bills nothing, a one-question items edit re-bills that question and a rulings edit re-bills all. The assessment schema asks for evidence_location and evidence_observed per item, and an award missing either is downgraded to uncertain (evidence_missing). Failures carry a provider-neutral code and a detail scrubbed of URLs, paths, emails and keys; every attempt records its finish reason and truncation, counted per seat; refusal details, worker errors and the printed plan carry no path and no traceback. The sheets folder is the bundle: manifest.json (release_id, engine version, hashes, seats, questions, refusals, digests), one <question>.json per question, and a director-only contact.html of pages 2-11. The new request hash makes every earlier read stale: the first L3 run re-reads and re-assesses everything. Not deployed, not a skill; skills[] unchanged. 64 offline tests added, 359 pass. Related to seq 3174; closes none.","2.7.0":"The conclave learns to score (L2). It turns L1's key-blind reads into what five TAs use on exam day: a per-item recommendation for every copy, one review sheet per question, and one command. Four new commands beside run: python -m conclave score (version detection, deterministic checks from the spec's checks.py and triage, with zero provider calls), assess (one assessment read per copy × question × seat covering that question's M and J items, holding a runner-owned frame, rulings.md, the items' rubric text, the detected version's key and the pages, and never another seat's output or the seat's own extraction), sheets (a self-contained HTML page per question with inline CSS and JS, lazy-loaded page JPEGs, a CSV, cases.html, and one for_marc.html) and grade (run, score, assess and sheets in order, resumable at every stage). The scoring files (items.json, key.json, rulings.md, checks.py, cases.json) load through their own function; the extraction loader still cannot see them, and that guarantee keeps its test. A scoring_hash over items, key, rulings and checks invalidates scoring and assessment and never an extraction read. The reuse rule is tightened: a stored read or assessment is reused only if its model equals the seat's current model and its request_hash matches (for a read: prompt_hash, the preamble and the effective DPI); reads L1 wrote carry none and are stale. Votes are per rubric item and per seat, extractions are never merged, and triage is a pure function in which a split vote has no path to a recommendation, on a high-stakes item or any other: unanimous award or deny, otherwise TA look, and advisory items always TA decides. A version conflict scores nothing and is listed for Marc; other_version_match is recorded and shown only on Marc's page. With --identify, and only in the sheet writer, the local part of each copy's :submitters[0][:email] becomes the copy key on the sheets and nowhere else: not in the run folder, a checkpoint, a log line, recommendations.json or a request. Nothing is deployed or registered: skills[] is unchanged and the live quiz pipeline behaves exactly as before. 123 offline tests added; 295 pass. Advances the first definition-of-done criterion of seq 3174; closes none.","2.6.0":"The conclave arrives: a local paper-exam engine. conclave/ is a new library in this repository with a command-line caller (python -m conclave run), built for the 200-copy paper midterm of 2026-09-29. It is not deployed and not registered as a skill — skills[] is unchanged, because the conclave is a local library rather than a capability Lucie offers over A2A, and nothing in lucie_agent.py, main.py or skills/ imports it or names it. batch_analyze, verify_grading and the live quiz pipeline are untouched and behave exactly as before. A read-only recon on 2026-09-19 found batch_analyze unable to carry a paper exam: synchronous inside one request with blocking SDK calls, no checkpoint, the solution in every vision call, two hard-wired seats, JSON pulled out with a greedy regex so that a parse failure came back as success, and no page-1 guard. Each is answered here by a rule rather than by care. Nothing about any particular exam is hard-coded: an exam is a spec folder outside this repository holding pages.json and, per question, an extraction prompt and a draft-07 schema. Anything else that folder holds — items.json, key.json, a rubric — is neither read nor hashed, and the loader hands out no path into it, so there is no code path by which a key, a rubric or a reference image could enter an extraction request. Page 1 is refused in code twice, in the spec loader and again in the rasterizer for any caller at all, because it is the Gradescope cover sheet and carries the student's name. Identity: the submissions folder is listed by *.pdf suffix only, so the submission_metadata.yml Gradescope ships full of student names is never opened; each copy gets an opaque c- token, stable across runs through token_map.json in the submissions folder; checkpoints, logs, summaries and exception messages carry tokens only; requests carry page images and prompt text and nothing else; and the CLI refuses a submissions or run folder inside this repository, including the gitignored output/, because .gitignore protects the history and not the disk. Seats are data: three async providers (anthropic, gemini, openai_compat), each asking for structured output in its own SDK's spelling read off the installed package, every response validated locally against the question's schema regardless of what the provider promised, SDK-level retries set to zero because the runner owns retries, and a missing key variable fatal before the first call with the variable named and no value echoed. A read is either ok with a schema-valid extraction or failed with one of six kinds (provider_error, timeout, malformed_json, schema_invalid, empty, refused); parsing is a strict json.loads with no regex salvage, and a failed read is never an extraction and never a vote. The runner is async over (copy x question x seat) with a semaphore per seat, exponential backoff with jitter, exactly one extra attempt for malformed output, a per-seat circuit breaker that halts a seat after N consecutive failures while the others carry on, and an atomically written checkpoint per read; resume compares the per-question prompt_hash, so editing one question's prompt re-runs that question and leaves every other question resumable. A copy whose page count differs from the spec's page_count is refused with zero reads and counted in the summary. jsonschema is added to conclave/requirements.txt and installed into the local env only — the root requirements.txt is unchanged, because the deployed server needs none of it. 144 offline tests added across six test_conclave_*.py files and conclave_fixtures.py, with no network, no key and synthetic PDFs; 172 pass (python -m unittest test_a2a_client test_report_prompts test_conclave_spec test_conclave_pages test_conclave_seats test_conclave_identity test_conclave_runner test_conclave_cli). Scoring, rubric judgement and TA sheets are the next brief; the vocabularies they will use (Outcome, Flag) are already fixed in conclave/types.py. Eleven version sites and all three changelogs reconciled to 2.6.0. Advances the first definition-of-done criterion of work item seq 3174; closes none","2.5.1":"No student name reaches a report prompt. All four report templates in core/report_generator.py opened their context block with STUDENT: {student_name}, so every report call sent the name to the model provider; the row is removed from each. No template asked the model to use the name, and analyze_submission and batch_analyze stitch student_name into their payloads from their own locals, so stored reports, response shapes and the generate_report signature are unchanged. test_report_prompts.py pins the templates and the three live prompt paths. No skill added, removed or renamed. Closes no work item","2.5.0":"Agent URLs come from the environment, and nothing is defaulted. utils/a2a_client.py carried a hardcoded fallback for all four peers and every one was wrong: the production defaults named *.onrender.com hosts dead since the Hetzner/Coolify migration (the fleet answers on *.xmmersia.com), and the local Gaston default said port 8011 while Gaston has always served 8020. Lucie reached her peers only because the environment set every variable, so the defaults were never exercised and their being lies never showed. Following the house rule (Le Veilleur v5.84.0), GASTON_URL, LUMIERE_URL, LE_VEILLEUR_URL and LE_MARTEAU_URL are now read with no production fallback; an unset or blank one logs one ERROR line naming the variable and that agent is skipped, and a call to it raises AgentNotConfiguredError naming the variable rather than reaching a ghost. Unknown-agent remains a separate ValueError. The localhost defaults stay for local development but become an opt-in behind LUCIE_LOCAL_AGENTS and their ports were re-read from each target repo (Gaston 8020, Lumiere 8014, Le Veilleur 8023, Le Marteau 8022) — the old production/local test keyed on RENDER or PRODUCTION could not carry them, because Coolify sets neither and would have taken the local branch on the deployed box. verify_connections now walks all four known agents rather than only the configured ones, so a missing variable reports not_configured instead of vanishing from the report. 23 tests added (test_a2a_client.py). No skill, capability, or vision pipeline touched. Closes no work item","2.4.4":"Repo image moves to the self-hosted runner. Every repo-image trigger had failed since 2026-08-22 — not a bug: seven repos beating hourly against a 2,000-minute Actions free tier project to roughly 5,000 min/mo, and a deliberate $0 Actions budget pauses runs rather than billing them, so runs-on: ubuntu-latest was asking for a machine the cap withholds. The job now runs on the self-hosted xmmersia-image runner on the Hetzner box, whose minutes are not billed; the label must match exactly, because a mismatched label does not fail the job, it queues it forever. timeout-minutes: 10 is added in the same edit because one runner serves this repo serially and a hung job would block every run behind it. The three triggers, the steps, REPO_IMAGE_DB_URL and tools/repo_image_indexer.py are untouched; no skill, capability or vision path is involved.","2.4.3":"Repo-image pipeline arrives — a fleet clone of the Portal's. A GitHub Actions workflow captures this repository's structure (file tree with hashes, extracted surfaces, per-push change ledger, hourly heartbeat on minute :05) into Supabase, so Claude can see how stale a project-knowledge snapshot is instead of guessing. No file contents are stored. The surface extractors are Lucie's own: nine version sites, all three changelogs, the module docstring headers, the README headings, and a card-versus-registered-skills audit that follows this repo's actual by-hand registration in lucie_agent.py rather than the filesystem discovery the card advertises. The card's skill examples[] and tags[] are deliberately not extracted — they carry computing_id and student_name placeholders, and the pipeline's PII guard keeps identifier-shaped text out of the system database. No skill, capability, or vision pipeline touched","2.4.2":"Batch completion_check results now carry status_label and private_assessment, so Marc's private audit records reach Le Veilleur instead of being silently dropped — the batch assessment payload is field-for-field the analyze_submission payload; verify_grading's fuzzy PDF match now skips empty identifiers, which previously matched the first PDF in the index and audited another student's work; graded_by unified on 'lucie' in the batch witness path (was 'ai')","2.4.1":"Agent card corrected — name and URL. Fleet-wide card-truth campaign after the 2026-07-21 Architecture audit: name was 'LUCIE' and is now 'Lucie' (the acronym expansion keeps its capitals where spelled out); url was the dead onrender.com host and is now https://lucie.xmmersia.com, verified live before writing. The health_check description and identity essence were title-cased. A repo-wide grep across this repo and the Portal confirmed nothing compares against 'LUCIE' before the rename. No skill, capability, or vision pipeline touched","2.4.0":"Batch witness-feedback wiring (three-layer inputs in batch_analyze); inbound auth on /api/analyze and /api/verify; smart page loading in batch_analyze and verify_grading; Layer 1 (guidance.md) self-loaded when caller omits it","2.0.1":"Fix health_check timeout: remove outbound HTTP, add background polling loop","2.0.0":"Merged student feedback + TA verification into unified agent with three skills","1.0.0":"Restructured as proper Xmmersia A2A agent with dual vision","0.1.0":"Original standalone CLI script (lucie.py, lucie_complex.py)"}}}